diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 5ebbf211..799b8003 100755
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,4 +1,12 @@
+Drupal 7.58, 2018-03-28
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-002.
+
+Drupal 7.57, 2018-02-21
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-001.
+
Drupal 7.56, 2017-06-21
-----------------------
- Fixed security issues (access bypass). See SA-CORE-2017-003.
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index c06055ed..06acf935 100755
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -8,7 +8,7 @@
/**
* The current system version.
*/
-define('VERSION', '7.56');
+define('VERSION', '7.58');
/**
* Core API compatibility.
@@ -2632,6 +2632,10 @@ function _drupal_bootstrap_configuration() {
timer_start('page');
// Initialize the configuration, including variables from settings.php.
drupal_settings_initialize();
+
+ // Sanitize unsafe keys from the request.
+ require_once DRUPAL_ROOT . '/includes/request-sanitizer.inc';
+ DrupalRequestSanitizer::sanitize();
}
/**
diff --git a/includes/common.inc b/includes/common.inc
index a32930a5..d7dc47f2 100755
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2236,8 +2236,11 @@ function url($path = NULL, array $options = array()) {
'prefix' => ''
);
+ // Determine whether this is an external link, but ensure that the current
+ // path is always treated as internal by default (to prevent external link
+ // injection vulnerabilities).
if (!isset($options['external'])) {
- $options['external'] = url_is_external($path);
+ $options['external'] = $path === $_GET['q'] ? FALSE : url_is_external($path);
}
// Preserve the original path before altering or aliasing.
diff --git a/includes/request-sanitizer.inc b/includes/request-sanitizer.inc
new file mode 100644
index 00000000..1daa6b53
--- /dev/null
+++ b/includes/request-sanitizer.inc
@@ -0,0 +1,82 @@
+ implode(', ', $get_sanitized_keys))), E_USER_NOTICE);
+ }
+
+ // Process request body parameters.
+ $post_sanitized_keys = array();
+ $_POST = self::stripDangerousValues($_POST, $whitelist, $post_sanitized_keys);
+ if ($log_sanitized_keys && $post_sanitized_keys) {
+ _drupal_trigger_error_with_delayed_logging(format_string('Potentially unsafe keys removed from request body parameters (POST): @keys', array('@keys' => implode(', ', $post_sanitized_keys))), E_USER_NOTICE);
+ }
+
+ // Process cookie parameters.
+ $cookie_sanitized_keys = array();
+ $_COOKIE = self::stripDangerousValues($_COOKIE, $whitelist, $cookie_sanitized_keys);
+ if ($log_sanitized_keys && $cookie_sanitized_keys) {
+ _drupal_trigger_error_with_delayed_logging(format_string('Potentially unsafe keys removed from cookie parameters (COOKIE): @keys', array('@keys' => implode(', ', $cookie_sanitized_keys))), E_USER_NOTICE);
+ }
+
+ $request_sanitized_keys = array();
+ $_REQUEST = self::stripDangerousValues($_REQUEST, $whitelist, $request_sanitized_keys);
+
+ self::$sanitized = TRUE;
+ }
+ }
+
+ /**
+ * Strips dangerous keys from the provided input.
+ *
+ * @param mixed $input
+ * The input to sanitize.
+ * @param string[] $whitelist
+ * An array of keys to whitelist as safe.
+ * @param string[] $sanitized_keys
+ * An array of keys that have been removed.
+ *
+ * @return mixed
+ * The sanitized input.
+ */
+ protected static function stripDangerousValues($input, array $whitelist, array &$sanitized_keys) {
+ if (is_array($input)) {
+ foreach ($input as $key => $value) {
+ if ($key !== '' && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) {
+ unset($input[$key]);
+ $sanitized_keys[] = $key;
+ }
+ else {
+ $input[$key] = self::stripDangerousValues($input[$key], $whitelist, $sanitized_keys);
+ }
+ }
+ }
+ return $input;
+ }
+
+}
diff --git a/misc/drupal.js b/misc/drupal.js
index d86ea1fa..19fbc712 100755
--- a/misc/drupal.js
+++ b/misc/drupal.js
@@ -27,6 +27,42 @@ $.fn.init = function (selector, context, rootjQuery) {
};
$.fn.init.prototype = jquery_init.prototype;
+/**
+ * Pre-filter Ajax requests to guard against XSS attacks.
+ *
+ * See https://github.com/jquery/jquery/issues/2432
+ */
+if ($.ajaxPrefilter) {
+ // For newer versions of jQuery, use an Ajax prefilter to prevent
+ // auto-executing script tags from untrusted domains. This is similar to the
+ // fix that is built in to jQuery 3.0 and higher.
+ $.ajaxPrefilter(function (s) {
+ if (s.crossDomain) {
+ s.contents.script = false;
+ }
+ });
+}
+else if ($.httpData) {
+ // For the version of jQuery that ships with Drupal core, override
+ // jQuery.httpData to prevent auto-detecting "script" data types from
+ // untrusted domains.
+ var jquery_httpData = $.httpData;
+ $.httpData = function (xhr, type, s) {
+ // @todo Consider backporting code from newer jQuery versions to check for
+ // a cross-domain request here, rather than using Drupal.urlIsLocal() to
+ // block scripts from all URLs that are not on the same site.
+ if (!type && !Drupal.urlIsLocal(s.url)) {
+ var content_type = xhr.getResponseHeader('content-type') || '';
+ if (content_type.indexOf('javascript') >= 0) {
+ // Default to a safe data type.
+ type = 'text';
+ }
+ }
+ return jquery_httpData.call(this, xhr, type, s);
+ };
+ $.httpData.prototype = jquery_httpData.prototype;
+}
+
/**
* Attach all registered behaviors to a page element.
*
@@ -137,7 +173,7 @@ Drupal.detachBehaviors = function (context, settings, trigger) {
*/
Drupal.checkPlain = function (str) {
var character, regex,
- replace = { '&': '&', '"': '"', '<': '<', '>': '>' };
+ replace = { '&': '&', "'": ''', '"': '"', '<': '<', '>': '>' };
str = String(str);
for (character in replace) {
if (replace.hasOwnProperty(character)) {
diff --git a/modules/aggregator/aggregator.info b/modules/aggregator/aggregator.info
index 09caa008..e8645d1f 100755
--- a/modules/aggregator/aggregator.info
+++ b/modules/aggregator/aggregator.info
@@ -7,8 +7,8 @@ files[] = aggregator.test
configure = admin/config/services/aggregator/settings
stylesheets[all][] = aggregator.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/aggregator/tests/aggregator_test.info b/modules/aggregator/tests/aggregator_test.info
index 575be566..e6d5a0a7 100755
--- a/modules/aggregator/tests/aggregator_test.info
+++ b/modules/aggregator/tests/aggregator_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/block/block.info b/modules/block/block.info
index a0ff83f0..934bfdea 100755
--- a/modules/block/block.info
+++ b/modules/block/block.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = block.test
configure = admin/structure/block
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/block/tests/block_test.info b/modules/block/tests/block_test.info
index beff5962..d7e22203 100755
--- a/modules/block/tests/block_test.info
+++ b/modules/block/tests/block_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/block/tests/themes/block_test_theme/block_test_theme.info b/modules/block/tests/themes/block_test_theme/block_test_theme.info
index 6e7b9c96..31b43824 100755
--- a/modules/block/tests/themes/block_test_theme/block_test_theme.info
+++ b/modules/block/tests/themes/block_test_theme/block_test_theme.info
@@ -13,8 +13,8 @@ regions[footer] = Footer
regions[highlighted] = Highlighted
regions[help] = Help
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/blog/blog.info b/modules/blog/blog.info
index d241eca2..84ad946d 100755
--- a/modules/blog/blog.info
+++ b/modules/blog/blog.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = blog.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/book/book.info b/modules/book/book.info
index 164043df..53e454bf 100755
--- a/modules/book/book.info
+++ b/modules/book/book.info
@@ -7,8 +7,8 @@ files[] = book.test
configure = admin/content/book/settings
stylesheets[all][] = book.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/color/color.info b/modules/color/color.info
index 086f8cf1..0d17ce11 100755
--- a/modules/color/color.info
+++ b/modules/color/color.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = color.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/comment/comment.info b/modules/comment/comment.info
index 3dbf6e60..5d5abbf4 100755
--- a/modules/comment/comment.info
+++ b/modules/comment/comment.info
@@ -9,8 +9,8 @@ files[] = comment.test
configure = admin/content/comment
stylesheets[all][] = comment.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/contact/contact.info b/modules/contact/contact.info
index 466e9417..b365daf9 100755
--- a/modules/contact/contact.info
+++ b/modules/contact/contact.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = contact.test
configure = admin/structure/contact
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/contextual/contextual.info b/modules/contextual/contextual.info
index fd73edd5..ba47a4cb 100755
--- a/modules/contextual/contextual.info
+++ b/modules/contextual/contextual.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = contextual.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/dashboard/dashboard.info b/modules/dashboard/dashboard.info
index bc9c98a9..d6e98660 100755
--- a/modules/dashboard/dashboard.info
+++ b/modules/dashboard/dashboard.info
@@ -7,8 +7,8 @@ files[] = dashboard.test
dependencies[] = block
configure = admin/dashboard/customize
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/dblog/dblog.info b/modules/dblog/dblog.info
index dc39510e..cb85f811 100755
--- a/modules/dblog/dblog.info
+++ b/modules/dblog/dblog.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = dblog.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/field.info b/modules/field/field.info
index 241c2971..e05108ec 100755
--- a/modules/field/field.info
+++ b/modules/field/field.info
@@ -11,8 +11,8 @@ dependencies[] = field_sql_storage
required = TRUE
stylesheets[all][] = theme/field.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.info b/modules/field/modules/field_sql_storage/field_sql_storage.info
index 48881e26..05434613 100755
--- a/modules/field/modules/field_sql_storage/field_sql_storage.info
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.info
@@ -7,8 +7,8 @@ dependencies[] = field
files[] = field_sql_storage.test
required = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/modules/list/list.info b/modules/field/modules/list/list.info
index e7427bcc..bef7e4a2 100755
--- a/modules/field/modules/list/list.info
+++ b/modules/field/modules/list/list.info
@@ -7,8 +7,8 @@ dependencies[] = field
dependencies[] = options
files[] = tests/list.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/modules/list/tests/list_test.info b/modules/field/modules/list/tests/list_test.info
index ce3ca4c9..2d4d6cce 100755
--- a/modules/field/modules/list/tests/list_test.info
+++ b/modules/field/modules/list/tests/list_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/modules/number/number.info b/modules/field/modules/number/number.info
index 39a33641..c65d94ae 100755
--- a/modules/field/modules/number/number.info
+++ b/modules/field/modules/number/number.info
@@ -6,8 +6,8 @@ core = 7.x
dependencies[] = field
files[] = number.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/modules/options/options.info b/modules/field/modules/options/options.info
index bb212833..632ca24f 100755
--- a/modules/field/modules/options/options.info
+++ b/modules/field/modules/options/options.info
@@ -6,8 +6,8 @@ core = 7.x
dependencies[] = field
files[] = options.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/modules/text/text.info b/modules/field/modules/text/text.info
index 86dcdc77..fe93a35a 100755
--- a/modules/field/modules/text/text.info
+++ b/modules/field/modules/text/text.info
@@ -7,8 +7,8 @@ dependencies[] = field
files[] = text.test
required = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field/tests/field_test.info b/modules/field/tests/field_test.info
index 54ea80f3..ce3c7251 100755
--- a/modules/field/tests/field_test.info
+++ b/modules/field/tests/field_test.info
@@ -6,8 +6,8 @@ files[] = field_test.entity.inc
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/field_ui/field_ui.info b/modules/field_ui/field_ui.info
index c46a4bf7..a415ae99 100755
--- a/modules/field_ui/field_ui.info
+++ b/modules/field_ui/field_ui.info
@@ -6,8 +6,8 @@ core = 7.x
dependencies[] = field
files[] = field_ui.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/file/file.info b/modules/file/file.info
index 722e3a7f..7269b4ad 100755
--- a/modules/file/file.info
+++ b/modules/file/file.info
@@ -6,8 +6,8 @@ core = 7.x
dependencies[] = field
files[] = tests/file.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/file/file.module b/modules/file/file.module
index fb5e9b94..1e98f11b 100755
--- a/modules/file/file.module
+++ b/modules/file/file.module
@@ -140,7 +140,7 @@ function file_file_download($uri, $field_type = 'file') {
}
// Find out which (if any) fields of this type contain the file.
- $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type);
+ $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type, FALSE);
// Stop processing if there are no references in order to avoid returning
// headers for files controlled by other modules. Make an exception for
@@ -1067,11 +1067,18 @@ function file_icon_map($file) {
* @param $field_type
* (optional) The name of a field type. If given, limits the reference check
* to fields of the given type.
+ * @param $check_access
+ * (optional) A boolean that specifies whether the permissions of the current
+ * user should be checked when retrieving references. If FALSE, all
+ * references to the file are returned. If TRUE, only references from
+ * entities that the current user has access to are returned. Defaults to
+ * TRUE for backwards compatibility reasons, but FALSE is recommended for
+ * most situations.
*
* @return
* An integer value.
*/
-function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') {
+function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file', $check_access = TRUE) {
$references = drupal_static(__FUNCTION__, array());
$fields = isset($field) ? array($field['field_name'] => $field) : field_info_fields();
@@ -1082,6 +1089,11 @@ function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISI
$query
->fieldCondition($file_field, 'fid', $file->fid)
->age($age);
+ if (!$check_access) {
+ // Neutralize the 'entity_field_access' query tag added by
+ // field_sql_storage_field_storage_query().
+ $query->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');
+ }
$references[$field_name] = $query->execute();
}
}
diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test
index b3a1424d..f764a903 100755
--- a/modules/file/tests/file.test
+++ b/modules/file/tests/file.test
@@ -1626,6 +1626,79 @@ class FilePrivateTestCase extends FileFieldTestCase {
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that another anonymous user cannot access the permanent file when it is referenced by an unpublished node.');
}
+
+ /**
+ * Tests file access for private nodes when file download access is granted.
+ */
+ function testPrivateFileDownloadAccessGranted() {
+ // Tell file_module_test to attempt to grant access to all private files,
+ // and ensure that it is doing so correctly.
+ $test_file = $this->getTestFile('text');
+ $uri = file_unmanaged_move($test_file->uri, 'private://');
+ $file_url = file_create_url($uri);
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Access is not granted to an arbitrary private file by default.');
+ variable_set('file_module_test_grant_download_access', TRUE);
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Access is granted to an arbitrary private file after a module grants access to all private files in hook_file_download().');
+
+ // Create a public node with a file attached.
+ $type_name = 'page';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name, array('uri_scheme' => 'private'));
+ $test_file = $this->getTestFile('text');
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => FALSE));
+ $node = node_load($nid, NULL, TRUE);
+ $file_url = file_create_url($node->{$field_name}[LANGUAGE_NONE][0]['uri']);
+
+ // Unpublish the node and ensure that only administrators (not anonymous
+ // users) can access the node and download the file; the expectation is
+ // that the File module's hook_file_download() implementation will deny
+ // access and thereby override the file_module_test module's access grant.
+ $node->status = NODE_NOT_PUBLISHED;
+ node_save($node);
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet("node/$nid");
+ $this->assertResponse(200, 'Administrator can access the unpublished node.');
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Administrator can download the file attached to the unpublished node.');
+ $this->drupalLogOut();
+ $this->drupalGet("node/$nid");
+ $this->assertResponse(403, 'Anonymous user cannot access the unpublished node.');
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Anonymous user cannot download the file attached to the unpublished node.');
+
+ // Re-publish the node and ensure that the node and file can be accessed by
+ // everyone.
+ $node->status = NODE_PUBLISHED;
+ node_save($node);
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet("node/$nid");
+ $this->assertResponse(200, 'Administrator can access the published node.');
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Administrator can download the file attached to the published node.');
+ $this->drupalLogOut();
+ $this->drupalGet("node/$nid");
+ $this->assertResponse(200, 'Anonymous user can access the published node.');
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Anonymous user can download the file attached to the published node.');
+
+ // Make the node private via the node access system and test that only
+ // administrators (not anonymous users) can access the node and download
+ // the file.
+ $node->private = TRUE;
+ node_save($node);
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet("node/$nid");
+ $this->assertResponse(200, 'Administrator can access the private node.');
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Administrator can download the file attached to the private node.');
+ $this->drupalLogOut();
+ $this->drupalGet("node/$nid");
+ $this->assertResponse(403, 'Anonymous user cannot access the private node.');
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Anonymous user cannot download the file attached to the private node.');
+ }
}
/**
diff --git a/modules/file/tests/file_module_test.info b/modules/file/tests/file_module_test.info
index 47f4abce..958bf660 100755
--- a/modules/file/tests/file_module_test.info
+++ b/modules/file/tests/file_module_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/file/tests/file_module_test.module b/modules/file/tests/file_module_test.module
index f66c749d..1acfacab 100755
--- a/modules/file/tests/file_module_test.module
+++ b/modules/file/tests/file_module_test.module
@@ -67,3 +67,18 @@ function file_module_test_form_submit($form, &$form_state) {
}
drupal_set_message(t('The file id is %fid.', array('%fid' => $fid)));
}
+
+/**
+ * Implements hook_file_download().
+ */
+function file_module_test_file_download($uri) {
+ if (variable_get('file_module_test_grant_download_access')) {
+ // Mimic what file_get_content_headers() would do if we had a full $file
+ // object to pass to it.
+ return array(
+ 'Content-Type' => mime_header_encode(file_get_mimetype($uri)),
+ 'Content-Length' => filesize($uri),
+ 'Cache-Control' => 'private',
+ );
+ }
+}
diff --git a/modules/filter/filter.info b/modules/filter/filter.info
index 71af77d4..4a46c85c 100755
--- a/modules/filter/filter.info
+++ b/modules/filter/filter.info
@@ -7,8 +7,8 @@ files[] = filter.test
required = TRUE
configure = admin/config/content/formats
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/forum/forum.info b/modules/forum/forum.info
index 031ffd25..d5b1c769 100755
--- a/modules/forum/forum.info
+++ b/modules/forum/forum.info
@@ -9,8 +9,8 @@ files[] = forum.test
configure = admin/structure/forum
stylesheets[all][] = forum.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/help/help.info b/modules/help/help.info
index 2e825571..2c851ea9 100755
--- a/modules/help/help.info
+++ b/modules/help/help.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = help.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/image/image.info b/modules/image/image.info
index 57988719..dd1200c6 100755
--- a/modules/image/image.info
+++ b/modules/image/image.info
@@ -7,8 +7,8 @@ dependencies[] = file
files[] = image.test
configure = admin/config/media/image-styles
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/image/tests/image_module_test.info b/modules/image/tests/image_module_test.info
index 6139caaf..c6b3a6ab 100755
--- a/modules/image/tests/image_module_test.info
+++ b/modules/image/tests/image_module_test.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = image_module_test.module
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/locale/locale.info b/modules/locale/locale.info
index 0d3585b3..674d1f56 100755
--- a/modules/locale/locale.info
+++ b/modules/locale/locale.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = locale.test
configure = admin/config/regional/language
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/locale/tests/locale_test.info b/modules/locale/tests/locale_test.info
index b957dfc5..49da5e51 100755
--- a/modules/locale/tests/locale_test.info
+++ b/modules/locale/tests/locale_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/menu/menu.info b/modules/menu/menu.info
index 72ed9bbe..9b7ef57b 100755
--- a/modules/menu/menu.info
+++ b/modules/menu/menu.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = menu.test
configure = admin/structure/menu
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/node/node.info b/modules/node/node.info
index 98b85321..11ae2cd3 100755
--- a/modules/node/node.info
+++ b/modules/node/node.info
@@ -9,8 +9,8 @@ required = TRUE
configure = admin/structure/types
stylesheets[all][] = node.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/node/tests/node_access_test.info b/modules/node/tests/node_access_test.info
index 66125d4d..00792901 100755
--- a/modules/node/tests/node_access_test.info
+++ b/modules/node/tests/node_access_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/node/tests/node_test.info b/modules/node/tests/node_test.info
index 17820690..f56e2e51 100755
--- a/modules/node/tests/node_test.info
+++ b/modules/node/tests/node_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/node/tests/node_test_exception.info b/modules/node/tests/node_test_exception.info
index 3588e373..a4c11859 100755
--- a/modules/node/tests/node_test_exception.info
+++ b/modules/node/tests/node_test_exception.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/openid/openid.info b/modules/openid/openid.info
index 67a1cd04..69d26632 100755
--- a/modules/openid/openid.info
+++ b/modules/openid/openid.info
@@ -5,8 +5,8 @@ package = Core
core = 7.x
files[] = openid.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/openid/tests/openid_test.info b/modules/openid/tests/openid_test.info
index 6caed214..7186198b 100755
--- a/modules/openid/tests/openid_test.info
+++ b/modules/openid/tests/openid_test.info
@@ -6,8 +6,8 @@ core = 7.x
dependencies[] = openid
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/overlay/overlay.info b/modules/overlay/overlay.info
index fb576ce8..1bf7e9ef 100755
--- a/modules/overlay/overlay.info
+++ b/modules/overlay/overlay.info
@@ -4,8 +4,8 @@ package = Core
version = VERSION
core = 7.x
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/path/path.info b/modules/path/path.info
index f9b50f74..b5b0eb9b 100755
--- a/modules/path/path.info
+++ b/modules/path/path.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = path.test
configure = admin/config/search/path
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/php/php.info b/modules/php/php.info
index eb82a497..236f9310 100755
--- a/modules/php/php.info
+++ b/modules/php/php.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = php.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/poll/poll.info b/modules/poll/poll.info
index f981ce86..eeed31d5 100755
--- a/modules/poll/poll.info
+++ b/modules/poll/poll.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = poll.test
stylesheets[all][] = poll.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/profile/profile.info b/modules/profile/profile.info
index d6ee35a7..1480c618 100755
--- a/modules/profile/profile.info
+++ b/modules/profile/profile.info
@@ -11,8 +11,8 @@ configure = admin/config/people/profile
; See user_system_info_alter().
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/rdf/rdf.info b/modules/rdf/rdf.info
index c7271c2c..18e62970 100755
--- a/modules/rdf/rdf.info
+++ b/modules/rdf/rdf.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
files[] = rdf.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/rdf/tests/rdf_test.info b/modules/rdf/tests/rdf_test.info
index 90aacd10..a302a7b5 100755
--- a/modules/rdf/tests/rdf_test.info
+++ b/modules/rdf/tests/rdf_test.info
@@ -6,8 +6,8 @@ core = 7.x
hidden = TRUE
dependencies[] = blog
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/search/search.info b/modules/search/search.info
index c66fcfdd..248f476c 100755
--- a/modules/search/search.info
+++ b/modules/search/search.info
@@ -8,8 +8,8 @@ files[] = search.test
configure = admin/config/search/settings
stylesheets[all][] = search.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/search/tests/search_embedded_form.info b/modules/search/tests/search_embedded_form.info
index acf8f528..7e1b7367 100755
--- a/modules/search/tests/search_embedded_form.info
+++ b/modules/search/tests/search_embedded_form.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/search/tests/search_extra_type.info b/modules/search/tests/search_extra_type.info
index ac996e56..534edade 100755
--- a/modules/search/tests/search_extra_type.info
+++ b/modules/search/tests/search_extra_type.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/search/tests/search_node_tags.info b/modules/search/tests/search_node_tags.info
index 2df10960..16438984 100644
--- a/modules/search/tests/search_node_tags.info
+++ b/modules/search/tests/search_node_tags.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/shortcut/shortcut.info b/modules/shortcut/shortcut.info
index f4956cf0..c490374e 100755
--- a/modules/shortcut/shortcut.info
+++ b/modules/shortcut/shortcut.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = shortcut.test
configure = admin/config/user-interface/shortcut
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info
index 7d1747a0..26b3485a 100755
--- a/modules/simpletest/simpletest.info
+++ b/modules/simpletest/simpletest.info
@@ -57,8 +57,8 @@ files[] = tests/upgrade/update.trigger.test
files[] = tests/upgrade/update.field.test
files[] = tests/upgrade/update.user.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/actions_loop_test.info b/modules/simpletest/tests/actions_loop_test.info
index c3c44697..2edf253d 100755
--- a/modules/simpletest/tests/actions_loop_test.info
+++ b/modules/simpletest/tests/actions_loop_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/ajax_forms_test.info b/modules/simpletest/tests/ajax_forms_test.info
index 3d8d13f9..9736647e 100755
--- a/modules/simpletest/tests/ajax_forms_test.info
+++ b/modules/simpletest/tests/ajax_forms_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/ajax_test.info b/modules/simpletest/tests/ajax_test.info
index a2dd2b10..fe2f90b0 100755
--- a/modules/simpletest/tests/ajax_test.info
+++ b/modules/simpletest/tests/ajax_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/batch_test.info b/modules/simpletest/tests/batch_test.info
index 42a87d30..a53e2159 100755
--- a/modules/simpletest/tests/batch_test.info
+++ b/modules/simpletest/tests/batch_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/boot_test_1.info b/modules/simpletest/tests/boot_test_1.info
index ed218005..873825da 100644
--- a/modules/simpletest/tests/boot_test_1.info
+++ b/modules/simpletest/tests/boot_test_1.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/boot_test_2.info b/modules/simpletest/tests/boot_test_2.info
index f2164d52..e8529ba9 100644
--- a/modules/simpletest/tests/boot_test_2.info
+++ b/modules/simpletest/tests/boot_test_2.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index 83161fad..0490dd53 100755
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -76,7 +76,7 @@ class DrupalAlterTestCase extends DrupalWebTestCase {
class CommonURLUnitTest extends DrupalWebTestCase {
public static function getInfo() {
return array(
- 'name' => 'URL generation tests',
+ 'name' => 'URL generation unit tests',
'description' => 'Confirm that url(), drupal_get_query_parameters(), drupal_http_build_query(), and l() work correctly with various input.',
'group' => 'System',
);
@@ -372,6 +372,38 @@ class CommonURLUnitTest extends DrupalWebTestCase {
}
}
+/**
+ * Web tests for URL generation functions.
+ */
+class CommonURLWebTest extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'URL generation web tests',
+ 'description' => 'Confirm that URL-generating functions work correctly on specific site paths.',
+ 'group' => 'System',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('common_test');
+ }
+
+ /**
+ * Tests the url() function on internal paths which mimic external URLs.
+ */
+ function testInternalPathMimicsExternal() {
+ // Ensure that calling url(current_path()) on "/http://example.com" (an
+ // internal path which mimics an external URL) always links to the internal
+ // path, not the external URL. This helps protect against external URL link
+ // injection vulnerabilities.
+ variable_set('common_test_link_to_current_path', TRUE);
+ $this->drupalGet('/http://example.com');
+ $this->clickLink('link which should point to the current path');
+ $this->assertUrl('/http://example.com');
+ $this->assertText('link which should point to the current path');
+ }
+}
+
/**
* Tests url_is_external().
*/
diff --git a/modules/simpletest/tests/common_test.info b/modules/simpletest/tests/common_test.info
index 6087d7b8..8c56f80c 100755
--- a/modules/simpletest/tests/common_test.info
+++ b/modules/simpletest/tests/common_test.info
@@ -7,8 +7,8 @@ stylesheets[all][] = common_test.css
stylesheets[print][] = common_test.print.css
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/common_test.module b/modules/simpletest/tests/common_test.module
index 2eb8cd5d..d092c924 100755
--- a/modules/simpletest/tests/common_test.module
+++ b/modules/simpletest/tests/common_test.module
@@ -99,6 +99,9 @@ function common_test_init() {
if (variable_get('common_test_redirect_current_path', FALSE)) {
drupal_goto(current_path());
}
+ if (variable_get('common_test_link_to_current_path', FALSE)) {
+ drupal_set_message(l('link which should point to the current path', current_path()));
+ }
}
/**
diff --git a/modules/simpletest/tests/common_test_cron_helper.info b/modules/simpletest/tests/common_test_cron_helper.info
index 6e6d36ce..bf8a7290 100755
--- a/modules/simpletest/tests/common_test_cron_helper.info
+++ b/modules/simpletest/tests/common_test_cron_helper.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/database_test.info b/modules/simpletest/tests/database_test.info
index 46a5da8a..3ff31eda 100755
--- a/modules/simpletest/tests/database_test.info
+++ b/modules/simpletest/tests/database_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info
index 1895e95f..b9715351 100644
--- a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info
+++ b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info
@@ -7,8 +7,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info b/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info
index 1fce8044..f918fdc2 100755
--- a/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info
+++ b/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info b/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
index 8acc0cd4..9edaf9f6 100755
--- a/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
+++ b/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/entity_cache_test.info b/modules/simpletest/tests/entity_cache_test.info
index b9f1c703..212ce833 100755
--- a/modules/simpletest/tests/entity_cache_test.info
+++ b/modules/simpletest/tests/entity_cache_test.info
@@ -6,8 +6,8 @@ core = 7.x
dependencies[] = entity_cache_test_dependency
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/entity_cache_test_dependency.info b/modules/simpletest/tests/entity_cache_test_dependency.info
index 4f5f45d8..58049876 100755
--- a/modules/simpletest/tests/entity_cache_test_dependency.info
+++ b/modules/simpletest/tests/entity_cache_test_dependency.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/entity_crud_hook_test.info b/modules/simpletest/tests/entity_crud_hook_test.info
index 320b3c91..9f5f3ead 100755
--- a/modules/simpletest/tests/entity_crud_hook_test.info
+++ b/modules/simpletest/tests/entity_crud_hook_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/entity_query_access_test.info b/modules/simpletest/tests/entity_query_access_test.info
index 94da93ed..328e5d9c 100755
--- a/modules/simpletest/tests/entity_query_access_test.info
+++ b/modules/simpletest/tests/entity_query_access_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/error_test.info b/modules/simpletest/tests/error_test.info
index 3f06c833..bf6e044b 100755
--- a/modules/simpletest/tests/error_test.info
+++ b/modules/simpletest/tests/error_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/file_test.info b/modules/simpletest/tests/file_test.info
index 57cabc21..3ceb3ebe 100755
--- a/modules/simpletest/tests/file_test.info
+++ b/modules/simpletest/tests/file_test.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = file_test.module
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/filter_test.info b/modules/simpletest/tests/filter_test.info
index b4853f87..96c6c4b5 100755
--- a/modules/simpletest/tests/filter_test.info
+++ b/modules/simpletest/tests/filter_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/form_test.info b/modules/simpletest/tests/form_test.info
index f3c910ec..7706be2b 100755
--- a/modules/simpletest/tests/form_test.info
+++ b/modules/simpletest/tests/form_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/image_test.info b/modules/simpletest/tests/image_test.info
index 0c86fb8d..82d2a3ba 100755
--- a/modules/simpletest/tests/image_test.info
+++ b/modules/simpletest/tests/image_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/menu_test.info b/modules/simpletest/tests/menu_test.info
index e5301a78..26d70dc6 100755
--- a/modules/simpletest/tests/menu_test.info
+++ b/modules/simpletest/tests/menu_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/module_test.info b/modules/simpletest/tests/module_test.info
index 5aab1282..1ebdd859 100755
--- a/modules/simpletest/tests/module_test.info
+++ b/modules/simpletest/tests/module_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/path_test.info b/modules/simpletest/tests/path_test.info
index 61ba3525..43ecd96d 100755
--- a/modules/simpletest/tests/path_test.info
+++ b/modules/simpletest/tests/path_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/psr_0_test/psr_0_test.info b/modules/simpletest/tests/psr_0_test/psr_0_test.info
index 3658d3ce..ca69f5c2 100755
--- a/modules/simpletest/tests/psr_0_test/psr_0_test.info
+++ b/modules/simpletest/tests/psr_0_test/psr_0_test.info
@@ -5,8 +5,8 @@ core = 7.x
hidden = TRUE
package = Testing
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/psr_4_test/psr_4_test.info b/modules/simpletest/tests/psr_4_test/psr_4_test.info
index 6a66365f..75e5b0cd 100644
--- a/modules/simpletest/tests/psr_4_test/psr_4_test.info
+++ b/modules/simpletest/tests/psr_4_test/psr_4_test.info
@@ -5,8 +5,8 @@ core = 7.x
hidden = TRUE
package = Testing
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/requirements1_test.info b/modules/simpletest/tests/requirements1_test.info
index 1fba9cac..c5b3a606 100755
--- a/modules/simpletest/tests/requirements1_test.info
+++ b/modules/simpletest/tests/requirements1_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/requirements2_test.info b/modules/simpletest/tests/requirements2_test.info
index 1a992fd0..b12197e8 100755
--- a/modules/simpletest/tests/requirements2_test.info
+++ b/modules/simpletest/tests/requirements2_test.info
@@ -7,8 +7,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/session_test.info b/modules/simpletest/tests/session_test.info
index a89aa897..4987deae 100755
--- a/modules/simpletest/tests/session_test.info
+++ b/modules/simpletest/tests/session_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_dependencies_test.info b/modules/simpletest/tests/system_dependencies_test.info
index a714682d..4b03f292 100755
--- a/modules/simpletest/tests/system_dependencies_test.info
+++ b/modules/simpletest/tests/system_dependencies_test.info
@@ -6,8 +6,8 @@ core = 7.x
hidden = TRUE
dependencies[] = _missing_dependency
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info b/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info
index a7cd470c..cb2749e3 100755
--- a/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info
+++ b/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info
@@ -6,8 +6,8 @@ core = 7.x
hidden = TRUE
dependencies[] = system_incompatible_core_version_test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_incompatible_core_version_test.info b/modules/simpletest/tests/system_incompatible_core_version_test.info
index e01a7ac7..338521f6 100755
--- a/modules/simpletest/tests/system_incompatible_core_version_test.info
+++ b/modules/simpletest/tests/system_incompatible_core_version_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 5.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info b/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info
index 2b23dd50..edf6dd12 100755
--- a/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info
+++ b/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info
@@ -7,8 +7,8 @@ hidden = TRUE
; system_incompatible_module_version_test declares version 1.0
dependencies[] = system_incompatible_module_version_test (>2.0)
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_incompatible_module_version_test.info b/modules/simpletest/tests/system_incompatible_module_version_test.info
index 89ba10b3..51f3efc5 100755
--- a/modules/simpletest/tests/system_incompatible_module_version_test.info
+++ b/modules/simpletest/tests/system_incompatible_module_version_test.info
@@ -5,8 +5,8 @@ version = 1.0
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_project_namespace_test.info b/modules/simpletest/tests/system_project_namespace_test.info
index 63f1cabc..1507739b 100644
--- a/modules/simpletest/tests/system_project_namespace_test.info
+++ b/modules/simpletest/tests/system_project_namespace_test.info
@@ -6,8 +6,8 @@ core = 7.x
hidden = TRUE
dependencies[] = drupal:filter
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/system_test.info b/modules/simpletest/tests/system_test.info
index adbea26a..4b6175b5 100755
--- a/modules/simpletest/tests/system_test.info
+++ b/modules/simpletest/tests/system_test.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = system_test.module
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/taxonomy_test.info b/modules/simpletest/tests/taxonomy_test.info
index 1ca57175..3ccf2299 100755
--- a/modules/simpletest/tests/taxonomy_test.info
+++ b/modules/simpletest/tests/taxonomy_test.info
@@ -6,8 +6,8 @@ core = 7.x
hidden = TRUE
dependencies[] = taxonomy
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/theme_test.info b/modules/simpletest/tests/theme_test.info
index 58a10a73..f5cb1eaf 100755
--- a/modules/simpletest/tests/theme_test.info
+++ b/modules/simpletest/tests/theme_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info b/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info
index 508de704..ed247b7e 100755
--- a/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info
+++ b/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info
@@ -6,8 +6,8 @@ hidden = TRUE
settings[basetheme_only] = base theme value
settings[subtheme_override] = base theme value
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info b/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info
index 0e378310..dfd8bd8a 100755
--- a/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info
+++ b/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info
@@ -6,8 +6,8 @@ hidden = TRUE
settings[subtheme_override] = subtheme value
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/themes/test_theme/test_theme.info b/modules/simpletest/tests/themes/test_theme/test_theme.info
index edb2b8b8..c132c910 100755
--- a/modules/simpletest/tests/themes/test_theme/test_theme.info
+++ b/modules/simpletest/tests/themes/test_theme/test_theme.info
@@ -17,8 +17,8 @@ stylesheets[all][] = system.base.css
settings[theme_test_setting] = default value
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info b/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info
index af1fd912..a4f5649e 100644
--- a/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info
+++ b/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info
@@ -4,8 +4,8 @@ core = 7.x
hidden = TRUE
engine = nyan_cat
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/update_script_test.info b/modules/simpletest/tests/update_script_test.info
index 321b2c3d..f3bfc50b 100755
--- a/modules/simpletest/tests/update_script_test.info
+++ b/modules/simpletest/tests/update_script_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/update_test_1.info b/modules/simpletest/tests/update_test_1.info
index b8b69569..a7431d5d 100755
--- a/modules/simpletest/tests/update_test_1.info
+++ b/modules/simpletest/tests/update_test_1.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/update_test_2.info b/modules/simpletest/tests/update_test_2.info
index b8b69569..a7431d5d 100755
--- a/modules/simpletest/tests/update_test_2.info
+++ b/modules/simpletest/tests/update_test_2.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/update_test_3.info b/modules/simpletest/tests/update_test_3.info
index b8b69569..a7431d5d 100755
--- a/modules/simpletest/tests/update_test_3.info
+++ b/modules/simpletest/tests/update_test_3.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/url_alter_test.info b/modules/simpletest/tests/url_alter_test.info
index 5f969dff..23676af5 100755
--- a/modules/simpletest/tests/url_alter_test.info
+++ b/modules/simpletest/tests/url_alter_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/simpletest/tests/xmlrpc_test.info b/modules/simpletest/tests/xmlrpc_test.info
index c7d3ca44..ab787cc3 100755
--- a/modules/simpletest/tests/xmlrpc_test.info
+++ b/modules/simpletest/tests/xmlrpc_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/statistics/statistics.info b/modules/statistics/statistics.info
index 9147bc48..26c1794e 100755
--- a/modules/statistics/statistics.info
+++ b/modules/statistics/statistics.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = statistics.test
configure = admin/config/system/statistics
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/syslog/syslog.info b/modules/syslog/syslog.info
index 6d00c449..3f8b6ca4 100755
--- a/modules/syslog/syslog.info
+++ b/modules/syslog/syslog.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = syslog.test
configure = admin/config/development/logging
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/system/system.info b/modules/system/system.info
index f765e17d..da08a99e 100755
--- a/modules/system/system.info
+++ b/modules/system/system.info
@@ -12,8 +12,8 @@ files[] = system.test
required = TRUE
configure = admin/config/system
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/system/tests/cron_queue_test.info b/modules/system/tests/cron_queue_test.info
index 8d694daf..86d9e089 100644
--- a/modules/system/tests/cron_queue_test.info
+++ b/modules/system/tests/cron_queue_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/system/tests/system_cron_test.info b/modules/system/tests/system_cron_test.info
index a4986beb..662c7c45 100644
--- a/modules/system/tests/system_cron_test.info
+++ b/modules/system/tests/system_cron_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/taxonomy/taxonomy.info b/modules/taxonomy/taxonomy.info
index dc9aa777..1f2fec80 100755
--- a/modules/taxonomy/taxonomy.info
+++ b/modules/taxonomy/taxonomy.info
@@ -8,8 +8,8 @@ files[] = taxonomy.module
files[] = taxonomy.test
configure = admin/structure/taxonomy
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/toolbar/toolbar.info b/modules/toolbar/toolbar.info
index 31278e86..f0257612 100755
--- a/modules/toolbar/toolbar.info
+++ b/modules/toolbar/toolbar.info
@@ -4,8 +4,8 @@ core = 7.x
package = Core
version = VERSION
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/tracker/tracker.info b/modules/tracker/tracker.info
index fd5ec6fa..d7cb61b6 100755
--- a/modules/tracker/tracker.info
+++ b/modules/tracker/tracker.info
@@ -6,8 +6,8 @@ version = VERSION
core = 7.x
files[] = tracker.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/translation/tests/translation_test.info b/modules/translation/tests/translation_test.info
index 4dcb554c..9b597a0e 100755
--- a/modules/translation/tests/translation_test.info
+++ b/modules/translation/tests/translation_test.info
@@ -5,8 +5,8 @@ package = Testing
version = VERSION
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/translation/translation.info b/modules/translation/translation.info
index c75554a6..3e6513f0 100755
--- a/modules/translation/translation.info
+++ b/modules/translation/translation.info
@@ -6,8 +6,8 @@ version = VERSION
core = 7.x
files[] = translation.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/trigger/tests/trigger_test.info b/modules/trigger/tests/trigger_test.info
index 599bf6ff..13e0698d 100755
--- a/modules/trigger/tests/trigger_test.info
+++ b/modules/trigger/tests/trigger_test.info
@@ -4,8 +4,8 @@ package = Testing
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/trigger/trigger.info b/modules/trigger/trigger.info
index 1b12e9a4..4c21ba8f 100755
--- a/modules/trigger/trigger.info
+++ b/modules/trigger/trigger.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = trigger.test
configure = admin/structure/trigger
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/aaa_update_test.info b/modules/update/tests/aaa_update_test.info
index 746224b1..96ac28aa 100755
--- a/modules/update/tests/aaa_update_test.info
+++ b/modules/update/tests/aaa_update_test.info
@@ -4,8 +4,8 @@ package = Testing
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/bbb_update_test.info b/modules/update/tests/bbb_update_test.info
index da5444c5..954fb75b 100755
--- a/modules/update/tests/bbb_update_test.info
+++ b/modules/update/tests/bbb_update_test.info
@@ -4,8 +4,8 @@ package = Testing
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/ccc_update_test.info b/modules/update/tests/ccc_update_test.info
index 8e77f3f7..f3b7d043 100755
--- a/modules/update/tests/ccc_update_test.info
+++ b/modules/update/tests/ccc_update_test.info
@@ -4,8 +4,8 @@ package = Testing
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info b/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info
index f1bab4f8..d21dd9ca 100644
--- a/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info
+++ b/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info
@@ -3,8 +3,8 @@ description = Test theme which is used as admin theme.
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info b/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info
index 19bd9c45..07eb50ff 100755
--- a/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info
+++ b/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info
@@ -3,8 +3,8 @@ description = Test theme which acts as a base theme for other test subthemes.
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info b/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info
index 2edb6410..26b480c5 100755
--- a/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info
+++ b/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info
@@ -4,8 +4,8 @@ core = 7.x
base theme = update_test_basetheme
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/tests/update_test.info b/modules/update/tests/update_test.info
index 9271156c..7b42bc2e 100755
--- a/modules/update/tests/update_test.info
+++ b/modules/update/tests/update_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/update/update.info b/modules/update/update.info
index 109ed2b8..4d9c3691 100755
--- a/modules/update/update.info
+++ b/modules/update/update.info
@@ -6,8 +6,8 @@ core = 7.x
files[] = update.test
configure = admin/reports/updates/settings
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/user/tests/user_form_test.info b/modules/user/tests/user_form_test.info
index 52b2068f..17ce34a5 100755
--- a/modules/user/tests/user_form_test.info
+++ b/modules/user/tests/user_form_test.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/user/user.info b/modules/user/user.info
index 1e318c24..83e2b8e3 100755
--- a/modules/user/user.info
+++ b/modules/user/user.info
@@ -9,8 +9,8 @@ required = TRUE
configure = admin/config/people
stylesheets[all][] = user.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/modules/user/user.module b/modules/user/user.module
index cfcd10ba..12ca2800 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -2359,26 +2359,14 @@ function user_external_login_register($name, $module) {
* following properties:
* - uid: The user ID number.
* - login: The UNIX timestamp of the user's last login.
- * @param array $options
- * (optional) A keyed array of settings. Supported options are:
- * - langcode: A language code to be used when generating locale-sensitive
- * urls. If langcode is NULL the users preferred language is used.
*
* @return
* A unique URL that provides a one-time log in for the user, from which
* they can change their password.
*/
-function user_pass_reset_url($account, $options = array()) {
+function user_pass_reset_url($account) {
$timestamp = REQUEST_TIME;
- $url_options = array('absolute' => TRUE);
- if (isset($options['langcode'])) {
- $languages = language_list();
- $url_options['language'] = $languages[$options['langcode']];
- }
- else {
- $url_options['language'] = user_preferred_language($account);
- }
- return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), $url_options);
+ return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE));
}
/**
@@ -2390,10 +2378,6 @@ function user_pass_reset_url($account, $options = array()) {
* - uid: The user ID number.
* - pass: The hashed user password string.
* - login: The UNIX timestamp of the user's last login.
- * @param array $options
- * (optional) A keyed array of settings. Supported options are:
- * - langcode: A language code to be used when generating locale-sensitive
- * urls. If langcode is NULL the users preferred language is used.
*
* @return
* A unique URL that may be used to confirm the cancellation of the user
@@ -2402,17 +2386,9 @@ function user_pass_reset_url($account, $options = array()) {
* @see user_mail_tokens()
* @see user_cancel_confirm()
*/
-function user_cancel_url($account, $options = array()) {
+function user_cancel_url($account) {
$timestamp = REQUEST_TIME;
- $url_options = array('absolute' => TRUE);
- if (isset($options['langcode'])) {
- $languages = language_list();
- $url_options['language'] = $languages[$options['langcode']];
- }
- else {
- $url_options['language'] = user_preferred_language($account);
- }
- return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), $url_options);
+ return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE));
}
/**
@@ -2902,7 +2878,7 @@ Your account on [site:name] has been canceled.
if ($replace) {
// We do not sanitize the token replacement, since the output of this
// replacement is intended for an e-mail message, not a web browser.
- return token_replace($text, $variables, array('language' => $language, 'langcode' => $langcode, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
+ return token_replace($text, $variables, array('language' => $language, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
}
return $text;
@@ -2929,8 +2905,8 @@ Your account on [site:name] has been canceled.
*/
function user_mail_tokens(&$replacements, $data, $options) {
if (isset($data['user'])) {
- $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options);
- $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options);
+ $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user']);
+ $replacements['[user:cancel-url]'] = user_cancel_url($data['user']);
}
}
diff --git a/modules/user/user.test b/modules/user/user.test
index f26cf260..0875e0ac 100644
--- a/modules/user/user.test
+++ b/modules/user/user.test
@@ -2320,26 +2320,6 @@ class UserTokenReplaceTestCase extends DrupalWebTestCase {
);
}
- public function setUp() {
- parent::setUp('locale');
-
- $account = $this->drupalCreateUser(array('access administration pages', 'administer languages'));
- $this->drupalLogin($account);
-
- // Add language.
- $edit = array('langcode' => 'de');
- $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
-
- // Enable URL language detection and selection.
- $edit = array('language[enabled][locale-url]' => 1);
- $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
-
- // Reset static caching.
- drupal_static_reset('language_list');
- drupal_static_reset('locale_url_outbound_alter');
- drupal_static_reset('locale_language_url_rewrite_url');
- }
-
/**
* Creates a user, then tests the tokens generated from it.
*/
@@ -2390,39 +2370,6 @@ class UserTokenReplaceTestCase extends DrupalWebTestCase {
$output = token_replace($input, array('user' => $account), array('language' => $language, 'sanitize' => FALSE));
$this->assertEqual($output, $expected, format_string('Unsanitized user token %token replaced.', array('%token' => $input)));
}
-
- $languages = language_list();
-
- // Generate login and cancel link.
- $tests = array();
- $tests['[user:one-time-login-url]'] = user_pass_reset_url($account);
- $tests['[user:cancel-url]'] = user_cancel_url($account);
-
- // Generate tokens with interface language.
- $link = url('user', array('absolute' => TRUE));
- foreach ($tests as $input => $expected) {
- $output = token_replace($input, array('user' => $account), array('langcode' => $language->language, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
- $this->assertTrue(strpos($output, $link) === 0, 'Generated URL is in interface language.');
- }
-
- // Generate tokens with the user's preferred language.
- $edit['language'] = 'de';
- $account = user_save($account, $edit);
- $link = url('user', array('language' => $languages[$account->language], 'absolute' => TRUE));
- foreach ($tests as $input => $expected) {
- $output = token_replace($input, array('user' => $account), array('callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
- $this->assertTrue(strpos($output, $link) === 0, "Generated URL is in the user's preferred language.");
- }
-
- // Generate tokens with one specific language.
- $link = url('user', array('language' => $languages['de'], 'absolute' => TRUE));
- foreach ($tests as $input => $expected) {
- foreach (array($user1, $user2) as $account) {
- $output = token_replace($input, array('user' => $account), array('langcode' => 'de', 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
- $this->assertTrue(strpos($output, $link) === 0, "Generated URL in in the requested language.");
- }
- }
-
}
}
diff --git a/profiles/minimal/minimal.info b/profiles/minimal/minimal.info
index 99535ae8..1b363abd 100755
--- a/profiles/minimal/minimal.info
+++ b/profiles/minimal/minimal.info
@@ -5,8 +5,8 @@ core = 7.x
dependencies[] = block
dependencies[] = dblog
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/profiles/standard/standard.info b/profiles/standard/standard.info
index d687764d..a3fd9e29 100755
--- a/profiles/standard/standard.info
+++ b/profiles/standard/standard.info
@@ -24,8 +24,8 @@ dependencies[] = field_ui
dependencies[] = file
dependencies[] = rdf
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info b/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info
index d7db04ed..7a11e329 100755
--- a/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info
+++ b/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info
@@ -6,8 +6,8 @@ core = 7.x
hidden = TRUE
files[] = drupal_system_listing_compatible_test.test
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
index eb63b745..d2bd9479 100755
--- a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
+++ b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
@@ -8,8 +8,8 @@ version = VERSION
core = 6.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/profiles/testing/testing.info b/profiles/testing/testing.info
index 81282d98..e9fec6dc 100755
--- a/profiles/testing/testing.info
+++ b/profiles/testing/testing.info
@@ -4,8 +4,8 @@ version = VERSION
core = 7.x
hidden = TRUE
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/sites/all/modules/contrib/admin/adb/LICENSE.txt b/sites/all/modules/contrib/admin/adb/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/admin/adb/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/admin/adb/adb.info b/sites/all/modules/contrib/admin/adb/adb.info
new file mode 100644
index 00000000..a7f3a367
--- /dev/null
+++ b/sites/all/modules/contrib/admin/adb/adb.info
@@ -0,0 +1,12 @@
+name = Access denied backtrace
+description = Enable backtrace for Access Denied to get details about why was triggered
+package = Development
+core = 7.x
+configure = admin/config/development/access-denied-backtrace/configure
+
+; Information added by drupal.org packaging script on 2013-07-14
+version = "7.x-1.6"
+core = "7.x"
+project = "adb"
+datestamp = "1373817378"
+
diff --git a/sites/all/modules/contrib/admin/adb/adb.install b/sites/all/modules/contrib/admin/adb/adb.install
new file mode 100644
index 00000000..179596c4
--- /dev/null
+++ b/sites/all/modules/contrib/admin/adb/adb.install
@@ -0,0 +1,83 @@
+ 'Table that contains logs of all system events.',
+ 'fields' => array(
+ 'adbid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique access deneid backtrace event ID.',
+ ),
+ 'uid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {users}.uid of the user who triggered the event.',
+ ),
+ 'location' => array(
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'description' => 'URL of the origin of the event.',
+ ),
+ 'node_access_denied' => array(
+ 'type' => 'text',
+ 'not null' => FALSE,
+ 'size' => 'medium',
+ 'description' => 'User permissions.',
+ ),
+ 'permissions' => array(
+ 'type' => 'text',
+ 'not null' => FALSE,
+ 'size' => 'medium',
+ 'description' => 'User permissions.',
+ ),
+ 'role_permissions' => array(
+ 'type' => 'text',
+ 'not null' => FALSE,
+ 'size' => 'medium',
+ 'description' => 'role permissions.',
+ ),
+ 'backtrace' => array(
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'description' => 'Text of backtrace log execution.',
+ ),
+ 'timestamp' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Unix timestamp of when event occurred.',
+ ),
+ ),
+'primary key' => array('adbid'),
+'indexes' => array(
+ 'uid' => array('uid'),
+ ),
+);
+
+return $schema;
+}
+
+/**
+ * Add field permissions in table adb.
+ */
+function adb_update_7150() {
+
+ db_add_field('adb', 'permissions', array('type' => 'text', 'not null' => FALSE, 'size' => 'medium'));
+ return t('Added permissions column to adb table.');
+}
+
+/**
+ * Add fields role_permissiones and node_access_denied in table adb.
+ */
+function adb_update_7160() {
+
+ db_add_field('adb', 'role_permissions', array('type' => 'text', 'not null' => FALSE, 'size' => 'medium'));
+ db_add_field('adb', 'node_access_denied', array('type' => 'text', 'not null' => FALSE, 'size' => 'medium'));
+ return t('Added role_permissions and node_access_denied columns to adb table.');
+}
diff --git a/sites/all/modules/contrib/admin/adb/adb.module b/sites/all/modules/contrib/admin/adb/adb.module
new file mode 100644
index 00000000..9598c289
--- /dev/null
+++ b/sites/all/modules/contrib/admin/adb/adb.module
@@ -0,0 +1,430 @@
+ "Last access denied errors backtrace",
+ 'description' => "View 'access denied' errors backtrace (403s).",
+ 'page callback' => 'adb_last',
+ 'access arguments' => array('access site reports'),
+ );
+
+ $items['admin/reports/event/backtrace/%'] = array(
+ 'title' => 'Details',
+ 'page callback' => 'adb_event',
+ 'page arguments' => array(4),
+ 'access arguments' => array('access site reports'),
+ );
+
+ $items['admin/config/development/access-denied-backtrace/configure'] = array(
+ 'title' => 'Access denied backtrace settings',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('adb_settings'),
+ 'access arguments' => array('administer access denied backtrace'),
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function adb_permission() {
+
+ return array(
+ 'administer access denied backtrace' => array(
+ 'title' => t('administer access denied backtrace'),
+ 'description' => t('Change which roles are enabled to record access denied backtrace using the admin interface.'),
+ ),
+ );
+
+} // fontyourface_perm
+
+/**
+ * Generate a adb settings form.
+ *
+ * @ingroup forms
+ * @see filter_admin_format_form_validate()
+ * @see filter_admin_format_form_submit()
+ */
+function adb_settings($form, &$form_state) {
+
+ // Add user role access selection.
+ $form['adb_roles'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Roles'),
+ '#options' => array_map('check_plain', user_roles()),
+ '#default_value' => variable_get('adb_roles',array()),
+ '#description' => t('Configure what roles will be able to record access denied backtrace'),
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Menu callback; displays details about access denied backtrace log message.
+ */
+function adb_event($id) {
+ $severity = watchdog_severity_levels();
+ $result = db_query('SELECT adb.*, u.name, u.uid FROM {adb} adb INNER JOIN {users} u ON adb.uid = u.uid WHERE adb.adbid = :id', array(':id' => $id))->fetchObject();
+
+ if ($dblog = $result) {
+ $rows = array(
+ array(
+ array('data' => t('Date'), 'header' => TRUE),
+ format_date($dblog->timestamp, 'long'),
+ ),
+ array(
+ array('data' => t('User'), 'header' => TRUE),
+ theme('username', array('account' => $dblog)),
+ ),
+ array(
+ array('data' => t('Location'), 'header' => TRUE),
+ l($dblog->location, $dblog->location),
+ ),
+ array(
+ array('data' => t('User permissions'), 'header' => TRUE),
+ $dblog->permissions,
+ ),
+ array(
+ array('data' => t('User role permissions'), 'header' => TRUE),
+ $dblog->role_permissions,
+ ),
+ array(
+ array('data' => t('Node access denied'), 'header' => TRUE),
+ $dblog->node_access_denied,
+ ),
+ array(
+ array('data' => t('Backtrace'), 'header' => TRUE),
+ theme('adb_message', array('event' => $dblog)),
+ ),
+ );
+ $build['dblog_table'] = array(
+ '#theme' => 'table',
+ '#rows' => $rows,
+ '#attributes' => array('class' => array('dblog-event')),
+ );
+ return $build;
+ }
+ else {
+ return '';
+ }
+}
+/**
+ * Implements hook_theme().
+ */
+function adb_theme() {
+ return array(
+ 'adb_message' => array(
+ 'variables' => array('event' => NULL),
+ ),
+ );
+}
+
+/**
+ * Returns HTML for a log message.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - event: An object with at least the message and variables properties.
+ * - link: (optional) Format message as link, event->wid is required.
+ *
+ * @ingroup themeable
+ */
+function theme_adb_message($variables) {
+ $event = $variables['event'];
+ $link = (isset($variables['link']))?$variables['link']:FALSE;
+
+ $output = $event->backtrace;
+ // Truncate message to 56 chars.
+
+ if($link) {
+ $output = truncate_utf8(filter_xss($output, array()), 56, TRUE, TRUE);
+ $output = l($output, 'admin/reports/event/backtrace/' . $event->adbid, array('html' => TRUE));
+ }
+
+ return $output;
+}
+
+/**
+ * Menu callback; generic function to display a page of the last access denied
+ * backtrace.
+ *
+ * Messages are not truncated because events from this page have no detail view.
+ *
+ */
+function adb_last() {
+ $rows = array();
+
+ $build['dblog_clear_log_form'] = drupal_get_form('adb_clear_log_form');
+
+ $header = array(
+ array('data' => t('Date'), 'field' => 'adb.adbid', 'sort' => 'desc'),
+ t('Backtrace'),
+ array('data' => t('User'), 'field' => 'u.name'),
+ );
+
+ $query = db_select('adb', 'adb')->extend('PagerDefault')->extend('TableSort');
+ $query->leftJoin('users', 'u', 'adb.uid = u.uid');
+ $query
+ ->fields('adb', array('adbid', 'uid', 'timestamp', 'backtrace'))
+ ->addField('u', 'name');
+
+ $result = $query
+ ->limit(50)
+ ->orderByHeader($header)
+ ->execute();
+
+ foreach ($result as $dblog) {
+ $rows[] = array('data' =>
+ array(
+ // Cells
+ format_date($dblog->timestamp, 'short'),
+ theme('adb_message', array('event' => $dblog, 'link' => TRUE)),
+ theme('username', array('account' => $dblog)),
+ ),
+ // Attributes for tr
+ 'class' => array(drupal_html_class('adb')),
+ );
+ }
+
+ $build['dblog_table'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#attributes' => array('id' => 'admin-dblog'),
+ '#empty' => t('No access denied bractrace log available.'),
+ );
+ $build['dblog_pager'] = array('#theme' => 'pager');
+
+ return $build;
+}
+
+/**
+ * Invokes a hook in all enabled modules that implement it.
+ *
+ * @param $hook
+ * The name of the hook to invoke.
+ * @param ...
+ * Arguments to pass to the hook.
+ *
+ * @return
+ * An array of return values of the hook implementations. If modules return
+ * arrays from their implementations, those are merged into one array.
+ */
+function adb_validate_module_access($node, $op, $account) {
+ $args = array($node, $op, $account);
+
+ $hook = 'node_access';
+ $return = array();
+ foreach (module_implements($hook) as $module) {
+ $function = $module . '_' . $hook;
+ if (function_exists($function)) {
+ $result = call_user_func_array($function, $args);
+ if (isset($result) && is_array($result)) {
+ foreach ($result as $subaccess) {
+ if ($subaccess == NODE_ACCESS_DENY) {
+ $return[] = $module;
+ break;
+ }
+ }
+ $return = array_merge_recursive($return, $result);
+ } elseif (isset($result) && $result == NODE_ACCESS_DENY) {
+ $return[] = $module;
+ }
+ }
+ }
+
+ return t('Modules denying') . " " . implode(',', $return);
+}
+
+/**
+ * Implements hook_watchdog().
+ *
+ * If an 'access denied' error is logged and user role is enabled for debug, the
+ * execution trace is stored to try to fix what is wrong.
+ */
+function adb_watchdog($log_entry) {
+ $account = user_load($log_entry['uid']);
+
+ $adb_roles = array_filter(variable_get('adb_roles',array()));
+ $valid_role = array_intersect_key($adb_roles,$log_entry['user']->roles);
+
+ $account_rights = '';
+ $rights = drupal_static('node_access', array());
+
+ $permissions_denied = '';
+ if (isset($rights[$log_entry['uid']])) {
+ $account_rights = $rights[$log_entry['uid']];
+
+ foreach ($account_rights as $node => $rights) {
+ foreach ($rights as $action => $access) {
+ if (!$access) {
+ $permissions_denied .= $action . ' ' . $node . ". ";
+ $permissions_denied .= adb_validate_module_access($node, $action, $log_entry['user']) . ". ";
+ }
+ }
+ }
+ }
+ else {
+ // Common message for annonymous users.
+ $permissions_denied = t('There are not explicit access denied for this user');
+ }
+
+ $user_access = drupal_static('user_access', array());
+ $role_permissions = user_role_permissions($account->roles);
+
+ //Validate if entry is access denied and current user belog to enabled role to
+ //record backtrace
+ if (!empty($valid_role) && $log_entry['type'] == 'access denied' ) {
+
+ $backtrace = _ddebug_backtrace(TRUE);
+ // Pop the stack up to the drupal_access_denied() call.
+ for ($i = 0; $i < 5; ++$i) {
+ array_shift($backtrace);
+ }
+
+ $user_role_permissions = (isset($role_permissions[$log_entry['uid']])?$role_permissions[$log_entry['uid']]:array());
+ $user_access_permissions = (isset($user_access[$log_entry['uid']]))?$user_access[$log_entry['uid']]:array();
+
+
+ /* print_r($user_access[$log_entry['uid']]);
+ print_r($user_role_permissions);*/
+ Database::getConnection('default', 'default')->insert('adb')
+ ->fields(array(
+ 'uid' => $log_entry['uid'],
+ 'node_access_denied' => $permissions_denied,
+ 'permissions' => _dprint_r($user_access_permissions, TRUE),
+ 'role_permissions' => _dprint_r($user_role_permissions, TRUE),
+ 'backtrace' => _dprint_r($backtrace, TRUE),
+ 'location' => $log_entry['request_uri'],
+ 'timestamp' => $log_entry['timestamp'],
+ ))
+ ->execute();
+ }
+}
+
+/**
+ * Return form for dblog clear button.
+ *
+ * @ingroup forms
+ * @see dblog_clear_log_submit()
+ */
+function adb_clear_log_form($form) {
+ $form['adb_clear'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Clear access denied backtraces'),
+ '#description' => t('This will permanently remove the access denied backtraces from the database.'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+ $form['adb_clear']['clear'] = array(
+ '#type' => 'submit',
+ '#value' => t('Clear access denied backtraces'),
+ '#submit' => array('adb_clear_log_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback: clear database with log messages.
+ */
+function adb_clear_log_submit() {
+ db_delete('adb')->execute();
+ drupal_set_message(t('Database access denied backtrace cleared.'));
+}
+
+/**
+ * Pretty-print a variable to the browser (no krumo).
+ * Displays only for users with proper permissions. If
+ * you want a string returned instead of a print, use the 2nd param.
+ * based in devel function dprint_r
+ */
+function _dprint_r($input, $return = FALSE, $name = NULL, $function = 'print_r', $check = TRUE) {
+ if ($name) {
+ $name .= ' => ';
+ }
+ if ($function == 'drupal_var_export') {
+ include_once DRUPAL_ROOT . '/includes/utility.inc';
+ $output = drupal_var_export($input);
+ } else {
+ ob_start();
+ $function($input);
+ $output = ob_get_clean();
+ }
+
+ if ($check) {
+ $output = check_plain($output);
+ }
+ if (count($input, COUNT_RECURSIVE) > ADB_DEVEL_MIN_TEXTAREA) {
+ // don't use fapi here because sometimes fapi will not be loaded
+ $printed_value = "';
+ } else {
+ $printed_value = '
' . $name . $output . '
';
+ }
+
+ if ($return) {
+ return $printed_value;
+ } else {
+ print $printed_value;
+ }
+}
+
+/**
+ * Print the function call stack.
+ * copied from devel module but with access for all roles
+ */
+function _ddebug_backtrace($return = FALSE, $pop = 0) {
+
+ $backtrace = debug_backtrace();
+ while ($pop-- > 0) {
+ array_shift($backtrace);
+ }
+ $counter = count($backtrace);
+ $path = $backtrace[$counter - 1]['file'];
+ $path = substr($path, 0, strlen($path) - 10);
+ $paths[$path] = strlen($path) + 1;
+ $paths[DRUPAL_ROOT] = strlen(DRUPAL_ROOT) + 1;
+ $nbsp = "\xC2\xA0";
+
+ // Show message if error_level is ERROR_REPORTING_DISPLAY_SOME or higher.
+ // (This is Drupal's error_level, which is different from $error_level,
+ // and we purposely ignore the difference between _SOME and _ALL,
+ // see #970688!)
+ if (variable_get('error_level', 1) >= 1) {
+ while (!empty($backtrace)) {
+ $call = array();
+ if (isset($backtrace[0]['file'])) {
+ $call['file'] = $backtrace[0]['file'];
+ foreach ($paths as $path => $len) {
+ if (strpos($backtrace[0]['file'], $path) === 0) {
+ $call['file'] = substr($backtrace[0]['file'], $len);
+ }
+ }
+ $call['file'] .= ':' . $backtrace[0]['line'];
+ }
+ if (isset($backtrace[1])) {
+ if (isset($backtrace[1]['class'])) {
+ $function = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
+ } else {
+ $function = $backtrace[1]['function'] . '()';
+ }
+ $call['args'] = $backtrace[1]['args'];
+ } else {
+ $function = 'main()';
+ $call['args'] = $_GET;
+ }
+ $nicetrace[($counter <= 10 ? $nbsp : '') . --$counter . ': ' . $function] = $call;
+ array_shift($backtrace);
+ }
+ if ($return) {
+ return $nicetrace;
+ }
+ kprint_r($nicetrace);
+ }
+}
diff --git a/sites/all/modules/contrib/admin/modules_weight/LICENSE.txt b/sites/all/modules/contrib/admin/modules_weight/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/admin/modules_weight/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/admin/modules_weight/README.txt b/sites/all/modules/contrib/admin/modules_weight/README.txt
new file mode 100644
index 00000000..b1e3740e
--- /dev/null
+++ b/sites/all/modules/contrib/admin/modules_weight/README.txt
@@ -0,0 +1,9 @@
+This module provide admin interface for users/admins has the access to modules
+page to reorder the module weights as they want.
+
+INSTALLATION :
+1. download the module and uncompresse it to sites/all/module and enable it
+2. go to admin/config/system/modules-weight and reorder the modules weight :)
+
+This module just display non-core module, that's because displaying core module in the configuration form will reorder the system core modules execution even if you didn't change them and as some might notice all core modules has 0 weight value by default.
+Downloads
\ No newline at end of file
diff --git a/sites/all/modules/contrib/admin/modules_weight/modules_weight.info b/sites/all/modules/contrib/admin/modules_weight/modules_weight.info
new file mode 100644
index 00000000..ee382190
--- /dev/null
+++ b/sites/all/modules/contrib/admin/modules_weight/modules_weight.info
@@ -0,0 +1,10 @@
+name = Modules Weight
+description = This module provide admin interface to order the modules execution order.
+core = 7.x
+configure = admin/config/system/modules-weight
+; Information added by Drupal.org packaging script on 2015-07-31
+version = "7.x-1.4+4-dev"
+core = "7.x"
+project = "modules_weight"
+datestamp = "1438365840"
+
diff --git a/sites/all/modules/contrib/admin/modules_weight/modules_weight.module b/sites/all/modules/contrib/admin/modules_weight/modules_weight.module
new file mode 100644
index 00000000..42af1470
--- /dev/null
+++ b/sites/all/modules/contrib/admin/modules_weight/modules_weight.module
@@ -0,0 +1,186 @@
+ 'Modules Weight',
+ 'description' => 'Provide admin interface to order the modules execution.',
+ 'type' => MENU_NORMAL_ITEM,
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('modules_weight_admin_config_page_form'),
+ 'access arguments' => array('administer site configuration'),
+ );
+ $items['admin/config/system/modules-weight/default'] = array(
+ 'title' => 'Modules Weight',
+ 'description' => 'jQuery twitter search block config',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => 2,
+ );
+
+ $items['admin/config/system/modules-weight/configration'] = array(
+ 'title' => 'Modules Weight configurations',
+ 'description' => 'Configure Modules Weight.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('modules_weight_configuration_form'),
+ 'access arguments' => array('administer system'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 2,
+ );
+ return $items;
+}
+
+function modules_weight_admin_config_page_form($form, &$form_state) {
+
+ $form['modules_weight']['#tree'] = TRUE;
+
+ $result = db_select('system', 's')
+ ->condition('s.type', 'module')
+ ->condition('s.status', 1)
+ ->fields('s', array('weight', 'info', 'name'))
+ ->orderBy('weight', 'ASC')
+ ->execute();
+
+ $show_system_module = variable_get('show_system_modules', 0);
+
+ foreach ($result as $module) {
+ $info = unserialize($module->info);
+ if ($info['package'] != 'Core' || $show_system_module) {
+ $delta = modules_weight_prepare_delta($module->weight);
+ $form['modules_weight'][$module->name] = array(
+ 'name' => array(
+ '#markup' => t($info['name']),
+ ),
+ 'description' => array(
+ '#markup' => t($info['description']),
+ ),
+ 'weight' => array(
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $module->weight,
+ '#delta' => $delta,
+ '#title-display' => 'invisible',
+ ),
+ 'package' => array(
+ '#markup' => t($info['package']),
+ ),
+ 'old_weight_value' => array(
+ '#type' => 'hidden',
+ '#value' => $module->weight,
+ ),
+ );
+ }
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save Changes'));
+ return $form;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function modules_weight_theme() {
+ return array(
+ 'modules_weight_admin_config_page_form' => array(
+ 'render element' => 'form',
+ ),
+ );
+}
+
+function theme_modules_weight_admin_config_page_form($variables) {
+ $form = $variables['form'];
+
+ $rows = array();
+
+ foreach (element_children($form['modules_weight']) as $id) {
+
+ $form['modules_weight'][$id]['weight']['#attributes']['class'] = array('module-weight');
+
+ $rows[] = array(
+ 'data' => array(
+ drupal_render($form['modules_weight'][$id]['name']),
+ drupal_render($form['modules_weight'][$id]['description']),
+ drupal_render($form['modules_weight'][$id]['weight']),
+ drupal_render($form['modules_weight'][$id]['package']),
+ ),
+ 'class' => array('draggable'),
+ );
+ }
+
+ $header = array(t('Name'), t('Description'), t('Weight'), t('Package'));
+
+ $table_id = 'module-items-table';
+
+ $output = theme('table', array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => $table_id),
+ ));
+
+ $output .= drupal_render_children($form);
+ //Remove tabledrage functionlity due to issue related to re-weight all module
+ // for more info : https://www.drupal.org/node/2205787
+
+ //drupal_add_tabledrag($table_id, 'order', 'self', 'module-weight');
+
+ return $output;
+}
+
+/**
+ * Submit callback for the modules_weight_admin_config_page_form form.
+ *
+ * Updates the 'weight' column for each module in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ */
+function modules_weight_admin_config_page_form_submit($form, $form_state) {
+ foreach ($form_state['values']['modules_weight'] as $name => $weight) {
+ if($weight['weight'] != $weight['old_weight_value']) {
+ db_query(
+ "UPDATE {system} SET weight = :weight WHERE name = :name",
+ array(':weight' => $weight['weight'], ':name' => $name)
+ );
+ }
+ }
+}
+
+function modules_weight_configuration_form() {
+ $form = array();
+ $form['show_system_modules'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show system modules'),
+ '#return_value' => 1,
+ '#default_value' => variable_get('show_system_modules', 0),
+ );
+ $form['notice'] = array(
+ '#markup' => '' . t("cautions: This module just display non-core module by, if you check this option it will cause unexpected behavior in system, (USE IT ON YOUR OWN RISCK) that's because displaying core module in the configuration form will reorder the system core modules execution even if you didn't change them and as you might notice all core modules has 0 weight value by default.") . '',
+ );
+ return system_settings_form($form);
+}
+
+/**
+ * Prepares the delta for the weight field on the administration form.
+ * If a module has a weight higher then 100 (or lower than 100), it will use that
+ * value as delta and the '#weight' field will turn into a textfield most likely
+ *
+ * @param $weight
+ * @return int
+ */
+function modules_weight_prepare_delta($weight) {
+ $delta = 100;
+ if ((int) $weight > $delta) {
+ return (int) $weight;
+ }
+ if ((int) $weight < -100) {
+ return (int) $weight * -1;
+ }
+
+ return $delta;
+}
\ No newline at end of file
diff --git a/sites/all/modules/contrib/admin/permission_report/LICENSE.txt b/sites/all/modules/contrib/admin/permission_report/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/admin/permission_report/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/admin/permission_report/permission_report.info b/sites/all/modules/contrib/admin/permission_report/permission_report.info
new file mode 100644
index 00000000..dfee5c88
--- /dev/null
+++ b/sites/all/modules/contrib/admin/permission_report/permission_report.info
@@ -0,0 +1,10 @@
+name = Permission report
+description = Calculates and displays the a permission report for a user and shows which roles grant what permission for a given user. Also provides ability to list users in a role, and dig down into complicated role and permission problems.
+core = 7.x
+package = Permissions
+; Information added by Drupal.org packaging script on 2017-03-19
+version = "7.x-1.1"
+core = "7.x"
+project = "permission_report"
+datestamp = "1489907285"
+
diff --git a/sites/all/modules/contrib/admin/permission_report/permission_report.module b/sites/all/modules/contrib/admin/permission_report/permission_report.module
new file mode 100644
index 00000000..295aee9c
--- /dev/null
+++ b/sites/all/modules/contrib/admin/permission_report/permission_report.module
@@ -0,0 +1,342 @@
+
+ * @copyright Nicholas Vahalik 2013
+ * @package permission_report
+ **/
+
+/**
+ * Implements hook_menu().
+ */
+function permission_report_menu() {
+ $items = array();
+
+ $items['admin/reports/permissions'] = array(
+ 'title' => 'Permissions (role report)',
+ 'page callback' => 'permission_report_role_list',
+ 'access arguments' => array('view permission report'),
+ 'description' => 'View roles and the users in them.',
+ );
+
+ $items['admin/reports/permissions/roles'] = array(
+ 'title' => 'Roles',
+ 'page callback' => 'permission_report_role_list',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'access arguments' => array('view permission report'),
+ 'weight' => -5,
+ );
+
+ $items['admin/reports/permissions/perms'] = array(
+ 'title' => 'Permissions',
+ 'page callback' => 'permission_report_permission_list',
+ 'type' => MENU_LOCAL_TASK,
+ 'access arguments' => array('view permission report'),
+ );
+
+ $items['admin/reports/permissions/perms/%'] = array(
+ 'title' => 'Permissions',
+ 'type' => MENU_CALLBACK,
+ 'page arguments' => array(4),
+ 'page callback' => 'permission_report_user_having_perm',
+ 'access arguments' => array('view permission report'),
+ );
+
+ $items['admin/reports/permissions/roles/%'] = array(
+ 'title' => 'Roles',
+ 'type' => MENU_CALLBACK,
+ 'page callback' => 'permission_report_users_having_role',
+ 'page arguments' => array(4),
+ 'access arguments' => array('view permission report'),
+ );
+
+ $items['user/%user/permission_report'] = array(
+ 'title' => 'Permission Report',
+ 'page callback' => 'permission_report_user_report',
+ 'page arguments' => array(1),
+ 'type' => MENU_LOCAL_TASK,
+ 'access arguments' => array('view permission report'),
+ 'weight' => 2,
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_admin_paths().
+ */
+function permission_report_admin_paths() {
+ $paths = array(
+ 'user/*/permission_report' => TRUE,
+ );
+ return $paths;
+}
+
+/**
+ * Implements hook_user_view().
+ */
+function permission_report_user_view($account, $view_mode, $langcode) {
+ if (user_access('view permission report', $account)) {
+ $account->content['summary']['rsop'] = array(
+ '#type' => 'user_profile_item',
+ '#title' => t('Resultant Set of Permissions'),
+ '#markup' => l(t('View permission report report for !s', array('!s' => $account->name)), "user/$account->uid/permission_report"),
+ '#attributes' => array('class' => 'permission_report'),
+ );
+ }
+}
+
+/**
+ * Displays a permission report for a given user.
+ *
+ * @param $user User object.
+ *
+ * @return string
+ */
+function permission_report_user_report($user) {
+
+ // Render role/permission overview:
+ $options = array();
+ $row = array();
+ $can_admin_access = user_access('administer access control');
+
+ foreach (module_implements('permission') as $module) {
+ if ($permissions = module_invoke($module, 'permission')) {
+ $rows[] = array(array(
+ 'data' => t('@module module', array('@module' => $module)),
+ 'class' => 'module',
+ 'id' => 'module-' . $module,
+ 'colspan' => 3,
+ ));
+ asort($permissions);
+ foreach ($permissions as $perm => $meta) {
+ $options = array();
+ $display_roles = array();
+
+ $roles = _permission_report_roles_having_perm($perm, $user);
+
+ if (array_key_exists('description', $meta)) {
+ $options = array('attributes' => array('alt' => $meta['description']));
+ }
+
+ foreach ($roles as $rid => $name) {
+ $display_roles[] = $can_admin_access ? l($name, "admin/reports/permissions/roles/$rid") : t($name);
+ }
+ $rows[] = array(
+ array('data' => l(strip_tags($meta['title']), "admin/reports/permissions/perms/$perm", $options)),
+ array('data' => user_access($perm, $user) ? 'Yes' : 'No'),
+ array('data' => implode(', ', $display_roles)),
+ );
+ }
+ }
+ }
+
+ return theme('table', array('header' => array('Permission', 'Access', 'Roles'), 'rows' => $rows, 'attributes' => array('id' => 'permissions')));
+}
+
+/**
+ * Return an array of users keyed by IDs that have access to a specific permission.
+ *
+ * @param $permission Permission string.
+ *
+ * @return array
+ **/
+function _permission_report_users_having_perm($permission) {
+ $roles = _permission_report_roles_having_perm($permission);
+
+ if (count($roles) > 0) {
+ $query = db_select('users', 'u');
+ $query->innerJoin('users_roles', 'ur', 'ur.uid = u.uid');
+ $query->addField('u', 'uid');
+ $query->addField('u', 'name');
+ $query->condition('ur.rid', array_keys($roles), 'IN');
+ $users = $query->execute()->fetchAllKeyed();
+ return $users;
+ }
+
+ return array();
+}
+
+/**
+ * Gets a list of roles that have a permission, optionally limited
+ * to a specific role.
+ */
+function _permission_report_roles_having_perm($permission, $user = NULL) {
+ $query = db_select('role', 'r');
+ $query->addField('r', 'rid');
+ $query->addField('r', 'name');
+ $query->innerJoin('role_permission', 'p', 'r.rid = p.rid');
+ $query->condition('p.permission', $permission);
+
+ if ($user) {
+ $query->innerJoin('users_roles', 'ur', 'r.rid = ur.rid');
+ $query->condition('ur.uid', $user->uid);
+ }
+
+ return $query->execute()->fetchAllKeyed();
+}
+
+/**
+ * Generates a report of users having a particular role.
+ **/
+function permission_report_users_having_role($rid) {
+ $users_having_roles = $rows = array();
+
+ $query = db_select('users', 'u');
+ $query->addField('u', 'uid');
+ $query->addField('u', 'name');
+ $query->innerJoin('users_roles', 'ur', 'ur.uid = u.uid');
+ $query->condition('ur.rid', $rid);
+ $query->condition('u.status', 1);
+
+ $users_having_role = $query->execute()->fetchAll();
+
+ $query = db_select('role', 'r');
+ $query->addField('r', 'name');
+ $query->condition('rid', $rid);
+
+ $role_name = $query->execute()->fetchField();
+
+ $view_users = user_access('access user profiles');
+ drupal_set_title(t('Users in "!name" role', array('!name' => $role_name)));
+
+ $users_header = array(array(
+ 'data' => 'User',
+ 'colspan' => 2,
+ ));
+
+ foreach ($users_having_role as $user) {
+ $rows[] = array(
+ array('data' => ($user->uid !== 0) ? ($view_users ? l($user->name, "user/$user->uid") : $user->name) : variable_get('anonymous', t('Anonymous')))
+ ,
+ array('data' => l('Permission report', "user/$user->uid/permission_report")),
+ );
+ }
+
+ return theme('table', array('header' => $users_header, 'rows' => $rows));
+}
+
+/**
+ * List of roles with the number of users in each role.
+ */
+function permission_report_role_list() {
+ $user_in_roles = db_query('SELECT r.rid rid, r.name name, COUNT(ur.uid) as user_count FROM {role} r INNER JOIN {users_roles} ur USING (rid) INNER JOIN {users} u USING(uid) WHERE u.status = :u_status GROUP BY r.rid ORDER BY r.name', array(':u_status' => 1))->fetchAllAssoc('rid');
+ $can_admin_access = user_access('administer access control');
+ $all_users = user_roles();
+ // Remove 'authenticated user' and 'anonymous user'.
+ unset($all_users[1], $all_users[2]);
+ $rows = array();
+ foreach ($all_users as $rid => $name) {
+ $count = (isset($user_in_roles[$rid])) ? $user_in_roles[$rid]->user_count : 0;
+ $rows[] = array(
+ $can_admin_access ? l($name, "admin/people/permissions/$rid") : t($name),
+ l(format_plural($count, '1 user', '@count users'), "admin/reports/permissions/roles/$rid"),
+ );
+ }
+
+ $roles_header = array(
+ array(
+ 'data' => 'Role',
+ 'colspan' => 2,
+ ),
+ );
+
+ return theme('table', array('header' => $roles_header, 'rows' => $rows));
+}
+
+/**
+ * Creates a report showing which users have a specific permission.
+ */
+function permission_report_user_having_perm($permission) {
+
+ $view_users = user_access('access user profiles');
+ $can_admin_access = user_access('administer access control');
+ $output = '';
+
+ drupal_set_title(t('Permissions report for "!permission"', array('!permission' => $permission)));
+
+ foreach (_permission_report_users_having_perm($permission) as $uid => $name) {
+ $users_rows[] = array(
+ array('data' => ($uid !== 0) ? ($view_users ? l($name, "user/$uid") : $name) : variable_get('anonymous', t('Anonymous'))),
+ array('data' => l('Permission report', 'user/' . $uid . '/permission_report')),
+ );
+ }
+
+ $users_header = array(
+ array(
+ 'data' => 'User',
+ 'colspan' => 2,
+ ),
+ );
+
+ foreach (_permission_report_roles_having_perm($permission) as $rid => $name) {
+ $roles_rows[] = array(
+ array('data' => $can_admin_access ? l($name, "admin/reports/permissions/roles/$rid") : t($name)),
+ array('data' => l('Permission report', "admin/reports/permissions/roles/$rid")),
+ );
+ }
+
+ $roles_header = array(
+ array(
+ 'data' => 'Role',
+ 'colspan' => 2,
+ ),
+ );
+
+ $output .= '
';
+ $output .= theme('table', array('header' => $roles_header, 'rows' => $roles_rows));
+
+ return $output;
+}
+
+/**
+ * Creates a report which lists all permissions and the number of users which
+ * have those permissions.
+ */
+function permission_report_permission_list() {
+ $can_admin_access = user_access('administer access control');
+
+ foreach (module_implements('permission') as $module) {
+ if ($permissions = module_invoke($module, 'permission')) {
+ $rows[] = array(array(
+ 'data' => t('@module module', array('@module' => $module)),
+ 'class' => 'module',
+ 'id' => 'module-' . $module,
+ 'colspan' => 3,
+ ));
+
+ asort($permissions);
+
+ foreach ($permissions as $perm => $meta) {
+ $display_roles = array();
+
+ $roles = _permission_report_roles_having_perm($perm);
+ foreach ($roles as $rid => $name) {
+ $display_roles[] = $can_admin_access ? l($name, "admin/reports/permissions/roles/$rid") : t($name);
+ }
+ $rows[] = array(
+ array('data' => $meta['title']),
+ array('data' => l(format_plural(count(_permission_report_users_having_perm($perm)), '1 user', '@count users'), "admin/reports/permissions/perms/$perm")),
+ );
+ }
+ }
+ }
+ return theme('table', array('header' => array('Permission', 'Users'), 'rows' => $rows, 'attributes' => array('id' => 'permissions', 'sticky' => TRUE)));
+}
+
+/**
+ * Implements hook_permission().
+ */
+function permission_report_permission() {
+ return array(
+ 'view permission report' => array(
+ 'title' => t('View permission report'),
+ 'description' => t('Allows a user to view permission reports.'),
+ ),
+ );
+}
+
diff --git a/sites/all/modules/contrib/dev/examples/LICENSE.txt b/sites/all/modules/contrib/dev/examples/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/dev/examples/README.txt b/sites/all/modules/contrib/dev/examples/README.txt
new file mode 100644
index 00000000..5904fefe
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/README.txt
@@ -0,0 +1,62 @@
+Examples for Developers
+=======================
+
+http://drupal.org/project/examples
+
+What Is This?
+-------------
+
+This set of modules is intended to provide working examples of Drupal's
+features and APIs. The modules strive to be simple, well documented and
+modification friendly, in order to help developers quickly learn their inner
+workings.
+
+These examples are meant to teach you about code-level development for Drupal
+7. Some solutions might be better served using a contributed module, so that
+you don't end up having to re-invent the wheel in PHP.
+
+
+How To Use The Examples
+-----------------------
+
+There are three main ways to interact with the examples in this project:
+
+1. Enable the modules and use them within Drupal. Not all modules will have
+obvious things to see within Drupal. For instance, while the Page and Form API
+examples will show you forms, the Database API example will not show you much
+within Drupal itself.
+
+2. Read the code. Much effort has gone into making the example code readable,
+not only in terms of the code itself, but also the extensive inline comments
+and documentation blocks.
+
+3. Browse the code and documentation on the web. There are two main places to
+do this:
+
+* https://api.drupal.org/api/examples is the main API site for all of Drupal.
+It has all manner of cross-linked references between the example code and the
+APIs being demonstrated.
+
+* http://drupalcode.org/project/examples.git allows you to browse the git
+repository for the Examples project.
+
+
+How To Install The Modules
+--------------------------
+
+1. Install Examples for Developers (unpacking it to your Drupal
+/sites/all/modules directory if you're installing by hand, for example).
+
+2. Enable any Example modules in Admin menu > Site building > Modules.
+
+3. Rebuild access permissions if you are prompted to.
+
+4. Profit! The examples will appear in your Navigation menu (on the left
+sidebar by default; you'll need to reenable it if you removed it).
+
+Now you can read the code and its comments and see the result, experiment with
+it, and hopefully quickly grasp how things work.
+
+If you find a problem, incorrect comment, obsolete or improper code or such,
+please search for an issue about it at http://drupal.org/project/issues/examples
+If there isn't already an issue for it, please create a new one.
diff --git a/sites/all/modules/contrib/dev/examples/action_example/action_example.info b/sites/all/modules/contrib/dev/examples/action_example/action_example.info
new file mode 100644
index 00000000..05dd5e0e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/action_example/action_example.info
@@ -0,0 +1,13 @@
+name = Action example
+description = Demonstrates providing actions that can be associated to triggers.
+package = Example modules
+core = 7.x
+dependencies[] = trigger
+files[] = action_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/action_example/action_example.module b/sites/all/modules/contrib/dev/examples/action_example/action_example.module
new file mode 100644
index 00000000..993106e0
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/action_example/action_example.module
@@ -0,0 +1,375 @@
+ array(
+ 'label' => t('Action Example: A basic example action that does nothing'),
+ 'type' => 'system',
+ 'configurable' => FALSE,
+ 'triggers' => array('any'),
+ ),
+ 'action_example_unblock_user_action' => array(
+ 'label' => t('Action Example: Unblock a user'),
+ 'type' => 'user',
+ 'configurable' => FALSE,
+ 'triggers' => array('any'),
+ ),
+ 'action_example_node_sticky_action' => array(
+ 'type' => 'node',
+ 'label' => t('Action Example: Promote to frontpage and sticky on top any content created by :'),
+ 'configurable' => TRUE,
+ 'behavior' => array('changes_property'),
+ 'triggers' => array('node_presave', 'node_insert', 'node_update'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a menu entry which explains what the module does.
+ */
+function action_example_menu() {
+ $items['examples/action_example'] = array(
+ 'title' => 'Action Example',
+ 'description' => 'Provides a basic information page.',
+ 'page callback' => '_action_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+
+/**
+ * A simple page to explain to the developer what to do.
+ */
+function _action_example_page() {
+ return t("The Action Example provides three example actions which can be configured on the Actions configuration page and assigned to triggers on the Triggers configuration page.", array('@actions_url' => url('admin/config/system/actions'), '@triggers_url' => url('admin/structure/trigger/node')));
+}
+
+/**
+ * Action function for action_example_basic_action.
+ *
+ * This action is not expecting any type of entity object, and can be used with
+ * any trigger type or any event.
+ *
+ * @param object $entity
+ * An optional entity object.
+ * @param array $context
+ * Array with parameters for this action: depends on the trigger.
+ *
+ * @see action_example_action_info()
+ */
+function action_example_basic_action(&$entity, $context = array()) {
+ // In this case we are ignoring the entity and the context. This case of
+ // action is useful when your action does not depend on the context, and
+ // the function must do something regardless the scope of the trigger.
+ // Simply announces that the action was executed using a message.
+ drupal_set_message(t('action_example_basic_action fired'));
+ watchdog('action_example', 'action_example_basic_action fired.');
+}
+
+/**
+ * Action function for action_example_unblock_user_action.
+ *
+ * This action is expecting an entity object user, node or comment. If none of
+ * the above is provided (because it was not called from an user/node/comment
+ * trigger event), then the action will be taken on the current logged in user.
+ *
+ * Unblock an user. This action can be fired from different trigger types:
+ * - User trigger: this user will be unblocked.
+ * - Node/Comment trigger: the author of the node or comment will be unblocked.
+ * - Other: (including system or custom defined types), current user will be
+ * unblocked. (Yes, this seems like an incomprehensible use-case.)
+ *
+ * @param object $entity
+ * An optional user object (could be a user, or an author if context is
+ * node or comment)
+ * @param array $context
+ * Array with parameters for this action: depends on the trigger. The context
+ * is not used in this example.
+ */
+function action_example_unblock_user_action(&$entity, $context = array()) {
+
+ // First we check that entity is a user object. If this is the case, then this
+ // is a user-type trigger.
+ if (isset($entity->uid)) {
+ $uid = $entity->uid;
+ }
+ elseif (isset($context['uid'])) {
+ $uid = $context['uid'];
+ }
+ // If neither of those are valid, then block the current user.
+ else {
+ $uid = $GLOBALS['user']->uid;
+ }
+ $account = user_load($uid);
+ $account = user_save($account, array('status' => 1));
+ watchdog('action_example', 'Unblocked user %name.', array('%name' => $account->name));
+ drupal_set_message(t('Unblocked user %name', array('%name' => $account->name)));
+}
+
+/**
+ * Form function for action_example_node_sticky_action.
+ *
+ * Since we defined action_example_node_sticky_action as 'configurable' => TRUE,
+ * this action requires a configuration form to create/configure the action.
+ * In this circumstance, Drupal will attempt to call a function named by
+ * combining the action name (action_example_node_sticky_action) and _form, in
+ * this case yielding action_example_node_sticky_action_form.
+ *
+ * In Drupal, actions requiring creation and configuration are called 'advanced
+ * actions', because they must be customized to define their functionality.
+ *
+ * The 'action_example_node_sticky_action' allows creating rules to promote and
+ * set sticky content created by selected users on certain events. A form is
+ * used to configure which user is affected by this action, and this form
+ * includes the standard _validate and _submit hooks.
+ */
+
+
+/**
+ * Generates settings form for action_example_node_sticky_action().
+ *
+ * @param array $context
+ * An array of options of this action (in case it is being edited)
+ *
+ * @return array
+ * Settings form as Form API array.
+ *
+ * @see action_example_action_info()
+ */
+function action_example_node_sticky_action_form($context) {
+ /*
+ * We return a configuration form to set the requirements that will
+ * match this action before being executed. This is a regular Drupal form and
+ * may include any type of information you want, but all the fields of the
+ * form will be saved into the $context variable.
+ *
+ * In this case we are promoting all content types submitted by this user, but
+ * it is possible to extend these conditions providing more options in the
+ * settings form.
+ */
+ $form['author'] = array(
+ '#title' => t('Author name'),
+ '#type' => 'textfield',
+ '#description' => t('Any content created, presaved or updated by this user will be promoted to front page and set as sticky.'),
+ '#default_value' => isset($context['author']) ? $context['author'] : '',
+ );
+ // Verify user permissions and provide an easier way to fill this field.
+ if (user_access('access user profiles')) {
+ $form['author']['#autocomplete_path'] = 'user/autocomplete';
+ }
+ // No more options, return the form.
+ return $form;
+}
+
+/**
+ * Validates settings form for action_example_node_sticky_action().
+ *
+ * Verifies that user exists before continuing.
+ */
+function action_example_node_sticky_action_validate($form, $form_state) {
+ if (!$account = user_load_by_name($form_state['values']['author'])) {
+ form_set_error('author', t('Please, provide a valid username'));
+ }
+}
+
+/**
+ * Submit handler for action_example_node_sticky_action.
+ *
+ * Returns an associative array of values which will be available in the
+ * $context when an action is executed.
+ */
+function action_example_node_sticky_action_submit($form, $form_state) {
+ return array('author' => $form_state['values']['author']);
+}
+
+/**
+ * Action function for action_example_node_sticky_action.
+ *
+ * Promote and set sticky flag. This is the special action that has been
+ * customized using the configuration form, validated with the validation
+ * function, and submitted with the submit function.
+ *
+ * @param object $node
+ * A node object provided by the associated trigger.
+ * @param array $context
+ * Array with the following elements:
+ * - 'author': username of the author's content this function will promote and
+ * set as sticky.
+ */
+function action_example_node_sticky_action($node, $context) {
+ if (function_exists('dsm')) {
+ dsm($node, 'action_example_node_sticky_action is firing. Here is the $node');
+ dsm($context, 'action_example_node_sticky_action is firing. Here is the $context');
+ }
+ // Get the user configured for this special action.
+ $account = user_load_by_name($context['author']);
+ // Is the node created by this user? then promote and set as sticky.
+ if ($account->uid == $node->uid) {
+ $node->promote = NODE_PROMOTED;
+ $node->sticky = NODE_STICKY;
+ watchdog('action',
+ 'Set @type %title to sticky and promoted by special action for user %username.',
+ array(
+ '@type' => node_type_get_name($node),
+ '%title' => $node->title,
+ '%username' => $account->name,
+ )
+ );
+ drupal_set_message(
+ t('Set @type %title to sticky and promoted by special action for user %username.',
+ array(
+ '@type' => node_type_get_name($node),
+ '%title' => $node->title,
+ '%username' => $account->name,
+ )
+ )
+ );
+ }
+}
+/**
+ * @} End of "defgroup action_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/action_example/action_example.test b/sites/all/modules/contrib/dev/examples/action_example/action_example.test
new file mode 100644
index 00000000..c6686ab9
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/action_example/action_example.test
@@ -0,0 +1,111 @@
+ 'Action example',
+ 'description' => 'Perform various tests on action_example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('trigger', 'action_example');
+ }
+
+ /**
+ * Test Action Example.
+ *
+ * 1. action_example_basic_action: Configure a action_example_basic_action to
+ * happen when user logs in.
+ * 2. action_example_unblock_user_action: When a user's profile is being
+ * viewed, unblock that user.
+ * 3. action_example_node_sticky_action: Create a user, configure that user
+ * to always be stickied using advanced configuration. Have the user
+ * create content; verify that it gets stickied.
+ */
+ public function testActionExample() {
+ // Create an administrative user.
+ $admin_user = $this->drupalCreateUser(
+ array(
+ 'administer actions',
+ 'access comments',
+ 'access content',
+ 'post comments',
+ 'skip comment approval',
+ 'create article content',
+ 'access user profiles',
+ 'administer users',
+ )
+ );
+ $this->drupalLogin($admin_user);
+
+ // 1. Assign basic action; then logout and login user and see if it puts
+ // the message on the screen.
+ $hash = drupal_hash_base64('action_example_basic_action');
+ $edit = array('aid' => $hash);
+ $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-login-assign-form');
+
+ $this->drupalLogout();
+ $this->drupalLogin($admin_user);
+ $this->assertText(t('action_example_basic_action fired'));
+
+ // 2. Unblock: When a user's profile is being viewed, unblock.
+ $normal_user = $this->drupalCreateUser();
+ // Create blocked user.
+ user_save($normal_user, array('status' => 0));
+ $normal_user = user_load($normal_user->uid, TRUE);
+ $this->assertFalse($normal_user->status, 'Normal user status has been set to blocked');
+
+ $hash = drupal_hash_base64('action_example_unblock_user_action');
+ $edit = array('aid' => $hash);
+ $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-view-assign-form');
+
+ $this->drupalGet("user/$normal_user->uid");
+ $normal_user = user_load($normal_user->uid, TRUE);
+ $this->assertTrue($normal_user->status, 'Normal user status has been set to unblocked');
+ $this->assertRaw(t('Unblocked user %name', array('%name' => $normal_user->name)));
+
+ // 3. Create a user whose posts are always to be stickied.
+ $sticky_user = $this->drupalCreateUser(
+ array(
+ 'access comments',
+ 'access content',
+ 'post comments',
+ 'skip comment approval',
+ 'create article content',
+ )
+ );
+
+ $action_label = $this->randomName();
+ $edit = array(
+ 'actions_label' => $action_label,
+ 'author' => $sticky_user->name,
+ );
+ $aid = $this->configureAdvancedAction('action_example_node_sticky_action', $edit);
+ $edit = array('aid' => drupal_hash_base64($aid));
+ $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-insert-assign-form');
+ // Now create a node and verify that it gets stickied.
+ $this->drupalLogout();
+ $this->drupalLogin($sticky_user);
+ $node = $this->drupalCreateNode();
+ $this->assertTrue($node->sticky, 'Node was set to sticky on creation');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.css b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.css
new file mode 100644
index 00000000..e1cdc694
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.css
@@ -0,0 +1,17 @@
+/*
+ * @file
+ * CSS for ajax_example.
+ *
+ * See @link ajax_example_dependent_dropdown_degrades @endlink for
+ * details on what this file does. It is not used in any other example.
+ */
+
+/* Hides the next button when not degrading to non-javascript browser */
+html.js .next-button {
+ display: none;
+}
+
+/* Makes the next/choose button align to the right of the select control */
+.form-item-dropdown-first, .form-item-question-type-select {
+ display: inline-block;
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.info b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.info
new file mode 100644
index 00000000..2dc5b590
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.info
@@ -0,0 +1,12 @@
+name = AJAX Example
+description = An example module showing how to use Drupal AJAX forms
+package = Example modules
+core = 7.x
+files[] = ajax_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.install b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.install
new file mode 100644
index 00000000..6d3d2130
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.install
@@ -0,0 +1,56 @@
+ 'Stores example settings for nodes.',
+ 'fields' => array(
+ 'nid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {node}.nid to store settings.',
+ ),
+ 'example_1' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Node Form Example 1 checkbox',
+ ),
+ 'example_2' => array(
+ 'type' => 'varchar',
+ 'length' => 256,
+ 'not null' => FALSE,
+ 'default' => '',
+ 'description' => 'Node Form Example 2 textfield',
+ ),
+ ),
+ 'primary key' => array('nid'),
+ 'foreign keys' => array(
+ 'dnv_node' => array(
+ 'table' => 'node',
+ 'columns' => array('nid' => 'nid'),
+ ),
+ ),
+ );
+ return $schema;
+}
+
+/**
+ * Add the new ajax_example_node_form_alter table.
+ */
+function ajax_example_update_7100() {
+ if (!db_table_exists('ajax_example_node_form_alter')) {
+ $schema = ajax_example_schema();
+ db_create_table('ajax_example_node_form_alter', $schema['ajax_example_node_form_alter']);
+ return st('Created table ajax_example_node_form_alter');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.js b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.js
new file mode 100644
index 00000000..2d06038c
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.js
@@ -0,0 +1,29 @@
+/*
+ * @file
+ * JavaScript for ajax_example.
+ *
+ * See @link ajax_example_dependent_dropdown_degrades @endlink for
+ * details on what this file does. It is not used in any other example.
+ */
+
+(function($) {
+
+ // Re-enable form elements that are disabled for non-ajax situations.
+ Drupal.behaviors.enableFormItemsForAjaxForms = {
+ attach: function() {
+ // If ajax is enabled.
+ if (Drupal.ajax) {
+ $('.enabled-for-ajax').removeAttr('disabled');
+ }
+
+ // Below is only for the demo case of showing with js turned off.
+ // It overrides the behavior of the CSS that would normally turn off
+ // the 'ok' button when JS is enabled. Here, for demonstration purposes,
+ // we have AJAX disabled but JS turned on, so use this to simulate.
+ if (!Drupal.ajax) {
+ $('html.js .next-button').show();
+ }
+ }
+ };
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.module b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.module
new file mode 100644
index 00000000..4bfa26c2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.module
@@ -0,0 +1,693 @@
+ 'AJAX Example',
+ 'page callback' => 'ajax_example_intro',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+
+ // Change the description of a form element.
+ $items['examples/ajax_example/simplest'] = array(
+ 'title' => 'Simplest AJAX Example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_simplest'),
+ 'access callback' => TRUE,
+ 'weight' => 0,
+ );
+ // Generate a changing number of checkboxes.
+ $items['examples/ajax_example/autocheckboxes'] = array(
+ 'title' => 'Generate checkboxes',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_autocheckboxes'),
+ 'access callback' => TRUE,
+ 'weight' => 1,
+ );
+ // Generate different textfields based on form state.
+ $items['examples/ajax_example/autotextfields'] = array(
+ 'title' => 'Generate textfields',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_autotextfields'),
+ 'access callback' => TRUE,
+ 'weight' => 2,
+ );
+
+ // Submit a form without a page reload.
+ $items['examples/ajax_example/submit_driven_ajax'] = array(
+ 'title' => 'Submit-driven AJAX',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_submit_driven_ajax'),
+ 'access callback' => TRUE,
+ 'weight' => 3,
+ );
+
+ // Repopulate a dropdown based on form state.
+ $items['examples/ajax_example/dependent_dropdown'] = array(
+ 'title' => 'Dependent dropdown',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dependent_dropdown'),
+ 'access callback' => TRUE,
+ 'weight' => 4,
+ );
+ // Repopulate a dropdown, but this time with graceful degredation.
+ // See ajax_example_graceful_degradation.inc.
+ $items['examples/ajax_example/dependent_dropdown_degrades'] = array(
+ 'title' => 'Dependent dropdown (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dependent_dropdown_degrades'),
+ 'access callback' => TRUE,
+ 'weight' => 5,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/dependent_dropdown_degrades_no_js'] = array(
+ 'title' => 'Dependent dropdown with javascript off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dependent_dropdown_degrades', TRUE),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 5,
+ );
+
+ // Populate a form section based on input in another element.
+ $items['examples/ajax_example/dynamic_sections'] = array(
+ 'title' => 'Dynamic Sections (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dynamic_sections'),
+ 'access callback' => TRUE,
+ 'weight' => 6,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/dynamic_sections_no_js'] = array(
+ 'title' => 'Dynamic Sections w/JS turned off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dynamic_sections', TRUE),
+ 'access callback' => TRUE,
+ 'weight' => 6,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ );
+
+ // A classic multi-step wizard, but with no page reloads.
+ // See ajax_example_graceful_degradation.inc.
+ $items['examples/ajax_example/wizard'] = array(
+ 'title' => 'Wizard (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_wizard'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 7,
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/wizard_no_js'] = array(
+ 'title' => 'Wizard w/JS turned off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_wizard', TRUE),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 7,
+ );
+
+ // Add-more button that creates additional form elements.
+ // See ajax_example_graceful_degradation.inc.
+ $items['examples/ajax_example/add_more'] = array(
+ 'title' => 'Add-more button (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_add_more'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 8,
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/add_more_no_js'] = array(
+ 'title' => 'Add-more button w/JS turned off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_add_more', TRUE),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 8,
+ );
+
+ // Use the AJAX framework outside the context of a form using the use-ajax
+ // class. See ajax_example_misc.inc.
+ $items['examples/ajax_example/ajax_link'] = array(
+ 'title' => 'Ajax Link ("use-ajax" class)',
+ 'page callback' => 'ajax_example_render_link',
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_misc.inc',
+ 'weight' => 9,
+ );
+ // Use the AJAX framework outside the context of a form using a renderable
+ // array of type link with the #ajax property. See ajax_example_misc.inc.
+ $items['examples/ajax_example/ajax_link_renderable'] = array(
+ 'title' => 'Ajax Link (Renderable Array)',
+ 'page callback' => 'ajax_example_render_link_ra',
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_misc.inc',
+ 'weight' => 9,
+ );
+ // A menu callback is required when using ajax outside of the Form API.
+ $items['ajax_link_callback'] = array(
+ 'page callback' => 'ajax_link_response',
+ 'access callback' => 'user_access',
+ 'access arguments' => array('access content'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'ajax_example_misc.inc',
+ );
+
+ // Use AJAX framework commands outside of the #ajax form property.
+ // See ajax_example_advanced.inc.
+ $items['examples/ajax_example/advanced_commands'] = array(
+ 'title' => 'AJAX framework commands',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_advanced_commands'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_advanced.inc',
+ 'weight' => 100,
+ );
+
+ // Autocomplete examples.
+ $items['examples/ajax_example/simple_autocomplete'] = array(
+ 'title' => 'Autocomplete (simple)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_simple_autocomplete'),
+ 'access arguments' => array('access user profiles'),
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'weight' => 10,
+ );
+ $items['examples/ajax_example/simple_user_autocomplete_callback'] = array(
+ 'page callback' => 'ajax_example_simple_user_autocomplete_callback',
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access user profiles'),
+ );
+ $items['examples/ajax_example/node_autocomplete'] = array(
+ 'title' => 'Autocomplete (node with nid)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_unique_autocomplete'),
+ 'access arguments' => array('access content'),
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'weight' => 11,
+ );
+ $items['examples/ajax_example/unique_node_autocomplete_callback'] = array(
+ 'page callback' => 'ajax_example_unique_node_autocomplete_callback',
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access content'),
+ );
+ $items['examples/ajax_example/node_by_author'] = array(
+ 'title' => 'Autocomplete (node limited by author)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_node_by_author_autocomplete'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'weight' => 12,
+ );
+ $items['examples/ajax_example/node_by_author_autocomplete'] = array(
+ 'page callback' => 'ajax_example_node_by_author_node_autocomplete_callback',
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access content'),
+ );
+ // This is the landing page for the progress bar example. It uses
+ // drupal_get_form() in order to build the form.
+ $items['examples/ajax_example/progressbar'] = array(
+ 'title' => 'Progress bar example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_progressbar_form'),
+ 'access arguments' => array('access content'),
+ 'file' => 'ajax_example_progressbar.inc',
+ );
+ // This is the callback route for the AJAX-based progress bar.
+ $items['examples/ajax_example/progressbar/progress/%'] = array(
+ 'title' => 'Progress bar progress',
+ 'page callback' => 'ajax_example_progressbar_progress',
+ 'page arguments' => array(4),
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access content'),
+ 'file' => 'ajax_example_progressbar.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * A basic introduction page for the ajax_example module.
+ */
+function ajax_example_intro() {
+ $markup = t('The AJAX example module provides many examples of AJAX including forms, links, and AJAX commands.');
+
+ $list[] = l(t('Simplest AJAX Example'), 'examples/ajax_example/simplest');
+ $list[] = l(t('Generate checkboxes'), 'examples/ajax_example/autocheckboxes');
+ $list[] = l(t('Generate textfields'), 'examples/ajax_example/autotextfields');
+ $list[] = l(t('Submit-driven AJAX'), 'examples/ajax_example/submit_driven_ajax');
+ $list[] = l(t('Dependent dropdown'), 'examples/ajax_example/dependent_dropdown');
+ $list[] = l(t('Dependent dropdown (with graceful degradation)'), 'examples/ajax_example/dependent_dropdown_degrades');
+ $list[] = l(t('Dynamic Sections w/JS turned off'), 'examples/ajax_example/dependent_dropdown_degrades_no_js');
+ $list[] = l(t('Wizard (with graceful degradation)'), 'examples/ajax_example/wizard');
+ $list[] = l(t('Wizard w/JS turned off'), 'examples/ajax_example/wizard_no_js');
+ $list[] = l(t('Add-more button (with graceful degradation)'), 'examples/ajax_example/add_more');
+ $list[] = l(t('Add-more button w/JS turned off'), 'examples/ajax_example/add_more_no_js');
+ $list[] = l(t('Ajax Link ("use-ajax" class)'), 'examples/ajax_example/ajax_link');
+ $list[] = l(t('Ajax Link (Renderable Array)'), 'examples/ajax_example/ajax_link_renderable');
+ $list[] = l(t('AJAX framework commands'), 'examples/ajax_example/advanced_commands');
+ $list[] = l(t('Autocomplete (simple)'), 'examples/ajax_example/simple_autocomplete');
+ $list[] = l(t('Autocomplete (node with nid)'), 'examples/ajax_example/node_autocomplete');
+ $list[] = l(t('Autocomplete (node limited by author)'), 'examples/ajax_example/node_by_author');
+
+ $variables['items'] = $list;
+ $variables['type'] = 'ul';
+ $markup .= theme('item_list', $variables);
+
+ return $markup;
+}
+
+/**
+ * Basic AJAX callback example.
+ *
+ * Simple form whose ajax-enabled 'changethis' member causes a text change
+ * in the description of the 'replace_textfield' member.
+ *
+ * See @link http://drupal.org/node/262422 Form API Tutorial @endlink
+ */
+function ajax_example_simplest($form, &$form_state) {
+ $form = array();
+ $form['changethis'] = array(
+ '#title' => t("Choose something and explain why"),
+ '#type' => 'select',
+ '#options' => array(
+ 'one' => 'one',
+ 'two' => 'two',
+ 'three' => 'three',
+ ),
+ '#ajax' => array(
+ // #ajax has two required keys: callback and wrapper.
+ // 'callback' is a function that will be called when this element changes.
+ 'callback' => 'ajax_example_simplest_callback',
+ // 'wrapper' is the HTML id of the page element that will be replaced.
+ 'wrapper' => 'replace_textfield_div',
+ // There are also several optional keys - see ajax_example_autocheckboxes
+ // below for details on 'method', 'effect' and 'speed' and
+ // ajax_example_dependent_dropdown for 'event'.
+ ),
+ );
+
+ // This entire form element will be replaced whenever 'changethis' is updated.
+ $form['replace_textfield'] = array(
+ '#type' => 'textfield',
+ '#title' => t("Why"),
+ // The prefix/suffix provide the div that we're replacing, named by
+ // #ajax['wrapper'] above.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // An AJAX request calls the form builder function for every change.
+ // We can change how we build the form based on $form_state.
+ if (!empty($form_state['values']['changethis'])) {
+ $form['replace_textfield']['#description'] = t("Say why you chose '@value'", array('@value' => $form_state['values']['changethis']));
+ }
+ return $form;
+}
+
+/**
+ * Callback for ajax_example_simplest.
+ *
+ * On an ajax submit, the form builder function is called again, then the $form
+ * and $form_state are passed to this callback function so it can select which
+ * portion of the form to send on to the client.
+ *
+ * @return array
+ * Renderable array (the textfield element)
+ */
+function ajax_example_simplest_callback($form, $form_state) {
+ // The form has already been submitted and updated. We can return the replaced
+ // item as it is.
+ return $form['replace_textfield'];
+}
+
+/**
+ * Form manipulation through AJAX.
+ *
+ * AJAX-enabled select element causes replacement of a set of checkboxes
+ * based on the selection.
+ */
+function ajax_example_autocheckboxes($form, &$form_state) {
+ // Since the form builder is called after every AJAX request, we rebuild
+ // the form based on $form_state.
+ $num_checkboxes = !empty($form_state['values']['howmany_select']) ? $form_state['values']['howmany_select'] : 1;
+
+ $form['howmany_select'] = array(
+ '#title' => t('How many checkboxes do you want?'),
+ '#type' => 'select',
+ '#options' => array(1 => 1, 2 => 2, 3 => 3, 4 => 4),
+ '#default_value' => $num_checkboxes,
+ '#ajax' => array(
+ 'callback' => 'ajax_example_autocheckboxes_callback',
+ 'wrapper' => 'checkboxes-div',
+ // 'method' defaults to replaceWith, but valid values also include
+ // append, prepend, before and after.
+ // 'method' => 'replaceWith',
+ // 'effect' defaults to none. Other valid values are 'fade' and 'slide'.
+ // See ajax_example_autotextfields for an example of 'fade'.
+ 'effect' => 'slide',
+ // 'speed' defaults to 'slow'. You can also use 'fast'
+ // or a number of milliseconds for the animation to last.
+ // 'speed' => 'slow',
+ // Don't show any throbber...
+ 'progress' => array('type' => 'none'),
+ ),
+ );
+
+ $form['checkboxes_fieldset'] = array(
+ '#title' => t("Generated Checkboxes"),
+ // The prefix/suffix provide the div that we're replacing, named by
+ // #ajax['wrapper'] above.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#type' => 'fieldset',
+ '#description' => t('This is where we get automatically generated checkboxes'),
+ );
+
+ for ($i = 1; $i <= $num_checkboxes; $i++) {
+ $form['checkboxes_fieldset']["checkbox$i"] = array(
+ '#type' => 'checkbox',
+ '#title' => "Checkbox $i",
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Callback for autocheckboxes.
+ *
+ * Callback element needs only select the portion of the form to be updated.
+ * Since #ajax['callback'] return can be HTML or a renderable array (or an
+ * array of commands), we can just return a piece of the form.
+ * See @link ajax_example_advanced.inc AJAX Advanced Commands for more details
+ * on AJAX framework commands.
+ *
+ * @return array
+ * Renderable array (the checkboxes fieldset)
+ */
+function ajax_example_autocheckboxes_callback($form, $form_state) {
+ return $form['checkboxes_fieldset'];
+}
+
+
+/**
+ * Show/hide textfields based on AJAX-enabled checkbox clicks.
+ */
+function ajax_example_autotextfields($form, &$form_state) {
+
+ $form['ask_first_name'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Ask me my first name'),
+ '#ajax' => array(
+ 'callback' => 'ajax_example_autotextfields_callback',
+ 'wrapper' => 'textfields',
+ 'effect' => 'fade',
+ ),
+ );
+ $form['ask_last_name'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Ask me my last name'),
+ '#ajax' => array(
+ 'callback' => 'ajax_example_autotextfields_callback',
+ 'wrapper' => 'textfields',
+ 'effect' => 'fade',
+ ),
+ );
+
+ $form['textfields'] = array(
+ '#title' => t("Generated text fields for first and last name"),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#type' => 'fieldset',
+ '#description' => t('This is where we put automatically generated textfields'),
+ );
+
+ // Since checkboxes return TRUE or FALSE, we have to check that
+ // $form_state has been filled as well as what it contains.
+ if (!empty($form_state['values']['ask_first_name']) && $form_state['values']['ask_first_name']) {
+ $form['textfields']['first_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First Name'),
+ );
+ }
+ if (!empty($form_state['values']['ask_last_name']) && $form_state['values']['ask_last_name']) {
+ $form['textfields']['last_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last Name'),
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Click Me'),
+ );
+
+ return $form;
+}
+
+/**
+ * Callback for autotextfields.
+ *
+ * Selects the piece of the form we want to use as replacement text and returns
+ * it as a form (renderable array).
+ *
+ * @return array
+ * Renderable array (the textfields element)
+ */
+function ajax_example_autotextfields_callback($form, $form_state) {
+ return $form['textfields'];
+}
+
+
+/**
+ * A very basic form which with an AJAX-enabled submit.
+ *
+ * On submit, the markup in the #markup element is updated.
+ */
+function ajax_example_submit_driven_ajax($form, &$form_state) {
+ $form['box'] = array(
+ '#type' => 'markup',
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#markup' => '
Initial markup for box
',
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_submit_driven_callback',
+ 'wrapper' => 'box',
+ ),
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Callback for submit_driven example.
+ *
+ * Select the 'box' element, change the markup in it, and return it as a
+ * renderable array.
+ *
+ * @return array
+ * Renderable array (the box element)
+ */
+function ajax_example_submit_driven_callback($form, $form_state) {
+ // In most cases, it is recommended that you put this logic in form generation
+ // rather than the callback. Submit driven forms are an exception, because
+ // you may not want to return the form at all.
+ $element = $form['box'];
+ $element['#markup'] = "Clicked submit ({$form_state['values']['op']}): " . date('c');
+ return $element;
+}
+
+
+/**
+ * AJAX-based dropdown example form.
+ *
+ * A form with a dropdown whose options are dependent on a
+ * choice made in a previous dropdown.
+ *
+ * On changing the first dropdown, the options in the second
+ * are updated.
+ */
+function ajax_example_dependent_dropdown($form, &$form_state) {
+ // Get the list of options to populate the first dropdown.
+ $options_first = _ajax_example_get_first_dropdown_options();
+ // If we have a value for the first dropdown from $form_state['values'] we use
+ // this both as the default value for the first dropdown and also as a
+ // parameter to pass to the function that retrieves the options for the
+ // second dropdown.
+ $selected = isset($form_state['values']['dropdown_first']) ? $form_state['values']['dropdown_first'] : key($options_first);
+
+ $form['dropdown_first'] = array(
+ '#type' => 'select',
+ '#title' => 'Instrument Type',
+ '#options' => $options_first,
+ '#default_value' => $selected,
+ // Bind an ajax callback to the change event (which is the default for the
+ // select form type) of the first dropdown. It will replace the second
+ // dropdown when rebuilt.
+ '#ajax' => array(
+ // When 'event' occurs, Drupal will perform an ajax request in the
+ // background. Usually the default value is sufficient (eg. change for
+ // select elements), but valid values include any jQuery event,
+ // most notably 'mousedown', 'blur', and 'submit'.
+ // 'event' => 'change',
+ 'callback' => 'ajax_example_dependent_dropdown_callback',
+ 'wrapper' => 'dropdown-second-replace',
+ ),
+ );
+
+ $form['dropdown_second'] = array(
+ '#type' => 'select',
+ '#title' => $options_first[$selected] . ' ' . t('Instruments'),
+ // The entire enclosing div created here gets replaced when dropdown_first
+ // is changed.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ // When the form is rebuilt during ajax processing, the $selected variable
+ // will now have the new value and so the options will change.
+ '#options' => _ajax_example_get_second_dropdown_options($selected),
+ '#default_value' => isset($form_state['values']['dropdown_second']) ? $form_state['values']['dropdown_second'] : '',
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+ return $form;
+}
+
+/**
+ * Selects just the second dropdown to be returned for re-rendering.
+ *
+ * Since the controlling logic for populating the form is in the form builder
+ * function, all we do here is select the element and return it to be updated.
+ *
+ * @return array
+ * Renderable array (the second dropdown)
+ */
+function ajax_example_dependent_dropdown_callback($form, $form_state) {
+ return $form['dropdown_second'];
+}
+
+/**
+ * Helper function to populate the first dropdown.
+ *
+ * This would normally be pulling data from the database.
+ *
+ * @return array
+ * Dropdown options.
+ */
+function _ajax_example_get_first_dropdown_options() {
+ // drupal_map_assoc() just makes an array('String' => 'String'...).
+ return drupal_map_assoc(
+ array(
+ t('String'),
+ t('Woodwind'),
+ t('Brass'),
+ t('Percussion'),
+ )
+ );
+}
+
+/**
+ * Helper function to populate the second dropdown.
+ *
+ * This would normally be pulling data from the database.
+ *
+ * @param string $key
+ * This will determine which set of options is returned.
+ *
+ * @return array
+ * Dropdown options
+ */
+function _ajax_example_get_second_dropdown_options($key = '') {
+ $options = array(
+ t('String') => drupal_map_assoc(
+ array(
+ t('Violin'),
+ t('Viola'),
+ t('Cello'),
+ t('Double Bass'),
+ )
+ ),
+ t('Woodwind') => drupal_map_assoc(
+ array(
+ t('Flute'),
+ t('Clarinet'),
+ t('Oboe'),
+ t('Bassoon'),
+ )
+ ),
+ t('Brass') => drupal_map_assoc(
+ array(
+ t('Trumpet'),
+ t('Trombone'),
+ t('French Horn'),
+ t('Euphonium'),
+ )
+ ),
+ t('Percussion') => drupal_map_assoc(
+ array(
+ t('Bass Drum'),
+ t('Timpani'),
+ t('Snare Drum'),
+ t('Tambourine'),
+ )
+ ),
+ );
+ if (isset($options[$key])) {
+ return $options[$key];
+ }
+ else {
+ return array();
+ }
+}
+/**
+ * @} End of "defgroup ajax_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.test b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.test
new file mode 100644
index 00000000..9e72f0f5
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example.test
@@ -0,0 +1,75 @@
+ 'Ajax example',
+ 'description' => 'Checks behavior of the Ajax Example',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable module.
+ */
+ public function setUp() {
+ parent::setUp('ajax_example');
+ }
+
+ /**
+ * Check the non-JS version of the "Dynamic Sections" example.
+ */
+ public function testDynamicSectionsNoJs() {
+ // The path to the example form.
+ $path = 'examples/ajax_example/dynamic_sections_no_js';
+ // Confirmation text for right and wrong answers.
+ $wrong = t('Wrong answer. Try again. (Hint: The right answer is "George Washington".)');
+ $right = t('You got the right answer: George Washington');
+ // For each question style, choose some parameters.
+ $params = array(
+ t('Multiple Choice') => array(
+ 'value' => t('Abraham Lincoln'),
+ 'answer' => t('Abraham Lincoln'),
+ 'response' => $wrong,
+ ),
+ t('True/False') => array(
+ 'value' => t('George Washington'),
+ 'answer' => t('George Washington'),
+ 'response' => $right,
+ ),
+ t('Fill-in-the-blanks') => array(
+ 'value' => NULL,
+ 'answer' => t('George Washington'),
+ 'response' => $right,
+ ),
+ );
+ foreach ($params as $style => $q_and_a) {
+ // Submit the initial form.
+ $edit = array('question_type_select' => $style);
+ $this->drupalPost($path, $edit, t('Choose'));
+ $this->assertResponse(200, format_string('Question style "@style" selected.', array('@style' => $style)));
+ // For convenience, make variables out of the entries in $QandA.
+ extract($q_and_a);
+ // Check for the expected input field.
+ $this->assertFieldByName('question', $value);
+ // Now, submit the dynamically generated form.
+ $edit = array('question' => $answer);
+ $this->drupalPost(NULL, $edit, t('Submit your answer'));
+ $this->assertRaw($response, 'Dynamic form has been submitted.');
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_advanced.inc b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_advanced.inc
new file mode 100644
index 00000000..7c61d36c
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_advanced.inc
@@ -0,0 +1,400 @@
+ 'markup',
+ '#markup' => t("
Demonstrates how AJAX commands can be used.
"),
+ );
+
+ // Shows the 'after' command with a callback generating commands.
+ $form['after_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("This shows the Ajax 'after' command. Click to put something below the div that says 'Something can be inserted after this'"),
+ );
+
+ $form['after_command_example_fieldset']['after_command_example'] = array(
+ '#value' => t("AJAX 'After': Click to put something after the div"),
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_after_callback',
+ ),
+ '#suffix' => "
Something can be inserted after this
+
'After' Command Status: Unknown
",
+ );
+
+ // Shows the 'alert' command.
+ $form['alert_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("Demonstrates the AJAX 'alert' command. Click the button."),
+ );
+ $form['alert_command_example_fieldset']['alert_command_example'] = array(
+ '#value' => t("AJAX 'Alert': Click to alert"),
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_alert_callback',
+ ),
+ );
+
+ // Shows the 'append' command.
+ $form['append_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("This shows the Ajax 'append' command. Click to put something below the div that says 'Something can be inserted after this'"),
+ );
+
+ $form['append_command_example_fieldset']['append_command_example'] = array(
+ '#value' => t("AJAX 'Append': Click to append something"),
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_append_callback',
+ ),
+ '#suffix' => "
",
+ );
+
+ // Shows the 'changed' command.
+ $form['changed_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("Demonstrates the AJAX 'changed' command. If region is 'changed', it is marked with CSS. This example also puts an asterisk by changed content."),
+ );
+
+ $form['changed_command_example_fieldset']['changed_command_example'] = array(
+ '#title' => t("AJAX changed: If checked, div is marked as changed."),
+ '#type' => 'checkbox',
+ '#default_value' => FALSE,
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_changed_callback',
+ ),
+ '#suffix' => "
",
+ );
+
+ // Shows the AJAX 'data' command. But there is no use of this information,
+ // as this would require a javascript client to use the data.
+ $form['data_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("Demonstrates the AJAX 'data' command."),
+ );
+
+ $form['data_command_example_fieldset']['data_command_example'] = array(
+ '#title' => t("AJAX data: Set a key/value pair on a selector."),
+ '#type' => 'textfield',
+ '#default_value' => 'color=green',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_data_callback',
+ ),
+ '#suffix' => "
This div should have key='time'/value='a time string' attached.
' . t("This example does a simplest possible autocomplete by username. You'll need a few users on your system for it to make sense.") . '
',
+ );
+
+ $form['user'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose a user (or a people, depending on your usage preference)'),
+ // The autocomplete path is provided in hook_menu in ajax_example.module.
+ '#autocomplete_path' => 'examples/ajax_example/simple_user_autocomplete_callback',
+ );
+
+ return $form;
+}
+
+/**
+ * This is just a copy of user_autocomplete().
+ *
+ * It works simply by searching usernames (and of course in Drupal usernames
+ * are unique, so can be used for identifying a record.)
+ *
+ * The returned $matches array has
+ * * key: string which will be displayed once the autocomplete is selected
+ * * value: the value which will is displayed in the autocomplete pulldown.
+ *
+ * In the simplest cases (see user_autocomplete()) these are the same, and
+ * nothing needs to be done. However, more more complicated autocompletes
+ * require more work. Here we demonstrate the difference by displaying the UID
+ * along with the username in the dropdown.
+ *
+ * In the end, though, we'll be doing something with the value that ends up in
+ * the textfield, so it needs to uniquely identify the record we want to access.
+ * This is demonstrated in ajax_example_unique_autocomplete().
+ *
+ * @param string $string
+ * The string that will be searched.
+ */
+function ajax_example_simple_user_autocomplete_callback($string = "") {
+ $matches = array();
+ if ($string) {
+ $result = db_select('users')
+ ->fields('users', array('name', 'uid'))
+ ->condition('name', db_like($string) . '%', 'LIKE')
+ ->range(0, 10)
+ ->execute();
+ foreach ($result as $user) {
+ // In the simplest case (see user_autocomplete), the key and the value are
+ // the same. Here we'll display the uid along with the username in the
+ // dropdown.
+ $matches[$user->name] = check_plain($user->name) . " (uid=$user->uid)";
+ }
+ }
+
+ drupal_json_output($matches);
+}
+
+/**
+ * An autocomplete form to look up nodes by title.
+ *
+ * An autocomplete form which looks up nodes by title in the node table,
+ * but must keep track of the nid, because titles are certainly not guaranteed
+ * to be unique.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * * @return array
+ * Form array.
+ */
+function ajax_example_unique_autocomplete($form, &$form_state) {
+
+ $form['info'] = array(
+ '#markup' => '
' . t("This example does a node autocomplete by title. The difference between this and a username autocomplete is that the node title may not be unique, so we have to use the nid for uniqueness, placing it in a parseable location in the textfield.") . '
',
+ );
+
+ $form['node'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose a node by title'),
+ // The autocomplete path is provided in hook_menu in ajax_example.module.
+ '#autocomplete_path' => 'examples/ajax_example/unique_node_autocomplete_callback',
+ );
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ );
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Node title validation handler.
+ *
+ * Validate handler to convert our string like "Some node title [3325]" into a
+ * nid.
+ *
+ * In case the user did not actually use the autocomplete or have a valid string
+ * there, we'll try to look up a result anyway giving it our best guess.
+ *
+ * Since the user chose a unique node, we must now use the same one in our
+ * submit handler, which means we need to look in the string for the nid.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ */
+function ajax_example_unique_autocomplete_validate($form, &$form_state) {
+ $title = $form_state['values']['node'];
+ $matches = array();
+
+ // This preg_match() looks for the last pattern like [33334] and if found
+ // extracts the numeric portion.
+ $result = preg_match('/\[([0-9]+)\]$/', $title, $matches);
+ if ($result > 0) {
+ // If $result is nonzero, we found a match and can use it as the index into
+ // $matches.
+ $nid = $matches[$result];
+ // Verify that it's a valid nid.
+ $node = node_load($nid);
+ if (empty($node)) {
+ form_error($form['node'], t('Sorry, no node with nid %nid can be found', array('%nid' => $nid)));
+ return;
+ }
+ }
+ // BUT: Not everybody will have javascript turned on, or they might hit ESC
+ // and not use the autocomplete values offered. In that case, we can attempt
+ // to come up with a useful value. This is not absolutely necessary, and we
+ // *could* just emit a form_error() as below.
+ else {
+ $nid = db_select('node')
+ ->fields('node', array('nid'))
+ ->condition('title', db_like($title) . '%', 'LIKE')
+ ->range(0, 1)
+ ->execute()
+ ->fetchField();
+ }
+
+ // Now, if we somehow found a nid, assign it to the node. If we failed, emit
+ // an error.
+ if (!empty($nid)) {
+ $form_state['values']['node'] = $nid;
+ }
+ else {
+ form_error($form['node'], t('Sorry, no node starting with %title can be found', array('%title' => $title)));
+ }
+}
+
+/**
+ * Submit handler for node lookup unique autocomplete example.
+ *
+ * Here the nid has already been placed in $form_state['values']['node'] by the
+ * validation handler.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ */
+function ajax_example_unique_autocomplete_submit($form, &$form_state) {
+ $node = node_load($form_state['values']['node']);
+ drupal_set_message(t('You found node %nid with title %title', array('%nid' => $node->nid, '%title' => $node->title)));
+}
+
+/**
+ * Autocomplete callback for nodes by title.
+ *
+ * Searches for a node by title, but then identifies it by nid, so the actual
+ * returned value can be used later by the form.
+ *
+ * The returned $matches array has
+ * - key: The title, with the identifying nid in brackets, like "Some node
+ * title [3325]"
+ * - value: the title which will is displayed in the autocomplete pulldown.
+ *
+ * Note that we must use a key style that can be parsed successfully and
+ * unambiguously. For example, if we might have node titles that could have
+ * [3325] in them, then we'd have to use a more restrictive token.
+ *
+ * @param string $string
+ * The string that will be searched.
+ */
+function ajax_example_unique_node_autocomplete_callback($string = "") {
+ $matches = array();
+ if ($string) {
+ $result = db_select('node')
+ ->fields('node', array('nid', 'title'))
+ ->condition('title', db_like($string) . '%', 'LIKE')
+ ->range(0, 10)
+ ->execute();
+ foreach ($result as $node) {
+ $matches[$node->title . " [$node->nid]"] = check_plain($node->title);
+ }
+ }
+
+ drupal_json_output($matches);
+}
+
+/**
+ * Search by title and author.
+ *
+ * In this example, we'll look up nodes by title, but we want only nodes that
+ * have been authored by a particular user. That means that we'll have to make
+ * an autocomplete function which takes a username as an argument, and use
+ * #ajax to change the #autocomplete_path based on the selected user.
+ *
+ * Although the implementation of the validate handler may look complex, it's
+ * just ambitious. The idea here is:
+ * 1. Autcomplete to get a valid username.
+ * 2. Use #ajax to update the node element with a #autocomplete_callback that
+ * gives the context for the username.
+ * 3. Do an autcomplete on the node field that is limited by the username.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_autocomplete($form, &$form_state) {
+
+ $form['intro'] = array(
+ '#markup' => '
' . t("This example uses a user autocomplete to dynamically change a node title autocomplete using #ajax.
+ This is a way to get past the fact that we have no other way to provide context to the autocomplete function.
+ It won't work very well unless you have a few users who have created some content that you can search for.") . '
',
+ );
+
+ $form['author'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose the username that authored nodes you are interested in'),
+ // Since we just need simple user lookup, we can use the simplest function
+ // of them all, user_autocomplete().
+ '#autocomplete_path' => 'user/autocomplete',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_node_by_author_ajax_callback',
+ 'wrapper' => 'autocomplete-by-node-ajax-replace',
+ ),
+ );
+
+ // This form element with autocomplete will be replaced by #ajax whenever the
+ // author changes, allowing the search to be limited by user.
+ $form['node'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose a node by title'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#disabled' => TRUE,
+ );
+
+ // When the author changes in the author field, we'll change the
+ // autocomplete_path to match.
+ if (!empty($form_state['values']['author'])) {
+ $author = user_load_by_name($form_state['values']['author']);
+ if (!empty($author)) {
+ $autocomplete_path = 'examples/ajax_example/node_by_author_autocomplete/' . $author->uid;
+ $form['node']['#autocomplete_path'] = $autocomplete_path;
+ $form['node']['#title'] = t('Choose a node title authored by %author', array('%author' => $author->name));
+ $form['node']['#disabled'] = FALSE;
+ }
+ }
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ );
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * AJAX callback for author form element.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_ajax_callback($form, $form_state) {
+ return $form['node'];
+}
+
+/**
+ * Validate handler to convert our title string into a nid.
+ *
+ * In case the user did not actually use the autocomplete or have a valid string
+ * there, we'll try to look up a result anyway giving it our best guess.
+ *
+ * Since the user chose a unique node, we must now use the same one in our
+ * submit handler, which means we need to look in the string for the nid.
+ *
+ * This handler looks complex because it's ambitious (and tries to punt and
+ * find a node if they've entered a valid username and part of a title), but
+ * you *could* just do a form_error() if nothing were found, forcing people to
+ * use the autocomplete to look up the relevant items.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_autocomplete_validate($form, &$form_state) {
+ $title = $form_state['values']['node'];
+ $author = $form_state['values']['author'];
+ $matches = array();
+
+ // We must have a valid user.
+ $account = user_load_by_name($author);
+ if (empty($account)) {
+ form_error($form['author'], t('You must choose a valid author username'));
+ return;
+ }
+ // This preg_match() looks for the last pattern like [33334] and if found
+ // extracts the numeric portion.
+ $result = preg_match('/\[([0-9]+)\]$/', $title, $matches);
+ if ($result > 0) {
+ // If $result is nonzero, we found a match and can use it as the index into
+ // $matches.
+ $nid = $matches[$result];
+ // Verify that it's a valid nid.
+ $node = node_load($nid);
+ if (empty($node)) {
+ form_error($form['node'], t('Sorry, no node with nid %nid can be found', array('%nid' => $nid)));
+ return;
+ }
+ }
+ // BUT: Not everybody will have javascript turned on, or they might hit ESC
+ // and not use the autocomplete values offered. In that case, we can attempt
+ // to come up with a useful value. This is not absolutely necessary, and we
+ // *could* just emit a form_error() as below. Here we'll find the *first*
+ // matching title and assume that is adequate.
+ else {
+ $nid = db_select('node')
+ ->fields('node', array('nid'))
+ ->condition('uid', $account->uid)
+ ->condition('title', db_like($title) . '%', 'LIKE')
+ ->range(0, 1)
+ ->execute()
+ ->fetchField();
+ }
+
+ // Now, if we somehow found a nid, assign it to the node. If we failed, emit
+ // an error.
+ if (!empty($nid)) {
+ $form_state['values']['node'] = $nid;
+ }
+ else {
+ form_error($form['node'], t('Sorry, no node starting with %title can be found', array('%title' => $title)));
+ }
+}
+
+/**
+ * Submit handler for node lookup unique autocomplete example.
+ *
+ * Here the nid has already been placed in $form_state['values']['node'] by the
+ * validation handler.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_autocomplete_submit($form, &$form_state) {
+ $node = node_load($form_state['values']['node']);
+ $account = user_load($node->uid);
+ drupal_set_message(t('You found node %nid with title !title_link, authored by !user_link',
+ array(
+ '%nid' => $node->nid,
+ '!title_link' => l($node->title, 'node/' . $node->nid),
+ '!user_link' => theme('username', array('account' => $account)),
+ )
+ ));
+}
+
+/**
+ * Autocomplete callback for nodes by title but limited by author.
+ *
+ * Searches for a node by title given the passed-in author username.
+ *
+ * The returned $matches array has
+ * - key: The title, with the identifying nid in brackets, like "Some node
+ * title [3325]"
+ * - value: the title which will is displayed in the autocomplete pulldown.
+ *
+ * Note that we must use a key style that can be parsed successfully and
+ * unambiguously. For example, if we might have node titles that could have
+ * [3325] in them, then we'd have to use a more restrictive token.
+ *
+ * @param int $author_uid
+ * The author username to limit the search.
+ * @param string $string
+ * The string that will be searched.
+ */
+function ajax_example_node_by_author_node_autocomplete_callback($author_uid, $string = "") {
+ $matches = array();
+ if ($author_uid > 0 && trim($string)) {
+ $result = db_select('node')
+ ->fields('node', array('nid', 'title'))
+ ->condition('uid', $author_uid)
+ ->condition('title', db_like($string) . '%', 'LIKE')
+ ->range(0, 10)
+ ->execute();
+ foreach ($result as $node) {
+ $matches[$node->title . " [$node->nid]"] = check_plain($node->title);
+ }
+ }
+
+ drupal_json_output($matches);
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_graceful_degradation.inc b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_graceful_degradation.inc
new file mode 100644
index 00000000..fe7cfb8d
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_graceful_degradation.inc
@@ -0,0 +1,668 @@
+ 'fieldset',
+ );
+ $form['dropdown_first_fieldset']['dropdown_first'] = array(
+ '#type' => 'select',
+ '#title' => 'Instrument Type',
+ '#options' => $options_first,
+ '#attributes' => array('class' => array('enabled-for-ajax')),
+
+ // The '#ajax' property allows us to bind a callback to the server whenever
+ // this form element changes. See ajax_example_autocheckboxes and
+ // ajax_example_dependent_dropdown in ajax_example.module for more details.
+ '#ajax' => array(
+ 'callback' => 'ajax_example_dependent_dropdown_degrades_first_callback',
+ 'wrapper' => 'dropdown-second-replace',
+ ),
+ );
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded. This is for demonstration purposes only.
+ if ($no_js_use) {
+ unset($form['dropdown_first_fieldset']['dropdown_first']['#ajax']);
+ }
+
+ // Since we don't know if the user has js or not, we always need to output
+ // this element, then hide it with with css if javascript is enabled.
+ $form['dropdown_first_fieldset']['continue_to_second'] = array(
+ '#type' => 'submit',
+ '#value' => t('Choose'),
+ '#attributes' => array('class' => array('next-button')),
+ );
+
+ $form['dropdown_second_fieldset'] = array(
+ '#type' => 'fieldset',
+ );
+ $form['dropdown_second_fieldset']['dropdown_second'] = array(
+ '#type' => 'select',
+ '#title' => $options_first[$selected] . ' ' . t('Instruments'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#attributes' => array('class' => array('enabled-for-ajax')),
+ // When the form is rebuilt during processing (either AJAX or multistep),
+ // the $selected variable will now have the new value and so the options
+ // will change.
+ '#options' => _ajax_example_get_second_dropdown_options($selected),
+ );
+ $form['dropdown_second_fieldset']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('OK'),
+ // This class allows attached js file to override the disabled attribute,
+ // since it's not necessary in ajax-enabled form.
+ '#attributes' => array('class' => array('enabled-for-ajax')),
+ );
+
+ // Disable dropdown_second if a selection has not been made on dropdown_first.
+ if (empty($form_state['values']['dropdown_first'])) {
+ $form['dropdown_second_fieldset']['dropdown_second']['#disabled'] = TRUE;
+ $form['dropdown_second_fieldset']['dropdown_second']['#description'] = t('You must make your choice on the first dropdown before changing this second one.');
+ $form['dropdown_second_fieldset']['submit']['#disabled'] = TRUE;
+ }
+
+ return $form;
+}
+
+/**
+ * Submit function for ajax_example_dependent_dropdown_degrades().
+ */
+function ajax_example_dependent_dropdown_degrades_submit($form, &$form_state) {
+
+ // Now handle the case of the next, previous, and submit buttons.
+ // only submit will result in actual submission, all others rebuild.
+ switch ($form_state['triggering_element']['#value']) {
+ case t('OK'):
+ // Submit: We're done.
+ drupal_set_message(t('Your values have been submitted. dropdown_first=@first, dropdown_second=@second', array('@first' => $form_state['values']['dropdown_first'], '@second' => $form_state['values']['dropdown_second'])));
+ return;
+ }
+ // 'Choose' or anything else will cause rebuild of the form and present
+ // it again.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Selects just the second dropdown to be returned for re-rendering.
+ *
+ * @return array
+ * Renderable array (the second dropdown).
+ */
+function ajax_example_dependent_dropdown_degrades_first_callback($form, $form_state) {
+ return $form['dropdown_second_fieldset']['dropdown_second'];
+}
+
+
+/**
+ * Dynamically-enabled form with graceful no-JS degradation.
+ *
+ * Example of a form with portions dynamically enabled or disabled, but
+ * with graceful degradation in the case of no javascript.
+ *
+ * The idea here is that certain parts of the form don't need to be displayed
+ * unless a given option is selected, but then they should be displayed and
+ * configured.
+ *
+ * The third $no_js_use argument is strictly for demonstrating operation
+ * without javascript, without making the user/developer turn off javascript.
+ */
+function ajax_example_dynamic_sections($form, &$form_state, $no_js_use = FALSE) {
+
+ // Attach the CSS and JS we need to show this with and without javascript.
+ // Without javascript we need an extra "Choose" button, and this is
+ // hidden when we have javascript enabled.
+ $form['#attached']['css'] = array(
+ drupal_get_path('module', 'ajax_example') . '/ajax_example.css',
+ );
+ $form['#attached']['js'] = array(
+ drupal_get_path('module', 'ajax_example') . '/ajax_example.js',
+ );
+ $form['description'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('This example demonstrates a form which dynamically creates various sections based on the configuration in the form.
+ It deliberately allows graceful degradation to a non-javascript environment.
+ In a non-javascript environment, the "Choose" button next to the select control
+ is displayed; in a javascript environment it is hidden by the module CSS.
+
The basic idea here is that the form is built up based on
+ the selection in the question_type_select field, and it is built the same
+ whether we are in a javascript/AJAX environment or not.
+
+ Try the AJAX version and the simulated-non-AJAX version.
+ ', array('!ajax_link' => url('examples/ajax_example/dynamic_sections'), '!non_ajax_link' => url('examples/ajax_example/dynamic_sections_no_js'))) . '
',
+ );
+ $form['question_type_select'] = array(
+ '#type' => 'select',
+ '#title' => t('Question style'),
+ '#options' => drupal_map_assoc(
+ array(
+ t('Choose question style'),
+ t('Multiple Choice'),
+ t('True/False'),
+ t('Fill-in-the-blanks'),
+ )
+ ),
+ '#ajax' => array(
+ 'wrapper' => 'questions-fieldset-wrapper',
+ 'callback' => 'ajax_example_dynamic_sections_select_callback',
+ ),
+ );
+ // The CSS for this module hides this next button if JS is enabled.
+ $form['question_type_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Choose'),
+ '#attributes' => array('class' => array('next-button')),
+ // No need to validate when submitting this.
+ '#limit_validation_errors' => array(),
+ '#validate' => array(),
+ );
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ unset($form['question_type_select']['#ajax']);
+ }
+
+ // This fieldset just serves as a container for the part of the form
+ // that gets rebuilt.
+ $form['questions_fieldset'] = array(
+ '#type' => 'fieldset',
+ // These provide the wrapper referred to in #ajax['wrapper'] above.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ if (!empty($form_state['values']['question_type_select'])) {
+
+ $form['questions_fieldset']['question'] = array(
+ '#markup' => t('Who was the first president of the U.S.?'),
+ );
+ $question_type = $form_state['values']['question_type_select'];
+
+ switch ($question_type) {
+ case t('Multiple Choice'):
+ $form['questions_fieldset']['question'] = array(
+ '#type' => 'radios',
+ '#title' => t('Who was the first president of the United States'),
+ '#options' => drupal_map_assoc(
+ array(
+ t('George Bush'),
+ t('Adam McGuire'),
+ t('Abraham Lincoln'),
+ t('George Washington'),
+ )
+ ),
+ );
+ break;
+
+ case t('True/False'):
+ $form['questions_fieldset']['question'] = array(
+ '#type' => 'radios',
+ '#title' => t('Was George Washington the first president of the United States?'),
+ '#options' => array(t('George Washington') => t("True"), 0 => t("False")),
+ '#description' => t('Click "True" if you think George Washington was the first president of the United States.'),
+ );
+ break;
+
+ case t('Fill-in-the-blanks'):
+ $form['questions_fieldset']['question'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Who was the first president of the United States'),
+ '#description' => t('Please type the correct answer to the question.'),
+ );
+ break;
+ }
+
+ $form['questions_fieldset']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit your answer'),
+ );
+ }
+ return $form;
+}
+
+/**
+ * Validation function for ajax_example_dynamic_sections().
+ */
+function ajax_example_dynamic_sections_validate($form, &$form_state) {
+ $answer = $form_state['values']['question'];
+ if ($answer !== t('George Washington')) {
+ form_set_error('question', t('Wrong answer. Try again. (Hint: The right answer is "George Washington".)'));
+ }
+}
+
+/**
+ * Submit function for ajax_example_dynamic_sections().
+ */
+function ajax_example_dynamic_sections_submit($form, &$form_state) {
+ // This is only executed when a button is pressed, not when the AJAXified
+ // select is changed.
+ // Now handle the case of the next, previous, and submit buttons.
+ // Only submit will result in actual submission, all others rebuild.
+ switch ($form_state['triggering_element']['#value']) {
+ case t('Submit your answer'):
+ // Submit: We're done.
+ $form_state['rebuild'] = FALSE;
+ $answer = $form_state['values']['question'];
+
+ // Special handling for the checkbox.
+ if ($answer == 1 && $form['questions_fieldset']['question']['#type'] == 'checkbox') {
+ $answer = $form['questions_fieldset']['question']['#title'];
+ }
+ if ($answer === t('George Washington')) {
+ drupal_set_message(t('You got the right answer: @answer', array('@answer' => $answer)));
+ }
+ else {
+ drupal_set_message(t('Sorry, your answer (@answer) is wrong', array('@answer' => $answer)));
+ }
+ return;
+
+ // Any other form element will cause rebuild of the form and present
+ // it again.
+ case t('Choose'):
+ $form_state['values']['question_type_select'] = $form_state['input']['question_type_select'];
+ // Fall through.
+ default:
+ $form_state['rebuild'] = TRUE;
+ }
+}
+
+/**
+ * Callback for the select element.
+ *
+ * This just selects and returns the questions_fieldset.
+ */
+function ajax_example_dynamic_sections_select_callback($form, $form_state) {
+ return $form['questions_fieldset'];
+}
+
+/**
+ * Wizard form.
+ *
+ * This example is a classic wizard, where a different and sequential form
+ * is presented on each step of the form.
+ *
+ * In the AJAX version, the form is replaced for each wizard section. In the
+ * multistep version, it causes a new page load.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form.
+ * @param bool $no_js_use
+ * Used for this demonstration only. If true means that the form should be
+ * built using a simulated no-javascript approach (ajax.js will not be
+ * loaded.)
+ *
+ * @return array
+ * Form array.
+ */
+function ajax_example_wizard($form, &$form_state, $no_js_use = FALSE) {
+
+ // Provide a wrapper around the entire form, since we'll replace the whole
+ // thing with each submit.
+ $form['#prefix'] = '
';
+ $form['#suffix'] = '
';
+ // We want to deal with hierarchical form values.
+ $form['#tree'] = TRUE;
+ $form['description'] = array(
+ '#markup' => '
' . t('This example is a step-by-step wizard. The AJAX version does it without page reloads; the multistep version is the same code but simulates a non-javascript environment, showing it with page reloads.',
+ array('!ajax' => url('examples/ajax_example/wizard'), '!multistep' => url('examples/ajax_example/wizard_no_js')))
+ . '
',
+ );
+
+ // $form_state['storage'] has no specific drupal meaning, but it is
+ // traditional to keep variables for multistep forms there.
+ $step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step'];
+ $form_state['storage']['step'] = $step;
+
+ switch ($step) {
+ case 1:
+ $form['step1'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Step 1: Personal details'),
+ );
+ $form['step1']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your name'),
+ '#default_value' => empty($form_state['values']['step1']['name']) ? '' : $form_state['values']['step1']['name'],
+ '#required' => TRUE,
+ );
+ break;
+
+ case 2:
+ $form['step2'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Step 2: Street address info'),
+ );
+ $form['step2']['address'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your street address'),
+ '#default_value' => empty($form_state['values']['step2']['address']) ? '' : $form_state['values']['step2']['address'],
+ '#required' => TRUE,
+ );
+ break;
+
+ case 3:
+ $form['step3'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Step 3: City info'),
+ );
+ $form['step3']['city'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your city'),
+ '#default_value' => empty($form_state['values']['step3']['city']) ? '' : $form_state['values']['step3']['city'],
+ '#required' => TRUE,
+ );
+ break;
+ }
+ if ($step == 3) {
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t("Submit your information"),
+ );
+ }
+ if ($step < 3) {
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Next step'),
+ '#ajax' => array(
+ 'wrapper' => 'wizard-form-wrapper',
+ 'callback' => 'ajax_example_wizard_callback',
+ ),
+ );
+ }
+ if ($step > 1) {
+ $form['prev'] = array(
+ '#type' => 'submit',
+ '#value' => t("Previous step"),
+
+ // Since all info will be discarded, don't validate on 'prev'.
+ '#limit_validation_errors' => array(),
+ // #submit is required to use #limit_validation_errors
+ '#submit' => array('ajax_example_wizard_submit'),
+ '#ajax' => array(
+ 'wrapper' => 'wizard-form-wrapper',
+ 'callback' => 'ajax_example_wizard_callback',
+ ),
+ );
+ }
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ // For demonstration only! You don't need this.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ // For demonstration only.
+ unset($form['next']['#ajax']);
+ unset($form['prev']['#ajax']);
+ }
+
+ return $form;
+}
+
+/**
+ * Wizard callback function.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form.
+ *
+ * @return array
+ * Form array.
+ */
+function ajax_example_wizard_callback($form, $form_state) {
+ return $form;
+}
+
+/**
+ * Submit function for ajax_example_wizard.
+ *
+ * In AJAX this is only submitted when the final submit button is clicked,
+ * but in the non-javascript situation, it is submitted with every
+ * button click.
+ */
+function ajax_example_wizard_submit($form, &$form_state) {
+
+ // Save away the current information.
+ $current_step = 'step' . $form_state['storage']['step'];
+ if (!empty($form_state['values'][$current_step])) {
+ $form_state['storage']['values'][$current_step] = $form_state['values'][$current_step];
+ }
+
+ // Increment or decrement the step as needed. Recover values if they exist.
+ if ($form_state['triggering_element']['#value'] == t('Next step')) {
+ $form_state['storage']['step']++;
+ // If values have already been entered for this step, recover them from
+ // $form_state['storage'] to pre-populate them.
+ $step_name = 'step' . $form_state['storage']['step'];
+ if (!empty($form_state['storage']['values'][$step_name])) {
+ $form_state['values'][$step_name] = $form_state['storage']['values'][$step_name];
+ }
+ }
+ if ($form_state['triggering_element']['#value'] == t('Previous step')) {
+ $form_state['storage']['step']--;
+ // Recover our values from $form_state['storage'] to pre-populate them.
+ $step_name = 'step' . $form_state['storage']['step'];
+ $form_state['values'][$step_name] = $form_state['storage']['values'][$step_name];
+ }
+
+ // If they're done, submit.
+ if ($form_state['triggering_element']['#value'] == t('Submit your information')) {
+ $value_message = t('Your information has been submitted:') . ' ';
+ foreach ($form_state['storage']['values'] as $step => $values) {
+ $value_message .= "$step: ";
+ foreach ($values as $key => $value) {
+ $value_message .= "$key=$value, ";
+ }
+ }
+ drupal_set_message($value_message);
+ $form_state['rebuild'] = FALSE;
+ return;
+ }
+
+ // Otherwise, we still have work to do.
+ $form_state['rebuild'] = TRUE;
+}
+
+
+/**
+ * Form with 'add more' and 'remove' buttons.
+ *
+ * This example shows a button to "add more" - add another textfield, and
+ * the corresponding "remove" button.
+ *
+ * It works equivalently with javascript or not, and does the same basic steps
+ * either way.
+ *
+ * The basic idea is that we build the form based on the setting of
+ * $form_state['num_names']. The custom submit functions for the "add-one"
+ * and "remove-one" buttons increment and decrement $form_state['num_names']
+ * and then force a rebuild of the form.
+ *
+ * The $no_js_use argument is simply for demonstration: When set, it prevents
+ * '#ajax' from being set, thus making the example behave as if javascript
+ * were disabled in the browser.
+ */
+function ajax_example_add_more($form, &$form_state, $no_js_use = FALSE) {
+ $form['description'] = array(
+ '#markup' => '
' . t('This example shows an add-more and a remove-last button. The AJAX version does it without page reloads; the non-js version is the same code but simulates a non-javascript environment, showing it with page reloads.',
+ array('!ajax' => url('examples/ajax_example/add_more'), '!multistep' => url('examples/ajax_example/add_more_no_js')))
+ . '
',
+ );
+
+ // Because we have many fields with the same values, we have to set
+ // #tree to be able to access them.
+ $form['#tree'] = TRUE;
+ $form['names_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('People coming to the picnic'),
+ // Set up the wrapper so that AJAX will be able to replace the fieldset.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // Build the fieldset with the proper number of names. We'll use
+ // $form_state['num_names'] to determine the number of textfields to build.
+ if (empty($form_state['num_names'])) {
+ $form_state['num_names'] = 1;
+ }
+ for ($i = 0; $i < $form_state['num_names']; $i++) {
+ $form['names_fieldset']['name'][$i] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ );
+ }
+ $form['names_fieldset']['add_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add one more'),
+ '#submit' => array('ajax_example_add_more_add_one'),
+ // See the examples in ajax_example.module for more details on the
+ // properties of #ajax.
+ '#ajax' => array(
+ 'callback' => 'ajax_example_add_more_callback',
+ 'wrapper' => 'names-fieldset-wrapper',
+ ),
+ );
+ if ($form_state['num_names'] > 1) {
+ $form['names_fieldset']['remove_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Remove one'),
+ '#submit' => array('ajax_example_add_more_remove_one'),
+ '#ajax' => array(
+ 'callback' => 'ajax_example_add_more_callback',
+ 'wrapper' => 'names-fieldset-wrapper',
+ ),
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ // For demonstration only! You don't need this.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ if (!empty($form['names_fieldset']['remove_name']['#ajax'])) {
+ unset($form['names_fieldset']['remove_name']['#ajax']);
+ }
+ unset($form['names_fieldset']['add_name']['#ajax']);
+ }
+
+ return $form;
+}
+
+/**
+ * Callback for both ajax-enabled buttons.
+ *
+ * Selects and returns the fieldset with the names in it.
+ */
+function ajax_example_add_more_callback($form, $form_state) {
+ return $form['names_fieldset'];
+}
+
+/**
+ * Submit handler for the "add-one-more" button.
+ *
+ * Increments the max counter and causes a rebuild.
+ */
+function ajax_example_add_more_add_one($form, &$form_state) {
+ $form_state['num_names']++;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for the "remove one" button.
+ *
+ * Decrements the max counter and causes a form rebuild.
+ */
+function ajax_example_add_more_remove_one($form, &$form_state) {
+ if ($form_state['num_names'] > 1) {
+ $form_state['num_names']--;
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Final submit handler.
+ *
+ * Reports what values were finally set.
+ */
+function ajax_example_add_more_submit($form, &$form_state) {
+ $output = t('These people are coming to the picnic: @names',
+ array(
+ '@names' => implode(', ', $form_state['values']['names_fieldset']['name']),
+ )
+ );
+ drupal_set_message($output);
+}
+/**
+ * @} End of "defgroup ajax_degradation_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_misc.inc b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_misc.inc
new file mode 100644
index 00000000..0b8084bb
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_misc.inc
@@ -0,0 +1,116 @@
+use-ajax class applied to it, so if
+javascript is enabled, ajax.js will try to submit it via an AJAX call instead
+of a normal page load. The URL also contains the '/nojs/' magic string, which
+is stripped if javascript is enabled, allowing the server code to tell by the
+URL whether JS was enabled or not, letting it do different things based on that.");
+ $output = "
" . $explanation . "
";
+ // The use-ajax class is special, so that the link will call without causing
+ // a page reload. Note the /nojs portion of the path - if javascript is
+ // enabled, this part will be stripped from the path before it is called.
+ $link = l(t('Click here'), 'ajax_link_callback/nojs/', array('attributes' => array('class' => array('use-ajax'))));
+ $output .= "
$link
";
+ return $output;
+}
+
+/**
+ * AJAX-enabled link in a renderable array.
+ *
+ * Demonstrates a clickable AJAX-enabled link using a renderable array with the
+ * #ajax property.
+ *
+ * A link that is constructed as a renderable array can have the #ajax property,
+ * which ensures that the link submission is done without a page refresh. The
+ * href of the link is used as the ajax callback, but it degrades gracefully
+ * without JavaScript because if the 'nojs' portion of the href is not stripped
+ * out by js, the callback will return content as required for a full page
+ * reload.
+ *
+ * The necessary JavaScript file, ajax.js, will be included on the page
+ * automatically.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_render_link_ra() {
+ $explanation = "
+The link below has been rendered as an element with the #ajax property, so if
+javascript is enabled, ajax.js will try to submit it via an AJAX call instead
+of a normal page load. The URL also contains the '/nojs/' magic string, which
+is stripped if javascript is enabled, allowing the server code to tell by the
+URL whether JS was enabled or not, letting it do different things based on that.";
+ $build['my_div'] = array(
+ '#markup' => $explanation . '',
+ );
+ $build['ajax_link'] = array(
+ '#type' => 'link',
+ '#title' => t('Click here'),
+ // Note the /nojs portion of the href - if javascript is enabled,
+ // this part will be stripped from the path before it is called.
+ '#href' => 'ajax_link_callback/nojs/',
+ '#id' => 'ajax_link',
+ '#ajax' => array(
+ 'wrapper' => 'myDiv',
+ 'method' => 'html',
+ ),
+ );
+ return $build;
+}
+
+/**
+ * Callback for link example.
+ *
+ * Takes different logic paths based on whether Javascript was enabled.
+ * If $type == 'ajax', it tells this function that ajax.js has rewritten
+ * the URL and thus we are doing an AJAX and can return an array of commands.
+ *
+ * @param string $type
+ * Either 'ajax' or 'nojs. Type is simply the normal URL argument to this URL.
+ *
+ * @return string|array
+ * If $type == 'ajax', returns an array of AJAX Commands.
+ * Otherwise, just returns the content, which will end up being a page.
+ *
+ * @ingroup ajax_example
+ */
+function ajax_link_response($type = 'ajax') {
+ if ($type == 'ajax') {
+ $output = t("This is some content delivered via AJAX");
+ $commands = array();
+ // See ajax_example_advanced.inc for more details on the available commands
+ // and how to use them.
+ $commands[] = ajax_command_append('#myDiv', $output);
+ $page = array('#type' => 'ajax', '#commands' => $commands);
+ ajax_deliver($page);
+ }
+ else {
+ $output = t("This is some content delivered via a page load.");
+ return $output;
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_node_form_alter.inc b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_node_form_alter.inc
new file mode 100644
index 00000000..3dd073bd
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_node_form_alter.inc
@@ -0,0 +1,149 @@
+ 'checkbox',
+ '#title' => t('AJAX Example 1'),
+ '#description' => t('Enable to show second field.'),
+ '#default_value' => $node->ajax_example['example_1'],
+ '#ajax' => array(
+ 'callback' => 'ajax_example_form_node_callback',
+ 'wrapper' => 'ajax-example-form-node',
+ 'effect' => 'fade',
+ ),
+ );
+ $form['container'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // If the state values exist and 'ajax_example_1' state value is 1 or
+ // if the state values don't exist and 'example1' variable is 1 then
+ // display the ajax_example_2 field.
+ if (!empty($form_state['values']['ajax_example_1']) && $form_state['values']['ajax_example_1'] == 1
+ || empty($form_state['values']) && $node->ajax_example['example_1']) {
+
+ $form['container']['ajax_example_2'] = array(
+ '#type' => 'textfield',
+ '#title' => t('AJAX Example 2'),
+ '#description' => t('AJAX Example 2'),
+ '#default_value' => empty($form_state['values']['ajax_example_2']) ? $node->ajax_example['example_2'] : $form_state['values']['ajax_example_2'],
+ );
+ }
+}
+
+/**
+ * Returns changed part of the form.
+ *
+ * @return array
+ * Form API array.
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_form_node_callback($form, $form_state) {
+ return $form['container'];
+}
+
+/**
+ * Implements hook_node_submit().
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_submit($node, $form, &$form_state) {
+ $values = $form_state['values'];
+ // Move the new data into the node object.
+ $node->ajax_example['example_1'] = $values['ajax_example_1'];
+ // Depending on the state of ajax_example_1; it may not exist.
+ $node->ajax_example['example_2'] = isset($values['ajax_example_2']) ? $values['ajax_example_2'] : '';
+}
+
+/**
+ * Implements hook_node_prepare().
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_prepare($node) {
+ if (empty($node->ajax_example)) {
+ // Set default values, since this only runs when adding a new node.
+ $node->ajax_example['example_1'] = 0;
+ $node->ajax_example['example_2'] = '';
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_load($nodes, $types) {
+ $result = db_query('SELECT * FROM {ajax_example_node_form_alter} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)))->fetchAllAssoc('nid');
+
+ foreach ($nodes as &$node) {
+ $node->ajax_example['example_1']
+ = isset($result[$node->nid]->example_1) ?
+ $result[$node->nid]->example_1 : 0;
+ $node->ajax_example['example_2']
+ = isset($result[$node->nid]->example_2) ?
+ $result[$node->nid]->example_2 : '';
+ }
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_insert($node) {
+ if (isset($node->ajax_example)) {
+ db_insert('ajax_example_node_form_alter')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'example_1' => $node->ajax_example['example_1'],
+ 'example_2' => $node->ajax_example['example_2'],
+ ))
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_node_update().
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_update($node) {
+ if (db_select('ajax_example_node_form_alter', 'a')->fields('a')->condition('nid', $node->nid, '=')->execute()->fetchAssoc()) {
+ db_update('ajax_example_node_form_alter')
+ ->fields(array(
+ 'example_1' => $node->ajax_example['example_1'],
+ 'example_2' => $node->ajax_example['example_2'],
+ ))
+ ->condition('nid', $node->nid)
+ ->execute();
+ }
+ else {
+ // Cleaner than doing it again.
+ ajax_example_node_insert($node);
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_delete($node) {
+ db_delete('ajax_example_node_form_alter')
+ ->condition('nid', $node->nid)
+ ->execute();
+}
diff --git a/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_progressbar.inc b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_progressbar.inc
new file mode 100644
index 00000000..b611150c
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/ajax_example/ajax_example_progressbar.inc
@@ -0,0 +1,116 @@
+ '',
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ '#ajax' => array(
+ // Here we set up our AJAX callback handler.
+ 'callback' => 'ajax_example_progressbar_callback',
+ // Tell FormAPI about our progress bar.
+ 'progress' => array(
+ 'type' => 'bar',
+ 'message' => t('Execute..'),
+ // Have the progress bar access this URL path.
+ 'url' => url('examples/ajax_example/progressbar/progress/' . $form_state['time']),
+ // The time interval for the progress bar to check for updates.
+ 'interval' => 1000,
+ ),
+ ),
+ );
+
+ return $form;
+}
+
+/**
+ * Get the progress bar execution status, as JSON.
+ *
+ * This is the menu handler for
+ * examples/ajax_example/progressbar/progress/$time.
+ *
+ * This function is our wholly arbitrary job that we're checking the status for.
+ * In this case, we're reading a system variable that is being updated by
+ * ajax_example_progressbar_callback().
+ *
+ * We set up the AJAX progress bar to check the status every second, so this
+ * will execute about once every second.
+ *
+ * The progress bar JavaScript accepts two values: message and percentage. We
+ * set those in an array and in the end convert it JSON for sending back to the
+ * client-side JavaScript.
+ *
+ * @param int $time
+ * Timestamp.
+ *
+ * @see ajax_example_progressbar_callback()
+ */
+function ajax_example_progressbar_progress($time) {
+ $progress = array(
+ 'message' => t('Starting execute...'),
+ 'percentage' => -1,
+ );
+
+ $completed_percentage = variable_get('example_progressbar_' . $time, 0);
+
+ if ($completed_percentage) {
+ $progress['message'] = t('Executing...');
+ $progress['percentage'] = $completed_percentage;
+ }
+
+ drupal_json_output($progress);
+}
+
+/**
+ * Our submit handler.
+ *
+ * This handler spends some time changing a variable and sleeping, and then
+ * finally returns a form element which marks the #progress-status DIV as
+ * completed.
+ *
+ * While this is occurring, ajax_example_progressbar_progress() will be called
+ * a number of times by the client-sid JavaScript, which will poll the variable
+ * being set here.
+ *
+ * @see ajax_example_progressbar_progress()
+ */
+function ajax_example_progressbar_callback($form, &$form_state) {
+ $variable_name = 'example_progressbar_' . $form_state['time'];
+ $commands = array();
+
+ variable_set($variable_name, 10);
+ sleep(2);
+ variable_set($variable_name, 40);
+ sleep(2);
+ variable_set($variable_name, 70);
+ sleep(2);
+ variable_set($variable_name, 90);
+ sleep(2);
+ variable_del($variable_name);
+
+ $commands[] = ajax_command_html('#progress-status', t('Executed.'));
+
+ return array(
+ '#type' => 'ajax',
+ '#commands' => $commands,
+ );
+}
diff --git a/sites/all/modules/contrib/dev/examples/batch_example/batch_example.info b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.info
new file mode 100644
index 00000000..64760ff7
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.info
@@ -0,0 +1,12 @@
+name = Batch example
+description = An example outlining how a module can define batch operations.
+package = Example modules
+core = 7.x
+files[] = batch_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/batch_example/batch_example.install b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.install
new file mode 100644
index 00000000..4ce14503
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.install
@@ -0,0 +1,85 @@
+fetchField();
+ // A place to store messages during the run.
+ $sandbox['messages'] = array();
+ // Last node read via the query.
+ $sandbox['current_node'] = -1;
+ }
+
+ // Process nodes by groups of 10 (arbitrary value).
+ // 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;
+
+ // Retrieve the next group of nids.
+ $result = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->orderBy('n.nid', 'ASC')
+ ->where('n.nid > :nid', array(':nid' => $sandbox['current_node']))
+ ->extend('PagerDefault')
+ ->limit($limit)
+ ->execute();
+ foreach ($result as $row) {
+ // Here we actually perform a dummy 'update' on the current node.
+ $node = db_query('SELECT nid FROM {node} WHERE nid = :nid', array(':nid' => $row->nid))->fetchField();
+
+ // Update our progress information.
+ $sandbox['progress']++;
+ $sandbox['current_node'] = $row->nid;
+ }
+
+ // 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']);
+
+ // Set up a per-run message; Make a copy of $sandbox so we can change it.
+ // This is simply a debugging stanza to illustrate how to capture status
+ // from each pass through hook_update_N().
+ $sandbox_status = $sandbox;
+ // Don't want them in the output.
+ unset($sandbox_status['messages']);
+ $sandbox['messages'][] = t('$sandbox=') . print_r($sandbox_status, TRUE);
+
+ if ($sandbox['#finished']) {
+ // hook_update_N() may optionally return a string which will be displayed
+ // to the user.
+ $final_message = '
' . implode('
', $sandbox['messages']) . "
";
+ return t('The batch_example demonstration update did what it was supposed to do: @message', array('@message' => $final_message));
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/batch_example/batch_example.module b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.module
new file mode 100644
index 00000000..f16509e9
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.module
@@ -0,0 +1,318 @@
+ 'Batch example',
+ 'description' => 'Example of Drupal batch processing',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('batch_example_simple_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Form builder function to allow choice of which batch to run.
+ */
+function batch_example_simple_form() {
+ $form['description'] = array(
+ '#type' => 'markup',
+ '#markup' => t('This example offers two different batches. The first does 1000 identical operations, each completed in on run; the second does 20 operations, but each takes more than one run to operate if there are more than 5 nodes.'),
+ );
+ $form['batch'] = array(
+ '#type' => 'select',
+ '#title' => 'Choose batch',
+ '#options' => array(
+ 'batch_1' => t('batch 1 - 1000 operations, each loading the same node'),
+ 'batch_2' => t('batch 2 - 20 operations. each one loads all nodes 5 at a time'),
+ ),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Go',
+ );
+
+ // If no nodes, prevent submission.
+ // Find out if we have a node to work with. Otherwise it won't work.
+ $nid = batch_example_lowest_nid();
+ if (empty($nid)) {
+ drupal_set_message(t("You don't currently have any nodes, and this example requires a node to work with. As a result, this form is disabled."));
+ $form['submit']['#disabled'] = TRUE;
+ }
+ return $form;
+}
+
+/**
+ * Submit handler.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form.
+ */
+function batch_example_simple_form_submit($form, &$form_state) {
+ $function = 'batch_example_' . $form_state['values']['batch'];
+
+ // Reset counter for debug information.
+ $_SESSION['http_request_count'] = 0;
+
+ // Execute the function named batch_example_1 or batch_example_2.
+ $batch = $function();
+ batch_set($batch);
+}
+
+
+/**
+ * Batch 1 definition: Load the node with the lowest nid 1000 times.
+ *
+ * This creates an operations array defining what batch 1 should do, including
+ * what it should do when it's finished. In this case, each operation is the
+ * same and by chance even has the same $nid to operate on, but we could have
+ * a mix of different types of operations in the operations array.
+ */
+function batch_example_batch_1() {
+ $nid = batch_example_lowest_nid();
+ $num_operations = 1000;
+ drupal_set_message(t('Creating an array of @num operations', array('@num' => $num_operations)));
+
+ $operations = array();
+ // Set up an operations array with 1000 elements, each doing function
+ // batch_example_op_1.
+ // Each operation in the operations array means at least one new HTTP request,
+ // running Drupal from scratch to accomplish the operation. If the operation
+ // returns with $context['finished'] != TRUE, then it will be called again.
+ // In this example, $context['finished'] is always TRUE.
+ for ($i = 0; $i < $num_operations; $i++) {
+ // Each operation is an array consisting of
+ // - The function to call.
+ // - An array of arguments to that function.
+ $operations[] = array(
+ 'batch_example_op_1',
+ array(
+ $nid,
+ t('(Operation @operation)', array('@operation' => $i)),
+ ),
+ );
+ }
+ $batch = array(
+ 'operations' => $operations,
+ 'finished' => 'batch_example_finished',
+ );
+ return $batch;
+}
+
+/**
+ * Batch operation for batch 1: load a node.
+ *
+ * This is the function that is called on each operation in batch 1.
+ */
+function batch_example_op_1($nid, $operation_details, &$context) {
+ $node = node_load($nid, NULL, TRUE);
+
+ // Store some results for post-processing in the 'finished' callback.
+ // The contents of 'results' will be available as $results in the
+ // 'finished' function (in this example, batch_example_finished()).
+ $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
+
+ // Optional message displayed under the progressbar.
+ $context['message'] = t('Loading node "@title"', array('@title' => $node->title)) . ' ' . $operation_details;
+
+ _batch_example_update_http_requests();
+}
+
+/**
+ * Batch 2 : Prepare a batch definition that will load all nodes 20 times.
+ */
+function batch_example_batch_2() {
+ $num_operations = 20;
+
+ // Give helpful information about how many nodes are being operated on.
+ $node_count = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
+ drupal_set_message(
+ t('There are @node_count nodes so each of the @num operations will require @count HTTP requests.',
+ array(
+ '@node_count' => $node_count,
+ '@num' => $num_operations,
+ '@count' => ceil($node_count / 5),
+ )
+ )
+ );
+
+ $operations = array();
+ // 20 operations, each one loads all nodes.
+ for ($i = 0; $i < $num_operations; $i++) {
+ $operations[] = array(
+ 'batch_example_op_2',
+ array(t('(Operation @operation)', array('@operation' => $i))),
+ );
+ }
+ $batch = array(
+ 'operations' => $operations,
+ 'finished' => 'batch_example_finished',
+ // Message displayed while processing the batch. Available placeholders are:
+ // @current, @remaining, @total, @percentage, @estimate and @elapsed.
+ // These placeholders are replaced with actual values in _batch_process(),
+ // using strtr() instead of t(). The values are determined based on the
+ // number of operations in the 'operations' array (above), NOT by the number
+ // of nodes that will be processed. In this example, there are 20
+ // operations, so @total will always be 20, even though there are multiple
+ // nodes per operation.
+ // Defaults to t('Completed @current of @total.').
+ 'title' => t('Processing batch 2'),
+ 'init_message' => t('Batch 2 is starting.'),
+ 'progress_message' => t('Processed @current out of @total.'),
+ 'error_message' => t('Batch 2 has encountered an error.'),
+ );
+ return $batch;
+}
+
+/**
+ * Batch operation for batch 2 : load all nodes, 5 by five.
+ *
+ * After each group of 5 control is returned to the batch API for later
+ * continuation.
+ */
+function batch_example_op_2($operation_details, &$context) {
+ // Use the $context['sandbox'] at your convenience to store the
+ // information needed to track progression between successive calls.
+ if (empty($context['sandbox'])) {
+ $context['sandbox'] = array();
+ $context['sandbox']['progress'] = 0;
+ $context['sandbox']['current_node'] = 0;
+
+ // Save node count for the termination message.
+ $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
+ }
+
+ // Process nodes by groups of 5 (arbitrary value).
+ // When a group of five 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.
+ // That way even though we're already processing at the operation level
+ // the operation itself is interruptible.
+ $limit = 5;
+
+ // Retrieve the next group of nids.
+ $result = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->orderBy('n.nid', 'ASC')
+ ->where('n.nid > :nid', array(':nid' => $context['sandbox']['current_node']))
+ ->extend('PagerDefault')
+ ->limit($limit)
+ ->execute();
+ foreach ($result as $row) {
+ // Here we actually perform our dummy 'processing' on the current node.
+ $node = node_load($row->nid, NULL, TRUE);
+
+ // Store some results for post-processing in the 'finished' callback.
+ // The contents of 'results' will be available as $results in the
+ // 'finished' function (in this example, batch_example_finished()).
+ $context['results'][] = $node->nid . ' : ' . check_plain($node->title) . ' ' . $operation_details;
+
+ // Update our progress information.
+ $context['sandbox']['progress']++;
+ $context['sandbox']['current_node'] = $node->nid;
+ $context['message'] = check_plain($node->title);
+ }
+
+ // Inform the batch engine that we are not finished,
+ // and provide an estimation of the completion level we reached.
+ if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+ $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']);
+ }
+ _batch_example_update_http_requests();
+}
+
+/**
+ * Batch 'finished' callback used by both batch 1 and batch 2.
+ */
+function batch_example_finished($success, $results, $operations) {
+ if ($success) {
+ // Here we could do something meaningful with the results.
+ // We just display the number of nodes we processed...
+ drupal_set_message(t('@count results processed in @requests HTTP requests.', array('@count' => count($results), '@requests' => _batch_example_get_http_requests())));
+ drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
+ }
+ else {
+ // An error occurred.
+ // $operations contains the operations that remained unprocessed.
+ $error_operation = reset($operations);
+ drupal_set_message(
+ t('An error occurred while processing @operation with arguments : @args',
+ array(
+ '@operation' => $error_operation[0],
+ '@args' => print_r($error_operation[0], TRUE),
+ )
+ ),
+ 'error'
+ );
+ }
+}
+
+/**
+ * Utility function - simply queries and loads the lowest nid.
+ *
+ * @return int|NULL
+ * A nid or NULL if there are no nodes.
+ */
+function batch_example_lowest_nid() {
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->orderBy('n.nid', 'ASC')
+ ->extend('PagerDefault')
+ ->limit(1);
+ $nid = $select->execute()->fetchField();
+ return $nid;
+}
+
+/**
+ * Utility function to increment HTTP requests in a session variable.
+ */
+function _batch_example_update_http_requests() {
+ $_SESSION['http_request_count']++;
+}
+
+/**
+ * Utility function to count the HTTP requests in a session variable.
+ *
+ * @return int
+ * Number of requests.
+ */
+function _batch_example_get_http_requests() {
+ return !empty($_SESSION['http_request_count']) ? $_SESSION['http_request_count'] : 0;
+}
+/**
+ * @} End of "defgroup batch_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/batch_example/batch_example.test b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.test
new file mode 100644
index 00000000..cc93f5aa
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/batch_example/batch_example.test
@@ -0,0 +1,60 @@
+ 'Batch example functionality',
+ 'description' => 'Verify the defined batches.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('batch_example');
+ // Create user.
+ $this->webUser = $this->drupalCreateUser();
+ }
+
+ /**
+ * Login user, create 30 nodes and test both batch examples.
+ */
+ public function testBatchExampleBasic() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Create 30 nodes.
+ for ($count = 0; $count < 30; $count++) {
+ $node = $this->drupalCreateNode();
+ }
+
+ // Launch Batch 1
+ $result = $this->drupalPost('examples/batch_example', array('batch' => 'batch_1'), t('Go'));
+ // Check that 1000 operations were performed.
+ $this->assertText('1000 results processed');
+
+ // Launch Batch 2
+ $result = $this->drupalPost('examples/batch_example', array('batch' => 'batch_2'), t('Go'));
+ // Check that 600 operations were performed.
+ $this->assertText('600 results processed');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/block_example/block_example.info b/sites/all/modules/contrib/dev/examples/block_example/block_example.info
new file mode 100644
index 00000000..8008a378
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/block_example/block_example.info
@@ -0,0 +1,13 @@
+name = Block Example
+description = An example outlining how a module can define blocks.
+package = Example modules
+core = 7.x
+dependencies[] = block
+files[] = block_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/block_example/block_example.install b/sites/all/modules/contrib/dev/examples/block_example/block_example.install
new file mode 100644
index 00000000..50541823
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/block_example/block_example.install
@@ -0,0 +1,14 @@
+ 'block_example_page',
+ 'access callback' => TRUE,
+ 'title' => 'Block Example',
+ );
+ return $items;
+}
+
+/**
+ * Simple page function to explain what the block example is about.
+ */
+function block_example_page() {
+ $page = array(
+ '#type' => 'markup',
+ '#markup' => t('The Block Example provides three sample blocks which demonstrate the various block APIs. To experiment with the blocks, enable and configure them on the block admin page.', array('@url' => url('admin/structure/block'))),
+ );
+ return $page;
+}
+/**
+ * Implements hook_block_info().
+ *
+ * This hook declares what blocks are provided by the module.
+ */
+function block_example_block_info() {
+ // This hook returns an array, each component of which is an array of block
+ // information. The array keys are the 'delta' values used in other block
+ // hooks.
+ //
+ // The required block information is a block description, which is shown
+ // to the site administrator in the list of possible blocks. You can also
+ // provide initial settings for block weight, status, etc.
+ //
+ // Many options are defined in hook_block_info():
+ $blocks['example_configurable_text'] = array(
+ // info: The name of the block.
+ 'info' => t('Example: configurable text string'),
+ // Block caching options (per role, per user, etc.)
+ // DRUPAL_CACHE_PER_ROLE is the default.
+ 'cache' => DRUPAL_CACHE_PER_ROLE,
+ );
+
+ // This sample shows how to provide default settings. In this case we'll
+ // enable the block in the first sidebar and make it visible only on
+ // 'node/*' pages. See the hook_block_info() documentation for these.
+ $blocks['example_empty'] = array(
+ 'info' => t('Example: empty block'),
+ 'status' => TRUE,
+ 'region' => 'sidebar_first',
+ 'visibility' => BLOCK_VISIBILITY_LISTED,
+ 'pages' => 'node/*',
+ );
+
+ $blocks['example_uppercase'] = array(
+ // info: The name of the block.
+ 'info' => t('Example: uppercase this please'),
+ 'status' => TRUE,
+ 'region' => 'sidebar_first',
+ );
+
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_configure().
+ *
+ * This hook declares configuration options for blocks provided by this module.
+ */
+function block_example_block_configure($delta = '') {
+ $form = array();
+ // The $delta parameter tells us which block is being configured.
+ // In this example, we'll allow the administrator to customize
+ // the text of the 'configurable text string' block defined in this module.
+ if ($delta == 'example_configurable_text') {
+ // All we need to provide is the specific configuration options for our
+ // block. Drupal will take care of the standard block configuration options
+ // (block title, page visibility, etc.) and the save button.
+ $form['block_example_string'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Block contents'),
+ '#size' => 60,
+ '#description' => t('This text will appear in the example block.'),
+ '#default_value' => variable_get('block_example_string', t('Some example content.')),
+ );
+ }
+ return $form;
+}
+
+/**
+ * Implements hook_block_save().
+ *
+ * This hook declares how the configured options for a block
+ * provided by this module are saved.
+ */
+function block_example_block_save($delta = '', $edit = array()) {
+ // We need to save settings from the configuration form.
+ // We need to check $delta to make sure we are saving the right block.
+ if ($delta == 'example_configurable_text') {
+ // Have Drupal save the string to the database.
+ variable_set('block_example_string', $edit['block_example_string']);
+ }
+}
+
+/**
+ * Implements hook_block_view().
+ *
+ * This hook generates the contents of the blocks themselves.
+ */
+function block_example_block_view($delta = '') {
+ // The $delta parameter tells us which block is being requested.
+ switch ($delta) {
+ case 'example_configurable_text':
+ // The subject is displayed at the top of the block. Note that it
+ // should be passed through t() for translation. The title configured
+ // for the block using Drupal UI supercedes this one.
+ $block['subject'] = t('Title of first block (example_configurable_text)');
+ // The content of the block is typically generated by calling a custom
+ // function.
+ $block['content'] = block_example_contents($delta);
+ break;
+
+ case 'example_empty':
+ $block['subject'] = t('Title of second block (example_empty)');
+ $block['content'] = block_example_contents($delta);
+ break;
+
+ case 'example_uppercase':
+ $block['subject'] = t("uppercase this please");
+ $block['content'] = t("This block's title will be changed to uppercase. Any other block with 'uppercase' in the subject or title will also be altered. If you change this block's title through the UI to omit the word 'uppercase', it will still be altered to uppercase as the subject key has not been changed.");
+ break;
+ }
+ return $block;
+}
+
+/**
+ * A module-defined block content function.
+ */
+function block_example_contents($which_block) {
+ switch ($which_block) {
+ case 'example_configurable_text':
+ // Modules would typically perform some database queries to fetch the
+ // content for their blocks. Here, we'll just use the variable set in the
+ // block configuration or, if none has set, a default value.
+ // Block content can be returned in two formats: renderable arrays
+ // (as here) are preferred though a simple string will work as well.
+ // Block content created through the UI defaults to a string.
+ $result = array(
+ '#markup' => variable_get('block_example_string',
+ t('A default value. This block was created at %time',
+ array('%time' => date('c'))
+ )
+ ),
+ );
+ return $result;
+
+ case 'example_empty':
+ // It is possible that a block not have any content, since it is
+ // probably dynamically constructed. In this case, Drupal will not display
+ // the block at all. This block will not be displayed.
+ return;
+ }
+}
+
+/*
+ * The following hooks can be used to alter blocks
+ * provided by your own or other modules.
+ */
+
+/**
+ * Implements hook_block_list_alter().
+ *
+ * This hook allows you to add, remove or modify blocks in the block list. The
+ * block list contains the block definitions. This example requires
+ * search module and the search block enabled
+ * to see how this hook implementation works.
+ *
+ * You may also be interested in hook_block_info_alter(), which allows changes
+ * to the behavior of blocks.
+ */
+function block_example_block_list_alter(&$blocks) {
+ // We are going to make the search block sticky on bottom of regions. For
+ // this example, we will modify the block list and append the search block at
+ // the end of the list, so even if the administrator configures the block to
+ // be on the top of the region, it will demote to bottom again.
+ foreach ($blocks as $bid => $block) {
+ if (($block->module == 'search') && ($block->delta == 'form')) {
+ // Remove the block from the list and append to the end.
+ unset($blocks[$bid]);
+ $blocks[$bid] = $block;
+ break;
+ }
+ }
+}
+
+/**
+ * Implements hook_block_view_alter().
+ *
+ * This hook allows you to modify the output of any block in the system.
+ *
+ * In addition, instead of hook_block_view_alter(), which is called for all
+ * blocks, you can also use hook_block_view_MODULE_DELTA_alter() to alter a
+ * specific block. To change only our block using
+ * hook_block_view_MODULE_DELTA_alter, we would use the function:
+ * block_example_block_view_block_example_example_configurable_text_alter()
+ *
+ * We are going to uppercase the subject (the title of the block as shown to the
+ * user) of any block if the string "uppercase" appears in the block title or
+ * subject. Default block titles are set programmatically in the subject key;
+ * titles created through the UI are saved in the title key. This module creates
+ * an example block to demonstrate this effect (default title set
+ * programmatically as subject). You can also demonstrate the effect of this
+ * hook by creating a new block whose title has the string 'uppercase' in it
+ * (set as title through the UI).
+ */
+function block_example_block_view_alter(&$data, $block) {
+ // We'll search for the string 'uppercase'.
+ if ((!empty($block->title) && stristr($block->title, 'uppercase')) || (!empty($data['subject']) && stristr($data['subject'], 'uppercase'))) {
+ // This will uppercase the default title.
+ $data['subject'] = isset($data['subject']) ? drupal_strtoupper($data['subject']) : '';
+ // This will uppercase a title set in the UI.
+ $block->title = isset($block->title) ? drupal_strtoupper($block->title) : '';
+ }
+}
+/**
+ * @} End of "defgroup block_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/block_example/block_example.test b/sites/all/modules/contrib/dev/examples/block_example/block_example.test
new file mode 100644
index 00000000..6698a11f
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/block_example/block_example.test
@@ -0,0 +1,114 @@
+ 'Block example functionality',
+ 'description' => 'Test the configuration options and block created by Block Example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('block_example', 'search');
+ // Create user. Search content permission granted for the search block to
+ // be shown.
+ $this->webUser = $this->drupalCreateUser(
+ array(
+ 'administer blocks',
+ 'search content',
+ 'access contextual links',
+ )
+ );
+ }
+
+ /**
+ * Functional test for our block example.
+ *
+ * Login user, create an example node, and test block functionality through
+ * the admin and user interfaces.
+ */
+ public function testBlockExampleBasic() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Find the blocks in the settings page.
+ $this->drupalGet('admin/structure/block');
+ $this->assertRaw(t('Example: configurable text string'), 'Block configurable-string found.');
+ $this->assertRaw(t('Example: empty block'), 'Block empty-block found.');
+
+ // Verify the default settings for block are processed.
+ $this->assertFieldByName('blocks[block_example_example_empty][region]', 'sidebar_first', 'Empty block is enabled in first sidebar successfully verified.');
+ $this->assertFieldByName('blocks[block_example_example_configurable_text][region]', -1, 'Configurable text block is disabled in first sidebar successfully verified.');
+
+ // Verify that blocks are not shown.
+ $this->drupalGet('/');
+ $this->assertNoRaw(t('Title of first block (example_configurable_text)'), 'Block configurable test not found.');
+ $this->assertNoRaw(t('Title of second block (example_empty)'), 'Block empty not found.');
+
+ // Enable the Configurable text block and verify.
+ $this->drupalPost('admin/structure/block', array('blocks[block_example_example_configurable_text][region]' => 'sidebar_first'), t('Save blocks'));
+ $this->assertFieldByName('blocks[block_example_example_configurable_text][region]', 'sidebar_first', 'Configurable text block is enabled in first sidebar successfully verified.');
+
+ // Verify that blocks are there. Empty block will not be shown, because it
+ // is empty.
+ $this->drupalGet('/');
+ $this->assertRaw(t('Title of first block (example_configurable_text)'), 'Block configurable text found.');
+
+ // Change content of configurable text block.
+ $string = $this->randomName();
+ $this->drupalPost('admin/structure/block/manage/block_example/example_configurable_text/configure', array('block_example_string' => $string), t('Save block'));
+
+ // Verify that new content is shown.
+ $this->drupalGet('/');
+ $this->assertRaw($string, 'Content of configurable text block successfully verified.');
+
+ // Make sure our example uppercased block is shown as altered by the
+ // hook_block_view_alter().
+ $this->assertRaw(t('UPPERCASE THIS PLEASE'));
+
+ // Create a new block and make sure it gets uppercased.
+ $post = array(
+ 'title' => t('configurable block to be uppercased'),
+ 'info' => t('configurable block to be uppercased'),
+ 'body[value]' => t('body of new block'),
+ 'regions[bartik]' => 'sidebar_first',
+ );
+ $this->drupalPost('admin/structure/block/add', $post, t('Save block'));
+ $this->drupalGet('/');
+ $this->assertRaw(('CONFIGURABLE BLOCK TO BE UPPERCASED'));
+
+ // Verify that search block is at the bottom of the region.
+ // Enable the search block on top of sidebar_first.
+ $block_options = array(
+ 'blocks[search_form][region]' => 'sidebar_first',
+ 'blocks[search_form][weight]' => -9,
+ );
+ $this->drupalPost('admin/structure/block', $block_options, t('Save blocks'));
+
+ // The first 'configure block' link should be from our configurable block,
+ // the second from the Navigation menu, and the fifth (#4) from
+ // search block if it was successfully pushed to the bottom.
+ $this->drupalGet('/');
+ $this->clickLink('Configure block', 4);
+ $this->assertText(t("'@search' block", array('@search' => t('Search form'))), 'hook_block_info_alter successfully verified.');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/cache_example/cache_example.info b/sites/all/modules/contrib/dev/examples/cache_example/cache_example.info
new file mode 100644
index 00000000..d7e406dd
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/cache_example/cache_example.info
@@ -0,0 +1,13 @@
+name = Cache Example
+description = An example outlining how to use Cache API.
+package = Example modules
+core = 7.x
+
+files[] = cache_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/cache_example/cache_example.module b/sites/all/modules/contrib/dev/examples/cache_example/cache_example.module
new file mode 100644
index 00000000..8e2d8732
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/cache_example/cache_example.module
@@ -0,0 +1,246 @@
+ 'Cache example',
+ 'description' => 'Example of Drupal Cache API',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('cache_example_page_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Main page for cache_example.
+ *
+ * Displays a page/form which outlines how Drupal's cache works.
+ */
+function cache_example_page_form($form, &$form_state) {
+ // Log execution time.
+ $start_time = microtime(TRUE);
+
+ // Try to load the files count from cache. This function will accept two
+ // arguments:
+ // - cache object name (cid)
+ // - cache bin, the (optional) cache bin (most often a database table) where
+ // the object is to be saved.
+ //
+ // cache_get() returns the cached object or FALSE if object does not exist.
+ if ($cache = cache_get('cache_example_files_count')) {
+ /*
+ * Get cached data. Complex data types will be unserialized automatically.
+ */
+ $files_count = $cache->data;
+ }
+ else {
+ // If there was no cached data available we have to search filesystem.
+ // Recursively get all files from Drupal's folder.
+ $files_count = count(file_scan_directory('.', '/.*/'));
+
+ // Since we have recalculated, we now need to store the new data into cache.
+ // Complex data types will be automatically serialized before being saved
+ // into cache.
+ // Here we use the default setting and create an unexpiring cache item.
+ // See below for an example that creates an expiring cache item.
+ cache_set('cache_example_files_count', $files_count);
+ }
+
+ $end_time = microtime(TRUE);
+ $duration = $end_time - $start_time;
+
+ // Format intro message.
+ $intro_message = '
' . t('This example will search the entire drupal folder and display a count of the files in it.') . ' ';
+ $intro_message .= t('This can take a while, since there are a lot of files to be searched.') . ' ';
+ $intro_message .= t('We will search filesystem just once and save output to the cache. We will use cached data for later requests.') . '
';
+ $intro_message .= '
' . t('Reload this page to see cache in action.', array('@url' => request_uri())) . ' ';
+ $intro_message .= t('You can use the button below to remove cached data.') . '
';
+
+ $form['file_search'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('File search caching'),
+ );
+ $form['file_search']['introduction'] = array(
+ '#markup' => $intro_message,
+ );
+
+ $color = empty($cache) ? 'red' : 'green';
+ $retrieval = empty($cache) ? t('calculated by traversing the filesystem') : t('retrieved from cache');
+
+ $form['file_search']['statistics'] = array(
+ '#type' => 'item',
+ '#markup' => t('%count files exist in this Drupal installation; @retrieval in @time ms. (Source: @source)',
+ array(
+ '%count' => $files_count,
+ '@retrieval' => $retrieval,
+ '@time' => number_format($duration * 1000, 2),
+ '@color' => $color,
+ '@source' => empty($cache) ? t('actual file search') : t('cached'),
+ )
+ ),
+ );
+ $form['file_search']['remove_file_count'] = array(
+ '#type' => 'submit',
+ '#submit' => array('cache_example_form_expire_files'),
+ '#value' => t('Explicitly remove cached file count'),
+ );
+
+ $form['expiration_demo'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Cache expiration settings'),
+ );
+ $form['expiration_demo']['explanation'] = array(
+ '#markup' => t('A cache item can be set as CACHE_PERMANENT, meaning that it will only be removed when explicitly cleared, or it can have an expiration time (a Unix timestamp).'),
+ );
+ $expiring_item = cache_get('cache_example_expiring_item');
+ $item_status = $expiring_item ?
+ t('Cache item exists and is set to expire at %time', array('%time' => $expiring_item->data)) :
+ t('Cache item does not exist');
+ $form['expiration_demo']['current_status'] = array(
+ '#type' => 'item',
+ '#title' => t('Current status of cache item "cache_example_expiring_item"'),
+ '#markup' => $item_status,
+ );
+ $form['expiration_demo']['expiration'] = array(
+ '#type' => 'select',
+ '#title' => t('Time before cache expiration'),
+ '#options' => array(
+ 'never_remove' => t('CACHE_PERMANENT'),
+ -10 => t('Immediate expiration'),
+ 10 => t('10 seconds from form submission'),
+ 60 => t('1 minute from form submission'),
+ 300 => t('5 minutes from form submission'),
+ ),
+ '#default_value' => -10,
+ '#description' => t('Any cache item can be set to only expire when explicitly cleared, or to expire at a given time.'),
+ );
+ $form['expiration_demo']['create_cache_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create a cache item with this expiration'),
+ '#submit' => array('cache_example_form_create_expiring_item'),
+ );
+
+ $form['cache_clearing'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Expire and remove options'),
+ '#description' => t("We have APIs to expire cached items and also to just remove them. Unfortunately, they're all the same API, cache_clear_all"),
+ );
+ $form['cache_clearing']['cache_clear_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Type of cache clearing to do'),
+ '#options' => array(
+ 'expire' => t('Remove items from the "cache" bin that have expired'),
+ 'remove_all' => t('Remove all items from the "cache" bin regardless of expiration (super-wildcard)'),
+ 'remove_wildcard' => t('Remove all items from the "cache" bin that match the pattern "cache_example"'),
+ ),
+ '#default_value' => 'expire',
+ );
+ // Submit button to clear cached data.
+ $form['cache_clearing']['clear_expired'] = array(
+ '#type' => 'submit',
+ '#value' => t('Clear or expire cache'),
+ '#submit' => array('cache_example_form_cache_clearing'),
+ '#access' => user_access('administer site configuration'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler that explicitly clears cache_example_files_count from cache.
+ */
+function cache_example_form_expire_files($form, &$form_state) {
+ // Clear cached data. This function will delete cached object from cache bin.
+ //
+ // The first argument is cache id to be deleted. Since we've provided it
+ // explicitly, it will be removed whether or not it has an associated
+ // expiration time. The second argument (required here) is the cache bin.
+ // Using cache_clear_all() explicitly in this way
+ // forces removal of the cached item.
+ cache_clear_all('cache_example_files_count', 'cache');
+
+ // Display message to the user.
+ drupal_set_message(t('Cached data key "cache_example_files_count" was cleared.'), 'status');
+}
+
+/**
+ * Submit handler to create a new cache item with specified expiration.
+ */
+function cache_example_form_create_expiring_item($form, &$form_state) {
+ $interval = $form_state['values']['expiration'];
+ if ($interval == 'never_remove') {
+ $expiration = CACHE_PERMANENT;
+ $expiration_friendly = t('Never expires');
+ }
+ else {
+ $expiration = time() + $interval;
+ $expiration_friendly = format_date($expiration);
+ }
+ // Set the expiration to the actual Unix timestamp of the end of the required
+ // interval.
+ cache_set('cache_example_expiring_item', $expiration_friendly, 'cache', $expiration);
+ drupal_set_message(t('cache_example_expiring_item was set to expire at %time', array('%time' => $expiration_friendly)));
+}
+
+/**
+ * Submit handler to demonstrate the various uses of cache_clear_all().
+ */
+function cache_example_form_cache_clearing($form, &$form_state) {
+ switch ($form_state['values']['cache_clear_type']) {
+ case 'expire':
+ // Here we'll remove all cache keys in the 'cache' bin that have expired.
+ cache_clear_all(NULL, 'cache');
+ drupal_set_message(t('cache_clear_all(NULL, "cache") was called, removing any expired cache items.'));
+ break;
+
+ case 'remove_all':
+ // This removes all keys in a bin using a super-wildcard. This
+ // has nothing to do with expiration. It's just brute-force removal.
+ cache_clear_all('*', 'cache', TRUE);
+ drupal_set_message(t('ALL entries in the "cache" bin were removed with cache_clear_all("*", "cache", TRUE).'));
+ break;
+
+ case 'remove_wildcard':
+ // We can also explicitly remove all cache items whose cid begins with
+ // 'cache_example' by using a wildcard. This again is brute-force
+ // removal, not expiration.
+ cache_clear_all('cache_example', 'cache', TRUE);
+ drupal_set_message(t('Cache entries whose cid began with "cache_example" in the "cache" bin were removed with cache_clear_all("cache_example", "cache", TRUE).'));
+ break;
+ }
+}
+
+/**
+ * @} End of "defgroup cache_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/cache_example/cache_example.test b/sites/all/modules/contrib/dev/examples/cache_example/cache_example.test
new file mode 100644
index 00000000..a445efaa
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/cache_example/cache_example.test
@@ -0,0 +1,81 @@
+ 'Cache example functionality',
+ 'description' => 'Test the Cache Example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable module.
+ */
+ public function setUp() {
+ parent::setUp('cache_example');
+ }
+
+ /**
+ * Functional tests for cache_example.
+ *
+ * Load cache example page and test if displaying uncached version. Reload
+ * once again and test if displaying cached version. Find reload link and
+ * click on it. Clear cache at the end and test if displaying uncached
+ * version again.
+ */
+ public function testCacheExampleBasic() {
+ // We need administrative privileges to clear the cache.
+ $admin_user = $this->drupalCreateUser(array('administer site configuration'));
+ $this->drupalLogin($admin_user);
+
+ // Get uncached output of cache example page and assert some things to be
+ // sure.
+ $this->drupalGet('examples/cache_example');
+ $this->assertText('Source: actual file search');
+ // Reload the page; the number should be cached.
+ $this->drupalGet('examples/cache_example');
+ $this->assertText('Source: cached');
+
+ // Now push the button to remove the count.
+ $this->drupalPost('examples/cache_example', array(), t('Explicitly remove cached file count'));
+ $this->assertText('Source: actual file search');
+
+ // Create a cached item. First make sure it doesn't already exist.
+ $this->assertText('Cache item does not exist');
+ $this->drupalPost('examples/cache_example', array('expiration' => -10), t('Create a cache item with this expiration'));
+ // We should now have an already-expired item.
+ $this->assertText('Cache item exists and is set to expire');
+ // Now do the expiration operation.
+ $this->drupalPost('examples/cache_example', array('cache_clear_type' => 'expire'), t('Clear or expire cache'));
+ // And verify that it was removed.
+ $this->assertText('Cache item does not exist');
+
+ // Create a cached item. This time we'll make it not expire.
+ $this->drupalPost('examples/cache_example', array('expiration' => 'never_remove'), t('Create a cache item with this expiration'));
+ // We should now have an never-remove item.
+ $this->assertText('Cache item exists and is set to expire at Never expires');
+ // Now do the expiration operation.
+ $this->drupalPost('examples/cache_example', array('cache_clear_type' => 'expire'), t('Clear or expire cache'));
+ // And verify that it was not removed.
+ $this->assertText('Cache item exists and is set to expire at Never expires');
+ // Now do full removal.
+ $this->drupalPost('examples/cache_example', array('cache_clear_type' => 'remove_wildcard'), t('Clear or expire cache'));
+ // And verify that it was removed.
+ $this->assertText('Cache item does not exist');
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual-links-example-object.tpl.php b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual-links-example-object.tpl.php
new file mode 100644
index 00000000..4a073d46
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual-links-example-object.tpl.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.info b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.info
new file mode 100644
index 00000000..04a7f775
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.info
@@ -0,0 +1,12 @@
+name = Contextual links example
+description = Demonstrates how to use contextual links for enhancing the user experience.
+package = Example modules
+core = 7.x
+files[] = contextual_links_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.module b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.module
new file mode 100644
index 00000000..e80c79a7
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.module
@@ -0,0 +1,388 @@
+ path. If the path
+ // you are adding corresponds to a commonly performed action on the node, you
+ // can choose to expose it as a contextual link. Since the Node module
+ // already has code to display all contextual links underneath the node/
+ // path (such as "Edit" and "Delete") when a node is being rendered outside
+ // of its own page (for example, when a teaser of the node is being displayed
+ // on the front page of the site), you only need to inform Drupal's menu
+ // system that your path is a contextual link also, and it will automatically
+ // appear with the others. In the example below, we add a contextual link
+ // named "Example action" to the list.
+ $items['node/%node/example-action'] = array(
+ 'title' => 'Example action',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('contextual_links_example_node_action_form', 1),
+ 'access callback' => TRUE,
+ // To be displayed as a contextual link, a menu item should be defined as
+ // one of the node's local tasks.
+ 'type' => MENU_LOCAL_TASK,
+ // To make the local task display as a contextual link, specify the
+ // optional 'context' argument. The most common method is to set both
+ // MENU_CONTEXT_PAGE and MENU_CONTEXT_INLINE (shown below), which causes
+ // the link to display as both a tab on the node page and as an entry in
+ // the contextual links dropdown. This is recommended for most cases
+ // because not all users who have permission to visit the "Example action"
+ // page will necessarily have access to contextual links, and they still
+ // need a way to get to the page via the user interface.
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ // If we give the item a large weight, we can make it display as the last
+ // tab on the page, as well as the last item inside the contextual links
+ // dropdown.
+ 'weight' => 80,
+ );
+
+ // Second example (attaching contextual links to a block):
+ //
+ // If your module provides content that is displayed in a block, you can
+ // attach contextual links to the block that allow actions to be performed on
+ // it. This is useful for administrative pages that affect the content
+ // wherever it is displayed or used on the site. For configuration options
+ // that only affect the appearance of the content in the block itself, it is
+ // better to implement hook_block_configure() rather than creating a separate
+ // administrative page (this allows your options to appear when an
+ // administrator clicks the existing "Configure block" contextual link
+ // already provided by the Block module).
+ //
+ // In the code below, we assume that your module has a type of object
+ // ("contextual links example object") that will be displayed in a block. The
+ // code below defines menu items for this object using a standard pattern,
+ // with "View" and "Edit object" as the object's local tasks, and makes the
+ // "Edit object" item display as a contextual link in addition to a tab. Once
+ // the contextual links are defined here, additional steps are required to
+ // actually display the content in a block and attach the contextual links to
+ // the block itself. This occurs in contextual_links_example_block_info() and
+ // contextual_links_example_block_view().
+ $items['examples/contextual-links/%contextual_links_example_object'] = array(
+ 'title' => 'Contextual links example object',
+ 'page callback' => 'contextual_links_example_object_page',
+ 'page arguments' => array(2),
+ 'access callback' => TRUE,
+ );
+ $items['examples/contextual-links/%contextual_links_example_object/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['examples/contextual-links/%contextual_links_example_object/edit'] = array(
+ 'title' => 'Edit object',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('contextual_links_example_object_edit_form', 2),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ // As in our first example, this is the line of code that makes "Edit
+ // "object" display as a contextual link in addition to as a tab.
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+
+ // Third example (attaching contextual links directly to your module's
+ // content):
+ //
+ // Sometimes your module may want to display its content in an arbitrary
+ // location and attach contextual links there. For example, you might
+ // display your content in a listing on its own page and then attach the
+ // contextual links directly to each piece of content in the listing. Here,
+ // we will reuse the menu items and contextual links that were defined for
+ // our example object above, and display them in a listing in
+ // contextual_links_overview_page().
+ $items['examples/contextual-links'] = array(
+ 'title' => 'Contextual Links Example',
+ 'page callback' => 'contextual_links_overview_page',
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Menu loader callback for the object defined by this module.
+ *
+ * @param int $id
+ * The ID of the object to load.
+ *
+ * @return object|FALSE
+ * A fully loaded object, or FALSE if the object does not exist.
+ */
+function contextual_links_example_object_load($id) {
+ // In a real use case, this function might load an object from the database.
+ // For the sake of this example, we just define a stub object with a basic
+ // title and content for any numeric ID that is passed in.
+ if (is_numeric($id)) {
+ $object = new stdClass();
+ $object->id = $id;
+ $object->title = t('Title for example object @id', array('@id' => $id));
+ $object->content = t('This is the content of example object @id.', array('@id' => $id));
+ return $object;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implements hook_block_info().
+ */
+function contextual_links_example_block_info() {
+ // Define the block that will display our module's content.
+ $blocks['example']['info'] = t('Contextual links example block');
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function contextual_links_example_block_view($delta = '') {
+ if ($delta == 'example') {
+ // Display our module's content inside a block. In a real use case, we
+ // might define a new block for each object that exists. For the sake of
+ // this example, though, we only define one block and hardcode it to always
+ // display object #1.
+ $id = 1;
+ $object = contextual_links_example_object_load($id);
+ $block['subject'] = t('Contextual links example block for object @id', array('@id' => $id));
+ $block['content'] = array(
+ // In order to attach contextual links, the block's content must be a
+ // renderable array. (Normally this would involve themed output using
+ // #theme, but for simplicity we just use HTML markup directly here.)
+ '#type' => 'markup',
+ '#markup' => filter_xss($object->content),
+ // Contextual links are attached to the block array using the special
+ // #contextual_links property. The #contextual_links property contains an
+ // array, keyed by the name of each module that is attaching contextual
+ // links to it.
+ '#contextual_links' => array(
+ 'contextual_links_example' => array(
+ // Each element is itself an array, containing two elements which are
+ // combined together to form the base path whose contextual links
+ // should be attached. The two elements are split such that the first
+ // is the static part of the path and the second is the dynamic part.
+ // (This split is for performance reasons.) For example, the code
+ // below tells Drupal to load the menu item corresponding to the path
+ // "examples/contextual-links/$id" and attach all this item's
+ // contextual links (which were defined in hook_menu()) to the object
+ // when it is rendered. If the contextual links you are attaching
+ // don't have any dynamic elements in their path, you can pass an
+ // empty array as the second element.
+ 'examples/contextual-links',
+ array($id),
+ ),
+ ),
+ );
+ // Since we are attaching our contextual links to a block, and the Block
+ // module takes care of rendering the block in such a way that contextual
+ // links are supported, we do not need to do anything else here. When the
+ // appropriate conditions are met, the contextual links we have defined
+ // will automatically appear attached to the block, next to the "Configure
+ // block" link that the Block module itself provides.
+ return $block;
+ }
+}
+
+/**
+ * Menu callback; displays a listing of objects defined by this module.
+ *
+ * @see contextual_links_example_theme()
+ * @see contextual-links-example-object.tpl.php
+ * @see contextual_links_example_block_view()
+ */
+function contextual_links_overview_page() {
+ $build = array();
+
+ // For simplicity, we will hardcode this example page to list five of our
+ // module's objects.
+ for ($id = 1; $id <= 5; $id++) {
+ $object = contextual_links_example_object_load($id);
+ $build[$id] = array(
+ // To support attaching contextual links to an object that we are
+ // displaying on our own, the object must be themed in a particular way.
+ // See contextual_links_example_theme() and
+ // contextual-links-example-object.tpl.php for more discussion.
+ '#theme' => 'contextual_links_example_object',
+ '#object' => $object,
+ // Contextual links are attached to the block using the special
+ // #contextual_links property. See contextual_links_example_block_view()
+ // for discussion of the syntax used here.
+ '#contextual_links' => array(
+ 'contextual_links_example' => array(
+ 'examples/contextual-links',
+ array($id),
+ ),
+ ),
+ );
+ }
+
+ return $build;
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * @see template_preprocess_contextual_links_example_object()
+ */
+function contextual_links_example_theme() {
+ // The core Contextual Links module imposes two restrictions on how an object
+ // must be themed in order for it to display the object's contextual links in
+ // the user interface:
+ // - The object must use a template file rather than a theme function. See
+ // contextual-links-example-object.tpl.php for more information on how the
+ // template file should be structured.
+ // - The first variable passed to the template must be a renderable array. In
+ // this case, we accomplish that via the most common method, by passing a
+ // single renderable element.
+ return array(
+ 'contextual_links_example_object' => array(
+ 'template' => 'contextual-links-example-object',
+ 'render element' => 'element',
+ ),
+ );
+}
+
+/**
+ * Process variables for contextual-links-example-object.tpl.php.
+ *
+ * @see contextual_links_overview_page()
+ */
+function template_preprocess_contextual_links_example_object(&$variables) {
+ // Here we take the object that is being themed and define some useful
+ // variables that we will print in the template file.
+ $variables['title'] = filter_xss($variables['element']['#object']->title);
+ $variables['content'] = filter_xss($variables['element']['#object']->content);
+}
+
+/**
+ * Menu callback; displays an object defined by this module on its own page.
+ *
+ * @see contextual_links_overview_page()
+ */
+function contextual_links_example_object_page($object) {
+ // Here we render the object but without the #contextual_links property,
+ // since we don't want contextual links to appear when the object is already
+ // being displayed on its own page.
+ $build = array(
+ '#theme' => 'contextual_links_example_object',
+ '#object' => $object,
+ );
+
+ return $build;
+}
+
+/**
+ * Form callback; display the form for editing our module's content.
+ *
+ * @ingroup forms
+ * @see contextual_links_example_object_edit_form_submit()
+ */
+function contextual_links_example_object_edit_form($form, &$form_state, $object) {
+ $form['text'] = array(
+ '#markup' => t('This is the page that would allow you to edit object @id.', array('@id' => $object->id)),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $form['object_id'] = array(
+ '#type' => 'value',
+ '#value' => $object->id,
+ );
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for contextual_links_example_object_edit_form().
+ */
+function contextual_links_example_object_edit_form_submit($form, &$form_state) {
+ drupal_set_message(t('Object @id was edited.', array('@id' => $form_state['values']['object_id'])));
+}
+
+/**
+ * Form callback; display the form for performing an example action on a node.
+ *
+ * @ingroup forms
+ * @see contextual_links_example_node_action_form_submit()
+ */
+function contextual_links_example_node_action_form($form, &$form_state, $node) {
+ $form['text'] = array(
+ '#markup' => t('This is the page that would allow you to perform an example action on node @nid.', array('@nid' => $node->nid)),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $form['nid'] = array(
+ '#type' => 'value',
+ '#value' => $node->nid,
+ );
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for contextual_links_example_node_action_form().
+ */
+function contextual_links_example_node_action_form_submit($form, &$form_state) {
+ drupal_set_message(t('The example action was performed on node @nid.', array('@nid' => $form_state['values']['nid'])));
+}
+/**
+ * @} End of "defgroup contextual_links_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.test b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.test
new file mode 100644
index 00000000..a7abc0ed
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/contextual_links_example/contextual_links_example.test
@@ -0,0 +1,62 @@
+ 'Contextual links example functionality',
+ 'description' => 'Tests the behavior of the contextual links provided by the Contextual links example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('contextual', 'contextual_links_example');
+ $this->webUser = $this->drupalCreateUser(array('access contextual links', 'administer blocks'));
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Test the various contextual links that this module defines and displays.
+ */
+ public function testContextualLinksExample() {
+ // Create a node and promote it to the front page. Then view the front page
+ // and verify that the "Example action" contextual link works.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
+ $this->drupalGet('');
+ $this->clickLink(t('Example action'));
+ $this->assertUrl('node/' . $node->nid . '/example-action', array('query' => array('destination' => 'node')));
+
+ // Visit our example overview page and click the third contextual link.
+ // This should take us to a page for editing the third object we defined.
+ $this->drupalGet('examples/contextual-links');
+ $this->clickLink('Edit object', 2);
+ $this->assertUrl('examples/contextual-links/3/edit', array('query' => array('destination' => 'examples/contextual-links')));
+
+ // Enable our module's block, go back to the front page, and click the
+ // "Edit object" contextual link that we expect to be there.
+ $edit['blocks[contextual_links_example_example][region]'] = 'sidebar_first';
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ $this->drupalGet('');
+ $this->clickLink('Edit object');
+ $this->assertUrl('examples/contextual-links/1/edit', array('query' => array('destination' => 'node')));
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/cron_example/cron_example.info b/sites/all/modules/contrib/dev/examples/cron_example/cron_example.info
new file mode 100644
index 00000000..d96563fd
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/cron_example/cron_example.info
@@ -0,0 +1,12 @@
+name = Cron example
+description = Demonstrates hook_cron() and related features
+package = Example modules
+core = 7.x
+files[] = cron_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/cron_example/cron_example.module b/sites/all/modules/contrib/dev/examples/cron_example/cron_example.module
new file mode 100644
index 00000000..6e9c2bd4
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/cron_example/cron_example.module
@@ -0,0 +1,266 @@
+ 'Cron Example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('cron_example_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * The form to provide a link to cron.php.
+ */
+function cron_example_form($form, &$form_state) {
+ $form['status'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Cron status information'),
+ );
+ $form['status']['intro'] = array(
+ '#markup' => '
' . t('The cron example demonstrates hook_cron() and hook_cron_queue_info() processing. If you have administrative privileges you can run cron from this page and see the results.') . '
' . t('There are currently %queue_1 items in queue 1 and %queue_2 items in queue 2',
+ array(
+ '%queue_1' => $queue_1->numberOfItems(),
+ '%queue_2' => $queue_2->numberOfItems(),
+ )) . '
',
+ );
+ $form['cron_queue_setup']['num_items'] = array(
+ '#type' => 'select',
+ '#title' => t('Number of items to add to queue'),
+ '#options' => drupal_map_assoc(array(1, 5, 10, 100, 1000)),
+ '#default_value' => 5,
+ );
+ $form['cron_queue_setup']['queue'] = array(
+ '#type' => 'radios',
+ '#title' => t('Queue to add items to'),
+ '#options' => array(
+ 'cron_example_queue_1' => t('Queue 1'),
+ 'cron_example_queue_2' => t('Queue 2'),
+ ),
+ '#default_value' => 'cron_example_queue_1',
+ );
+ $form['cron_queue_setup']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add jobs to queue'),
+ '#submit' => array('cron_example_add_jobs_to_queue'),
+ );
+
+ $form['configuration'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Configuration of cron_example_cron()'),
+ );
+ $form['configuration']['cron_example_interval'] = array(
+ '#type' => 'select',
+ '#title' => t('Cron interval'),
+ '#description' => t('Time after which cron_example_cron will respond to a processing request.'),
+ '#default_value' => variable_get('cron_example_interval', 60 * 60),
+ '#options' => array(
+ 60 => t('1 minute'),
+ 300 => t('5 minutes'),
+ 3600 => t('1 hour'),
+ 60 * 60 * 24 => t('1 day'),
+ ),
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Allow user to directly execute cron, optionally forcing it.
+ */
+function cron_example_form_cron_run_submit($form, &$form_state) {
+ if (!empty($form_state['values']['cron_reset'])) {
+ variable_set('cron_example_next_execution', 0);
+ }
+
+ // We don't usually use globals in this way. This is used here only to
+ // make it easy to tell if cron was run by this form.
+ $GLOBALS['cron_example_show_status_message'] = TRUE;
+ if (drupal_cron_run()) {
+ drupal_set_message(t('Cron ran successfully.'));
+ }
+ else {
+ drupal_set_message(t('Cron run failed.'), 'error');
+ }
+}
+
+/**
+ * Submit function used to add the items to the queue.
+ */
+function cron_example_add_jobs_to_queue($form, &$form_state) {
+ $queue = $form_state['values']['queue'];
+ $num_items = $form_state['values']['num_items'];
+
+ $queue = DrupalQueue::get($queue);
+ for ($i = 1; $i <= $num_items; $i++) {
+ $item = new stdClass();
+ $item->created = time();
+ $item->sequence = $i;
+ $queue->createItem($item);
+ }
+}
+/**
+ * Implements hook_cron().
+ *
+ * hook_cron() is the traditional (pre-Drupal 7) hook for doing "background"
+ * processing. It gets called every time the Drupal cron runs and must decide
+ * what it will do.
+ *
+ * In this example, it does a watchdog() call after the time named in
+ * the variable 'cron_example_next_execution' has arrived, and then it
+ * resets that variable to a time in the future.
+ */
+function cron_example_cron() {
+ // Default to an hourly interval. Of course, cron has to be running at least
+ // hourly for this to work.
+ $interval = variable_get('cron_example_interval', 60 * 60);
+ // We usually don't want to act every time cron runs (which could be every
+ // minute) so keep a time for the next run in a variable.
+ if (time() >= variable_get('cron_example_next_execution', 0)) {
+ // This is a silly example of a cron job.
+ // It just makes it obvious that the job has run without
+ // making any changes to your database.
+ watchdog('cron_example', 'cron_example ran');
+ if (!empty($GLOBALS['cron_example_show_status_message'])) {
+ drupal_set_message(t('cron_example executed at %time', array('%time' => date_iso8601(time(0)))));
+ }
+ variable_set('cron_example_next_execution', time() + $interval);
+ }
+}
+
+
+/**
+ * Implements hook_cron_queue_info().
+ *
+ * hook_cron_queue_info() and family are new since Drupal 7, and allow any
+ * process to add work to the queue to be acted on when cron runs. Queues are
+ * described and worker callbacks are provided, and then only the worker
+ * callback needs to be implemented.
+ *
+ * All the details of queue use are done by the cron_queue implementation, so
+ * one doesn't need to know much about DrupalQueue().
+ *
+ * @see queue_example.module
+ */
+function cron_example_cron_queue_info() {
+ $queues['cron_example_queue_1'] = array(
+ 'worker callback' => 'cron_example_queue_1_worker',
+ // One second max runtime per cron run.
+ 'time' => 1,
+ );
+ $queues['cron_example_queue_2'] = array(
+ 'worker callback' => 'cron_example_queue_2_worker',
+ 'time' => 10,
+ );
+ return $queues;
+}
+
+/**
+ * Simple worker for our queues.
+ *
+ * @param object $item
+ * Any object to be worked on.
+ */
+function cron_example_queue_1_worker($item) {
+ cron_example_queue_report_work(1, $item);
+}
+
+/**
+ * Simple worker for our queues.
+ *
+ * @param object $item
+ * Any object to be worked on.
+ */
+function cron_example_queue_2_worker($item) {
+ cron_example_queue_report_work(2, $item);
+}
+
+/**
+ * Simple reporter for the workers.
+ *
+ * @param int $worker
+ * Worker number.
+ * @param object $item
+ * The $item which was stored in the cron queue.
+ */
+function cron_example_queue_report_work($worker, $item) {
+ if (!empty($GLOBALS['cron_example_show_status_message'])) {
+ drupal_set_message(
+ t('Queue @worker worker processed item with sequence @sequence created at @time',
+ array(
+ '@worker' => $worker,
+ '@sequence' => $item->sequence,
+ '@time' => date_iso8601($item->created),
+ )
+ )
+ );
+ }
+ watchdog('cron_example', 'Queue @worker worker processed item with sequence @sequence created at @time',
+ array(
+ '@worker' => $worker,
+ '@sequence' => $item->sequence,
+ '@time' => date_iso8601($item->created),
+ )
+ );
+}
+
+/**
+ * @} End of "defgroup cron_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/cron_example/cron_example.test b/sites/all/modules/contrib/dev/examples/cron_example/cron_example.test
new file mode 100644
index 00000000..8dc8104e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/cron_example/cron_example.test
@@ -0,0 +1,84 @@
+ 'Cron example functionality',
+ 'description' => 'Test the functionality of the Cron Example.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('cron_example');
+ // Create user. Search content permission granted for the search block to
+ // be shown.
+ $this->webUser = $this->drupalCreateUser(array('administer site configuration'));
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Test running cron through the user interface.
+ */
+ public function testCronExampleBasic() {
+ // Pretend that cron has never been run (even though simpletest seems to
+ // run it once...)
+ variable_set('cron_example_next_execution', 0);
+ $this->drupalGet('examples/cron_example');
+
+ // Initial run should cause cron_example_cron() to fire.
+ $post = array();
+ $this->drupalPost('examples/cron_example', $post, t('Run cron now'));
+ $this->assertText(t('cron_example executed at'));
+
+ // Forcing should also cause cron_example_cron() to fire.
+ $post['cron_reset'] = TRUE;
+ $this->drupalPost(NULL, $post, t('Run cron now'));
+ $this->assertText(t('cron_example executed at'));
+
+ // But if followed immediately and not forced, it should not fire.
+ $post['cron_reset'] = FALSE;
+ $this->drupalPost(NULL, $post, t('Run cron now'));
+ $this->assertNoText(t('cron_example executed at'));
+
+ $this->assertText(t('There are currently 0 items in queue 1 and 0 items in queue 2'));
+ $post = array(
+ 'num_items' => 5,
+ 'queue' => 'cron_example_queue_1',
+ );
+ $this->drupalPost(NULL, $post, t('Add jobs to queue'));
+ $this->assertText('There are currently 5 items in queue 1 and 0 items in queue 2');
+ $post = array(
+ 'num_items' => 100,
+ 'queue' => 'cron_example_queue_2',
+ );
+ $this->drupalPost(NULL, $post, t('Add jobs to queue'));
+ $this->assertText('There are currently 5 items in queue 1 and 100 items in queue 2');
+
+ $post = array();
+ $this->drupalPost('examples/cron_example', $post, t('Run cron now'));
+ $this->assertPattern('/Queue 1 worker processed item with sequence 5 /');
+ $this->assertPattern('/Queue 2 worker processed item with sequence 100 /');
+ }
+}
+
+/**
+ * @} End of "addtogroup cron_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.info b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.info
new file mode 100644
index 00000000..50505607
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.info
@@ -0,0 +1,12 @@
+name = DBTNG example
+description = An example module showing how use the database API: DBTNG.
+package = Example modules
+core = 7.x
+files[] = dbtng_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.install b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.install
new file mode 100644
index 00000000..46f4a6d7
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.install
@@ -0,0 +1,102 @@
+ 'John',
+ 'surname' => 'Doe',
+ 'age' => 0,
+ );
+ db_insert('dbtng_example')
+ ->fields($fields)
+ ->execute();
+
+ // Add another entry.
+ $fields = array(
+ 'name' => 'John',
+ 'surname' => 'Roe',
+ 'age' => 100,
+ 'uid' => 1,
+ );
+ db_insert('dbtng_example')
+ ->fields($fields)
+ ->execute();
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * Defines the database tables used by this module.
+ * Remember that the easiest way to create the code for hook_schema is with
+ * the @link http://drupal.org/project/schema schema module @endlink
+ *
+ * @see hook_schema()
+ * @ingroup dbtng_example
+ */
+function dbtng_example_schema() {
+
+ $schema['dbtng_example'] = array(
+ 'description' => 'Stores example person entries for demonstration purposes.',
+ 'fields' => array(
+ 'pid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique person ID.',
+ ),
+ 'uid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "Creator user's {users}.uid",
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Name of the person.',
+ ),
+ 'surname' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Surname of the person.',
+ ),
+ 'age' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ 'description' => 'The age of the person in years.',
+ ),
+ ),
+ 'primary key' => array('pid'),
+ 'indexes' => array(
+ 'name' => array('name'),
+ 'surname' => array('surname'),
+ 'age' => array('age'),
+ ),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.module b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.module
new file mode 100644
index 00000000..f468dc48
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.module
@@ -0,0 +1,579 @@
+fields(array('name' => 'John', 'surname' => 'Doe'))
+ * ->execute();
+ * @endcode
+ *
+ * db_update() example:
+ * @code
+ * // UPDATE {dbtng_example} SET name = 'Jane' WHERE name = 'John'
+ * db_update('dbtng_example')
+ * ->fields(array('name' => 'Jane'))
+ * ->condition('name', 'John')
+ * ->execute();
+ * @endcode
+ *
+ * db_delete() example:
+ * @code
+ * // DELETE FROM {dbtng_example} WHERE name = 'Jane'
+ * db_delete('dbtng_example')
+ * ->condition('name', 'Jane')
+ * ->execute();
+ * @endcode
+ *
+ * See @link database Database Abstraction Layer @endlink
+ * @see db_insert()
+ * @see db_update()
+ * @see db_delete()
+ * @see drupal_write_record()
+ */
+
+/**
+ * Save an entry in the database.
+ *
+ * The underlying DBTNG function is db_insert().
+ *
+ * In Drupal 6, this would have been:
+ * @code
+ * db_query(
+ * "INSERT INTO {dbtng_example} (name, surname, age)
+ * VALUES ('%s', '%s', '%d')",
+ * $entry['name'],
+ * $entry['surname'],
+ * $entry['age']
+ * );
+ * @endcode
+ *
+ * Exception handling is shown in this example. It could be simplified
+ * without the try/catch blocks, but since an insert will throw an exception
+ * and terminate your application if the exception is not handled, it is best
+ * to employ try/catch.
+ *
+ * @param array $entry
+ * An array containing all the fields of the database record.
+ *
+ * @see db_insert()
+ */
+function dbtng_example_entry_insert($entry) {
+ $return_value = NULL;
+ try {
+ $return_value = db_insert('dbtng_example')
+ ->fields($entry)
+ ->execute();
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('db_insert failed. Message = %message, query= %query',
+ array('%message' => $e->getMessage(), '%query' => $e->query_string)), 'error');
+ }
+ return $return_value;
+}
+
+/**
+ * Update an entry in the database.
+ *
+ * The former, deprecated techniques used db_query() or drupal_write_record():
+ * @code
+ * drupal_write_record('dbtng_example', $entry, $entry['pid']);
+ * @endcode
+ *
+ * @code
+ * db_query(
+ * "UPDATE {dbtng_example}
+ * SET name = '%s', surname = '%s', age = '%d'
+ * WHERE pid = %d",
+ * $entry['pid']
+ * );
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing all the fields of the item to be updated.
+ *
+ * @see db_update()
+ */
+function dbtng_example_entry_update($entry) {
+ try {
+ // db_update()...->execute() returns the number of rows updated.
+ $count = db_update('dbtng_example')
+ ->fields($entry)
+ ->condition('pid', $entry['pid'])
+ ->execute();
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('db_update failed. Message = %message, query= %query',
+ array('%message' => $e->getMessage(), '%query' => $e->query_string)), 'error');
+ }
+ return $count;
+}
+
+/**
+ * Delete an entry from the database.
+ *
+ * The usage of db_query is deprecated except for static queries.
+ * Formerly, a deletion might have been accomplished like this:
+ * @code
+ * db_query("DELETE FROM {dbtng_example} WHERE pid = %d", $entry['pid]);
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing at least the person identifier 'pid' element of the
+ * entry to delete.
+ *
+ * @see db_delete()
+ */
+function dbtng_example_entry_delete($entry) {
+ db_delete('dbtng_example')
+ ->condition('pid', $entry['pid'])
+ ->execute();
+
+}
+
+
+/**
+ * Read from the database using a filter array.
+ *
+ * In Drupal 6, the standard function to perform reads was db_query(), and
+ * for static queries, it still is.
+ *
+ * db_query() used an SQL query with placeholders and arguments as parameters.
+ *
+ * @code
+ * // Old way
+ * $query = "SELECT * FROM {dbtng_example} n WHERE n.uid = %d AND name = '%s'";
+ * $result = db_query($query, $uid, $name);
+ * @endcode
+ *
+ * Drupal 7 DBTNG provides an abstracted interface that will work with a wide
+ * variety of database engines.
+ *
+ * db_query() is deprecated except when doing a static query. The following is
+ * perfectly acceptable in Drupal 7. See
+ * @link http://drupal.org/node/310072 the handbook page on static queries @endlink
+ *
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * db_query(
+ * "SELECT * FROM {dbtng_example} WHERE uid = :uid and name = :name",
+ * array(':uid' => 0, ':name' => 'John')
+ * )->execute();
+ * @endcode
+ *
+ * But for more dynamic queries, Drupal provides the db_select() API method, so
+ * there are several ways to perform the same SQL query. See the
+ * @link http://drupal.org/node/310075 handbook page on dynamic queries. @endlink
+ *
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->condition('uid', 0)
+ * ->condition('name', 'John')
+ * ->execute();
+ * @endcode
+ *
+ * Here is db_select with named placeholders:
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * $arguments = array(':name' => 'John', ':uid' => 0);
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->where('uid = :uid AND name = :name', $arguments)
+ * ->execute();
+ * @endcode
+ *
+ * Conditions are stacked and evaluated as AND and OR depending on the type of
+ * query. For more information, read the conditional queries handbook page at:
+ * http://drupal.org/node/310086
+ *
+ * The condition argument is an 'equal' evaluation by default, but this can be
+ * altered:
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE age > 18
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->condition('age', 18, '>')
+ * ->execute();
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing all the fields used to search the entries in the table.
+ *
+ * @return object
+ * An object containing the loaded entries if found.
+ *
+ * @see db_select()
+ * @see db_query()
+ * @see http://drupal.org/node/310072
+ * @see http://drupal.org/node/310075
+ */
+function dbtng_example_entry_load($entry = array()) {
+ // Read all fields from the dbtng_example table.
+ $select = db_select('dbtng_example', 'example');
+ $select->fields('example');
+
+ // Add each field and value as a condition to this query.
+ foreach ($entry as $field => $value) {
+ $select->condition($field, $value);
+ }
+ // Return the result in object format.
+ return $select->execute()->fetchAll();
+}
+
+/**
+ * Render a filtered list of entries in the database.
+ *
+ * DBTNG also helps processing queries that return several rows, providing the
+ * found objects in the same query execution call.
+ *
+ * This function queries the database using a JOIN between users table and the
+ * example entries, to provide the username that created the entry, and creates
+ * a table with the results, processing each row.
+ *
+ * SELECT
+ * e.pid as pid, e.name as name, e.surname as surname, e.age as age
+ * u.name as username
+ * FROM
+ * {dbtng_example} e
+ * JOIN
+ * users u ON e.uid = u.uid
+ * WHERE
+ * e.name = 'John' AND e.age > 18
+ *
+ * @see db_select()
+ * @see http://drupal.org/node/310075
+ */
+function dbtng_example_advanced_list() {
+ $output = '';
+
+ $select = db_select('dbtng_example', 'e');
+ // Join the users table, so we can get the entry creator's username.
+ $select->join('users', 'u', 'e.uid = u.uid');
+ // Select these specific fields for the output.
+ $select->addField('e', 'pid');
+ $select->addField('u', 'name', 'username');
+ $select->addField('e', 'name');
+ $select->addField('e', 'surname');
+ $select->addField('e', 'age');
+ // Filter only persons named "John".
+ $select->condition('e.name', 'John');
+ // Filter only persons older than 18 years.
+ $select->condition('e.age', 18, '>');
+ // Make sure we only get items 0-49, for scalability reasons.
+ $select->range(0, 50);
+
+ // Now, loop all these entries and show them in a table. Note that there is no
+ // db_fetch_* object or array function being called here. Also note that the
+ // following line could have been written as
+ // $entries = $select->execute()->fetchAll() which would return each selected
+ // record as an object instead of an array.
+ $entries = $select->execute()->fetchAll(PDO::FETCH_ASSOC);
+ if (!empty($entries)) {
+ $rows = array();
+ foreach ($entries as $entry) {
+ // Sanitize the data before handing it off to the theme layer.
+ $rows[] = array_map('check_plain', $entry);
+ }
+ // Make a table for them.
+ $header = array(t('Id'), t('Created by'), t('Name'), t('Surname'), t('Age'));
+ $output .= theme('table', array('header' => $header, 'rows' => $rows));
+ }
+ else {
+ drupal_set_message(t('No entries meet the filter criteria (Name = "John" and Age > 18).'));
+ }
+ return $output;
+}
+
+/**
+ * Implements hook_help().
+ *
+ * Show some help on each form provided by this module.
+ */
+function dbtng_example_help($path) {
+ $output = '';
+ switch ($path) {
+ case 'examples/dbtng':
+ $output = t('Generate a list of all entries in the database. There is no filter in the query.');
+ break;
+
+ case 'examples/dbtng/advanced':
+ $output = t('A more complex list of entries in the database.') . ' ';
+ $output .= t('Only the entries with name = "John" and age older than 18 years are shown, the username of the person who created the entry is also shown.');
+ break;
+
+ case 'examples/dbtng/update':
+ $output = t('Demonstrates a database update operation.');
+ break;
+
+ case 'examples/dbtng/add':
+ $output = t('Add an entry to the dbtng_example table.');
+ break;
+ }
+ return $output;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Set up calls to drupal_get_form() for all our example cases.
+ */
+function dbtng_example_menu() {
+ $items = array();
+
+ $items['examples/dbtng'] = array(
+ 'title' => 'DBTNG Example',
+ 'page callback' => 'dbtng_example_list',
+ 'access callback' => TRUE,
+ );
+ $items['examples/dbtng/list'] = array(
+ 'title' => 'List',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['examples/dbtng/add'] = array(
+ 'title' => 'Add entry',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('dbtng_example_form_add'),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -9,
+ );
+ $items['examples/dbtng/update'] = array(
+ 'title' => 'Update entry',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('dbtng_example_form_update'),
+ 'type' => MENU_LOCAL_TASK,
+ 'access callback' => TRUE,
+ 'weight' => -5,
+ );
+ $items['examples/dbtng/advanced'] = array(
+ 'title' => 'Advanced list',
+ 'page callback' => 'dbtng_example_advanced_list',
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ );
+
+ return $items;
+}
+
+/**
+ * Render a list of entries in the database.
+ */
+function dbtng_example_list() {
+ $output = '';
+
+ // Get all entries in the dbtng_example table.
+ if ($entries = dbtng_example_entry_load()) {
+ $rows = array();
+ foreach ($entries as $entry) {
+ // Sanitize the data before handing it off to the theme layer.
+ $rows[] = array_map('check_plain', (array) $entry);
+ }
+ // Make a table for them.
+ $header = array(t('Id'), t('uid'), t('Name'), t('Surname'), t('Age'));
+ $output .= theme('table', array('header' => $header, 'rows' => $rows));
+ }
+ else {
+ drupal_set_message(t('No entries have been added yet.'));
+ }
+ return $output;
+}
+
+/**
+ * Prepare a simple form to add an entry, with all the interesting fields.
+ */
+function dbtng_example_form_add($form, &$form_state) {
+ $form = array();
+
+ $form['add'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Add a person entry'),
+ );
+ $form['add']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#size' => 15,
+ );
+ $form['add']['surname'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Surname'),
+ '#size' => 15,
+ );
+ $form['add']['age'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Age'),
+ '#size' => 5,
+ '#description' => t("Values greater than 127 will cause an exception. Try it - it's a great example why exception handling is needed with DTBNG."),
+ );
+ $form['add']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for 'add entry' form.
+ */
+function dbtng_example_form_add_submit($form, &$form_state) {
+ global $user;
+
+ // Save the submitted entry.
+ $entry = array(
+ 'name' => $form_state['values']['name'],
+ 'surname' => $form_state['values']['surname'],
+ 'age' => $form_state['values']['age'],
+ 'uid' => $user->uid,
+ );
+ $return = dbtng_example_entry_insert($entry);
+ if ($return) {
+ drupal_set_message(t("Created entry @entry", array('@entry' => print_r($entry, TRUE))));
+ }
+}
+
+/**
+ * Sample UI to update a record.
+ */
+function dbtng_example_form_update($form, &$form_state) {
+ $form = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ $entries = dbtng_example_entry_load();
+ $keyed_entries = array();
+ if (empty($entries)) {
+ $form['no_values'] = array(
+ '#value' => t("No entries exist in the table dbtng_example table."),
+ );
+ return $form;
+ }
+
+ foreach ($entries as $entry) {
+ $options[$entry->pid] = t("@pid: @name @surname (@age)",
+ array(
+ '@pid' => $entry->pid,
+ '@name' => $entry->name,
+ '@surname' => $entry->surname,
+ '@age' => $entry->age,
+ )
+ );
+ $keyed_entries[$entry->pid] = $entry;
+ }
+ $default_entry = !empty($form_state['values']['pid']) ? $keyed_entries[$form_state['values']['pid']] : $entries[0];
+
+ $form_state['entries'] = $keyed_entries;
+
+ $form['pid'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => t('Choose entry to update'),
+ '#default_value' => $default_entry->pid,
+ '#ajax' => array(
+ 'wrapper' => 'updateform',
+ 'callback' => 'dbtng_example_form_update_callback',
+ ),
+ );
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Updated first name'),
+ '#size' => 15,
+ '#default_value' => $default_entry->name,
+ );
+
+ $form['surname'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Updated last name'),
+ '#size' => 15,
+ '#default_value' => $default_entry->surname,
+ );
+ $form['age'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Updated age'),
+ '#size' => 4,
+ '#default_value' => $default_entry->age,
+ '#description' => t("Values greater than 127 will cause an exception"),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Update'),
+ );
+ return $form;
+}
+
+/**
+ * AJAX callback handler for the pid select.
+ *
+ * When the pid changes, populates the defaults from the database in the form.
+ */
+function dbtng_example_form_update_callback($form, $form_state) {
+ $entry = $form_state['entries'][$form_state['values']['pid']];
+ // Setting the #value of items is the only way I was able to figure out
+ // to get replaced defaults on these items. #default_value will not do it
+ // and shouldn't.
+ foreach (array('name', 'surname', 'age') as $item) {
+ $form[$item]['#value'] = $entry->$item;
+ }
+ return $form;
+}
+
+/**
+ * Submit handler for 'update entry' form.
+ */
+function dbtng_example_form_update_submit($form, &$form_state) {
+ global $user;
+
+ // Save the submitted entry.
+ $entry = array(
+ 'pid' => $form_state['values']['pid'],
+ 'name' => $form_state['values']['name'],
+ 'surname' => $form_state['values']['surname'],
+ 'age' => $form_state['values']['age'],
+ 'uid' => $user->uid,
+ );
+ $count = dbtng_example_entry_update($entry);
+ drupal_set_message(t("Updated entry @entry (@count row updated)",
+ array('@count' => $count, '@entry' => print_r($entry, TRUE))));
+}
+/**
+ * @} End of "defgroup dbtng_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.test b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.test
new file mode 100644
index 00000000..6302bcee
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/dbtng_example/dbtng_example.test
@@ -0,0 +1,191 @@
+ 'DBTNG example unit and UI tests',
+ 'description' => 'Various unit tests on the dbtng example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('dbtng_example');
+ }
+
+ /**
+ * Test default module installation, two entries in the database table.
+ */
+ public function testInstall() {
+ $result = dbtng_example_entry_load();
+ $this->assertEqual(
+ count($result),
+ 2,
+ 'Found two entries in the table after installing the module.'
+ );
+ }
+
+
+ /**
+ * Test the UI.
+ */
+ public function testUI() {
+ // Test the basic list.
+ $this->drupalGet('examples/dbtng');
+ $this->assertPattern("/John[td\/<>\w]+Doe/", "Text 'John Doe' found in table");
+
+ // Test the add tab.
+ // Add the new entry.
+ $this->drupalPost('examples/dbtng/add',
+ array(
+ 'name' => 'Some',
+ 'surname' => 'Anonymous',
+ 'age' => 33,
+ ),
+ t('Add')
+ );
+ // Now find the new entry.
+ $this->drupalGet('examples/dbtng');
+ $this->assertPattern("/Some[td\/<>\w]+Anonymous/", "Text 'Some Anonymous' found in table");
+
+ // Try the update tab.
+ // Find out the pid of our "anonymous" guy.
+ $result = dbtng_example_entry_load(array('surname' => 'Anonymous'));
+ $this->drupalGet("examples/dbtng");
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry in the table with surname = "Anonymous".'
+ );
+ $entry = $result[0];
+ unset($entry->uid);
+ $entry->name = 'NewFirstName';
+ $this->drupalPost('examples/dbtng/update', (array) $entry, t('Update'));
+ // Now find the new entry.
+ $this->drupalGet('examples/dbtng');
+ $this->assertPattern("/NewFirstName[td\/<>\w]+Anonymous/", "Text 'NewFirstName Anonymous' found in table");
+
+ // Try the advanced tab.
+ $this->drupalGet('examples/dbtng/advanced');
+ $rows = $this->xpath("//*[@id='block-system-main']/div/table[1]/tbody/tr");
+ $this->assertEqual(count($rows), 1, "One row found in advanced view");
+ $this->assertFieldByXPath("//*[@id='block-system-main']/div/table[1]/tbody/tr/td[4]", "Roe", "Name 'Roe' Exists in advanced list");
+ }
+
+ /**
+ * Test several combinations, adding entries, updating and deleting.
+ */
+ public function testAPIExamples() {
+ // Create a new entry.
+ $entry = array(
+ 'name' => 'James',
+ 'surname' => 'Doe',
+ 'age' => 23,
+ );
+ dbtng_example_entry_insert($entry);
+
+ // Save another entry.
+ $entry = array(
+ 'name' => 'Jane',
+ 'surname' => 'NotDoe',
+ 'age' => 19,
+ );
+ dbtng_example_entry_insert($entry);
+
+ // Verify that 4 records are found in the database.
+ $result = dbtng_example_entry_load();
+ $this->assertEqual(
+ count($result),
+ 4,
+ 'Found a total of four entries in the table after creating two additional entries.'
+ );
+
+ // Verify 2 of these records have 'Doe' as surname.
+ $result = dbtng_example_entry_load(array('surname' => 'Doe'));
+ $this->assertEqual(
+ count($result),
+ 2,
+ 'Found two entries in the table with surname = "Doe".'
+ );
+
+ // Now find our not-Doe entry.
+ $result = dbtng_example_entry_load(array('surname' => 'NotDoe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry in the table with surname "NotDoe');
+ // Our NotDoe will be changed to "NowDoe".
+ $entry = $result[0];
+ $entry->surname = "NowDoe";
+ dbtng_example_entry_update((array) $entry);
+
+ $result = dbtng_example_entry_load(array('surname' => 'NowDoe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ "Found renamed 'NowDoe' surname");
+
+ // Read only John Doe entry.
+ $result = dbtng_example_entry_load(array('name' => 'John', 'surname' => 'Doe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry for John Doe.'
+ );
+ // Get the entry.
+ $entry = (array) end($result);
+ // Change age to 45
+ $entry['age'] = 45;
+ // Update entry in database.
+ dbtng_example_entry_update((array) $entry);
+
+ // Find entries with age = 45
+ // Read only John Doe entry.
+ $result = dbtng_example_entry_load(array('surname' => 'NowDoe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry with surname = Nowdoe.'
+ );
+
+ // Verify it is Jane NowDoe.
+ $entry = (array) end($result);
+ $this->assertEqual(
+ $entry['name'],
+ 'Jane',
+ 'The name Jane is found in the entry'
+ );
+ $this->assertEqual(
+ $entry['surname'],
+ 'NowDoe',
+ 'The surname NowDoe is found in the entry'
+ );
+
+ // Delete the entry.
+ dbtng_example_entry_delete($entry);
+
+ // Verify that now there are only 3 records.
+ $result = dbtng_example_entry_load();
+ $this->assertEqual(
+ count($result),
+ 3,
+ 'Found only three records, a record was deleted.'
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/email_example/email_example.info b/sites/all/modules/contrib/dev/examples/email_example/email_example.info
new file mode 100644
index 00000000..8b123ab7
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/email_example/email_example.info
@@ -0,0 +1,12 @@
+name = E-mail Example
+description = Demonstrate Drupal's e-mail APIs.
+package = Example modules
+core = 7.x
+files[] = email_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/email_example/email_example.module b/sites/all/modules/contrib/dev/examples/email_example/email_example.module
new file mode 100644
index 00000000..6b84bc81
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/email_example/email_example.module
@@ -0,0 +1,212 @@
+ $message['language']->language,
+ );
+
+ switch ($key) {
+ // Send a simple message from the contact form.
+ case 'contact_message':
+ $message['subject'] = t('E-mail sent from @site-name', array('@site-name' => variable_get('site_name', 'Drupal')), $options);
+ // Note that the message body is an array, not a string.
+ $message['body'][] = t('@name sent you the following message:', array('@name' => $user->name), $options);
+ // Because this is just user-entered text, we do not need to translate it.
+ // Since user-entered text may have unintentional HTML entities in it like
+ // '<' or '>', we need to make sure these entities are properly escaped,
+ // as the body will later be transformed from HTML to text, meaning
+ // that a normal use of '<' will result in truncation of the message.
+ $message['body'][] = check_plain($params['message']);
+ break;
+ }
+}
+
+/**
+ * Sends an e-mail.
+ *
+ * @param array $form_values
+ * An array of values from the contact form fields that were submitted.
+ * There are just two relevant items: $form_values['email'] and
+ * $form_values['message'].
+ */
+function email_example_mail_send($form_values) {
+ // All system mails need to specify the module and template key (mirrored from
+ // hook_mail()) that the message they want to send comes from.
+ $module = 'email_example';
+ $key = 'contact_message';
+
+ // Specify 'to' and 'from' addresses.
+ $to = $form_values['email'];
+ $from = variable_get('site_mail', 'admin@example.com');
+
+ // "params" loads in additional context for email content completion in
+ // hook_mail(). In this case, we want to pass in the values the user entered
+ // into the form, which include the message body in $form_values['message'].
+ $params = $form_values;
+
+ // The language of the e-mail. This will one of three values:
+ // - user_preferred_language(): Used for sending mail to a particular website
+ // user, so that the mail appears in their preferred language.
+ // - global $language: Used when sending a mail back to the user currently
+ // viewing the site. This will send it in the language they're currently
+ // using.
+ // - language_default(): Used when sending mail to a pre-existing, 'neutral'
+ // address, such as the system e-mail address, or when you're unsure of the
+ // language preferences of the intended recipient.
+ //
+ // Since in our case, we are sending a message to a random e-mail address that
+ // is not necessarily tied to a user account, we will use the site's default
+ // language.
+ $language = language_default();
+
+ // Whether or not to automatically send the mail when drupal_mail() is
+ // called. This defaults to TRUE, and is normally what you want unless you
+ // need to do additional processing before drupal_mail_send() is called.
+ $send = TRUE;
+ // Send the mail, and check for success. Note that this does not guarantee
+ // message delivery; only that there were no PHP-related issues encountered
+ // while sending.
+ $result = drupal_mail($module, $key, $to, $language, $params, $from, $send);
+ if ($result['result'] == TRUE) {
+ drupal_set_message(t('Your message has been sent.'));
+ }
+ else {
+ drupal_set_message(t('There was a problem sending your message and it was not sent.'), 'error');
+ }
+
+}
+
+/**
+ * Implements hook_mail_alter().
+ *
+ * This function is not required to send an email using Drupal's mail system.
+ *
+ * Hook_mail_alter() provides an interface to alter any aspect of email sent by
+ * Drupal. You can use this hook to add a common site footer to all outgoing
+ * email, add extra header fields, and/or modify the email in anyway. HTML-izing
+ * the outgoing email is one possibility.
+ */
+function email_example_mail_alter(&$message) {
+ // For the purpose of this example, modify all the outgoing messages and
+ // attach a site signature. The signature will be translated to the language
+ // in which message was built.
+ $options = array(
+ 'langcode' => $message['language']->language,
+ );
+
+ $signature = t("\n--\nMail altered by email_example module.", array(), $options);
+ if (is_array($message['body'])) {
+ $message['body'][] = $signature;
+ }
+ else {
+ // Some modules use the body as a string, erroneously.
+ $message['body'] .= $signature;
+ }
+}
+
+/**
+ * Supporting functions.
+ */
+
+/**
+ * Implements hook_menu().
+ *
+ * Set up a page with an e-mail contact form on it.
+ */
+function email_example_menu() {
+ $items['example/email_example'] = array(
+ 'title' => 'E-mail Example: contact form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('email_example_form'),
+ 'access arguments' => array('access content'),
+ );
+
+ return $items;
+}
+
+/**
+ * The contact form.
+ */
+function email_example_form() {
+ $form['intro'] = array(
+ '#markup' => t('Use this form to send a message to an e-mail address. No spamming!'),
+ );
+ $form['email'] = array(
+ '#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#required' => TRUE,
+ );
+ $form['message'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message'),
+ '#required' => TRUE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Form validation logic for the contact form.
+ */
+function email_example_form_validate($form, &$form_state) {
+ if (!valid_email_address($form_state['values']['email'])) {
+ form_set_error('email', t('That e-mail address is not valid.'));
+ }
+}
+
+/**
+ * Form submission logic for the contact form.
+ */
+function email_example_form_submit($form, &$form_state) {
+ email_example_mail_send($form_state['values']);
+}
+/**
+ * @} End of "defgroup email_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/email_example/email_example.test b/sites/all/modules/contrib/dev/examples/email_example/email_example.test
new file mode 100644
index 00000000..f95a2cda
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/email_example/email_example.test
@@ -0,0 +1,103 @@
+ 'Email example',
+ 'description' => 'Verify the email submission using the contact form.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the email_example module.
+ parent::setUp('email_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testContactForm() {
+ // Create and login user.
+ $account = $this->drupalCreateUser();
+ $this->drupalLogin($account);
+
+ // Set default language for t() translations.
+ $t_options = array(
+ 'langcode' => language_default()->language,
+ );
+
+ // First try to send to an invalid email address.
+ $email_options = array(
+ 'email' => $this->randomName(),
+ 'message' => $this->randomName(128),
+ );
+ $result = $this->drupalPost('example/email_example', $email_options, t('Submit'));
+
+ // Verify that email address is invalid and email was not sent.
+ $this->assertText(t('That e-mail address is not valid.'), 'Options were validated and form submitted.');
+ $this->assertTrue(!count($this->drupalGetMails()), 'No email was sent.');
+
+ // Now try with a valid email address.
+ $email_options['email'] = $this->randomName() . '@' . $this->randomName() . '.drupal';
+ $result = $this->drupalPost('example/email_example', $email_options, t('Submit'));
+
+ // Verify that email address is valid and email was sent.
+ $this->assertTrue(count($this->drupalGetMails()), 'An email has been sent.');
+
+ // Validate sent email.
+ $email = $this->drupalGetMails();
+ // Grab the first entry.
+ $email = $email[0];
+
+ // Verify email recipient.
+ $this->assertEqual(
+ $email['to'],
+ $email_options['email'],
+ 'Email recipient successfully verified.'
+ );
+
+ // Verify email subject.
+ $this->assertEqual(
+ $email['subject'],
+ t('E-mail sent from @site-name', array('@site-name' => variable_get('site_name', 'Drupal')), $t_options),
+ 'Email subject successfully verified.'
+ );
+
+ // Verify email body.
+ $this->assertTrue(
+ strstr(
+ $email['body'],
+ t('@name sent you the following message:', array('@name' => $account->name), $t_options)
+ ),
+ 'Email body successfully verified.'
+ );
+
+ // Verify that signature is attached.
+ $this->assertTrue(
+ strstr(
+ $email['body'],
+ t("--\nMail altered by email_example module.", array(), $t_options)
+ ),
+ 'Email signature successfully verified.'
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/entity_example/entity_example.info b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.info
new file mode 100644
index 00000000..4cb8bc1a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.info
@@ -0,0 +1,14 @@
+name = Entity Example
+description = A simple entity example showing the main steps required to set up your own entity.
+core = 7.x
+package = Example modules
+dependencies[] = field
+files[] = entity_example.test
+configure = admin/structure/entity_example_basic/manage
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/entity_example/entity_example.install b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.install
new file mode 100644
index 00000000..9735dce6
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.install
@@ -0,0 +1,71 @@
+ 'The base table for our basic entity.',
+ 'fields' => array(
+ 'basic_id' => array(
+ 'description' => 'Primary key of the basic entity.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ // If we allow multiple bundles, then the schema must handle that;
+ // We'll put it in the 'bundle_type' field to avoid confusion with the
+ // entity type.
+ 'bundle_type' => array(
+ 'description' => 'The bundle type',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => TRUE,
+ ),
+ // Additional properties are just things that are common to all
+ // entities and don't require field storage.
+ 'item_description' => array(
+ 'description' => 'A description of the item',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp of the entity creation time.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('basic_id'),
+ );
+
+ return $schema;
+}
+
+
+/**
+ * Implements hook_uninstall().
+ *
+ * At uninstall time we'll notify field.module that the entity was deleted
+ * so that attached fields can be cleaned up.
+ *
+ * @ingroup entity_example
+ */
+function entity_example_uninstall() {
+ field_attach_delete_bundle('entity_example_basic', 'first_example_bundle');
+}
diff --git a/sites/all/modules/contrib/dev/examples/entity_example/entity_example.module b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.module
new file mode 100644
index 00000000..de39adb0
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.module
@@ -0,0 +1,635 @@
+ t('Example Basic Entity'),
+
+ // The controller for our Entity, extending the Drupal core controller.
+ 'controller class' => 'EntityExampleBasicController',
+
+ // The table for this entity defined in hook_schema()
+ 'base table' => 'entity_example_basic',
+
+ // Returns the uri elements of an entity.
+ 'uri callback' => 'entity_example_basic_uri',
+
+ // IF fieldable == FALSE, we can't attach fields.
+ 'fieldable' => TRUE,
+
+ // entity_keys tells the controller what database fields are used for key
+ // functions. It is not required if we don't have bundles or revisions.
+ // Here we do not support a revision, so that entity key is omitted.
+ 'entity keys' => array(
+ // The 'id' (basic_id here) is the unique id.
+ 'id' => 'basic_id' ,
+ // Bundle will be determined by the 'bundle_type' field.
+ 'bundle' => 'bundle_type',
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'bundle_type',
+ ),
+
+ // FALSE disables caching. Caching functionality is handled by Drupal core.
+ 'static cache' => TRUE,
+
+ // Bundles are alternative groups of fields or configuration
+ // associated with a base entity type.
+ 'bundles' => array(
+ 'first_example_bundle' => array(
+ 'label' => 'First example bundle',
+ // 'admin' key is used by the Field UI to provide field and
+ // display UI pages.
+ 'admin' => array(
+ 'path' => 'admin/structure/entity_example_basic/manage',
+ 'access arguments' => array('administer entity_example_basic entities'),
+ ),
+ ),
+ ),
+ // View modes allow entities to be displayed differently based on context.
+ // As a demonstration we'll support "Tweaky", but we could have and support
+ // multiple display modes.
+ 'view modes' => array(
+ 'tweaky' => array(
+ 'label' => t('Tweaky'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ );
+
+ return $info;
+}
+
+/**
+ * Fetch a basic object.
+ *
+ * This function ends up being a shim between the menu system and
+ * entity_example_basic_load_multiple().
+ *
+ * This function gets its name from the menu system's wildcard
+ * naming conventions. For example, /path/%wildcard would end
+ * up calling wildcard_load(%wildcard value). In our case defining
+ * the path: examples/entity_example/basic/%entity_example_basic in
+ * hook_menu() tells Drupal to call entity_example_basic_load().
+ *
+ * @param int $basic_id
+ * Integer specifying the basic entity id.
+ * @param bool $reset
+ * A boolean indicating that the internal cache should be reset.
+ *
+ * @return object
+ * A fully-loaded $basic object or FALSE if it cannot be loaded.
+ *
+ * @see entity_example_basic_load_multiple()
+ * @see entity_example_menu()
+ */
+function entity_example_basic_load($basic_id = NULL, $reset = FALSE) {
+ $basic_ids = (isset($basic_id) ? array($basic_id) : array());
+ $basic = entity_example_basic_load_multiple($basic_ids, array(), $reset);
+ return $basic ? reset($basic) : FALSE;
+}
+
+/**
+ * Loads multiple basic entities.
+ *
+ * We only need to pass this request along to entity_load(), which
+ * will in turn call the load() method of our entity controller class.
+ */
+function entity_example_basic_load_multiple($basic_ids = FALSE, $conditions = array(), $reset = FALSE) {
+ return entity_load('entity_example_basic', $basic_ids, $conditions, $reset);
+}
+
+/**
+ * Implements the uri callback.
+ */
+function entity_example_basic_uri($basic) {
+ return array(
+ 'path' => 'examples/entity_example/basic/' . $basic->basic_id,
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function entity_example_menu() {
+ $items['examples/entity_example'] = array(
+ 'title' => 'Entity Example',
+ 'page callback' => 'entity_example_info_page',
+ 'access arguments' => array('view any entity_example_basic entity'),
+ );
+
+ // This provides a place for Field API to hang its own
+ // interface and has to be the same as what was defined
+ // in basic_entity_info() above.
+ $items['admin/structure/entity_example_basic/manage'] = array(
+ 'title' => 'Administer entity_example_basic entity type',
+ 'page callback' => 'entity_example_basic_list_entities',
+ 'access arguments' => array('administer entity_example_basic entities'),
+ );
+
+ // Add example entities.
+ $items['admin/structure/entity_example_basic/manage/add'] = array(
+ 'title' => 'Add an Entity Example Basic Entity',
+ 'page callback' => 'entity_example_basic_add',
+ 'access arguments' => array('create entity_example_basic entities'),
+ 'type' => MENU_LOCAL_ACTION,
+ );
+
+ // List of all entity_example_basic entities.
+ $items['admin/structure/entity_example_basic/manage/list'] = array(
+ 'title' => 'List',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ );
+
+ // The page to view our entities - needs to follow what
+ // is defined in basic_uri and will use load_basic to retrieve
+ // the necessary entity info.
+ $items['examples/entity_example/basic/%entity_example_basic'] = array(
+ 'title callback' => 'entity_example_basic_title',
+ 'title arguments' => array(3),
+ 'page callback' => 'entity_example_basic_view',
+ 'page arguments' => array(3),
+ 'access arguments' => array('view any entity_example_basic entity'),
+ );
+
+ // 'View' tab for an individual entity page.
+ $items['examples/entity_example/basic/%entity_example_basic/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ // 'Edit' tab for an individual entity page.
+ $items['examples/entity_example/basic/%entity_example_basic/edit'] = array(
+ 'title' => 'Edit',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('entity_example_basic_form', 3),
+ 'access arguments' => array('edit any entity_example_basic entity'),
+ 'type' => MENU_LOCAL_TASK,
+ );
+
+ // Add example entities.
+ $items['examples/entity_example/basic/add'] = array(
+ 'title' => 'Add an Entity Example Basic Entity',
+ 'page callback' => 'entity_example_basic_add',
+ 'access arguments' => array('create entity_example_basic entities'),
+ );
+
+ return $items;
+}
+
+/**
+ * Basic information for the page.
+ */
+function entity_example_info_page() {
+ $content['preface'] = array(
+ '#type' => 'item',
+ '#markup' => t('The entity example provides a simple example entity.'),
+ );
+ if (user_access('administer entity_example_basic entities')) {
+ $content['preface']['#markup'] = t('You can administer these and add fields and change the view !link.',
+ array('!link' => l(t('here'), 'admin/structure/entity_example_basic/manage'))
+ );
+ }
+ $content['table'] = entity_example_basic_list_entities();
+
+ return $content;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function entity_example_permission() {
+ $permissions = array(
+ 'administer entity_example_basic entities' => array(
+ 'title' => t('Administer entity_example_basic entities'),
+ ),
+ 'view any entity_example_basic entity' => array(
+ 'title' => t('View any Entity Example Basic entity'),
+ ),
+ 'edit any entity_example_basic entity' => array(
+ 'title' => t('Edit any Entity Example Basic entity'),
+ ),
+ 'create entity_example_basic entities' => array(
+ 'title' => t('Create Entity Example Basic Entities'),
+ ),
+ );
+ return $permissions;
+}
+
+/**
+ * Returns a render array with all entity_example_basic entities.
+ *
+ * In this basic example we know that there won't be many entities,
+ * so we'll just load them all for display. See pager_example.module
+ * to implement a pager. Most implementations would probably do this
+ * with the contrib Entity API module, or a view using views module,
+ * but we avoid using non-core features in the Examples project.
+ *
+ * @see pager_example.module
+ */
+function entity_example_basic_list_entities() {
+ $content = array();
+ // Load all of our entities.
+ $entities = entity_example_basic_load_multiple();
+ if (!empty($entities)) {
+ foreach ($entities as $entity) {
+ // Create tabular rows for our entities.
+ $rows[] = array(
+ 'data' => array(
+ 'id' => $entity->basic_id,
+ 'item_description' => l($entity->item_description, 'examples/entity_example/basic/' . $entity->basic_id),
+ 'bundle' => $entity->bundle_type,
+ ),
+ );
+ }
+ // Put our entities into a themed table. See theme_table() for details.
+ $content['entity_table'] = array(
+ '#theme' => 'table',
+ '#rows' => $rows,
+ '#header' => array(t('ID'), t('Item Description'), t('Bundle')),
+ );
+ }
+ else {
+ // There were no entities. Tell the user.
+ $content[] = array(
+ '#type' => 'item',
+ '#markup' => t('No entity_example_basic entities currently exist.'),
+ );
+ }
+ return $content;
+}
+
+/**
+ * Callback for a page title when this entity is displayed.
+ */
+function entity_example_basic_title($entity) {
+ return t('Entity Example Basic (item_description=@item_description)', array('@item_description' => $entity->item_description));
+}
+
+/**
+ * Menu callback to display an entity.
+ *
+ * As we load the entity for display, we're responsible for invoking a number
+ * of hooks in their proper order.
+ *
+ * @see hook_entity_prepare_view()
+ * @see hook_entity_view()
+ * @see hook_entity_view_alter()
+ */
+function entity_example_basic_view($entity, $view_mode = 'tweaky') {
+ // Our entity type, for convenience.
+ $entity_type = 'entity_example_basic';
+ // Start setting up the content.
+ $entity->content = array(
+ '#view_mode' => $view_mode,
+ );
+ // Build fields content - this is where the Field API really comes in to play.
+ // The task has very little code here because it all gets taken care of by
+ // field module.
+ // field_attach_prepare_view() lets the fields load any data they need
+ // before viewing.
+ field_attach_prepare_view($entity_type, array($entity->basic_id => $entity),
+ $view_mode);
+ // We call entity_prepare_view() so it can invoke hook_entity_prepare_view()
+ // for us.
+ entity_prepare_view($entity_type, array($entity->basic_id => $entity));
+ // Now field_attach_view() generates the content for the fields.
+ $entity->content += field_attach_view($entity_type, $entity, $view_mode);
+
+ // OK, Field API done, now we can set up some of our own data.
+ $entity->content['created'] = array(
+ '#type' => 'item',
+ '#title' => t('Created date'),
+ '#markup' => format_date($entity->created),
+ );
+ $entity->content['item_description'] = array(
+ '#type' => 'item',
+ '#title' => t('Item Description'),
+ '#markup' => $entity->item_description,
+ );
+
+ // Now to invoke some hooks. We need the language code for
+ // hook_entity_view(), so let's get that.
+ global $language;
+ $langcode = $language->language;
+ // And now invoke hook_entity_view().
+ module_invoke_all('entity_view', $entity, $entity_type, $view_mode,
+ $langcode);
+ // Now invoke hook_entity_view_alter().
+ drupal_alter(array('entity_example_basic_view', 'entity_view'),
+ $entity->content, $entity_type);
+
+ // And finally return the content.
+ return $entity->content;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ *
+ * This exposes the "extra fields" (usually properties that can be configured
+ * as if they were fields) of the entity as pseudo-fields
+ * so that they get handled by the Entity and Field core functionality.
+ * Node titles get treated in a similar manner.
+ */
+function entity_example_field_extra_fields() {
+ $form_elements['item_description'] = array(
+ 'label' => t('Item Description'),
+ 'description' => t('Item Description (an extra form field)'),
+ 'weight' => -5,
+ );
+ $display_elements['created'] = array(
+ 'label' => t('Creation date'),
+ 'description' => t('Creation date (an extra display field)'),
+ 'weight' => 0,
+ );
+ $display_elements['item_description'] = array(
+ 'label' => t('Item Description'),
+ 'description' => t('Just like title, but trying to point out that it is a separate property'),
+ 'weight' => 0,
+ );
+
+ // Since we have only one bundle type, we'll just provide the extra_fields
+ // for it here.
+ $extra_fields['entity_example_basic']['first_example_bundle']['form'] = $form_elements;
+ $extra_fields['entity_example_basic']['first_example_bundle']['display'] = $display_elements;
+
+ return $extra_fields;
+}
+
+/**
+ * Provides a wrapper on the edit form to add a new entity.
+ */
+function entity_example_basic_add() {
+ // Create a basic entity structure to be used and passed to the validation
+ // and submission functions.
+ $entity = entity_get_controller('entity_example_basic')->create();
+ return drupal_get_form('entity_example_basic_form', $entity);
+}
+
+/**
+ * Form function to create an entity_example_basic entity.
+ *
+ * The pattern is:
+ * - Set up the form for the data that is specific to your
+ * entity: the columns of your base table.
+ * - Call on the Field API to pull in the form elements
+ * for fields attached to the entity.
+ */
+function entity_example_basic_form($form, &$form_state, $entity) {
+ $form['item_description'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Item Description'),
+ '#required' => TRUE,
+ '#default_value' => $entity->item_description,
+ );
+
+ $form['basic_entity'] = array(
+ '#type' => 'value',
+ '#value' => $entity,
+ );
+
+ field_attach_form('entity_example_basic', $entity, $form, $form_state);
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#weight' => 100,
+ );
+ $form['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete'),
+ '#submit' => array('entity_example_basic_edit_delete'),
+ '#weight' => 200,
+ );
+
+ return $form;
+}
+
+
+/**
+ * Validation handler for entity_example_basic_add_form form.
+ *
+ * We pass things straight through to the Field API to handle validation
+ * of the attached fields.
+ */
+function entity_example_basic_form_validate($form, &$form_state) {
+ field_attach_form_validate('entity_example_basic', $form_state['values']['basic_entity'], $form, $form_state);
+}
+
+
+/**
+ * Form submit handler: Submits basic_add_form information.
+ */
+function entity_example_basic_form_submit($form, &$form_state) {
+ $entity = $form_state['values']['basic_entity'];
+ $entity->item_description = $form_state['values']['item_description'];
+ field_attach_submit('entity_example_basic', $entity, $form, $form_state);
+ $entity = entity_example_basic_save($entity);
+ $form_state['redirect'] = 'examples/entity_example/basic/' . $entity->basic_id;
+}
+
+/**
+ * Form deletion handler.
+ *
+ * @todo: 'Are you sure?' message.
+ */
+function entity_example_basic_edit_delete($form, &$form_state) {
+ $entity = $form_state['values']['basic_entity'];
+ entity_example_basic_delete($entity);
+ drupal_set_message(t('The entity %item_description (ID %id) has been deleted',
+ array('%item_description' => $entity->item_description, '%id' => $entity->basic_id))
+ );
+ $form_state['redirect'] = 'examples/entity_example';
+}
+
+/**
+ * We save the entity by calling the controller.
+ */
+function entity_example_basic_save(&$entity) {
+ return entity_get_controller('entity_example_basic')->save($entity);
+}
+
+/**
+ * Use the controller to delete the entity.
+ */
+function entity_example_basic_delete($entity) {
+ entity_get_controller('entity_example_basic')->delete($entity);
+}
+
+/**
+ * EntityExampleBasicControllerInterface definition.
+ *
+ * We create an interface here because anyone could come along and
+ * use hook_entity_info_alter() to change our controller class.
+ * We want to let them know what methods our class needs in order
+ * to function with the rest of the module, so here's a handy list.
+ *
+ * @see hook_entity_info_alter()
+ */
+interface EntityExampleBasicControllerInterface
+ extends DrupalEntityControllerInterface {
+
+ /**
+ * Create an entity.
+ */
+ public function create();
+
+ /**
+ * Save an entity.
+ *
+ * @param object $entity
+ * The entity to save.
+ */
+ public function save($entity);
+
+ /**
+ * Delete an entity.
+ *
+ * @param object $entity
+ * The entity to delete.
+ */
+ public function delete($entity);
+
+}
+
+/**
+ * EntityExampleBasicController extends DrupalDefaultEntityController.
+ *
+ * Our subclass of DrupalDefaultEntityController lets us add a few
+ * important create, update, and delete methods.
+ */
+class EntityExampleBasicController
+ extends DrupalDefaultEntityController
+ implements EntityExampleBasicControllerInterface {
+
+ /**
+ * Create and return a new entity_example_basic entity.
+ */
+ public function create() {
+ $entity = new stdClass();
+ $entity->type = 'entity_example_basic';
+ $entity->basic_id = 0;
+ $entity->bundle_type = 'first_example_bundle';
+ $entity->item_description = '';
+ return $entity;
+ }
+
+ /**
+ * Saves the custom fields using drupal_write_record().
+ */
+ public function save($entity) {
+ // If our entity has no basic_id, then we need to give it a
+ // time of creation.
+ if (empty($entity->basic_id)) {
+ $entity->created = time();
+ }
+ // Invoke hook_entity_presave().
+ module_invoke_all('entity_presave', $entity, 'entity_example_basic');
+ // The 'primary_keys' argument determines whether this will be an insert
+ // or an update. So if the entity already has an ID, we'll specify
+ // basic_id as the key.
+ $primary_keys = $entity->basic_id ? 'basic_id' : array();
+ // Write out the entity record.
+ drupal_write_record('entity_example_basic', $entity, $primary_keys);
+ // We're going to invoke either hook_entity_update() or
+ // hook_entity_insert(), depending on whether or not this is a
+ // new entity. We'll just store the name of hook_entity_insert()
+ // and change it if we need to.
+ $invocation = 'entity_insert';
+ // Now we need to either insert or update the fields which are
+ // attached to this entity. We use the same primary_keys logic
+ // to determine whether to update or insert, and which hook we
+ // need to invoke.
+ if (empty($primary_keys)) {
+ field_attach_insert('entity_example_basic', $entity);
+ }
+ else {
+ field_attach_update('entity_example_basic', $entity);
+ $invocation = 'entity_update';
+ }
+ // Invoke either hook_entity_update() or hook_entity_insert().
+ module_invoke_all($invocation, $entity, 'entity_example_basic');
+ return $entity;
+ }
+
+ /**
+ * Delete a single entity.
+ *
+ * Really a convenience function for deleteMultiple().
+ */
+ public function delete($entity) {
+ $this->deleteMultiple(array($entity));
+ }
+
+ /**
+ * Delete one or more entity_example_basic entities.
+ *
+ * Deletion is unfortunately not supported in the base
+ * DrupalDefaultEntityController class.
+ *
+ * @param array $entities
+ * An array of entity IDs or a single numeric ID.
+ */
+ public function deleteMultiple($entities) {
+ $basic_ids = array();
+ if (!empty($entities)) {
+ $transaction = db_transaction();
+ try {
+ foreach ($entities as $entity) {
+ // Invoke hook_entity_delete().
+ module_invoke_all('entity_delete', $entity, 'entity_example_basic');
+ field_attach_delete('entity_example_basic', $entity);
+ $basic_ids[] = $entity->basic_id;
+ }
+ db_delete('entity_example_basic')
+ ->condition('basic_id', $basic_ids, 'IN')
+ ->execute();
+ }
+ catch (Exception $e) {
+ $transaction->rollback();
+ watchdog_exception('entity_example', $e);
+ throw $e;
+ }
+ }
+ }
+}
+
+/**
+ * @} End of "defgroup entity_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/entity_example/entity_example.test b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.test
new file mode 100644
index 00000000..89ad1678
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/entity_example/entity_example.test
@@ -0,0 +1,162 @@
+ 'Entity example',
+ 'description' => 'Basic entity example tests',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('entity_example');
+
+ // Create and login user with access.
+ $permissions = array(
+ 'access content',
+ 'view any entity_example_basic entity',
+ 'edit any entity_example_basic entity',
+ 'create entity_example_basic entities',
+ 'administer entity_example_basic entities',
+ 'administer site configuration',
+ 'administer fields',
+ );
+ $account = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($account);
+
+ // Attach a field.
+ $field = array(
+ 'field_name' => 'entity_example_test_text' ,
+ 'type' => 'text',
+ );
+ field_create_field($field);
+ $instance = array(
+ 'label' => 'Subject',
+ 'field_name' => 'entity_example_test_text',
+ 'entity_type' => 'entity_example_basic',
+ 'bundle' => 'first_example_bundle',
+ );
+ field_create_instance($instance);
+ }
+
+ /**
+ * Test Entity Example features.
+ *
+ * - CRUD
+ * - Table display
+ * - User access
+ * - Field management
+ * - Display management
+ */
+ public function testEntityExampleBasic() {
+ // Create 10 entities.
+ for ($i = 1; $i <= 10; $i++) {
+ $edit[$i]['item_description'] = $this->randomName();
+ $edit[$i]['entity_example_test_text[und][0][value]'] = $this->randomName(32);
+
+ $this->drupalPost('examples/entity_example/basic/add', $edit[$i], 'Save');
+ $this->assertText('item_description=' . $edit[$i]['item_description']);
+
+ $this->drupalGet('examples/entity_example/basic/' . $i);
+ $this->assertText('item_description=' . $edit[$i]['item_description']);
+ $this->assertText($edit[$i]['entity_example_test_text[und][0][value]']);
+ }
+
+ // Delete entity 5.
+ $this->drupalPost('examples/entity_example/basic/5/edit', $edit[5], 'Delete');
+ $this->drupalGet('examples/entity_example/basic/5');
+ $this->assertResponse(404, 'Deleted entity 5 no longer exists');
+ unset($edit[5]);
+
+ // Update entity 2 and verify the update.
+ $edit[2] = array(
+ 'item_description' => 'updated entity 2 ',
+ 'entity_example_test_text[und][0][value]' => 'updated entity 2 test text',
+ );
+ $this->drupalPost('examples/entity_example/basic/2/edit', $edit[2], 'Save');
+ $this->assertText('item_description=' . $edit[2]['item_description']);
+ $this->assertText('updated entity 2 test text');
+
+ // View the entity list page and verify that the items which still exist
+ // are there, and that the deleted #5 no longer is there.
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ foreach ($edit as $id => $item) {
+ $this->assertRaw('examples/entity_example/basic/' . $id . '">' . $item['item_description'] . '');
+ }
+ $this->assertNoRaw('examples/entity_example/basic/5">');
+
+ // Add a field through the field UI and verify that it behaves correctly.
+ $field_edit = array(
+ 'fields[_add_new_field][label]' => 'New junk field',
+ 'fields[_add_new_field][field_name]' => 'new_junk_field',
+ 'fields[_add_new_field][type]' => 'text',
+ 'fields[_add_new_field][widget_type]' => 'text_textfield',
+ );
+ $this->drupalPost('admin/structure/entity_example_basic/manage/fields', $field_edit, t('Save'));
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+ $this->drupalPost(NULL, array(), t('Save settings'));
+ $this->assertResponse(200);
+
+ // Now verify that we can edit and view this entity with fields.
+ $edit[10]['field_new_junk_field[und][0][value]'] = $this->randomName();
+ $this->drupalPost('examples/entity_example/basic/10/edit', $edit[10], 'Save');
+ $this->assertResponse(200);
+ $this->assertText('item_description=' . $edit[10]['item_description']);
+ $this->assertText($edit[10]['field_new_junk_field[und][0][value]'], 'Custom field updated successfully');
+
+ // Create and login user without view access.
+ $account = $this->drupalCreateUser(array('access content'));
+ $this->drupalLogin($account);
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ $this->assertResponse(403);
+ $this->drupalGet('examples/entity_example/basic/2');
+ $this->assertResponse(403, 'User does not have permission to view entity');
+
+ // Create and login user with view access but no edit access.
+ $account = $this->drupalCreateUser(array('access content', 'view any entity_example_basic entity'));
+ $this->drupalLogin($account);
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ $this->assertResponse(403, 'Denied access to admin manage page');
+ $this->drupalGet('examples/entity_example/basic/2');
+ $this->assertResponse(200, 'User has permission to view entity');
+ $this->drupalGet('examples/entity_example/basic/2/edit');
+ $this->assertResponse(403, 'User is denied edit privileges');
+
+ // Create and login user with view and edit but no manage privs.
+ $account = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'view any entity_example_basic entity',
+ 'edit any entity_example_basic entity',
+ )
+ );
+ $this->drupalLogin($account);
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ $this->assertResponse(403, 'Denied access to admin manage page');
+ $this->drupalGet('examples/entity_example/basic/2');
+ $this->assertResponse(200, 'User has permission to view entity');
+ $this->drupalGet('examples/entity_example/basic/2/edit');
+ $this->assertResponse(200, 'User has edit privileges');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/examples.index.php b/sites/all/modules/contrib/dev/examples/examples.index.php
new file mode 100644
index 00000000..8056fee0
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/examples.index.php
@@ -0,0 +1,40 @@
+ array('type' => 'varchar', 'length' => 7, 'not null' => FALSE),
+ );
+ $indexes = array(
+ 'rgb' => array('rgb'),
+ );
+ return array(
+ 'columns' => $columns,
+ 'indexes' => $indexes,
+ );
+}
diff --git a/sites/all/modules/contrib/dev/examples/field_example/field_example.js b/sites/all/modules/contrib/dev/examples/field_example/field_example.js
new file mode 100644
index 00000000..3ea7a26a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_example/field_example.js
@@ -0,0 +1,25 @@
+/**
+ * @file
+ * Javascript for Field Example.
+ */
+
+/**
+ * Provides a farbtastic colorpicker for the fancier widget.
+ */
+(function ($) {
+ Drupal.behaviors.field_example_colorpicker = {
+ attach: function(context) {
+ $(".edit-field-example-colorpicker").live("focus", function(event) {
+ var edit_field = this;
+ var picker = $(this).closest('div').parent().find(".field-example-colorpicker");
+
+ // Hide all color pickers except this one.
+ $(".field-example-colorpicker").hide();
+ $(picker).show();
+ $.farbtastic(picker, function(color) {
+ edit_field.value = color;
+ }).setColor(edit_field.value);
+ });
+ }
+ }
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/field_example/field_example.module b/sites/all/modules/contrib/dev/examples/field_example/field_example.module
new file mode 100644
index 00000000..241d06f0
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_example/field_example.module
@@ -0,0 +1,389 @@
+ array(
+ 'label' => t('Example Color RGB'),
+ 'description' => t('Demonstrates a field composed of an RGB color.'),
+ 'default_widget' => 'field_example_3text',
+ 'default_formatter' => 'field_example_simple_text',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_validate().
+ *
+ * This hook gives us a chance to validate content that's in our
+ * field. We're really only interested in the $items parameter, since
+ * it holds arrays representing content in the field we've defined.
+ * We want to verify that the items only contain RGB hex values like
+ * this: #RRGGBB. If the item validates, we do nothing. If it doesn't
+ * validate, we add our own error notification to the $errors parameter.
+ *
+ * @see field_example_field_widget_error()
+ */
+function field_example_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
+ foreach ($items as $delta => $item) {
+ if (!empty($item['rgb'])) {
+ if (!preg_match('@^#[0-9a-f]{6}$@', $item['rgb'])) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'field_example_invalid',
+ 'message' => t('Color must be in the HTML format #abcdef.'),
+ );
+ }
+ }
+ }
+}
+
+
+/**
+ * Implements hook_field_is_empty().
+ *
+ * hook_field_is_empty() is where Drupal asks us if this field is empty.
+ * Return TRUE if it does not contain data, FALSE if it does. This lets
+ * the form API flag an error when required fields are empty.
+ */
+function field_example_field_is_empty($item, $field) {
+ return empty($item['rgb']);
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ *
+ * We need to tell Drupal that we have two different types of formatters
+ * for this field. One will change the text color, and the other will
+ * change the background color.
+ *
+ * @see field_example_field_formatter_view()
+ */
+function field_example_field_formatter_info() {
+ return array(
+ // This formatter just displays the hex value in the color indicated.
+ 'field_example_simple_text' => array(
+ 'label' => t('Simple text-based formatter'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ // This formatter changes the background color of the content region.
+ 'field_example_color_background' => array(
+ 'label' => t('Change the background of the output text'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * Two formatters are implemented.
+ * - field_example_simple_text just outputs markup indicating the color that
+ * was entered and uses an inline style to set the text color to that value.
+ * - field_example_color_background does the same but also changes the
+ * background color of div.region-content.
+ *
+ * @see field_example_field_formatter_info()
+ */
+function field_example_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+
+ switch ($display['type']) {
+ // This formatter simply outputs the field as text and with a color.
+ case 'field_example_simple_text':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ // We create a render array to produce the desired markup,
+ // "
The color code ... #hexcolor
".
+ // See theme_html_tag().
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#attributes' => array(
+ 'style' => 'color: ' . $item['rgb'],
+ ),
+ '#value' => t('The color code in this field is @code', array('@code' => $item['rgb'])),
+ );
+ }
+ break;
+
+ // This formatter adds css to the page changing the '.region-content' area's
+ // background color. If there are many fields, the last one will win.
+ case 'field_example_color_background':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => t('The content area color has been changed to @code', array('@code' => $item['rgb'])),
+ '#attached' => array(
+ 'css' => array(
+ array(
+ 'data' => 'div.region-content { background-color:' . $item['rgb'] . ';}',
+ 'type' => 'inline',
+ ),
+ ),
+ ),
+ );
+ }
+ break;
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * Three widgets are provided.
+ * - A simple text-only widget where the user enters the '#ffffff'.
+ * - A 3-textfield widget that gathers the red, green, and blue values
+ * separately.
+ * - A farbtastic colorpicker widget that chooses the value graphically.
+ *
+ * These widget types will eventually show up in hook_field_widget_form,
+ * where we will have to flesh them out.
+ *
+ * @see field_example_field_widget_form()
+ */
+function field_example_field_widget_info() {
+ return array(
+ 'field_example_text' => array(
+ 'label' => t('RGB value as #ffffff'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ 'field_example_3text' => array(
+ 'label' => t('RGB text field'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ 'field_example_colorpicker' => array(
+ 'label' => t('Color Picker'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * hook_widget_form() is where Drupal tells us to create form elements for
+ * our field's widget.
+ *
+ * We provide one of three different forms, depending on the widget type of
+ * the Form API item provided.
+ *
+ * The 'field_example_colorpicker' and 'field_example_text' are essentially
+ * the same, but field_example_colorpicker adds a javascript colorpicker
+ * helper.
+ *
+ * field_example_3text displays three text fields, one each for red, green,
+ * and blue. However, the field type defines a single text column,
+ * rgb, which needs an HTML color spec. Define an element validate
+ * handler that converts our r, g, and b fields into a simulated single
+ * 'rgb' form element.
+ */
+function field_example_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ $value = isset($items[$delta]['rgb']) ? $items[$delta]['rgb'] : '';
+
+ $widget = $element;
+ $widget['#delta'] = $delta;
+
+ switch ($instance['widget']['type']) {
+
+ case 'field_example_colorpicker':
+ $widget += array(
+ '#suffix' => '',
+ '#attributes' => array('class' => array('edit-field-example-colorpicker')),
+ '#attached' => array(
+ // Add Farbtastic color picker.
+ 'library' => array(
+ array('system', 'farbtastic'),
+ ),
+ // Add javascript to trigger the colorpicker.
+ 'js' => array(drupal_get_path('module', 'field_example') . '/field_example.js'),
+ ),
+ );
+ // DELIBERATE fall-through: From here on the field_example_text and
+ // field_example_colorpicker are exactly the same.
+ case 'field_example_text':
+ $widget += array(
+ '#type' => 'textfield',
+ '#default_value' => $value,
+ // Allow a slightly larger size that the field length to allow for some
+ // configurations where all characters won't fit in input field.
+ '#size' => 7,
+ '#maxlength' => 7,
+ );
+ break;
+
+ case 'field_example_3text':
+ // Convert rgb value into r, g, and b for #default_value.
+ if (!empty($value)) {
+ preg_match_all('@..@', substr($value, 1), $match);
+ }
+ else {
+ $match = array(array());
+ }
+
+ // Make this a fieldset with the three text fields.
+ $widget += array(
+ '#type' => 'fieldset',
+ '#element_validate' => array('field_example_3text_validate'),
+
+ // #delta is set so that the validation function will be able
+ // to access external value information which otherwise would be
+ // unavailable.
+ '#delta' => $delta,
+
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'field_example') . '/field_example.css'),
+ ),
+ );
+
+ // Create a textfield for saturation values for Red, Green, and Blue.
+ foreach (array('r' => t('Red'), 'g' => t('Green'), 'b' => t('Blue')) as $key => $title) {
+ $widget[$key] = array(
+ '#type' => 'textfield',
+ '#title' => $title,
+ '#size' => 2,
+ '#default_value' => array_shift($match[0]),
+ '#attributes' => array('class' => array('rgb-entry')),
+ '#description' => t('The 2-digit hexadecimal representation of @color saturation, like "a1" or "ff"', array('@color' => $title)),
+ );
+ // Since Form API doesn't allow a fieldset to be required, we
+ // have to require each field element individually.
+ if ($instance['required'] == 1) {
+ $widget[$key]['#required'] = 1;
+ }
+ }
+ break;
+
+ }
+
+ $element['rgb'] = $widget;
+ return $element;
+}
+
+
+/**
+ * Validate the individual fields and then convert to RGB string.
+ */
+function field_example_3text_validate($element, &$form_state) {
+ // @todo: Isn't there a better way to find out which element?
+ $delta = $element['#delta'];
+ $field = $form_state['field'][$element['#field_name']][$element['#language']]['field'];
+ $field_name = $field['field_name'];
+ if (isset($form_state['values'][$field_name][$element['#language']][$delta]['rgb'])) {
+ $values = $form_state['values'][$field_name][$element['#language']][$delta]['rgb'];
+ foreach (array('r', 'g', 'b') as $colorfield) {
+ $colorfield_value = hexdec($values[$colorfield]);
+ // If they left any empty, we'll set the value empty and quit.
+ if (strlen($values[$colorfield]) == 0) {
+ form_set_value($element, '', $form_state);
+ return;
+ }
+ // If they gave us anything that's not hex, reject it.
+ if ((strlen($values[$colorfield]) != 2) || $colorfield_value < 0 || $colorfield_value > 255) {
+ form_error($element[$colorfield], t("Saturation value must be a 2-digit hexadecimal value between 00 and ff."));
+ }
+ }
+
+ $value = sprintf('#%02s%02s%02s', $values['r'], $values['g'], $values['b']);
+ form_set_value($element, $value, $form_state);
+ }
+}
+
+/**
+ * Implements hook_field_widget_error().
+ *
+ * hook_field_widget_error() lets us figure out what to do with errors
+ * we might have generated in hook_field_validate(). Generally, we'll just
+ * call form_error().
+ *
+ * @see field_example_field_validate()
+ * @see form_error()
+ */
+function field_example_field_widget_error($element, $error, $form, &$form_state) {
+ switch ($error['error']) {
+ case 'field_example_invalid':
+ form_error($element, $error['message']);
+ break;
+ }
+}
+
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a simple user interface that tells the developer where to go.
+ */
+function field_example_menu() {
+ $items['examples/field_example'] = array(
+ 'title' => 'Field Example',
+ 'page callback' => '_field_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * A simple page to explain to the developer what to do.
+ */
+function _field_example_page() {
+ return t("The Field Example provides a field composed of an HTML RGB value, like #ff00ff. To use it, add the field to a content type.");
+}
+/**
+ * @} End of "defgroup field_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/field_example/field_example.test b/sites/all/modules/contrib/dev/examples/field_example/field_example.test
new file mode 100644
index 00000000..6baf973d
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_example/field_example.test
@@ -0,0 +1,184 @@
+ 'Field Example',
+ 'description' => 'Create a content type with example_field_rgb fields, create a node, check for correct values.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the email_example module.
+ parent::setUp(array('field_ui', 'field_example'));
+ }
+
+ /**
+ * Test basic functionality of the example field.
+ *
+ * - Creates a content type.
+ * - Adds a single-valued field_example_rgb to it.
+ * - Adds a multivalued field_example_rgb to it.
+ * - Creates a node of the new type.
+ * - Populates the single-valued field.
+ * - Populates the multivalued field with two items.
+ * - Tests the result.
+ */
+ public function testExampleFieldBasic() {
+ $content_type_machine = strtolower($this->randomName(10));
+ $title = $this->randomName(20);
+
+ // Create and login user.
+ $account = $this->drupalCreateUser(array('administer content types', 'administer fields'));
+ $this->drupalLogin($account);
+
+ $this->drupalGet('admin/structure/types');
+
+ // Create the content type.
+ $this->clickLink(t('Add content type'));
+
+ $edit = array(
+ 'name' => $content_type_machine,
+ '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_machine)));
+
+ $single_text_field = strtolower($this->randomName(10));
+ $single_colorpicker_field = strtolower($this->randomName(10));
+ $single_3text_field = strtolower($this->randomName(10));
+ $multivalue_3text_field = strtolower($this->randomName(10));
+
+ // Description of fields to be created;
+ $fields[$single_text_field] = array(
+ 'widget' => 'field_example_text',
+ 'cardinality' => '1',
+ );
+ $fields[$single_colorpicker_field] = array(
+ 'widget' => 'field_example_colorpicker',
+ 'cardinality' => 1,
+ );
+ $fields[$single_3text_field] = array(
+ 'widget' => 'field_example_3text',
+ 'cardinality' => 1,
+ );
+ $fields[$multivalue_3text_field] = array(
+ 'widget' => 'field_example_3text',
+ 'cardinality' => -1,
+ );
+
+ foreach ($fields as $fieldname => $details) {
+ $this->createField($fieldname, $details['widget'], $details['cardinality']);
+ }
+
+ // Somehow clicking "save" isn't enough, and we have to do a
+ // node_types_rebuild().
+ node_types_rebuild();
+ 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.');
+
+ $permission = 'create ' . $content_type_machine . ' content';
+ // Reset the permissions cache.
+ $this->checkPermissions(array($permission), TRUE);
+
+ // Now that we have a new content type, create a user that has privileges
+ // on the content type.
+ $account = $this->drupalCreateUser(array($permission));
+ $this->drupalLogin($account);
+
+ $this->drupalGet('node/add/' . $content_type_machine);
+
+ // Add a node.
+ $edit = array(
+ 'title' => $title,
+ 'field_' . $single_text_field . '[und][0][rgb]' => '#000001',
+ 'field_' . $single_colorpicker_field . '[und][0][rgb]' => '#000002',
+
+ 'field_' . $single_3text_field . '[und][0][rgb][r]' => '00',
+ 'field_' . $single_3text_field . '[und][0][rgb][g]' => '00',
+ 'field_' . $single_3text_field . '[und][0][rgb][b]' => '03',
+
+ 'field_' . $multivalue_3text_field . '[und][0][rgb][r]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][0][rgb][g]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][0][rgb][b]' => '04',
+
+ );
+ // We want to add a 2nd item to the multivalue field, so hit "add another".
+ $this->drupalPost(NULL, $edit, t('Add another item'));
+
+ $edit = array(
+ 'field_' . $multivalue_3text_field . '[und][1][rgb][r]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][1][rgb][g]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][1][rgb][b]' => '05',
+ );
+ // Now we can fill in the second item in the multivalue field and save.
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertText(t('@content_type_machine @title has been created', array('@content_type_machine' => $content_type_machine, '@title' => $title)));
+
+ $output_strings = $this->xpath("//div[contains(@class,'field-type-field-example-rgb')]/div/div/p/text()");
+
+ $this->assertEqual((string) $output_strings[0], "The color code in this field is #000001");
+ $this->assertEqual((string) $output_strings[1], "The color code in this field is #000002");
+ $this->assertEqual((string) $output_strings[2], "The color code in this field is #000003");
+ $this->assertEqual((string) $output_strings[3], "The color code in this field is #000004");
+ $this->assertEqual((string) $output_strings[4], "The color code in this field is #000005");
+ }
+
+ /**
+ * Utility function to create fields on a content type.
+ *
+ * @param string $field_name
+ * Name of the field, like field_something
+ * @param string $widget_type
+ * Widget type, like field_example_3text
+ * @param int $cardinality
+ * Cardinality
+ */
+ protected function createField($field_name, $widget_type, $cardinality) {
+ // Add a singleton field_example_text field.
+ $edit = array(
+ 'fields[_add_new_field][label]' => $field_name,
+ 'fields[_add_new_field][field_name]' => $field_name,
+ 'fields[_add_new_field][type]' => 'field_example_rgb',
+ 'fields[_add_new_field][widget_type]' => $widget_type,
+
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // There are no settings for this, so just press the button.
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+
+ $edit = array('field[cardinality]' => (string) $cardinality);
+
+ // Using all the default settings, so press the button.
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+ debug(
+ t('Saved settings for field %field_name with widget %widget_type and cardinality %cardinality',
+ array(
+ '%field_name' => $field_name,
+ '%widget_type' => $widget_type,
+ '%cardinality' => $cardinality,
+ )
+ )
+ );
+ $this->assertText(t('Saved @name configuration.', array('@name' => $field_name)));
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.css b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.css
new file mode 100644
index 00000000..59cda31e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.css
@@ -0,0 +1,22 @@
+/**
+ * @file
+ * CSS for Field Example.
+ */
+.stickynote {
+background:#fefabc;
+padding:0.8em;
+font-family:cursive;
+font-size:1.1em;
+color: #000;
+width:15em;
+
+-moz-transform: rotate(2deg);
+-webkit-transform: rotate(2deg);
+-o-transform: rotate(2deg);
+-ms-transform: rotate(2deg);
+transform: rotate(2deg);
+
+box-shadow: 0px 4px 6px #333;
+-moz-box-shadow: 0px 4px 6px #333;
+-webkit-box-shadow: 0px 4px 6px #333;
+}
diff --git a/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.info b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.info
new file mode 100644
index 00000000..6f538f72
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.info
@@ -0,0 +1,12 @@
+name = Field Permission Example
+description = A Field API Example: Fieldnote with Permissions
+package = Example modules
+core = 7.x
+files[] = tests/field_permission_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.install b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.install
new file mode 100644
index 00000000..55388d20
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.install
@@ -0,0 +1,32 @@
+ array('type' => 'text', 'size' => 'normal', 'not null' => FALSE),
+ );
+ return array(
+ 'columns' => $columns,
+ );
+}
diff --git a/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.module b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.module
new file mode 100644
index 00000000..a1a96139
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_permission_example/field_permission_example.module
@@ -0,0 +1,329 @@
+ t('View own fieldnote'));
+ $perms['edit own fieldnote'] = array('title' => t('Edit own fieldnote'));
+ $perms['view any fieldnote'] = array('title' => t('View any fieldnote'));
+ $perms['edit any fieldnote'] = array('title' => t('Edit any fieldnote'));
+
+ return $perms;
+}
+
+/**
+ * Implements hook_field_access().
+ *
+ * We want to make sure that fields aren't being seen or edited
+ * by those who shouldn't.
+ *
+ * We have to build a permission string similar to those in
+ * hook_permission() in order to ask Drupal whether the user
+ * has that permission. Permission strings will end up being
+ * like 'view any fieldnote' or 'edit own fieldnote'.
+ *
+ * The tricky thing here is that a field can be attached to any type
+ * of entity, so it's not always trivial to figure out whether
+ * $account 'owns' the entity. We'll support access restrictions for
+ * user and node entity types, and be permissive with others,
+ * since that's easy to demonstrate.
+ *
+ * @see field_permission_example_permissions()
+ */
+function field_permission_example_field_access($op, $field, $entity_type, $entity, $account) {
+ // This hook will be invoked for every field type, so we have to
+ // check that it's the one we're interested in.
+ if ($field['type'] == 'field_permission_example_fieldnote') {
+ // First we'll check if the user has the 'superuser'
+ // permissions that node provides. This way administrators
+ // will be able to administer the content types.
+ if (user_access('bypass node access', $account)) {
+ drupal_set_message(t('User can bypass node access.'));
+ return TRUE;
+ }
+ if (user_access('administer content types', $account)) {
+ drupal_set_message(t('User can administer content types.'));
+ return TRUE;
+ }
+ // Now check for our own permissions.
+ // $context will end up being either 'any' or 'own.'
+ $context = 'any';
+ switch ($entity_type) {
+ case 'user':
+ case 'node':
+ // While administering the field itself, $entity will be
+ // NULL, so we have to check it.
+ if ($entity) {
+ if ($entity->uid == $account->uid) {
+ $context = 'own';
+ }
+ }
+ }
+ // Assemble a permission string, such as
+ // 'view any fieldnote'
+ $permission = $op . ' ' . $context . ' fieldnote';
+ // Finally, ask Drupal if this account has that permission.
+ $access = user_access($permission, $account);
+ $status = 'FALSE';
+ if ($access) {
+ $status = 'TRUE';
+ }
+ drupal_set_message($permission . ': ' . $status);
+ return $access;
+ }
+ // We have no opinion on field types other than our own.
+ return TRUE;
+}
+
+/**
+ * Implements hook_field_info().
+ *
+ * Provides the description of the field.
+ */
+function field_permission_example_field_info() {
+ return array(
+ // We name our field as the associative name of the array.
+ 'field_permission_example_fieldnote' => array(
+ 'label' => t('Fieldnote'),
+ 'description' => t('Place a note-taking field on entities, with granular permissions.'),
+ 'default_widget' => 'field_permission_example_widget',
+ 'default_formatter' => 'field_permission_example_formatter',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_is_empty().
+ *
+ * hook_field_is_empty() is where Drupal asks us if this field is empty.
+ * Return TRUE if it does not contain data, FALSE if it does. This lets
+ * the form API flag an error when required fields are empty.
+ */
+function field_permission_example_field_is_empty($item, $field) {
+ return empty($item['notes']);
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ *
+ * We need to tell Drupal about our excellent field formatter.
+ *
+ * It's some text in a div, styled to look like a sticky note.
+ *
+ * @see field_permission_example_field_formatter_view()
+ */
+function field_permission_example_field_formatter_info() {
+ return array(
+ // This formatter simply displays the text in a text field.
+ 'field_permission_example_formatter' => array(
+ 'label' => t('Simple text-based formatter'),
+ 'field types' => array('field_permission_example_fieldnote'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * Here we output the field for general consumption.
+ *
+ * The field will have a sticky note appearance, thanks to some
+ * simple CSS.
+ *
+ * Note that all of the permissions and access logic happens
+ * in hook_field_access(), and none of it is here.
+ */
+function field_permission_example_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+
+ switch ($display['type']) {
+ case 'field_permission_example_formatter':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ // We wrap the fieldnote content up in a div tag.
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ '#value' => check_plain($item['notes']),
+ // Let's give the note a nice sticky-note CSS appearance.
+ '#attributes' => array(
+ 'class' => 'stickynote',
+ ),
+ // ..And this is the CSS for the stickynote.
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'field_permission_example') .
+ '/field_permission_example.css'),
+ ),
+ );
+ }
+ break;
+ }
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * We're implementing just one widget: A basic textarea.
+ *
+ * @see field_permission_example_field_widget_form()
+ */
+function field_permission_example_field_widget_info() {
+ return array(
+ 'field_permission_example_widget' => array(
+ 'label' => t('Field note textarea'),
+ 'field types' => array('field_permission_example_fieldnote'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * Drupal wants us to create a form for our field. We'll use
+ * something very basic like a default textarea.
+ *
+ * @see field_permission_example_field_widget_info()
+ */
+function field_permission_example_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ // Grab the existing value for the field.
+ $value = isset($items[$delta]['notes']) ? $items[$delta]['notes'] : '';
+ // Grab a reference to the form element.
+ $widget = $element;
+ // Set up the delta for our return element.
+ $widget['#delta'] = $delta;
+
+ // Figure out which widget we need to generate.
+ // In our case, there's only one type.
+ switch ($instance['widget']['type']) {
+ case 'field_permission_example_widget':
+ $widget += array(
+ '#type' => 'textarea',
+ '#default_value' => $value,
+ );
+ break;
+ }
+
+ $element['notes'] = $widget;
+ return $element;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a simple user interface that gives the developer some clues.
+ */
+function field_permission_example_menu() {
+ $items['examples/field_permission_example'] = array(
+ 'title' => 'Field Permission Example',
+ 'page callback' => '_field_permission_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * A simple page to explain to the developer what to do.
+ *
+ * @see field_permission_example.module
+ */
+function _field_permission_example_page() {
+ $page = t("
The Field Permission Example module shows how you can restrict view and edit permissions within your field implementation. It adds a new field type called Fieldnote. Fieldnotes appear as simple text boxes on the create/edit form, and as sticky notes when viewed. By 'sticky note' we mean 'Post-It Note' but that's a trademarked term.
To see this field in action, add it to a content type or user profile. Go to the permissions page (");
+ $page .= l(t('admin/people/permissions'), 'admin/people/permissions');
+ $page .= t(") and look at the 'Field Permission Example' section. This allows you to change which roles can see and edit Fieldnote fields.
Creating different users with different capabilities will let you see these behaviors in action. Fieldnote helpfully displays a message telling you which permissions it is trying to resolve for the current field/user combination.
Definitely look through the code to see various implementation details.
");
+ return $page;
+}
+/**
+ * @} End of "defgroup field_permission_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/field_permission_example/tests/field_permission_example.test b/sites/all/modules/contrib/dev/examples/field_permission_example/tests/field_permission_example.test
new file mode 100644
index 00000000..7e6c9b71
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/field_permission_example/tests/field_permission_example.test
@@ -0,0 +1,572 @@
+ 'Generic Field Test',
+ 'description' => 'Someone neglected to override GenericFieldTest::getInfo().',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Supply the field types we wish to test.
+ *
+ * Return an array of field types to instantiate and test.
+ *
+ * @return array
+ * The field types we wish to use.
+ */
+ protected function getFieldTypes() {
+ return array('these_are_not', 'valid_field_types', 'please_override');
+ }
+
+ /**
+ * The module to enable.
+ *
+ * @return string
+ * Module machine name.
+ */
+ protected function getModule() {
+ return 'this-is-not-a-module-name-please-override';
+ }
+
+ /**
+ * Simpletest's setUp().
+ *
+ * We want to be able to subclass this class, so we jump
+ * through a few hoops in order to get the modules from args
+ * and add our own.
+ */
+ public function setUp() {
+ $this->instanceNames = array();
+ $modules = func_get_args();
+ if (isset($modules[0]) && is_array($modules[0])) {
+ $modules = $modules[0];
+ }
+ $modules[] = 'node';
+ $modules[] = 'field_ui';
+ parent::setUp($modules);
+ }
+
+ /**
+ * Verify that all required fields are specified in hook_field_info().
+ *
+ * The full list is label, description, settings, instance_settings,
+ * default_widget, default_formatter, no_ui.
+ *
+ * Some are optional, and we won't check for those.
+ *
+ * In a sane world, this would be a unit test, rather than a
+ * web test, but module_implements is unavailable to us
+ * in unit tests.
+ *
+ * @see hook_field_info()
+ */
+ public function runTestGenericFieldInfo() {
+ $field_types = $this->getFieldTypes();
+ $module = $this->getModule();
+ $info_keys = array(
+ 'label',
+ 'description',
+ 'default_widget',
+ 'default_formatter',
+ );
+ // We don't want to use field_info_field_types()
+ // because there is a hook_field_info_alter().
+ // We're testing the module here, not the rest of
+ // the system. So invoke hook_field_info() ourselves.
+ $modules = module_implements('field_info');
+ $this->assertTrue(in_array($module, $modules),
+ 'Module ' . $module . ' implements hook_field_info()');
+
+ foreach ($field_types as $field_type) {
+ $field_info = module_invoke($module, 'field_info');
+ $this->assertTrue(isset($field_info[$field_type]),
+ 'Module ' . $module . ' defines field type ' . $field_type);
+ $field_info = $field_info[$field_type];
+ foreach ($info_keys as $key) {
+ $this->assertTrue(
+ isset($field_info[$key]),
+ $field_type . "'s " . $key . ' is set.'
+ );
+ }
+ }
+ }
+
+ /**
+ * Add all testable fields as instances to a content type.
+ *
+ * As a side-effect: Store the names of the instances created
+ * in $this->$instance_names.
+ *
+ * @param object $node_type
+ * A content type object. If none is provided, one will be generated.
+ *
+ * @return object
+ * The content type object that has the fields attached.
+ */
+ public function codeTestGenericAddAllFields($node_type = NULL) {
+ $this->instanceNames = array();
+ if (!$node_type) {
+ $node_type = $this->drupalCreateContentType();
+ }
+ foreach ($this->getFieldTypes() as $field_type) {
+ $instance_name = drupal_strtolower($this->randomName(32));
+ $field = array(
+ 'field_name' => $instance_name,
+ 'type' => $field_type,
+ );
+ $field = field_create_field($field);
+ $instance = array(
+ 'field_name' => $instance_name,
+ 'entity_type' => 'node',
+ 'bundle' => $node_type->name,
+ 'label' => drupal_strtolower($this->randomName(20)),
+ );
+ // Finally create the instance.
+ $instance = field_create_instance($instance);
+ // Reset the caches...
+ _field_info_collate_fields(TRUE);
+ // Grab this instance.
+ $verify_instance = field_info_instance('node', $instance_name, $node_type->name);
+ $this->assertTrue($verify_instance, 'Instance object exists.');
+ $this->assertTrue(
+ $verify_instance != NULL,
+ 'field_info_instance() says ' . $instance_name . ' (' . $node_type->name . ') was created.'
+ );
+ $this->instanceNames[] = $instance_name;
+ }
+ return $node_type;
+ }
+
+ /**
+ * Remove all fields in $this->field_names.
+ *
+ * @param mixed $node_type
+ * A content type object. If none is specified,
+ * the test fails.
+ */
+ public function codeTestGenericRemoveAllFields($node_type = NULL) {
+ if (!$node_type) {
+ $this->fail('No node type.');
+ }
+ if (count($this->instanceNames) < 1) {
+ $this->fail('There are no instances to remove.');
+ return;
+ }
+ foreach ($this->instanceNames as $instance_name) {
+ $instance = field_info_instance('node', $instance_name, $node_type->name);
+ $this->assertTrue($instance, "Instance exists, now we'll delete it.");
+ field_delete_field($instance_name);
+ $instance = field_info_instance('node', $instance_name, $node_type->name);
+ $this->assertFalse($instance, 'Instance was deleted.');
+ }
+ $this->instanceNames = array();
+ }
+
+ /**
+ * Add and delete all field types through Form API.
+ *
+ * @access public
+ */
+ public function formTestGenericFieldNodeAddDeleteForm() {
+ // Create and login user.
+ $account = $this->drupalCreateUser(array(
+ 'administer content types',
+ 'administer fields',
+ ));
+ $this->drupalLogin($account);
+
+ // Add a content type.
+ $node_type = $this->drupalCreateContentType();
+
+ // Add all our testable fields.
+ $field_names = $this->formAddAllFields($node_type);
+
+ // Now let's delete all the fields.
+ foreach ($field_names as $field_name) {
+ // This is the path for the 'delete' link on field admin page.
+ $this->drupalGet('admin/structure/types/manage/' .
+ $node_type->name . '/fields/field_' . $field_name . '/delete');
+ // Click the 'delete' button.
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->assertText(t('The field @field has been deleted from the @type content type.',
+ array('@field' => $field_name, '@type' => $node_type->name)));
+ }
+ }
+
+ /**
+ * Add all fields using Form API.
+ *
+ * @param mixed $node_type
+ * A content type object. If none is specified,
+ * the test fails.
+ */
+ protected function formAddAllFields($node_type = NULL) {
+ if (!$node_type) {
+ $this->fail('No content type specified.');
+ }
+ // Get all our field types.
+ $field_types = $this->getFieldTypes();
+ // Keep a list of no_ui fields so we can tell the user.
+ $unsafe_field_types = array();
+ $field_names = array();
+
+ $manage_path = 'admin/structure/types/manage/' . $node_type->name . '/fields';
+ foreach ($field_types as $field_type) {
+ // Get the field info.
+ $field_info = field_info_field_types($field_type);
+ // Exclude no_ui field types.
+ if (isset($field_info['no_ui']) && $field_info['no_ui']) {
+ $unsafe_field_types[] = $field_type;
+ }
+ else {
+ // Generate a name for our field.
+ // 26 is max length for field name.
+ $field_name = drupal_strtolower($this->randomName(26));
+ $field_names[$field_type] = $field_name;
+ // Create the field through Form API.
+ $this->formCreateField($manage_path, $field_type, $field_name,
+ $field_info['default_widget'], 1);
+ }
+ }
+
+ // Tell the user which fields we couldn't test.
+ if (!empty($unsafe_field_types)) {
+ debug(
+ 'Unable to attach these no_ui fields: ' .
+ implode(', ', $unsafe_field_types)
+ );
+ }
+
+ // Somehow clicking "save" isn't enough, and we have to
+ // rebuild a few caches.
+ node_types_rebuild();
+ menu_rebuild();
+ return $field_names;
+ }
+
+ /**
+ * Create a field using the content type management form.
+ *
+ * @param mixed $manage_path
+ * Path to our content type management form.
+ * @param mixed $field_type
+ * The type of field we're adding.
+ * @param mixed $field_name
+ * The name of the field instance we want.
+ * @param mixed $widget_type
+ * Which widget would we like?
+ * @param mixed $cardinality
+ * Cardinality for this field instance.
+ */
+ protected function formCreateField($manage_path, $field_type, $field_name, $widget_type, $cardinality) {
+ // $manage_path is the field edit form for our content type.
+ $this->drupalGet($manage_path);
+ $edit = array(
+ 'fields[_add_new_field][label]' => $field_name,
+ 'fields[_add_new_field][field_name]' => $field_name,
+ 'fields[_add_new_field][type]' => $field_type,
+ 'fields[_add_new_field][widget_type]' => $widget_type,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // Assume there are no settings for this,
+ // so just press the button.
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+
+ $edit = array('field[cardinality]' => (string) $cardinality);
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+
+ debug(
+ t('Saved settings for field !field_name with widget !widget_type and cardinality !cardinality',
+ array(
+ '!field_name' => $field_name,
+ '!widget_type' => $widget_type,
+ '!cardinality' => $cardinality,
+ )
+ )
+ );
+
+ $this->assertText(t('Saved @name configuration.', array('@name' => $field_name)));
+ }
+
+ /**
+ * Create a node with some field content.
+ *
+ * @return object
+ * Node object for the created node.
+ */
+ public function createFieldContentForUser(
+ $account = NULL,
+ $content = 'testable_content',
+ $node_type = NULL,
+ $instance_name = '',
+ $column = NULL
+ ) {
+ if (!$column) {
+ $this->fail('No column name given.');
+ return NULL;
+ }
+ if (!$account) {
+ $account = $this->drupalCreateUser(array(
+ 'bypass node access',
+ 'administer content types',
+ ));
+ }
+ $this->drupalLogin($account);
+
+ if (!$node_type) {
+ $node_type = $this->codeTestGenericAddAllFields();
+ }
+
+ if (!$instance_name) {
+ $instance_name = reset($this->instanceNames);
+ }
+ $field = array();
+ $field[LANGUAGE_NONE][0][$column] = $content;
+
+ $settings = array(
+ 'type' => $node_type->name,
+ $instance_name => $field,
+ );
+ $node = $this->drupalCreateNode($settings);
+
+ $this->assertTrue($node, 'Node of type ' . $node->type . ' allegedly created.');
+
+ $node = node_load($node->nid);
+ debug('Loaded node id: ' . $node->nid);
+ $this->assertTrue($node->$instance_name, 'Field actually created.');
+ $field = $node->$instance_name;
+ $this->assertTrue($field[LANGUAGE_NONE][0][$column] == $content,
+ 'Content was stored properly on the field.');
+ return $node;
+ }
+
+}
+
+class FieldTestPermissionsExample extends GenericFieldTest {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp(array('field_permission_example'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field Permission Example',
+ 'description' => 'Various tests on the functionality of the Fieldnote field.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getFieldTypes() {
+ return array('field_permission_example_fieldnote');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getModule() {
+ return 'field_permission_example';
+ }
+
+ /**
+ * Override createFieldContentForUser().
+ *
+ * We override so we can make sure $column is set to 'notes'.
+ */
+ public function createFieldContentForUser(
+ $account = NULL,
+ $content = 'fieldnote_testable_content',
+ $node_type = NULL,
+ $instance_name = '',
+ $column = 'notes'
+ ) {
+ return parent::createFieldContentForUser($account, $content, $node_type, $instance_name, $column);
+ }
+
+
+ /**
+ * Test of hook_field_info() and other implementation requirements.
+ *
+ * @see GenericFieldTest::runTestGenericFieldInfo()
+ */
+ public function testFieldnoteInfo() {
+ $this->runTestGenericFieldInfo();
+ }
+
+ /**
+ * Add and remove the field through Form API.
+ */
+ public function testAddRemoveFieldnoteForm() {
+ $this->formTestGenericFieldNodeAddDeleteForm();
+ }
+
+ /**
+ * Add and remove the field through code.
+ */
+ public function testAddRemoveFieldnoteCode() {
+ $node_type = $this->codeTestGenericAddAllFields();
+ $this->codeTestGenericRemoveAllFields($node_type);
+ }
+
+ /**
+ * Test view permissions.
+ */
+ public function testFieldnoteViewPerms() {
+ // We create two sets of content so we can get a few
+ // test cases out of the way.
+ $view_own_content = $this->randomName(23);
+ $view_any_content = $this->randomName(23);
+ $view_own_node = $this->createFieldContentForUser(NULL, $view_own_content);
+ // Get the type of the node so we can create another one.
+ $node_type = node_type_load($view_own_node->type);
+ $view_any_node = $this->createFieldContentForUser(NULL, $view_any_content, $node_type);
+
+ // There should be a node now, with some lovely content, but it's the wrong
+ // user for the view-own test.
+ $view_own_account = $this->drupalCreateUser(array(
+ 'view own fieldnote',
+ ));
+ debug("Created user with 'view own fieldnote' permission.");
+
+ // Now change the user id for the test node.
+ $view_own_node = node_load($view_own_node->nid);
+ $view_own_node->uid = $view_own_account->uid;
+ node_save($view_own_node);
+ $view_own_node = node_load($view_own_node->nid);
+ $this->assertTrue($view_own_node->uid == $view_own_account->uid, 'New user assigned to node.');
+
+ // Now we want to look at the page with the field and
+ // check that we can see it.
+ $this->drupalLogin($view_own_account);
+
+ $this->drupalGet('node/' . $view_own_node->nid);
+ // Check that the field content is present.
+ $output_strings = $this->xpath("//div[contains(@class,'stickynote')]/text()");
+ $this->assertEqual((string) reset($output_strings), $view_own_content);
+ debug("'view own fieldnote' can view own field.");
+
+ // This account shouldn't be able to see the field on the
+ // 'view any' node.
+ $this->drupalGet('node/' . $view_any_node->nid);
+ // Check that the field content is not present.
+ $output_strings = $this->xpath("//div[contains(@class,'stickynote')]/text()");
+ $this->assertNotEqual((string) reset($output_strings), $view_any_content);
+ debug("'view own fieldnote' cannot view other field.");
+
+ // Now, to test for 'view any fieldnote' we create another user
+ // with that permission, and try to look at the same node.
+ $view_any_account = $this->drupalCreateUser(array(
+ 'view any fieldnote',
+ ));
+ debug("Created user with 'view any fieldnote' permission.");
+ $this->drupalLogin($view_any_account);
+ // This account should be able to see the field on the
+ // 'view any' node.
+ $this->drupalGet('node/' . $view_any_node->nid);
+ // Check that the field content is present.
+ $output_strings = $this->xpath("//div[contains(@class,'stickynote')]/text()");
+ $this->assertEqual((string) reset($output_strings), $view_any_content);
+ debug("'view any fieldnote' can view other field.");
+ }
+
+ /**
+ * Test edit permissions.
+ *
+ * Note that this is mostly identical to testFieldnoteViewPerms() and could
+ * probably be refactored.
+ */
+ public function testFieldnoteEditPerms() {
+ // We create two sets of content so we can get a few
+ // test cases out of the way.
+ $edit_own_content = $this->randomName(23);
+ $edit_any_content = $this->randomName(23);
+ $edit_own_node = $this->createFieldContentForUser(NULL, $edit_own_content);
+ // Get the type of the node so we can create another one.
+ $node_type = node_type_load($edit_own_node->type);
+ $edit_any_node = $this->createFieldContentForUser(NULL, $edit_any_content, $node_type);
+
+ $edit_own_account = $this->drupalCreateUser(array(
+ 'edit own ' . $node_type->name . ' content',
+ 'edit own fieldnote',
+ ));
+ debug("Created user with 'edit own fieldnote' permission.");
+
+ // Now change the user id for the test node.
+ $edit_own_node = node_load($edit_own_node->nid);
+ $edit_own_node->uid = $edit_own_account->uid;
+ node_save($edit_own_node);
+ $edit_own_node = node_load($edit_own_node->nid);
+ $this->assertTrue($edit_own_node->uid == $edit_own_account->uid, 'New edit test user assigned to node.');
+
+ // Now we want to look at the page with the field and
+ // check that we can see it.
+ $this->drupalLogin($edit_own_account);
+
+ $this->drupalGet('node/' . $edit_own_node->nid . '/edit');
+ $this->assertText($edit_own_content, "'edit own fieldnote' can edit own fieldnote.");
+
+ // This account shouldn't be able to edit the field on the
+ // 'edit any' node.
+ $this->drupalGet('node/' . $edit_any_node->nid . '/edit');
+ $this->assertNoText($edit_any_content, "'edit own fieldnote' can not edit any fieldnote.");
+
+ // Now, to test for 'edit any fieldnote' we create another user
+ // with that permission, and try to edit at the same node.
+ // We have to add the ability to edit any node content, as well
+ // or Drupal will deny us access to the page.
+ $edit_any_account = $this->drupalCreateUser(array(
+ 'edit any ' . $node_type->name . ' content',
+ 'edit any fieldnote',
+ ));
+ debug("Created user with 'edit any fieldnote' permission.");
+ $this->drupalLogin($edit_any_account);
+ // This account should be able to see the field on the
+ // 'edit any' node.
+ $this->drupalGet('node/' . $edit_any_node->nid . '/edit');
+ $this->assertText($edit_any_content, "'edit any fieldnote' can edit any fieldnote.");
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/file_example/file_example.info b/sites/all/modules/contrib/dev/examples/file_example/file_example.info
new file mode 100644
index 00000000..92d81e5e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/file_example/file_example.info
@@ -0,0 +1,13 @@
+name = File example
+description = Examples of using the Drupal File API and Stream Wrappers.
+package = Example modules
+core = 7.x
+files[] = file_example_session_streams.inc
+files[] = file_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/file_example/file_example.module b/sites/all/modules/contrib/dev/examples/file_example/file_example.module
new file mode 100644
index 00000000..c08bd746
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/file_example/file_example.module
@@ -0,0 +1,570 @@
+ 'File Example',
+ 'page callback' => 'file_example_intro',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+ $items['examples/file_example/fileapi'] = array(
+ 'title' => 'Use File API to read/write a file',
+ 'page callback' => 'drupal_get_form',
+ 'access arguments' => array('use file example'),
+ 'page arguments' => array('file_example_readwrite'),
+ );
+ $items['examples/file_example/access_session'] = array(
+ 'page callback' => 'file_example_session_contents',
+ 'access arguments' => array('use file example'),
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+}
+
+
+/**
+ * Implements hook_permission().
+ */
+function file_example_permission() {
+ return array(
+ 'use file example' => array(
+ 'title' => t('Use the examples in the File Example module'),
+ ),
+ );
+}
+
+/**
+ * A simple introduction to the workings of this module.
+ */
+function file_example_intro() {
+ $markup = t('The file example module provides a form and code to demonstrate the Drupal 7 file api. Experiment with the form, and then look at the submit handlers in the code to understand the file api.');
+ return array('#markup' => $markup);
+}
+/**
+ * Form builder function.
+ *
+ * A simple form that allows creation of a file, managed or unmanaged. It
+ * also allows reading/deleting a file and creation of a directory.
+ */
+function file_example_readwrite($form, &$form_state) {
+ if (empty($_SESSION['file_example_default_file'])) {
+ $_SESSION['file_example_default_file'] = 'session://drupal.txt';
+ }
+ $default_file = $_SESSION['file_example_default_file'];
+ if (empty($_SESSION['file_example_default_directory'])) {
+ $_SESSION['file_example_default_directory'] = 'session://directory1';
+ }
+ $default_directory = $_SESSION['file_example_default_directory'];
+
+ $form['write_file'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Write to a file'),
+ );
+ $form['write_file']['write_contents'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter something you would like to write to a file') . ' ' . date('m'),
+ '#default_value' => t('Put some text here or just use this text'),
+ );
+
+ $form['write_file']['destination'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $default_file,
+ '#title' => t('Optional: Enter the streamwrapper saying where it should be written'),
+ '#description' => t('This may be public://some_dir/test_file.txt or private://another_dir/some_file.txt, for example. If you include a directory, it must already exist. The default is "public://". Since this example supports session://, you can also use something like session://somefile.txt.'),
+ );
+
+ $form['write_file']['managed_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Write managed file'),
+ '#submit' => array('file_example_managed_write_submit'),
+ );
+ $form['write_file']['unmanaged_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Write unmanaged file'),
+ '#submit' => array('file_example_unmanaged_write_submit'),
+ );
+ $form['write_file']['unmanaged_php'] = array(
+ '#type' => 'submit',
+ '#value' => t('Unmanaged using PHP'),
+ '#submit' => array('file_example_unmanaged_php_submit'),
+ );
+
+ $form['fileops'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Read from a file'),
+ );
+ $form['fileops']['fileops_file'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $default_file,
+ '#title' => t('Enter the URI of a file'),
+ '#description' => t('This must be a stream-type description like public://some_file.txt or http://drupal.org or private://another_file.txt or (for this example) session://yet_another_file.txt.'),
+ );
+ $form['fileops']['read_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Read the file and store it locally'),
+ '#submit' => array('file_example_read_submit'),
+ );
+ $form['fileops']['delete_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete file'),
+ '#submit' => array('file_example_delete_submit'),
+ );
+ $form['fileops']['check_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Check to see if file exists'),
+ '#submit' => array('file_example_file_check_exists_submit'),
+ );
+
+ $form['directory'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Create or prepare a directory'),
+ );
+
+ $form['directory']['directory_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Directory to create/prepare/delete'),
+ '#default_value' => $default_directory,
+ '#description' => t('This is a directory as in public://some/directory or private://another/dir.'),
+ );
+ $form['directory']['create_directory'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create directory'),
+ '#submit' => array('file_example_create_directory_submit'),
+ );
+ $form['directory']['delete_directory'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete directory'),
+ '#submit' => array('file_example_delete_directory_submit'),
+ );
+ $form['directory']['check_directory'] = array(
+ '#type' => 'submit',
+ '#value' => t('Check to see if directory exists'),
+ '#submit' => array('file_example_check_directory_submit'),
+ );
+
+ $form['debug'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Debugging'),
+ );
+ $form['debug']['show_raw_session'] = array(
+ '#type' => 'submit',
+ '#value' => t('Show raw $_SESSION contents'),
+ '#submit' => array('file_example_show_session_contents_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler to write a managed file.
+ *
+ * The key functions used here are:
+ * - file_save_data(), which takes a buffer and saves it to a named file and
+ * also creates a tracking record in the database and returns a file object.
+ * In this function we use FILE_EXISTS_RENAME (the default) as the argument,
+ * which means that if there's an existing file, create a new non-colliding
+ * filename and use it.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ */
+function file_example_managed_write_submit($form, &$form_state) {
+ $data = $form_state['values']['write_contents'];
+ $uri = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL;
+
+ // Managed operations work with a file object.
+ $file_object = file_save_data($data, $uri, FILE_EXISTS_RENAME);
+ if (!empty($file_object)) {
+ $url = file_create_url($file_object->uri);
+ $_SESSION['file_example_default_file'] = $file_object->uri;
+ drupal_set_message(
+ t('Saved managed file: %file to destination %destination (accessible via !url, actual uri=@uri)',
+ array(
+ '%file' => print_r($file_object, TRUE),
+ '%destination' => $uri, '@uri' => $file_object->uri,
+ '!url' => l(t('this URL'), $url),
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('Failed to save the managed file'), 'error');
+ }
+}
+
+/**
+ * Submit handler to write an unmanaged file.
+ *
+ * The key functions used here are:
+ * - file_unmanaged_save_data(), which takes a buffer and saves it to a named
+ * file, but does not create any kind of tracking record in the database.
+ * This example uses FILE_EXISTS_REPLACE for the third argument, meaning
+ * that if there's an existing file at this location, it should be replaced.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ */
+function file_example_unmanaged_write_submit($form, &$form_state) {
+ $data = $form_state['values']['write_contents'];
+ $destination = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL;
+
+ // With the unmanaged file we just get a filename back.
+ $filename = file_unmanaged_save_data($data, $destination, FILE_EXISTS_REPLACE);
+ if ($filename) {
+ $url = file_create_url($filename);
+ $_SESSION['file_example_default_file'] = $filename;
+ drupal_set_message(
+ t('Saved file as %filename (accessible via !url, uri=@uri)',
+ array(
+ '%filename' => $filename,
+ '@uri' => $filename,
+ '!url' => l(t('this URL'), $url),
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('Failed to save the file'), 'error');
+ }
+}
+
+/**
+ * Submit handler to write an unmanaged file using plain PHP functions.
+ *
+ * The key functions used here are:
+ * - file_unmanaged_save_data(), which takes a buffer and saves it to a named
+ * file, but does not create any kind of tracking record in the database.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ * - drupal_tempnam() generates a temporary filename for use.
+ */
+function file_example_unmanaged_php_submit($form, &$form_state) {
+ $data = $form_state['values']['write_contents'];
+ $destination = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL;
+
+ if (empty($destination)) {
+ // If no destination has been provided, use a generated name.
+ $destination = drupal_tempnam('public://', 'file');
+ }
+
+ // With all traditional PHP functions we can use the stream wrapper notation
+ // for a file as well.
+ $fp = fopen($destination, 'w');
+
+ // To demonstrate the fact that everything is based on streams, we'll do
+ // multiple 5-character writes to put this to the file. We could easily
+ // (and far more conveniently) write it in a single statement with
+ // fwrite($fp, $data).
+ $length = strlen($data);
+ $write_size = 5;
+ for ($i = 0; $i < $length; $i += $write_size) {
+ $result = fwrite($fp, substr($data, $i, $write_size));
+ if ($result === FALSE) {
+ drupal_set_message(t('Failed writing to the file %file', array('%file' => $destination)), 'error');
+ fclose($fp);
+ return;
+ }
+ }
+ $url = file_create_url($destination);
+ $_SESSION['file_example_default_file'] = $destination;
+ drupal_set_message(
+ t('Saved file as %filename (accessible via !url, uri=@uri)',
+ array(
+ '%filename' => $destination,
+ '@uri' => $destination,
+ '!url' => l(t('this URL'), $url),
+ )
+ )
+ );
+}
+
+/**
+ * Submit handler for reading a stream wrapper.
+ *
+ * Drupal now has full support for PHP's stream wrappers, which means that
+ * instead of the traditional use of all the file functions
+ * ($fp = fopen("/tmp/some_file.txt");) far more sophisticated and generalized
+ * (and extensible) things can be opened as if they were files. Drupal itself
+ * provides the public:// and private:// schemes for handling public and
+ * private files. PHP provides file:// (the default) and http://, so that a
+ * URL can be read or written (as in a POST) as if it were a file. In addition,
+ * new schemes can be provided for custom applications, as will be demonstrated
+ * below.
+ *
+ * Here we take the stream wrapper provided in the form. We grab the
+ * contents with file_get_contents(). Notice that's it's as simple as that:
+ * file_get_contents("http://example.com") or
+ * file_get_contents("public://somefile.txt") just works. Although it's
+ * not necessary, we use file_unmanaged_save_data() to save this file locally
+ * and then find a local URL for it by using file_create_url().
+ */
+function file_example_read_submit($form, &$form_state) {
+ $uri = $form_state['values']['fileops_file'];
+
+ if (!is_file($uri)) {
+ drupal_set_message(t('The file %uri does not exist', array('%uri' => $uri)), 'error');
+ return;
+ }
+
+ // Make a working filename to save this by stripping off the (possible)
+ // file portion of the streamwrapper. If it's an evil file extension,
+ // file_munge_filename() will neuter it.
+ $filename = file_munge_filename(preg_replace('@^.*/@', '', $uri), '', TRUE);
+ $buffer = file_get_contents($uri);
+
+ if ($buffer) {
+ $sourcename = file_unmanaged_save_data($buffer, 'public://' . $filename);
+ if ($sourcename) {
+ $url = file_create_url($sourcename);
+ $_SESSION['file_example_default_file'] = $sourcename;
+ drupal_set_message(
+ t('The file was read and copied to %filename which is accessible at !url',
+ array(
+ '%filename' => $sourcename,
+ '!url' => l($url, $url),
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('Failed to save the file'));
+ }
+ }
+ else {
+ // We failed to get the contents of the requested file.
+ drupal_set_message(t('Failed to retrieve the file %file', array('%file' => $uri)));
+ }
+}
+
+/**
+ * Submit handler to delete a file.
+ */
+function file_example_delete_submit($form, &$form_state) {
+
+ $uri = $form_state['values']['fileops_file'];
+
+ // Since we don't know if the file is managed or not, look in the database
+ // to see. Normally, code would be working with either managed or unmanaged
+ // files, so this is not a typical situation.
+ $file_object = file_example_get_managed_file($uri);
+
+ // If a managed file, use file_delete().
+ if (!empty($file_object)) {
+ $result = file_delete($file_object);
+ if ($result !== TRUE) {
+ drupal_set_message(t('Failed deleting managed file %uri. Result was %result',
+ array(
+ '%uri' => $uri,
+ '%result' => print_r($result, TRUE),
+ )
+ ), 'error');
+ }
+ else {
+ drupal_set_message(t('Successfully deleted managed file %uri', array('%uri' => $uri)));
+ $_SESSION['file_example_default_file'] = $uri;
+ }
+ }
+ // Else use file_unmanaged_delete().
+ else {
+ $result = file_unmanaged_delete($uri);
+ if ($result !== TRUE) {
+ drupal_set_message(t('Failed deleting unmanaged file %uri', array('%uri' => $uri, 'error')));
+ }
+ else {
+ drupal_set_message(t('Successfully deleted unmanaged file %uri', array('%uri' => $uri)));
+ $_SESSION['file_example_default_file'] = $uri;
+ }
+ }
+}
+
+/**
+ * Submit handler to check existence of a file.
+ */
+function file_example_file_check_exists_submit($form, &$form_state) {
+ $uri = $form_state['values']['fileops_file'];
+ if (is_file($uri)) {
+ drupal_set_message(t('The file %uri exists.', array('%uri' => $uri)));
+ }
+ else {
+ drupal_set_message(t('The file %uri does not exist.', array('%uri' => $uri)));
+ }
+
+}
+/**
+ * Submit handler for directory creation.
+ *
+ * Here we create a directory and set proper permissions on it using
+ * file_prepare_directory().
+ */
+function file_example_create_directory_submit($form, &$form_state) {
+ $directory = $form_state['values']['directory_name'];
+
+ // The options passed to file_prepare_directory are a bitmask, so we can
+ // specify either FILE_MODIFY_PERMISSIONS (set permissions on the directory),
+ // FILE_CREATE_DIRECTORY, or both together:
+ // FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY.
+ // FILE_MODIFY_PERMISSIONS will set the permissions of the directory by
+ // by default to 0755, or to the value of the variable 'file_chmod_directory'.
+ if (!file_prepare_directory($directory, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY)) {
+ drupal_set_message(t('Failed to create %directory.', array('%directory' => $directory)), 'error');
+ }
+ else {
+ drupal_set_message(t('Directory %directory is ready for use.', array('%directory' => $directory)));
+ $_SESSION['file_example_default_directory'] = $directory;
+ }
+}
+
+/**
+ * Submit handler for directory deletion.
+ *
+ * @see file_unmanaged_delete_recursive()
+ */
+function file_example_delete_directory_submit($form, &$form_state) {
+ $directory = $form_state['values']['directory_name'];
+
+ $result = file_unmanaged_delete_recursive($directory);
+ if (!$result) {
+ drupal_set_message(t('Failed to delete %directory.', array('%directory' => $directory)), 'error');
+ }
+ else {
+ drupal_set_message(t('Recursively deleted directory %directory.', array('%directory' => $directory)));
+ $_SESSION['file_example_default_directory'] = $directory;
+ }
+}
+
+/**
+ * Submit handler to test directory existence.
+ *
+ * This actually just checks to see if the directory is writable
+ *
+ * @param array $form
+ * FormAPI form.
+ * @param array $form_state
+ * FormAPI form state.
+ */
+function file_example_check_directory_submit($form, &$form_state) {
+ $directory = $form_state['values']['directory_name'];
+ $result = is_dir($directory);
+ if (!$result) {
+ drupal_set_message(t('Directory %directory does not exist.', array('%directory' => $directory)));
+ }
+ else {
+ drupal_set_message(t('Directory %directory exists.', array('%directory' => $directory)));
+ }
+}
+
+/**
+ * Utility submit function to show the contents of $_SESSION.
+ */
+function file_example_show_session_contents_submit($form, &$form_state) {
+ // If the devel module is installed, use it's nicer message format.
+ if (module_exists('devel')) {
+ dsm($_SESSION['file_example'], t('Entire $_SESSION["file_example"]'));
+ }
+ else {
+ drupal_set_message('
' . print_r($_SESSION['file_example'], TRUE) . '
');
+ }
+}
+
+/**
+ * Utility function to check for and return a managed file.
+ *
+ * In this demonstration code we don't necessarily know if a file is managed
+ * or not, so often need to check to do the correct behavior. Normal code
+ * would not have to do this, as it would be working with either managed or
+ * unmanaged files.
+ *
+ * @param string $uri
+ * The URI of the file, like public://test.txt.
+ */
+function file_example_get_managed_file($uri) {
+ $fid = db_query('SELECT fid FROM {file_managed} WHERE uri = :uri', array(':uri' => $uri))->fetchField();
+ if (!empty($fid)) {
+ $file_object = file_load($fid);
+ return $file_object;
+ }
+ return FALSE;
+}
+
+/**
+ * Implements hook_stream_wrappers().
+ *
+ * hook_stream_wrappers() is Drupal's way of exposing the class that PHP will
+ * use to provide a new stream wrapper class. In this case, we'll expose the
+ * 'session' scheme, so a file reference like "session://example/example.txt"
+ * is readable and writable as a location in the $_SESSION variable.
+ *
+ * @see FileExampleSessionStreamWrapper
+ */
+function file_example_stream_wrappers() {
+ $wrappers = array(
+ 'session' => array(
+ 'name' => t('Example: $_SESSION variable storage'),
+ 'class' => 'FileExampleSessionStreamWrapper',
+ 'description' => t('Store files in the $_SESSION variable as an example.'),
+ ),
+ );
+ return $wrappers;
+}
+
+/**
+ * Show the contents of a session file.
+ *
+ * This page callback function is called by the Menu API for the path
+ * examples/file_example/access_session. Any extra path elements
+ * beyond this are considered to be the session path. E.g.:
+ * examples/file_example/access_session/foo/bar.txt would be the
+ * equivalent of session://foo/bar.txt, which will map into
+ * $_SESSION as keys: $_SESSION['foo']['bar.txt']
+ *
+ * Menu API will pass in additional path elements as function arguments. You
+ * can obtain these with func_get_args().
+ *
+ * @return string
+ * A message containing the contents of the session file.
+ *
+ * @see file_get_contents()
+ */
+function file_example_session_contents() {
+ $path_components = func_get_args();
+ $session_path = 'session://' . implode('/', $path_components);
+ $content = file_get_contents($session_path);
+ if ($content !== FALSE) {
+ return t('Contents of @path :',
+ array('@path' => check_plain($session_path))) . ' ' .
+ print_r($content, TRUE);
+ }
+ return t('Unable to load contents of: @path',
+ array('@path' => check_plain($session_path)));
+}
+
+/**
+ * @} End of "defgroup file_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/file_example/file_example.test b/sites/all/modules/contrib/dev/examples/file_example/file_example.test
new file mode 100644
index 00000000..41e73c61
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/file_example/file_example.test
@@ -0,0 +1,149 @@
+ 'File Example Functionality',
+ 'description' => 'Test File Example features and sample streamwrapper.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp(array('file_example'));
+ $this->priviledgedUser = $this->drupalCreateUser(array('use file example'));
+ $this->drupalLogin($this->priviledgedUser);
+ }
+
+ /**
+ * Test the basic File Example UI.
+ *
+ * - Create a directory to work with
+ * - Foreach scheme create and read files using each of the three methods.
+ */
+ public function testFileExampleBasic() {
+
+ $expected_text = array(
+ t('Write managed file') => t('Saved managed file'),
+ t('Write unmanaged file') => t('Saved file as'),
+ t('Unmanaged using PHP') => t('Saved file as'),
+ );
+ // For each of the three buttons == three write types.
+ $buttons = array(
+ t('Write managed file'),
+ t('Write unmanaged file'),
+ t('Unmanaged using PHP'),
+ );
+ foreach ($buttons as $button) {
+ // For each scheme supported by Drupal + the session:// wrapper.
+ $schemes = array('public', 'private', 'temporary', 'session');
+ foreach ($schemes as $scheme) {
+ // Create a directory for use.
+ $dirname = $scheme . '://' . $this->randomName(10);
+
+ // Directory does not yet exist; assert that.
+ $edit = array(
+ 'directory_name' => $dirname,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if directory exists'));
+ $this->assertRaw(t('Directory %dirname does not exist', array('%dirname' => $dirname)), 'Verify that directory does not exist.');
+
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Create directory'));
+ $this->assertRaw(t('Directory %dirname is ready for use', array('%dirname' => $dirname)));
+
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if directory exists'));
+ $this->assertRaw(t('Directory %dirname exists', array('%dirname' => $dirname)), 'Verify that directory now does exist.');
+
+ // Create a file in the directory we created.
+ $content = $this->randomName(30);
+ $filename = $dirname . '/' . $this->randomName(30) . '.txt';
+
+ // Assert that the file we're about to create does not yet exist.
+ $edit = array(
+ 'fileops_file' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if file exists'));
+ $this->assertRaw(t('The file %filename does not exist', array('%filename' => $filename)), 'Verify that file does not yet exist.');
+
+ debug(
+ t('Processing button=%button, scheme=%scheme, dir=%dirname, file=%filename',
+ array(
+ '%button' => $button,
+ '%scheme' => $scheme,
+ '%filename' => $filename,
+ '%dirname' => $dirname,
+ )
+ )
+ );
+ $edit = array(
+ 'write_contents' => $content,
+ 'destination' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, $button);
+ $this->assertText($expected_text[$button]);
+
+ // Capture the name of the output file, as it might have changed due
+ // to file renaming.
+ $element = $this->xpath('//span[@id="uri"]');
+ $output_filename = (string) $element[0];
+ debug($output_filename, 'Name of output file');
+
+ // Click the link provided that is an easy way to get the data for
+ // checking and make sure that the data we put in is what we get out.
+ if (!in_array($scheme, array('private', 'temporary'))) {
+ $this->clickLink(t('this URL'));
+ $this->assertText($content);
+ }
+
+ // Verify that the file exists.
+ $edit = array(
+ 'fileops_file' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if file exists'));
+ $this->assertRaw(t('The file %filename exists', array('%filename' => $filename)), 'Verify that file now exists.');
+
+ // Now read the file that got written above and verify that we can use
+ // the writing tools.
+ $edit = array(
+ 'fileops_file' => $output_filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Read the file and store it locally'));
+
+ $this->assertText(t('The file was read and copied'));
+
+ $edit = array(
+ 'fileops_file' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Delete file'));
+ $this->assertText(t('Successfully deleted'));
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if file exists'));
+ $this->assertRaw(t('The file %filename does not exist', array('%filename' => $filename)), 'Verify file has been deleted.');
+
+ $edit = array(
+ 'directory_name' => $dirname,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Delete directory'));
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if directory exists'));
+ $this->assertRaw(t('Directory %dirname does not exist', array('%dirname' => $dirname)), 'Verify that directory does not exist after deletion.');
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/file_example/file_example_session_streams.inc b/sites/all/modules/contrib/dev/examples/file_example/file_example_session_streams.inc
new file mode 100644
index 00000000..dc4850c3
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/file_example/file_example_session_streams.inc
@@ -0,0 +1,698 @@
+uri = $uri;
+ }
+
+ /**
+ * Implements getUri().
+ */
+ public function getUri() {
+ return $this->uri;
+ }
+
+ /**
+ * Implements getTarget().
+ *
+ * The "target" is the portion of the URI to the right of the scheme.
+ * So in session://example/test.txt, the target is 'example/test.txt'.
+ */
+ public function getTarget($uri = NULL) {
+ if (!isset($uri)) {
+ $uri = $this->uri;
+ }
+
+ list($scheme, $target) = explode('://', $uri, 2);
+
+ // Remove erroneous leading or trailing, forward-slashes and backslashes.
+ // In the session:// scheme, there is never a leading slash on the target.
+ return trim($target, '\/');
+ }
+
+ /**
+ * Implements getMimeType().
+ */
+ public static function getMimeType($uri, $mapping = NULL) {
+ if (!isset($mapping)) {
+ // The default file map, defined in file.mimetypes.inc is quite big.
+ // We only load it when necessary.
+ include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc';
+ $mapping = file_mimetype_mapping();
+ }
+
+ $extension = '';
+ $file_parts = explode('.', basename($uri));
+
+ // Remove the first part: a full filename should not match an extension.
+ array_shift($file_parts);
+
+ // Iterate over the file parts, trying to find a match.
+ // For my.awesome.image.jpeg, we try:
+ // - jpeg
+ // - image.jpeg, and
+ // - awesome.image.jpeg
+ while ($additional_part = array_pop($file_parts)) {
+ $extension = drupal_strtolower($additional_part . ($extension ? '.' . $extension : ''));
+ if (isset($mapping['extensions'][$extension])) {
+ return $mapping['mimetypes'][$mapping['extensions'][$extension]];
+ }
+ }
+
+ return 'application/octet-stream';
+ }
+
+ /**
+ * Implements getDirectoryPath().
+ *
+ * In this case there is no directory string, so return an empty string.
+ */
+ public function getDirectoryPath() {
+ return '';
+ }
+
+ /**
+ * Overrides getExternalUrl().
+ *
+ * We have set up a helper function and menu entry to provide access to this
+ * key via HTTP; normally it would be accessible some other way.
+ */
+ public function getExternalUrl() {
+ $path = $this->getLocalPath();
+ $url = url('examples/file_example/access_session/' . $path, array('absolute' => TRUE));
+ return $url;
+ }
+
+ /**
+ * We have no concept of chmod, so just return TRUE.
+ */
+ public function chmod($mode) {
+ return TRUE;
+ }
+
+ /**
+ * Implements realpath().
+ */
+ public function realpath() {
+ return 'session://' . $this->getLocalPath();
+ }
+
+ /**
+ * Returns the local path.
+ *
+ * Here we aren't doing anything but stashing the "file" in a key in the
+ * $_SESSION variable, so there's not much to do but to create a "path"
+ * which is really just a key in the $_SESSION variable. So something
+ * like 'session://one/two/three.txt' becomes
+ * $_SESSION['file_example']['one']['two']['three.txt'] and the actual path
+ * is "one/two/three.txt".
+ *
+ * @param string $uri
+ * Optional URI, supplied when doing a move or rename.
+ */
+ protected function getLocalPath($uri = NULL) {
+ if (!isset($uri)) {
+ $uri = $this->uri;
+ }
+
+ $path = str_replace('session://', '', $uri);
+ $path = trim($path, '/');
+ return $path;
+ }
+
+ /**
+ * Opens a stream, as for fopen(), file_get_contents(), file_put_contents().
+ *
+ * @param string $uri
+ * A string containing the URI to the file to open.
+ * @param string $mode
+ * The file mode ("r", "wb" etc.).
+ * @param int $options
+ * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
+ * @param string &$opened_path
+ * A string containing the path actually opened.
+ *
+ * @return bool
+ * Returns TRUE if file was opened successfully. (Always returns TRUE).
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-open.php
+ */
+ public function stream_open($uri, $mode, $options, &$opened_path) {
+ $this->uri = $uri;
+ // We make $session_content a reference to the appropriate key in the
+ // $_SESSION variable. So if the local path were
+ // /example/test.txt it $session_content would now be a
+ // reference to $_SESSION['file_example']['example']['test.txt'].
+ $this->sessionContent = &$this->uri_to_session_key($uri);
+
+ // Reset the stream pointer since this is an open.
+ $this->streamPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Return a reference to the correct $_SESSION key.
+ *
+ * @param string $uri
+ * The uri: session://something
+ * @param bool $create
+ * If TRUE, create the key
+ *
+ * @return array|bool
+ * A reference to the array at the end of the key-path, or
+ * FALSE if the path doesn't map to a key-path (and $create is FALSE).
+ */
+ protected function &uri_to_session_key($uri, $create = TRUE) {
+ // Since our uri_to_session_key() method returns a reference, we
+ // have to set up a failure flag variable.
+ $fail = FALSE;
+ $path = $this->getLocalPath($uri);
+ $path_components = explode('/', $path);
+ // Set up a reference to the root session:// 'directory.'
+ $var = &$_SESSION['file_example'];
+ // Handle case of just session://.
+ if (count($path_components) < 1) {
+ return $var;
+ }
+ // Walk through the path components and create keys in $_SESSION,
+ // unless we're told not to create them.
+ foreach ($path_components as $component) {
+ if ($create || isset($var[$component])) {
+ $var = &$var[$component];
+ }
+ else {
+ // This path doesn't exist as keys, either because the
+ // key doesn't exist, or because we're told not to create it.
+ return $fail;
+ }
+ }
+ return $var;
+ }
+
+ /**
+ * Support for flock().
+ *
+ * The $_SESSION variable has no locking capability, so return TRUE.
+ *
+ * @param int $operation
+ * One of the following:
+ * - LOCK_SH to acquire a shared lock (reader).
+ * - LOCK_EX to acquire an exclusive lock (writer).
+ * - LOCK_UN to release a lock (shared or exclusive).
+ * - LOCK_NB if you don't want flock() to block while locking (not
+ * supported on Windows).
+ *
+ * @return bool
+ * Always returns TRUE at the present time. (no support)
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-lock.php
+ */
+ public function stream_lock($operation) {
+ return TRUE;
+ }
+
+ /**
+ * Support for fread(), file_get_contents() etc.
+ *
+ * @param int $count
+ * Maximum number of bytes to be read.
+ *
+ * @return string
+ * The string that was read, or FALSE in case of an error.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-read.php
+ */
+ public function stream_read($count) {
+ if (is_string($this->sessionContent)) {
+ $remaining_chars = drupal_strlen($this->sessionContent) - $this->streamPointer;
+ $number_to_read = min($count, $remaining_chars);
+ if ($remaining_chars > 0) {
+ $buffer = drupal_substr($this->sessionContent, $this->streamPointer, $number_to_read);
+ $this->streamPointer += $number_to_read;
+ return $buffer;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for fwrite(), file_put_contents() etc.
+ *
+ * @param string $data
+ * The string to be written.
+ *
+ * @return int
+ * The number of bytes written (integer).
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-write.php
+ */
+ public function stream_write($data) {
+ // Sanitize the data in a simple way since we're putting it into the
+ // session variable.
+ $data = check_plain($data);
+ $this->sessionContent = substr_replace($this->sessionContent, $data, $this->streamPointer);
+ $this->streamPointer += drupal_strlen($data);
+ return drupal_strlen($data);
+ }
+
+ /**
+ * Support for feof().
+ *
+ * @return bool
+ * TRUE if end-of-file has been reached.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+ */
+ public function stream_eof() {
+ return FALSE;
+ }
+
+ /**
+ * Support for fseek().
+ *
+ * @param int $offset
+ * The byte offset to got to.
+ * @param int $whence
+ * SEEK_SET, SEEK_CUR, or SEEK_END.
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+ */
+ public function stream_seek($offset, $whence) {
+ if (drupal_strlen($this->sessionContent) >= $offset) {
+ $this->streamPointer = $offset;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for fflush().
+ *
+ * @return bool
+ * TRUE if data was successfully stored (or there was no data to store).
+ * This always returns TRUE, as this example provides and needs no
+ * flush support.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-flush.php
+ */
+ public function stream_flush() {
+ return TRUE;
+ }
+
+ /**
+ * Support for ftell().
+ *
+ * @return int
+ * The current offset in bytes from the beginning of file.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+ */
+ public function stream_tell() {
+ return $this->streamPointer;
+ }
+
+ /**
+ * Support for fstat().
+ *
+ * @return array
+ * An array with file status, or FALSE in case of an error - see fstat()
+ * for a description of this array.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+ */
+ public function stream_stat() {
+ return array(
+ 'size' => drupal_strlen($this->sessionContent),
+ );
+ }
+
+ /**
+ * Support for fclose().
+ *
+ * @return bool
+ * TRUE if stream was successfully closed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-close.php
+ */
+ public function stream_close() {
+ $this->streamPointer = 0;
+ // Unassign the reference.
+ unset($this->sessionContent);
+ return TRUE;
+ }
+
+ /**
+ * Support for unlink().
+ *
+ * @param string $uri
+ * A string containing the uri to the resource to delete.
+ *
+ * @return bool
+ * TRUE if resource was successfully deleted.
+ *
+ * @see http://php.net/manual/en/streamwrapper.unlink.php
+ */
+ public function unlink($uri) {
+ $path = $this->getLocalPath($uri);
+ $path_components = preg_split('/\//', $path);
+ $unset = '$_SESSION[\'file_example\']';
+ foreach ($path_components as $component) {
+ $unset .= '[\'' . $component . '\']';
+ }
+ // TODO: Is there a better way to delete from an array?
+ // drupal_array_get_nested_value() doesn't work because it only returns
+ // a reference; unsetting a reference only unsets the reference.
+ eval("unset($unset);");
+ return TRUE;
+ }
+
+ /**
+ * Support for rename().
+ *
+ * @param string $from_uri
+ * The uri to the file to rename.
+ * @param string $to_uri
+ * The new uri for file.
+ *
+ * @return bool
+ * TRUE if file was successfully renamed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.rename.php
+ */
+ public function rename($from_uri, $to_uri) {
+ $from_key = &$this->uri_to_session_key($from_uri);
+ $to_key = &$this->uri_to_session_key($to_uri);
+ if (is_dir($to_key) || is_file($to_key)) {
+ return FALSE;
+ }
+ $to_key = $from_key;
+ unset($from_key);
+ return TRUE;
+ }
+
+ /**
+ * Gets the name of the directory from a given path.
+ *
+ * @param string $uri
+ * A URI.
+ *
+ * @return string
+ * A string containing the directory name.
+ *
+ * @see drupal_dirname()
+ */
+ public function dirname($uri = NULL) {
+ list($scheme, $target) = explode('://', $uri, 2);
+ $target = $this->getTarget($uri);
+ if (strpos($target, '/')) {
+ $dirname = preg_replace('@/[^/]*$@', '', $target);
+ }
+ else {
+ $dirname = '';
+ }
+ return $scheme . '://' . $dirname;
+ }
+
+ /**
+ * Support for mkdir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to create.
+ * @param int $mode
+ * Permission flags - see mkdir().
+ * @param int $options
+ * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
+ *
+ * @return bool
+ * TRUE if directory was successfully created.
+ *
+ * @see http://php.net/manual/en/streamwrapper.mkdir.php
+ */
+ public function mkdir($uri, $mode, $options) {
+ // If this already exists, then we can't mkdir.
+ if (is_dir($uri) || is_file($uri)) {
+ return FALSE;
+ }
+
+ // Create the key in $_SESSION;
+ $this->uri_to_session_key($uri, TRUE);
+
+ // Place a magic file inside it to differentiate this from an empty file.
+ $marker_uri = $uri . '/.isadir.txt';
+ $this->uri_to_session_key($marker_uri, TRUE);
+ return TRUE;
+ }
+
+ /**
+ * Support for rmdir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to delete.
+ * @param int $options
+ * A bit mask of STREAM_REPORT_ERRORS.
+ *
+ * @return bool
+ * TRUE if directory was successfully removed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.rmdir.php
+ */
+ public function rmdir($uri, $options) {
+ $path = $this->getLocalPath($uri);
+ $path_components = preg_split('/\//', $path);
+ $unset = '$_SESSION[\'file_example\']';
+ foreach ($path_components as $component) {
+ $unset .= '[\'' . $component . '\']';
+ }
+ // TODO: I really don't like this eval.
+ debug($unset, 'array element to be unset');
+ eval("unset($unset);");
+
+ return TRUE;
+ }
+
+ /**
+ * Support for stat().
+ *
+ * This important function goes back to the Unix way of doing things.
+ * In this example almost the entire stat array is irrelevant, but the
+ * mode is very important. It tells PHP whether we have a file or a
+ * directory and what the permissions are. All that is packed up in a
+ * bitmask. This is not normal PHP fodder.
+ *
+ * @param string $uri
+ * A string containing the URI to get information about.
+ * @param int $flags
+ * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
+ *
+ * @return array|bool
+ * An array with file status, or FALSE in case of an error - see fstat()
+ * for a description of this array.
+ *
+ * @see http://php.net/manual/en/streamwrapper.url-stat.php
+ */
+ public function url_stat($uri, $flags) {
+ // Get a reference to the $_SESSION key for this URI.
+ $key = $this->uri_to_session_key($uri, FALSE);
+ // Default to fail.
+ $return = FALSE;
+ $mode = 0;
+
+ // We will call an array a directory and the root is always an array.
+ if (is_array($key) && array_key_exists('.isadir.txt', $key)) {
+ // S_IFDIR means it's a directory.
+ $mode = 0040000;
+ }
+ elseif ($key !== FALSE) {
+ // S_IFREG, means it's a file.
+ $mode = 0100000;
+ }
+
+ if ($mode) {
+ $size = 0;
+ if ($mode == 0100000) {
+ $size = drupal_strlen($key);
+ }
+
+ // There are no protections on this, so all writable.
+ $mode |= 0777;
+ $return = array(
+ 'dev' => 0,
+ 'ino' => 0,
+ 'mode' => $mode,
+ 'nlink' => 0,
+ 'uid' => 0,
+ 'gid' => 0,
+ 'rdev' => 0,
+ 'size' => $size,
+ 'atime' => 0,
+ 'mtime' => 0,
+ 'ctime' => 0,
+ 'blksize' => 0,
+ 'blocks' => 0,
+ );
+ }
+ return $return;
+ }
+
+ /**
+ * Support for opendir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to open.
+ * @param int $options
+ * Whether or not to enforce safe_mode (0x04).
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
+ */
+ public function dir_opendir($uri, $options) {
+ $var = &$this->uri_to_session_key($uri, FALSE);
+ if ($var === FALSE || !array_key_exists('.isadir.txt', $var)) {
+ return FALSE;
+ }
+
+ // We grab the list of key names, flip it so that .isadir.txt can easily
+ // be removed, then flip it back so we can easily walk it as a list.
+ $this->directoryKeys = array_flip(array_keys($var));
+ unset($this->directoryKeys['.isadir.txt']);
+ $this->directoryKeys = array_keys($this->directoryKeys);
+ $this->directoryPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Support for readdir().
+ *
+ * @return string|bool
+ * The next filename, or FALSE if there are no more files in the directory.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-readdir.php
+ */
+ public function dir_readdir() {
+ if ($this->directoryPointer < count($this->directoryKeys)) {
+ $next = $this->directoryKeys[$this->directoryPointer];
+ $this->directoryPointer++;
+ return $next;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for rewinddir().
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
+ */
+ public function dir_rewinddir() {
+ $this->directoryPointer = 0;
+ }
+
+ /**
+ * Support for closedir().
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-closedir.php
+ */
+ public function dir_closedir() {
+ $this->directoryPointer = 0;
+ unset($this->directoryKeys);
+ return TRUE;
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/filter_example/filter_example.info b/sites/all/modules/contrib/dev/examples/filter_example/filter_example.info
new file mode 100644
index 00000000..75d14081
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/filter_example/filter_example.info
@@ -0,0 +1,12 @@
+name = Filter example
+description = An example module showing how to define a custom filter.
+package = Example modules
+core = 7.x
+files[] = filter_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/filter_example/filter_example.module b/sites/all/modules/contrib/dev/examples/filter_example/filter_example.module
new file mode 100644
index 00000000..f5e9dffe
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/filter_example/filter_example.module
@@ -0,0 +1,203 @@
+, and replace it by the current time.
+ *
+ * Foo filter
+ *
+ * Drupal has several content formats (they are not filters), and in our example
+ * the foo replacement can be configured for each one of them, allowing an html
+ * or php replacement, so the module includes a settings callback, with options
+ * to configure that replacements. Also, a Tips callback will help showing the
+ * current replacement for the content type being edited.
+ *
+ * Time filter.
+ *
+ * This filter is a little trickier to implement than the previous one.
+ * Since the input involves special HTML characters (< and >) we have to
+ * run the filter before HTML is escaped/stripped by other filters. But
+ * we want to use HTML in our result as well, and so if we run this filter
+ * first our replacement string could be escaped or stripped. The solution
+ * is to use the "prepare" operation to escape the special characters, and
+ * to later replace our escaped version in the "process" step.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function filter_example_menu() {
+ $items['examples/filter_example'] = array(
+ 'title' => 'Filter Example',
+ 'page callback' => '_filter_example_information',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_help().
+ */
+function filter_example_help($path, $arg) {
+ switch ($path) {
+ case 'admin/help#filter_example':
+ return _filter_example_information();
+ }
+}
+
+/**
+ * Simply returns a little bit of information about the example.
+ */
+function _filter_example_information() {
+ return t("
This example provides two filters.
Foo Filter replaces
+ 'foo' with a configurable replacement.
Time Tag replaces the string
+ '<time />' with the current time.
To use these filters, go to !link and
+ configure an input format, or create a new one.
",
+ array('!link' => l(t('admin/config/content/formats'), 'admin/config/content/formats'))
+ );
+}
+
+/**
+ * Implements hook_filter_info().
+ *
+ * Here we define the different filters provided by the module. For this
+ * example, time_filter is a very static and simple replacement, but it requires
+ * some preparation of the string because of the special html tags < and >. The
+ * foo_filter is more complex, including its own settings and inline tips.
+ */
+function filter_example_filter_info() {
+ $filters['filter_foo'] = array(
+ 'title' => t('Foo Filter (example)'),
+ 'description' => t('Every instance of "foo" in the input text will be replaced with a preconfigured replacement.'),
+ 'process callback' => '_filter_example_filter_foo_process',
+ 'default settings' => array(
+ 'filter_example_foo' => 'bar',
+ ),
+ 'settings callback' => '_filter_example_filter_foo_settings',
+ 'tips callback' => '_filter_example_filter_foo_tips',
+ );
+ $filters['filter_time'] = array(
+ 'title' => t('Time Tag (example)'),
+ 'description' => t("Every instance of the special <time /> tag will be replaced with the current date and time in the user's specified time zone."),
+ 'prepare callback' => '_filter_example_filter_time_prepare',
+ 'process callback' => '_filter_example_filter_time_process',
+ 'tips callback' => '_filter_example_filter_time_tips',
+ );
+ return $filters;
+}
+
+/*
+ * Foo filter
+ *
+ * Drupal has several text formats (they are not filters), and in our example
+ * the foo replacement can be configured for each one of them, so the module
+ * includes a settings callback, with options to configure those replacements.
+ * Also, a Tips callback will help showing the current replacement
+ * for the content type being edited.
+ */
+
+/**
+ * Settings callback for foo filter.
+ *
+ * Make use of $format to have different replacements for every input format.
+ * Since we allow the administrator to define the string that gets substituted
+ * when "foo" is encountered, we need to provide an interface for this kind of
+ * customization. The object format is also an argument of the callback.
+ *
+ * The settings defined in this form are stored in database by the filter
+ * module, and they will be available in the $filter argument.
+ */
+function _filter_example_filter_foo_settings($form, $form_state, $filter, $format, $defaults) {
+ $settings['filter_example_foo'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Substitution string'),
+ '#default_value' => isset($filter->settings['filter_example_foo']) ? $filter->settings['filter_example_foo'] : $defaults['filter_example_foo'],
+ '#description' => t('The string to substitute for "foo" everywhere in the text.'),
+ );
+ return $settings;
+}
+
+/**
+ * Foo filter process callback.
+ *
+ * The actual filtering is performed here. The supplied text should be returned,
+ * once any necessary substitutions have taken place. The example just replaces
+ * foo with our custom defined string in the settings page.
+ */
+function _filter_example_filter_foo_process($text, $filter, $format) {
+ $replacement = isset($filter->settings['filter_example_foo']) ? $filter->settings['filter_example_foo'] : 'bar';
+ return str_replace('foo', $replacement, $text);
+}
+
+
+/**
+ * Filter tips callback for foo filter.
+ *
+ * The tips callback allows filters to provide help text to users during the
+ * content editing process. Short tips are provided on the content editing
+ * screen, while long tips are provided on a separate linked page. Short tips
+ * are optional, but long tips are highly recommended.
+ */
+function _filter_example_filter_foo_tips($filter, $format, $long = FALSE) {
+ $replacement = isset($filter->settings['filter_example_foo']) ? $filter->settings['filter_example_foo'] : 'bar';
+ if (!$long) {
+ // This string will be shown in the content add/edit form.
+ return t('foo replaced with %replacement.', array('%replacement' => $replacement));
+ }
+ else {
+ return t('Every instance of "foo" in the input text will be replaced with a configurable value. You can configure this value and put whatever you want there. The replacement value is "%replacement".', array('%replacement' => $replacement));
+ }
+}
+
+/**
+ * Time filter prepare callback.
+ *
+ * We'll use [filter-example-time] as a replacement for the time tag.
+ * Note that in a more complicated filter a closing tag may also be
+ * required. For more information, see "Temporary placeholders and
+ * delimiters" at http://drupal.org/node/209715.
+ */
+function _filter_example_filter_time_prepare($text, $filter) {
+ return preg_replace('!!', '[filter-example-time]', $text);
+}
+
+/**
+ * Time filter process callback.
+ *
+ * Now, in the "process" step, we'll search for our escaped time tags and
+ * do the real filtering: replace the xml tag with the date.
+ */
+function _filter_example_filter_time_process($text, $filter) {
+ return str_replace('[filter-example-time]', '' . format_date(time()) . '', $text);
+}
+
+
+/**
+ * Filter tips callback for time filter.
+ *
+ * The tips callback allows filters to provide help text to users during the
+ * content editing process. Short tips are provided on the content editing
+ * screen, while long tips are provided on a separate linked page. Short tips
+ * are optional, but long tips are highly recommended.
+ */
+function _filter_example_filter_time_tips($filter, $format, $long = FALSE) {
+ return t('<time /> is replaced with the current time.');
+}
+
+/**
+ * @} End of "defgroup filter_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/filter_example/filter_example.test b/sites/all/modules/contrib/dev/examples/filter_example/filter_example.test
new file mode 100644
index 00000000..56c412a8
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/filter_example/filter_example.test
@@ -0,0 +1,109 @@
+ 'Filter example functionality',
+ 'description' => 'Verify that content is processed by example filter.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('filter_example');
+
+ // Load the used input formats.
+ $this->filteredHtml = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Filtered HTML'))->fetchObject();
+ $this->fullHtml = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchObject();
+
+ // Create user.
+ $this->webUser = $this->drupalCreateUser(array(
+ 'administer filters',
+ filter_permission_name($this->filteredHtml),
+ filter_permission_name($this->fullHtml),
+ 'bypass node access',
+ ));
+ }
+
+ /**
+ * Functional test of the foo filter.
+ *
+ * Login user, create an example node, and test blog functionality through
+ * the admin and user interfaces.
+ */
+ public function testFilterExampleBasic() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Enable both filters in format id 1 (default format).
+ $edit = array(
+ 'filters[filter_time][status]' => TRUE,
+ 'filters[filter_foo][status]' => TRUE,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $this->filteredHtml->format, $edit, t('Save configuration'));
+
+ // Create a content type to test the filters (with default format).
+ $content_type = $this->drupalCreateContentType();
+
+ // Create a test node.
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ "body[$langcode][0][value]" => 'What foo is it? it is ',
+ );
+ $result = $this->drupalPost('node/add/' . $content_type->type, $edit, t('Save'));
+ $this->assertResponse(200);
+ $time = format_date(time());
+ $this->assertRaw('What bar is it? it is ' . $time . '');
+
+ // Enable foo filter in other format id 2
+ $edit = array(
+ 'filters[filter_foo][status]' => TRUE,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $this->fullHtml->format, $edit, t('Save configuration'));
+
+ // Change foo filter replacement with a random string in format id 2
+ $replacement = $this->randomName();
+ $options = array(
+ 'filters[filter_foo][settings][filter_example_foo]' => $replacement,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $this->fullHtml->format, $options, t('Save configuration'));
+
+ // Create a test node with content format 2
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ "body[$langcode][0][value]" => 'What foo is it? it is ',
+ "body[$langcode][0][format]" => $this->fullHtml->format,
+ );
+ $result = $this->drupalPost('node/add/' . $content_type->type, $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Only foo filter is enabled.
+ $this->assertRaw("What " . $replacement . " is it", 'Foo filter successfully verified.');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example.info b/sites/all/modules/contrib/dev/examples/form_example/form_example.info
new file mode 100644
index 00000000..a8b6aa13
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example.info
@@ -0,0 +1,12 @@
+name = Form example
+description = Examples of using the Drupal Form API.
+package = Example modules
+core = 7.x
+files[] = form_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example.module b/sites/all/modules/contrib/dev/examples/form_example/form_example.module
new file mode 100644
index 00000000..b3df1dfc
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example.module
@@ -0,0 +1,234 @@
+ 'Form Example',
+ 'page callback' => 'form_example_intro',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+ $items['examples/form_example/tutorial'] = array(
+ 'title' => 'Form Tutorial',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_1'),
+ 'access callback' => TRUE,
+ 'description' => 'A set of ten tutorials',
+ 'file' => 'form_example_tutorial.inc',
+ 'type' => MENU_NORMAL_ITEM,
+ );
+ $items['examples/form_example/tutorial/1'] = array(
+ 'title' => '#1',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_1'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 1: Simplest form',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/2'] = array(
+ 'title' => '#2',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_2'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 2: Form with a submit button',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/3'] = array(
+ 'title' => '#3',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_3'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 3: Fieldsets',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/4'] = array(
+ 'title' => '#4',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_4'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 4: Required fields',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/5'] = array(
+ 'title' => '#5',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_5'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 5: More element attributes',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/6'] = array(
+ 'title' => '#6',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_6'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 6: Form with a validate handler',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/7'] = array(
+ 'title' => '#7',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_7'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 7: Form with a submit handler',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/8'] = array(
+ 'title' => '#8',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_8'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 8: Basic multistep form',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/9'] = array(
+ 'title' => '#9',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_9'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 9: Form with dynamically added new fields',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ 'weight' => 9,
+ );
+ $items['examples/form_example/tutorial/10'] = array(
+ 'title' => '#10',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_10'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 10: Form with file upload',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ 'weight' => 10,
+ );
+ $items['examples/form_example/tutorial/11'] = array(
+ 'title' => '#11',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_11'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 11: generating a confirmation form',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ 'weight' => 11,
+ );
+ $items['examples/form_example/tutorial/11/confirm/%'] = array(
+ 'title' => 'Name Confirmation',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_11_confirm_name', 5),
+ 'access callback' => TRUE,
+ 'description' => 'Confirmation form for tutorial 11. Generated using the confirm_form function',
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/states'] = array(
+ 'title' => '#states example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_states_form'),
+ 'access callback' => TRUE,
+ 'description' => 'How to use the #states attribute in FAPI',
+ 'file' => 'form_example_states.inc',
+ );
+ $items['examples/form_example/wizard'] = array(
+ 'title' => 'Extensible wizard example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_wizard'),
+ 'access callback' => TRUE,
+ 'description' => 'A general approach to a wizard multistep form.',
+ 'file' => 'form_example_wizard.inc',
+ );
+ $items['examples/form_example/element_example'] = array(
+ 'title' => 'Element example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_element_demo_form'),
+ 'access callback' => TRUE,
+ 'file' => 'form_example_elements.inc',
+ 'weight' => 100,
+ );
+
+ return $items;
+}
+
+/**
+ * Page callback for our general info page.
+ */
+function form_example_intro() {
+ $markup = t('The form example module provides a tutorial, extensible multistep example, an element example, and a #states example');
+ return array('#markup' => $markup);
+}
+
+/**
+ * Implements hook_help().
+ */
+function form_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/form_example/tutorial':
+ // TODO: Update the URL.
+ $help = t('This form example tutorial for Drupal 7 is the code from the Handbook 10-step tutorial');
+ break;
+
+ case 'examples/form_example/element_example':
+ $help = t('The Element Example shows how modules can provide their own Form API element types. Four different element types are demonstrated.');
+ break;
+ }
+ if (!empty($help)) {
+ return '
' . $help . '
';
+ }
+}
+
+/**
+ * Implements hook_element_info().
+ *
+ * To keep the various pieces of the example together in external files,
+ * this just returns _form_example_elements().
+ */
+function form_example_element_info() {
+ require_once 'form_example_elements.inc';
+ return _form_example_element_info();
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * The only theme implementation is by the element example. To keep the various
+ * parts of the example together, this actually returns
+ * _form_example_element_theme().
+ */
+function form_example_theme($existing, $type, $theme, $path) {
+ require_once 'form_example_elements.inc';
+ return _form_example_element_theme($existing, $type, $theme, $path);
+}
+/**
+ * @} End of "defgroup form_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example.test b/sites/all/modules/contrib/dev/examples/form_example/form_example.test
new file mode 100644
index 00000000..b439c62a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example.test
@@ -0,0 +1,275 @@
+ 'Form Example',
+ 'description' => 'Various tests on the form_example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules.
+ */
+ public function setUp() {
+ parent::setUp('form_example');
+ }
+
+ /**
+ * Test each tutorial.
+ */
+ public function testTutorials() {
+ // Tutorial #1
+ $this->drupalGet('examples/form_example/tutorial');
+ $this->assertText(t('#9'));
+
+ // #2
+ $this->drupalPost('examples/form_example/tutorial/2', array('name' => t('name')), t('Submit'));
+
+ // #4
+ $this->drupalPost('examples/form_example/tutorial/4',
+ array('first' => t('firstname'), 'last' => t('lastname')), t('Submit'));
+ $this->drupalPost('examples/form_example/tutorial/4', array(), t('Submit'));
+ $this->assertText(t('First name field is required'));
+ $this->assertText(t('Last name field is required'));
+
+ // #5
+ $this->drupalPost('examples/form_example/tutorial/5',
+ array('first' => t('firstname'), 'last' => t('lastname')), t('Submit'));
+ $this->assertText(t('Please enter your first name'));
+ $this->drupalPost('examples/form_example/tutorial/4', array(), t('Submit'));
+ $this->assertText(t('First name field is required'));
+ $this->assertText(t('Last name field is required'));
+
+ // #6
+ $this->drupalPost(
+ 'examples/form_example/tutorial/6',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1955,
+ ),
+ t('Submit'));
+ $this->assertNoText(t('Enter a year between 1900 and 2000'));
+ $this->drupalPost(
+ 'examples/form_example/tutorial/6',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1855,
+ ),
+ t('Submit')
+ );
+
+ $this->assertText(t('Enter a year between 1900 and 2000'));
+
+ // #7
+ $this->drupalPost(
+ 'examples/form_example/tutorial/7',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1955,
+ ),
+ t('Submit')
+ );
+ $this->assertText(t('The form has been submitted. name="firstname lastname", year of birth=1955'));
+ $this->drupalPost(
+ 'examples/form_example/tutorial/7',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1855,
+ ),
+ t('Submit')
+ );
+
+ $this->assertText(t('Enter a year between 1900 and 2000'));
+
+ // Test tutorial #8.
+ $this->drupalPost(
+ 'examples/form_example/tutorial/8',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1955,
+ ),
+ t('Next >>')
+ );
+
+ $this->drupalPost(NULL, array('color' => t('green')), t('<< Back'));
+ $this->drupalPost(NULL, array(), t('Next >>'));
+ $this->drupalPost(NULL, array('color' => t('red')), t('Submit'));
+ $this->assertText(t('The form has been submitted. name="firstname lastname", year of birth=1955'));
+ $this->assertText(t('And the favorite color is red'));
+
+ // #9
+ $url = 'examples/form_example/tutorial/9';
+ for ($i = 1; $i <= 4; $i++) {
+ if ($i > 1) {
+ // Later steps of multistep form take NULL.
+ $url = NULL;
+ }
+ $this->drupalPost(
+ $url,
+ array(
+ "name[$i][first]" => "firstname $i",
+ "name[$i][last]" => "lastname $i",
+ "name[$i][year_of_birth]" => 1950 + $i,
+ ),
+ t('Add another name')
+ );
+ $this->assertText(t('Name #@num', array('@num' => $i + 1)));
+ }
+
+ // Now remove the last name added (#5).
+ $this->drupalPost(NULL, array(), t('Remove latest name'));
+ $this->assertNoText("Name #5");
+
+ $this->drupalPost(NULL, array(), t('Submit'));
+
+ $this->assertText('Form 9 has been submitted');
+ for ($i = 1; $i <= 4; $i++) {
+ $this->assertText(t('@num: firstname @num lastname @num (@year)', array('@num' => $i, '@year' => 1950 + $i)));
+ }
+
+ // #10
+ $url = 'examples/form_example/tutorial/10';
+
+ $this->drupalPost($url, array(), t('Submit'));
+ $this->assertText(t('No file was uploaded.'));
+
+ // Get sample images.
+ $images = $this->drupalGetTestFiles('image');
+ foreach ($images as $image) {
+ $this->drupalPost($url, array('files[file]' => drupal_realpath($image->uri)), t('Submit'));
+ $this->assertText(t('The form has been submitted and the image has been saved, filename: @filename.', array('@filename' => $image->filename)));
+ }
+
+ // #11: Confirmation form.
+ // Try to submit without a name.
+ $url = 'examples/form_example/tutorial/11';
+ $this->drupalPost($url, array(), t('Submit'));
+ $this->assertText('Name field is required.');
+
+ // Verify that we can enter a name and get the confirmation form.
+ $this->drupalPost(
+ $url,
+ array('name' => t('name 1')), t('Submit')
+ );
+ $this->assertText(t('Is this really your name?'));
+ $this->assertFieldById('edit-name', 'name 1');
+
+ // Check the 'yes' button.
+ $confirmation_text = t("Confirmation form submission recieved. According to your submission your name is '@name'", array('@name' => 'name 1'));
+ $url = 'examples/form_example/tutorial/11/confirm/name%201';
+ $this->drupalPost($url, array(), t('This is my name'));
+ $this->assertText($confirmation_text);
+
+ // Check the 'no' button.
+ $this->drupalGet($url);
+ $this->clickLink(t('Nope, not my name'));
+ $this->assertNoText($confirmation_text);
+ }
+
+ /**
+ * Test Wizard tutorial.
+ *
+ * @TODO improve this using drupal_form_submit
+ */
+ public function testWizard() {
+ // Check if the wizard is there.
+ $this->drupalGet('examples/form_example/wizard');
+ $this->assertText(t('Extensible wizard example'));
+
+ $first_name = $this->randomName(8);
+ $last_name = $this->randomName(8);
+ $city = $this->randomName(8);
+ $aunts_name = $this->randomName(8);
+
+ // Submit the first step of the wizard.
+ $options = array(
+ 'first_name' => $first_name,
+ 'last_name' => $last_name,
+ );
+ $this->drupalPost('examples/form_example/wizard', $options, t('Next'));
+
+ // A label city is created, and two buttons appear, Previous and Next.
+ $this->assertText(t('Hint: Do not enter "San Francisco", and do not leave this out.'));
+
+ // Go back to the beginning and verify that the value is there.
+ $this->drupalPost(NULL, array(), t('Previous'));
+ $this->assertFieldByName('first_name', $first_name);
+ $this->assertFieldByName('last_name', $last_name);
+
+ // Go next. We should keep our values.
+ $this->drupalPost(NULL, array(), t('Next'));
+ $this->assertText(t('Hint: Do not enter "San Francisco", and do not leave this out.'));
+
+ // Try "San Francisco".
+ $this->drupalPost(NULL, array('city' => 'San Francisco'), t('Next'));
+ $this->assertText(t('You were warned not to enter "San Francisco"'));
+
+ // Try the real city.
+ $this->drupalPost(NULL, array('city' => $city), t('Next'));
+
+ // Enter the Aunt's name, but then the previous button.
+ $this->drupalPost(NULL, array('aunts_name' => $aunts_name), t('Previous'));
+ $this->assertFieldByName('city', $city);
+
+ // Now go forward and then press finish; check for correct values.
+ $this->drupalPost(NULL, array(), t('Next'));
+ $this->drupalPost(NULL, array('aunts_name' => $aunts_name), t('Finish'));
+
+ $this->assertRaw(t('[first_name] => @first_name', array('@first_name' => $first_name)));
+ $this->assertRaw(t('[last_name] => @last_name', array('@last_name' => $last_name)));
+ $this->assertRaw(t('[city] => @city', array('@city' => $city)));
+ $this->assertRaw(t('[aunts_name] => @aunts_name', array('@aunts_name' => $aunts_name)));
+ }
+
+
+ /**
+ * Test the element_example form for correct behavior.
+ */
+ public function testElementExample() {
+ // Make one basic POST with a set of values and check for correct responses.
+ $edit = array(
+ 'a_form_example_textfield' => $this->randomName(),
+ 'a_form_example_checkbox' => TRUE,
+ 'a_form_example_element_discrete[areacode]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_discrete[prefix]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_discrete[extension]' => sprintf('%04d', rand(0, 9999)),
+ 'a_form_example_element_combined[areacode]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_combined[prefix]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_combined[extension]' => sprintf('%04d', rand(0, 9999)),
+ );
+ $this->drupalPost('examples/form_example/element_example', $edit, t('Submit'));
+ $this->assertText(t('a_form_example_textfield has value @value', array('@value' => $edit['a_form_example_textfield'])));
+ $this->assertText(t('a_form_example_checkbox has value 1'));
+ $this->assertPattern(t('/areacode.*!areacode/', array('!areacode' => $edit['a_form_example_element_discrete[areacode]'])));
+ $this->assertPattern(t('/prefix.*!prefix/', array('!prefix' => $edit['a_form_example_element_discrete[prefix]'])));
+ $this->assertPattern(t('/extension.*!extension/', array('!extension' => $edit['a_form_example_element_discrete[extension]'])));
+
+ $this->assertText(t('a_form_example_element_combined has value @value', array('@value' => $edit['a_form_example_element_combined[areacode]'] . $edit['a_form_example_element_combined[prefix]'] . $edit['a_form_example_element_combined[extension]'])));
+
+ // Now flip the checkbox and check for correct behavior.
+ $edit['a_form_example_checkbox'] = FALSE;
+ $this->drupalPost('examples/form_example/element_example', $edit, t('Submit'));
+ $this->assertText(t('a_form_example_checkbox has value 0'));
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example_elements.inc b/sites/all/modules/contrib/dev/examples/form_example/form_example_elements.inc
new file mode 100644
index 00000000..1348a80a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example_elements.inc
@@ -0,0 +1,531 @@
+ TRUE,
+
+ // Use theme('textfield') to format this element on output.
+ '#theme' => array('textfield'),
+
+ // Do not provide autocomplete.
+ '#autocomplete_path' => FALSE,
+
+ // Allow theme('form_element') to control the markup surrounding this
+ // value on output.
+ '#theme_wrappers' => array('form_element'),
+ );
+
+ // form_example_checkbox is mostly a copy of the system-defined checkbox
+ // element.
+ $types['form_example_checkbox'] = array(
+ // This is an HTML .
+ '#input' => TRUE,
+
+ // @todo: Explain #return_value.
+ '#return_value' => TRUE,
+
+ // Our #process array will use the standard process functions used for a
+ // regular checkbox.
+ '#process' => array('form_process_checkbox', 'ajax_process_form'),
+
+ // Use theme('form_example_checkbox') to render this element on output.
+ '#theme' => 'form_example_checkbox',
+
+ // Use theme('form_element') to provide HTML wrappers for this element.
+ '#theme_wrappers' => array('form_element'),
+
+ // Place the title after the element (to the right of the checkbox).
+ // This attribute affects the behavior of theme_form_element().
+ '#title_display' => 'after',
+
+ // We use the default function name for the value callback, so it does not
+ // have to be listed explicitly. The pattern for the default function name
+ // is form_type_TYPENAME_value().
+ // '#value_callback' => 'form_type_form_example_checkbox_value',
+ );
+
+ // This discrete phonenumber element keeps its values as the separate elements
+ // area code, prefix, extension.
+ $types['form_example_phonenumber_discrete'] = array(
+ // #input == TRUE means that the form value here will be used to determine
+ // what #value will be.
+ '#input' => TRUE,
+
+ // #process is an array of callback functions executed when this element is
+ // processed. Here it provides the child form elements which define
+ // areacode, prefix, and extension.
+ '#process' => array('form_example_phonenumber_discrete_process'),
+
+ // Validation handlers for this element. These are in addition to any
+ // validation handlers that might.
+ '#element_validate' => array('form_example_phonenumber_discrete_validate'),
+ '#autocomplete_path' => FALSE,
+ '#theme_wrappers' => array('form_example_inline_form_element'),
+ );
+
+ // Define form_example_phonenumber_combined, which combines the phone
+ // number into a single validated text string.
+ $types['form_example_phonenumber_combined'] = array(
+ '#input' => TRUE ,
+ '#process' => array('form_example_phonenumber_combined_process'),
+ '#element_validate' => array('form_example_phonenumber_combined_validate'),
+ '#autocomplete_path' => FALSE,
+ '#value_callback' => 'form_example_phonenumber_combined_value',
+ '#default_value' => array(
+ 'areacode' => '',
+ 'prefix' => '',
+ 'extension' => '',
+ ),
+ '#theme_wrappers' => array('form_example_inline_form_element'),
+ );
+ return $types;
+}
+
+
+/**
+ * Value callback for form_example_phonenumber_combined.
+ *
+ * Builds the current combined value of the phone number only when the form
+ * builder is not processing the input.
+ *
+ * @param array $element
+ * Form element.
+ * @param array $input
+ * Input.
+ * @param array $form_state
+ * Form state.
+ *
+ * @return array
+ * The modified element.
+ */
+function form_example_phonenumber_combined_value(&$element, $input = FALSE, $form_state = NULL) {
+ if (!$form_state['process_input']) {
+ $matches = array();
+ $match = preg_match('/^(\d{3})(\d{3})(\d{4})$/', $element['#default_value'], $matches);
+ if ($match) {
+ // Get rid of the "all match" element.
+ array_shift($matches);
+ list($element['areacode'], $element['prefix'], $element['extension']) = $matches;
+ }
+ }
+ return $element;
+}
+
+/**
+ * Value callback for form_example_checkbox element type.
+ *
+ * Copied from form_type_checkbox_value().
+ *
+ * @param array $element
+ * The form element whose value is being populated.
+ * @param mixed $input
+ * The incoming input to populate the form element. If this is FALSE, meaning
+ * there is no input, the element's default value should be returned.
+ *
+ * @return int
+ * The value represented by the form element.
+ */
+function form_type_form_example_checkbox_value($element, $input = FALSE) {
+ if ($input === FALSE) {
+ return isset($element['#default_value']) ? $element['#default_value'] : 0;
+ }
+ else {
+ return isset($input) ? $element['#return_value'] : 0;
+ }
+}
+
+/**
+ * Process callback for the discrete version of phonenumber.
+ */
+function form_example_phonenumber_discrete_process($element, &$form_state, $complete_form) {
+ // #tree = TRUE means that the values in $form_state['values'] will be stored
+ // hierarchically. In this case, the parts of the element will appear in
+ // $form_state['values'] as
+ // $form_state['values']['']['areacode'],
+ // $form_state['values']['']['prefix'],
+ // etc. This technique is preferred when an element has member form
+ // elements.
+ $element['#tree'] = TRUE;
+
+ // Normal FAPI field definitions, except that #value is defined.
+ $element['areacode'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#value' => $element['#value']['areacode'],
+ '#required' => TRUE,
+ '#prefix' => '(',
+ '#suffix' => ')',
+ );
+ $element['prefix'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#required' => TRUE,
+ '#value' => $element['#value']['prefix'],
+ );
+ $element['extension'] = array(
+ '#type' => 'textfield',
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#value' => $element['#value']['extension'],
+ );
+
+ return $element;
+}
+
+/**
+ * Validation handler for the discrete version of the phone number.
+ *
+ * Uses regular expressions to check that:
+ * - the area code is a three digit number.
+ * - the prefix is numeric 3-digit number.
+ * - the extension is a numeric 4-digit number.
+ *
+ * Any problems are shown on the form element using form_error().
+ */
+function form_example_phonenumber_discrete_validate($element, &$form_state) {
+ if (isset($element['#value']['areacode'])) {
+ if (0 == preg_match('/^\d{3}$/', $element['#value']['areacode'])) {
+ form_error($element['areacode'], t('The area code is invalid.'));
+ }
+ }
+ if (isset($element['#value']['prefix'])) {
+ if (0 == preg_match('/^\d{3}$/', $element['#value']['prefix'])) {
+ form_error($element['prefix'], t('The prefix is invalid.'));
+ }
+ }
+ if (isset($element['#value']['extension'])) {
+ if (0 == preg_match('/^\d{4}$/', $element['#value']['extension'])) {
+ form_error($element['extension'], t('The extension is invalid.'));
+ }
+ }
+ return $element;
+}
+
+/**
+ * Process callback for the combined version of the phonenumber element.
+ */
+function form_example_phonenumber_combined_process($element, &$form_state, $complete_form) {
+ // #tree = TRUE means that the values in $form_state['values'] will be stored
+ // hierarchically. In this case, the parts of the element will appear in
+ // $form_state['values'] as
+ // $form_state['values']['']['areacode'],
+ // $form_state['values']['']['prefix'],
+ // etc. This technique is preferred when an element has member form
+ // elements.
+ $element['#tree'] = TRUE;
+
+ // Normal FAPI field definitions, except that #value is defined.
+ $element['areacode'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#required' => TRUE,
+ '#prefix' => '(',
+ '#suffix' => ')',
+ );
+ $element['prefix'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#required' => TRUE,
+ );
+ $element['extension'] = array(
+ '#type' => 'textfield',
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#required' => TRUE,
+ );
+
+ $matches = array();
+ $match = preg_match('/^(\d{3})(\d{3})(\d{4})$/', $element['#default_value'], $matches);
+ if ($match) {
+ // Get rid of the "all match" element.
+ array_shift($matches);
+ list($element['areacode']['#default_value'], $element['prefix']['#default_value'], $element['extension']['#default_value']) = $matches;
+ }
+
+ return $element;
+}
+
+/**
+ * Phone number validation function for the combined phonenumber.
+ *
+ * Uses regular expressions to check that:
+ * - the area code is a three digit number
+ * - the prefix is numeric 3-digit number
+ * - the extension is a numeric 4-digit number
+ *
+ * Any problems are shown on the form element using form_error().
+ *
+ * The combined value is then updated in the element.
+ */
+function form_example_phonenumber_combined_validate($element, &$form_state) {
+ $lengths = array(
+ 'areacode' => 3,
+ 'prefix' => 3,
+ 'extension' => 4,
+ );
+ foreach ($lengths as $member => $length) {
+ $regex = '/^\d{' . $length . '}$/';
+ if (!empty($element['#value'][$member]) && 0 == preg_match($regex, $element['#value'][$member])) {
+ form_error($element[$member], t('@member is invalid', array('@member' => $member)));
+ }
+ }
+
+ // Consolidate into the three parts into one combined value.
+ $value = $element['areacode']['#value'] . $element['prefix']['#value'] . $element['extension']['#value'];
+ form_set_value($element, $value, $form_state);
+ return $element;
+}
+
+/**
+ * Called by form_example_theme() to provide hook_theme().
+ *
+ * This is kept in this file so it can be with the theme functions it presents.
+ * Otherwise it would get lonely.
+ */
+function _form_example_element_theme() {
+ return array(
+ 'form_example_inline_form_element' => array(
+ 'render element' => 'element',
+ 'file' => 'form_example_elements.inc',
+ ),
+ 'form_example_checkbox' => array(
+ 'render element' => 'element',
+ 'file' => 'form_example_elements.inc',
+ ),
+ );
+}
+
+/**
+ * Themes a custom checkbox.
+ *
+ * This doesn't actually do anything, but is here to show that theming can
+ * be done here.
+ */
+function theme_form_example_checkbox($variables) {
+ $element = $variables['element'];
+ return theme('checkbox', $element);
+}
+
+/**
+ * Formats child form elements as inline elements.
+ */
+function theme_form_example_inline_form_element($variables) {
+ $element = $variables['element'];
+
+ // Add element #id for #type 'item'.
+ if (isset($element['#markup']) && !empty($element['#id'])) {
+ $attributes['id'] = $element['#id'];
+ }
+ // Add element's #type and #name as class to aid with JS/CSS selectors.
+ $attributes['class'] = array('form-item');
+ if (!empty($element['#type'])) {
+ $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-');
+ }
+ if (!empty($element['#name'])) {
+ $attributes['class'][] = 'form-item-' . strtr($element['#name'],
+ array(
+ ' ' => '-',
+ '_' => '-',
+ '[' => '-',
+ ']' => '',
+ )
+ );
+ }
+ // Add a class for disabled elements to facilitate cross-browser styling.
+ if (!empty($element['#attributes']['disabled'])) {
+ $attributes['class'][] = 'form-disabled';
+ }
+ $output = '
' . "\n";
+
+ // If #title is not set, we don't display any label or required marker.
+ if (!isset($element['#title'])) {
+ $element['#title_display'] = 'none';
+ }
+ $prefix = isset($element['#field_prefix']) ? '' . $element['#field_prefix'] . ' ' : '';
+ $suffix = isset($element['#field_suffix']) ? ' ' . $element['#field_suffix'] . '' : '';
+
+ switch ($element['#title_display']) {
+ case 'before':
+ $output .= ' ' . theme('form_element_label', $variables);
+ $output .= ' ' . '
' . $prefix . $element['#children'] . $suffix . "
\n";
+ break;
+
+ case 'invisible':
+ case 'after':
+ $output .= ' ' . $prefix . $element['#children'] . $suffix;
+ $output .= ' ' . theme('form_element_label', $variables) . "\n";
+ break;
+
+ case 'none':
+ case 'attribute':
+ // Output no label and no required marker, only the children.
+ $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
+ break;
+ }
+
+ if (!empty($element['#description'])) {
+ $output .= '
' . $element['#description'] . "
\n";
+ }
+
+ $output .= "
\n";
+
+ return $output;
+}
+
+/**
+ * Form content for examples/form_example/element_example.
+ *
+ * Simple form to demonstrate how to use the various new FAPI elements
+ * we've defined.
+ */
+function form_example_element_demo_form($form, &$form_state) {
+ $form['a_form_example_textfield'] = array(
+ '#type' => 'form_example_textfield',
+ '#title' => t('Form Example textfield'),
+ '#default_value' => variable_get('form_example_textfield', ''),
+ '#description' => t('form_example_textfield is a new type, but it is actually uses the system-provided functions of textfield'),
+ );
+
+ $form['a_form_example_checkbox'] = array(
+ '#type' => 'form_example_checkbox',
+ '#title' => t('Form Example checkbox'),
+ '#default_value' => variable_get('form_example_checkbox', FALSE),
+ '#description' => t('Nothing more than a regular checkbox but with a theme provided by this module.'),
+ );
+
+ $form['a_form_example_element_discrete'] = array(
+ '#type' => 'form_example_phonenumber_discrete',
+ '#title' => t('Discrete phone number'),
+ '#default_value' => variable_get(
+ 'form_example_element_discrete',
+ array(
+ 'areacode' => '999',
+ 'prefix' => '999',
+ 'extension' => '9999',
+ )
+ ),
+ '#description' => t('A phone number : areacode (XXX), prefix (XXX) and extension (XXXX). This one uses a "discrete" element type, one which stores the three parts of the telephone number separately.'),
+ );
+
+ $form['a_form_example_element_combined'] = array(
+ '#type' => 'form_example_phonenumber_combined',
+ '#title' => t('Combined phone number'),
+ '#default_value' => variable_get('form_example_element_combined', '0000000000'),
+ '#description' => t('form_example_element_combined one uses a "combined" element type, one with a single 10-digit value which is broken apart when needed.'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for form_example_element_demo_form().
+ */
+function form_example_element_demo_form_submit($form, &$form_state) {
+ // Exclude unnecessary elements.
+ unset($form_state['values']['submit'], $form_state['values']['form_id'], $form_state['values']['op'], $form_state['values']['form_token'], $form_state['values']['form_build_id']);
+
+ foreach ($form_state['values'] as $key => $value) {
+ variable_set($key, $value);
+ drupal_set_message(
+ t('%name has value %value',
+ array(
+ '%name' => $key,
+ '%value' => print_r($value, TRUE),
+ )
+ )
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example_states.inc b/sites/all/modules/contrib/dev/examples/form_example/form_example_states.inc
new file mode 100644
index 00000000..28aed715
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example_states.inc
@@ -0,0 +1,296 @@
+ array(
+ * 'visible' => array(
+ * ':input[name="student_type"]' => array('value' => 'high_school'),
+ * ),
+ * ),
+ * @endcode
+ * Meaning that the element is to be made visible when the condition is met.
+ * The condition is a combination of a jQuery selector (which selects the
+ * element we want to test) and a condition for that element. In this case,
+ * the condition is whether the return value of the 'student_type' element is
+ * 'high_school'. If it is, this element will be visible.
+ *
+ * So the syntax is:
+ * @code
+ * '#states' => array(
+ * 'action_to_take_on_this_form_element' => array(
+ * 'jquery_selector_for_another_element' => array(
+ * 'condition_type' => value,
+ * ),
+ * ),
+ * ),
+ * @endcode
+ *
+ * If you need an action to take place only when two different conditions are
+ * true, then you add both of those conditions to the action. See the
+ * 'country_writein' element below for an example.
+ *
+ * Note that the easiest way to select a textfield, checkbox, or select is with
+ * the
+ * @link http://api.jquery.com/input-selector/ ':input' jquery shortcut @endlink,
+ * which selects any any of those.
+ *
+ * There are examples below of changing or hiding an element when a checkbox
+ * is checked, when a textarea is filled, when a select has a given value.
+ *
+ * See drupal_process_states() for full documentation.
+ *
+ * @see forms_api_reference.html
+ */
+function form_example_states_form($form, &$form_state) {
+ $form['student_type'] = array(
+ '#type' => 'radios',
+ '#options' => array(
+ 'high_school' => t('High School'),
+ 'undergraduate' => t('Undergraduate'),
+ 'graduate' => t('Graduate'),
+ ),
+ '#title' => t('What type of student are you?'),
+ );
+ $form['high_school'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('High School Information'),
+ // This #states rule says that the "high school" fieldset should only
+ // be shown if the "student_type" form element is set to "High School".
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'high_school'),
+ ),
+ ),
+ );
+
+ // High school information.
+ $form['high_school']['tests_taken'] = array(
+ '#type' => 'checkboxes',
+ '#options' => drupal_map_assoc(array(t('SAT'), t('ACT'))),
+ '#title' => t('What standardized tests did you take?'),
+ // This #states rule says that this checkboxes array will be visible only
+ // when $form['student_type'] is set to t('High School').
+ // It uses the jQuery selector :input[name=student_type] to choose the
+ // element which triggers the behavior, and then defines the "High School"
+ // value as the one that triggers visibility.
+ '#states' => array(
+ // Action to take.
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'high_school'),
+ ),
+ ),
+ );
+
+ $form['high_school']['sat_score'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your SAT score:'),
+ '#size' => 4,
+
+ // This #states rule limits visibility to when the $form['tests_taken']
+ // 'SAT' checkbox is checked."
+ '#states' => array(
+ // Action to take.
+ 'visible' => array(
+ ':input[name="tests_taken[SAT]"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+ $form['high_school']['act_score'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your ACT score:'),
+ '#size' => 4,
+
+ // Set this element visible if the ACT checkbox above is checked.
+ '#states' => array(
+ // Action to take.
+ 'visible' => array(
+ ':input[name="tests_taken[ACT]"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ // Undergrad information.
+ $form['undergraduate'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Undergraduate Information'),
+ // This #states rule says that the "undergraduate" fieldset should only
+ // be shown if the "student_type" form element is set to "Undergraduate".
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'undergraduate'),
+ ),
+ ),
+ );
+
+ $form['undergraduate']['how_many_years'] = array(
+ '#type' => 'select',
+ '#title' => t('How many years have you completed?'),
+ // The options here are integers, but since all the action here happens
+ // using the DOM on the client, we will have to use strings to work with
+ // them.
+ '#options' => array(
+ 1 => t('One'),
+ 2 => t('Two'),
+ 3 => t('Three'),
+ 4 => t('Four'),
+ 5 => t('Lots'),
+ ),
+ );
+
+ $form['undergraduate']['comment'] = array(
+ '#type' => 'item',
+ '#description' => t("Wow, that's a long time."),
+ '#states' => array(
+ 'visible' => array(
+ // Note that '5' must be used here instead of the integer 5.
+ // The information is coming from the DOM as a string.
+ ':input[name="how_many_years"]' => array('value' => '5'),
+ ),
+ ),
+ );
+ $form['undergraduate']['school_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your college or university:'),
+ );
+ $form['undergraduate']['school_country'] = array(
+ '#type' => 'select',
+ '#options' => drupal_map_assoc(array(t('UK'), t('Other'))),
+ '#title' => t('In what country is your college or university located?'),
+ );
+ $form['undergraduate']['country_writein'] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#title' => t('Please enter the name of the country where your college or university is located.'),
+
+ // Only show this field if school_country is set to 'Other'.
+ '#states' => array(
+ // Action to take: Make visible.
+ 'visible' => array(
+ ':input[name="school_country"]' => array('value' => t('Other')),
+ ),
+ ),
+ );
+
+ $form['undergraduate']['thanks'] = array(
+ '#type' => 'item',
+ '#description' => t('Thanks for providing both your school and your country.'),
+ '#states' => array(
+ // Here visibility requires that two separate conditions be true.
+ 'visible' => array(
+ ':input[name="school_country"]' => array('value' => t('Other')),
+ ':input[name="country_writein"]' => array('filled' => TRUE),
+ ),
+ ),
+ );
+ $form['undergraduate']['go_away'] = array(
+ '#type' => 'submit',
+ '#value' => t('Done with form'),
+ '#states' => array(
+ // Here visibility requires that two separate conditions be true.
+ 'visible' => array(
+ ':input[name="school_country"]' => array('value' => t('Other')),
+ ':input[name="country_writein"]' => array('filled' => TRUE),
+ ),
+ ),
+ );
+
+ // Graduate student information.
+ $form['graduate'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Graduate School Information'),
+ // This #states rule says that the "graduate" fieldset should only
+ // be shown if the "student_type" form element is set to "Graduate".
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'graduate'),
+ ),
+ ),
+ );
+ $form['graduate']['more_info'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Please describe your graduate studies'),
+ );
+
+ $form['graduate']['info_provide'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Check here if you have provided information above'),
+ '#disabled' => TRUE,
+ '#states' => array(
+ // Mark this checkbox checked if the "more_info" textarea has something
+ // in it, if it's 'filled'.
+ 'checked' => array(
+ ':input[name="more_info"]' => array('filled' => TRUE),
+ ),
+ ),
+ );
+
+ $form['average'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter your average'),
+ // To trigger a state when the same controlling element can have more than
+ // one possible value, put all values in a higher-level array.
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array(
+ array('value' => 'high_school'),
+ array('value' => 'undergraduate'),
+ ),
+ ),
+ ),
+ );
+
+ $form['expand_more_info'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Check here if you want to add more information.'),
+ );
+ $form['more_info'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Additional Information'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+
+ // Expand the expand_more_info fieldset if the box is checked.
+ '#states' => array(
+ 'expanded' => array(
+ ':input[name="expand_more_info"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+ $form['more_info']['feedback'] = array(
+ '#type' => 'textarea',
+ '#title' => t('What do you have to say?'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit your information'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for form_example_states_form().
+ */
+function form_example_states_form_submit($form, &$form_state) {
+ drupal_set_message(t('Submitting values: @values', array('@values' => var_export($form_state['values'], TRUE))));
+}
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example_tutorial.inc b/sites/all/modules/contrib/dev/examples/form_example/form_example_tutorial.inc
new file mode 100644
index 00000000..4042129e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example_tutorial.inc
@@ -0,0 +1,934 @@
+Drupal handbook.');
+}
+
+/**
+ * Tutorial Example 1.
+ *
+ * This first form function is from the
+ * @link http://drupal.org/node/717722 Form Tutorial handbook page @endlink
+ *
+ * It just creates a very basic form with a textfield.
+ *
+ * This function is called the "form constructor function". It builds the form.
+ * It takes a two arguments, $form and $form_state, but if drupal_get_form()
+ * sends additional arguments, they will be provided after $form_state.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_1($form, &$form_state) {
+
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with nothing but a textfield'),
+ );
+ // This is the first form element. It's a textfield with a label, "Name"
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ );
+ return $form;
+}
+
+/**
+ * This is Example 2, a basic form with a submit button.
+ *
+ * @see http://drupal.org/node/717726
+ * @ingroup form_example
+ */
+function form_example_tutorial_2($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A simple form with a submit button'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ );
+
+ // Adds a simple submit button that refreshes the form and clears its
+ // contents. This is the default behavior for forms.
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 3: A basic form with fieldsets.
+ *
+ * We establish a fieldset element and then place two text fields within
+ * it, one for a first name and one for a last name. This helps us group
+ * related content.
+ *
+ * Study the code below and you'll notice that we renamed the array of the first
+ * and last name fields by placing them under the $form['name']
+ * array. This tells Form API these fields belong to the $form['name'] fieldset.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_3($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with a fieldset'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ );
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 4: Basic form with required fields.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_4($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with required fields'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ // Make the fieldset collapsible.
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ // Make these fields required.
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 5: Basic form with additional element attributes.
+ *
+ * This demonstrates additional attributes of text form fields.
+ *
+ * See the
+ * @link http://api.drupal.org/api/file/developer/topics/forms_api.html complete form reference @endlink
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_5($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with additional attributes'),
+ '#description' => t('This one adds #default_value and #description'),
+ );
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ '#default_value' => "First name",
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 6: A basic form with a validate handler.
+ *
+ * From http://drupal.org/node/717736
+ * @see form_example_tutorial_6_validate()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_6($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with a validation handler'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ '#default_value' => "First name",
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+
+ // New form field added to permit entry of year of birth.
+ // The data entered into this field will be validated with
+ // the default validation function.
+ $form['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => "Year of birth",
+ '#description' => 'Format is "YYYY"',
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Validation handler for Tutorial 6.
+ *
+ * Now we add a handler/function to validate the data entered into the
+ * "year of birth" field to make sure it's between the values of 1900
+ * and 2000. If not, it displays an error. The value report is
+ * $form_state['values'] (see http://drupal.org/node/144132#form-state).
+ *
+ * Notice the name of the function. It is simply the name of the form
+ * followed by '_validate'. This is always the name of the default validation
+ * function. An alternate list of validation functions could have been provided
+ * in $form['#validate'].
+ *
+ * @see form_example_tutorial_6()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_6_validate($form, &$form_state) {
+ $year_of_birth = $form_state['values']['year_of_birth'];
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
+ }
+}
+
+/**
+ * Example 7: With a submit handler.
+ *
+ * From the handbook page:
+ * http://drupal.org/node/717740
+ *
+ * @see form_example_tutorial_7_validate()
+ * @see form_example_tutorial_7_submit()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_7($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with a submit handler'),
+ );
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ '#default_value' => "First name",
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+ $form['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => "Year of birth",
+ '#description' => 'Format is "YYYY"',
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+
+/**
+ * Validation function for form_example_tutorial_7().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_7_validate($form, &$form_state) {
+ $year_of_birth = $form_state['values']['year_of_birth'];
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
+ }
+}
+
+/**
+ * Submit function for form_example_tutorial_7().
+ *
+ * Adds a submit handler/function to our form to send a successful
+ * completion message to the screen.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_7_submit($form, &$form_state) {
+ drupal_set_message(t('The form has been submitted. name="@first @last", year of birth=@year_of_birth',
+ array(
+ '@first' => $form_state['values']['first'],
+ '@last' => $form_state['values']['last'],
+ '@year_of_birth' => $form_state['values']['year_of_birth'],
+ )
+ ));
+}
+
+/**
+ * Example 8: A simple multistep form with a Next and a Back button.
+ *
+ * Handbook page: http://drupal.org/node/717750.
+ *
+ * For more extensive multistep forms, see
+ * @link form_example_wizard.inc form_example_wizard.inc @endlink
+ *
+ *
+ * Adds logic to our form builder to give it two pages.
+ * The @link ajax_example_wizard AJAX Example's Wizard Example @endlink
+ * gives an AJAX version of this same idea.
+ *
+ * @see form_example_tutorial_8_page_two()
+ * @see form_example_tutorial_8_page_two_back()
+ * @see form_example_tutorial_8_page_two_submit()
+ * @see form_example_tutorial_8_next_submit()
+ * @see form_example_tutorial.inc
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8($form, &$form_state) {
+
+ // Display page 2 if $form_state['page_num'] == 2
+ if (!empty($form_state['page_num']) && $form_state['page_num'] == 2) {
+ return form_example_tutorial_8_page_two($form, $form_state);
+ }
+
+ // Otherwise we build page 1.
+ $form_state['page_num'] = 1;
+
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A basic multistep form (page 1)'),
+ );
+
+ $form['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ '#required' => TRUE,
+ '#default_value' => !empty($form_state['values']['first']) ? $form_state['values']['first'] : '',
+ );
+ $form['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#default_value' => !empty($form_state['values']['last']) ? $form_state['values']['last'] : '',
+ );
+ $form['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => "Year of birth",
+ '#description' => 'Format is "YYYY"',
+ '#default_value' => !empty($form_state['values']['year_of_birth']) ? $form_state['values']['year_of_birth'] : '',
+ );
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => 'Next >>',
+ '#submit' => array('form_example_tutorial_8_next_submit'),
+ '#validate' => array('form_example_tutorial_8_next_validate'),
+ );
+ return $form;
+}
+
+/**
+ * Returns the form for the second page of form_example_tutorial_8().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_page_two($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A basic multistep form (page 2)'),
+ );
+
+ $form['color'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Favorite color'),
+ '#required' => TRUE,
+ '#default_value' => !empty($form_state['values']['color']) ? $form_state['values']['color'] : '',
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ '#submit' => array('form_example_tutorial_8_page_two_submit'),
+ );
+ $form['back'] = array(
+ '#type' => 'submit',
+ '#value' => t('<< Back'),
+ '#submit' => array('form_example_tutorial_8_page_two_back'),
+ // We won't bother validating the required 'color' field, since they
+ // have to come back to this page to submit anyway.
+ '#limit_validation_errors' => array(),
+ );
+ return $form;
+}
+
+
+/**
+ * Validate handler for the next button on first page.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_next_validate($form, &$form_state) {
+ $year_of_birth = $form_state['values']['year_of_birth'];
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
+ }
+}
+
+/**
+ * Submit handler for form_example_tutorial_8() next button.
+ *
+ * Capture the values from page one and store them away so they can be used
+ * at final submit time.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_next_submit($form, &$form_state) {
+
+ // Values are saved for each page.
+ // to carry forward to subsequent pages in the form.
+ // and we tell FAPI to rebuild the form.
+ $form_state['page_values'][1] = $form_state['values'];
+
+ if (!empty($form_state['page_values'][2])) {
+ $form_state['values'] = $form_state['page_values'][2];
+ }
+
+ // When form rebuilds, it will look at this to figure which page to build.
+ $form_state['page_num'] = 2;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Back button handler submit handler.
+ *
+ * Since #limit_validation_errors = array() is set, values from page 2
+ * will be discarded. We load the page 1 values instead.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_page_two_back($form, &$form_state) {
+ $form_state['values'] = $form_state['page_values'][1];
+ $form_state['page_num'] = 1;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * The page 2 submit handler.
+ *
+ * This is the final submit handler. Gather all the data together and output
+ * it in a drupal_set_message().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_page_two_submit($form, &$form_state) {
+ // Normally, some code would go here to alter the database with the data
+ // collected from the form. Instead sets a message with drupal_set_message()
+ // to validate that the code worked.
+ $page_one_values = $form_state['page_values'][1];
+ drupal_set_message(t('The form has been submitted. name="@first @last", year of birth=@year_of_birth',
+ array(
+ '@first' => $page_one_values['first'],
+ '@last' => $page_one_values['last'],
+ '@year_of_birth' => $page_one_values['year_of_birth'],
+ )
+ ));
+
+ if (!empty($page_one_values['first2'])) {
+ drupal_set_message(t('Second name: name="@first @last", year of birth=@year_of_birth',
+ array(
+ '@first' => $page_one_values['first2'],
+ '@last' => $page_one_values['last2'],
+ '@year_of_birth' => $page_one_values['year_of_birth2'],
+ )
+ ));
+ }
+ drupal_set_message(t('And the favorite color is @color', array('@color' => $form_state['values']['color'])));
+
+ // If we wanted to redirect on submission, set $form_state['redirect']. For
+ // simple redirects, the value can be a string of the path to redirect to. For
+ // example, to redirect to /node, one would specify the following:
+ //
+ // $form_state['redirect'] = 'node';
+ //
+ // For more complex redirects, this value can be set to an array of options to
+ // pass to drupal_goto(). For example, to redirect to /foo?bar=1#baz, one
+ // would specify the following:
+ //
+ // @code
+ // $form_state['redirect'] = array(
+ // 'foo',
+ // array(
+ // 'query' => array('bar' => 1),
+ // 'fragment' => 'baz',
+ // ),
+ // );
+ // @endcode
+ //
+ // The first element in the array is the path to redirect to, and the second
+ // element in the array is the array of options. For more information on the
+ // available options, see http://api.drupal.org/url.
+}
+
+/**
+ * Example 9: A form with a dynamically added new fields.
+ *
+ * This example adds default values so that when the form is rebuilt,
+ * the form will by default have the previously-entered values.
+ *
+ * From handbook page http://drupal.org/node/717746.
+ *
+ * @see form_example_tutorial_9_add_name()
+ * @see form_example_tutorial_9_remove_name()
+ * @see form_example_tutorial_9_submit()
+ * @see form_example_tutorial_9_validate()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9($form, &$form_state) {
+
+ // We will have many fields with the same name, so we need to be able to
+ // access the form hierarchically.
+ $form['#tree'] = TRUE;
+
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with dynamically added new fields'),
+ );
+
+ if (empty($form_state['num_names'])) {
+ $form_state['num_names'] = 1;
+ }
+
+ // Build the number of name fieldsets indicated by $form_state['num_names']
+ for ($i = 1; $i <= $form_state['num_names']; $i++) {
+ $form['name'][$i] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name #@num', array('@num' => $i)),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ $form['name'][$i]['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#description' => t("Enter first name."),
+ '#size' => 20,
+ '#maxlength' => 20,
+ '#required' => TRUE,
+ );
+ $form['name'][$i]['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter Last name'),
+ '#required' => TRUE,
+ );
+ $form['name'][$i]['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => t("Year of birth"),
+ '#description' => t('Format is "YYYY"'),
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+
+ // Adds "Add another name" button.
+ $form['add_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add another name'),
+ '#submit' => array('form_example_tutorial_9_add_name'),
+ );
+
+ // If we have more than one name, this button allows removal of the
+ // last name.
+ if ($form_state['num_names'] > 1) {
+ $form['remove_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Remove latest name'),
+ '#submit' => array('form_example_tutorial_9_remove_name'),
+ // Since we are removing a name, don't validate until later.
+ '#limit_validation_errors' => array(),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Submit handler for "Add another name" button on form_example_tutorial_9().
+ *
+ * $form_state['num_names'] tells the form builder function how many name
+ * fieldsets to build, so here we increment it.
+ *
+ * All elements of $form_state are persisted, so there's no need to use a
+ * particular key, like the old $form_state['storage']. We can just use
+ * $form_state['num_names'].
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_add_name($form, &$form_state) {
+ // Everything in $form_state is persistent, so we'll just use
+ // $form_state['add_name']
+ $form_state['num_names']++;
+
+ // Setting $form_state['rebuild'] = TRUE causes the form to be rebuilt again.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for "Remove name" button on form_example_tutorial_9().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_remove_name($form, &$form_state) {
+ if ($form_state['num_names'] > 1) {
+ $form_state['num_names']--;
+ }
+
+ // Setting $form_state['rebuild'] = TRUE causes the form to be rebuilt again.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Validate function for form_example_tutorial_9().
+ *
+ * Adds logic to validate the form to check the validity of the new fields,
+ * if they exist.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_validate($form, &$form_state) {
+
+ for ($i = 1; $i <= $form_state['num_names']; $i++) {
+ $year_of_birth = $form_state['values']['name'][$i]['year_of_birth'];
+
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error("name][$i][year_of_birth", t('Enter a year between 1900 and 2000.'));
+ }
+ }
+}
+
+/**
+ * Submit function for form_example_tutorial_9().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_submit($form, &$form_state) {
+ $output = t("Form 9 has been submitted.");
+ for ($i = 1; $i <= $form_state['num_names']; $i++) {
+ $output .= t("@num: @first @last (@date)...",
+ array(
+ '@num' => $i,
+ '@first' => $form_state['values']['name'][$i]['first'],
+ '@last' => $form_state['values']['name'][$i]['last'],
+ '@date' => $form_state['values']['name'][$i]['year_of_birth'],
+ )
+ ) . ' ';
+ }
+ drupal_set_message($output);
+}
+
+/**
+ * Example 10: A form with a file upload field.
+ *
+ * This example allows the user to upload a file to Drupal which is stored
+ * physically and with a reference in the database.
+ *
+ * @see form_example_tutorial_10_submit()
+ * @see form_example_tutorial_10_validate()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_10($form_state) {
+ // If you are familiar with how browsers handle files, you know that
+ // enctype="multipart/form-data" is required. Drupal takes care of that, so
+ // you don't need to include it yourself.
+ $form['file'] = array(
+ '#type' => 'file',
+ '#title' => t('Image'),
+ '#description' => t('Upload a file, allowed extensions: jpg, jpeg, png, gif'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for form_example_tutorial_10().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_10_validate($form, &$form_state) {
+ $file = file_save_upload('file', array(
+ // Validates file is really an image.
+ 'file_validate_is_image' => array(),
+ // Validate extensions.
+ 'file_validate_extensions' => array('png gif jpg jpeg'),
+ ));
+ // If the file passed validation:
+ if ($file) {
+ // Move the file into the Drupal file system.
+ if ($file = file_move($file, 'public://')) {
+ // Save the file for use in the submit handler.
+ $form_state['storage']['file'] = $file;
+ }
+ else {
+ form_set_error('file', t("Failed to write the uploaded file to the site's file folder."));
+ }
+ }
+ else {
+ form_set_error('file', t('No file was uploaded.'));
+ }
+}
+
+/**
+ * Submit handler for form_example_tutorial_10().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_10_submit($form, &$form_state) {
+ $file = $form_state['storage']['file'];
+ // We are done with the file, remove it from storage.
+ unset($form_state['storage']['file']);
+ // Make the storage of the file permanent.
+ $file->status = FILE_STATUS_PERMANENT;
+ // Save file status.
+ file_save($file);
+ // Set a response to the user.
+ drupal_set_message(t('The form has been submitted and the image has been saved, filename: @filename.', array('@filename' => $file->filename)));
+}
+
+/**
+ * Example 11: adding a confirmation form.
+ *
+ * This example generates a simple form that, when submitted, directs
+ * the user to a confirmation form generated using the confirm_form function.
+ * It asks the user to verify that the name they input was correct
+ *
+ * @see form_example_tutorial_11_submit()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11($form, &$form_state) {
+ // This form is identical to the one in example 2 except for one thing: We are
+ // adding an #action tag to direct the form submission to a confirmation page.
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A set of two forms that demonstrate the confirm_form function. This form has an explicit action to direct the form to a confirmation page'),
+ );
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#required' => TRUE,
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Submit function for form_example_tutorial_11().
+ *
+ * Adds a submit handler/function to our form to redirect
+ * the user to a confirmation page.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11_submit($form, &$form_state) {
+ // Simple submit function that changes the redirect of the form based on the
+ // value of the name field.
+ $name = $form_state['values']['name'];
+ $form_state['redirect'] = 'examples/form_example/tutorial/11/confirm/' . urlencode($name);
+}
+
+/**
+ * Example 11: A form generated with confirm_form().
+ *
+ * This function generates the confirmation form using the confirm_form()
+ * function. If confirmed, it sets a drupal message to demonstrate it's success.
+ *
+ * @param string $name
+ * The urlencoded name entered by the user.
+ *
+ * @see form_example_tutorial_11_confirm_name_submit()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11_confirm_name($form, $form_state, $name) {
+ // confirm_form() returns a complete form array for confirming an action.
+ // It has 7 arguments: $form, $question, $path, $description, $yes, $no, and
+ // $name.
+ // - $form: Additional elements to add to the form that will be available in
+ // the submit handler.
+ // - $question: What is the user confirming? This will be the title of the
+ // page.
+ // - $path: Where should the page go if the user hits cancel?
+ // - $description = NULL: Additional text to display.
+ // - $yes = NULL: Anchor text for the confirmation button. Defaults to
+ // t('Confirm').
+ // - $no = NULL: Anchor text for the cancel link. Defaults to t('Cancel').
+ // - $name = 'confirm': The internal name used to refer to the confirmation
+ // item.
+
+
+
+ // First we make a textfield for our user's name. confirm_form() allows us to
+ // Add form elements to the confirmation form, so we'll take advangage of
+ // that.
+ $user_name_text_field = array(
+ 'name' => array(
+ '#type' => 'textfield',
+ // We don't want the user to be able to edit their name here.
+ '#disabled' => TRUE,
+ '#title' => t('Your name:'),
+ '#value' => urldecode($name),
+ ),
+ );
+
+ // The question to ask the user.
+ $confirmation_question = t('Is this really your name?');
+
+ // If the user clicks 'no,' they're sent to this path.
+ $cancel_path = 'examples/form_example/tutorial/11';
+
+ // Some helpful descriptive text.
+ $description = t('Please verify whether or not you have input your name correctly. If you verify you will be sent back to the form and a message will be set. Otherwise you will be sent to the same page but with no message.');
+
+ // These are the text for our yes and no buttons.
+ $yes_button = t('This is my name');
+ $no_button = t('Nope, not my name');
+
+ // The name Form API will use to refer to our confirmation form.
+ $confirm_name = 'confirm_example';
+
+ // Finally, call confirm_form() with our information, and then return the form
+ // array it gives us.
+ return confirm_form(
+ $user_name_text_field,
+ $confirmation_question,
+ $cancel_path,
+ $description,
+ $yes_button,
+ $no_button,
+ $confirm_name
+ );
+}
+
+/**
+ * Submit function for form_example_tutorial_11_confirm_form().
+ *
+ * Adds a submit handler/function to the confirmation form
+ * if this point is reached the submission has been confirmed
+ * so we will set a message to demonstrate the success.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11_confirm_name_submit($form, &$form_state) {
+ drupal_set_message(t("Confirmation form submission recieved. According to your submission your name is '@name'", array("@name" => $form_state['values']['name'])));
+ $form_state['redirect'] = 'examples/form_example/tutorial/11';
+}
diff --git a/sites/all/modules/contrib/dev/examples/form_example/form_example_wizard.inc b/sites/all/modules/contrib/dev/examples/form_example/form_example_wizard.inc
new file mode 100644
index 00000000..3caf57f7
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/form_example/form_example_wizard.inc
@@ -0,0 +1,325 @@
+ array(
+ 'form' => 'form_example_wizard_personal_info',
+ ),
+ 2 => array(
+ 'form' => 'form_example_wizard_location_info',
+ ),
+ 3 => array(
+ 'form' => 'form_example_wizard_other_info',
+ ),
+ );
+}
+
+/**
+ * The primary formbuilder function for the wizard form.
+ *
+ * This is the form that you should call with drupal_get_form() from your code,
+ * and it will include the rest of the step forms defined. You are not required
+ * to change this function, as this will handle all the step actions for you.
+ *
+ * This form has two defined submit handlers to process the different steps:
+ * - Previous: handles the way to get back one step in the wizard.
+ * - Next: handles each step form submission,
+ *
+ * The third handler, the finish button handler, is the default form_submit
+ * handler used to process the information.
+ *
+ * You are not required to change the next or previous handlers, but you must
+ * change the form_example_wizard_submit handler to perform the operations you
+ * need on the collected information.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard($form, &$form_state) {
+
+ // Initialize a description of the steps for the wizard.
+ if (empty($form_state['step'])) {
+ $form_state['step'] = 1;
+
+ // This array contains the function to be called at each step to get the
+ // relevant form elements. It will also store state information for each
+ // step.
+ $form_state['step_information'] = _form_example_steps();
+ }
+ $step = &$form_state['step'];
+ drupal_set_title(t('Extensible Wizard: Step @step', array('@step' => $step)));
+
+ // Call the function named in $form_state['step_information'] to get the
+ // form elements to display for this step.
+ $form = $form_state['step_information'][$step]['form']($form, $form_state);
+
+ // 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 ($step > 1) {
+ $form['prev'] = array(
+ '#type' => 'submit',
+ '#value' => t('Previous'),
+ '#name' => 'prev',
+ '#submit' => array('form_example_wizard_previous_submit'),
+ '#limit_validation_errors' => array(),
+ );
+ }
+
+ // Show the Next button only if there are more steps defined.
+ if ($step < count($form_state['step_information'])) {
+ // The Next button should be included on every step.
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Next'),
+ '#name' => 'next',
+ '#submit' => array('form_example_wizard_next_submit'),
+ );
+ }
+ else {
+ // Just in case there are no more steps, we use the default submit handler
+ // of the form wizard. Call this button Finish, Submit, or whatever you
+ // want to show. When this button is clicked, the
+ // form_example_wizard_submit handler will be called.
+ $form['finish'] = array(
+ '#type' => 'submit',
+ '#value' => t('Finish'),
+ );
+ }
+
+ // Include each validation function defined for the different steps.
+ if (function_exists($form_state['step_information'][$step]['form'] . '_validate')) {
+ $form['next']['#validate'] = array($form_state['step_information'][$step]['form'] . '_validate');
+ }
+
+ return $form;
+}
+
+/**
+ * Submit handler for the "previous" button.
+ *
+ * This function:
+ * - Stores away $form_state['values']
+ * - Decrements the step counter
+ * - Replaces $form_state['values'] with the values from the previous state.
+ * - Forces form rebuild.
+ *
+ * You are not required to change this function.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_previous_submit($form, &$form_state) {
+ $current_step = &$form_state['step'];
+ $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];
+ if ($current_step > 1) {
+ $current_step--;
+ $form_state['values'] = $form_state['step_information'][$current_step]['stored_values'];
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for the 'next' button.
+ *
+ * This function:
+ * - Saves away $form_state['values']
+ * - Increments the step count.
+ * - Replace $form_state['values'] from the last time we were at this page
+ * or with array() if we haven't been here before.
+ * - Force form rebuild.
+ *
+ * You are not required to change this function.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_next_submit($form, &$form_state) {
+ $current_step = &$form_state['step'];
+ $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];
+
+ if ($current_step < count($form_state['step_information'])) {
+ $current_step++;
+ if (!empty($form_state['step_information'][$current_step]['stored_values'])) {
+ $form_state['values'] = $form_state['step_information'][$current_step]['stored_values'];
+ }
+ else {
+ $form_state['values'] = array();
+ }
+ // Force rebuild with next step.
+ $form_state['rebuild'] = TRUE;
+ return;
+ }
+}
+
+/**
+ * The previous code was a 'skeleton' of a multistep wizard form. You are not
+ * required to change a line on the previous code (apart from defining your own
+ * steps in the _form_example_steps() function.
+ *
+ * All the code included from here is the content of the wizard, the steps of
+ * the form.
+ *
+ * First, let's show the defined steps for the wizard example.
+ * @ingroup form_example
+ */
+
+/**
+ * Returns form elements for the 'personal info' page of the wizard.
+ *
+ * This is the first step of the wizard, asking for two textfields: first name
+ * and last name.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_personal_info($form, &$form_state) {
+ $form = array();
+ $form['first_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First Name'),
+ '#default_value' => !empty($form_state['values']['first_name']) ? $form_state['values']['first_name'] : '',
+ );
+ $form['last_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last Name'),
+ '#default_value' => !empty($form_state['values']['last_name']) ? $form_state['values']['last_name'] : '',
+ );
+ return $form;
+}
+
+/**
+ * Returns form elements for the 'location info' page of the wizard.
+ *
+ * This is the second step of the wizard. This step asks for a textfield value:
+ * a City. This step also includes a validation declared later.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_location_info($form, &$form_state) {
+ $form = array();
+ $form['city'] = array(
+ '#type' => 'textfield',
+ '#title' => t('City'),
+ '#description' => t('Hint: Do not enter "San Francisco", and do not leave this out.'),
+ '#required' => TRUE,
+ '#default_value' => !empty($form_state['values']['city']) ? $form_state['values']['city'] : '',
+
+ );
+ return $form;
+}
+
+/**
+ * Custom validation form for the 'location info' page of the wizard.
+ *
+ * This is the validation function for the second step of the wizard.
+ * The city cannot be empty or be "San Francisco".
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_location_info_validate($form, &$form_state) {
+ if ($form_state['values']['city'] == 'San Francisco') {
+ form_set_error('city', t('You were warned not to enter "San Francisco"'));
+ }
+}
+
+/**
+ * Returns form elements for the 'other info' page of the wizard.
+ *
+ * This is the third and last step of the example wizard.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_other_info($form, &$form_state) {
+ $form = array();
+ $form['aunts_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t("Your first cousin's aunt's Social Security number"),
+ '#default_value' => !empty($form_state['values']['aunts_name']) ? $form_state['values']['aunts_name'] : '',
+ );
+ return $form;
+}
+
+/**
+ * Wizard form submit handler.
+ *
+ * This function:
+ * - Saves away $form_state['values']
+ * - Process all the form values.
+ *
+ * And now comes the magic of the wizard, the function that should handle all
+ * the inputs from the user on each different step.
+ *
+ * This demonstration handler just do a drupal_set_message() with the
+ * information collected on each different step of the wizard.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_submit($form, &$form_state) {
+ $current_step = &$form_state['step'];
+ $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];
+
+ // In this case we've completed the final page of the wizard, so process the
+ // submitted information.
+ drupal_set_message(t('This information was collected by this wizard:'));
+ foreach ($form_state['step_information'] as $index => $value) {
+ // Remove FAPI fields included in the values (form_token, form_id and
+ // form_build_id. This is not required, you may access the values using
+ // $value['stored_values'] but I'm removing them to make a more clear
+ // representation of the collected information as the complete array will
+ // be passed through drupal_set_message().
+ unset($value['stored_values']['form_id']);
+ unset($value['stored_values']['form_build_id']);
+ unset($value['stored_values']['form_token']);
+
+ // Now show all the values.
+ drupal_set_message(t('Step @num collected the following values:
' . t('Use this form to upload an image and choose an Image Style to use when displaying the image. This demonstrates basic use of the Drupal 7 Image styles & effects system.') . '
';
+ $output .= '
' . t('Image styles can be added/edited using the !link.', array('!link' => l(t('Image styles UI'), 'admin/config/media/image-styles'))) . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_image_default_styles().
+ *
+ * hook_image_default_styles() declares to Drupal any image styles that are
+ * provided by the module. An image style is a collection of image effects that
+ * are performed in a specified order, manipulating the image and generating a
+ * new derivative image.
+ *
+ * This hook can be used to declare image styles that your module depends on or
+ * allow you to define image styles in code and gain the benefits of using
+ * a version control system.
+ */
+function image_example_image_default_styles() {
+ // This hook returns an array, each component of which describes an image
+ // style. The array keys are the machine-readable image style names and
+ // to avoid namespace conflicts should begin with the name of the
+ // implementing module. e.g.) 'mymodule_stylename'. Styles names should
+ // use only alpha-numeric characters, underscores (_), and hyphens (-).
+ $styles = array();
+ $styles['image_example_style'] = array();
+
+ // Each style array consists of an 'effects' array that is made up of
+ // sub-arrays which define the individual image effects that are combined
+ // together to create the image style.
+ $styles['image_example_style']['effects'] = array(
+ array(
+ // Name of the image effect. See image_image_effect_info() in
+ // modules/image/image.effects.inc for a list of image effects available
+ // in Drupal 7 core.
+ 'name' => 'image_scale',
+ // Arguments to pass to the effect callback function.
+ // The arguments that an effect accepts are documented with each
+ // individual image_EFFECT_NAME_effect function. See image_scale_effect()
+ // for an example.
+ 'data' => array(
+ 'width' => 100,
+ 'height' => 100,
+ 'upscale' => 1,
+ ),
+ // The order in which image effects should be applied when using this
+ // style.
+ 'weight' => 0,
+ ),
+ // Add a second effect to this image style. Effects are executed in order
+ // and are cumulative. When applying an image style to an image the result
+ // will be the combination of all effects associated with that style.
+ array(
+ 'name' => 'image_example_colorize',
+ 'data' => array(
+ 'color' => '#FFFF66',
+ ),
+ 'weight' => 1,
+ ),
+ );
+
+ return $styles;
+}
+
+/**
+ * Implements hook_image_style_save().
+ *
+ * Allows modules to respond to updates to an image style's
+ * settings.
+ */
+function image_example_image_style_save($style) {
+ // The $style parameter is an image style array with one notable exception.
+ // When a user has chosen to replace a deleted style with another style the
+ // $style['name'] property contains the name of the replacement style and
+ // $style['old_name'] contains the name of the style being deleted.
+ //
+ // Here we update a variable that contains the name of the image style that
+ // the block provided by this module uses when formatting images to use the
+ // new user chosen style name.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('image_example_style_name', '')) {
+ variable_set('image_example_style_name', $style['name']);
+ }
+}
+
+/**
+ * Implements hook_image_style_delete().
+ *
+ * This hook allows modules to respond to image styles being deleted.
+ *
+ * @see image_example_style_save()
+ */
+function image_example_image_style_delete($style) {
+ // See information about $style paramater in documentation for
+ // image_example_style_save().
+ //
+ // Update the modules variable that contains the name of the image style
+ // being deleted to the name of the replacement style.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('image_example_style_name', '')) {
+ variable_set('image_example_style_name', $style['name']);
+ }
+}
+
+/**
+ * Implements hook_image_style_flush().
+ *
+ * This hook allows modules to respond when a style is being flushed. Styles
+ * are flushed any time a style is updated, an effect associated with the style
+ * is updated, a new effect is added to the style, or an existing effect is
+ * removed.
+ *
+ * Flushing removes all images generated using this style from the host. Once a
+ * style has been flushed derivative images will need to be regenerated. New
+ * images will be generated automatically as needed but it is worth noting that
+ * on a busy site with lots of images this could have an impact on performance.
+ *
+ * Note: This function does not currently have any effect as the example module
+ * does not use any caches. It is demonstrated here for completeness sake only.
+ */
+function image_example_style_flush($style) {
+ // Empty any caches populated by our module that could contain stale data
+ // after the style has been flushed. Stale data occurs because the module may
+ // have cached content with a reference to the derivative image which is
+ // being deleted.
+ cache_clear_all('*', 'image_example', TRUE);
+}
+
+/**
+ * Implements hook_image_styles_alter().
+ *
+ * Allows your module to modify, add, or remove image styles provided
+ * by other modules. The best use of this hook is to modify default styles that
+ * have not been overriden by the user. Altering styles that have been
+ * overriden by the user could have an adverse affect on the user experience.
+ * If you add an effect to a style through this hook and the user attempts to
+ * remove the effect it will immediatly be re-applied.
+ */
+function image_example_image_styles_alter(&$styles) {
+ // The $styles paramater is an array of image style arrays keyed by style
+ // name. You can check to see if a style has been overriden by checking the
+ // $styles['stylename']['storage'] property.
+ // Verify that the effect has not been overriden.
+ if ($styles['thumbnail']['storage'] == IMAGE_STORAGE_DEFAULT) {
+ // Add an additional colorize effect to the system provided thumbnail
+ // effect.
+ $styles['thumbnail']['effects'][] = array(
+ 'label' => t('Colorize #FFFF66'),
+ 'name' => 'image_example_colorize',
+ 'effect callback' => 'image_example_colorize_effect',
+ 'data' => array(
+ 'color' => '#FFFF66',
+ ),
+ 'weight' => 1,
+ );
+ }
+}
+
+/**
+ * Implements hook_image_effect_info().
+ *
+ * This hook allows your module to define additional image manipulation effects
+ * that can be used with image styles.
+ */
+function image_example_image_effect_info() {
+ $effects = array();
+
+ // The array is keyed on the machine-readable effect name.
+ $effects['image_example_colorize'] = array(
+ // Human readable name of the effect.
+ 'label' => t('Colorize'),
+ // (optional) Brief description of the effect that will be shown when
+ // adding or configuring this image effect.
+ 'help' => t('The colorize effect will first remove all color from the source image and then tint the image using the color specified.'),
+ // Name of function called to perform this effect.
+ 'effect callback' => 'image_example_colorize_effect',
+ // (optional) Name of function that provides a $form array with options for
+ // configuring the effect. Note that you only need to return the fields
+ // specific to your module. Submit buttons will be added automatically, and
+ // configuration options will be serailized and added to the 'data' element
+ // of the effect. The function will recieve the $effect['data'] array as
+ // its only parameter.
+ 'form callback' => 'image_example_colorize_form',
+ // (optional) Name of a theme function that will output a summary of this
+ // effects configuation. Used when displaying list of effects associated
+ // with an image style. In this example the function
+ // theme_image_example_colorize_summary will be called via the theme()
+ // function. Your module must also implement hook_theme() in order for this
+ // function to work correctly. See image_example_theme() and
+ // theme_image_example_colorize_summary().
+ 'summary theme' => 'image_example_colorize_summary',
+ );
+
+ return $effects;
+}
+
+/**
+ * Form Builder; Configuration settings for colorize effect.
+ *
+ * Create a $form array with the fields necessary for configuring the
+ * image_example_colorize effect.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the colorize options. Therefore it does not not need to
+ * include metadata about the effect, nor a submit button.
+ *
+ * @param array $data
+ * The current configuration for this colorize effect.
+ */
+function image_example_colorize_form($data) {
+ $form = array();
+ // You do not need to worry about handling saving/updating/deleting of the
+ // data collected. The image module will automatically serialize and store
+ // all data associated with an effect.
+ $form['color'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Color'),
+ '#description' => t('The color to use when colorizing the image. Use web-style hex colors. e.g.) #FF6633.'),
+ '#default_value' => isset($data['color']) ? $data['color'] : '',
+ '#size' => 7,
+ '#max_length' => 7,
+ '#required' => TRUE,
+ );
+ return $form;
+}
+
+/**
+ * Image effect callback; Colorize an image resource.
+ *
+ * @param object $image
+ * An image object returned by image_load().
+ * @param array $data
+ * An array of attributes to use when performing the colorize effect with the
+ * following items:
+ * - "color": The web-style hex color to use when colorizing the image.
+ *
+ * @return bool
+ * TRUE on success. FALSE on failure to colorize image.
+ */
+function image_example_colorize_effect(&$image, $data) {
+ // Image manipulation should be done to the $image->resource, which will be
+ // automatically saved as a new image once all effects have been applied.
+ // If your effect makes changes to the $image->resource that relate to any
+ // information stored in the $image->info array (width, height, etc.) you
+ // should update that information as well. See modules/system/image.gd.inc
+ // for examples of functions that perform image manipulations.
+ //
+ // Not all GD installations are created equal. It is a good idea to check for
+ // the existence of image manipulation functions before using them.
+ // PHP installations using non-bundled GD do not have imagefilter(). More
+ // information about image manipulation functions is available in the PHP
+ // manual. http://www.php.net/manual/en/book.image.php
+ if (!function_exists('imagefilter')) {
+ watchdog('image', 'The image %image could not be colorized because the imagefilter() function is not available in this PHP installation.', array('%file' => $image->source));
+ return FALSE;
+ }
+
+ // Verify that Drupal is using the PHP GD library for image manipulations
+ // since this effect depends on functions in the GD library.
+ if ($image->toolkit != 'gd') {
+ watchdog('image', 'Image colorize failed on %path. Using non GD toolkit.', array('%path' => $image->source), WATCHDOG_ERROR);
+ return FALSE;
+ }
+
+ // Convert short #FFF syntax to full #FFFFFF syntax.
+ if (strlen($data['color']) == 4) {
+ $c = $data['color'];
+ $data['color'] = $c[0] . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
+ }
+
+ // Convert #FFFFFF syntax to hexadecimal colors.
+ $data['color'] = hexdec(str_replace('#', '0x', $data['color']));
+
+ // Convert the hexadecimal color value to a color index value.
+ $rgb = array();
+ for ($i = 16; $i >= 0; $i -= 8) {
+ $rgb[] = (($data['color'] >> $i) & 0xFF);
+ }
+
+ // First desaturate the image, and then apply the new color.
+ imagefilter($image->resource, IMG_FILTER_GRAYSCALE);
+ imagefilter($image->resource, IMG_FILTER_COLORIZE, $rgb[0], $rgb[1], $rgb[2]);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function image_example_theme() {
+ return array(
+ 'image_example_colorize_summary' => array(
+ 'variables' => array('data' => NULL),
+ ),
+ 'image_example_image' => array(
+ 'variables' => array('image' => NULL, 'style' => NULL),
+ 'file' => 'image_example.pages.inc',
+ ),
+ );
+}
+
+/**
+ * Formats a summary of an image colorize effect.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - data: The current configuration for this colorize effect.
+ */
+function theme_image_example_colorize_summary($variables) {
+ $data = $variables['data'];
+ return t('as color #@color.', array('@color' => $data['color']));
+}
+/**
+ * @} End of "defgroup image_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/image_example/image_example.pages.inc b/sites/all/modules/contrib/dev/examples/image_example/image_example.pages.inc
new file mode 100644
index 00000000..d984defc
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/image_example/image_example.pages.inc
@@ -0,0 +1,166 @@
+ theme('image_example_image', array('image' => $image, 'style' => $style)),
+ );
+ }
+
+ // Use the #managed_file FAPI element to upload an image file.
+ $form['image_example_image_fid'] = array(
+ '#title' => t('Image'),
+ '#type' => 'managed_file',
+ '#description' => t('The uploaded image will be displayed on this page using the image style chosen below.'),
+ '#default_value' => variable_get('image_example_image_fid', ''),
+ '#upload_location' => 'public://image_example_images/',
+ );
+
+ // Provide a select field for choosing an image style to use when displaying
+ // the image.
+ $form['image_example_style_name'] = array(
+ '#title' => t('Image style'),
+ '#type' => 'select',
+ '#description' => t('Choose an image style to use when displaying this image.'),
+ // The image_style_options() function returns an array of all available
+ // image styles both the key and the value of the array are the image
+ // style's name. The function takes on paramater, a boolean flag
+ // signifying whether or not the array should include a option.
+ '#options' => image_style_options(TRUE),
+ '#default_value' => variable_get('image_example_style_name', ''),
+ );
+
+ // Submit Button.
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+
+ return $form;
+}
+
+/**
+ * Verifies that the user supplied an image with the form..
+ *
+ * @ingroup image_example
+ */
+function image_example_style_form_validate($form, &$form_state) {
+ if (!isset($form_state['values']['image_example_image_fid']) || !is_numeric($form_state['values']['image_example_image_fid'])) {
+ form_set_error('image_example_image_fid', t('Please select an image to upload.'));
+ }
+}
+
+/**
+ * Form Builder; Display a form for uploading an image.
+ *
+ * @ingroup image_example
+ */
+function image_example_style_form_submit($form, &$form_state) {
+ // When using the #managed_file form element the file is automatically
+ // uploaded an saved to the {file} table. The value of the corresponding
+ // form element is set to the {file}.fid of the new file.
+ //
+ // If fid is not 0 we have a valid file.
+ if ($form_state['values']['image_example_image_fid'] != 0) {
+ // The new file's status is set to 0 or temporary and in order to ensure
+ // that the file is not removed after 6 hours we need to change it's status
+ // to 1. Save the ID of the uploaded image for later use.
+ $file = file_load($form_state['values']['image_example_image_fid']);
+ $file->status = FILE_STATUS_PERMANENT;
+ file_save($file);
+
+ // When a module is managing a file, it must manage the usage count.
+ // Here we increment the usage count with file_usage_add().
+ file_usage_add($file, 'image_example', 'sample_image', 1);
+
+ // Save the fid of the file so that the module can reference it later.
+ variable_set('image_example_image_fid', $file->fid);
+ drupal_set_message(t('The image @image_name was uploaded and saved with an ID of @fid and will be displayed using the style @style.',
+ array(
+ '@image_name' => $file->filename,
+ '@fid' => $file->fid,
+ '@style' => $form_state['values']['image_example_style_name'],
+ )
+ ));
+ }
+ // If the file was removed we need to remove the module's reference to the
+ // removed file's fid, and remove the file.
+ elseif ($form_state['values']['image_example_image_fid'] == 0) {
+ // Retrieve the old file's id.
+ $fid = variable_get('image_example_image_fid', FALSE);
+ $file = $fid ? file_load($fid) : FALSE;
+ if ($file) {
+ // When a module is managing a file, it must manage the usage count.
+ // Here we decrement the usage count with file_usage_delete().
+ file_usage_delete($file, 'image_example', 'sample_image', 1);
+
+ // The file_delete() function takes a file object and checks to see if
+ // the file is being used by any other modules. If it is the delete
+ // operation is cancelled, otherwise the file is deleted.
+ file_delete($file);
+ }
+
+ // Either way the module needs to update it's reference since even if the
+ // file is in use by another module and not deleted we no longer want to
+ // use it.
+ variable_set('image_example_image_fid', FALSE);
+ drupal_set_message(t('The image @image_name was removed.', array('@image_name' => $file->filename)));
+ }
+
+ // Save the name of the image style chosen by the user.
+ variable_set('image_example_style_name', $form_state['values']['image_example_style_name']);
+}
+
+/**
+ * Theme function displays an image rendered using the specified style.
+ *
+ * @ingroup image_example
+ */
+function theme_image_example_image($variables) {
+ $image = $variables['image'];
+ $style = $variables['style'];
+
+ // theme_image_style() is the primary method for displaying images using
+ // one of the defined styles. The $variables array passed to the theme
+ // contains the following two important values:
+ // - 'style_name': the name of the image style to use when displaying the
+ // image.
+ // - 'path': the $file->uri of the image to display.
+ //
+ // When given a style and an image path the function will first determine
+ // if a derivative image already exists, in which case the existing image
+ // will be displayed. If the derivative image does not already exist the
+ // function returns an tag with a specially crafted callback URL
+ // as the src attribute for the tag. When accessed, the callback URL will
+ // generate the derivative image and serve it to the browser.
+ $output = theme('image_style',
+ array(
+ 'style_name' => $style,
+ 'path' => $image->uri,
+ 'getsize' => FALSE,
+ )
+ );
+ $output .= '
' . t('This image is being displayed using the image style %style_name.', array('%style_name' => $style)) . '
';
+ return $output;
+}
diff --git a/sites/all/modules/contrib/dev/examples/image_example/image_example.test b/sites/all/modules/contrib/dev/examples/image_example/image_example.test
new file mode 100644
index 00000000..92561515
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/image_example/image_example.test
@@ -0,0 +1,111 @@
+ 'Image example functionality',
+ 'description' => 'Test functionality of the Image Example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('image_example');
+ // Create user with permission to administer image styles.
+ $this->webUser = $this->drupalCreateUser(array('administer image styles', 'administer blocks'));
+ }
+
+ /**
+ * Test implementations of image API hooks.
+ */
+ public function testImageExample() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Verify that the default style added by
+ // image_example_image_default_styles() is in the list of image styles.
+ $image_styles = image_styles();
+ $this->assertTrue(isset($image_styles['image_example_style']), 'The default style image_example_style is in the list of image styles.');
+
+ // Verify that the effect added to the default 'thumbnail' style by
+ // image_example_image_styles_alter() is present.
+ $this->assertTrue((isset($image_styles['thumbnail']['effects'][1]['name']) && $image_styles['thumbnail']['effects'][1]['name'] == 'image_example_colorize'), 'Effect added to the thumbnail style via hook_image_styles_alter() is present.');
+
+ // Create a new image style and add the effect provided by
+ // image_example_effect_info().
+ $new_style = array('name' => drupal_strtolower($this->randomName()));
+ $new_style = image_style_save($new_style);
+ $this->assertTrue(isset($new_style['isid']), format_string('Image style @style_name created.', array('@style_name' => $new_style['name'])));
+
+ $edit = array(
+ 'new' => 'image_example_colorize',
+ );
+ $this->drupalPost('admin/config/media/image-styles/edit/' . $new_style['name'], $edit, t('Add'));
+
+ // Verify the 'color' field provided by image_example_colorize_form()
+ // appears on the effect configuration page. And that we can fill it out.
+ $this->assertField('data[color]', 'Color field provided by image_example_effect_colorize_form is present on effect configuration page.');
+ $edit = array(
+ 'data[color]' => '#000000',
+ );
+ $this->drupalPost(NULL, $edit, t('Add effect'));
+ $this->assertText(t('The image effect was successfully applied.'), format_string('Colorize effect added to @style_name.', array('@style_name' => $new_style['name'])));
+
+ // Set the variable 'image_example_style_name' to the name of our new style
+ // then rename the style and ensure the variable name is changed.
+ // @todo Enable this block once http://drupal.org/node/713872 is fixed.
+ if (defined('bug_713872_fixed')) {
+ $style = image_style_load($new_style['name']);
+ variable_set('image_example_style_name', $style['name']);
+ $style['name'] = drupal_strtolower($this->randomName());
+ $style = image_style_save($style);
+ $variable = variable_get('image_example_style_name', '');
+ $this->assertTrue(($variable == $style['name']), 'Variable image_example_style_name successfully updated when renaming image style.');
+ }
+ }
+
+ /**
+ * Tests for image block provided by module.
+ */
+ public function testImageExamplePage() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+ $this->drupalCreateNode(array('promote' => 1));
+
+ // Upload an image to the image page.
+ $images = $this->drupalGetTestFiles('image');
+ $edit = array(
+ 'files[image_example_image_fid]' => drupal_realpath($images[0]->uri),
+ 'image_example_style_name' => 'image_example_style',
+ );
+ $this->drupalPost('image_example/styles', $edit, t('Save'));
+ $this->assertText(t('The image @image_name was uploaded', array('@image_name' => $images[0]->filename)), 'Image uploaded to image block.');
+
+ // Verify the image is displayed.
+ $this->drupalGet('image_example/styles');
+ $fid = variable_get('image_example_image_fid', FALSE);
+ $image = isset($fid) ? file_load($fid) : NULL;
+ $this->assertRaw(file_uri_target($image->uri), 'Image is displayed');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/js_example/accordion.tpl.php b/sites/all/modules/contrib/dev/examples/js_example/accordion.tpl.php
new file mode 100644
index 00000000..8ac70832
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/accordion.tpl.php
@@ -0,0 +1,59 @@
+
+
+ Mauris mauris ante, blandit et, ultrices a, suscipit eget, quam. Integer
+ ut neque. Vivamus nisi metus, molestie vel, gravida in, condimentum sit
+ amet, nunc. Nam a nibh. Donec suscipit eros. Nam mi. Proin viverra leo ut
+ odio. Curabitur malesuada. Vestibulum a velit eu ante scelerisque vulputate.
+
+ Sed non urna. Donec et ante. Phasellus eu ligula. Vestibulum sit amet
+ purus. Vivamus hendrerit, dolor at aliquet laoreet, mauris turpis porttitor
+ velit, faucibus interdum tellus libero ac justo. Vivamus non quam. In
+ suscipit faucibus urna.
+
+ Nam enim risus, molestie et, porta ac, aliquam ac, risus. Quisque lobortis.
+ Phasellus pellentesque purus in massa. Aenean in pede. Phasellus ac libero
+ ac tellus pellentesque semper. Sed ac felis. Sed commodo, magna quis
+ lacinia ornare, quam ante aliquam nisi, eu iaculis leo purus venenatis dui.
+
+ Cras dictum. Pellentesque habitant morbi tristique senectus et netus
+ et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in
+ faucibus orci luctus et ultrices posuere cubilia Curae; Aenean lacinia
+ mauris vel est.
+
+
+ Suspendisse eu nisl. Nullam ut libero. Integer dignissim consequat lectus.
+ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
+ inceptos himenaeos.
+
+
+
+
+
diff --git a/sites/all/modules/contrib/dev/examples/js_example/css/jsweights.css b/sites/all/modules/contrib/dev/examples/js_example/css/jsweights.css
new file mode 100644
index 00000000..e2d58c0f
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/css/jsweights.css
@@ -0,0 +1,5 @@
+
+div#js-weights div {
+ font-size: 20px;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js/black.js b/sites/all/modules/contrib/dev/examples/js_example/js/black.js
new file mode 100644
index 00000000..c1daf7d2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js/black.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsBlack = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.black;
+ var newDiv = $('').css('color', 'black').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js/blue.js b/sites/all/modules/contrib/dev/examples/js_example/js/blue.js
new file mode 100644
index 00000000..e69c188a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js/blue.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsBlue = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.blue;
+ var newDiv = $('').css('color', 'blue').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js/brown.js b/sites/all/modules/contrib/dev/examples/js_example/js/brown.js
new file mode 100644
index 00000000..3e3fbc31
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js/brown.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsBrown = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.brown;
+ var newDiv = $('').css('color', 'brown').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js/green.js b/sites/all/modules/contrib/dev/examples/js_example/js/green.js
new file mode 100644
index 00000000..f6b1f323
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js/green.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsGreen = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.green;
+ var newDiv = $('').css('color', 'green').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js/purple.js b/sites/all/modules/contrib/dev/examples/js_example/js/purple.js
new file mode 100644
index 00000000..20c48f58
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js/purple.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsPurple = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.purple;
+ var newDiv = $('').css('color', 'purple').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js/red.js b/sites/all/modules/contrib/dev/examples/js_example/js/red.js
new file mode 100644
index 00000000..b26870b1
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js/red.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsRed = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.red;
+ var newDiv = $('').css('color', 'red').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js_example.info b/sites/all/modules/contrib/dev/examples/js_example/js_example.info
new file mode 100644
index 00000000..a6596b6e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js_example.info
@@ -0,0 +1,12 @@
+name = JS Example
+description = An example module showing how to use some of the new JavaScript features in Drupal 7
+package = Example modules
+core = 7.x
+files[] = js_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js_example.module b/sites/all/modules/contrib/dev/examples/js_example/js_example.module
new file mode 100644
index 00000000..6f23a1e8
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js_example.module
@@ -0,0 +1,122 @@
+ array(
+ 'template' => 'accordion',
+ 'variables' => array('title' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function js_example_menu() {
+ $items = array();
+ $items['js_example/weights'] = array(
+ 'title' => 'JS Example: see weighting in action',
+ 'page callback' => 'js_example_js_weights',
+ 'access callback' => TRUE,
+ );
+ $items['js_example/accordion'] = array(
+ 'title' => 'JS Example: jQuery UI accordion',
+ 'page callback' => 'js_example_accordion',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Weights demonstration.
+ *
+ * Here we demonstrate attaching a number of scripts to the render array.
+ * These scripts generate content according to 'weight' and color.
+ *
+ * On the Drupal side, we do three main things:
+ * - Create a container DIV, with an ID all the scripts can recognize.
+ * - Attach some scripts which generate color-coded content. We use the
+ * 'weight' attribute to set the order in which the scripts are included.
+ * - Add the color->weight array to the settings variable in each *color*.js
+ * file. This is where Drupal passes data out to JavaScript.
+ *
+ * Each of the color scripts (red.js, blue.js, etc) uses jQuery to find our
+ * DIV, and then add some content to it. The order in which the color scripts
+ * execute will end up being the order of the content.
+ *
+ * The 'weight' form atttribute determines the order in which a script is
+ * output to the page. To see this in action:
+ * - Uncheck the 'Aggregate Javascript files' setting at:
+ * admin/config/development/performance.
+ * - Load the page: js_example/weights. Examine the page source.
+ * You will see that the color js scripts have been added in the
+ * element in weight order.
+ *
+ * To test further, change a weight in the $weights array below, then save
+ * this file and reload js_example/weights. Examine the new source to see the
+ * reordering.
+ *
+ * @return array
+ * A renderable array.
+ */
+function js_example_js_weights() {
+ // Add some css to show which line is output by which script.
+ drupal_add_css(drupal_get_path('module', 'js_example') . '/css/jsweights.css');
+ // Create an array of items with random-ish weight values.
+ $weights = array(
+ 'red' => 100,
+ 'blue' => 23,
+ 'green' => 3,
+ 'brown' => 45,
+ 'black' => 5,
+ 'purple' => 60,
+ );
+ // Attach the weights array to our JavaScript settings. This allows the
+ // color scripts to discover their weight values, by accessing
+ // settings.jsWeights.*color*. The color scripts only use this information for
+ // display to the user.
+ drupal_add_js(array('jsWeights' => $weights), array('type' => 'setting'));
+ // Add our individual scripts. We add them in an arbitrary order, but the
+ // 'weight' attribute will cause Drupal to render (and thus load and execute)
+ // them in the weighted order.
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/red.js', array('weight' => $weights['red']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/blue.js', array('weight' => $weights['blue']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/green.js', array('weight' => $weights['green']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/brown.js', array('weight' => $weights['brown']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/black.js', array('weight' => $weights['black']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/purple.js', array('weight' => $weights['purple']));
+ // Main container DIV. We give it a unique ID so that the JavaScript can
+ // find it using jQuery.
+ $output = '';
+ return $output;
+}
+
+/**
+ * Demonstrate accordion effect.
+ */
+function js_example_accordion() {
+ $title = t('Click sections to expand or collapse:');
+ $build['myelement'] = array(
+ '#theme' => 'my_accordion',
+ '#title' => $title,
+ );
+ $build['myelement']['#attached']['library'][] = array('system', 'ui.accordion');
+ $build['myelement']['#attached']['js'][] = array('data' => '(function($){$(function() { $("#accordion").accordion(); })})(jQuery);', 'type' => 'inline');
+ $output = drupal_render($build);
+ return $output;
+}
diff --git a/sites/all/modules/contrib/dev/examples/js_example/js_example.test b/sites/all/modules/contrib/dev/examples/js_example/js_example.test
new file mode 100644
index 00000000..d4697315
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/js_example/js_example.test
@@ -0,0 +1,46 @@
+ 'JavaScript Example',
+ 'description' => 'Functional tests for the JavaScript Example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('js_example');
+ }
+
+ /**
+ * Tests the menu paths defined in js_example module.
+ */
+ public function testJsExampleMenus() {
+ $paths = array(
+ 'js_example/weights',
+ 'js_example/accordion',
+ );
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertResponse(200, '200 response for path: ' . $path);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/menu_example/menu_example.info b/sites/all/modules/contrib/dev/examples/menu_example/menu_example.info
new file mode 100644
index 00000000..99290522
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/menu_example/menu_example.info
@@ -0,0 +1,12 @@
+name = Menu example
+description = An example of advanced uses of the menu APIs.
+package = Example modules
+core = 7.x
+files[] = menu_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/menu_example/menu_example.module b/sites/all/modules/contrib/dev/examples/menu_example/menu_example.module
new file mode 100644
index 00000000..6b60b4e2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/menu_example/menu_example.module
@@ -0,0 +1,546 @@
+ MENU_NORMAL_ITEM,
+ //
+ // The menu title. Do NOT use t() which is called by default. You can
+ // override the use of t() by defining a 'title callback'. This is explained
+ // in the 'menu_example/title_callbacks' example below.
+ 'title' => 'Menu Example',
+
+ // Description (hover flyover for menu link). Does NOT use t(), which is
+ // called automatically.
+ 'description' => 'Simplest possible menu type, and the parent menu entry for others',
+
+ // Function to be called when this path is accessed.
+ 'page callback' => '_menu_example_basic_instructions',
+
+ // Arguments to the page callback. Here's we'll use them just to provide
+ // content for our page.
+ 'page arguments' => array(t('This page is displayed by the simplest (and base) menu example. Note that the title of the page is the same as the link title. You can also visit a similar page with no menu link. Also, note that there is a hook_menu_alter() example that has changed the path of one of the menu items.', array('!link' => url('examples/menu_example/path_only')))),
+
+ // If the page is meant to be accessible to all users, you can set 'access
+ // callback' to TRUE. This bypasses all access checks. For an explanation on
+ // how to use the permissions system to restrict access for certain users,
+ // see the example 'examples/menu_example/permissioned/controlled' below.
+ 'access callback' => TRUE,
+
+ // If the page callback is located in another file, specify it here and
+ // that file will be automatically loaded when needed.
+ // 'file' => 'menu_example.module',
+ //
+ // We can choose which menu gets the link. The default is 'navigation'.
+ // 'menu_name' => 'navigation',
+ //
+ // Show the menu link as expanded.
+ 'expanded' => TRUE,
+ );
+
+ // Show a menu link in a menu other than the default "Navigation" menu.
+ // The menu must already exist.
+ $items['examples/menu_example_alternate_menu'] = array(
+ 'title' => 'Menu Example: Menu in alternate menu',
+
+ // Machine name of the menu in which the link should appear.
+ 'menu_name' => 'main-menu',
+
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This will be in the Main menu instead of the default Navigation menu')),
+ 'access callback' => TRUE,
+ );
+
+ // A menu entry with simple permissions using user_access().
+ //
+ // First, provide a courtesy menu item that mentions the existence of the
+ // permissioned item.
+ $items['examples/menu_example/permissioned'] = array(
+ 'title' => 'Permissioned Example',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('A menu item that requires the "access protected menu example" permission is at examples/menu_example/permissioned/controlled', array('!link' => url('examples/menu_example/permissioned/controlled')))),
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+
+ // Now provide the actual permissioned menu item.
+ $items['examples/menu_example/permissioned/controlled'] = array(
+
+ // The title - do NOT use t() as t() is called automatically.
+ 'title' => 'Permissioned Menu Item',
+ 'description' => 'This menu entry will not appear and the page will not be accessible without the "access protected menu example" permission.',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This menu entry will not show and the page will not be accessible without the "access protected menu example" permission.')),
+
+ // For a permissioned menu entry, we provide an access callback which
+ // determines whether the current user should have access. The default is
+ // user_access(), which we'll use in this case. Since it's the default,
+ // we don't even have to enter it.
+ // 'access callback' => 'user_access',
+ //
+ // The 'access arguments' are passed to the 'access callback' to help it
+ // do its job. In the case of user_access(), we need to pass a permission
+ // as the first argument.
+ 'access arguments' => array('access protected menu example'),
+
+ // The optional weight element tells how to order the submenu items.
+ // Higher weights are "heavier", dropping to the bottom of the menu.
+ 'weight' => 10,
+ );
+
+ /*
+ * We will define our own "access callback" function. We'll use
+ * menu_example_custom_access() rather than the default user_access().
+ *
+ * The function takes a "role" of the user as an argument.
+ */
+ $items['examples/menu_example/custom_access'] = array(
+ 'title' => 'Custom Access Example',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('A menu item that requires the user to posess a role of "authenticated user" is at examples/menu_example/custom_access/page', array('!link' => url('examples/menu_example/custom_access/page')))),
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ 'weight' => -5,
+ );
+
+ $items['examples/menu_example/custom_access/page'] = array(
+ 'title' => 'Custom Access Menu Item',
+ 'description' => 'This menu entry will not show and the page will not be accessible without the user being an "authenticated user".',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This menu entry will not be visible and access will result in a 403 error unless the user has the "authenticated user" role. This is accomplished with a custom access callback.')),
+ 'access callback' => 'menu_example_custom_access',
+ 'access arguments' => array('authenticated user'),
+ );
+
+ // A menu router entry with no menu link. This could be used any time we
+ // don't want the user to see a link in the menu. Otherwise, it's the same
+ // as the "simplest" entry above. MENU_CALLBACK is used for all menu items
+ // which don't need a visible menu link, including services and other pages
+ // that may be linked to but are not intended to be accessed directly.
+ //
+ // First, provide a courtesy link in the menu so people can find this.
+ $items['examples/menu_example/path_only'] = array(
+ 'title' => 'MENU_CALLBACK example',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('A menu entry with no menu link (MENU_CALLBACK) is at !link', array('!link' => url('examples/menu_example/path_only/callback')))),
+ 'access callback' => TRUE,
+ 'weight' => 20,
+ );
+ $items['examples/menu_example/path_only/callback'] = array(
+
+ // A type of MENU_CALLBACK means leave the path completely out of the menu
+ // links.
+ 'type' => MENU_CALLBACK,
+
+ // The title is still used for the page title, even though it's not used
+ // for the menu link text, since there's no menu link.
+ 'title' => 'Callback Only',
+
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('The menu entry for this page is of type MENU_CALLBACK, so it provides only a path but not a link in the menu links, but it is the same in every other way to the simplest example.')),
+ 'access callback' => TRUE,
+ );
+
+ // A menu entry with tabs.
+ // For tabs we need at least 3 things:
+ // 1) A parent MENU_NORMAL_ITEM menu item (examples/menu_example/tabs in this
+ // example.)
+ // 2) A primary tab (the one that is active when we land on the base menu).
+ // This tab is of type MENU_DEFAULT_LOCAL_TASK.
+ // 3) Some other menu entries for the other tabs, of type MENU_LOCAL_TASK.
+ $items['examples/menu_example/tabs'] = array(
+ // 'type' => MENU_NORMAL_ITEM, // Not necessary since this is the default.
+ 'title' => 'Tabs',
+ 'description' => 'Shows how to create primary and secondary tabs',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This is the "tabs" menu entry.')),
+ 'access callback' => TRUE,
+ 'weight' => 30,
+ );
+
+ // For the default local task, we need very little configuration, as the
+ // callback and other conditions are handled by the parent callback.
+ $items['examples/menu_example/tabs/default'] = array(
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'title' => 'Default primary tab',
+ 'weight' => 1,
+ );
+ // Now add the rest of the tab entries.
+ foreach (array(t('second') => 2, t('third') => 3, t('fourth') => 4) as $tabname => $weight) {
+ $items["examples/menu_example/tabs/$tabname"] = array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => $tabname,
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This is the tab "@tabname" in the "basic tabs" example', array('@tabname' => $tabname))),
+ 'access callback' => TRUE,
+
+ // The weight property overrides the default alphabetic ordering of menu
+ // entries, allowing us to get our tabs in the order we want.
+ 'weight' => $weight,
+ );
+ }
+
+ // Finally, we'll add secondary tabs to the default tab of the tabs entry.
+ //
+ // The default local task needs very little information.
+ $items['examples/menu_example/tabs/default/first'] = array(
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'title' => 'Default secondary tab',
+ // The additional page callback and related items are handled by the
+ // parent menu item.
+ );
+ foreach (array(t('second'), t('third')) as $tabname) {
+ $items["examples/menu_example/tabs/default/$tabname"] = array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => $tabname,
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This is the secondary tab "@tabname" in the "basic tabs" example "default" tab', array('@tabname' => $tabname))),
+ 'access callback' => TRUE,
+ );
+ }
+
+ // All the portions of the URL after the base menu are passed to the page
+ // callback as separate arguments, and can be captured by the page callback
+ // in its argument list. Our _menu_example_menu_page() function captures
+ // arguments in its function signature and can output them.
+ $items['examples/menu_example/use_url_arguments'] = array(
+ 'title' => 'Extra Arguments',
+ 'description' => 'The page callback can use the arguments provided after the path used as key',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This page demonstrates using arguments in the path (portions of the path after "menu_example/url_arguments". For example, access it with !link1 or !link2).', array('!link1' => url('examples/menu_example/use_url_arguments/one/two'), '!link2' => url('examples/menu_example/use_url_arguments/firstarg/secondarg')))),
+ 'access callback' => TRUE,
+ 'weight' => 40,
+ );
+
+ // The menu title can be dynamically created by using the 'title callback'
+ // which by default is t(). Here we provide a title callback which adjusts
+ // the menu title based on the current user's username.
+ $items['examples/menu_example/title_callbacks'] = array(
+ 'title callback' => '_menu_example_simple_title_callback',
+ 'title arguments' => array(t('Dynamic title: username=')),
+ 'description' => 'The title of this menu item is dynamically generated',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('The menu title is dynamically changed by the title callback')),
+ 'access callback' => TRUE,
+ 'weight' => 50,
+ );
+
+ // Sometimes we need to capture a specific argument within the menu path,
+ // as with the menu entry
+ // 'example/menu_example/placeholder_argument/3333/display', where we need to
+ // capture the "3333". In that case, we use a placeholder in the path provided
+ // in the menu entry. The (odd) way this is done is by using
+ // array(numeric_position_value) as the value for 'page arguments'. The
+ // numeric_position_value is the zero-based index of the portion of the URL
+ // which should be passed to the 'page callback'.
+ //
+ // First we provide a courtesy link with information on how to access
+ // an item with a placeholder.
+ $items['examples/menu_example/placeholder_argument'] = array(
+ 'title' => 'Placeholder Arguments',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('Demonstrate placeholders by visiting examples/menu_example/placeholder_argument/3343/display', array('!link' => url('examples/menu_example/placeholder_argument/3343/display')))),
+ 'access callback' => TRUE,
+ 'weight' => 60,
+ );
+
+ // Now the actual entry.
+ $items['examples/menu_example/placeholder_argument/%/display'] = array(
+ 'title' => 'Placeholder Arguments',
+ 'page callback' => '_menu_example_menu_page',
+
+ // Pass the value of '%', which is zero-based argument 3, to the
+ // 'page callback'. So if the URL is
+ // 'examples/menu_example/placeholder_argument/333/display' then the value
+ // 333 will be passed into the 'page callback'.
+ 'page arguments' => array(3),
+ 'access callback' => TRUE,
+ );
+
+ // Drupal provides magic placeholder processing as well, so if the placeholder
+ // is '%menu_example_arg_optional', the function
+ // menu_example_arg_optional_load($arg) will be called to translate the path
+ // argument to a more substantial object. $arg will be the value of the
+ // placeholder. Then the return value of menu_example_id_load($arg) will be
+ // passed to the 'page callback'.
+ // In addition, if (in this case) menu_example_arg_optional_to_arg() exists,
+ // then a menu link can be created using the results of that function as a
+ // default for %menu_example_arg_optional.
+ $items['examples/menu_example/default_arg/%menu_example_arg_optional'] = array(
+ 'title' => 'Processed Placeholder Arguments',
+ 'page callback' => '_menu_example_menu_page',
+ // Argument 3 (4rd arg) is the one we want.
+ 'page arguments' => array(3),
+ 'access callback' => TRUE,
+ 'weight' => 70,
+ );
+
+ $items['examples/menu_example/menu_original_path'] = array(
+ 'title' => 'Menu path that will be altered by hook_menu_alter()',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This menu item was created strictly to allow the hook_menu_alter() function to have something to operate on. hook_menu defined the path as examples/menu_example/menu_original_path. The hook_menu_alter() changes it to examples/menu_example/menu_altered_path. You can try navigating to both paths and see what happens!')),
+ 'access callback' => TRUE,
+ 'weight' => 80,
+ );
+ return $items;
+}
+
+/**
+ * Page callback for the simplest introduction menu entry.
+ *
+ * @param string $content
+ * Some content passed in.
+ */
+function _menu_example_basic_instructions($content = NULL) {
+ $base_content = t(
+ 'This is the base page of the Menu Example. There are a number of examples
+ here, from the most basic (like this one) to extravagant mappings of loaded
+ placeholder arguments. Enjoy!');
+ return '
' . $base_content . '
' . $content . '
';
+}
+
+/**
+ * Page callback for use with most of the menu entries.
+ *
+ * The arguments it receives determine what it outputs.
+ *
+ * @param string $content
+ * The base content to output.
+ * @param string $arg1
+ * First additional argument from the path used to access the menu
+ * @param string $arg2
+ * Second additional argument.
+ */
+function _menu_example_menu_page($content = NULL, $arg1 = NULL, $arg2 = NULL) {
+ $output = '
';
+ }
+ return $output;
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * Provides a demonstration access string.
+ */
+function menu_example_permission() {
+ return array(
+ 'access protected menu example' => array(
+ 'title' => t('Access the protected menu example'),
+ ),
+ );
+
+}
+
+/**
+ * Determine whether the current user has the role specified.
+ *
+ * @param string $role_name
+ * The role required for access
+ *
+ * @return bool
+ * True if the acting user has the role specified.
+ */
+function menu_example_custom_access($role_name) {
+ $access_granted = in_array($role_name, $GLOBALS['user']->roles);
+ return $access_granted;
+}
+
+/**
+ * Utility function to provide mappings from integers to some strings.
+ *
+ * This would normally be some database lookup to get an object or array from
+ * a key.
+ *
+ * @param int $id
+ * The integer key.
+ *
+ * @return string
+ * The string to which the integer key mapped, or NULL if it did not map.
+ */
+function _menu_example_mappings($id) {
+ $mapped_value = NULL;
+ static $mappings = array(
+ 1 => 'one',
+ 2 => 'two',
+ 3 => 'three',
+ 99 => 'jackpot! default',
+ );
+ if (isset($mappings[$id])) {
+ $mapped_value = $mappings[$id];
+ }
+ return $mapped_value;
+}
+
+/**
+ * The special _load function to load menu_example.
+ *
+ * Given an integer $id, load the string that should be associated with it.
+ * Normally this load function would return an array or object with more
+ * information.
+ *
+ * @param int $id
+ * The integer to load.
+ *
+ * @return string
+ * A string loaded from the integer.
+ */
+function menu_example_id_load($id) {
+ // Just map a magic value here. Normally this would load some more complex
+ // object from the database or other context.
+ $mapped_value = _menu_example_mappings($id);
+ if (!empty($mapped_value)) {
+ return t('Loaded value was %loaded', array('%loaded' => $mapped_value));
+ }
+ else {
+ return t('Sorry, the id %id was not found to be loaded', array('%id' => $id));
+ }
+}
+
+/**
+ * Implements hook_menu_alter().
+ *
+ * Changes the path 'examples/menu_example/menu_original_path' to
+ * 'examples/menu_example/menu_altered_path'.
+ * Changes the title callback of the 'user/UID' menu item.
+ *
+ * Change the path 'examples/menu_example/menu_original_path' to
+ * 'examples/menu_example/menu_altered_path'. This change will prevent the
+ * page from appearing at the original path (since the item is being unset).
+ * You will need to go to examples/menu_example/menu_altered_path manually to
+ * see the page.
+ *
+ * Remember that hook_menu_alter() only runs at menu_rebuild() time, not every
+ * time the page is built, so this typically happens only at cache clear time.
+ *
+ * The $items argument is the complete list of menu router items ready to be
+ * written to the menu_router table.
+ */
+function menu_example_menu_alter(&$items) {
+ if (!empty($items['examples/menu_example/menu_original_path'])) {
+ $items['examples/menu_example/menu_altered_path'] = $items['examples/menu_example/menu_original_path'];
+ $items['examples/menu_example/menu_altered_path']['title'] = 'Menu item altered by hook_menu_alter()';
+ unset($items['examples/menu_example/menu_original_path']);
+ }
+
+ // Here we will change the title callback to our own function, changing the
+ // 'user' link from the traditional to always being "username's account".
+ if (!empty($items['user/%user'])) {
+ $items['user/%user']['title callback'] = 'menu_example_user_page_title';
+ }
+}
+
+/**
+ * Title callback to rewrite the '/user' menu link.
+ *
+ * @param string $base_string
+ * string to be prepended to current user's name.
+ */
+function _menu_example_simple_title_callback($base_string) {
+ global $user;
+ $username = !empty($user->name) ? $user->name : t('anonymous');
+ return $base_string . ' ' . $username;
+}
+
+/**
+ * Title callback to rename the title dynamically, based on user_page_title().
+ *
+ * @param object $account
+ * User account related to the visited page.
+ */
+function menu_example_user_page_title($account) {
+ return is_object($account) ? t("@name's account", array('@name' => format_username($account))) : '';
+}
+
+/**
+ * Implements hook_menu_link_alter().
+ *
+ * This code will get the chance to alter a menu link when it is being saved
+ * in the menu interface at admin/build/menu. Whatever we do here overrides
+ * anything the user/administrator might have been trying to do.
+ */
+function menu_example_menu_link_alter(&$item, $menu) {
+ // Force the link title to remain 'Clear Cache' no matter what the admin
+ // does with the web interface.
+ if ($item['link_path'] == 'devel/cache/clear') {
+ $item['link_title'] = 'Clear Cache';
+ };
+}
+
+/**
+ * Loads an item based on its $id.
+ *
+ * In this case we're just creating a more extensive string. In a real example
+ * we would load or create some type of object.
+ *
+ * @param int $id
+ * Id of the item.
+ */
+function menu_example_arg_optional_load($id) {
+ $mapped_value = _menu_example_mappings($id);
+ if (!empty($mapped_value)) {
+ return t('Loaded value was %loaded', array('%loaded' => $mapped_value));
+ }
+ else {
+ return t('Sorry, the id %id was not found to be loaded', array('%id' => $id));
+ }
+}
+
+/**
+ * Utility function to provide default argument for wildcard.
+ *
+ * A to_arg() function is used to provide a default for the arg in the
+ * wildcard. The purpose is to provide a menu link that will function if no
+ * argument is given. For example, in the case of the menu item
+ * 'examples/menu_example/default_arg/%menu_example_arg_optional' the third argument
+ * is required, and the menu system cannot make a menu link using this path
+ * since it contains a placeholder. However, when the to_arg() function is
+ * provided, the menu system will create a menu link pointing to the path
+ * which would be created with the to_arg() function filling in the
+ * %menu_example_arg_optional.
+ *
+ * @param string $arg
+ * The arg (URL fragment) to be tested.
+ */
+function menu_example_arg_optional_to_arg($arg) {
+ // If our argument is not provided, give a default of 99.
+ return (empty($arg) || $arg == '%') ? 99 : $arg;
+}
+/**
+ * @} End of "defgroup menu_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/menu_example/menu_example.test b/sites/all/modules/contrib/dev/examples/menu_example/menu_example.test
new file mode 100644
index 00000000..fd511abe
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/menu_example/menu_example.test
@@ -0,0 +1,116 @@
+ 'Menu example functionality',
+ 'description' => 'Checks behavior of Menu Example.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('menu_example');
+ }
+
+ /**
+ * Test the various menus.
+ */
+ public function testMenuExample() {
+ $this->drupalGet('');
+ $this->assertText(t('Menu Example: Menu in alternate menu'));
+ $this->clickLink(t('Menu Example'));
+ $this->assertText(t('This is the base page of the Menu Example'));
+
+ $this->drupalGet('examples/menu_example_alternate_menu');
+ $this->assertResponse(200);
+
+ $this->clickLink(t('Custom Access Example'));
+ $this->assertText(t('Custom Access Example'));
+
+ $this->clickLink(t('examples/menu_example/custom_access/page'));
+ $this->assertResponse(403);
+
+ $this->drupalGet('examples/menu_example/permissioned');
+ $this->assertText(t('Permissioned Example'));
+
+ $this->clickLink('examples/menu_example/permissioned/controlled');
+ $this->assertResponse(403);
+
+ $this->drupalGet('examples/menu_example');
+
+ $this->clickLink(t('MENU_CALLBACK example'));
+
+ $this->drupalGet('examples/menu_example/path_only/callback');
+ $this->assertText(t('The menu entry for this page is of type MENU_CALLBACK'));
+
+ $this->clickLink(t('Tabs'));
+ $this->assertText(t('This is the "tabs" menu entry'));
+
+ $this->drupalGet('examples/menu_example/tabs/second');
+ $this->assertText(t('This is the tab "second" in the "basic tabs" example'));
+
+ $this->clickLink(t('third'));
+ $this->assertText(t('This is the tab "third" in the "basic tabs" example'));
+
+ $this->clickLink(t('Extra Arguments'));
+
+ $this->drupalGet('examples/menu_example/use_url_arguments/one/two');
+ $this->assertText(t('Argument 1=one'));
+
+ $this->clickLink(t('Placeholder Arguments'));
+
+ $this->clickLink(t('examples/menu_example/placeholder_argument/3343/display'));
+ $this->assertRaw('
3343
');
+
+ $this->clickLink(t('Processed Placeholder Arguments'));
+ $this->assertText(t('Loaded value was jackpot! default'));
+
+ // Create a user with permissions to access protected menu entry.
+ $web_user = $this->drupalCreateUser(array('access protected menu example'));
+
+ // Use custom overridden drupalLogin function to verify the user is logged
+ // in.
+ $this->drupalLogin($web_user);
+
+ // Check that our title callback changing /user dynamically is working.
+ // Using ' because of the format_username function.
+ $this->assertRaw(t("@name's account", array('@name' => format_username($web_user))), format_string('Title successfully changed to account name: %name.', array('%name' => $web_user->name)));
+
+ // Now start testing other menu entries.
+ $this->drupalGet('examples/menu_example');
+
+ $this->clickLink(t('Custom Access Example'));
+ $this->assertText(t('Custom Access Example'));
+
+ $this->drupalGet('examples/menu_example/custom_access/page');
+ $this->assertResponse(200);
+
+ $this->drupalGet('examples/menu_example/permissioned');
+ $this->assertText('Permissioned Example');
+ $this->clickLink('examples/menu_example/permissioned/controlled');
+ $this->assertText('This menu entry will not show');
+
+ $this->drupalGet('examples/menu_example/menu_altered_path');
+ $this->assertText('This menu item was created strictly to allow the hook_menu_alter()');
+
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.info b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.info
new file mode 100644
index 00000000..50cceb61
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.info
@@ -0,0 +1,12 @@
+name = Node access example
+description = Demonstrates how a module can use Drupal's node access system
+package = Example modules
+core = 7.x
+files[] = node_access_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.install b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.install
new file mode 100644
index 00000000..2a25dbb8
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.install
@@ -0,0 +1,31 @@
+ 'Example table for node_access_example module',
+ 'fields' => array(
+ 'nid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'private' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('nid'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.module b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.module
new file mode 100644
index 00000000..4de28878
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.module
@@ -0,0 +1,482 @@
+ 'Node Access Example',
+ 'page callback' => 'node_access_example_private_node_listing',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Our hook_menu() page callback function.
+ *
+ * Information for the user about what nodes are marked private on the system
+ * and which of those the user has access to.
+ *
+ * The queries showing what is accessible to the current user demonstrate the
+ * use of the 'node_access' tag to make sure that we don't show inappropriate
+ * information to unprivileged users.
+ *
+ * @return string
+ * Page content.
+ *
+ * @see page_example
+ */
+function node_access_example_private_node_listing() {
+ $content = '
' . t('This example shows how a module can use the Drupal node access system to allow access to specific nodes. You will need to look at the code and then experiment with it by creating nodes, marking them private, and accessing them as various users.') . '
';
+
+ // Find out how many nodes are marked private.
+ $query = db_select('node', 'n');
+ $query->addExpression('COUNT(n.nid)', 'private_count');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $num_private = $query
+ ->condition('nae.private', 1)->execute()->fetchField();
+
+ // Find out how many nodes owned by this user are marked private.
+ $query = db_select('node', 'n');
+ $query->addExpression('COUNT(n.nid)', 'private_count');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $num_personal = $query
+ ->condition('n.uid', $GLOBALS['user']->uid)
+ ->condition('nae.private', 1)
+ ->execute()->fetchfield();
+
+ $content .= '
' . t('There are currently @num private nodes in the system @num_personal are yours.', array('@num' => $num_private, '@num_personal' => $num_personal)) . '
';
+
+ // Use a 'node_access' tag with a query to find out how many this user has
+ // access to. This will be the standard way to make lists while respecting
+ // node access restrictions.
+ $query = db_select('node', 'n');
+ $query->addExpression('COUNT(n.nid)', 'private_count');
+ $query->addTag('node_access');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $num_private_accessible = $query->condition('nae.private', 1)->execute()->fetchField();
+ $content .= '
' . t('You have access to @num private nodes.', array('@num' => $num_private_accessible)) . '
';
+
+ // Use the key 'node_access' tag to get the key data from the nodes this
+ // has access to.
+ $query = db_select('node', 'n', array('fetch' => PDO::FETCH_ASSOC));
+ $query->addTag('node_access');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $query->join('users', 'u', 'u.uid = n.uid');
+ $result = $query->fields('n', array('nid', 'title', 'uid'))
+ ->fields('u', array('name'))
+ ->condition('nae.private', 1)->execute();
+
+ $rows = array();
+ foreach ($result as $node) {
+ $node['nid'] = l($node['nid'], 'node/' . $node['nid']);
+ $rows[] = array('data' => $node, 'class' => array('accessible'));
+ }
+ $content .= '
';
+
+ return array('#markup' => $content);
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * We create two permissions, which we can use as a base for our grant/deny
+ * decision:
+ *
+ * - 'access any private content' allows global access to content marked
+ * private by other users.
+ * - 'edit any private content' allows global edit
+ * privileges, basically overriding the node access system.
+ *
+ * Note that the 'edit any * content' and 'delete any * content' permissions
+ * will allow edit or delete permissions to the holder, regardless of what
+ * this module does.
+ *
+ * @see hook_permissions()
+ */
+function node_access_example_permission() {
+ return array(
+ 'access any private content' => array(
+ 'title' => t('Access any private content'),
+ 'description' => t('May view posts of other users even though they are marked private.'),
+ ),
+ 'edit any private content' => array(
+ 'title' => t('Edit any private content'),
+ 'description' => t('May edit posts of other users even though they are marked private.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_node_access().
+ *
+ * Allows view and edit access to private nodes, when the account requesting
+ * access has the username 'foobar'.
+ *
+ * hook_node_access() was introduced in Drupal 7. We use it here to demonstrate
+ * allowing certain privileges to an arbitrary user.
+ *
+ * @see hook_node_access()
+ */
+function node_access_example_node_access($node, $op, $account) {
+ // If $node is a string, the node has not yet been created. We don't care
+ // about that case.
+ if (is_string($node)) {
+ return NODE_ACCESS_IGNORE;
+ }
+ if (($op == 'view' || $op == 'update') && (!empty($account->name) && $account->name == 'foobar') && !empty($node->private)) {
+ drupal_set_message(t('Access to node @nid allowed because requester name (@name) is specifically allowed', array('@name' => $node->name, '@uid' => $account->uid)));
+ return NODE_ACCESS_ALLOW;
+ }
+ return NODE_ACCESS_IGNORE;
+}
+
+/**
+ * Here we define a constant for our node access grant ID, for the
+ * node_access_example_view and node_access_example_edit realms. This ID could
+ * be any integer, but here we choose 23, because it is this author's favorite
+ * number.
+ */
+define('NODE_ACCESS_EXAMPLE_GRANT_ALL', 23);
+
+/**
+ * Implements hook_node_grants().
+ *
+ * Tell the node access system what grant IDs the user belongs to for each
+ * realm, based on the operation being performed.
+ *
+ * When the user tries to perform an operation on the node, Drupal calls
+ * hook_node_grants() to determine grant ID and realm for the user. Drupal
+ * looks up the grant ID and realm for the node, and compares them to the
+ * grant ID and realm provided here. If grant ID and realm match for both
+ * user and node, then the operation is allowed.
+ *
+ * Grant ID and realm are both determined per node, by your module in
+ * hook_node_access_records().
+ *
+ * In our example, we've created three access realms: One for authorship, and
+ * two that track with the permission system.
+ *
+ * We always add node_access_example_author to the list of grants, with a grant
+ * ID equal to their user ID. We do this because in our model, authorship
+ * always gives you permission to edit or delete your nodes, even if they're
+ * marked private.
+ *
+ * Then we compare the user's permissions to the operation to determine whether
+ * the user falls into the other two realms: node_access_example_view, and/or
+ * node_access_example_edit. If the user has the 'access any private content'
+ * permission we defined in hook_permission(), they're declared as belonging to
+ * the node_access_example_realm. Similarly, if they have the 'edit any private
+ * content' permission, we add the node_access_example_edit realm to the list
+ * of grants they have.
+ *
+ * @see node_access_example_permission()
+ * @see node_access_example_node_access_records()
+ */
+function node_access_example_node_grants($account, $op) {
+ $grants = array();
+ // First grant a grant to the author for own content.
+ // Do not grant to anonymous user else all anonymous users would be author.
+ if ($account->uid) {
+ $grants['node_access_example_author'] = array($account->uid);
+ }
+
+ // Then, if "access any private content" is allowed to the account,
+ // grant view, update, or delete as necessary.
+ if ($op == 'view' && user_access('access any private content', $account)) {
+ $grants['node_access_example_view'] = array(NODE_ACCESS_EXAMPLE_GRANT_ALL);
+ }
+
+ if (($op == 'update' || $op == 'delete') && user_access('edit any private content', $account)) {
+ $grants['node_access_example_edit'] = array(NODE_ACCESS_EXAMPLE_GRANT_ALL);
+ }
+
+ return $grants;
+}
+
+/**
+ * Implements hook_node_access_records().
+ *
+ * All node access modules must implement this hook. If the module is
+ * interested in the privacy of the node passed in, return a list
+ * of node access values for each grant ID we offer.
+ *
+ * In this example, for each node which is marked 'private,' we define
+ * three realms:
+ *
+ * The first and second are realms are 'node_access_example_view' and
+ * 'node_access_example_edit,' which have a single grant ID, 1. The
+ * user is either a member of these realms or not, depending upon the
+ * operation and the access permission set.
+ *
+ * The third is node_access_example_author. It gives the node
+ * author special privileges. node_access_example_author has one grant ID for
+ * every UID, and each user is automatically a member of the group where
+ * GID == UID. This has the effect of giving each user their own grant ID
+ * for nodes they authored, within this realm.
+ *
+ * Drupal calls this hook when a node is saved, or when access permissions
+ * change in order to rebuild the node access database table(s).
+ *
+ * The array you return will define the realm and the grant ID for the
+ * given node. This is stored in the {node_access} table for subsequent
+ * comparison against the user's realm and grant IDs, which you'll
+ * supply in hook_node_grants().
+ *
+ * Realm names and grant IDs are arbitrary. Official drupal naming
+ * conventions do not cover access realms, but since all realms are
+ * stored in the same database table, it's probably a good idea to
+ * use descriptive names which follow the module name, such as
+ * 'mymodule_realmname'.
+ *
+ * @see node_access_example_node_grants()
+ */
+function node_access_example_node_access_records($node) {
+ // We only care about the node if it's been marked private. If not, it is
+ // treated just like any other node and we completely ignore it.
+ if (!empty($node->private)) {
+ $grants = array();
+ $grants[] = array(
+ 'realm' => 'node_access_example_view',
+ 'gid' => NODE_ACCESS_EXAMPLE_GRANT_ALL,
+ 'grant_view' => 1,
+ 'grant_update' => 0,
+ 'grant_delete' => 0,
+ 'priority' => 0,
+ );
+ $grants[] = array(
+ 'realm' => 'node_access_example_edit',
+ 'gid' => NODE_ACCESS_EXAMPLE_GRANT_ALL,
+ 'grant_view' => 1,
+ 'grant_update' => 1,
+ 'grant_delete' => 1,
+ 'priority' => 0,
+ );
+
+ // For the node_access_example_author realm, the grant ID (gid) is
+ // equivalent to the node author's user ID (UID).
+ // We check the node UID so that we don't grant author privileges for
+ // anonymous nodes to anonymous users.
+ if ($node->uid) {
+ $grants[] = array(
+ 'realm' => 'node_access_example_author',
+ 'gid' => $node->uid,
+ 'grant_view' => 1,
+ 'grant_update' => 1,
+ 'grant_delete' => 1,
+ 'priority' => 0,
+ );
+ }
+ return $grants;
+ }
+ // Return nothing if the node has not been marked private.
+}
+
+/**
+ * Implements hook_form_alter().
+ *
+ * This module adds a simple checkbox to the node form labeled private. If the
+ * checkbox is checked, only the node author and users with
+ * 'access any private content' privileges may see it.
+ */
+function node_access_example_form_alter(&$form, $form_state) {
+ if (!empty($form['#node_edit_form'])) {
+ $form['node_access_example'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Node Access Example'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#weight' => 8,
+ );
+
+ $form['node_access_example']['private'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Private'),
+ '#description' => t('Check here if this content should be set private and only shown to privileged users.'),
+ '#default_value' => isset($form['#node']->private) ? $form['#node']->private : FALSE,
+ );
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * Gather and add the private setting for the nodes Drupal is loading.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_load($nodes, $types) {
+ $result = db_query('SELECT nid, private FROM {node_access_example} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)));
+ foreach ($result as $record) {
+ $nodes[$record->nid]->private = $record->private;
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ *
+ * Delete the node_access_example record when the node is deleted.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_delete($node) {
+ db_delete('node_access_example')->condition('nid', $node->nid)->execute();
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * Insert a new access record when a node is created.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_insert($node) {
+ if (isset($node->private)) {
+ db_insert('node_access_example')->fields(
+ array(
+ 'nid' => $node->nid,
+ 'private' => (int) $node->private,
+ )
+ )->execute();
+ }
+ drupal_set_message(t('New node @nid was created and private=@private', array('@nid' => $node->nid, '@private' => !empty($node->private) ? 1 : 0)));
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * If the record in the node_access_example table already exists, we must
+ * update it. If it doesn't exist, we create it.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_update($node) {
+ // Find out if there is already a node_access_example record.
+ $exists = db_query('SELECT nid FROM {node_access_example} WHERE nid = :nid',
+ array(':nid' => $node->nid))->fetchField();
+
+ // If there is already a record, update it with the new private value.
+ if ($exists) {
+ $num_updated = db_update('node_access_example')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'private' => !empty($node->private) ? 1 : 0,
+ ))
+ ->condition('nid', $node->nid)
+ ->execute();
+ drupal_set_message(
+ t("Updated node @nid to set private=@private (@num nodes actually updated)",
+ array(
+ '@private' => $node->private,
+ '@num' => $num_updated,
+ '@nid' => $node->nid,
+ )
+ )
+ );
+ }
+ // Otherwise, create a new record.
+ else {
+ node_access_example_node_insert($node);
+ drupal_set_message(t('Inserted new node_access nid=@nid, private=@private', array('@nid' => $node->nid, '@private' => $node->private)));
+ }
+
+}
+
+/**
+ * @} End of "defgroup node_access_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.test b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.test
new file mode 100644
index 00000000..61257b71
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_access_example/node_access_example.test
@@ -0,0 +1,338 @@
+ 'Node Access Example functionality',
+ 'description' => 'Checks behavior of Node Access Example.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('node_access_example', 'search');
+ node_access_rebuild();
+ }
+
+ /**
+ * Test the "private" node access.
+ *
+ * - Create 3 users with "access content" and "create article" permissions.
+ * - Each user creates one private and one not private article.
+ * - Run cron to update search index.
+ * - Test that each user can view the other user's non-private article.
+ * - Test that each user cannot view the other user's private article.
+ * - Test that each user finds only appropriate (non-private + own private)
+ * in search results.
+ * - Logout.
+ * - Test that anonymous user can't view, edit or delete private content which
+ * has author.
+ * - Test that anonymous user can't view, edit or delete private content with
+ * anonymous author.
+ * - Create another user with 'view any private content'.
+ * - Test that user 4 can view all content created above.
+ * - Test that user 4 can search for all content created above.
+ * - Test that user 4 cannot edit private content above.
+ * - Create another user with 'edit any private content'
+ * - Test that user 5 can edit private content.
+ * - Test that user 5 can delete private content.
+ * - Test listings of nodes with 'node_access' tag on database search.
+ */
+ public function testNodeAccessBasic() {
+ $num_simple_users = 3;
+ $simple_users = array();
+
+ // Nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private;.
+ $nodes_by_user = array();
+ // Titles keyed by nid.
+ $titles = array();
+ // Array of nids marked private.
+ $private_nodes = array();
+ for ($i = 0; $i < $num_simple_users; $i++) {
+ $simple_users[$i] = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'create article content',
+ 'search content',
+ )
+ );
+ }
+ foreach ($simple_users as $web_user) {
+ $this->drupalLogin($web_user);
+ foreach (array(0 => 'Public', 1 => 'Private') as $is_private => $type) {
+ $edit = array(
+ 'title' => t('@private_public Article created by @user', array('@private_public' => $type, '@user' => $web_user->name)),
+ );
+ if ($is_private) {
+ $edit['private'] = TRUE;
+ $edit['body[und][0][value]'] = 'private node';
+ }
+ else {
+ $edit['body[und][0][value]'] = 'public node';
+ }
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+ debug(t('Created article with private=@private', array('@private' => $is_private)));
+ $this->assertText(t('Article @title has been created', array('@title' => $edit['title'])));
+ $nid = db_query('SELECT nid FROM {node} WHERE title = :title', array(':title' => $edit['title']))->fetchField();
+ $this->assertText(t('New node @nid was created and private=@private', array('@nid' => $nid, '@private' => $is_private)));
+ $private_status = db_query('SELECT private FROM {node_access_example} where nid = :nid', array(':nid' => $nid))->fetchField();
+ $this->assertTrue($is_private == $private_status, 'Node was properly set to private or not private in node_access_example table.');
+ if ($is_private) {
+ $private_nodes[] = $nid;
+ }
+ $titles[$nid] = $edit['title'];
+ $nodes_by_user[$web_user->uid][$nid] = $is_private;
+ }
+ }
+ debug($nodes_by_user);
+ // Build the search index.
+ $this->cronRun();
+ foreach ($simple_users as $web_user) {
+ $this->drupalLogin($web_user);
+ // Check to see that we find the number of search results expected.
+ $this->checkSearchResults('Private node', 1);
+ // Check own nodes to see that all are readable.
+ foreach (array_keys($nodes_by_user) as $uid) {
+ // All of this user's nodes should be readable to same.
+ if ($uid == $web_user->uid) {
+ foreach ($nodes_by_user[$uid] as $nid => $is_private) {
+ $this->drupalGet('node/' . $nid);
+ $this->assertResponse(200);
+ $this->assertTitle($titles[$nid] . ' | Drupal', 'Correct title for node found');
+ }
+ }
+ else {
+ // Otherwise, for other users, private nodes should get a 403,
+ // but we should be able to read non-private nodes.
+ foreach ($nodes_by_user[$uid] as $nid => $is_private) {
+ $this->drupalGet('node/' . $nid);
+ $this->assertResponse(
+ $is_private ? 403 : 200,
+ format_string('Node @nid by user @uid should get a @response for this user (@web_user_uid)',
+ array(
+ '@nid' => $nid,
+ '@uid' => $uid,
+ '@response' => $is_private ? 403 : 200,
+ '@web_user_uid' => $web_user->uid,
+ )
+ )
+ );
+ if (!$is_private) {
+ $this->assertTitle($titles[$nid] . ' | Drupal', 'Correct title for node was found');
+ }
+ }
+ }
+ }
+
+ // Check to see that the correct nodes are shown on examples/node_access.
+ $this->drupalGet('examples/node_access');
+ $accessible = $this->xpath("//tr[contains(@class,'accessible')]");
+ $this->assertEqual(count($accessible), 1, 'One private item accessible');
+ foreach ($accessible as $row) {
+ $this->assertEqual($row->td[2], $web_user->uid, 'Accessible row owned by this user');
+ }
+ }
+
+ // Test cases for anonymous user.
+ $this->drupalLogout();
+
+ // Test that private nodes with authors are not accessible.
+ foreach ($private_nodes as $nid) {
+ if (($node = node_load($nid)) === FALSE) {
+ continue;
+ }
+ $this->checkNodeAccess($nid, FALSE, FALSE, FALSE);
+ }
+
+ // Test that private nodes that don't have author are not accessible.
+ foreach ($private_nodes as $nid) {
+ if (($node = node_load($nid)) === FALSE) {
+ continue;
+ }
+ $original_uid = $node->uid;
+
+ // Change node author to anonymous.
+ $node->uid = 0;
+ node_save($node);
+ $node = node_load($nid);
+ $this->assertEqual($node->uid, 0);
+
+ $this->checkNodeAccess($nid, FALSE, FALSE, FALSE);
+
+ // Change node to original author.
+ $node->uid = $original_uid;
+ node_save($node);
+ }
+
+ // Now test that a user with 'access any private content' can view content.
+ $access_user = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'create article content',
+ 'access any private content',
+ 'search content',
+ )
+ );
+ $this->drupalLogin($access_user);
+
+ // Check to see that we find the number of search results expected.
+ $this->checkSearchResults('Private node', 3);
+
+ foreach ($nodes_by_user as $uid => $private_status) {
+ foreach ($private_status as $nid => $is_private) {
+ $this->drupalGet('node/' . $nid);
+ $this->assertResponse(200);
+ }
+ }
+
+ // Check to see that the correct nodes are shown on examples/node_access.
+ // This user should be able to see all 3 of them.
+ $this->drupalGet('examples/node_access');
+ $accessible = $this->xpath("//tr[contains(@class,'accessible')]");
+ $this->assertEqual(count($accessible), 3);
+
+ // Test that a user named 'foobar' can edit any private node due to
+ // node_access_example_node_access(). Note that this user will not be
+ // able to search for private nodes, and will not have available nodes
+ // shown on examples/node_access, because node_access() is not called
+ // for node listings, only for actual access to a node.
+ $edit_user = $this->drupalCreateUser(
+ array(
+ 'access comments',
+ 'access content',
+ 'post comments',
+ 'skip comment approval',
+ 'search content',
+ )
+ );
+ // Update the name of the user to 'foobar'.
+ db_update('users')
+ ->fields(array(
+ 'name' => 'foobar',
+ ))
+ ->condition('uid', $edit_user->uid)
+ ->execute();
+
+ $edit_user->name = 'foobar';
+ $this->drupalLogin($edit_user);
+
+ // Try to edit each of the private nodes.
+ foreach ($private_nodes as $nid) {
+ $body = $this->randomName();
+ $edit = array('body[und][0][value]' => $body);
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+ $this->assertText(t('has been updated'), 'Node was updated by "foobar" user');
+ }
+
+ // Test that a privileged user can edit and delete private content.
+ // This test should go last, as the nodes get deleted.
+ $edit_user = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'access any private content',
+ 'edit any private content',
+ )
+ );
+ $this->drupalLogin($edit_user);
+ foreach ($private_nodes as $nid) {
+ $body = $this->randomName();
+ $edit = array('body[und][0][value]' => $body);
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+ $this->assertText(t('has been updated'));
+ $this->drupalPost('node/' . $nid . '/edit', array(), t('Delete'));
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->assertText(t('has been deleted'));
+ }
+ }
+
+ /**
+ * Helper function.
+ *
+ * On the search page, search for a string and assert the expected number
+ * of results.
+ *
+ * @param string $search_query
+ * String to search for
+ * @param int $expected_result_count
+ * Expected result count
+ */
+ protected function checkSearchResults($search_query, $expected_result_count) {
+ $this->drupalPost('search/node', array('keys' => $search_query), t('Search'));
+ $search_results = $this->xpath("//ol[contains(@class, 'search-results')]/li");
+ $this->assertEqual(count($search_results), $expected_result_count, 'Found the expected number of search results');
+ }
+
+ /**
+ * Helper function.
+ *
+ * Test if a node with the id $nid has expected access grants.
+ *
+ * @param int $nid
+ * Node that will be checked.
+ *
+ * @return bool
+ * Checker ran successfully
+ */
+ protected function checkNodeAccess($nid, $grant_view, $grant_update, $grant_delete) {
+ // Test if node can be viewed.
+ if (!$this->checkResponse($grant_view, 'node/' . $nid)) {
+ return FALSE;
+ }
+
+ // Test if private node can be edited.
+ if (!$this->checkResponse($grant_update, 'node/' . $nid . '/edit')) {
+ return FALSE;
+ }
+
+ // Test if private node can be deleted.
+ if (!$this->checkResponse($grant_delete, 'node/' . $nid . '/delete')) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+
+ /**
+ * Helper function.
+ *
+ * Test if there is access to an $url
+ *
+ * @param bool $grant
+ * Access to the $url
+ *
+ * @param string $url
+ * url to make the get call.
+ *
+ * @return bool
+ * Get response
+ */
+ protected function checkResponse($grant, $url) {
+ $this->drupalGet($url);
+ if ($grant) {
+ $response = $this->assertResponse(200);
+ }
+ else {
+ $response = $this->assertResponse(403);
+ }
+ return $response;
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/node_example/node_example.info b/sites/all/modules/contrib/dev/examples/node_example/node_example.info
new file mode 100644
index 00000000..0bfd4c03
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_example/node_example.info
@@ -0,0 +1,13 @@
+name = Node example
+description = Demonstrates a custom content type and uses the field api.
+package = Example modules
+core = 7.x
+dependencies[] = image
+files[] = node_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/node_example/node_example.module b/sites/all/modules/contrib/dev/examples/node_example/node_example.module
new file mode 100644
index 00000000..c684b4c8
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_example/node_example.module
@@ -0,0 +1,377 @@
+ 'node_example_page',
+ 'access arguments' => array('access content'),
+ 'title' => 'Node Example',
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_node_info().
+ *
+ * We use hook_node_info() to define our node content type.
+ */
+function node_example_node_info() {
+ // We define the node type as an associative array.
+ return array(
+ 'node_example' => array(
+ 'name' => t('Example Node Type'),
+ // 'base' tells Drupal the base string for hook functions.
+ // This is often the module name; if base is set to 'mymodule',
+ // Drupal would call mymodule_insert() or similar for node
+ // hooks. In our case, the base is 'node_example'.
+ 'base' => 'node_example',
+ 'description' => t('This is an example node type with a few fields.'),
+ 'title_label' => t('Example Title'),
+ // We'll set the 'locked' attribute to TRUE, so users won't be
+ // able to change the machine name of our content type.
+ 'locked' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_node_type_insert().
+ *
+ * Much like hook_node_insert() lets us know that a node is being
+ * inserted into the database, hook_node_type_insert() lets us know
+ * that a new content type has been inserted.
+ *
+ * Since Drupal will at some point insert our new content type,
+ * this gives us a chance to add the fields we want.
+ *
+ * It is called for all inserts to the content type database, so
+ * we have to make sure we're only modifying the type we're
+ * concerned with.
+ */
+function node_example_node_type_insert($content_type) {
+ if ($content_type->type == 'node_example') {
+ // First we add the body field. Node API helpfully gives us
+ // node_add_body_field().
+ // We'll set the body label now, although we could also set
+ // it along with our other instance properties later.
+ $body_instance = node_add_body_field($content_type, t('Example Description'));
+
+ // Add our example_node_list view mode to the body instance
+ // display by instructing the body to display as a summary.
+ $body_instance['display']['example_node_list'] = array(
+ 'label' => 'hidden',
+ 'type' => 'text_summary_or_trimmed',
+ );
+
+ // Save our changes to the body field instance.
+ field_update_instance($body_instance);
+
+ // Create all the fields we are adding to our content type.
+ foreach (_node_example_installed_fields() as $field) {
+ field_create_field($field);
+ }
+
+ // Create all the instances for our fields.
+ foreach (_node_example_installed_instances() as $instance) {
+ $instance['entity_type'] = 'node';
+ $instance['bundle'] = 'node_example';
+ field_create_instance($instance);
+ }
+ }
+}
+
+/**
+ * Implements hook_form().
+ *
+ * Drupal needs for us to provide a form that lets the user
+ * add content. This is the form that the user will see if
+ * they go to node/add/node-example.
+ *
+ * You can get fancy with this form, or you can just punt
+ * and return the default form that node_content will provide.
+ */
+function node_example_form($node, $form_state) {
+ return node_content_form($node, $form_state);
+}
+
+/**
+ * Callback that builds our content and returns it to the browser.
+ *
+ * This callback comes from hook_menu().
+ *
+ * @return array
+ * A renderable array showing a list of our nodes.
+ *
+ * @see node_load()
+ * @see node_view()
+ * @see node_example_field_formatter_view()
+ */
+function node_example_page() {
+ // We'll start building a renderable array that will be our page.
+ // For now we just declare the array.
+ $renderable_array = array();
+ // We query the database and find all of the nodes for the type we defined.
+ $sql = 'SELECT nid FROM {node} n WHERE n.type = :type AND n.status = :status';
+ $result = db_query($sql,
+ array(
+ ':type' => 'node_example',
+ ':status' => 1,
+ )
+ );
+ $renderable_array['explanation'] = array(
+ '#markup' => t("Node Example nodes you've created will be displayed here. Note that the color fields will be displayed differently in this list, than if you view the node normally. Click on the node title to see the difference. This is a result of using our 'example_node_list' node view type."),
+ );
+ // Loop through each of our node_example nodes and instruct node_view
+ // to use our "example_node_list" view.
+ // http://api.drupal.org/api/function/node_load/7
+ // http://api.drupal.org/api/function/node_view/7
+ foreach ($result as $row) {
+ $node = node_load($row->nid);
+ $renderable_array['node_list'][] = node_view($node, 'example_node_list');
+ }
+ return $renderable_array;
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ *
+ * We need to modify the default node entity info by adding a new view mode to
+ * be used in functions like node_view() or node_build_content().
+ */
+function node_example_entity_info_alter(&$entity_info) {
+ // Add our new view mode to the list of view modes...
+ $entity_info['node']['view modes']['example_node_list'] = array(
+ 'label' => t('Example Node List'),
+ 'custom settings' => TRUE,
+ );
+}
+
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function node_example_field_formatter_info() {
+ return array(
+ 'node_example_colors' => array(
+ 'label' => t('Node Example Color Handle'),
+ 'field types' => array('text'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * @todo: We need to provide a formatter for the colors that a user is allowed
+ * to enter during node creation.
+ */
+function node_example_field_formatter_view($object_type, $object, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+ switch ($display['type']) {
+ case 'node_example_colors':
+ foreach ($items as $delta => $item) {
+ $element[$delta]['#type'] = 'markup';
+ $color = $item['safe_value'];
+ $element[$delta]['#markup'] = theme('example_node_color', array('color' => $color));
+ }
+ break;
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * This lets us tell Drupal about our theme functions and their arguments.
+ */
+function node_example_theme($existing, $type, $theme, $path) {
+ return array(
+ 'example_node_color' => array(
+ 'variables' => array('color' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_help().
+ */
+function node_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/node_example':
+ return "
" . t("The Node Example module provides a custom node type.
+ You can create new Example Node nodes using the node add form.",
+ array('!nodeadd' => url('node/add/node-example'))) . "
";
+ }
+}
+
+/**
+ * A custom theme function.
+ *
+ * By using this function to format our node-specific information, themes
+ * can override this presentation if they wish. This is a simplifed theme
+ * function purely for illustrative purposes.
+ */
+function theme_example_node_color($variables) {
+ $output = '' . $variables['color'] . '';
+ return $output;
+}
+
+/**
+ * Define the fields for our content type.
+ *
+ * This big array is factored into this function for readability.
+ *
+ * @return array
+ * An associative array specifying the fields we wish to add to our
+ * new node type.
+ */
+function _node_example_installed_fields() {
+ return array(
+ 'node_example_color' => array(
+ 'field_name' => 'node_example_color',
+ 'cardinality' => 3,
+ 'type' => 'text',
+ 'settings' => array(
+ 'max_length' => 60,
+ ),
+ ),
+ 'node_example_quantity' => array(
+ 'field_name' => 'node_example_quantity',
+ 'cardinality' => 1,
+ 'type' => 'text',
+ ),
+ 'node_example_image' => array(
+ 'field_name' => 'node_example_image',
+ 'type' => 'image',
+ 'cardinality' => 1,
+ ),
+ );
+}
+
+/**
+ * Define the field instances for our content type.
+ *
+ * The instance lets Drupal know which widget to use to allow the user to enter
+ * data and how to react in different view modes. We are going to display a
+ * page that uses a custom "node_example_list" view mode. We will set a
+ * cardinality of three allowing our content type to give the user three color
+ * fields.
+ *
+ * This big array is factored into this function for readability.
+ *
+ * @return array
+ * An associative array specifying the instances we wish to add to our new
+ * node type.
+ */
+function _node_example_installed_instances() {
+ return array(
+ 'node_example_color' => array(
+ 'field_name' => 'node_example_color',
+ 'label' => t('The colors available for this object.'),
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ ),
+ 'display' => array(
+ 'example_node_list' => array(
+ 'label' => 'hidden',
+ 'type' => 'node_example_colors',
+ ),
+ ),
+ ),
+ 'node_example_quantity' => array(
+ 'field_name' => 'node_example_quantity',
+ 'label' => t('Quantity required'),
+ 'type' => 'text',
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ ),
+ 'display' => array(
+ 'example_node_list' => array(
+ 'label' => 'hidden',
+ 'type' => 'hidden',
+ ),
+ ),
+ ),
+ 'node_example_image' => array(
+ 'field_name' => 'node_example_image',
+ 'label' => t('Upload an image:'),
+ 'required' => FALSE,
+ 'widget' => array(
+ 'type' => 'image_image',
+ 'weight' => 2.10,
+ ),
+ 'display' => array(
+ 'example_node_list' => array(
+ 'label' => 'hidden',
+ 'type' => 'image_link_content__thumbnail',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * @} End of "defgroup node_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/node_example/node_example.test b/sites/all/modules/contrib/dev/examples/node_example/node_example.test
new file mode 100644
index 00000000..3c2cd5e2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/node_example/node_example.test
@@ -0,0 +1,117 @@
+ 'Node example',
+ 'description' => 'Verify the custom node type creation.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('node_example');
+ }
+
+ /**
+ * API-level content type test.
+ *
+ * This test will verify that when the module is installed, it:
+ * - Adds a new content type, node_example.
+ * - Attaches a body field.
+ * - Attaches three other fields.
+ * - Creates a view mode, example_node_list.
+ */
+ public function testInstallationApi() {
+ // At this point, the module should be installed.
+ // First check for our content type.
+ $node_type = node_type_get_type('node_example');
+ $this->assertTrue($node_type, 'Node Example Type was created.', 'API');
+
+ // How about the body field?
+ $body = field_info_instance('node', 'body', 'node_example');
+ $this->assertTrue($body, 'Node Example Type has a body field.', 'API');
+
+ // Now look for our attached fields.
+ // We made a handy function that tells us...
+ $attached_fields = _node_example_installed_instances();
+ foreach ($attached_fields as $field_name => $field_info) {
+ $field = field_info_instance('node', $field_name, 'node_example');
+ $this->assertTrue($field,
+ 'Field: ' . $field_name . ' was attached to node_example.', 'API');
+ }
+
+ // And that view mode...
+ // entity_get_info() invokes hook_entity_info_alter(), so it's
+ // a good place to verify that our code works.
+ $entities = entity_get_info('node');
+ $this->assertTrue(isset($entities['view modes']['example_node_list']),
+ 'Added example_node_list view mode.', 'API');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testNodeCreation() {
+ // Create and login user.
+ $account = $this->drupalCreateUser(array('access content', 'create node_example content'));
+ $this->drupalLogin($account);
+
+ // Create a new node. The image makes it more complicated, so skip it.
+ $edit = array(
+ 'title' => $this->randomName(),
+ 'node_example_color[und][0][value]' => 'red',
+ 'node_example_color[und][1][value]' => 'green',
+ 'node_example_color[und][2][value]' => 'blue',
+ 'node_example_quantity[und][0][value]' => 100,
+ );
+ $this->drupalPost('node/add/node-example', $edit, t('Save'));
+ $this->assertText("Example Node Type " . $edit['title'] . " has been created", "Found node creation message");
+ $this->assertPattern("/The colors available.*red.*green.*blue/", "Correct 'colors available' on node page");
+
+ // Look on the examples page to make sure it shows up there also.
+ $this->drupalGet('examples/node_example');
+ $this->assertText($edit['title'], "Found random title string");
+ $this->assertPattern("/red.*green.*blue/", "Correct 'colors available' on node example page");
+
+ }
+
+ /**
+ * Check the value of body label.
+ *
+ * Checks whether body label has a value of "Example Description"
+ */
+ public function testBodyLabel() {
+ // Create and login user.
+ $account = $this->drupalCreateUser(array('access content', 'create node_example content'));
+ $this->drupalLogin($account);
+
+ // Request a node add node-example page.
+ // Test whether the body label equals 'Example Description'.
+ // Use '$this->assertRaw' to make certain to test the body label and not
+ // some other text.
+ $this->drupalGet('node/add/node-example');
+ $this->assertResponse(200, 'node/add/node-example page found');
+ $this->assertRaw('', 'Body label equals \'Example Description\'');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.info b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.info
new file mode 100644
index 00000000..1fb33cee
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.info
@@ -0,0 +1,12 @@
+name = NodeAPI example
+description = Demonstrates using the hook_node_* APIs (formerly hook_nodeapi) to alter a node from a different module.
+package = Example modules
+core = 7.x
+files[] = nodeapi_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.install b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.install
new file mode 100644
index 00000000..1d2a7ca2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.install
@@ -0,0 +1,80 @@
+ 'Stores information of extended content.',
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => 'Node ID that the rating is applied to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'vid' => array(
+ 'description' => 'Revision ID, as we are tracking rating with node revisions',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'rating' => array(
+ 'description' => 'The rating of the node.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('vid'),
+ 'indexes' => array(
+ 'nid' => array('nid'),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * We need to clean up our variables data when uninstalling our module.
+ *
+ * Our implementation of nodeapi_example_form_alter() automatically
+ * creates a nodeapi_example_node_type_ variable for each node type
+ * the user wants to rate.
+ *
+ * To delete our variables we call variable_del for our variables'
+ * namespace, 'nodeapi_example_node_type_'. Note that an average module would
+ * have known variables that it had created, and it could just delete those
+ * explicitly. For example, see render_example_uninstall(). It's important
+ * not to delete variables that might be owned by other modules, so normally
+ * we would just explicitly delete a set of known variables.
+ *
+ * hook_uninstall() will only be called when uninstalling a module, not when
+ * disabling a module. This allows our data to stay in the database if the user
+ * only disables our module without uninstalling it.
+ *
+ * @ingroup nodeapi_example
+ */
+function nodeapi_example_uninstall() {
+ // Simple DB query to get the names of our variables.
+ $results = db_select('variable', 'v')
+ ->fields('v', array('name'))
+ ->condition('name', 'nodeapi_example_node_type_%', 'LIKE')
+ ->execute();
+ // Loop through and delete each of our variables.
+ foreach ($results as $result) {
+ variable_del($result->name);
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.module b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.module
new file mode 100644
index 00000000..423b35e9
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.module
@@ -0,0 +1,282 @@
+ 'fieldset',
+ '#title' => t('Rating settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#group' => 'additional_settings',
+ '#weight' => -1,
+ );
+
+ $form['rating']['nodeapi_example_node_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('NodeAPI Example Rating'),
+ '#default_value' => variable_get('nodeapi_example_node_type_' . $form['#node_type']->type, FALSE),
+ '#options' => array(
+ FALSE => t('Disabled'),
+ TRUE => t('Enabled'),
+ ),
+ '#description' => t('Should this node have a rating attached to it?'),
+ );
+ }
+ // Here we check to see if the type and node field are set. If so, it could
+ // be a node edit form.
+ elseif (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] . '_node_form' == $form_id) {
+ // If the rating is enabled for this node type, we insert our control
+ // into the form.
+ $node = $form['#node'];
+ if (variable_get('nodeapi_example_node_type_' . $form['type']['#value'], FALSE)) {
+ $form['nodeapi_example_rating'] = array(
+ '#type' => 'select',
+ '#title' => t('Rating'),
+ '#default_value' => isset($node->nodeapi_example_rating) ? $node->nodeapi_example_rating : '',
+ '#options' => array(0 => t('Unrated'), 1, 2, 3, 4, 5),
+ '#required' => TRUE,
+ '#weight' => 0,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_node_validate().
+ *
+ * Check that the rating attribute is set in the form submission, since the
+ * field is required. If not, send error message.
+ */
+function nodeapi_example_node_validate($node, $form) {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ if (isset($node->nodeapi_example_rating) && !$node->nodeapi_example_rating) {
+ form_set_error('nodeapi_example_rating', t('You must rate this content.'));
+ }
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * Loads the rating information if available for any of the nodes in the
+ * argument list.
+ */
+function nodeapi_example_node_load($nodes, $types) {
+ // We can use $types to figure out if we need to process any of these nodes.
+ $our_types = array();
+ foreach ($types as $type) {
+ if (variable_get('nodeapi_example_node_type_' . $type, FALSE)) {
+ $our_types[] = $type;
+ }
+ }
+
+ // Now $our_types contains all the types from $types that we want
+ // to deal with. If it's empty, we can safely return.
+ if (!count($our_types)) {
+ return;
+ }
+
+ // Now we need to make a list of revisions based on $our_types
+ foreach ($nodes as $node) {
+ // We are using the revision id instead of node id.
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ $vids[] = $node->vid;
+ }
+ }
+ // Check if we should load rating for any of the nodes.
+ if (!isset($vids) || !count($vids)) {
+ return;
+ }
+
+ // When we read, we don't care about the node->nid; we look for the right
+ // revision ID (node->vid).
+ $result = db_select('nodeapi_example', 'e')
+ ->fields('e', array('nid', 'vid', 'rating'))
+ ->where('e.vid IN (:vids)', array(':vids' => $vids))
+ ->execute();
+
+ foreach ($result as $record) {
+ $nodes[$record->nid]->nodeapi_example_rating = $record->rating;
+ }
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * As a new node is being inserted into the database, we need to do our own
+ * database inserts.
+ */
+function nodeapi_example_node_insert($node) {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ // Notice that we are ignoring any revision information using $node->nid
+ db_insert('nodeapi_example')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'vid' => $node->vid,
+ 'rating' => $node->nodeapi_example_rating,
+ ))
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ *
+ * When a node is deleted, we need to remove all related records from our table,
+ * including all revisions. For the delete operations we use node->nid.
+ */
+function nodeapi_example_node_delete($node) {
+ // Notice that we're deleting even if the content type has no rating enabled.
+ db_delete('nodeapi_example')
+ ->condition('nid', $node->nid)
+ ->execute();
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * As an existing node is being updated in the database, we need to do our own
+ * database updates.
+ *
+ * This hook is called when an existing node has been changed. We can't simply
+ * update, since the node may not have a rating saved, thus no
+ * database field. So we first check the database for a rating. If there is one,
+ * we update it. Otherwise, we call nodeapi_example_node_insert() to create one.
+ */
+function nodeapi_example_node_update($node) {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ // Check first if this node has a saved rating.
+ $rating = db_select('nodeapi_example', 'e')
+ ->fields('e', array(
+ 'rating',
+ ))
+ ->where('e.vid = (:vid)', array(':vid' => $node->vid))
+ ->execute()->fetchField();
+
+ if ($rating) {
+ // Node has been rated before.
+ db_update('nodeapi_example')
+ ->fields(array('rating' => $node->nodeapi_example_rating))
+ ->condition('vid', $node->vid)
+ ->execute();
+ }
+ else {
+ // Node was not previously rated, so insert a new rating in database.
+ nodeapi_example_node_insert($node);
+ }
+ }
+}
+
+/**
+ * Implements hook_node_view().
+ *
+ * This is a typical implementation that simply runs the node text through
+ * the output filters.
+ *
+ * Finally, we need to take care of displaying our rating when the node is
+ * viewed. This operation is called after the node has already been prepared
+ * into HTML and filtered as necessary, so we know we are dealing with an
+ * HTML teaser and body. We will inject our additional information at the front
+ * of the node copy.
+ *
+ * Using node API 'hook_node_view' is more appropriate than using a filter here,
+ * because filters transform user-supplied content, whereas we are extending it
+ * with additional information.
+ */
+function nodeapi_example_node_view($node, $build_mode = 'full') {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ // Make sure to set a rating, also for nodes saved previously and not yet
+ // rated.
+ $rating = isset($node->nodeapi_example_rating) ? $node->nodeapi_example_rating : 0;
+ $node->content['nodeapi_example'] = array(
+ '#markup' => theme('nodeapi_example_rating', array('rating' => $rating)),
+ '#weight' => -1,
+ );
+ }
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * This lets us tell Drupal about our theme functions and their arguments.
+ */
+function nodeapi_example_theme() {
+ return array(
+ 'nodeapi_example_rating' => array(
+ 'variables' => array('rating' => NULL),
+ ),
+ );
+}
+
+/**
+ * A custom theme function.
+ *
+ * By using this function to format our rating, themes can override this
+ * presentation if they wish; for example, they could provide a star graphic
+ * for the rating. We also wrap the default presentation in a CSS class that
+ * is prefixed by the module name. This way, style sheets can modify the output
+ * without requiring theme code.
+ */
+function theme_nodeapi_example_rating($variables) {
+ $options = array(
+ 0 => t('Unrated'),
+ 1 => t('Poor'),
+ 2 => t('Needs improvement'),
+ 3 => t('Acceptable'),
+ 4 => t('Good'),
+ 5 => t('Excellent'),
+ );
+ $output = '
';
+ return $output;
+}
+
+/**
+ * @} End of "defgroup nodeapi_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.test b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.test
new file mode 100644
index 00000000..5dd756db
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/nodeapi_example/nodeapi_example.test
@@ -0,0 +1,222 @@
+ 'Node API example functionality',
+ 'description' => 'Demonstrate Node API hooks that allow altering a node. These are the former hook_nodeapi.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enables modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('nodeapi_example');
+
+ // Create admin user. This module has no access control, so we can use a
+ // trusted user. Revision access and revert permissions are required too.
+ $this->webUser = $this->drupalCreateUser(array(
+ // Required to set revision checkbox.
+ 'administer nodes',
+ 'administer content types',
+ 'bypass node access',
+ 'view revisions',
+ 'revert revisions',
+ ));
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Log user in, creates an example node, and uses the rating system.
+ */
+ public function testNodeExampleBasic() {
+
+ // Login the user.
+ $this->drupalLogin($this->webUser);
+
+ // Create custom content type.
+ $content_type = $this->drupalCreateContentType();
+ $type = $content_type->type;
+
+ // Go to edit the settings of this content type.
+ $this->drupalGet('admin/structure/types/manage/' . $type);
+ $this->assertResponse(200);
+
+ // Check if the new Rating options appear in the settings page.
+ $this->assertText(t('NodeAPI Example Rating'), 'Rating options found in content type.');
+ $this->assertFieldByName('nodeapi_example_node_type', 1, 'Rating is Disabled by default.');
+
+ // Disable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => 0,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Create an example node.
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ );
+ $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that the rating is not shown, as we have not yet enabled it.
+ $this->assertNoRaw('Rating: ', 'Extended rating information is not shown.');
+
+ // Save current current url (we are viewing the new node).
+ $node_url = $this->getUrl();
+
+ // Enable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => TRUE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Check previously create node. It should be not rated.
+ $this->drupalGet($node_url);
+ $this->assertResponse(200);
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Unrated'))), 'Content is not rated.');
+
+ // Rate the content, 4 is for "Good"
+ $rate = array(
+ 'nodeapi_example_rating' => 4,
+ );
+ $this->drupalPost($node_url . '/edit', $rate, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Good'))), 'Content is successfully rated.');
+
+ }
+
+ /**
+ * Test revisions of ratings.
+ *
+ * Logs user in, creates an example node, and tests rating functionality with
+ * a node using revisions.
+ */
+ public function testNodeExampleRevision() {
+
+ // Login the user.
+ $this->drupalLogin($this->webUser);
+
+ // Create custom content type.
+ $content_type = $this->drupalCreateContentType();
+ $type = $content_type->type;
+
+ // Go to edit the settings of this content type.
+ $this->drupalGet('admin/structure/types/manage/' . $type);
+ $this->assertResponse(200);
+
+ // Check if the new Rating options appear in the settings page.
+ $this->assertText(t('NodeAPI Example Rating'), 'Rating options found in content type.');
+ $this->assertFieldByName('nodeapi_example_node_type', 1, 'Rating is Disabled by default.');
+
+ // Disable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => 0,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Create an example node.
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ );
+ $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that the rating is not shown, as we have not yet enabled it.
+ $this->assertNoRaw('Rating: ', 'Extended rating information is not shown.');
+
+ // Save current current url (we are viewing the new node).
+ $node_url = $this->getUrl();
+
+ // Enable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => TRUE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Check previously create node. It should be not rated.
+ $this->drupalGet($node_url);
+ $this->assertResponse(200);
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Unrated'))), 'Content is not rated.');
+
+ // Rate the content, 4 is for "Good"
+ $rate = array(
+ 'nodeapi_example_rating' => 4,
+ );
+ $this->drupalPost($node_url . '/edit', $rate, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Good'))), 'Content is successfully rated.');
+
+ // Rate the content to poor using a new revision, 1 is for "Poor"
+ $rate = array(
+ 'nodeapi_example_rating' => 1,
+ 'revision' => 1,
+ );
+ $this->drupalPost($node_url . '/edit', $rate, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Poor'))), 'Content is successfully rated.');
+
+ // Now switch back to previous revision of the node.
+ $this->drupalGet($node_url . '/revisions');
+ // There is only a revision, so it must work just clicking the first link..
+ $this->clickLink('revert');
+ $revert_form = $this->getUrl();
+ $this->drupalPost($revert_form, array(), t('Revert'));
+
+ // Go back to the node page.
+ $this->drupalGet($node_url);
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Good'))), 'Content rating matches reverted revision.');
+
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/page_example/page_example.info b/sites/all/modules/contrib/dev/examples/page_example/page_example.info
new file mode 100644
index 00000000..376d4288
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/page_example/page_example.info
@@ -0,0 +1,12 @@
+name = Page example
+description = An example module showing how to define a page to be displayed to the user at a given URL.
+package = Example modules
+core = 7.x
+files[] = page_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/page_example/page_example.module b/sites/all/modules/contrib/dev/examples/page_example/page_example.module
new file mode 100644
index 00000000..7545c8f8
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/page_example/page_example.module
@@ -0,0 +1,213 @@
+ array(
+ 'title' => t('Access simple page'),
+ 'description' => t('Allow users to access simple page'),
+ ),
+ 'access arguments page' => array(
+ 'title' => t('Access page with arguments'),
+ 'description' => t('Allow users to access page with arguments'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Because hook_menu() registers URL paths for items defined by the function, it
+ * is necessary for modules that create pages. Each item can also specify a
+ * callback function for a given URL. The menu items returned here provide this
+ * information to the menu system.
+ *
+ * We will define some menus, and their paths will be interpreted as follows:
+ *
+ * If the user accesses http://example.com/?q=examples/page_example/simple,
+ * the menu system will first look for a menu item with that path. In this case
+ * it will find a match, and execute page_example_simple().
+ *
+ * If the user accesses http://example.com/?q=examples/page_example/arguments,
+ * the menu system will find no explicit match, and will fall back to the
+ * closest match, 'examples/page_example', executing page_example_description().
+ *
+ * If the user accesses
+ * http://example.com/?q=examples/page_example/arguments/1/2, the menu
+ * system will first look for examples/page_example/arguments/1/2. Not finding
+ * a match, it will look for examples/page_example/arguments/1/%. Again not
+ * finding a match, it will look for examples/page_example/arguments/%/2.
+ * Yet again not finding a match, it will look for
+ * examples/page_example/arguments/%/%. This time it finds a match, and so will
+ * execute page_example_arguments(1, 2). Since the parameters are passed to
+ * the function after the match, the function can do additional checking or
+ * make use of them before executing the callback function.
+ *
+ * @see hook_menu()
+ * @see menu_example
+ */
+function page_example_menu() {
+
+ // This is the minimum information you can provide for a menu item. This menu
+ // item will be created in the default menu, usually Navigation.
+ $items['examples/page_example'] = array(
+ 'title' => 'Page Example',
+ 'page callback' => 'page_example_description',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+
+ $items['examples/page_example/simple'] = array(
+ 'title' => 'Simple - no arguments',
+ 'page callback' => 'page_example_simple',
+ 'access arguments' => array('access simple page'),
+ );
+
+ // By using the MENU_CALLBACK type, we can register the callback for this
+ // path without the item appearing in the menu; the admin cannot enable the
+ // item in the menu, either.
+ //
+ // Notice that 'page arguments' is an array of numbers. These will be
+ // replaced with the corresponding parts of the menu path. In this case a 0
+ // would be replaced by 'examples', a 1 by 'page_example', and a 2 by
+ // 'arguments.' 3 and 4 will be replaced by whatever the user provides.
+ // These will be passed as arguments to the page_example_arguments() function.
+ $items['examples/page_example/arguments/%/%'] = array(
+ 'page callback' => 'page_example_arguments',
+ 'page arguments' => array(3, 4),
+ 'access arguments' => array('access arguments page'),
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Constructs a descriptive page.
+ *
+ * Our menu maps this function to the path 'examples/page_example'.
+ */
+function page_example_description() {
+ return array(
+ '#markup' =>
+ t('
The page_example provides two pages, "simple" and "arguments".
The simple page just returns a renderable array for display.
The arguments page takes two arguments and displays them, as in @arguments_link
',
+ array(
+ '@simple_link' => url('examples/page_example/simple', array('absolute' => TRUE)),
+ '@arguments_link' => url('examples/page_example/arguments/23/56', array('absolute' => TRUE)),
+ )
+ ),
+ );
+}
+
+/**
+ * Constructs a simple page.
+ *
+ * The simple page callback, mapped to the path 'examples/page_example/simple'.
+ *
+ * Page callbacks return a renderable array with the content area of the page.
+ * The theme system will later render and surround the content in the
+ * appropriate blocks, navigation, and styling.
+ *
+ * If you do not want to use the theme system (for example for outputting an
+ * image or XML), you should print the content yourself and not return anything.
+ */
+function page_example_simple() {
+ return array('#markup' => '
' . t('Simple page: The quick brown fox jumps over the lazy dog.') . '
');
+}
+
+/**
+ * A more complex page callback that takes arguments.
+ *
+ * This callback is mapped to the path 'examples/page_example/arguments/%/%'.
+ *
+ * The % arguments are passed in from the page URL. In our hook_menu
+ * implementation we instructed the menu system to extract the last two
+ * parameters of the path and pass them to this function as arguments.
+ *
+ * This function also demonstrates a more complex render array in the returned
+ * values. Instead of just rendering the HTML with a theme('item_list'), the
+ * list is left unrendered, and a #theme attached to it so that it can be
+ * rendered as late as possible, giving more parts of the system a chance to
+ * change it if necessary.
+ *
+ * Consult @link http://drupal.org/node/930760 Render Arrays documentation
+ * @endlink for details.
+ */
+function page_example_arguments($first, $second) {
+ // Make sure you don't trust the URL to be safe! Always check for exploits.
+ if (!is_numeric($first) || !is_numeric($second)) {
+ // We will just show a standard "access denied" page in this case.
+ drupal_access_denied();
+ // We actually don't get here.
+ return;
+ }
+
+ $list[] = t("First number was @number.", array('@number' => $first));
+ $list[] = t("Second number was @number.", array('@number' => $second));
+ $list[] = t('The total was @number.', array('@number' => $first + $second));
+
+ $render_array['page_example_arguments'] = array(
+ // The theme function to apply to the #items.
+ '#theme' => 'item_list',
+ // The list itself.
+ '#items' => $list,
+ '#title' => t('Argument Information'),
+ );
+ return $render_array;
+}
+/**
+ * @} End of "defgroup page_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/page_example/page_example.test b/sites/all/modules/contrib/dev/examples/page_example/page_example.test
new file mode 100644
index 00000000..e3a76c6f
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/page_example/page_example.test
@@ -0,0 +1,126 @@
+ 'Page example functionality',
+ 'description' => 'Creates page and render the content based on the arguments passed in the URL.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('page_example');
+ }
+
+ /**
+ * Generates a random string of ASCII numeric characters (values 48 to 57).
+ *
+ * @param int $length
+ * Length of random string to generate.
+ *
+ * @return string
+ * Randomly generated string.
+ */
+ protected static function randomNumber($length = 8) {
+ $str = '';
+ for ($i = 0; $i < $length; $i++) {
+ $str .= chr(mt_rand(48, 57));
+ }
+ return $str;
+ }
+
+ /**
+ * Verify that current user has no access to page.
+ *
+ * @param string $url
+ * URL to verify.
+ */
+ public function pageExampleVerifyNoAccess($url) {
+ // Test that page returns 403 Access Denied.
+ $this->drupalGet($url);
+ $this->assertResponse(403);
+ }
+
+ /**
+ * Functional test for various page types.
+ */
+ public function testPageExampleBasic() {
+
+ // Verify that anonymous user can't access the pages created by
+ // page_example module.
+ $this->pageExampleVerifyNoAccess('examples/page_example/simple');
+ $this->pageExampleVerifyNoAccess('examples/page_example/arguments/1/2');
+
+ // Create a regular user and login.
+ $this->webUser = $this->drupalCreateUser();
+ $this->drupalLogin($this->webUser);
+
+ // Verify that regular user can't access the pages created by
+ // page_example module.
+ $this->pageExampleVerifyNoAccess('examples/page_example/simple');
+ $this->pageExampleVerifyNoAccess('examples/page_example/arguments/1/2');
+
+ // Create a user with permissions to access 'simple' page and login.
+ $this->webUser = $this->drupalCreateUser(array('access simple page'));
+ $this->drupalLogin($this->webUser);
+
+ // Verify that user can access simple content.
+ $this->drupalGet('examples/page_example/simple');
+ $this->assertResponse(200, 'simple content successfully accessed.');
+ $this->assertText(t('The quick brown fox jumps over the lazy dog.'), 'Simple content successfully verified.');
+
+ // Check if user can't access arguments page.
+ $this->pageExampleVerifyNoAccess('examples/page_example/arguments/1/2');
+
+ // Create a user with permissions to access 'simple' page and login.
+ $this->webUser = $this->drupalCreateUser(array('access arguments page'));
+ $this->drupalLogin($this->webUser);
+
+ // Verify that user can access simple content.
+ $first = $this->randomNumber(3);
+ $second = $this->randomNumber(3);
+ $this->drupalGet('examples/page_example/arguments/' . $first . '/' . $second);
+ $this->assertResponse(200, 'arguments content successfully accessed.');
+ // Verify argument usage.
+ $this->assertRaw(t("First number was @number.", array('@number' => $first)), 'arguments first argument successfully verified.');
+ $this->assertRaw(t("Second number was @number.", array('@number' => $second)), 'arguments second argument successfully verified.');
+ $this->assertRaw(t('The total was @number.', array('@number' => $first + $second)), 'arguments content successfully verified.');
+
+ // Verify incomplete argument call to arguments content.
+ $this->drupalGet('examples/page_example/arguments/' . $first . '/');
+ $this->assertText("provides two pages");
+
+ // Verify invalid argument call to arguments content.
+ $this->drupalGet('examples/page_example/arguments/' . $first . '/' . $this->randomString());
+ $this->assertResponse(403, 'Invalid argument for arguments content successfully verified');
+
+ // Verify invalid argument call to arguments content.
+ $this->drupalGet('examples/page_example/arguments/' . $this->randomString() . '/' . $second);
+ $this->assertResponse(403, 'Invalid argument for arguments content successfully verified');
+
+ // Check if user can't access simple page.
+ $this->pageExampleVerifyNoAccess('examples/page_example/simple');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/pager_example/pager_example.info b/sites/all/modules/contrib/dev/examples/pager_example/pager_example.info
new file mode 100644
index 00000000..2a424ecf
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/pager_example/pager_example.info
@@ -0,0 +1,12 @@
+name = Pager example
+description = Demonstrates a page with content in a pager
+package = Example modules
+core = 7.x
+files[] = pager_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/pager_example/pager_example.module b/sites/all/modules/contrib/dev/examples/pager_example/pager_example.module
new file mode 100644
index 00000000..18a024ba
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/pager_example/pager_example.module
@@ -0,0 +1,98 @@
+' . t('The layout here is a themed as a table with a default limit of 10 rows per page. The limit can be changed in the code by changing the limit to some other value. This can be extended to add a filter form as well so the user can choose how many they would like to see on each screen.') . '
';
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function pager_example_menu() {
+ $items['examples/pager_example'] = array(
+ 'title' => 'Pager example',
+ 'description' => 'Show a page with a long list across multiple pages',
+ 'page callback' => 'pager_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Build the pager query.
+ *
+ * Uses the date_formats table since it is installed with ~35 rows
+ * in it and we don't have to create fake data in order to show
+ * this example.
+ *
+ * @return array
+ * A render array completely set up with a pager.
+ */
+function pager_example_page() {
+ // We are going to output the results in a table with a nice header.
+ $header = array(
+ array('data' => t('DFID')),
+ array('data' => t('Format')),
+ array('data' => t('Type')),
+ );
+
+ // We are extending the PagerDefault class here.
+ // It has a default of 10 rows per page.
+ // The extend('PagerDefault') part here does all the magic.
+ $query = db_select('date_formats', 'd')->extend('PagerDefault');
+ $query->fields('d', array('dfid', 'format', 'type'));
+
+ // Change the number of rows with the limit() call.
+ $result = $query
+ ->limit(10)
+ ->orderBy('d.dfid')
+ ->execute();
+
+ $rows = array();
+ foreach ($result as $row) {
+ // Normally we would add some nice formatting to our rows
+ // but for our purpose we are simply going to add our row
+ // to the array.
+ $rows[] = array('data' => (array) $row);
+ }
+
+ // Create a render array ($build) which will be themed as a table with a
+ // pager.
+ $build['pager_table'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#empty' => t('There are no date formats found in the db'),
+ );
+
+ // Attach the pager theme.
+ $build['pager_pager'] = array('#theme' => 'pager');
+
+ return $build;
+}
+/**
+ * @} End of "defgroup pager_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/pager_example/pager_example.test b/sites/all/modules/contrib/dev/examples/pager_example/pager_example.test
new file mode 100644
index 00000000..554b6f26
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/pager_example/pager_example.test
@@ -0,0 +1,57 @@
+ 'Pager Example',
+ 'description' => 'Verify the pager functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('pager_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testPagerPage() {
+ // No need to login for this test.
+ $this->drupalGet('examples/pager_example');
+ $this->assertText('next', 'Found next link');
+ $this->assertText('last', 'Found last link');
+
+ // On the first page we shouldn't see the first
+ // or previous links.
+ $this->assertNoText('first', 'No first link on the first page');
+ $this->assertNoText('previous', 'No previous link on the first page');
+
+ // Let's go to the second page.
+ $this->drupalGet('examples/pager_example', array('query' => array('page' => 1)));
+ $this->assertText('next', 'Found next link');
+ $this->assertText('last', 'Found last link');
+
+ // On the second page we should also see the first
+ // and previous links.
+ $this->assertText('first', 'Found first link');
+ $this->assertText('previous', 'Found previous link');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/queue_example/queue_example.css b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.css
new file mode 100644
index 00000000..fd80c4b1
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.css
@@ -0,0 +1,3 @@
+.form-item-string-to-add, div.form-item-claim-time {
+ display: inline;
+}
diff --git a/sites/all/modules/contrib/dev/examples/queue_example/queue_example.info b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.info
new file mode 100644
index 00000000..5b2bf25a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.info
@@ -0,0 +1,12 @@
+name = Queue example
+description = Examples of using the Drupal Queue API.
+package = Example modules
+core = 7.x
+files[] = queue_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/queue_example/queue_example.module b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.module
new file mode 100644
index 00000000..28bd1cc1
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.module
@@ -0,0 +1,351 @@
+ 'Queue Example: Insert and remove',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('queue_example_add_remove_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Form generator for managing the queue.
+ *
+ * Provides an interface to add items to the queue, to retrieve (claim)
+ * an item from the head of the queue, and to claim and delete. Also
+ * allows the user to run cron manually, so that claimed items can be
+ * released.
+ */
+function queue_example_add_remove_form($form, &$form_state) {
+ // Simple counter that makes it possible to put auto-incrementing default
+ // string into the string to insert.
+ if (empty($form_state['storage']['insert_counter'])) {
+ $form_state['storage']['insert_counter'] = 1;
+ }
+
+ $queue_name = !empty($form_state['values']['queue_name']) ? $form_state['values']['queue_name'] : 'queue_example_first_queue';
+ $items = queue_example_retrieve_queue($queue_name);
+
+ // Add CSS to make the form a bit denser.
+ $form['#attached']['css'] = array(drupal_get_path('module', 'queue_example') . '/queue_example.css');
+
+ $form['help'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('This page is an interface on the Drupal queue API. You can add new items to the queue, "claim" one (retrieve the next item and keep a lock on it), and delete one (remove it from the queue). Note that claims are not expired until cron runs, so there is a special button to run cron to perform any necessary expirations.') . '
',
+ );
+
+ $form['queue_name'] = array(
+ '#type' => 'select',
+ '#title' => t('Choose queue to examine'),
+ '#options' => drupal_map_assoc(array('queue_example_first_queue', 'queue_example_second_queue')),
+ '#default_value' => $queue_name,
+ );
+ $form['queue_show'] = array(
+ '#type' => 'submit',
+ '#value' => t('Show queue'),
+ '#submit' => array('queue_example_show_queue'),
+ );
+ $form['status_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Queue status for @name', array('@name' => $queue_name)),
+ '#collapsible' => TRUE,
+ );
+ $form['status_fieldset']['status'] = array(
+ '#type' => 'markup',
+ '#markup' => theme('queue_items', array('items' => $items)),
+ );
+ $form['insert_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Insert into @name', array('@name' => $queue_name)),
+ );
+ $form['insert_fieldset']['string_to_add'] = array(
+ '#type' => 'textfield',
+ '#size' => 10,
+ '#default_value' => t('item @counter', array('@counter' => $form_state['storage']['insert_counter'])),
+ );
+ $form['insert_fieldset']['add_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Insert into queue'),
+ '#submit' => array('queue_example_add_remove_form_insert'),
+ );
+ $form['claim_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Claim from queue'),
+ '#collapsible' => TRUE,
+ );
+
+ $form['claim_fieldset']['claim_time'] = array(
+ '#type' => 'radios',
+ '#title' => t('Claim time, in seconds'),
+ '#options' => array(
+ 0 => t('none'),
+ 5 => t('5 seconds'),
+ 60 => t('60 seconds'),
+ ),
+ '#description' => t('This time is only valid if cron runs during this time period. You can run cron manually below.'),
+ '#default_value' => !empty($form_state['values']['claim_time']) ? $form_state['values']['claim_time'] : 5,
+ );
+ $form['claim_fieldset']['claim_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Claim the next item from the queue'),
+ '#submit' => array('queue_example_add_remove_form_claim'),
+ );
+ $form['claim_fieldset']['claim_and_delete_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Claim the next item and delete it'),
+ '#submit' => array('queue_example_add_remove_form_delete'),
+ );
+ $form['claim_fieldset']['run_cron'] = array(
+ '#type' => 'submit',
+ '#value' => t('Run cron manually to expire claims'),
+ '#submit' => array('queue_example_add_remove_form_run_cron'),
+ );
+ $form['delete_queue'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete the queue and items in it'),
+ '#submit' => array('queue_example_add_remove_form_clear_queue'),
+ );
+ return $form;
+}
+
+/**
+ * Submit function for the insert-into-queue button.
+ */
+function queue_example_add_remove_form_insert($form, &$form_state) {
+ // Get a queue (of the default type) called 'queue_example_queue'.
+ // If the default queue class is SystemQueue this creates a queue that stores
+ // its items in the database.
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+
+ // Queue the string.
+ $queue->createItem($form_state['values']['string_to_add']);
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('Queued your string (@string_to_add). There are now @count items in the queue.', array('@count' => $count, '@string_to_add' => $form_state['values']['string_to_add'])));
+ // Setting 'rebuild' to TRUE allows us to keep information in $form_state.
+ $form_state['rebuild'] = TRUE;
+ // Unsetting the string_to_add allows us to set the incremented default value
+ // for the user so they don't have to type anything.
+ unset($form_state['input']['string_to_add']);
+ $form_state['storage']['insert_counter']++;
+}
+
+/**
+ * Submit function for the show-queue button.
+ */
+function queue_example_show_queue($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+
+ // Get the number of items.
+ $count = $queue->numberOfItems();
+
+ // Update the form item counter.
+ $form_state['storage']['insert_counter'] = $count + 1;
+
+ // Unset the string_to_add textbox.
+ unset($form_state['input']['string_to_add']);
+
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit function for the "claim" button.
+ *
+ * Claims (retrieves) an item from the queue and reports the results.
+ */
+function queue_example_add_remove_form_claim($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $item = $queue->claimItem($form_state['values']['claim_time']);
+ $count = $queue->numberOfItems();
+ if (!empty($item)) {
+ drupal_set_message(
+ t('Claimed item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.',
+ array(
+ '@count' => $count,
+ '@item_id' => $item->item_id,
+ '@string' => $item->data,
+ '@seconds' => $form_state['values']['claim_time'],
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('There were no items in the queue available to claim. There are @count items in the queue.', array('@count' => $count)));
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit function for "Claim and delete" button.
+ */
+function queue_example_add_remove_form_delete($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $count = $queue->numberOfItems();
+ $item = $queue->claimItem(60);
+ if (!empty($item)) {
+ drupal_set_message(
+ t('Claimed and deleted item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.',
+ array(
+ '@count' => $count,
+ '@item_id' => $item->item_id,
+ '@string' => $item->data,
+ '@seconds' => $form_state['values']['claim_time'],
+ )
+ )
+ );
+ $queue->deleteItem($item);
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('There are now @count items in the queue.', array('@count' => $count)));
+ }
+ else {
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('There were no items in the queue available to claim/delete. There are currently @count items in the queue.', array('@count' => $count)));
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit function for "run cron" button.
+ *
+ * Runs cron (to release expired claims) and reports the results.
+ */
+function queue_example_add_remove_form_run_cron($form, &$form_state) {
+ drupal_cron_run();
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('Ran cron. If claimed items expired, they should be expired now. There are now @count items in the queue', array('@count' => $count)));
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for clearing/deleting the queue.
+ */
+function queue_example_add_remove_form_clear_queue($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ $queue->deleteQueue();
+ drupal_set_message(t('Deleted the @queue_name queue and all items in it', array('@queue_name' => $form_state['values']['queue_name'])));
+}
+
+/**
+ * Retrieves the queue from the database for display purposes only.
+ *
+ * It is not recommended to access the database directly, and this is only here
+ * so that the user interface can give a good idea of what's going on in the
+ * queue.
+ *
+ * @param array $queue_name
+ * The name of the queue from which to fetch items.
+ */
+function queue_example_retrieve_queue($queue_name) {
+ $items = array();
+ $result = db_query("SELECT item_id, data, expire, created FROM {queue} WHERE name = :name ORDER BY item_id",
+ array(':name' => $queue_name),
+ array('fetch' => PDO::FETCH_ASSOC));
+ foreach ($result as $item) {
+ $items[] = $item;
+ }
+ return $items;
+}
+
+/**
+ * Themes the queue display.
+ *
+ * Again, this is not part of the demonstration of the queue API, but is here
+ * just to make the user interface more understandable.
+ *
+ * @param array $variables
+ * Our variables.
+ */
+function theme_queue_items($variables) {
+ $items = $variables['items'];
+ $rows = array();
+ foreach ($items as &$item) {
+ if ($item['expire'] > 0) {
+ $item['expire'] = t("Claimed: expires %expire", array('%expire' => date('r', $item['expire'])));
+ }
+ else {
+ $item['expire'] = t('Unclaimed');
+ }
+ $item['created'] = date('r', $item['created']);
+ $item['content'] = check_plain(unserialize($item['data']));
+ unset($item['data']);
+ $rows[] = $item;
+ }
+ if (!empty($rows)) {
+ $header = array(
+ t('Item ID'),
+ t('Claimed/Expiration'),
+ t('Created'),
+ t('Content/Data'),
+ );
+ $output = theme('table', array('header' => $header, 'rows' => $rows));
+ return $output;
+ }
+ else {
+ return t('There are no items in the queue.');
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function queue_example_theme() {
+ return array(
+ 'queue_items' => array(
+ 'variables' => array('items' => NULL),
+ ),
+ );
+}
+/**
+ * @} End of "defgroup queue_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/queue_example/queue_example.test b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.test
new file mode 100644
index 00000000..9901095e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/queue_example/queue_example.test
@@ -0,0 +1,75 @@
+ 'Queue Example functionality',
+ 'description' => 'Test Queue Example functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('queue_example');
+ }
+
+ /**
+ * Test the queue behavior through user interaction.
+ */
+ public function testQueueExampleBasic() {
+
+ // Load the queue with 5 items.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = array('queue_name' => 'queue_example_first_queue', 'string_to_add' => "boogie$i");
+ $this->drupalPost('queue_example/insert_remove', $edit, t('Insert into queue'));
+ $this->assertText(t('There are now @number items in the queue', array('@number' => $i)));
+ }
+ // Claim each of the 5 items with a claim time of 0 seconds.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item from the queue'));
+ $this->assertPattern(t('%Claimed item id=.*string=@string for 0 seconds.%', array('@string' => "boogie$i")));
+ }
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item from the queue'));
+ $this->assertText(t('There were no items in the queue available to claim'));
+
+ // Sleep a second so we can make sure that the timeouts actually time out.
+ // Local systems work fine with this but apparently the PIFR server is so
+ // fast that it needs a sleep before the cron run.
+ sleep(1);
+
+ // Run cron to release expired items.
+ $this->drupalPost(NULL, array(), t('Run cron manually to expire claims'));
+
+ $queue_items = queue_example_retrieve_queue('queue_example_first_queue');
+
+ // Claim and delete each of the 5 items which should now be available.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item and delete it'));
+ $this->assertPattern(t('%Claimed and deleted item id=.*string=@string for 0 seconds.%', array('@string' => "boogie$i")));
+ }
+ // Verify that nothing is left to claim.
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item from the queue'));
+ $this->assertText(t('There were no items in the queue available to claim'));
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.info b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.info
new file mode 100644
index 00000000..8059fa5a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.info
@@ -0,0 +1,12 @@
+name = RDF Example
+description = Demonstrates an RDF mapping using the RDF mapping API.
+package = Example modules
+core = 7.x
+files[] = rdf_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.install b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.install
new file mode 100644
index 00000000..3749d43a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.install
@@ -0,0 +1,129 @@
+ 'recipe',
+ 'name' => $t('Recipe'),
+ 'base' => 'node_content',
+ 'description' => $t('The recipe node is defined to demonstrate RDF mapping.'),
+ );
+
+ // Set additional defaults and save the content type.
+ $content_type = node_type_set_defaults($rdf_example);
+ node_type_save($content_type);
+
+ // Create all the fields we are adding to our content type.
+ // http://api.drupal.org/api/function/field_create_field/7
+ foreach (_rdf_example_installed_fields() as $field) {
+ field_create_field($field);
+ }
+
+ // Create all the instances for our fields.
+ // http://api.drupal.org/api/function/field_create_instance/7
+ foreach (_rdf_example_installed_instances() as $instance) {
+ $instance['entity_type'] = 'node';
+ $instance['bundle'] = $rdf_example['type'];
+ field_create_instance($instance);
+ }
+}
+
+/**
+ * Return a structured array defining the fields created by this content type.
+ *
+ * @ingroup rdf_example
+ */
+function _rdf_example_installed_fields() {
+ $t = get_t();
+ return array(
+ 'recipe_photo' => array(
+ 'field_name' => 'recipe_photo',
+ 'cardinality' => 1,
+ 'type' => 'image',
+ ),
+ 'recipe_summary' => array(
+ 'field_name' => 'recipe_summary',
+ 'cardinality' => 1,
+ 'type' => 'text',
+ 'settings' => array(
+ 'max_length' => 500,
+ ),
+ ),
+ );
+}
+
+/**
+ * Return a structured array defining the instances for this content type.
+ *
+ * @ingroup rdf_example
+ */
+function _rdf_example_installed_instances() {
+ $t = get_t();
+ return array(
+ 'recipe_photo' => array(
+ 'field_name' => 'recipe_photo',
+ 'label' => $t('Photo of the prepared dish'),
+ ),
+ 'recipe_summary' => array(
+ 'field_name' => 'recipe_summary',
+ 'label' => $t('Short summary describing the dish'),
+ 'widget' => array(
+ 'type' => 'text_textarea',
+ ),
+ ),
+ );
+}
+
+
+/**
+ * Implements hook_uninstall().
+ *
+ * @ingroup rdf_example
+ */
+function rdf_example_uninstall() {
+ // Delete recipe content.
+ $sql = 'SELECT nid FROM {node} n WHERE n.type = :type';
+ $result = db_query($sql, array(':type' => 'recipe'));
+ $nids = array();
+ foreach ($result as $row) {
+ $nids[] = $row->nid;
+ }
+ node_delete_multiple($nids);
+
+ // Delete field instances for now.
+ // Check status of http://drupal.org/node/1015846
+ $instances = field_info_instances('node', 'recipe');
+ foreach ($instances as $instance_name => $instance) {
+ field_delete_instance($instance);
+ }
+
+ // Delete node type.
+ node_type_delete('recipe');
+
+ field_purge_batch(1000);
+}
diff --git a/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.module b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.module
new file mode 100644
index 00000000..bdce13dd
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.module
@@ -0,0 +1,86 @@
+ 'node',
+ 'bundle' => 'recipe',
+ 'mapping' => array(
+ 'rdftype' => array('v:Recipe'),
+ // We don't use the default bundle mapping for title. Instead, we add
+ // the v:name property. We still want to use dc:title as well, though,
+ // so we include it in the array.
+ 'title' => array(
+ 'predicates' => array('dc:title', 'v:name'),
+ ),
+ 'recipe_summary' => array(
+ 'predicates' => array('v:summary'),
+ ),
+ // The photo URI isn't a string but instead points to a resource, so we
+ // indicate that the attribute type is rel. If type isn't specified, it
+ // defaults to property, which is used for string values.
+ 'recipe_photo' => array(
+ 'predicates' => array('v:photo'),
+ 'type' => 'rel',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_rdf_namespaces().
+ *
+ * This hook should be used to define any prefixes used by this module that are
+ * not already defined in core by rdf_rdf_namespaces.
+ *
+ * @see hook_rdf_namespaces()
+ */
+function rdf_example_rdf_namespaces() {
+ return array(
+ // Google's namespace for their custom vocabularies.
+ 'v' => 'http://rdf.data-vocabulary.org/#',
+ );
+}
+
+/**
+ * Implements hook_help().
+ */
+function rdf_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/rdf_example':
+ return "
" . t(
+ "The RDF Example module provides RDF mappings for a custom node type and
+ alters another node type's RDF mapping.
+ You can check your RDF using a parser by copying
+ and pasting your HTML source code into the box. For clearest results,
+ use Turtle as your output format.",
+ array('!parser' => url('http://www.w3.org/2007/08/pyRdfa/#distill_by_input'))
+ ) . "
";
+ }
+}
+/**
+ * @} End of "defgroup rdf_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.test b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.test
new file mode 100644
index 00000000..cde7cd94
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/rdf_example/rdf_example.test
@@ -0,0 +1,55 @@
+ 'RDFa markup',
+ 'description' => 'Test RDFa markup generation.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('rdf', 'field_test', 'rdf_example');
+ }
+
+ /**
+ * Test whether RDF mapping is define in markup.
+ *
+ * Create a recipe node and test whether the RDF mapping defined for this
+ * bundle is reflected in the markup.
+ */
+ public function testAttributesInMarkup() {
+ $node = $this->drupalCreateNode(array('type' => 'recipe'));
+ $this->drupalGet('node/' . $node->nid);
+ $iso_date = date('c', $node->changed);
+ $url = url('node/' . $node->nid);
+
+ // The title is mapped to dc:title and v:name and is exposed in a meta tag
+ // in the header.
+ $recipe_title = $this->xpath("//span[contains(@property, 'dc:title') and contains(@property, 'v:name') and @content='$node->title']");
+ $this->assertTrue(!empty($recipe_title), 'Title is exposed with dc:title and v:name in meta element.');
+
+ // Test that the type is applied and that the default mapping for date is
+ // used.
+ $recipe_meta = $this->xpath("//div[(@about='$url') and (@typeof='v:Recipe')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$iso_date']");
+ $this->assertTrue(!empty($recipe_meta), 'RDF type is present on post. Properties dc:date and dc:created are present on post date.');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/render_example/render_example.css b/sites/all/modules/contrib/dev/examples/render_example/render_example.css
new file mode 100644
index 00000000..6b2ae5f2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/render_example/render_example.css
@@ -0,0 +1,20 @@
+.render-array {
+ border: 2px solid black;
+ margin-top: 10px;
+ padding-left: 5px;
+ padding-top: 5px;
+}
+
+.render-header {
+ font-size: large;
+ font-style: italic;
+}
+
+.unrendered-label {
+ font-style: italic;
+ margin-top: 10px;
+}
+
+.rendered {
+ background-color: lightblue;
+}
diff --git a/sites/all/modules/contrib/dev/examples/render_example/render_example.info b/sites/all/modules/contrib/dev/examples/render_example/render_example.info
new file mode 100644
index 00000000..b4b6bd1b
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/render_example/render_example.info
@@ -0,0 +1,14 @@
+name = Render example
+description = Demonstrates drupal_render's capabilities and altering render arrays
+package = Example modules
+core = 7.x
+dependencies[] = devel
+stylesheets[all][] = render_example.css
+files[] = render_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/render_example/render_example.install b/sites/all/modules/contrib/dev/examples/render_example/render_example.install
new file mode 100644
index 00000000..d2aa2e87
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/render_example/render_example.install
@@ -0,0 +1,17 @@
+ 'Render Example',
+ 'page callback' => 'render_example_info',
+ 'access callback' => TRUE,
+ );
+ $items['examples/render_example/altering'] = array(
+ 'title' => 'Alter pages and blocks',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('render_example_demo_form'),
+ 'access arguments' => array('access devel information'),
+ );
+ $items['examples/render_example/arrays'] = array(
+ 'title' => 'Render array examples',
+ 'page callback' => 'render_example_arrays',
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+
+/**
+ * Simple basic information about the module; an entry point.
+ */
+function render_example_info() {
+ return t('The render example provides a
', array('!arrays' => url('examples/render_example/arrays'), '!alter' => url('examples/render_example/altering')));
+}
+
+
+/**
+ * Provides a number of render arrays and show what they do.
+ *
+ * Each array is keyed by a description; it's returned for rendering at page
+ * render time. It's easy to add new examples to this.
+ *
+ * The array items in $demos are intended to be raw, normal render arrays
+ * that can be experimented with to end up with different outcomes.
+ */
+function render_example_arrays() {
+
+ // Interval in seconds for cache update with #cache.
+ $interval = 60;
+
+ $demos = array(
+ // Demonstrate the simplest markup, a #markup element.
+ t('Super simple #markup') => array(
+ '#markup' => t('Some basic text in a #markup (shows basic markup and how it is rendered)'),
+ ),
+
+ // Shows how #prefix and #suffix can add markup into an array.
+ t('Using #prefix and #suffix') => array(
+ '#markup' => t('This one adds a prefix and suffix, which put a div around the item'),
+ '#prefix' => '
(prefix) ',
+ '#suffix' => ' (suffix)
',
+ ),
+
+ // When #theme is provided, it is the #theme function's job to figure out
+ // the meaning of the render array. The #theme function receives the entire
+ // element in $variables and must return it, where it will be the content
+ // of '#children'. When a #theme or other function is provided, custom
+ // properties can be invented and used as needed, as the #separator
+ // property provided here.
+ //
+ // If #theme is not provided, either explicitly or by the underlying
+ // element, then the children are rendered using their own properties and
+ // the results go into #children.
+ t('theme for an element') => array(
+ 'child' => array(
+ t('This is some text that should be put together'),
+ t('This is some more text that we need'),
+ ),
+ // An element we've created which will be used by our theming function.
+ '#separator' => ' | ',
+ '#theme' => 'render_example_aggregate',
+ ),
+
+ // #theme_wrappers provides an array of theme functions which theme the
+ // envelope or "wrapper" of a set of child elements. The theme function
+ // finds its element children (the sub-arrays) already rendered in
+ // '#children'.
+ t('theme_wrappers demonstration') => array(
+ 'child1' => array('#markup' => t('Markup for child1')),
+ 'child2' => array('#markup' => t('Markup for child2')),
+ '#theme_wrappers' => array('render_example_add_div', 'render_example_add_notes'),
+ ),
+
+ // Add '#pre_render' and '#post_render' handlers.
+ // - '#pre_render' functions get access to the array before it is rendered
+ // and can change it. This is similar to a theme function, but it is a
+ // specific fixed function and changes the array in place rather than
+ // rendering it..
+ // - '#post_render' functions get access to the rendered content, but also
+ // have the original array available.
+ t('pre_render and post_render') => array(
+ '#markup' => '
' . t('markup for pre_render and post_render example') . '
',
+ '#pre_render' => array('render_example_add_suffix'),
+ '#post_render' => array('render_example_add_prefix'),
+ ),
+
+ // Cache an element for $interval seconds using #cache.
+ // The assumption here is that this is an expensive item to render, perhaps
+ // large or otherwise expensive. Of course here it's just a piece of markup,
+ // so we don't get the value.
+ //
+ // #cache allows us to set
+ // - 'keys', an array of strings that will create the string cache key.
+ // - 'bin', the cache bin
+ // - 'expire', the expire timestamp. Note that this is actually limited
+ // to the granularity of a cron run.
+ // - 'granularity', a bitmask determining at what level the caching is done
+ // (user, role, page).
+ t('cache demonstration') => array(
+ // If your expensive function were to be executed here it would happen
+ // on every page load regardless of the cache. The actual markup is
+ // added via the #pre_render function, so that drupal_render() will only
+ // execute the expensive function if this array has not been cached.
+ '#markup' => '',
+ '#pre_render' => array('render_example_cache_pre_render'),
+ '#cache' => array(
+ 'keys' => array('render_example', 'cache', 'demonstration'),
+ 'bin' => 'cache',
+ 'expire' => time() + $interval,
+ 'granularity' => DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE,
+ ),
+ ),
+ );
+
+ // The rest of this function just places the above arrays in a context where
+ // they can be rendered (hopefully attractively and usefully) on the page.
+ $page_array = array();
+ foreach ($demos as $key => $item) {
+ $page_array[$key]['#theme_wrappers'] = array('render_array');
+ $page_array[$key]['#description'] = $key;
+
+ $page_array[$key]['unrendered'] = array(
+ '#prefix' => '
' . t('Unrendered array (as plain text and with a krumo version)') . ':
',
+ '#type' => 'markup',
+ '#markup' => htmlentities(drupal_var_export($item)),
+ );
+ $page_array[$key]['kpr'] = array(
+ // The kpr() function is from devel module and is here only allow us
+ // to output the array in a way that's easy to explore.
+ '#markup' => kpr($item, TRUE),
+ );
+ $page_array[$key]['hr'] = array('#markup' => '');
+ $page_array[$key]['rendered'] = array($item);
+ $page_array[$key]['rendered']['#prefix'] = '
Rendered version (light blue):
' . '
';
+ $page_array[$key]['rendered']['#suffix'] = '
';
+ }
+
+ return $page_array;
+}
+
+/**
+ * A '#pre_render' function.
+ *
+ * @param array $element
+ * The element which will be rendered.
+ *
+ * @return array
+ * The altered element. In this case we add the #markup.
+ */
+function render_example_cache_pre_render($element) {
+ $element['#markup'] = render_example_cache_expensive();
+
+ // The following line is due to the bug described in
+ // http://drupal.org/node/914792. A #markup element's #pre_render must set
+ // #children because it replaces the default #markup pre_render, which is
+ // drupal_pre_render_markup().
+ $element['#children'] = $element['#markup'];
+ return $element;
+}
+
+/**
+ * A potentially expensive function.
+ *
+ * @return string
+ * Some demo text.
+ */
+function render_example_cache_expensive() {
+ $interval = 60;
+ $time_message = t('The current time was %time when this was cached. Updated every %interval seconds', array('%time' => date('r'), '%interval' => $interval));
+ // Uncomment the following line to demonstrate that this function is not
+ // being run when the rendered array is cached.
+ // drupal_set_message($time_message);
+ return $time_message;
+}
+
+/**
+ * A '#pre_render' function.
+ *
+ * @param array $element
+ * The element which will be rendered.
+ *
+ * @return array
+ * The altered element. In this case we add a #prefix to it.
+ */
+function render_example_add_suffix($element) {
+ $element['#suffix'] = '
' . t('This #suffix was added by a #pre_render') . '
';
+
+ // The following line is due to the bug described in
+ // http://drupal.org/node/914792. A #markup element's #pre_render must set
+ // #children because it replaces the default #markup pre_render, which is
+ // drupal_pre_render_markup().
+ $element['#children'] = $element['#markup'];
+ return $element;
+}
+
+/**
+ * A '#post_render' function to add a little markup onto the end markup.
+ *
+ * @param string $markup
+ * The rendered element.
+ * @param array $element
+ * The element which was rendered (for reference)
+ *
+ * @return string
+ * Markup altered as necessary. In this case we add a little postscript to it.
+ */
+function render_example_add_prefix($markup, $element) {
+ $markup = '
This markup was added after rendering by a #post_render
' . $markup;
+ return $markup;
+}
+
+/**
+ * A #theme function.
+ *
+ * This #theme function has the responsibility of consolidating/rendering the
+ * children's markup and returning it, where it will be placed in the
+ * element's #children property.
+ */
+function theme_render_example_aggregate($variables) {
+ $output = '';
+ foreach (element_children($variables['element']['child']) as $item) {
+ $output .= $variables['element']['child'][$item] . $variables['element']['#separator'];
+ }
+ return $output;
+}
+
+/*************** Altering Section **************************
+ * The following section of the example builds and arranges the altering
+ * example.
+ */
+
+/**
+ * Builds the form that offers options of what items to show.
+ */
+function render_example_demo_form($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'markup',
+ '#markup' => t('This example shows what render arrays look like in the building of a page. It will not work unless the user running it has the "access devel information" privilege. It shows both the actual arrays used to build a page or block and also the capabilities of altering the page late in its lifecycle.'),
+ );
+
+ $form['show_arrays'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Show render arrays'),
+ );
+
+ foreach (array('block', 'page') as $type) {
+ $form['show_arrays']['render_example_show_' . $type] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show @type render arrays', array('@type' => $type)),
+ '#default_value' => variable_get('render_example_show_' . $type, FALSE),
+ );
+ }
+
+ $form['page_fiddling'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Make changes on page via hook_page_alter()'),
+ );
+ $form['page_fiddling']['render_example_note_about_render_arrays'] = array(
+ '#title' => t('Add a note about render arrays to top of sidebar_first (if it exists)'),
+ '#description' => t('Creates a simple render array that displays the use of #pre_render, #post_render, #theme, and #theme_wrappers.'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_note_about_render_arrays', FALSE),
+ );
+ $form['page_fiddling']['render_example_move_navigation_menu'] = array(
+ '#title' => t('Move the navigation menu to the top of the content area'),
+ '#description' => t('Uses hook_page_alter() to move the navigation menu into another region.'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_move_navigation_menu', FALSE),
+ );
+ $form['page_fiddling']['render_example_reverse_sidebar'] = array(
+ '#title' => t('Reverse ordering of sidebar_first elements (if it exists) - will affect the above'),
+ '#description' => t('Uses hook_page_alter() to reverse the ordering of items in sidebar_first'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_reverse_sidebar', FALSE),
+ );
+ $form['page_fiddling']['render_example_prefix'] = array(
+ '#title' => t('Use #prefix and #suffix to wrap a div around every block'),
+ '#description' => t('Uses hook_page_alter to wrap all blocks with a div using #prefix and #suffix'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_prefix'),
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Implements hook_page_alter().
+ *
+ * Alters the page in several different ways based on how the form has been
+ * configured.
+ */
+function render_example_page_alter(&$page) {
+
+ // Re-sort the sidebar in reverse order.
+ if (variable_get('render_example_reverse_sidebar', FALSE) && !empty($page['sidebar_first'])) {
+ $page['sidebar_first'] = array_reverse($page['sidebar_first']);
+ foreach (element_children($page['sidebar_first']) as $element) {
+ // Reverse the weights if they exist.
+ if (!empty($page['sidebar_first'][$element]['#weight'])) {
+ $page['sidebar_first'][$element]['#weight'] *= -1;
+ }
+ }
+ $page['sidebar_first']['#sorted'] = FALSE;
+ }
+
+ // Add a list of items to the top of sidebar_first.
+ // This shows how #theme and #theme_wrappers work.
+ if (variable_get('render_example_note_about_render_arrays', FALSE) && !empty($page['sidebar_first'])) {
+ $items = array(
+ t('Render arrays are everywhere in D7'),
+ t('Leave content unrendered as much as possible'),
+ t('This allows rearrangement and alteration very late in page cycle'),
+ );
+
+ $note = array(
+ '#title' => t('Render Array Example'),
+ '#items' => $items,
+
+ // The functions in #pre_render get to alter the actual data before it
+ // gets rendered by the various theme functions.
+ '#pre_render' => array('render_example_change_to_ol'),
+ // The functions in #post_render get both the element and the rendered
+ // data and can add to the rendered data.
+ '#post_render' => array('render_example_add_hr'),
+ // The #theme theme operation gets the first chance at rendering the
+ // element and its children.
+ '#theme' => 'item_list',
+ // Then the theme operations in #theme_wrappers can wrap more around
+ // what #theme left in #chilren.
+ '#theme_wrappers' => array('render_example_add_div', 'render_example_add_notes'),
+ '#weight' => -9999,
+ );
+ $page['sidebar_first']['render_array_note'] = $note;
+ $page['sidebar_first']['#sorted'] = FALSE;
+ }
+
+ // Move the navigation menu into the content area.
+ if (variable_get('render_example_move_navigation_menu', FALSE) && !empty($page['sidebar_first']['system_navigation']) && !empty($page['content'])) {
+ $page['content']['system_navigation'] = $page['sidebar_first']['system_navigation'];
+ $page['content']['system_navigation']['#weight'] = -99999;
+ unset($page['content']['#sorted']);
+ unset($page['sidebar_first']['system_navigation']);
+ }
+
+ // Show the render array used to build the page render array display.
+ if (variable_get('render_example_show_page', FALSE)) {
+ $form['render_example_page_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Page render array'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+ $form['render_example_page_fieldset']['markup'] = array(
+ // The kpr() function is from devel module and is here only allow us
+ // to output the array in a way that's easy to explore.
+ '#markup' => kpr($page, TRUE),
+ );
+ $page['content']['page_render_array'] = drupal_get_form('render_example_embedded_form', $form);
+ $page['content']['page_render_array']['#weight'] = -999999;
+ $page['content']['#sorted'] = FALSE;
+ }
+
+ // Add render array to the bottom of each block.
+ if (variable_get('render_example_show_block', FALSE)) {
+ foreach (element_children($page) as $region_name) {
+ foreach (element_children($page[$region_name]) as $block_name) {
+
+ // Push the block down a level so we can add another block after it.
+ $old_block = $page[$region_name][$block_name];
+ $page[$region_name][$block_name] = array(
+ $block_name => $old_block,
+ );
+ $form = array();
+ $form['render_example_block_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Block render array'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+
+ $form['render_example_block_fieldset']['markup'] = array(
+ '#type' => 'item',
+ '#title' => t('%blockname block render array', array('%blockname' => $block_name)),
+ // The kpr() function is from devel module and is here only allow us
+ // to output the array in a way that's easy to explore.
+ '#markup' => kpr($old_block, TRUE),
+ );
+
+ // Add the new block that contains the render array.
+ $page[$region_name][$block_name]['render_example_block_render_array'] = drupal_get_form('render_example_embedded_form', $form);
+ $page[$region_name][$block_name]['render_example_block_render_array']['#weight'] = 999;
+ }
+ }
+ }
+
+ // Add #prefix and #suffix to a block to wrap a div around it.
+ if (variable_get('render_example_prefix', FALSE)) {
+ foreach (element_children($page) as $region_name) {
+ foreach (element_children($page[$region_name]) as $block_name) {
+ $block = &$page[$region_name][$block_name];
+ $block['#prefix'] = '
Prefixed
';
+ $block['#suffix'] = 'Block suffix
';
+ }
+ }
+ }
+
+}
+
+/**
+ * Utility function to build a named form given a set of form elements.
+ *
+ * This is a standard form builder function that takes an additional array,
+ * which is itself a form.
+ *
+ * @param array $form
+ * Form API form array.
+ * @param array $form_state
+ * Form API form state array.
+ * @param array $form_items
+ * The form items to be included in this form.
+ */
+function render_example_embedded_form($form, &$form_state, $form_items) {
+ return $form_items;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function render_example_theme() {
+ $items = array(
+ 'render_example_add_div' => array(
+ 'render element' => 'element',
+ ),
+ 'render_example_add_notes' => array(
+ 'render element' => 'element',
+ ),
+ 'render_array' => array(
+ 'render element' => 'element',
+ ),
+ 'render_example_aggregate' => array(
+ 'render element' => 'element',
+ ),
+ );
+ return $items;
+}
+
+/**
+ * Wraps a div around the already-rendered #children.
+ */
+function theme_render_example_add_div($variables) {
+ $element = $variables['element'];
+ $output = '
';
+ return $output;
+}
+
+/**
+ * Wraps a div and add a little text after the rendered #children.
+ */
+function theme_render_example_add_notes($variables) {
+ $element = $variables['element'];
+ $output = '
';
+ $output .= $element['#children'];
+ $output .= '' . t('This is a note added by a #theme_wrapper') . '';
+ $output .= '
';
+ return $rendered;
+}
+
+/**
+ * Adds a #type to the element before it gets rendered.
+ *
+ * In this case, changes from the default 'ul' to 'ol'.
+ *
+ * @param array $element
+ * The element to be altered, in this case a list, ready for theme_item_list.
+ *
+ * @return array
+ * The altered list (with '#type')
+ */
+function render_example_change_to_ol($element) {
+ $element['#type'] = 'ol';
+ return $element;
+}
+
+/**
+ * Alter the rendered output after all other theming.
+ *
+ * This #post_render function gets to alter the rendered output after all
+ * theme functions have acted on it, and it receives the original data, so
+ * can make decisions based on that. In this example, no use is made of the
+ * passed-in $element.
+ *
+ * @param string $markup
+ * The already-rendered data
+ * @param array $element
+ * The data element that was rendered
+ *
+ * @return string
+ * The altered data.
+ */
+function render_example_add_hr($markup, $element) {
+ $output = $markup . '';
+ return $output;
+}
+/**
+ * @} End of "defgroup render_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/render_example/render_example.test b/sites/all/modules/contrib/dev/examples/render_example/render_example.test
new file mode 100644
index 00000000..744763f1
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/render_example/render_example.test
@@ -0,0 +1,150 @@
+ 'Render example functionality',
+ 'description' => 'Test Render Example',
+ 'group' => 'Examples',
+ 'dependencies' => array('devel'),
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('devel', 'render_example');
+ }
+
+
+ /**
+ * Assert that all of the xpaths in the array have results.
+ *
+ * @param array $xpath_array
+ * An array of xpaths, each of which must return something.
+ */
+ public function assertRenderResults($xpath_array) {
+ foreach ($xpath_array as $xpath) {
+ $result = $this->xpath($xpath);
+ $this->assertTrue(!empty($result), format_string('Found xpath %xpath', array('%xpath' => $xpath)));
+ }
+ }
+
+
+ /**
+ * Asserts that the string value of the result is the same as the passed text.
+ *
+ * @param array $xpath_array
+ * Array of keyed arrays of tests to be made. Each child array consists of
+ * $xpath => $expected_text
+ */
+ public function assertRenderedText($xpath_array) {
+ foreach ($xpath_array as $xpath => $text) {
+ $result = $this->xpath($xpath);
+ $this->assertTrue((string) $result[0][0] == $text, format_string('%ary selects text %text', array('%ary' => $xpath, '%text' => $text)));
+ }
+ }
+
+
+ /**
+ * Basic test of rendering through user interaction.
+ *
+ * Login user, create an example node, and test blog functionality through
+ * the admin and user interfaces.
+ */
+ public function testRenderExampleBasic() {
+
+ // Create a user that can access devel information and log in.
+ $web_user = $this->drupalCreateUser(array('access devel information', 'access content'));
+ $this->drupalLogin($web_user);
+
+ // Turn on the block render array display and make sure it shows up.
+ $edit = array(
+ 'render_example_show_block' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+
+ $xpath_array = array(
+ "//div[@id='sidebar-first']//fieldset[starts-with(@id, 'edit-render-example-block-fieldset')]",
+ '//*[@id="content"]//fieldset[contains(@id,"edit-render-example-block-fieldset")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Turn off block render array display and turn on the page render array
+ // display.
+ $edit = array(
+ 'render_example_show_page' => TRUE,
+ 'render_example_show_block' => FALSE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+
+ $xpath_array = array(
+ '//*[@id="content"]//fieldset[starts-with(@id,"edit-render-example-page-fieldset")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Add note about render arrays to the top of sidebar_first.
+ $edit = array(
+ 'render_example_note_about_render_arrays' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+ $xpath_array = array(
+ '//*[@id="sidebar-first"]//ol//li[starts-with(.,"Render arrays are everywhere")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Move the navigation menu to the top of the content area.
+ $edit = array(
+ 'render_example_move_navigation_menu' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+ $xpath_array = array(
+ '//*[@id="content"]//h2[starts-with(.,"Navigation")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Skip a test for reversing order of sidebar_first as I think it would
+ // be too fragile.
+ //
+ // Test the addition of #prefix and #suffix
+ $edit = array(
+ 'render_example_prefix' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+ $xpath_array = array(
+ '//*[@id="sidebar-first"]//*[contains(@class, "block-prefix")]/span[contains(@class, "block-suffix")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Test some rendering facets of the various render examples.
+ $this->drupalGet('examples/render_example/arrays');
+ $content = $this->xpath('//*[@class="render-array"][1]');
+
+ $xpath_array = array(
+ '//div[@class="rendered"][starts-with(.,"Some basic text in a #markup")]' => 'Some basic text in a #markup (shows basic markup and how it is rendered)',
+ '//div[@class="rendered"][starts-with(.,"This is some text that should be put to")]' => 'This is some text that should be put together | This is some more text that we need | ',
+ '//div[@class="rendered"][starts-with(.,"The current time was")]' => 'The current time was when this was cached. Updated every seconds',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"(prefix)This one")]' => '(prefix)This one adds a prefix and suffix, which put a div around the item(suffix)',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"markup for pre_")]' => 'markup for pre_render and post_render example',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"This markup was added")]' => 'This markup was added after rendering by a #post_render',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"This #suffix")]' => 'This #suffix was added by a #pre_render',
+ );
+ $this->assertRenderedText($xpath_array);
+
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.info b/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.info
new file mode 100644
index 00000000..838398ea
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.info
@@ -0,0 +1,13 @@
+name = SimpleTest Example
+description = Provides simpletest_example page node type.
+package = Example modules
+core = 7.x
+dependencies[] = simpletest
+files[] = simpletest_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.install b/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.install
new file mode 100644
index 00000000..0f412270
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.install
@@ -0,0 +1,29 @@
+ array(
+ 'name' => t('SimpleTest Example Node Type'),
+ 'base' => 'simpletest_example',
+ 'description' => t('simpletest_example page node type.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * In this case we're adding an addition permission that does the same
+ * as the one the node module offers, just to demonstrate this error.
+ */
+function simpletest_example_permission() {
+ $perms = array();
+ $perms['extra special edit any simpletest_example'] = array('title' => t('Extra special edit any SimpleTest Example'), 'description' => t('Extra special edit any SimpleTest Example'));
+ return $perms;
+}
+
+/**
+ * Implements hook_node_access().
+ *
+ * Demonstrates a bug that we'll find in our test.
+ *
+ * If this is running on the testbot, we don't want the error to show so will
+ * work around it by testing to see if we're in the 'checkout' directory.
+ */
+function simpletest_example_node_access($node, $op, $account) {
+ // Don't get involved if this isn't a simpletest_example node, etc.
+ $type = is_string($node) ? $node : $node->type;
+ if ($type != 'simpletest_example' || ($op != 'update' && $op != 'delete')) {
+ return NODE_ACCESS_IGNORE;
+ }
+
+ // This code has a BUG that we'll find in testing.
+ //
+ // This is the incorrect version we'll use to demonstrate test failure.
+ // The correct version should have ($op == 'update' || $op == 'delete').
+ // The author had mistakenly always tested with User 1 so it always
+ // allowed access and the bug wasn't noticed!
+ if (($op == 'delete') && (user_access('extra special edit any simpletest_example', $account) && ($account->uid == $node->uid))) {
+ return NODE_ACCESS_ALLOW;
+ }
+
+ return NODE_ACCESS_DENY;
+}
+
+/**
+ * Implements hook_form().
+ *
+ * Form for the node type.
+ */
+function simpletest_example_form($node, $form_state) {
+ $type = node_type_get_type($node);
+ $form = array();
+ if ($type->has_title) {
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => check_plain($type->title_label),
+ '#required' => TRUE,
+ '#default_value' => $node->title,
+ '#maxlength' => 255,
+ '#weight' => -5,
+ );
+ }
+ return $form;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides an explanation.
+ */
+function simpletest_example_menu() {
+ $items['examples/simpletest_example'] = array(
+ 'title' => 'Simpletest Example',
+ 'description' => 'Explain the simpletest example and allow the error logic to be executed.',
+ 'page callback' => '_simpletest_example_explanation',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Returns an explanation of this module.
+ */
+function _simpletest_example_explanation() {
+
+ $explanation = t("This Simpletest Example is designed to give an introductory tutorial to writing
+ a simpletest test. Please see the associated tutorial.");
+ return $explanation;
+}
+
+/**
+ * A simple self-contained function used to demonstrate unit tests.
+ *
+ * @see SimpletestUnitTestExampleTestCase
+ */
+function simpletest_example_empty_mysql_date($date_string) {
+ if (empty($date_string) || $date_string == '0000-00-00' || $date_string == '0000-00-00 00:00:00') {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * @} End of "defgroup simpletest_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.test b/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.test
new file mode 100644
index 00000000..403891e9
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/simpletest_example/simpletest_example.test
@@ -0,0 +1,265 @@
+ 'SimpleTest Example',
+ 'description' => 'Ensure that the simpletest_example content type provided functions properly.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Set up the test environment.
+ *
+ * This method is called once per test method, before the test is executed.
+ * It gives you a chance to control the setup of the test environment.
+ *
+ * If you need a different test environment, then you should create another
+ * test class which overloads DrupalWebTestCase::setUp() differently.
+ *
+ * @see DrupalWebTestCase::setUp()
+ */
+ public function setUp() {
+ // We call parent::setUp() with the list of modules we want to enable.
+ // This can be an array or just a list of arguments.
+ parent::setUp('simpletest_example');
+ // Create and log in our user. The user has the arbitrary privilege
+ // 'extra special edit any simpletest_example' which is provided by
+ // our module to grant access.
+ $this->privilegedUser = $this->drupalCreateUser(array('create simpletest_example content', 'extra special edit any simpletest_example'));
+ $this->drupalLogin($this->privilegedUser);
+ }
+
+ /**
+ * Create a simpletest_example node using the node form.
+ */
+ public function testSimpleTestExampleCreate() {
+ // Create node to edit.
+ $edit = array();
+ $edit['title'] = $this->randomName(8);
+ $edit["body[und][0][value]"] = $this->randomName(16);
+ $this->drupalPost('node/add/simpletest-example', $edit, t('Save'));
+ $this->assertText(t('SimpleTest Example Node Type @title has been created.', array('@title' => $edit['title'])));
+ }
+
+ /**
+ * Create a simpletest_example node and then see if our user can edit it.
+ */
+ public function testSimpleTestExampleEdit() {
+ $settings = array(
+ 'type' => 'simpletest_example',
+ 'title' => $this->randomName(32),
+ 'body' => array(LANGUAGE_NONE => array(array($this->randomName(64)))),
+ );
+ $node = $this->drupalCreateNode($settings);
+
+ // For debugging, we might output the node structure with $this->verbose()
+ // It would only be output if the testing settings had 'verbose' set.
+ $this->verbose('Node created: ' . var_export($node, TRUE));
+
+ // We'll run this test normally, but not on the testbot, as it would
+ // indicate that the examples module was failing tests.
+ if (!$this->runningOnTestbot()) {
+ // The debug() statement will output information into the test results.
+ // It can also be used in Drupal 7 anywhere in code and will come out
+ // as a drupal_set_message().
+ debug('We are not running on the PIFR testing server, so will go ahead and catch the failure.');
+ $this->drupalGet("node/{$node->nid}/edit");
+ // Make sure we don't get a 401 unauthorized response:
+ $this->assertResponse(200, 'User is allowed to edit the content.');
+
+ // Looking for title text in the page to determine whether we were
+ // successful opening edit form.
+ $this->assertText(t("@title", array('@title' => $settings['title'])), "Found title in edit form");
+ }
+ }
+
+ /**
+ * Detect if we're running on PIFR testbot.
+ *
+ * Skip intentional failure in that case. It happens that on the testbot the
+ * site under test is in a directory named 'checkout' or 'site_under_test'.
+ *
+ * @return bool
+ * TRUE if running on testbot.
+ */
+ public function runningOnTestbot() {
+ // @todo: Add this line back once the testbot variable is available.
+ // https://www.drupal.org/node/2565181
+ // return env('DRUPALCI');
+ return TRUE;
+ }
+}
+
+
+/**
+ * Although most core test cases are based on DrupalWebTestCase and are
+ * functional tests (exercising the web UI) we also have DrupalUnitTestCase,
+ * which executes much faster because a Drupal install does not have to be
+ * one. No environment is provided to a test case based on DrupalUnitTestCase;
+ * it must be entirely self-contained.
+ *
+ * @see DrupalUnitTestCase
+ *
+ * @ingroup simpletest_example
+ */
+class SimpleTestUnitTestExampleTestCase extends DrupalUnitTestCase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'SimpleTest Example unit tests',
+ 'description' => 'Test that simpletest_example_empty_mysql_date works properly.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Set up the test environment.
+ *
+ * Note that we use drupal_load() instead of passing our module dependency
+ * to parent::setUp(). That's because we're using DrupalUnitTestCase, and
+ * thus we don't want to install the module, only load it's code.
+ *
+ * Also, DrupalUnitTestCase can't actually install modules. This is by
+ * design.
+ */
+ public function setUp() {
+ drupal_load('module', 'simpletest_example');
+ parent::setUp();
+ }
+
+ /**
+ * Test simpletest_example_empty_mysql_date().
+ *
+ * Note that no environment is provided; we're just testing the correct
+ * behavior of a function when passed specific arguments.
+ */
+ public function testSimpleTestUnitTestExampleFunction() {
+ $result = simpletest_example_empty_mysql_date(NULL);
+ // Note that test assertion messages should never be translated, so
+ // this string is not wrapped in t().
+ $message = 'A NULL value should return TRUE.';
+ $this->assertTrue($result, $message);
+
+ $result = simpletest_example_empty_mysql_date('');
+ $message = 'An empty string should return TRUE.';
+ $this->assertTrue($result, $message);
+
+ $result = simpletest_example_empty_mysql_date('0000-00-00');
+ $message = 'An "empty" MySQL DATE should return TRUE.';
+ $this->assertTrue($result, $message);
+
+ $result = simpletest_example_empty_mysql_date(date('Y-m-d'));
+ $message = 'A valid date should return FALSE.';
+ $this->assertFalse($result, $message);
+ }
+}
+
+/**
+ * SimpleTestExampleMockModuleTestCase allows us to demonstrate how you can
+ * use a mock module to aid in functional testing in Drupal.
+ *
+ * If you have some functionality that's not intrinsic to the code under test,
+ * you can add a special mock module that only gets installed during test
+ * time. This allows you to implement APIs created by your module, or otherwise
+ * exercise the code in question.
+ *
+ * This test case class is very similar to SimpleTestExampleTestCase. The main
+ * difference is that we enable the simpletest_example_test module in the
+ * setUp() method. Then we can test for behaviors provided by that module.
+ *
+ * @see SimpleTestExampleTestCase
+ *
+ * @ingroup simpletest_example
+ */
+class SimpleTestExampleMockModuleTestCase extends DrupalWebTestCase {
+
+ /**
+ * Give display information to the SimpleTest system.
+ *
+ * getInfo() returns a keyed array of information for SimpleTest to show.
+ *
+ * It's a good idea to organize your tests consistently using the 'group'
+ * key.
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'SimpleTest Mock Module Example',
+ 'description' => "Ensure that we can modify SimpleTest Example's content types.",
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Set up the test environment.
+ *
+ * Note that we're enabling both the simpletest_example and
+ * simpletest_example_test modules.
+ */
+ public function setUp() {
+ // We call parent::setUp() with the list of modules we want to enable.
+ parent::setUp('simpletest_example', 'simpletest_example_test');
+ }
+
+ /**
+ * Test modifications made by our mock module.
+ *
+ * We create a simpletest_example node and then see if our submodule
+ * operated on it.
+ */
+ public function testSimpleTestExampleMockModule() {
+ // Create a user.
+ $test_user = $this->drupalCreateUser(array('access content'));
+ // Log them in.
+ $this->drupalLogin($test_user);
+ // Set up some content.
+ $settings = array(
+ 'type' => 'simpletest_example',
+ 'title' => $this->randomName(32),
+ 'body' => array(LANGUAGE_NONE => array(array($this->randomName(64)))),
+ );
+ // Create the content node.
+ $node = $this->drupalCreateNode($settings);
+ // View the node.
+ $this->drupalGet("node/{$node->nid}");
+ // Check that our module did it's thing.
+ $this->assertText(t('The test module did its thing.'), "Found evidence of test module.");
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/simpletest_example/tests/simpletest_example_test.info b/sites/all/modules/contrib/dev/examples/simpletest_example/tests/simpletest_example_test.info
new file mode 100644
index 00000000..3f032ee1
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/simpletest_example/tests/simpletest_example_test.info
@@ -0,0 +1,13 @@
+name = "SimpleTest Example Mock Module"
+description = "Mock module for the SimpleTest Example module."
+package = Example modules
+core = 7.x
+hidden = TRUE
+dependencies[] = simpletest_example
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/simpletest_example/tests/simpletest_example_test.module b/sites/all/modules/contrib/dev/examples/simpletest_example/tests/simpletest_example_test.module
new file mode 100644
index 00000000..dbaa9864
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/simpletest_example/tests/simpletest_example_test.module
@@ -0,0 +1,31 @@
+type == 'simpletest_example') {
+ $node->content['simpletest_example_test_section'] = array(
+ '#markup' => t('The test module did its thing.'),
+ '#weight' => -99,
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.info b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.info
new file mode 100644
index 00000000..d664f80c
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.info
@@ -0,0 +1,12 @@
+name = Tabledrag Example
+description = Demonstrates how to create tabledrag forms.
+package = Example modules
+core = 7.x
+files[] = tabledrag_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.install b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.install
new file mode 100644
index 00000000..cb10d43c
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.install
@@ -0,0 +1,151 @@
+ 'Stores some entries for our tabledrag fun.',
+ 'fields' => array(
+ 'id' => array(
+ 'description' => 'The primary identifier for each item',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'description' => 'A name for this item',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'description' => array(
+ 'description' => 'A description for this item',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'itemgroup' => array(
+ 'description' => 'The group this item belongs to',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'weight' => array(
+ 'description' => 'The sortable weight for this item',
+ 'type' => 'int',
+ 'length' => 11,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'pid' => array(
+ 'description' => 'The primary id of the parent for this item',
+ 'type' => 'int',
+ 'length' => 11,
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'depth' => array(
+ 'description' => 'The depth of this item within the tree',
+ 'type' => 'int',
+ 'size' => 'small',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('id'),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ *
+ * This datafills the example item info which will be used in the example.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_install() {
+ // Ensure translations don't break at install time.
+ $t = get_t();
+ // Insert some values into the database.
+ $rows = array(
+ array(
+ 'name' => $t('Item One'),
+ 'description' => $t('The first item'),
+ 'itemgroup' => $t('Group1'),
+ ),
+ array(
+ 'name' => $t('Item Two'),
+ 'description' => $t('The second item'),
+ 'itemgroup' => $t('Group1'),
+ ),
+ array(
+ 'name' => $t('Item Three'),
+ 'description' => $t('The third item'),
+ 'itemgroup' => $t('Group1'),
+ ),
+ array(
+ 'name' => $t('Item Four'),
+ 'description' => $t('The fourth item'),
+ 'itemgroup' => $t('Group2'),
+ ),
+ array(
+ 'name' => $t('Item Five'),
+ 'description' => $t('The fifth item'),
+ 'itemgroup' => $t('Group2'),
+ ),
+ array(
+ 'name' => $t('Item Six'),
+ 'description' => $t('The sixth item'),
+ 'itemgroup' => $t('Group2'),
+ ),
+ array(
+ 'name' => $t('Item Seven'),
+ 'description' => $t('The seventh item'),
+ 'itemgroup' => $t('Group3'),
+ ),
+ array(
+ 'name' => $t('A Root Node'),
+ 'description' => $t('This item cannot be nested under a parent item'),
+ 'itemgroup' => $t('Group3'),
+ ),
+ array(
+ 'name' => $t('A Leaf Item'),
+ 'description' => $t('This item cannot have child items'),
+ 'itemgroup' => $t('Group3'),
+ ),
+ );
+ if (db_table_exists('tabledrag_example')) {
+ foreach ($rows as $row) {
+ db_insert('tabledrag_example')->fields($row)->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * This removes the example data when the module is uninstalled.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_uninstall() {
+ db_drop_table('tabledrag_example');
+}
diff --git a/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.module b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.module
new file mode 100644
index 00000000..e6a8b5b2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.module
@@ -0,0 +1,84 @@
+' . t('The form here is a themed as a table that is sortable using tabledrag handles.') . '';
+ }
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * We'll let drupal_get_form() generate the form page for us, for both of
+ * these menu items.
+ *
+ * @see drupal_get_form()
+ */
+function tabledrag_example_menu() {
+ // Basic example with single-depth sorting.
+ $items['examples/tabledrag_example_simple'] = array(
+ 'title' => 'TableDrag example (simple)',
+ 'description' => 'Show a page with a sortable tabledrag form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('tabledrag_example_simple_form'),
+ 'access callback' => TRUE,
+ 'file' => 'tabledrag_example_simple_form.inc',
+ );
+ // Basic parent/child example.
+ $items['examples/tabledrag_example_parent'] = array(
+ 'title' => 'TableDrag example (parent/child)',
+ 'description' => 'Show a page with a sortable parent/child tabledrag form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('tabledrag_example_parent_form'),
+ 'access callback' => TRUE,
+ 'file' => 'tabledrag_example_parent_form.inc',
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * We need run our forms through custom theme functions in order to build the
+ * table structure which is required by tabledrag.js. Before we can use our
+ * custom theme functions, we need to implement hook_theme in order to register
+ * them with Drupal.
+ *
+ * We are defining our theme hooks with the same name as the form generation
+ * function so that Drupal automatically calls our theming function when the
+ * form is displayed.
+ */
+function tabledrag_example_theme() {
+ return array(
+ // Theme function for the 'simple' example.
+ 'tabledrag_example_simple_form' => array(
+ 'render element' => 'form',
+ 'file' => 'tabledrag_example_simple_form.inc',
+ ),
+ // Theme function for the 'parent/child' example.
+ 'tabledrag_example_parent_form' => array(
+ 'render element' => 'form',
+ 'file' => 'tabledrag_example_parent_form.inc',
+ ),
+ );
+}
+/**
+ * @} End of "defgroup tabledrag_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.test b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.test
new file mode 100644
index 00000000..a7ccfec2
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example.test
@@ -0,0 +1,46 @@
+ 'Tabledrag Example',
+ 'description' => 'Functional tests for the Tabledrag Example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('tabledrag_example');
+ }
+
+ /**
+ * Tests the menu paths defined in tabledrag_example module.
+ */
+ public function testTabledragExampleMenus() {
+ $paths = array(
+ 'examples/tabledrag_example_simple',
+ 'examples/tabledrag_example_parent',
+ );
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertResponse(200, '200 response for path: ' . $path);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example_parent_form.inc b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example_parent_form.inc
new file mode 100644
index 00000000..2b1e6ee4
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example_parent_form.inc
@@ -0,0 +1,333 @@
+id] = array(
+
+ // We'll use a form element of type '#markup' to display the item name.
+ 'name' => array(
+ '#markup' => $item->name,
+ ),
+
+ // We'll use a form element of type '#textfield' to display the item
+ // description, to demonstrate that form elements can be included in the
+ // table. We limit the input to 255 characters, which is the limit we
+ // set on the database field.
+ 'description' => array(
+ '#type' => 'textfield',
+ '#default_value' => $item->description,
+ '#size' => 20,
+ '#maxlength' => 255,
+ ),
+
+ // For parent/child relationships, we also need to add form items to
+ // store the current item's unique id and parent item's unique id.
+ //
+ // We would normally use a hidden element for this, but for this example
+ // we'll use a disabled textfield element called 'id' so that we can
+ // display the current item's id in the table.
+ //
+ // Because tabledrag modifies the #value of this element, we use
+ // '#default_value' instead of '#value' when defining a hidden element.
+ // Also, because tabledrag modifies '#value', we cannot use a markup
+ // element, which does not support the '#value' property. (Markup
+ // elements use the '#markup' property instead.)
+ 'id' => array(
+ // '#type' => 'hidden',
+ // '#default_value' => $item->id,
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#default_value' => $item->id,
+ '#disabled' => TRUE,
+ ),
+
+ // The same information holds true for the parent id field as for the
+ // item id field, described above.
+ 'pid' => array(
+ // '#type' => 'hidden',
+ // '#default_value' => $item->pid,
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#default_value' => $item->pid,
+ ),
+
+ // The 'weight' field will be manipulated as we move the items around in
+ // the table using the tabledrag activity. We use the 'weight' element
+ // defined in Drupal's Form API.
+ 'weight' => array(
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $item->weight,
+ '#delta' => 10,
+ '#title_display' => 'invisible',
+ ),
+
+ // We'll use a hidden form element to pass the current 'depth' of each
+ // item within our parent/child tree structure to the theme function.
+ // This will be used to calculate the initial amount of indentation to
+ // add before displaying any child item rows.
+ 'depth' => array(
+ '#type' => 'hidden',
+ '#value' => $item->depth,
+ ),
+ );
+ }
+
+ // Now we add our submit button, for submitting the form results.
+ //
+ // The 'actions' wrapper used here isn't strictly necessary for tabledrag,
+ // but is included as a Form API recommended practice.
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save Changes'));
+ return $form;
+}
+
+/**
+ * Theme callback for the tabledrag_example_parent_form form.
+ *
+ * The theme callback will format the $form data structure into a table and
+ * add our tabledrag functionality. (Note that drupal_add_tabledrag should be
+ * called from the theme layer, and not from a form declaration. This helps
+ * keep template files clean and readable, and prevents tabledrag.js from
+ * being added twice accidently.
+ *
+ * @ingroup tabledrag_example
+ */
+function theme_tabledrag_example_parent_form($variables) {
+ $form = $variables['form'];
+
+ // Initialize the variable which will store our table rows.
+ $rows = array();
+
+ // Iterate over each element in our $form['example_items'] array.
+ foreach (element_children($form['example_items']) as $id) {
+
+ // Before we add our 'weight' column to the row, we need to give the
+ // element a custom class so that it can be identified in the
+ // drupal_add_tabledrag call.
+ //
+ // This could also have been done during the form declaration by adding
+ // @code
+ // '#attributes' => array('class' => 'example-item-weight'),
+ // @endcode
+ // directly to the 'weight' element in tabledrag_example_simple_form().
+ $form['example_items'][$id]['weight']['#attributes']['class'] = array('example-item-weight');
+
+ // In the parent/child example, we must also set this same custom class on
+ // our id and parent_id columns (which could also have been done within
+ // the form declaration, as above).
+ $form['example_items'][$id]['id']['#attributes']['class'] = array('example-item-id');
+ $form['example_items'][$id]['pid']['#attributes']['class'] = array('example-item-pid');
+
+ // To support the tabledrag behaviour, we need to assign each row of the
+ // table a class attribute of 'draggable'. This will add the 'draggable'
+ // class to the
element for that row when the final table is
+ // rendered.
+ $class = array('draggable');
+
+ // We can add the 'tabledrag-root' class to a row in order to indicate
+ // that the row may not be nested under a parent row. In our sample data
+ // for this example, the description for the item with id '8' flags it as
+ // a 'root' item which should not be nested.
+ if ($id == '8') {
+ $class[] = 'tabledrag-root';
+ }
+
+ // We can add the 'tabledrag-leaf' class to a row in order to indicate
+ // that the row may not contain child rows. In our sample data for this
+ // example, the description for the item with id '9' flags it as a 'leaf'
+ // item which can not contain child items.
+ if ($id == '9') {
+ $class[] = 'tabledrag-leaf';
+ }
+
+ // If this is a child element, we need to add some indentation to the row,
+ // so that it appears nested under its parent. Our $depth parameter was
+ // calculated while building the tree in tabledrag_example_parent_get_data
+ $indent = theme('indentation', array('size' => $form['example_items'][$id]['depth']['#value']));
+ unset($form['example_items'][$id]['depth']);
+
+ // We are now ready to add each element of our $form data to the $rows
+ // array, so that they end up as individual table cells when rendered
+ // in the final table. We run each element through the drupal_render()
+ // function to generate the final html markup for that element.
+ $rows[] = array(
+ 'data' => array(
+ // Add our 'name' column, being sure to include our indentation.
+ $indent . drupal_render($form['example_items'][$id]['name']),
+ // Add our 'description' column.
+ drupal_render($form['example_items'][$id]['description']),
+ // Add our 'weight' column.
+ drupal_render($form['example_items'][$id]['weight']),
+ // Add our hidden 'id' column.
+ drupal_render($form['example_items'][$id]['id']),
+ // Add our hidden 'parent id' column.
+ drupal_render($form['example_items'][$id]['pid']),
+ ),
+ // To support the tabledrag behaviour, we need to assign each row of the
+ // table a class attribute of 'draggable'. This will add the 'draggable'
+ // class to the
element for that row when the final table is
+ // rendered.
+ 'class' => $class,
+ );
+ }
+
+ // We now define the table header values. Ensure that the 'header' count
+ // matches the final column count for your table.
+ //
+ // Normally, we would hide the headers on our hidden columns, but we are
+ // leaving them visible in this example.
+ // $header = array(t('Name'), t('Description'), '', '', '');
+ $header = array(t('Name'), t('Description'), t('Weight'), t('ID'), t('PID'));
+
+ // We also need to pass the drupal_add_tabledrag() function an id which will
+ // be used to identify the
element containing our tabledrag form.
+ // Because an element's 'id' should be unique on a page, make sure the value
+ // you select is NOT the same as the form ID used in your form declaration.
+ $table_id = 'example-items-table';
+
+ // We can render our tabledrag table for output.
+ $output = theme('table', array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => $table_id),
+ ));
+
+ // And then render any remaining form elements (such as our submit button).
+ $output .= drupal_render_children($form);
+
+ // We now call the drupal_add_tabledrag() function in order to add the
+ // tabledrag.js goodness onto our page.
+ //
+ // For our parent/child tree table, we need to pass it:
+ // - the $table_id of our
element (example-items-table),
+ // - the $action to be performed on our form items ('match'),
+ // - a string describing where $action should be applied ('parent'),
+ // - the $group value (pid column) class name ('example-item-pid'),
+ // - the $subgroup value (pid column) class name ('example-item-pid'),
+ // - the $source value (id column) class name ('example-item-id'),
+ // - an optional $hidden flag identifying if the columns should be hidden,
+ // - an optional $limit parameter to control the max parenting depth.
+ drupal_add_tabledrag($table_id, 'match', 'parent', 'example-item-pid', 'example-item-pid', 'example-item-id', FALSE);
+
+ // Because we also want to sort in addition to providing parenting, we call
+ // the drupal_add_tabledrag function again, instructing it to update the
+ // weight field as items at the same level are re-ordered.
+ drupal_add_tabledrag($table_id, 'order', 'sibling', 'example-item-weight', NULL, NULL, FALSE);
+
+ return $output;
+}
+
+/**
+ * Submit callback for the tabledrag_example_parent_form form.
+ *
+ * Updates the 'weight' column for each element in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_parent_form_submit($form, &$form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ foreach ($form_state['values']['example_items'] as $id => $item) {
+ db_query(
+ "UPDATE {tabledrag_example} SET weight = :weight, pid = :pid WHERE id = :id",
+ array(':weight' => $item['weight'], ':pid' => $item['pid'], ':id' => $id)
+ );
+ }
+}
+
+/**
+ * Retrives the tree structure from database, and sorts by parent/child/weight.
+ *
+ * The sorting should result in children items immediately following their
+ * parent items, with items at the same level of the hierarchy sorted by
+ * weight.
+ *
+ * The approach used here may be considered too database-intensive.
+ * Optimization of the approach is left as an exercise for the reader. :)
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_parent_get_data() {
+ // Get all 'root node' items (items with no parents), sorted by weight.
+ $rootnodes = db_query('SELECT id, name, description, weight, pid
+ FROM {tabledrag_example}
+ WHERE (pid = 0)
+ ORDER BY weight ASC');
+ // Initialize a variable to store our ordered tree structure.
+ $itemtree = array();
+ // Depth will be incremented in our _get_tree() function for the first
+ // parent item, so we start it at -1.
+ $depth = -1;
+ // Loop through the root nodes, and add their trees to the array.
+ foreach ($rootnodes as $parent) {
+ tabledrag_example_get_tree($parent, $itemtree, $depth);
+ }
+ return $itemtree;
+}
+
+/**
+ * Recursively adds to the $itemtree array, ordered by parent/child/weight.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_get_tree($parentitem, &$itemtree = array(), &$depth = 0) {
+ // Increase our $depth value by one.
+ $depth++;
+ // Set the current tree 'depth' for this item, used to calculate indentation.
+ $parentitem->depth = $depth;
+ // Add the parent item to the tree.
+ $itemtree[$parentitem->id] = $parentitem;
+ // Retrieve each of the children belonging to this parent.
+ $children = db_query('SELECT id, name, description, weight, pid
+ FROM {tabledrag_example}
+ WHERE (pid = :pid)
+ ORDER BY weight ASC',
+ array(':pid' => $parentitem->id));
+ foreach ($children as $child) {
+ // Make sure this child does not already exist in the tree, to avoid loops.
+ if (!in_array($child->id, array_keys($itemtree))) {
+ // Add this child's tree to the $itemtree array.
+ tabledrag_example_get_tree($child, $itemtree, $depth);
+ }
+ }
+ // Finished processing this tree branch. Decrease our $depth value by one
+ // to represent moving to the next branch.
+ $depth--;
+}
diff --git a/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example_simple_form.inc b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example_simple_form.inc
new file mode 100644
index 00000000..582f6b16
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tabledrag_example/tabledrag_example_simple_form.inc
@@ -0,0 +1,177 @@
+id] = array(
+
+ // We'll use a form element of type '#markup' to display the item name.
+ 'name' => array(
+ '#markup' => check_plain($item->name),
+ ),
+
+ // We'll use a form element of type '#textfield' to display the item
+ // description, which will allow the value to be changed via the form.
+ // We limit the input to 255 characters, which is the limit we set on
+ // the database field.
+ 'description' => array(
+ '#type' => 'textfield',
+ '#default_value' => check_plain($item->description),
+ '#size' => 20,
+ '#maxlength' => 255,
+ ),
+
+ // The 'weight' field will be manipulated as we move the items around in
+ // the table using the tabledrag activity. We use the 'weight' element
+ // defined in Drupal's Form API.
+ 'weight' => array(
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $item->weight,
+ '#delta' => 10,
+ '#title_display' => 'invisible',
+ ),
+ );
+ }
+
+ // Now we add our submit button, for submitting the form results.
+ //
+ // The 'actions' wrapper used here isn't strictly necessary for tabledrag,
+ // but is included as a Form API recommended practice.
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save Changes'));
+ return $form;
+}
+
+/**
+ * Theme callback for the tabledrag_example_simple_form form.
+ *
+ * The theme callback will format the $form data structure into a table and
+ * add our tabledrag functionality. (Note that drupal_add_tabledrag should be
+ * called from the theme layer, and not from a form declaration. This helps
+ * keep template files clean and readable, and prevents tabledrag.js from
+ * being added twice accidently.
+ *
+ * @return array
+ * The rendered tabledrag form
+ *
+ * @ingroup tabledrag_example
+ */
+function theme_tabledrag_example_simple_form($variables) {
+ $form = $variables['form'];
+
+ // Initialize the variable which will store our table rows.
+ $rows = array();
+
+ // Iterate over each element in our $form['example_items'] array.
+ foreach (element_children($form['example_items']) as $id) {
+
+ // Before we add our 'weight' column to the row, we need to give the
+ // element a custom class so that it can be identified in the
+ // drupal_add_tabledrag call.
+ //
+ // This could also have been done during the form declaration by adding
+ // '#attributes' => array('class' => 'example-item-weight'),
+ // directy to the 'weight' element in tabledrag_example_simple_form().
+ $form['example_items'][$id]['weight']['#attributes']['class'] = array('example-item-weight');
+
+ // We are now ready to add each element of our $form data to the $rows
+ // array, so that they end up as individual table cells when rendered
+ // in the final table. We run each element through the drupal_render()
+ // function to generate the final html markup for that element.
+ $rows[] = array(
+ 'data' => array(
+ // Add our 'name' column.
+ drupal_render($form['example_items'][$id]['name']),
+ // Add our 'description' column.
+ drupal_render($form['example_items'][$id]['description']),
+ // Add our 'weight' column.
+ drupal_render($form['example_items'][$id]['weight']),
+ ),
+ // To support the tabledrag behaviour, we need to assign each row of the
+ // table a class attribute of 'draggable'. This will add the 'draggable'
+ // class to the
element for that row when the final table is
+ // rendered.
+ 'class' => array('draggable'),
+ );
+ }
+
+ // We now define the table header values. Ensure that the 'header' count
+ // matches the final column count for your table.
+ $header = array(t('Name'), t('Description'), t('Weight'));
+
+ // We also need to pass the drupal_add_tabledrag() function an id which will
+ // be used to identify the
element containing our tabledrag form.
+ // Because an element's 'id' should be unique on a page, make sure the value
+ // you select is NOT the same as the form ID used in your form declaration.
+ $table_id = 'example-items-table';
+
+ // We can render our tabledrag table for output.
+ $output = theme('table', array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => $table_id),
+ ));
+
+ // And then render any remaining form elements (such as our submit button).
+ $output .= drupal_render_children($form);
+
+ // We now call the drupal_add_tabledrag() function in order to add the
+ // tabledrag.js goodness onto our page.
+ //
+ // For a basic sortable table, we need to pass it:
+ // - the $table_id of our
element,
+ // - the $action to be performed on our form items ('order'),
+ // - a string describing where $action should be applied ('siblings'),
+ // - and the class of the element containing our 'weight' element.
+ drupal_add_tabledrag($table_id, 'order', 'sibling', 'example-item-weight');
+
+ return $output;
+}
+
+/**
+ * Submit callback for the tabledrag_example_simple_form form.
+ *
+ * Updates the 'weight' column for each element in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_simple_form_submit($form, &$form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ foreach ($form_state['values']['example_items'] as $id => $item) {
+ db_query(
+ "UPDATE {tabledrag_example} SET weight = :weight WHERE id = :id",
+ array(':weight' => $item['weight'], ':id' => $id)
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.info b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.info
new file mode 100644
index 00000000..7d885f44
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.info
@@ -0,0 +1,12 @@
+name = Table Sort example
+description = Demonstrates how to create sortable output in a table.
+package = Example modules
+core = 7.x
+files[] = tablesort_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.install b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.install
new file mode 100644
index 00000000..11a89393
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.install
@@ -0,0 +1,78 @@
+ 1, 'alpha' => 'e', 'random' => '912cv21'),
+ array('numbers' => 2, 'alpha' => 'a', 'random' => '0kuykuh'),
+ array('numbers' => 3, 'alpha' => 'm', 'random' => 'fuye8734h'),
+ array('numbers' => 4, 'alpha' => 'w', 'random' => '80jsv772'),
+ array('numbers' => 5, 'alpha' => 'o', 'random' => 'd82sf-csj'),
+ array('numbers' => 6, 'alpha' => 's', 'random' => 'au832'),
+ array('numbers' => 7, 'alpha' => 'e', 'random' => 't982hkv'),
+ );
+
+ if (db_table_exists('tablesort_example')) {
+ foreach ($rows as $row) {
+ db_insert('tablesort_example')->fields($row)->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * It's good to clean up after ourselves
+ *
+ * @ingroup tablesort_example
+ */
+function tablesort_example_uninstall() {
+ db_drop_table('tablesort_example');
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * @ingroup tablesort_example
+ */
+function tablesort_example_schema() {
+ $schema['tablesort_example'] = array(
+ 'description' => 'Stores some values for sorting fun.',
+ 'fields' => array(
+ 'numbers' => array(
+ 'description' => 'This column simply holds numbers values',
+ 'type' => 'varchar',
+ 'length' => 2,
+ 'not null' => TRUE,
+ ),
+ 'alpha' => array(
+ 'description' => 'This column simply holds alpha values',
+ 'type' => 'varchar',
+ 'length' => 2,
+ 'not null' => TRUE,
+ ),
+ 'random' => array(
+ 'description' => 'This column simply holds random values',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array('numbers'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.module b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.module
new file mode 100644
index 00000000..25751552
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.module
@@ -0,0 +1,93 @@
+' . t('The layout here is a themed as a table that is sortable by clicking the header name.') . '';
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function tablesort_example_menu() {
+ $items['examples/tablesort_example'] = array(
+ 'title' => 'TableSort example',
+ 'description' => 'Show a page with a sortable table',
+ 'page callback' => 'tablesort_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Build the table render array.
+ *
+ * @return array
+ * A render array set for theming by theme_table().
+ */
+function tablesort_example_page() {
+ // We are going to output the results in a table with a nice header.
+ $header = array(
+ // The header gives the table the information it needs in order to make
+ // the query calls for ordering. TableSort uses the field information
+ // to know what database column to sort by.
+ array('data' => t('Numbers'), 'field' => 't.numbers'),
+ array('data' => t('Letters'), 'field' => 't.alpha'),
+ array('data' => t('Mixture'), 'field' => 't.random'),
+ );
+
+ // Using the TableSort Extender is what tells the the query object that we
+ // are sorting.
+ $query = db_select('tablesort_example', 't')
+ ->extend('TableSort');
+ $query->fields('t');
+
+ // Don't forget to tell the query object how to find the header information.
+ $result = $query
+ ->orderByHeader($header)
+ ->execute();
+
+ $rows = array();
+ foreach ($result as $row) {
+ // Normally we would add some nice formatting to our rows
+ // but for our purpose we are simply going to add our row
+ // to the array.
+ $rows[] = array('data' => (array) $row);
+ }
+
+ // Build the table for the nice output.
+ $build['tablesort_table'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ );
+
+ return $build;
+}
+
+/**
+ * @} End of "defgroup tablesort_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.test b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.test
new file mode 100644
index 00000000..5462c757
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/tablesort_example/tablesort_example.test
@@ -0,0 +1,66 @@
+ 'TableSort Example',
+ 'description' => 'Verify the tablesort functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('tablesort_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testTableSortPage() {
+ // No need to login for this test.
+ $this->drupalGet('examples/tablesort_example', array('query' => array('sort' => 'desc', 'order' => 'Numbers')));
+ $this->assertRaw('
+
7
e
t982hkv
', 'Ordered by Number descending');
+
+ $this->drupalGet('examples/tablesort_example', array('query' => array('sort' => 'asc', 'order' => 'Numbers')));
+ $this->assertRaw('
+
1
e
912cv21
', 'Ordered by Number ascending');
+
+ // Sort by Letters.
+ $this->drupalGet('examples/tablesort_example', array('query' => array('sort' => 'desc', 'order' => 'Letters')));
+ $this->assertRaw('
+
', 'Ordered by Mixture ascending');
+ }
+
+}
diff --git a/sites/all/modules/contrib/dev/examples/theming_example/theming-example-text-form.tpl.php b/sites/all/modules/contrib/dev/examples/theming_example/theming-example-text-form.tpl.php
new file mode 100644
index 00000000..e75ae89f
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/theming_example/theming-example-text-form.tpl.php
@@ -0,0 +1,29 @@
+
+ *
+ * The following snippet will print the contents of the $text_form_content
+ * array, hidden in the source of the page, for you to discover the individual
+ * element names.
+ *
+ * '; ?>
+ */
+?>
+
+
+
+
+
diff --git a/sites/all/modules/contrib/dev/examples/theming_example/theming_example.css b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.css
new file mode 100644
index 00000000..a24c698d
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.css
@@ -0,0 +1,11 @@
+/*
+ * style the list
+ * for OL you can have
+ * decimal | lower-roman | upper-roman | lower-alpha | upper-alpha
+ * for UL you can have
+ * disc | circle | square or an image eg url(x.png)
+ * you can also have 'none'
+ */
+ol.theming-example-list {
+ list-style-type: upper-alpha;
+}
diff --git a/sites/all/modules/contrib/dev/examples/theming_example/theming_example.info b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.info
new file mode 100644
index 00000000..af634c85
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.info
@@ -0,0 +1,12 @@
+name = Theming example
+description = An example module showing how to use theming.
+package = Example modules
+core = 7.x
+files[] = theming_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/theming_example/theming_example.module b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.module
new file mode 100644
index 00000000..b888033d
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.module
@@ -0,0 +1,386 @@
+ 'Theming Example',
+ 'description' => 'Some theming examples.',
+ 'page callback' => 'theming_example_page',
+ 'access callback' => TRUE,
+ 'access arguments' => array('access content'),
+ );
+ $items['examples/theming_example/theming_example_list_page'] = array(
+ 'title' => 'Theming a list',
+ 'page callback' => 'theming_example_list_page',
+ 'access arguments' => array('access content'),
+ 'weight' => 1,
+ );
+ $items['examples/theming_example/theming_example_select_form'] = array(
+ 'title' => 'Theming a form (select form)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('theming_example_select_form'),
+ 'access arguments' => array('access content'),
+ 'weight' => 2,
+ );
+ $items['examples/theming_example/theming_example_text_form'] = array(
+ 'title' => 'Theming a form (text form)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('theming_example_text_form'),
+ 'access arguments' => array('access content'),
+ 'weight' => 3,
+ );
+
+ return $items;
+
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * Defines the theming capabilities provided by this module.
+ */
+function theming_example_theme() {
+ return array(
+ 'theming_example_content_array' => array(
+ // We use 'render element' when the item to be passed is a self-describing
+ // render array (it will have #theme_wrappers)
+ 'render element' => 'element',
+ ),
+ 'theming_example_list' => array(
+ // We use 'variables' when the item to be passed is an array whose
+ // structure must be described here.
+ 'variables' => array(
+ 'title' => NULL,
+ 'items' => NULL,
+ ),
+ ),
+ 'theming_example_select_form' => array(
+ 'render element' => 'form',
+ ),
+ 'theming_example_text_form' => array(
+ 'render element' => 'form',
+ // In this one the rendering will be done by a template file
+ // (theming-example-text-form.tpl.php) instead of being rendered by a
+ // function. Note the use of dashes to separate words in place of
+ // underscores. The template file's extension is also left out so that
+ // it may be determined automatically depending on the template engine
+ // the site is using.
+ 'template' => 'theming-example-text-form',
+ ),
+ );
+}
+/**
+ * Initial landing page explaining the use of the module.
+ *
+ * We create a render array and specify the theme to be used through the use
+ * of #theme_wrappers. With all output, we aim to leave the content as a
+ * render array just as long as possible, so that other modules (or the theme)
+ * can alter it.
+ *
+ * @see render_example.module
+ * @see form_example_elements.inc
+ */
+function theming_example_page() {
+ $content[] = t('Some examples of pages and forms that are run through theme functions.');
+ $content[] = l(t('Simple page with a list'), 'examples/theming_example/theming_example_list_page');
+ $content[] = l(t('Simple form 1'), 'examples/theming_example/theming_example_select_form');
+ $content[] = l(t('Simple form 2'), 'examples/theming_example/theming_example_text_form');
+ $content['#theme_wrappers'] = array('theming_example_content_array');
+ return $content;
+}
+
+/**
+ * The list page callback.
+ *
+ * An example page where the output is supplied as an array which is themed
+ * into a list and styled with css.
+ *
+ * In this case we'll use the core-provided theme_item_list as a #theme_wrapper.
+ * Any theme need only override theme_item_list to change the behavior.
+ */
+function theming_example_list_page() {
+ $items = array(
+ t('First item'),
+ t('Second item'),
+ t('Third item'),
+ t('Fourth item'),
+ );
+
+ // First we'll create a render array that simply uses theme_item_list.
+ $title = t("A list returned to be rendered using theme('item_list')");
+ $build['render_version'] = array(
+ // We use #theme here instead of #theme_wrappers because theme_item_list()
+ // is the classic type of theme function that does not just assume a
+ // render array, but instead has its own properties (#type, #title, #items).
+ '#theme' => 'item_list',
+ // '#type' => 'ul', // The default type is 'ul'
+ // We can easily make sure that a css or js file is present using #attached.
+ '#attached' => array('css' => array(drupal_get_path('module', 'theming_example') . '/theming_example.css')),
+ '#title' => $title,
+ '#items' => $items,
+ '#attributes' => array('class' => array('render-version-list')),
+ );
+
+ // Now we'll create a render array which uses our own list formatter,
+ // theme('theming_example_list').
+ $title = t("The same list rendered by theme('theming_example_list')");
+ $build['our_theme_function'] = array(
+ '#theme' => 'theming_example_list',
+ '#attached' => array('css' => array(drupal_get_path('module', 'theming_example') . '/theming_example.css')),
+ '#title' => $title,
+ '#items' => $items,
+ );
+ return $build;
+}
+
+
+/**
+ * A simple form that displays a select box and submit button.
+ *
+ * This form will be be themed by the 'theming_example_select_form' theme
+ * handler.
+ */
+function theming_example_select_form($form, &$form_state) {
+ $options = array(
+ 'newest_first' => t('Newest first'),
+ 'newest_last' => t('Newest last'),
+ 'edited_first' => t('Edited first'),
+ 'edited_last' => t('Edited last'),
+ 'by_name' => t('By name'),
+ );
+ $form['choice'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => t('Choose which ordering you want'),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Go'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler for the select form.
+ *
+ * @param array $form
+ * Form API form array.
+ * @param array $form_state
+ * Form API form state array.
+ */
+function theming_example_select_form_submit($form, &$form_state) {
+ drupal_set_message(t('You chose %input', array('%input' => $form_state['values']['choice'])));
+}
+
+/**
+ * A simple form that displays a textfield and submit button.
+ *
+ * This form will be rendered by theme('form') (theme_form() by default)
+ * because we do not provide a theme function for it here.
+ */
+function theming_example_text_form($form, &$form_state) {
+ $form['text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Please input something!'),
+ '#required' => TRUE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Go'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler for the text form.
+ *
+ * @param array $form
+ * Form API form array.
+ * @param array $form_state
+ * Form API form state array.
+ */
+function theming_example_text_form_submit($form, &$form_state) {
+ drupal_set_message(t('You entered %input', array('%input' => $form_state['values']['text'])));
+}
+
+
+/**
+ * Theme a simple content array.
+ *
+ * This theme function uses the newer recommended format where a single
+ * render array is provided to the theme function.
+ */
+function theme_theming_example_content_array($variables) {
+ $element = $variables['element'];
+ $output = '';
+ foreach (element_children($element) as $count) {
+ if (!$count) {
+ // The first paragraph is bolded.
+ $output .= '
' . $element[$count] . '
';
+ }
+ else {
+ // Following paragraphs are just output as routine paragraphs.
+ $output .= '
' . $element[$count] . '
';
+ }
+ }
+ return $output;
+}
+
+/**
+ * Theming a simple list.
+ *
+ * This is just a simple wrapper around theme('item_list') but it's worth
+ * showing how a custom theme function can be implemented.
+ *
+ * @see theme_item_list()
+ */
+function theme_theming_example_list($variables) {
+ $title = $variables['title'];
+ $items = $variables['items'];
+
+ // Add the title to the list theme and
+ // state the list type. This defaults to 'ul'.
+ // Add a css class so that you can modify the list styling.
+ // We'll just call theme('item_list') to render.
+ $variables = array(
+ 'items' => $items,
+ 'title' => $title,
+ 'type' => 'ol',
+ 'attributes' => array('class' => 'theming-example-list'),
+ );
+ $output = theme('item_list', $variables);
+ return $output;
+}
+
+/**
+ * Theming a simple form.
+ *
+ * Since our form is named theming_example_select_form(), the default
+ * #theme function applied to is will be 'theming_example_select_form'
+ * if it exists. The form could also have specified a different
+ * #theme.
+ *
+ * Here we collect the title, theme it manually and
+ * empty the form title. We also wrap the form in a div.
+ */
+function theme_theming_example_select_form($variables) {
+ $form = $variables['form'];
+ $title = $form['choice']['#title'];
+ $form['choice']['#title'] = '';
+ $output = '' . $title . '';
+ $form['choice']['#prefix'] = '
';
+ $form['submit']['#suffix'] = '
';
+ $output .= drupal_render_children($form);
+ return $output;
+}
+
+/**
+ * Implements template_preprocess().
+ *
+ * We prepare variables for use inside the theming-example-text-form.tpl.php
+ * template file.
+ *
+ * In this example, we create a couple new variables, 'text_form' and
+ * 'text_form_content', that clean up the form output. Drupal will turn the
+ * array keys in the $variables array into variables for use in the template.
+ *
+ * So $variables['text_form'] becomes available as $text_form in the template.
+ *
+ * @see theming-example-text-form.tpl.php
+ */
+function template_preprocess_theming_example_text_form(&$variables) {
+ $variables['text_form_content'] = array();
+ $text_form_hidden = array();
+
+ // Each form element is rendered and saved as a key in $text_form_content, to
+ // give the themer the power to print each element independently in the
+ // template file. Hidden form elements have no value in the theme, so they
+ // are grouped into a single element.
+ foreach (element_children($variables['form']) as $key) {
+ $type = $variables['form'][$key]['#type'];
+ if ($type == 'hidden' || $type == 'token') {
+ $text_form_hidden[] = drupal_render($variables['form'][$key]);
+ }
+ else {
+ $variables['text_form_content'][$key] = drupal_render($variables['form'][$key]);
+ }
+ }
+ $variables['text_form_content']['hidden'] = implode($text_form_hidden);
+
+ // The entire form is then saved in the $text_form variable, to make it easy
+ // for the themer to print the whole form.
+ $variables['text_form'] = implode($variables['text_form_content']);
+}
+/**
+ * @} End of "defgroup theming_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/theming_example/theming_example.test b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.test
new file mode 100644
index 00000000..acf7445b
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/theming_example/theming_example.test
@@ -0,0 +1,66 @@
+ 'Theming Example',
+ 'description' => 'Verify theming example functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('theming_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testThemingPage() {
+ // No need to login for this test.
+ // Check that the main page has been themed (first line with ) and has
+ // content.
+ $this->drupalGet('examples/theming_example');
+ $this->assertRaw('Some examples of pages');
+ $this->assertRaw('examples/theming_example/theming_example_select_form">Simple form 1');
+
+ // Visit the list demonstration page and check that css gets loaded
+ // and do some spot checks on how the two lists were themed.
+ $this->drupalGet('examples/theming_example/theming_example_list_page');
+ $this->assertPattern('/@import.*theming_example.css/');
+ $first_ul = $this->xpath('//ul[contains(@class,"render-version-list")]');
+ $this->assertTrue($first_ul[0]->li[0] == 'First item');
+ $second_ul = $this->xpath('//ol[contains(@class,"theming-example-list")]');
+ $this->assertTrue($second_ul[0]->li[1] == 'Second item');
+
+ // Visit the select form page to do spot checks.
+ $this->drupalGet('examples/theming_example/theming_example_select_form');
+ // We did explicit theming to accomplish the below...
+ $this->assertRaw('Choose which ordering you want');
+ $this->assertRaw('
');
+ $this->assertNoPattern('/@import.*theming_example.css/');
+
+ // Visit the text form page and do spot checks.
+ $this->drupalGet('examples/theming_example/theming_example_text_form');
+ $this->assertText('Please input something!');
+ // If it were themed normally there would be a div wrapper in our pattern.
+ $this->assertPattern('%
\s* 'Token example',
+ 'description' => 'Test replacement tokens in real time.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('token_example_example_form'),
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ *
+ * @todo Remove this when the testbot can properly pick up dependencies
+ * for contrib modules.
+ */
+function token_example_entity_info_alter(&$info) {
+ if (isset($info['taxonomy_term'])) {
+ $info['taxonomy_term']['token type'] = 'term';
+ }
+ if (isset($info['taxonomy_vocabulary'])) {
+ $info['taxonomy_vocabulary']['token type'] = 'vocabulary';
+ }
+}
+
+/**
+ * Form builder; display lists of supported token entities and text to tokenize.
+ */
+function token_example_example_form($form, &$form_state) {
+ $entities = entity_get_info();
+ $token_types = array();
+
+ // Scan through the list of entities for supported token entities.
+ foreach ($entities as $entity => $info) {
+ $object_callback = "_token_example_get_{$entity}";
+ if (function_exists($object_callback) && $objects = $object_callback()) {
+ $form[$entity] = array(
+ '#type' => 'select',
+ '#title' => $info['label'],
+ '#options' => array(0 => t('Not selected')) + $objects,
+ '#default_value' => isset($form_state['storage'][$entity]) ? $form_state['storage'][$entity] : 0,
+ '#access' => !empty($objects),
+ );
+
+ // Build a list of supported token types based on the available entites.
+ if ($form[$entity]['#access']) {
+ $token_types[$entity] = !empty($info['token type']) ? $info['token type'] : $entity;
+ }
+ }
+ }
+
+ $form['text'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Enter your text here'),
+ '#default_value' => 'Hello [current-user:name]!',
+ );
+
+ // Display the results of tokenized text.
+ if (!empty($form_state['storage']['text'])) {
+ $form['text']['#default_value'] = $form_state['storage']['text'];
+
+ $data = array();
+ foreach ($entities as $entity => $info) {
+ if (!empty($form_state['storage'][$entity])) {
+ $objects = entity_load($entity, array($form_state['storage'][$entity]));
+ if ($objects) {
+ $data[$token_types[$entity]] = reset($objects);
+ }
+ }
+ }
+
+ // Display the tokenized text.
+ $form['text_tokenized'] = array(
+ '#type' => 'item',
+ '#title' => t('Result'),
+ '#markup' => token_replace($form_state['storage']['text'], $data),
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ if (module_exists('token')) {
+ $form['token_tree'] = array(
+ '#theme' => 'token_tree',
+ '#token_types' => $token_types,
+ );
+ }
+ else {
+ $form['token_tree'] = array(
+ '#markup' => '
' . t('Enable the Token module to view the available token browser.', array('@drupal-token' => 'http://drupal.org/project/token')) . '
',
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Submit callback; store the submitted values into storage.
+ */
+function token_example_example_form_submit($form, &$form_state) {
+ $form_state['storage'] = $form_state['values'];
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Builds a list of available content.
+ */
+function _token_example_get_node() {
+ if (!user_access('access content') && !user_access('bypass node access')) {
+ return array();
+ }
+
+ $node_query = db_select('node', 'n');
+ $node_query->fields('n', array('nid', 'title'));
+ $node_query->condition('n.status', NODE_PUBLISHED);
+ $node_query->orderBy('n.created', 'DESC');
+ $node_query->range(0, 10);
+ $node_query->addTag('node_access');
+ $nodes = $node_query->execute()->fetchAllKeyed();
+ $nodes = array_map('check_plain', $nodes);
+ return $nodes;
+}
+
+/**
+ * Builds a list of available comments.
+ */
+function _token_example_get_comment() {
+ if (!module_exists('comment') || (!user_access('access comments') && !user_access('administer comments'))) {
+ return array();
+ }
+
+ $comment_query = db_select('comment', 'c');
+ $comment_query->innerJoin('node', 'n', 'n.nid = c.nid');
+ $comment_query->fields('c', array('cid', 'subject'));
+ $comment_query->condition('n.status', NODE_PUBLISHED);
+ $comment_query->condition('c.status', COMMENT_PUBLISHED);
+ $comment_query->orderBy('c.created', 'DESC');
+ $comment_query->range(0, 10);
+ $comment_query->addTag('node_access');
+ $comments = $comment_query->execute()->fetchAllKeyed();
+ $comments = array_map('check_plain', $comments);
+ return $comments;
+}
+
+/**
+ * Builds a list of available user accounts.
+ */
+function _token_example_get_user() {
+ if (!user_access('access user profiles') &&
+ !user_access('administer users')) {
+ return array();
+ }
+
+ $account_query = db_select('users', 'u');
+ $account_query->fields('u', array('uid', 'name'));
+ $account_query->condition('u.uid', 0, '>');
+ $account_query->condition('u.status', 1);
+ $account_query->range(0, 10);
+ $accounts = $account_query->execute()->fetchAllKeyed();
+ $accounts = array_map('check_plain', $accounts);
+ return $accounts;
+}
+
+/**
+ * Builds a list of available taxonomy terms.
+ */
+function _token_example_get_taxonomy_term() {
+ $term_query = db_select('taxonomy_term_data', 'ttd');
+ $term_query->fields('ttd', array('tid', 'name'));
+ $term_query->range(0, 10);
+ $term_query->addTag('term_access');
+ $terms = $term_query->execute()->fetchAllKeyed();
+ $terms = array_map('check_plain', $terms);
+ return $terms;
+}
+
+/**
+ * Builds a list of available files.
+ */
+function _token_example_get_file() {
+ $file_query = db_select('file_managed', 'f');
+ $file_query->fields('f', array('fid', 'filename'));
+ $file_query->range(0, 10);
+ $files = $file_query->execute()->fetchAllKeyed();
+ $files = array_map('check_plain', $files);
+ return $files;
+}
+/**
+ * @} End of "defgroup token_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/token_example/token_example.test b/sites/all/modules/contrib/dev/examples/token_example/token_example.test
new file mode 100644
index 00000000..64a9cf14
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/token_example/token_example.test
@@ -0,0 +1,76 @@
+ 'Token example functionality',
+ 'description' => 'Verify the token example interface.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('token_example');
+ $this->webUser = $this->drupalCreateUser();
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Test interface.
+ */
+ public function testInterface() {
+ $filtered_id = db_query("SELECT format FROM {filter_format} WHERE name = 'Filtered HTML'")->fetchField();
+ $default_format_id = filter_default_format($this->webUser);
+
+ $this->drupalGet('examples/token');
+ $this->assertNoFieldByName('node');
+ $this->assertNoFieldByName('user');
+
+ $edit = array(
+ 'text' => 'User [current-user:name] is trying the token example!',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit'));
+ $this->assertText('User ' . $this->webUser->name . ' is trying the token example!');
+
+ // Create a node and then make the 'Plain text' text format the default.
+ $node = $this->drupalCreateNode(array('title' => 'Example node', 'status' => NODE_PUBLISHED));
+ db_update('filter_format')
+ ->fields(array('weight' => -10))
+ ->condition('name', 'Plain text')
+ ->execute();
+
+ $this->drupalGet('examples/token');
+
+ $edit = array(
+ 'text' => 'Would you like to view the [node:type-name] [node:title] with text format [node:body-format] (ID [node:body-format:id])?',
+ 'node' => $node->nid,
+ );
+ $this->drupalPost(NULL, $edit, t('Submit'));
+ $this->assertText('Would you like to view the Basic page Example node with text format Filtered HTML (ID ' . $filtered_id . ')?');
+
+ $edit = array(
+ 'text' => 'Your default text format is [default-format:name] (ID [default-format:id]).',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit'));
+ $this->assertText('Your default text format is Filtered HTML (ID ' . $default_format_id . ')');
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/token_example/token_example.tokens.inc b/sites/all/modules/contrib/dev/examples/token_example/token_example.tokens.inc
new file mode 100644
index 00000000..0aefa653
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/token_example/token_example.tokens.inc
@@ -0,0 +1,142 @@
+ t('Text formats'),
+ 'description' => t('Tokens related to text formats.'),
+ 'needs-data' => 'format',
+ );
+ $info['types']['default-format'] = array(
+ 'name' => t('Default text format'),
+ 'description' => t("Tokens related to the currently logged in user's default text format."),
+ 'type' => 'format',
+ );
+
+ // Tokens for the text format token type.
+ $info['tokens']['format']['id'] = array(
+ 'name' => t('ID'),
+ 'description' => t("The unique ID of the text format."),
+ );
+ $info['tokens']['format']['name'] = array(
+ 'name' => t('Name'),
+ 'description' => t("The name of the text format."),
+ );
+
+ // Node tokens.
+ $info['tokens']['node']['body-format'] = array(
+ 'name' => t('Body text format'),
+ 'description' => t("The name of the text format used on the node's body field."),
+ 'type' => 'format',
+ );
+
+ // Comment tokens.
+ if (module_exists('comment')) {
+ $info['tokens']['comment']['body-format'] = array(
+ 'name' => t('Body text format'),
+ 'description' => t("The name of the text format used on the comment's body field."),
+ 'type' => 'format',
+ );
+ }
+
+ return $info;
+}
+
+/**
+ * Implements hook_tokens().
+ *
+ * @ingroup token_example
+ */
+function token_example_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $replacements = array();
+ $sanitize = !empty($options['sanitize']);
+
+ // Text format tokens.
+ if ($type == 'format' && !empty($data['format'])) {
+ $format = $data['format'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'id':
+ // Since {filter_format}.format is an integer and not user-entered
+ // text, it does not need to ever be sanitized.
+ $replacements[$original] = $format->format;
+ break;
+
+ case 'name':
+ // Since the format name is user-entered text, santize when requested.
+ $replacements[$original] = $sanitize ? filter_xss($format->name) : $format->name;
+ break;
+ }
+ }
+ }
+
+ // Default format tokens.
+ if ($type == 'default-format') {
+ $default_id = filter_default_format();
+ $default_format = filter_format_load($default_id);
+ $replacements += token_generate('format', $tokens, array('format' => $default_format), $options);
+ }
+
+ // Node tokens.
+ if ($type == 'node' && !empty($data['node'])) {
+ $node = $data['node'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'body-format':
+ if ($items = field_get_items('node', $node, 'body')) {
+ $format = filter_format_load($items[0]['format']);
+ $replacements[$original] = $sanitize ? filter_xss($format->name) : $format->name;
+ }
+ break;
+ }
+ }
+
+ // Chained token relationships.
+ if ($format_tokens = token_find_with_prefix($tokens, 'body-format')) {
+ if ($items = field_get_items('node', $node, 'body')) {
+ $body_format = filter_format_load($items[0]['format']);
+ $replacements += token_generate('format', $format_tokens, array('format' => $body_format), $options);
+ }
+ }
+ }
+
+ // Comment tokens.
+ if ($type == 'comment' && !empty($data['comment'])) {
+ $comment = $data['comment'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'body-format':
+ if ($items = field_get_items('comment', $comment, 'comment_body')) {
+ $format = filter_format_load($items[0]['format']);
+ $replacements[$original] = $sanitize ? filter_xss($format->name) : $format->name;
+ }
+ break;
+ }
+ }
+
+ // Chained token relationships.
+ if ($format_tokens = token_find_with_prefix($tokens, 'body-format')) {
+ if ($items = field_get_items('comment', $comment, 'comment_body')) {
+ $body_format = filter_format_load($items[0]['format']);
+ $replacements += token_generate('format', $format_tokens, array('format' => $body_format), $options);
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.info b/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.info
new file mode 100644
index 00000000..99275870
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.info
@@ -0,0 +1,13 @@
+name = Trigger example
+description = An example showing how a module can provide triggers that can have actions associated with them.
+package = Example modules
+core = 7.x
+dependencies[] = trigger
+files[] = trigger_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.module b/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.module
new file mode 100644
index 00000000..13725470
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.module
@@ -0,0 +1,318 @@
+ array(
+ 'user_first_time_login' => array(
+ 'label' => t('After a user has logged in for the first time'),
+ ),
+ ),
+ 'trigger_example' => array(
+ 'triggersomething' => array(
+ 'label' => t('After the triggersomething button is clicked'),
+ ),
+ ),
+ );
+}
+
+/**
+ * Triggers are used most of the time to do something when an event happens.
+ * The most common type of event is a hook invocation,
+ * but that is not the only possibility.
+ */
+
+/**
+ * Trigger: triggersomething. Run actions associated with an arbitrary event.
+ *
+ * Here pressing a button is a trigger. We have defined a
+ * custom function as a trigger (trigger_example_triggersomething).
+ * It will ask for all actions attached to the 'triggersomething' event,
+ * prepare a basic 'context' for them
+ * and run all of them. This could have been implemented by a hook
+ * implementation, but in this demonstration, it will just be called in a
+ * form's submit.
+ *
+ * This function is executed during the submission of the example form defined
+ * in this module.
+ *
+ * @param array $options
+ * Array of arguments used to call the triggersomething function, if any.
+ */
+function trigger_example_triggersomething($options = array()) {
+ // Ask the trigger module for all actions enqueued for the 'triggersomething'
+ // trigger.
+ $aids = trigger_get_assigned_actions('triggersomething');
+ // Prepare a basic context, indicating group and "hook", and call all the
+ // actions with this context as arguments.
+ $context = array(
+ 'group' => 'trigger_example',
+ 'hook' => 'triggersomething',
+ );
+ actions_do(array_keys($aids), (object) $options, $context);
+}
+
+
+/**
+ * The next trigger is more complex, we are providing a trigger for a
+ * new event: "user first time login". We need to create this event
+ * first.
+ */
+
+/**
+ * Implements hook_user_login().
+ *
+ * User first login trigger: Run actions on user first login.
+ *
+ * The event "User first time login" does not exist, we should create it before
+ * it can be used. We use hook_user_login to be informed when a user logs in and
+ * try to find if the user has previously logged in before. If the user has not
+ * accessed previously, we make a call to our trigger function.
+ */
+function trigger_example_user_login(&$edit, $account, $category = NULL) {
+ // Verify user has never accessed the site: last access was creation date.
+ if ($account->access == 0) {
+ // Call the aproppriate trigger function.
+ _trigger_example_first_time_login('user_first_time_login', $edit, $account, $category);
+ }
+}
+
+/**
+ * Trigger function for "User first time login".
+ *
+ * This trigger is a user-type triggers, so is grouped with other user-type
+ * triggers. It needs to provide all the context that user-type triggers
+ * provide. For this example, we are going to copy the trigger.module
+ * implementation for the 'User has logged in' event.
+ *
+ * This function will run all the actions assigned to the
+ * 'user_first_time_login' trigger.
+ *
+ * For testing you can use an update query like this to reset a user to
+ * "never logged in":
+ * @code
+ * update users set access=created where name='test1';
+ * @endcode
+ *
+ * @param string $hook
+ * The trigger identification.
+ * @param array $edit
+ * Modifications for the account object (should be empty).
+ * @param object $account
+ * User object that has logged in.
+ * @param string $category
+ * Category of the profile.
+ */
+function _trigger_example_first_time_login($hook, &$edit, $account, $category = NULL) {
+ // Keep objects for reuse so that changes actions make to objects can persist.
+ static $objects;
+ // Get all assigned actions for the 'user_first_time_login' trigger.
+ $aids = trigger_get_assigned_actions($hook);
+ $context = array(
+ 'group' => 'user',
+ 'hook' => $hook,
+ 'form_values' => &$edit,
+ );
+ // Instead of making a call to actions_do for all triggers, doing this loop
+ // we provide the opportunity for actions to alter the account object, and
+ // the next action should have this altered account object as argument.
+ foreach ($aids as $aid => $info) {
+ $type = $info['type'];
+ if ($type != 'user') {
+ if (!isset($objects[$type])) {
+ $objects[$type] = _trigger_normalize_user_context($type, $account);
+ }
+ $context['user'] = $account;
+ actions_do($aid, $objects[$type], $context);
+ }
+ else {
+ actions_do($aid, $account, $context, $category);
+ }
+ }
+}
+
+/**
+ * Helper functions for the module interface to test the triggersomething
+ * trigger.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function trigger_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/trigger_example':
+ $explanation = t(
+ 'Click the button on this page to call trigger_example_triggersomething()
+ and fire the triggersomething event. First, you need to create an action
+ and assign it to the "After the triggersomething button is clicked" trigger,
+ or nothing will happen. Use the Actions settings page
+ and assign these actions to the triggersomething event on the
+ Triggers settings page.
+ The other example is the "user never logged in before" example. For that one,
+ assign an action to the "After a user has logged in for the first time" trigger
+ and then log a user in.', array('@actions-url' => url('admin/config/system/actions'), '@triggers-url' => url('admin/structure/trigger/trigger_example')));
+ return "
$explanation
";
+
+ case 'admin/structure/trigger/system':
+ return t('you can assign actions to run everytime an email is sent by Drupal');
+
+ case 'admin/structure/trigger/trigger_example':
+ $explanation = t(
+ "A trigger is a system event. For the trigger example, it's just a button-press.
+ To demonstrate the trigger example, choose to associate the 'display a message to the user'
+ action with the 'after the triggersomething button is pressed' trigger."
+ );
+ return "
$explanation
";
+ }
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a form that can be used to fire the module's triggers.
+ */
+function trigger_example_menu() {
+ $items['examples/trigger_example'] = array(
+ 'title' => 'Trigger Example',
+ 'description' => 'Provides a form to demonstrate the trigger example.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('trigger_example_form'),
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Trigger example test form.
+ *
+ * Provides a button to run the triggersomething event.
+ */
+function trigger_example_form($form_state) {
+ $form['triggersomething'] = array(
+ '#type' => 'submit',
+ '#value' => t('Run triggersomething event'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler for the trigger_example_form().
+ */
+function trigger_example_form_submit($form, $form_state) {
+ // If the user clicked the button, then run the triggersomething trigger.
+ if ($form_state['values']['op'] == t('Run triggersomething event')) {
+ trigger_example_triggersomething();
+ }
+}
+
+
+/**
+ * Optional usage of hook_trigger_info_alter().
+ *
+ * This function is not required to write your own triggers, but it may be
+ * useful when you want to alter existing triggers.
+ */
+
+/**
+ * Implements hook_trigger_info_alter().
+ *
+ * We call hook_trigger_info_alter when we want to change an existing trigger.
+ * As mentioned earlier, this hook is not required to create your own triggers,
+ * and should only be used when you need to alter current existing triggers. In
+ * this example implementation a little change is done to the existing trigger
+ * provided by core: 'cron'
+ *
+ * @see hook_trigger_info()
+ */
+function trigger_example_trigger_info_alter(&$triggers) {
+ // Make a simple change to an existing core trigger, altering the label
+ // "When cron runs" to our custom label "On cron execution"
+ $triggers['system']['cron']['label'] = t('On cron execution');
+}
diff --git a/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.test b/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.test
new file mode 100644
index 00000000..0c4c8c53
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/trigger_example/trigger_example.test
@@ -0,0 +1,89 @@
+ 'Trigger example',
+ 'description' => 'Perform various tests on trigger_example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('trigger', 'trigger_example');
+ }
+
+ /**
+ * Test assigning a configurable action to the triggersomething event.
+ */
+ public function testTriggersomethingEvent() {
+ // Create an administrative user.
+ $test_user = $this->drupalCreateUser(array('administer actions'));
+ $this->drupalLogin($test_user);
+
+ // Create a configurable action for display a message to the user.
+ $hash = drupal_hash_base64('system_message_action');
+ $action_label = $this->randomName();
+ $edit = array(
+ 'actions_label' => $action_label,
+ 'message' => $action_label,
+ );
+ $this->drupalPost('admin/config/system/actions/configure/' . $hash, $edit, t('Save'));
+ $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'system_message_action'))->fetchField();
+ // $aid is likely 3 but if we add more uses for the sequences table in
+ // core it might break, so it is easier to get the value from the database.
+ $edit = array('aid' => drupal_hash_base64($aid));
+
+ // Note that this only works because there's just one item on the page.
+ $this->drupalPost('admin/structure/trigger/trigger_example', $edit, t('Assign'));
+
+ // Request triggersomething form and submit.
+ $this->drupalPost('examples/trigger_example', array(), t('Run triggersomething event'));
+ // Verify the message is shown to the user.
+ $this->assertText($action_label, 'The triggersomething event executed the action.');
+ }
+
+ /**
+ * Test triggers at user login.
+ */
+ public function testUserLogin() {
+ // Create an administrative user.
+ $admin_user = $this->drupalCreateUser(array('administer actions'));
+ $this->drupalLogin($admin_user);
+
+ // Create a configurable action for display a message to the user.
+ $hash = drupal_hash_base64('system_message_action');
+ $action_label = $this->randomName();
+ $edit = array(
+ 'actions_label' => $action_label,
+ 'message' => $action_label,
+ );
+ $this->drupalPost('admin/config/system/actions/configure/' . $hash, $edit, t('Save'));
+ $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'system_message_action'))->fetchField();
+ $edit = array('aid' => drupal_hash_base64($aid));
+
+ // Find the correct trigger.
+ $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-first-time-login-assign-form');
+
+ $test_user = $this->drupalCreateUser();
+ $this->drupalLogin($test_user);
+ $this->assertText($action_label);
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.info b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.info
new file mode 100644
index 00000000..3e18949e
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.info
@@ -0,0 +1,12 @@
+name = Vertical tabs example
+description = Show how to use vertical tabs for enhancing user experience.
+package = Example modules
+core = 7.x
+files[] = vertical_tabs_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.js b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.js
new file mode 100644
index 00000000..b578020a
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.js
@@ -0,0 +1,23 @@
+(function ($) {
+
+/**
+ * Update the summary for the module's vertical tab.
+ */
+Drupal.behaviors.vertical_tabs_exampleFieldsetSummaries = {
+ attach: function (context) {
+ // Use the fieldset class to identify the vertical tab element
+ $('fieldset#edit-vertical-tabs-example', context).drupalSetSummary(function (context) {
+ // Depending on the checkbox status, the settings will be customized, so
+ // update the summary with the custom setting textfield string or a use a
+ // default string.
+ if ($('#edit-vertical-tabs-example-enabled', context).attr('checked')) {
+ return Drupal.checkPlain($('#edit-vertical-tabs-example-custom-setting', context).val());
+ }
+ else {
+ return Drupal.t('Using default');
+ }
+ });
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.module b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.module
new file mode 100644
index 00000000..571b96c6
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.module
@@ -0,0 +1,114 @@
+ 'Vertical tabs example',
+ 'description' => 'Shows how vertical tabs can best be supported by a custom module',
+ 'page callback' => '_vertical_tabs_example_explanation',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_form_alter().
+ *
+ * Adds custom fieldset to the node form, and attach ajax behaviour for vertical
+ * panels to update the settings description.
+ *
+ * @see vertical_tabs_example.js
+ */
+function vertical_tabs_example_form_alter(&$form, $form_state, $form_id) {
+ // Only include on node add/edit forms.
+ if (!empty($form['#node_edit_form'])) {
+
+ // Create a fieldset that will be included in the vertical tab.
+ $form['vertical_tabs_example'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Example vertical tab'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#tree' => TRUE,
+ // Send this tab to the top of the list.
+ '#weight' => -99,
+ // The #group value must match the name of the vertical tabs element.
+ // In most cases, this is 'additional_settings'.
+ '#group' => 'additional_settings',
+ // Vertical tabs provide its most usable appearance when they are used to
+ // include a summary of the information contained in the fieldset. To do
+ // this, we attach additional JavaScript to handle changing the summary
+ // when form settings are changed.
+ '#attached' => array(
+ 'js' => array(
+ 'vertical-tabs' => drupal_get_path('module', 'vertical_tabs_example') . '/vertical_tabs_example.js',
+ ),
+ ),
+ );
+
+ // The form elements below provide a demonstration of how a fieldset
+ // summary can be displayed in a collapsed tab.
+ //
+ // This checkbox is used to show or hide the custom settings form using
+ // javascript (altering states of a container defined later).
+ $form['vertical_tabs_example']['enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Change this setting'),
+ '#default_value' => FALSE,
+ );
+
+ // This container will be used to store the whole form for our custom
+ // settings. This way, showing/hiding the form using javascript is easier,
+ // as only one element should be set visible.
+ $form['vertical_tabs_example']['vertical_tabs_examplecontainer'] = array(
+ '#type' => 'container',
+ '#parents' => array('vertical_tabs_example'),
+ '#states' => array(
+ 'invisible' => array(
+ // If the checkbox is not enabled, show the container.
+ 'input[name="vertical_tabs_example[enabled]"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+
+ // The string of this textfield will be shown as summary in the vertical
+ // tab.
+ $form['vertical_tabs_example']['vertical_tabs_examplecontainer']['custom_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Use this setting instead'),
+ '#default_value' => 'I am a setting with a summary',
+ '#description' => t('As you type into this field, the summary will be updated in the tab.'),
+ );
+ }
+}
+
+/**
+ * Simple explanation page.
+ */
+function _vertical_tabs_example_explanation() {
+ return t("
The Vertical Tabs Example shows how a custom module can add a vertical tab to a node edit form, and support its summary field with JavaScript.
To see the effects of this module, add a piece of content and look at the set of tabs at the bottom. We've added one called 'Example vertical tab.'
", array('!node_add' => url('node/add')));
+}
+/**
+ * @} End of "defgroup vertical_tabs_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.test b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.test
new file mode 100644
index 00000000..41e69d80
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/vertical_tabs_example/vertical_tabs_example.test
@@ -0,0 +1,45 @@
+ 'Vertical Tabs Example',
+ 'description' => 'Functional tests for the Vertical Tabs Example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('vertical_tabs_example');
+ }
+
+ /**
+ * Tests the menu paths defined in vertical_tabs_example module.
+ */
+ public function testVerticalTabsExampleMenus() {
+ $paths = array(
+ 'examples/vertical_tabs',
+ );
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertResponse(200, '200 response for path: ' . $path);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.info b/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.info
new file mode 100644
index 00000000..9c56d8d3
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.info
@@ -0,0 +1,12 @@
+name = XMLRPC example
+description = This is an example of how to implement client and server communications using XML-RPC.
+package = Example modules
+core = 7.x
+files[] = xmlrpc_example.test
+
+; Information added by Drupal.org packaging script on 2016-09-18
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1474218553"
+
diff --git a/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.module b/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.module
new file mode 100644
index 00000000..4d24e098
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.module
@@ -0,0 +1,707 @@
+ 'XML-RPC Example',
+ 'description' => 'Information about the XML-RPC example',
+ 'page callback' => 'xmlrpc_example_info',
+ 'access callback' => TRUE,
+ );
+ // This is the server configuration form menu entry. This form can be used to
+ // configure the settings of the exposed services. An XML-RPC server does not
+ // require a configuration form, and has been included here as an example.
+ $items['examples/xmlrpc/server'] = array(
+ 'title' => 'XML-RPC Server configuration',
+ 'description' => 'Server configuration form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('xmlrpc_example_server_form'),
+ 'access callback' => TRUE,
+ 'weight' => 0,
+ );
+ // This is the client form menu entry. This form is used to allow user
+ // interaction with the services, but again, user interface is not required
+ // to create an XML-RPC client with Drupal.
+ $items['examples/xmlrpc/client'] = array(
+ 'title' => 'XML-RPC Client form',
+ 'description' => 'Demonstrates client side XML-RPC calls with Drupal',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('xmlrpc_example_client_form'),
+ 'access callback' => TRUE,
+ 'weight' => 1,
+ );
+ // This part is completely optional. It allows the modification of services
+ // defined by this or other modules. This configuration form is used to
+ // enable the hook_xmlrpc_alter API and alter current existing services.
+ $items['examples/xmlrpc/alter'] = array(
+ 'title' => 'XML-RPC Alterations',
+ 'description' => 'Demonstrates how to alter defined XML-RPC services',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('xmlrpc_example_alter_form'),
+ 'access callback' => TRUE,
+ 'weight' => 2,
+ );
+ return $items;
+}
+
+/**
+ * A simple landing-page information function.
+ */
+function xmlrpc_example_info() {
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+
+ $options = array(
+ 'system.listMethods' => array(),
+ );
+ // Make the xmlrpc request and process the results.
+ $supported_methods = xmlrpc($server, $options);
+ if ($supported_methods === FALSE) {
+ drupal_set_message(t('Error return from xmlrpc(): Error: @errno, Message: @message', array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())));
+ }
+
+ return array(
+ 'basic' => array(
+ '#markup' => t('This XML-RPC example presents code that shows
',
+ array(
+ '!server' => url('examples/xmlrpc/server'),
+ '!client' => url('examples/xmlrpc/client'),
+ '!alter' => url('examples/xmlrpc/alter'),
+ )
+ ),
+ ),
+ 'method_array' => array(
+ '#markup' => theme(
+ 'item_list',
+ array(
+ 'title' => t('These methods are supported by !server',
+ array('!server' => $server)
+ ),
+ 'items' => $supported_methods,
+ )
+ ),
+ ),
+ );
+}
+
+// This is the server part of the module, implementing a simple and little
+// server with just two simple services. The server is divided in two
+// different parts: the XML-RPC implementation (required) and a webform
+// interface (optional) to configure some settings in the server side.
+//
+// The XMLRPC server will define two different services:
+//
+// - subtract: perform the subtraction of two numbers. The minimum and maximum
+// values returned by the server can be configured in the server configuration
+// form.
+// - add: perform the addition of two numbers. The minimum and maximum values
+// returned by the server can be configured in the server configuration form.
+//
+// If the result value for the operation is over the maximum limit, a custom
+// error number 10001 is returned. This is an arbitrary number and could be any
+// number.
+//
+// If the result value for the operation is below the minimum limit, a custom
+// error number 10002 is returned. Again, this value is arbitrary and could be
+// any other number. Client applications must know the meaning of the error
+// numbers returned by the server.
+//
+// The following code is the XML-RPC implementation of the server part.
+// The first step is to define the methods. This methods should be associated
+// to callbacks that will be defined later.
+//
+/**
+ * Implements hook_xmlrpc().
+ *
+ * Provides Drupal with an array to map XML-RPC callbacks to existing functions.
+ * These functions may be defined in other modules. The example implementation
+ * defines specific functions for the example services.
+ *
+ * Note: Drupal's built-in XML-RPC server already includes several methods by
+ * default:
+ *
+ * Service dicovery methods:
+ * - system.listMethods: return a list of the methods the server has, by name.
+ * - system.methodSignature: return a description of the argument format a
+ * - system.methodHelp: returns a text description of a particular method.
+ * particular method expects.
+ *
+ * Other:
+ * - system.multicall: perform several method calls in a single xmlrpc request.
+ * - system.getCapabilities: determine if a given capability is supported.
+ *
+ * The methods defined by hook_xmlrpc() will be added to those provided by
+ * default by Drupal's XML-RPC server.
+ *
+ * @see hook_xmlrpc()
+ */
+function xmlrpc_example_xmlrpc() {
+ $methods[] = array(
+ // First argument is the method name.
+ 'xmlrpc_example.add',
+ // Callback to execute when this method is requested.
+ '_xmlrpc_example_server_add',
+ // An array defines the types of output and input values for this method.
+ array(
+ // The first value is the return type, an integer in this case.
+ 'int',
+ // First operand is an integer.
+ 'int',
+ // Second operand is an integer.
+ 'int',
+ ),
+ // Include a little description that is shown when XML-RPC server is
+ // requested for the implemented methods list.
+ // Method description.
+ t('Returns the sum of the two arguments.'),
+ );
+ // The subtract method is similar to the addition, only the method name,
+ // callback and description are different.
+ $methods[] = array(
+ 'xmlrpc_example.subtract',
+ '_xmlrpc_example_server_subtract',
+ array('int', 'int', 'int'),
+ t('Return difference of the two arguments.'),
+ );
+
+ return $methods;
+}
+
+// The following code for the server is optional if the callbacks already exist.
+// A server may implement methods associated to callbacks like node_load(),
+// variable_get() or any other existing function (php functions as well).
+//
+// If the callbacks associated to the methods don't exist they must be
+// created. This implementation requires two specific callbacks:
+// - _xmlrpc_example_server_add()
+// - _xmlrpc_example_server_subtract()
+//
+//
+/**
+ * This is the callback for the xmlrpc_example.add method.
+ *
+ * Sum the two arguments and return value or an error if the result is out of
+ * the configured limits.
+ *
+ * @param int|float $num1
+ * The first number to be summed.
+ * @param int|float $num2
+ * The second number to be summed.
+ *
+ * @return int|float
+ * The sum of the arguments, or error if it is not in server defined bounds.
+ *
+ * @see xmlrpc_error()
+ */
+function _xmlrpc_example_server_add($num1, $num2) {
+ $sum = $num1 + $num2;
+ // If result is not within maximum and minimum limits,
+ // return corresponding error.
+ $max = variable_get('xmlrpc_example_server_max', 10);
+ $min = variable_get('xmlrpc_example_server_min', 0);
+ if ($sum > $max) {
+ return xmlrpc_error(10001, t('Result is over the upper limit (@max) defined by the server.', array('@max' => $max)));
+ }
+ if ($sum < $min) {
+ return xmlrpc_error(10002, t('Result is below the lower limit defined by the server (@min).', array('@min' => $min)));
+ }
+ // Otherwise return the result.
+ return $sum;
+}
+
+/**
+ * This is the callback for the xmlrpc_example.subtract xmlrpc method.
+ *
+ * Return the difference of the two arguments, or an error if the result is out
+ * of the configured limits.
+ *
+ * @param int|float $num1
+ * First number
+ * @param int|float $num2
+ * Second number
+ *
+ * @return int|float
+ * The difference of the two arguments, or error if it is not in server
+ * defined bounds.
+ *
+ * @see xmlrpc_error()
+ */
+function _xmlrpc_example_server_subtract($num1, $num2) {
+ $diference = $num1 - $num2;
+ $max = variable_get('xmlrpc_example_server_max', 10);
+ $min = variable_get('xmlrpc_example_server_min', 0);
+
+ // If result is not within maximum and minimum limits,
+ // return corresponding error.
+ if ($diference > $max) {
+ return xmlrpc_error(10001, t('Result is above the upper limit (@max) defined by the server.', array('@max' => $max)));
+ }
+ if ($diference < $min) {
+ return xmlrpc_error(10002, t('Result is below the lower limit (@min) defined by the server.', array('@min' => $min)));
+ }
+ // Otherwise return the result.
+ return $diference;
+}
+
+// User interface for the XML-RPC Server part.
+// A server does not require an interface at all. In this implementation we
+// use a server configuration form to set the limits available for the addition
+// and subtraction operations.
+//
+/**
+ * Returns form array to configure the service options.
+ *
+ * Present a form to configure the service options. In this case the maximum
+ * and minimum values for any of the operations (add or subtraction).
+ */
+function xmlrpc_example_server_form() {
+ $form = array();
+ $form['explanation'] = array(
+ '#markup' => '
' . t('This is the XML-RPC server configuration page. Here you may define the maximum and minimum values for the addition or subtraction exposed services. ') . '
',
+ );
+ $form['xmlrpc_example_server_min'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter the minimum value returned by the subtraction or addition methods'),
+ '#description' => t('If the result of the operation is lower than this value, a custom XML-RPC error will be returned: 10002.'),
+ '#default_value' => variable_get('xmlrpc_example_server_min', 0),
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ $form['xmlrpc_example_server_max'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter the maximum value returned by sub or add methods'),
+ '#description' => t('if the result of the operation is bigger than this value, a custom XML-RPC error will be returned: 10001.'),
+ '#default_value' => variable_get('xmlrpc_example_server_max', 10),
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ $form['info'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('Use the XML-RPC Client example form to experiment', array('!link' => url('examples/xmlrpc/client'))) . '
' . t('Just a note of warning: The alter form has been used to disable the limits, so you may want to turn that off if you do not want it.', array('!link' => url('examples/xmlrpc/alter'))) . '
',
+ );
+ }
+ return system_settings_form($form);
+}
+
+
+// The server part of the module ends here.
+//
+// This is the client part of the module. If defines a form with two input
+// fields to call xmlrpc_example.add or xmlrpc_example.subtract methods on this
+// host. Please note that having a user interface to query an XML-RPC service is
+// not required. A method can be requested to a server using the xmlrpc()
+// function directly. We have included an user interface to make the testing
+// easier.
+//
+// The client user interface part of the module starts here.
+//
+/**
+ * Returns a form array to take input for two arguments.
+ *
+ * Present a form to get two arguments, and make a call to an XML-RPC server
+ * using these arguments as input, showing the result in a message.
+ */
+function xmlrpc_example_client_form() {
+ $form = array();
+ $form['explanation'] = array(
+ '#markup' => '
' . t('This example demonstrates how to make XML-RPC calls with Drupal. The "Request methods" button makes a request to the server and asks for the available list of methods, as a service discovery request. The "Add integers" and "Subtract integers" use the xmlrpc() function to act as a client, calling the XML-RPC server defined in this same example for some defined methods. An XML-RPC error will result if the result in the addition or subtraction requested is out of bounds defined by the server. These error numbers are defined by the server. The "Add and Subtract" button performs a multicall operation on the XML-RPC server: several requests in a single XML-RPC call. ') . '
',
+ );
+ // We are going to call add and subtract methods, and
+ // they work with integer values.
+ $form['num1'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter an integer'),
+ '#default_value' => 2,
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ $form['num2'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter a second integer'),
+ '#default_value' => 2,
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ // Include several buttons, each of them calling a different method.
+ // This button submits a XML-RPC call to the system.listMethods method.
+ $form['information'] = array(
+ '#type' => 'submit',
+ '#value' => t('Request methods'),
+ '#submit' => array('xmlrpc_example_client_request_methods_submit'),
+ );
+ // This button submits a XML-RPC call to the xmlrpc_example.add method.
+ $form['add'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add the integers'),
+ '#submit' => array('xmlrpc_example_client_add_submit'),
+ );
+ // This button submits a XML-RPC call to the xmlrpc_example.subtract method.
+ $form['subtract'] = array(
+ '#type' => 'submit',
+ '#value' => t('Subtract the integers'),
+ '#submit' => array('xmlrpc_example_client_subtract_submit'),
+ );
+ // This button submits a XML-RPC call to the system.multicall method.
+ $form['add_subtract'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add and Subtract'),
+ '#submit' => array('xmlrpc_example_client_multicall_submit'),
+ );
+ if (variable_get('xmlrpc_example_alter_enabled', FALSE)) {
+ $form['overridden'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('Just a note of warning: The alter form has been used to disable the limits, so you may want to turn that off if you do not want it.', array('!link' => url('examples/xmlrpc/alter'))) . '
',
+ );
+ }
+ return $form;
+}
+
+/**
+ * Submit handler to query system.listMethods.
+ *
+ * Submit: query the XML-RPC endpoint for the method system.listMethods
+ * and report the result as a Drupal message. The result is a list of the
+ * available methods in this XML-RPC server.
+ *
+ * Important note: Not all XML-RPC servers implement this method. Drupal's
+ * built-in XML-RPC server implements this method by default.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ */
+function xmlrpc_example_client_request_methods_submit($form, &$form_state) {
+ // First define the endpoint of the XML-RPC service, in this case this is our
+ // own server.
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ // Then we should define the method to call. xmlrpc() requires that all the
+ // information related to the called method be passed as an array in the form
+ // of 'method_name' => arguments_array
+ $options = array(
+ 'system.listMethods' => array(),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())),
+ 'error'
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response:
@response
',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+/**
+ * Submit handler to query xmlrpc_example.add.
+ *
+ * Submit: query the XML-RPC endpoint for the method xmlrpc_example.add
+ * and report the result as a Drupal message.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ */
+function xmlrpc_example_client_add_submit($form, &$form_state) {
+ // First define the endpoint of the XML-RPC service, in this case is our
+ // own server.
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ // Then we should define the method to call. xmlrpc() requires that all the
+ // information related to the called method is passed as an array in the form
+ // of 'method_name' => arguments_array
+ $options = array(
+ 'xmlrpc_example.add' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())),
+ 'error'
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response: @response',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+/**
+ * Submit handler to query xmlrpc_example.subtract.
+ *
+ * Submit: query the XML-RPC endpoint for the method xmlrpc_example.subtract
+ * and report the result as a Drupal message.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ * @see xmlrpc_example_client_add_submit()
+ */
+function xmlrpc_example_client_subtract_submit($form, &$form_state) {
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ $options = array(
+ 'xmlrpc_example.subtract' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())),
+ 'error'
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response: @response',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+/**
+ * Submit a multicall request.
+ *
+ * Submit a multicall request: query the XML-RPC endpoint for the methods
+ * xmlrpc_example.add and xmlrpc_example.subtract and report the result as a
+ * Drupal message. Drupal's XML-RPC client builds the system.multicall request
+ * automatically when there is more than one method to call.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ * @see xmlrpc_example_client_multicall_submit()
+ */
+function xmlrpc_example_client_multicall_submit($form, &$form_state) {
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+
+ /*
+ * Drupal's built-in xmlrpc server supports the system.multicall method.
+ *
+ * To make a multicall request, the main invoked method should be the
+ * function 'system.multicall', and the arguments to make this call must be
+ * defined as an array of single method calls, being the array keys the
+ * service methods to be called, and the array elements the method arguments.
+ *
+ * See the code below this comment as example.
+ */
+
+ // Build an array of several calls, Drupal's xmlrpc built-in support will
+ // construct the correct system.multicall request for the server.
+ $options = array(
+ 'xmlrpc_example.add' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ 'xmlrpc_example.subtract' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg()))
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response:
@response
',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+// The client part of the module ends here.
+//
+// The alteration part of the module starts here. hook_xmlrpc_alter() is
+// useful when you want to extend, limit or alter methods defined by other
+// modules. This part is not required to have an XML-RPC server or client
+// working, but is useful to understand what can we do using current xmlrpc
+// API provided by drupal.
+//
+// This code can be defined in other module to alter the methods exposed by
+// this xmlrpc demonstration server, or can be used to alter methods defined
+// by other modules implementing hook_xmlrpc()
+//
+// As with the rest of the example module, an user interface is not required to
+// make use of this hook. A configuration form is included to enable/disable
+// this functionality, but this part is optional if you want to implement
+// hook_xmlrpc_alter()
+//
+// This is the XML-RPC code for the alteration part. It will check if an option
+// to enable the functionality is enabled and then alter it. We alter the
+// 'xmlrpc_example.add' and 'xmlrpc_example.subtract' methods, changing the
+// associated callback with custom functions. The modified methods (with
+// new callbacks associated) will perform the addition or subtraction of the
+// integer inputs, but will never check for limits nor return errors.
+/**
+ * Implements hook_xmlrpc_alter().
+ *
+ * Check to see if xmlrpc_example.add and xmlrpc_example.subtract methods are
+ * defined and replace their callbacks with custom code.
+ *
+ * @see hook_xmlrpc_alter()
+ */
+function xmlrpc_example_xmlrpc_alter(&$methods) {
+
+ // Only perform alterations if instructed to do so.
+ if (!variable_get('xmlrpc_example_alter_enabled', 0)) {
+ return;
+ }
+ // Loop all defined methods (other modules may include additional methods).
+ foreach ($methods as $index => $method) {
+ // First element in the method array is the method name.
+ if ($method[0] == 'xmlrpc_example.add') {
+ // Replace current callback with custom callback
+ // (second argument of the array).
+ $methods[$index][1] = '_xmlrpc_example_alter_add';
+ }
+ // Do the same for the substraction method.
+ if ($method[0] == 'xmlrpc_example.subtract') {
+ $methods[$index][1] = '_xmlrpc_example_alter_subtract';
+ }
+ }
+}
+
+// Now we define the custom callbacks replacing the original defined by the
+// altered methods: xmlrpc_example.add and _xmlrpc_example.subtract. These
+// new callbacks will not check if the result of the operation is within the
+// limits defined by the server and will always return value of the operation.
+/**
+ * Sum the two arguments without limit checking.
+ *
+ * This is the replacement callback for the xmlrpc_example.add xmlrpc method.
+ *
+ * @param int|float $num1
+ * First number
+ * @param int|float $num2
+ * Second Number
+ *
+ * @return int|float
+ * The sum of the arguments
+ */
+function _xmlrpc_example_alter_add($num1, $num2) {
+ return $num1 + $num2;
+}
+
+/**
+ * Return the difference of the two arguments without limit checking.
+ *
+ * This is the replacement callback for xmlrpc_example.subtract xmlrpc method.
+ *
+ * @param int|float $num1
+ * First number
+ * @param int|float $num2
+ * Second Number
+ *
+ * @return int|float
+ * The difference of the two arguments
+ */
+function _xmlrpc_example_alter_subtract($num1, $num2) {
+ return $num1 - $num2;
+}
+
+
+// Our implementation of hook_xmlrpc_alter will work only if a system variable
+// is set to true, and we need a configuration form to enable/disable this
+// 'feature'. This is the user interface to enable or disable the
+// hook_xmlrpc_alter operations.
+/**
+ * Present a form to enable/disable the code implemented in hook_xmlrpc_alter.
+ */
+function xmlrpc_example_alter_form() {
+ $form = array();
+ $form['explanation'] = array(
+ '#markup' => '
' . t('This is a configuration form to enable the alteration of XML-RPC methods using hook_xmlrpc_alter. hook_xmlrpc_alter() can be used to alter the current defined methods by other modules. In this case as demonstration, we will overide current add and subtraction methods with others not being limited. Remember that this hook is optional and is not required to create XMLRPC services. ') . '
',
+ );
+ $form['xmlrpc_example_alter_enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Overide current xmlrpc_example.add and xmlrpc_example.subtraction methods'),
+ '#description' => t('If this checkbox is enabled, the default methods will be replaced with custom methods that ignore the XML-RPC server maximum and minimum restrictions.'),
+ '#default_value' => variable_get('xmlrpc_example_alter_enabled', 0),
+ );
+ $form['info'] = array(
+ '#markup' => '
' . t('Use the client submission form to see the results of checking this checkbox', array('!link' => url('examples/xmlrpc/client'))) . '
',
+ );
+ return system_settings_form($form);
+}
+/**
+ * @} End of "defgroup xmlrpc_example".
+ */
diff --git a/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.test b/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.test
new file mode 100644
index 00000000..01b15f90
--- /dev/null
+++ b/sites/all/modules/contrib/dev/examples/xmlrpc_example/xmlrpc_example.test
@@ -0,0 +1,139 @@
+ 'XMLRPC example functionality',
+ 'description' => 'Test xmlrpc service implementation.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable module.
+ */
+ public function setUp() {
+ parent::setUp('xmlrpc_example');
+
+ // Init common variables.
+ global $base_url;
+ $this->xmlRpcUrl = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ }
+
+ /**
+ * Perform several calls to the XML-RPC interface to test the services.
+ */
+ public function testXmlrpcExampleBasic() {
+ // Unit test functionality.
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(3, 4)));
+ $this->assertEqual($result, 7, 'Successfully added 3+4 = 7');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(4, 3)));
+ $this->assertEqual($result, 1, 'Successfully subtracted 4-3 = 1');
+
+ // Make a multicall request.
+ $options = array(
+ 'xmlrpc_example.add' => array(5, 2),
+ 'xmlrpc_example.subtract' => array(5, 2),
+ );
+ $expected = array(7, 3);
+ $result = xmlrpc($this->xmlRpcUrl, $options);
+ $this->assertEqual($result, $expected, 'Successfully called multicall request');
+
+ // Verify default limits.
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(3, 4)));
+ $this->assertEqual(xmlrpc_errno(), 10002, 'Results below minimum return custom error: 10002');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(7, 4)));
+ $this->assertEqual(xmlrpc_errno(), 10001, 'Results beyond maximum return custom error: 10001');
+ }
+
+ /**
+ * Perform several calls using XML-RPC web client.
+ */
+ public function testXmlrpcExampleClient() {
+ // Now test the UI.
+ // Add the integers.
+ $edit = array('num1' => 3, 'num2' => 5);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Add the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 8)));
+ // Subtract the integers.
+ $edit = array('num1' => 8, 'num2' => 3);
+ $result = $this->drupalPost('examples/xmlrpc/client', $edit, t('Subtract the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 5)));
+ // Request available methods.
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Request methods'));
+ $this->assertText('xmlrpc_example.add', 'The XML-RPC Add method was found.');
+ $this->assertText('xmlrpc_example.subtract', 'The XML-RPC Subtract method was found.');
+ // Before testing multicall, verify that method exists.
+ $this->assertText('system.multicall', 'The XML-RPC Multicall method was found.');
+ // Verify multicall request.
+ $edit = array('num1' => 5, 'num2' => 2);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Add and Subtract'));
+ $this->assertText('[0] => 7', 'The XML-RPC server returned the addition result.');
+ $this->assertText('[1] => 3', 'The XML-RPC server returned the subtraction result.');
+ }
+
+ /**
+ * Perform several XML-RPC requests with different server settings.
+ */
+ public function testXmlrpcExampleServer() {
+ // Set different minimum and maxmimum valuesI.
+ $options = array('xmlrpc_example_server_min' => 3, 'xmlrpc_example_server_max' => 7);
+ $this->drupalPost('examples/xmlrpc/server', $options, t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved'), 'Results limited to >= 3 and <= 7');
+
+ $edit = array('num1' => 8, 'num2' => 3);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Subtract the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 5)));
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(3, 4)));
+ $this->assertEqual($result, 7, 'Successfully added 3+4 = 7');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(4, 3)));
+ $this->assertEqual(xmlrpc_errno(), 10002, 'subtracting 4-3 = 1 returns custom error: 10002');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(7, 4)));
+ $this->assertEqual(xmlrpc_errno(), 10001, 'Adding 7 + 4 = 11 returns custom error: 10001');
+ }
+
+ /**
+ * Test XML-RPC requests with hook_xmlrpc_alter() functionality.
+ *
+ * Perform several XML-RPC requests altering the server behaviour with
+ * hook_xmlrpc_alter API.
+ */
+ public function testXmlrpcExampleAlter() {
+ // Enable XML-RPC service altering functionality.
+ $options = array('xmlrpc_example_alter_enabled' => TRUE);
+ $this->drupalPost('examples/xmlrpc/alter', $options, t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved'), 'Results are not limited due to methods alteration');
+
+ // After altering the functionality, the add and subtract methods have no
+ // limits and should not return any error.
+ $edit = array('num1' => 80, 'num2' => 3);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Subtract the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 77)));
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(30, 4)));
+ $this->assertEqual($result, 34, 'Successfully added 30+4 = 34');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(4, 30)));
+ $this->assertEqual($result, -26, 'Successfully subtracted 4-30 = -26');
+ }
+}
diff --git a/sites/all/modules/contrib/editor/wysiwyg/editors/css/tinymce-4.css b/sites/all/modules/contrib/editor/wysiwyg/editors/css/tinymce-4.css
new file mode 100644
index 00000000..9f6d2fef
--- /dev/null
+++ b/sites/all/modules/contrib/editor/wysiwyg/editors/css/tinymce-4.css
@@ -0,0 +1,4 @@
+.mce-container.mce-toolbar > .mce-container-body > .mce-btn-group > div {
+ /* Hack to make buttons wrap. */
+ white-space: normal !important;
+}
diff --git a/sites/all/modules/contrib/editor/wysiwyg/editors/css/wymeditor.css b/sites/all/modules/contrib/editor/wysiwyg/editors/css/wymeditor.css
new file mode 100644
index 00000000..ea86604b
--- /dev/null
+++ b/sites/all/modules/contrib/editor/wysiwyg/editors/css/wymeditor.css
@@ -0,0 +1,6 @@
+.wym_skin_compact .wym_dropdown ul {
+ margin-top: 0;
+}
+.wym_skin_compact .wym_iframe iframe {
+ height: 400px !important;
+}
diff --git a/sites/all/modules/contrib/editor/wysiwyg/editors/js/tinymce-4.js b/sites/all/modules/contrib/editor/wysiwyg/editors/js/tinymce-4.js
new file mode 100644
index 00000000..ff6980ac
--- /dev/null
+++ b/sites/all/modules/contrib/editor/wysiwyg/editors/js/tinymce-4.js
@@ -0,0 +1,245 @@
+(function ($) {
+
+/**
+ * Initialize editor instances.
+ *
+ * @see Drupal.wysiwyg.editor.init.ckeditor()
+ */
+Drupal.wysiwyg.editor.init.tinymce = function (settings, pluginInfo) {
+ // Fix Drupal toolbar obscuring editor toolbar in fullscreen mode.
+ var $drupalToolbars = $('#toolbar, #admin-menu', Drupal.overlayChild ? window.parent.document : document);
+ tinymce.on('AddEditor', function (e) {
+ e.editor.on('FullscreenStateChanged', function (e) {
+ if (e.state) {
+ $drupalToolbars.hide();
+ }
+ else {
+ $drupalToolbars.show();
+ }
+ });
+ });
+ // Register new plugins.
+ Drupal.wysiwyg.editor.update.tinymce(settings, pluginInfo);
+};
+
+/**
+ * Update the editor library when new settings are available.
+ *
+ * @see Drupal.wysiwyg.editor.update.ckeditor()
+ */
+Drupal.wysiwyg.editor.update.tinymce = function (settings, pluginInfo) {
+ // Load native external plugins.
+ // Array syntax required; 'native' is a predefined token in JavaScript.
+ var plugin;
+ for (plugin in pluginInfo['native']) {
+ if (!(plugin in tinymce.PluginManager.lookup || plugin in tinymce.PluginManager.urls)) {
+ tinymce.PluginManager.load(plugin, pluginInfo['native'][plugin]);
+ }
+ }
+ // Load Drupal plugins.
+ for (plugin in pluginInfo.drupal) {
+ if (!(plugin in tinymce.PluginManager.lookup)) {
+ Drupal.wysiwyg.editor.instance.tinymce.addPlugin(plugin, pluginInfo.drupal[plugin]);
+ }
+ }
+};
+
+/**
+ * Attach this editor to a target element.
+ *
+ * See Drupal.wysiwyg.editor.attach.none() for a full description of this hook.
+ */
+Drupal.wysiwyg.editor.attach.tinymce = function (context, params, settings) {
+ // Remove TinyMCE's internal mceItem class, which was incorrectly added to
+ // submitted content by Wysiwyg <2.1. TinyMCE only temporarily adds the class
+ // for placeholder elements. If preemptively set, the class prevents (native)
+ // editor plugins from gaining an active state, so we have to manually remove
+ // it prior to attaching the editor. This is done on the client-side instead
+ // of the server-side, as Wysiwyg has no way to figure out where content is
+ // stored, and the class only affects editing.
+ var $field = $('#' + params.field);
+ $field.val($field.val().replace(/(<.+?\s+class=['"][\w\s]*?)\bmceItem\b([\w\s]*?['"].*?>)/ig, '$1$2'));
+
+ // Attach editor.
+ settings.selector = '#' + params.field;
+ var oldSetup = settings.setup;
+ settings.setup = function (editor) {
+ editor.on('focus', function (e) {
+ Drupal.wysiwyg.activeId = this.id;
+ });
+ if (oldSetup) {
+ oldSetup(editor);
+ }
+ };
+ tinymce.init(settings);
+};
+
+/**
+ * Detach a single or all editors.
+ *
+ * See Drupal.wysiwyg.editor.detach.none() for a full description of this hook.
+ */
+Drupal.wysiwyg.editor.detach.tinymce = function (context, params, trigger) {
+ var instance;
+ if (typeof params !== 'undefined') {
+ instance = tinymce.get(params.field);
+ if (instance) {
+ instance.save();
+ if (trigger !== 'serialize') {
+ instance.remove();
+ }
+ }
+ }
+ else {
+ // Save contents of all editors back into textareas.
+ tinymce.triggerSave();
+ if (trigger !== 'serialize') {
+ // Remove all editor instances.
+ for (instance in tinymce.editors) {
+ if (!tinymce.editors.hasOwnProperty(instance)) {
+ continue;
+ }
+ tinymce.editors[instance].remove();
+ }
+ }
+ }
+};
+
+Drupal.wysiwyg.editor.instance.tinymce = {
+ addPlugin: function (plugin, pluginSettings) {
+ if (typeof Drupal.wysiwyg.plugins[plugin] !== 'object') {
+ return;
+ }
+
+ // Register plugin.
+ tinymce.PluginManager.add('drupal_' + plugin, function (editor) {
+ var button = {
+ title: pluginSettings.title,
+ image: pluginSettings.icon,
+ onPostRender: function () {
+ var self = this;
+ editor.on('nodeChange', function (e) {
+ // isNode: Return whether the plugin button should be enabled for
+ // the current selection.
+ if (typeof Drupal.wysiwyg.plugins[plugin].isNode == 'function') {
+ self.active(Drupal.wysiwyg.plugins[plugin].isNode(e.element));
+ }
+ });
+ }
+ };
+ if (typeof Drupal.wysiwyg.plugins[plugin].invoke == 'function') {
+ button.onclick = function () {
+ var data = {format: 'html', node: editor.selection.getNode(), content: editor.selection.getContent()};
+ // TinyMCE creates a completely new instance for fullscreen mode.
+ Drupal.wysiwyg.plugins[plugin].invoke(data, pluginSettings, editor.id);
+ };
+ }
+
+ // Register the plugin button.
+ editor.addButton('drupal_' + plugin, button);
+
+ /**
+ * Initialize the plugin, executed after the plugin has been created.
+ *
+ * @param ed
+ * The tinymce.Editor instance the plugin is initialized in.
+ * @param url
+ * The absolute URL of the plugin location.
+ */
+ editor.on('init', function (e) {
+ // Load custom CSS for editor contents on startup.
+ if (pluginSettings.css) {
+ editor.dom.loadCSS(pluginSettings.css);
+ }
+
+ });
+
+ // Attach: Replace plain text with HTML representations.
+ editor.on('beforeSetContent', function (e) {
+ if (typeof Drupal.wysiwyg.plugins[plugin].attach === 'function') {
+ e.content = Drupal.wysiwyg.plugins[plugin].attach(e.content, pluginSettings, e.target.id);
+ e.content = Drupal.wysiwyg.editor.instance.tinymce.prepareContent(e.content);
+ }
+ });
+
+ // Detach: Replace HTML representations with plain text.
+ editor.on('getContent', function (e) {
+ var editorId = (e.target.id === 'mce_fullscreen' ? e.target.getParam('fullscreen_editor_id') : e.target.id);
+ if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') {
+ e.content = Drupal.wysiwyg.plugins[plugin].detach(e.content, pluginSettings, editorId);
+ }
+ });
+ });
+ },
+
+ openDialog: function (dialog, params) {
+ var instanceId = this.getInstanceId();
+ var editor = tinymce.get(instanceId);
+ editor.windowManager.open({
+ file: dialog.url + '/' + instanceId,
+ width: dialog.width,
+ height: dialog.height,
+ inline: 1
+ }, params);
+ },
+
+ closeDialog: function (dialog) {
+ var editor = tinymce.get(this.getInstanceId());
+ editor.windowManager.close(dialog);
+ },
+
+ prepareContent: function (content) {
+ // Certain content elements need to have additional DOM properties applied
+ // to prevent this editor from highlighting an internal button in addition
+ // to the button of a Drupal plugin.
+ var specialProperties = {
+ img: {class: 'mceItem'}
+ };
+ // No .outerHTML() in jQuery :(
+ var $content = $('
' + content + '
');
+ // Find all placeholder/replacement content of Drupal plugins.
+ $content.find('.drupal-content').each(function () {
+ // Recursively process DOM elements below this element to apply special
+ // properties.
+ var $drupalContent = $(this);
+ $.each(specialProperties, function (element, properties) {
+ $drupalContent.find(element).andSelf().each(function () {
+ for (var property in properties) {
+ if (property === 'class') {
+ $(this).addClass(properties[property]);
+ }
+ else {
+ $(this).attr(property, properties[property]);
+ }
+ }
+ });
+ });
+ });
+ return $content.html();
+ },
+
+ insert: function (content) {
+ content = this.prepareContent(content);
+ tinymce.get(this.field).insertContent(content);
+ },
+
+ setContent: function (content) {
+ content = this.prepareContent(content);
+ tinymce.get(this.field).setContent(content);
+ },
+
+ getContent: function () {
+ return tinymce.get(this.getInstanceId()).getContent();
+ },
+
+ isFullscreen: function () {
+ var editor = tinymce.get(this.field);
+ return editor.plugins.fullscreen && editor.plugins.fullscreen.isFullscreen();
+ },
+
+ getInstanceId: function () {
+ return this.field;
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/editor/wysiwyg/editors/js/wymeditor-1.js b/sites/all/modules/contrib/editor/wysiwyg/editors/js/wymeditor-1.js
new file mode 100644
index 00000000..369e4156
--- /dev/null
+++ b/sites/all/modules/contrib/editor/wysiwyg/editors/js/wymeditor-1.js
@@ -0,0 +1,63 @@
+(function($) {
+
+/**
+ * Attach this editor to a target element.
+ */
+Drupal.wysiwyg.editor.attach.wymeditor = function (context, params, settings) {
+ // Prepend basePath to wymPath.
+ settings.wymPath = settings.basePath + settings.wymPath;
+ settings.postInit = function (instance) {
+ var $doc = $(instance._doc);
+ // Inject stylesheet for backwards compatibility.
+ if (settings.stylesheet) {
+ $doc.find('head').append('');
+ }
+ // Update activeId on focus.
+ $doc.find('body').focus(function () {
+ Drupal.wysiwyg.activeId = params.field;
+ });
+ };
+ // Attach editor.
+ $('#' + params.field).wymeditor(settings);
+};
+
+/**
+ * Detach a single editor instance.
+ */
+Drupal.wysiwyg.editor.detach.wymeditor = function (context, params, trigger) {
+ var $field = $('#' + params.field, context);
+ var index = $field.data(WYMeditor.WYM_INDEX);
+ if (typeof index == 'undefined' || !WYMeditor.INSTANCES[index]) {
+ return;
+ }
+ var instance = WYMeditor.INSTANCES[index];
+ instance.update();
+ if (trigger != 'serialize') {
+ instance.vanish();
+ }
+};
+
+Drupal.wysiwyg.editor.instance.wymeditor = {
+ insert: function (content) {
+ this.getInstance().insert(content);
+ },
+
+ setContent: function (content) {
+ this.getInstance().html(content);
+ },
+
+ getContent: function () {
+ return this.getInstance().html();
+ },
+
+ getInstance: function () {
+ var $field = $('#' + this.field);
+ var index = $field.data(WYMeditor.WYM_INDEX);
+ if (typeof index != 'undefined') {
+ return WYMeditor.INSTANCES[index];
+ }
+ return null;
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/editor/wysiwyg/includes/styling.inc b/sites/all/modules/contrib/editor/wysiwyg/includes/styling.inc
new file mode 100644
index 00000000..ce2547d1
--- /dev/null
+++ b/sites/all/modules/contrib/editor/wysiwyg/includes/styling.inc
@@ -0,0 +1,130 @@
+ $theme_key);
+ drupal_alter('wysiwyg_editor_styles', $elements, $context); $css = array();
+ return $elements;
+}
+
+/**
+ * Creates a cache of the stylesheets used by the currently set theme.
+ *
+ * Since this is a pre render callback for the styles element, it should run
+ * late enough to catch all the stylesheets added just before the actual markup
+ * for them is rendered.
+ *
+ * The first time this runs for a theme it's too late for a module to have any
+ * use of the cache, so wysiwyg_get_css() uses drupal_http_request() to fetch a
+ * dummy page, filling the cache before the original response is sent.
+ *
+ * Intended to run after Core has sorted/grouped/aggregated stylesheets.
+ */
+function _wysiwyg_pre_render_styles($elements) {
+ global $theme_key;
+ if (strpos(current_path(), 'wysiwyg_theme/') !== 0) {
+ return $elements;
+ }
+ $cached = cache_get('wysiwyg_css');
+ foreach (element_children($elements) as $child) {
+ if (isset($elements['#groups'][$child]['group']) && $elements['#groups'][$child]['group'] != CSS_THEME) {
+ continue;
+ }
+ switch ($elements[$child]['#tag']) {
+ case 'link':
+ $css[] = $elements[$child]['#attributes']['href'];
+ break;
+ case 'style':
+ if (!empty($elements[$child]['#attributes']['href'])) {
+ $css[] = $elements[$child]['#attributes']['href'];
+ }
+ elseif (!empty($elements[$child]['#value'])){
+ preg_match_all('/\@import url\("([^"]+)"\);/', $elements[$child]['#value'], $matches, PREG_SET_ORDER);
+ foreach ($matches as $val) {
+ $css[] = $val[1];
+ }
+ }
+ break;
+ }
+ $all = empty($cached->data) ? array() : $cached->data;
+ $all[$theme_key] = array('files' => $css, 'aggregated' => variable_get('preprocess_css', FALSE));
+ }
+ $all['_css_js_query_string'] = variable_get('css_js_query_string');
+ cache_set('wysiwyg_css', $all);
+ return $elements;
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/API.txt b/sites/all/modules/contrib/fields/hierarchical_select/API.txt
new file mode 100644
index 00000000..aa7cfaa8
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/API.txt
@@ -0,0 +1,693 @@
+
+Terminology
+-----------
+- item: an item in the hierarchy. A hierarchy can also be seen as a tree. In
+ that case, an item can be either a parent or a child. However, if
+ "multiple parents" are supported (i.e. a child can have multiple
+ parents), then it's actually not a tree but a directed acyclic graph
+ (see http://en.wikipedia.org/wiki/Directed_acyclic_graph), in which
+ each case technically is a "node".
+ An example: in the case of taxonomy, this is the term id (tid).
+- label: the label associated with an item in the hierarchy. You may now it
+ as "title" or something else similar.
+ An example: in the case of taxonomy, this is the actual term.
+- item type: a per-level, human-readable name that describes what kind of
+ items that level contains.
+- entity: an item is often associated with an entity. E.g. a term is usually
+ associated with a node.
+- form element: a form element allows the developer to assign a new value to
+ a #type property in a form item. Examples of form elements
+ supported by Drupal core are: select, checkboxes, textfield.
+- form item: an instance of a form element, with various other properties
+ defined, such as #title, #default_value and #description. These
+ are used to define a form in Drupal.
+- Hierarchical Select: this is the name of the module.
+- hierarchical_select: this is the internal name of the Hierarchical Select
+ form element.
+- hierarchical select: (note the difference in case) this is the part of the
+ widget with the multiple selects.
+- dropbox: this is the part of the widget where the selections are stored when
+ multiple selections are allowed.
+
+
+Form API usage
+--------------
+You have to make sure your form item is using the "hierarchical_select" form
+element type:
+
+ $form['select_some_term'] = array(
+ '#type' => 'hierarchical_select',
+ '#title' => t('Select the tag you wish to use.'),
+ '#size' => 1,
+ '#config' => array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vid,
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ 'labels' => array(
+ 0 => t('Main category'),
+ 1 => t('Subcategory'),
+ 2 => t('Third level category'),
+ ),
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'title' => t('All selections'),
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ 'sort' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(
+ 0 => 0,
+ 1 => 0,
+ 2 => 1,
+ ),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ // These settings cannot be configured through the UI: they can only be
+ // overridden through code.
+ 'animation_delay' => 400,
+ 'special_items' => array(),
+ 'render_flat_select' => 0,
+ ),
+ '#default_value' => '83',
+ );
+
+Now, let's explain what we see here:
+1) We've set the #type property to "hierarchical_select" instead of "select".
+2) The #size property is inherited by the selects of the hierarchical select.
+ You can use it to change a vertical size of the select (i.e. change how many
+ items are displayed in the select, similar to a form select multiple).
+3) There's a new property: #config. This must be an
+array. These are the items it can contain:
+ - module (required)
+ This will be passed through in the AJAX requests, to let Hierarchical
+ Select know which module's hooks should be used.
+
+ - params (optional, may be necessary for some implementations)
+ An array of parameters that will also be passed through in every AJAX
+ request.
+ e.g. In the case of taxonomy, this is the vocabulary id (vid). In case of
+ content_taxonomy, there's three parameters: vid, tid and depth (tid allows
+ one to define a new root, depth allows one to limit the depth of the
+ displayed hierarchy).
+
+ - save_lineage (optional, defaults to 0)
+ Triggers the lineage saving functionality. If enabled, the selection can
+ consist of multiple values.
+
+ - enforce_deepest (optional, defaults to 0)
+ Triggers the enforcing of a selection in the deepest level. If enabled, the
+ selection will always be a single value.
+
+ - resizable (optional, defaults to 1)
+ Makes the hierarchical select resizable.
+
+ - level_labels['status'] (optional, defaults to 0)
+ Whether level labels should be enabled or not. When save_lineage is
+ enabled, this will result in *empty* level labels.
+
+ - level_labels['labels'] (optional)
+ An array of labels, one per level. The label for the first level should be
+ the value of key 0.
+ When enforce_deepest is set to:
+ - 0, then you can provide n level labels, with n the number of levels
+ - 1, then you can provide only one level label.
+
+ - dropbox['status'] (optional, defaults to 0)
+ Whether the dropbox is enabled or not (the dropbox allows the user to make
+ multiple selections).
+
+ - dropbox['title'] (optional, defaults to "All selections:")
+ The title of the dropbox. The dropbox is the area where all selections are
+ displayed when the dropbox is enabled.
+
+ - dropbox['limit'] (optional, defaults to 0, which means "no limit")
+ Limit the number of selection that can be added to the dropbox. So this
+ allows you the restrict the number of items that can be selected when
+ the dropbox has been enabled.
+
+ - dropbox['reset_hs'] (optional, defaults to 1, which means "do reset")
+ Determines what will happen to the hierarchical select when the user has
+ added a selection to the dropbox.
+
+ - dropbox['sort'] (optional, defaults to 1, which means "do sort")
+ Determines whether the items in the dropbox will be automatically sorted.
+
+ - editability['status] (optional, defaults to 0)
+ Allow the user to create new items in the hierarchy.
+
+ - editability['item_types'] (optional, defaults to the empty array)
+ Only meaningful when editable is set to TRUE.
+ Set the item type for each level. E.g.: "country" for the first level,
+ "region" for the second and "city" for the third. When the user then wants
+ to create a new item, the default label for the new item will be of the
+ form "new ", e.g. "new region".
+
+ - editability['allowed_levels'] (optional, defaults to 1 for each level)
+ Only meaningful when editable is set to TRUE.
+ Specify in which levels the user is allowed to create new items. In the
+ example, the user is only allowed to create new items in the third level.
+ When a setting for a level is ommitted, it defaults to 1 (i.e. allowed for
+ that level). This means you only have to specify in which levels the user
+ is not allowed to create new items.
+ This only applies to *existing* levels: it does not affect the
+ allow_new_levels setting (the next setting).
+
+ - editability['allow_new_levels'] (optional, defaults to 0)
+ Only meaningful when editable is set to TRUE.
+ Allow the user to create new levels, i.e. when a certain item does not yet
+ have children, the user can create a first child for it (thus thereby
+ creating a new level).
+
+ - editability['max_levels'] (optional, defaults to 3)
+ Only meaningful when editable_settings['allow_new_levels'] is set to TRUE.
+ Limits the maximum number of levels. Don't set this too high or you'll end
+ up with very deep hierarchies. This only affects how deep new levels can be
+ created, it will not affect the existing hierarchy.
+
+ - entity_count['enabled'] (optional, defaults to 0)
+ Enables the display of entity counts, between parentheses, for each item in
+ the hierarchy.
+
+ - entity_count['require_entity'] (optional, defaults to 0)
+ Whether an item should only be displayed if it has at least one associated
+ entity.
+
+ - entity_count['settings']['count_children'] (optional, defaults to 0)
+ Whether the entity count should also count associated children of the entity.
+
+ - entity_count['settings']['entity_types'] (optional, defaults to array())
+ Which types of entities should be counted. This is a list of checkboxes that
+ allow the user to select entity types by bundles.
+
+ - animation_delay (optional, defaults to 400)
+ The delay of each animation (the drop in left and right animations), in ms.
+
+ - special_items (optional, defaults to the empty array)
+ Through this setting, you can mark each item with special properties it
+ possesses. There currently are two special properties: 'exclusive' and
+ 'none'.
+ Note: you should include these items in the hierarchy as if it were a
+ normal item and then you can mark them as special through this property.
+ * 'exclusive': Sometimes it's desirable to have exclusive lineages. When
+ such an option is selected, the user should not be able to
+ select anything else. This also means that nothing else in
+ the dropbox can be selected: if the dropbox contains
+ anything, it will be reset.
+ Can be applied to multiple items.
+ e.g. an 'entire_tree' item:
+ 'special_items' => array(
+ 'entire_tree' => array('exclusive'),
+ )
+ * 'none': Sometimes you want to replace the default '' option by
+ something else. This replacement should of course also exist in
+ the root level.
+ Can be applied to only one item.
+ e.g. an 'any' item (used in hs_taxonomy_views):
+ 'special_items' => array(
+ 'any' => array('none', 'exclusive'),
+ )
+ And a final example for a better overview:
+ 'special_items' => array(
+ 'entire_tree' => array('exclusive'),
+ 'any' => array('none', 'exclusive'),
+ )
+
+ - render_flat_select (optional, defaults to 0)
+ Because the hierarchical_select form element consists of multiple form
+ items, it doesn't work well in GET forms. By enabling this setting, a flat
+ select will also be rendered, that contains only the selected lineages.
+ Combine that with Drupal.HierarchicalSelect.prepareGETSubmit in the JS code
+ (or, alternatively, the 'prepare-GET-submit' event that can be triggered,
+ see the JavaScript events section for details) and you have a work-around
+ (which, admittedly, only works when JS is enabled).
+
+3) We *don't* specify a list of options: Hierarchical Select automatically
+generates the options for us, thanks to the 'module' and 'params' settings.
+
+
+Concepts
+--------
+- Item Unicity: each item in the hierarchy must be *unique*. It doesn't have
+ to be numerical, it can also be a string.
+ If your hierarchy does not have unique items by nature or by
+ design (your items may be unique per level instead), that's
+ not a problem. You can simply prepend the item's ancestors to
+ get a unique item.
+ e.g. you have an item "foobar" at the first, second and third
+ levels. By prepending the ancestors using the dash as the
+ separator, you'd get an item "foobar-foobar-foobar" at the
+ third level.
+ Also see the "Reserved item values" section.
+- #options: it's gone, because it was the inherent cause for scalability
+ problems: if a hierarchy consists of 10,000 or even 100,000 items,
+ this results in huge HTML being generated. Huge HTML means more
+ processing power necessary, and more bandwidth necessary. So where
+ does Hierarchical Select get its "options"? It uses the hooks that
+ every implementation has to implement to only get what it needs.
+- The General Concept: you should think of Hierarchical Select as an abstract
+ widget that can represent *any* hierarchy. To be able
+ to display any hierarchy, you obviously need some
+ universal way to "browse" a hierarchy.
+ If you are familiar with C++ or Java iterators, this
+ should come natural: the hooks you have to implement
+ is what allows Hierarchical Select to iterate over your
+ hierarchy. Then the heart of the iterator would be the
+ root_level() and children() hooks. params() allows you
+ to define which information is necessary before you can
+ determine *which* hierarchy or which *part* of the
+ hierarchy is being browsed. lineage() must return the
+ lineage, i.e. the item itself and all its ancestors,
+ this allows a hierarchy to be generated from just one
+ (selected) item.
+
+
+Reserved item values
+--------------------
+- Ensure that your items don't have a "none", "all", "create_new_item" nor
+ "label_\d+" values (the latter means "label_" followed by one or more
+ digits). Your values should also not contain a pipe ("|"), since pipes are
+ used to separate the selection of values that are sent back to the server
+ in the callbacks.
+- Valid 'empty' selections (i.e. if you want to set the #default_value
+ property of your form item), are -1 and the empty array. The empty string is
+ also considered valid, because Drupal core's Taxonomy module uses this as
+ the empty selection.
+
+
+Developer mode
+--------------
+When you are writing your implementation of the Hierarchical Select API, you
+will often wonder what Hierarchical Select is doing internally with the data
+you're feeding it. That's why there's a developer mode: it will show you this
+data, even the data generated in AJAX callbacks. It'll also show you the time
+it took to generate the lineage, to fill up the levels and to calculate the
+child info, to track down badly performing code.
+Also, when you're just creating a new HS config and it doesn't quite work
+right, it can be helpful to enable the developer mode. It will perform some
+basic diagnostics that might help you track down the cause.
+To use this, you must have a browser with console.log() support. Install
+Firebug Lite (http://getfirebug.com/lite.html) if your browser does not
+suport this. Next, go to Hierarchical Select's .module file and set the define
+for the HS_DEVELOPER_MODE constant to TRUE.
+When you now open Firebug (Firefox) or the Web Inspector (Safari), you'll see
+the debug output. New output is added after each callback to the server.
+
+
+Hierarchical Select implementations: gotcha's
+---------------------------------------------
+- "warning: Missing argument 1 for drupal_retrieve_form() …"
+ This implies that your implementation's module weight is heavier than
+ hierarchical_select.module. In that case, Hierarchical Select will not be
+ able to detect hierarchical_select form items, preventing it from applying
+ some magic, and AJAX updates won't work.
+
+
+Hierarchical Select compatibility: gotcha's
+-------------------------------------------
+- "Invalid response from server"
+ This typically means that some functions could not be found when
+ Hierarchical Select does an AJAX callback to the server, which in turn means
+ that some code (some PHP file) has not been included, while it should have
+ been. Instead of using module_load_include() or even require_once, you
+ should use form_load_include(). This function is new in Drupal 7 and will
+ ensure that all required PHP files are included automatically.
+
+
+Hierarchical Select API Tutorial
+--------------------------------
+Written by Stephen Barker of Digital Frontiers Media
+(http://drupal.org/user/106070) and reviewed by Wim Leers:
+ http://drupal.org/node/532724
+
+
+Hierarchical Select Small Hierarchy
+-----------------------------------
+Hierarchical Select includes a Hierarchical Select API implementation that
+allows one to use a hardcoded hierarchy. When it becomes to slow, you should
+move the hierarchy into the database and write a proper implementation.
+Below you can find an example of how to use the hs_smallhierarchy module. Just
+change the $hierarchy array to suit your needs and off you go! Look at the
+code of hs_smallhierarchy.module for full details, but this code example
+should get you started.
+
+ $hierarchy = array(
+ 'win' => array(
+ 'label' => 'Windows',
+ 'children' => array(
+ 'xp' => array('label' => 'XP'),
+ 'vista' => array(
+ 'label' => 'Vista',
+ 'children' => array(
+ 'x86' => array('label' => '32-bits'),
+ 'x64' => array('label' => '64-bits'),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $form['select_some_term'] = array(
+ '#type' => 'hierarchical_select',
+ '#title' => t('Select the tag you wish to use.'),
+ '#size' => 1,
+ '#config' => array(
+ 'module' => 'hs_smallhierarchy',
+ 'params' => array(
+ 'hierarchy' => $hierarchy,
+ 'id' => 'my-hierarchy-about-windows',
+ 'separator' => '|',
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ 'labels' => array(
+ 0 => t('Main category'),
+ 1 => t('Subcategory'),
+ 2 => t('Third level category'),
+ ),
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'title' => t('All selections'),
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ 'sort' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(
+ 0 => 0,
+ 1 => 0,
+ 2 => 1,
+ ),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ // These settings cannot be configured through the UI: they can only be
+ // overridden through code.
+ 'animation_delay' => 400,
+ 'exclusive_lineages' => array(),
+ 'render_flat_select' => 0,
+ ),
+ '#description' => 'Put your description here',
+ '#default_value' => 'win|xp|x86',
+ );
+
+
+Hooks
+-----
+1) hook_hierarchical_select_params();
+ Returns an array with the names of all parameters that are necessary for
+ this implementation to work.
+
+2) hook_hierarchical_select_root_level($params, $dropbox = FALSE);
+ Returns the root level of the hierarchy: an array of (item, label) pairs.
+ The $dropbox parameter can is optional and can even ommitted, as it's only
+ necessary if you need the dropbox to influence your hierarchy.
+
+3) hook_hierarchical_select_children($parent, $params, $dropbox = FALSE);
+ Gets the children of $parent ($parent is an item in the hierarchy) and
+ returns them: an array of (item, label) pairs, or the empty array if the
+ given $parent has no children.
+ The $dropbox parameter can is optional and can even ommitted, as it's only
+ necessary if you need the dropbox to influence your hierarchy.
+
+4) hook_hierarchical_select_lineage($item, $params);
+ Calculates the lineage of $item (array of items, with $item the last) and
+ returns it. Necessary when the "enforce_deepest" option is enabled.
+
+5) hook_hierarchical_select_valid_item($item, $params);
+ Validates an item, returns TRUE if valid, FALSE if invalid.
+
+6) hook_hierarchical_select_item_get_label($item, $params);
+ Given a valid item, returns the label. Is only used for rendering the
+ selections in the dropbox.
+
+7) hook_hierarchical_select_create_item($label, $parent, $params);
+ Given a parent item and the label of a new item, create a new item as a
+ child of the parent item. When $parent == 0, this means a new item is being
+ created at the root level.
+ Optional hook. When this hook is not implemented, this functionality will
+ never be used, even when you configure it that way in code.
+
+8) hook_hierarchical_select_entity_count($item, $params);
+ Given a item, get the number of entities (most of the time the entity type
+ is 'node') that are related to the given item. Used for the entity_count
+ and require_entity settings.
+ Optional hook. When this hook is not implemented, this functionality will
+ never be used, even when you configure it that way (i.e. when you enable
+ the entity_count and require_entity settings).
+
+9) hook_hierarchical_select_implementation_info();
+ Return metadata about this implementation.
+ This information is used to generate the implementations overview at
+ admin/settings/hierarchical_select/implementations. The expected format is:
+
+ array(
+ 'hierarchy type' => t('Taxonomy'),
+ 'entity type' => t('Node'),
+ 'entity' => t('Story'),
+ 'context type' => t('Node form'),
+ 'context' => '',
+ );
+
+ another example:
+
+ array(
+ 'hierarchy type' => t('Taxonomy'),
+ 'entity type' => t('Node'),
+ 'entity' => '',
+ 'context type' => t('Views exposed filter'),
+ 'context' => t('some view'),
+ );
+
+10) hook_hierarchical_select_config_info();
+ Return metadata about each available user-editable configuration for this
+ implementation.
+ Optional hook. This information is used to generate the configurations
+ overview at admin/settings/hierarchical_select/configs. The expected
+ format is:
+
+ $config_info[$config_id] = array(
+ 'config_id' => $config_id,
+ 'hierarchy type' => t('Taxonomy'),
+ 'hierarchy' => t($vocabulary->name),
+ 'entity type' => t('Node'),
+ 'entity' => implode(', ', array_map('t', $entities)),
+ 'edit link' => "admin/content/taxonomy/edit/vocabulary/$vid",
+ );
+
+
+Standardized configuration form
+-------------------------------
+Hierarchical Select 3 comes with a standardized configuration form:
+hierarchical_select_common_config_form(). This function accepts a lot of
+parameters, which allows you to use names typical to your module's hierarchy
+(e.g. 'leaf' instead of 'term' and 'tree' instead of 'vocabulary'). A submit
+handler is also provided, of course.
+An example:
+
+ // I'm not configuring all parameters here. For an example of that, see one
+ // of the included modules.
+ $form['foobar_hierarchical_select_config'] = hierarchical_select_common_config_form($module, $params, $config_id, $defaults, $strings, $max_hierarchy_depth, $preview_is_required);
+
+ // Add the the submit handler for the Hierarchical Select config form.
+ $parents = array('foobar_hierarchical_select_config');
+ $form['#submit'][] = 'hierarchical_select_common_config_form_submit';
+ $form['#hs_common_config_form_parents'] = $parents;
+
+
+Configuration management
+------------------------
+It's now possible to export Hierarchical Select configurations, and there is a
+function to set the configuration of a certain Hierarchical Select. Combine
+the two and you can manage your Hierarchical Select configurations in code!
+An example:
+
+ // The exported configuration.
+ $config = array( … );
+ $config_id = $config['config_id];
+
+ // Apply the configuration.
+ require_once(drupal_get_path('module', 'hierarchical_select') .'/includes/common.inc');
+ hierarchical_select_common_config_set($config_id, $config);
+
+
+JavaScript events
+-----------------
+The Hierarchical Select module's JavaScript code triggers several events, to
+allow for advanced interactions.
+
+You can find all hierarchical_select form items using this selector:
+
+ $('.hierarchical-select-wrapper');
+
+You can find a *specific* hierarchical_select form item using this selector:
+
+ $('#hierarchical-select-x-wrapper');
+
+where x is a number, or more accurately: a hsid (hierarchical select id).
+Retrieving all hsids in the current document can be done like this:
+
+ for (var hsid in Drupal.settings.HierarchicalSelect.settings) {
+ // …
+ }
+
+Alternatively, you can use one of the transliterated class names. A wrapper
+for Hierarchical Select looks like this:
+
+ …
+
+Hence, you could also use selectors such as these, to achieve the same effect,
+but with more robust code:
+ $('.hierarchical-select-wrapper-for-config-taxonomy-1:first')
+ .trigger('enforce-update');
+ $('.hierarchical-select-wrapper-for-name-edit-taxonomy-1:first')
+ .trigger('enforce-update');
+
+The following events are triggered:
+ - change-hierarchical-select
+ - update-hierarchical-select
+ - create-new-item
+ - cancel-new-item
+ - add-to-dropbox (check https://www.drupal.org/node/1277068)
+ - remove-from-dropbox
+ - enforced-update
+ - prepared-GET-submit
+All events are triggered *after* the animations have completed.
+
+However, it's often useful to do something *before* an event (especially
+because all of the above events perform an AJAX request to the server). So,
+the equivalent "before" events exist as well:
+ - before-update-hierarchical-select
+ - before-create-new-item
+ - before-cancel-new-item
+ - before-add-to-dropbox
+ - before-remove-from-dropbox
+ - before-enforced-update
+There is one exception: when the cache is enabled, the "before update
+hierarchical select" event will not be triggered. This makes sense, because
+updates from the cache are instantaneous.
+
+An example of binding a function to the 'create-new-item' event of the second
+(hsid == 1) hierarchical_select form item on the page:
+
+ $('#hierarchical-select-1-wrapper')
+ .bind('create-new-item', function() {
+ // …
+ });
+
+And finally, you can trigger a special event to enforce an update (this can be
+useful when you have changed a hierarchy through another form item, or for
+live previews, or …). You can then also pass additional information that will
+be POSTed. You can even disable normal updates, to manage that completely
+yourself via enforced updates. This allows you to write a Hierarchical Select
+implementation that gets some of its information ($params) from another form
+item!
+Suppose you'd like to enforce an update of the first (hsid == 0)
+hierarchical_select form item on the page:
+
+ $('#hierarchical-select-0-wrapper')
+ .trigger('enforce-update');
+
+Now let's move on to a more advanced example, in which we will disable normal
+updates and let another form item (here a select) provide a part of the
+information that will be used to render the Hierarchical Select. Effectively,
+this other form item will *influence* the hierarchy that will be presented by
+Hierarchical Select!
+
+ $(document).ready(function() {
+ Drupal.settings.specialfilter = {};
+
+ // .specialfilter-first: a select form item
+ // .specialfilter-second: a hierarchical_select form item
+
+ update = function() {
+ var selection = Drupal.settings.specialfilter.currentSelection;
+
+ // Send an extra parameter via POST: dynamicParameter. This is the stored
+ // selection.
+ $('.specialfilter-second')
+ .trigger('enforce-update',
+ [
+ { name : 'dynamicParameter', value : selection }
+ ]
+ );
+ };
+
+ attachHSBindings = function() {
+ // When a user navigates the hierarchical_select form item, we still want to
+ // POST the the extra dynamicParameter, or otherwise we will no longer have
+ // a hierarchy in the hierarchical_select form item that really depends on
+ // the select.
+ $('.specialfilter-second .hierarchical-select > select')
+ .change(function() { update(); });
+
+ $('.specialfilter-second')
+ .unbind('enforced-update').bind('enforced-update', function() { return attachHSBindings(); });
+ };
+
+ // Initialize after 25 ms, because otherwise the event binding of HS will
+ // not yet be ready, and hence this won't have any effect
+ setTimeout(function() {
+ // Get the initial selection (before the user has changed anything).
+ Drupal.settings.specialfilter.currentSelection = $('.specialfilter-first').attr('value');
+
+ // When the select form item changes, we want to *store* that selection, and
+ // update the hierarchical_select form item.
+ $('.specialfilter-first')
+ .change(function() {
+ // Store the current selection.
+ Drupal.settings.specialfilter.currentSelection = $(this).attr('value');
+
+ update();
+ });
+
+ $('.specialfilter-second')
+ .trigger('disable-updates');
+
+ attachHSBindings();
+ }, 25);
+ });
+
+The 'enforced-update' (notice the past tense!) event is triggered upon
+completion.
+An even more rarely used special event can be triggered to prepare the
+hierarchical_select form element for a get submit: the 'prepare GET submit'
+event. To use this event, the 'render_flat_select' setting should be enabled
+in the config.
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/LICENSE.txt b/sites/all/modules/contrib/fields/hierarchical_select/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/README.txt b/sites/all/modules/contrib/fields/hierarchical_select/README.txt
new file mode 100644
index 00000000..39719dd8
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/README.txt
@@ -0,0 +1,200 @@
+
+Description
+-----------
+This module defines the "hierarchical_select" form element, which is a greatly
+enhanced way for letting the user select items in a hierarchy.
+
+Hierarchical Select has the ability to save the entire lineage of a selection
+or only the "deepest" selection. You can configure it to force the user to
+make a selection as deep as possible in the tree, or allow the user to select
+an item anywhere in the tree. Levels can be labeled, you can configure limit
+the number of items that can be selected, configure a title for the dropbox,
+choose a site-wide animation delay, and so on. You can even create new items
+and levels through Hierarchical Select!
+
+
+Integrates with
+---------------
+* Taxonomy (Drupal core)
+
+
+Installation
+------------
+1) Place this module directory in your "modules" folder (this will usually be
+"sites/all/modules/"). Don't install your module in Drupal core's "modules"
+folder, since that will cause problems and is bad practice in general. If
+"sites/all/modules" doesn't exist yet, just create it.
+
+2) Enable the Hierarchical Select and Hierarchical Select Taxonomy modules.
+
+3) If you want to use it for one or more of your vocabularies, go to
+admin/structure/types and click the "manage fields" link for a content type on
+which you're using a Term reference field. Click the "edit" link for this Term
+reference field and then go to the "widget type" tab in the upper right corner.
+There, you can choose the "Hierarchical Select" widget type, and when you do,
+the entire Hierarchical Select configuration UI will appear: here you'll find
+a whole range of Hierarchical Select settings. All settings are explained
+there as well!
+
+
+Troubleshooting
+---------------
+If you ever have problems, make sure to go through these steps:
+
+1) Go to admin/reports/status (i.e. the Status Report). Ensure that the status
+ of the Hierarchical Select module is ok.
+
+2) Ensure that the page isn't being served from your browser's cache. Use
+ CTRL+R in Windows/Linux browsers, CMD+R in Mac OS X browsers to enforce the
+ browser to reload everything, preventing it from using its cache.
+
+3) When you're getting a JS alert with the following message: "Received an
+ invalid response from the server.", ensure that the page (of which this
+ form is a part) is *not* being cached.
+
+4) When Hierarchical Select seems to be misbehaving in a certain use case in
+ which terms with multiple parents are being used, make sure to enable the
+ "Save term lineage" setting.
+ Note: you may have to repeat this for every configuration in which the
+ vocabulary with terms that have multiple parents are being used. E.g. if
+ such a vocabulary is called "A", then go to
+ admin/config/content/hierarchical_select/configs
+ and edit all configuration that have "A" in the "Hierarchy" column.
+
+In case of problems, don't forget to try a hard refresh in your browser!
+
+
+Limitations
+-----------
+- Creating new items in the hierarchy in a multiple parents hierarchy (more
+ scientifically: a directed acyclic graph) is *not* supported.
+- Not the entire scalability problem can be solved by installing this set of
+ modules; read the maximum scalability section for details.
+- The child indicators only work in Firefox. This *cannot* be supported in
+ Safari or IE. See http://drupal.org/node/180691#comment-1044691.
+- The special [save-lineage-termpath] token only works with content_taxonomy
+ fields as long as you have the "Save option" set to either "Tag" or "Both".
+- In hierarchies where items can have multiple parent items and where you have
+ enabled Hierarchical Select's "save lineage" setting, it is impossible to
+ remember individual hierarchies, unless the underlying module supports it.
+ So far, no module supports this. Hierarchical Select is just a form element,
+ not a system for storing hierarchies.
+ For example, if you have created a multiple parent vocabulary through the
+ Taxonomy module, and you have terms like this:
+ A -> C
+ A -> D
+ B -> C
+ B -> D
+ If you then save any two lineages in which all four terms exist, all four
+ lineages will be rendered by Hierarchical Select, because only the four
+ terms are stored and thus there is no way to recover the originally selected
+ two lineages.
+- You can NOT expect the Hierarchical Select Taxonomy module to automagically
+ fix all existing nodes when you enable or disable the "save lineage" setting
+ and neither can you expect it to keep working properly when you reorganize
+ the term hierarchy. There's nothing I can do about this. Hierarchical Select
+ is merely a form element, it can't be held responsible for features that
+ Drupal core lacks or supports poorly.
+ See the following issues:
+ * http://drupal.org/node/1023762#comment-4054386
+ * http://drupal.org/node/976394#comment-4054456
+
+
+Rendering hierarchy lineages when viewing content
+-------------------------------------------------
+Hierarchical Select is obviously only used for input. Hence it is only used on
+the create/edit forms of content.
+Combine that with the fact that Hierarchical Select is the only module capable
+of restoring the lineage of saved items (e.g. Taxonomy terms). None of the
+Drupal core modules is capable of storing the lineage, but Hierarchical Select
+can reconstruct it relatively efficiently. However, this lineage is only
+visible when creating/editing content, not when viewing it.
+To allow you to display the lineages of stored items, I have provided a
+theming function that you can call from within e.g. your node.tpl.php file:
+the theme_hierarchical_select_selection_as_lineages($selection, $config)
+function.
+
+Sample usage (using Taxonomy and Hierarchical Select Taxonomy):
+
+
taxonomy, $config); ?>
+
+
+This will automatically render all lineages for vocabulary 2 (meaning that if
+you want to render the lineages of multiple vocabularies, you'll have to clone
+this piece of code once for every vocabulary). It will also automatically get
+the current Hierarchical Select configuration for that vocabulary.
+
+Alternatively, you could provide the $config array yourself. Only three keys
+are required: 1) module, 2) params, 3) save_lineage. For example:
+
+
taxonomy, $config); ?>
+
+
+If you don't like how the lineage is displayed, simply override the
+theme_hierarchical_select_selection_as_lineages() function from within your
+theme, create e.g. garland_hierarchical_select_selection_as_lineages().
+
+It's also worth mentioning that the 'hs_taxonomy_tree' tag was added to the
+queries that build the term tree. As a result now you can easily change/filter
+the elements that are selected by the module (see hs_taxonomy.module for more
+info).
+
+
+Setting a fixed size
+--------------------
+When you don't want users to be able to resize a hierarchical select
+themselves, you can set a fixed size in advance yourself
+Setting #size to >1 does *not* generate #multiple = TRUE selects! And the
+opposite is also true. #multiple sets the "multiple" HTML attribute. This
+enables the user to select multiple options of a select. #size just controls
+the "size" HTML attribute. This increases the vertical size of selects,
+thereby showing more options.
+See http://www.w3.org/TR/html401/interact/forms.html#adef-size-SELECT.
+
+
+Sponsors
+--------
+* Initial development:
+ Paul Ektov of http://autobin.ru.
+* Abstraction, to let other modules than taxonomy hook in:
+ Etienne Leers of http://creditcalc.biz.
+* Support for saving the term lineage:
+ Paul Ektov of http://autobin.ru.
+* Multiple select support:
+ Marmaladesoul, http://marmaladesoul.com.
+* Taxonomy Subscriptions support:
+ Mr Bidster Inc.
+* Ability to create new items/levels:
+ The Worx Company, http://www.worxco.com.
+* Ability to only show items that are associated with at least one entity:
+ Merge, http://merge.nl.
+* Views 2 support:
+ Merge, http://merge.nl.
+* Initial Drupal 7 port + folow-up fixes:
+ PingV, http://pingv.com.
+* Port of "save lineage" functionality to Drupal 7:
+ Bancard Data Service
+
+
+Author
+------
+Wim Leers
+
+* website: http://wimleers.com/
+* contact: http://wimleers.com/contact
+
+The author can be contacted for paid development on this module. This can vary
+from new features to Hierarchical Select itself, to new implementations (i.e.
+support for new kinds of hierarchies).
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/TODO.txt b/sites/all/modules/contrib/fields/hierarchical_select/TODO.txt
new file mode 100644
index 00000000..0e55ab54
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/TODO.txt
@@ -0,0 +1,44 @@
+HS core:
+✓ port: initial port
+✓ fix: JS code cleanup (remove hardcoded hacks)
+✓ fix: title + description (i.e. something's off with the theme wrapper)
+✓ fix: #value_callback may be necessary? (see file.module) OR: ensure #return_value works
+✓ fix: #element_validate callback: _hierarchical_select_validate() — verify this still works
+✓ port: support multiple HS on the same page
+✓ port: admin UI
+✓ port: "dropbox" support
+✓ upgrade path: delete cache_hierarchical_select
+✓ upgrade path: documentation
+✓ port: "create new item" support — see http://drupal.org/node/1087620
+✓ port: status report
+- port: render_flat_select support
+- port: client-side caching (use _hierarchical_select_json_convert_hierarchy_to_cache())
+- feature: live preview of HS on the common config form
+- refactor: use the proper #value_callback -> #process callback -> #after_build callback pipeline as described in the documentation for form_builder() in form.inc
+
+Taxonomy:
+✓ port: admin UI
+✓ port: "dropbox" support
+✓ port: "save lineage" support (i.e. support multiple parents, automatic warning shown through hs_taxonomy_hierarchical_select_root_level())
+✓ port: field formatters (from content_taxonomy)
+✓ port: taxonomy term (create/edit) form should be altered to include HS
+✓ upgrade path: migrate settings (no migration necessary)
+✓ upgrade path: documentation (no migration, no docs)
+✓ port: "create new item" support — see http://drupal.org/node/1087620
+- port: "entity_count" support — see http://drupal.org/node/1068462
+- refactor: use the vocabulary machine name internally instead of the vid
+- port: token support — see http://drupal.org/node/1248908
+- port: forum support
+- refactor: optimize HS API implementation: take advantage of improvements in Taxonomy
+
+HS Taxonomy Views:
+- everything — see http://drupal.org/node/1170192
+
+Menu:
+✓ everything
+
+Flat List:
+✓ everything
+
+Small Hierarchy:
+✓ everything
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/UPGRADE.txt b/sites/all/modules/contrib/fields/hierarchical_select/UPGRADE.txt
new file mode 100644
index 00000000..f3473ee4
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/UPGRADE.txt
@@ -0,0 +1,20 @@
+# Upgrading (from Drupal 6 to 7)
+
+1. **BE WARE THAT NOT ALL FUNCTIONALITY HAS BEEN PORTED!**
+
+ Make sure that you know if the part of Hierarchical Select's functionality
+ that you want to use has been ported. Otherwise, you may be in for a
+ frustrating upgrade experience.
+
+ See the included TODO.txt file for details. In a nutshell:
+
+ - Taxonomy support is almost complete, only "create new item", "entity count" and token support are missing
+ - Forum support has **not** yet been ported (but relies on Taxonomy, so this is trivial)
+ - Taxonomy Views support has **not** yet been ported
+ - Menu support has **not** yet been ported
+
+2. Upgrade this module just like any other: delete the old module, copy the
+ files of the new module and run update.php.
+ For details, see .
+
+3. That's it! :)
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select-rtl.css b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select-rtl.css
new file mode 100644
index 00000000..6bc6c2fa
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select-rtl.css
@@ -0,0 +1,57 @@
+
+
+/* The hierarchical select. */
+.hierarchical-select-wrapper .hierarchical-select .selects {
+ float: right; /* If a block is floated, it won't consume as much width as
+ available, only just enough. This allows the grippie to
+ perfectly scale with the with consumed by the selects. */
+}
+
+.hierarchical-select-wrapper .hierarchical-select .selects .grippie {
+ clear: right; /* clear: left; */
+ height: 9px;
+ overflow: hidden;
+ background: #eee url(images/grippie.png) no-repeat center 2px;
+ border: 1px solid #ddd;
+ border-top-width: 0;
+ cursor: s-resize;
+ margin-left: 0.5em; /* margin-right: 0.5em; */ /* Give the grippie the same margin as each select. */
+ min-width: 70px; /* Hack for IE, makes the grip usable, but not yet the same as in other browsers. */
+}
+
+.hierarchical-select-wrapper .hierarchical-select select,
+.hierarchical-select-wrapper .hierarchical-select .add-to-dropbox,
+.hierarchical-select-wrapper .hierarchical-select .create-new-item {
+ margin-left: .5em;
+ margin-right: 0; /* Reset ltr style */
+ float: right;
+}
+
+
+/* The pseudo-modal window for creating a new item or new level. */
+.hierarchical-select-wrapper .hierarchical-select .create-new-item-create,
+.hierarchical-select-wrapper .hierarchical-select .create-new-item-cancel {
+ float: left;
+ margin-right: .4em;
+ margin-left: 0; /* Reset ltr style */
+}
+
+.hierarchical-select-wrapper .hierarchical-select .create-new-item-input {
+ float: right;
+ clear: left;
+}
+
+
+/* Child level indicator. */
+.hierarchical-select-wrapper .hierarchical-select option.has-children {
+ background: url(images/arrow-rtl.png) no-repeat left center;
+ padding-left: 20px;
+ padding-right: 0;
+}
+
+
+/* Dropbox limit warning.*/
+p.hierarchical-select-dropbox-limit-warning {
+ padding-right: .5em;
+ padding-left: 0; /* Reset ltr style */
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.admin.inc b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.admin.inc
new file mode 100644
index 00000000..fb172179
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.admin.inc
@@ -0,0 +1,313 @@
+ t('All settings below will be used as site-wide defaults.'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $form['hierarchical_select_animation_delay'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Animation delay'),
+ '#description' => t(
+ 'The delay that will be used for the "drop in/out" effect when a
+ hierarchical select is being updated (in milliseconds).'
+ ),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#default_value' => variable_get('hierarchical_select_animation_delay', 400),
+ );
+ $form['hierarchical_select_level_labels_style'] = array(
+ '#type' => 'select',
+ '#title' => t('Level labels style'),
+ '#description' => t(
+ 'The style that will be used for level labels. This is not supported by
+ all browsers! If you want a consistent interface, choose to use no
+ style.'
+ ),
+ '#options' => array(
+ 'none' => t('No style'),
+ 'bold' => t('Bold'),
+ 'inversed' => t('Inversed'),
+ 'underlined' => t('Underlined'),
+ ),
+ '#default_value' => variable_get('hierarchical_select_level_labels_style', 'none'),
+ );
+ // TODO: port the HS client-side cache system to Drupal 7.
+ /*
+ $form['hierarchical_select_js_cache_system'] = array(
+ '#type' => 'radios',
+ '#title' => t('Cache in a HTML 5 client-side database'),
+ '#description' => t(
+ 'This feature only works in browsers that support the
+ HTML 5 client-side database storage specification
+ .
+ After enabling this, you will notice (in supporting browsers) that
+ refreshing the hierarchical select will not require a request to the
+ server when a part is being requested that has been requested before.',
+ array('!spec-url' => url('http://www.whatwg.org/specs/web-apps/current-work/multipage/section-sql.html'))
+ ),
+ '#options' => array(
+ 0 => t('Disabled'),
+ 1 => t('Enabled'),
+ ),
+ '#default_value' => variable_get('hierarchical_select_js_cache_system', 0),
+ );
+ */
+
+ return system_settings_form($form);
+}
+
+/**
+ * Menu callback; a table that lists all Hierarchical Select configs.
+ */
+function hierarchical_select_admin_configs() {
+ $header = array(t('Hierarchy type'), t('Hierarchy'), t('Entity type'), t('Bundle'), t('Context type'), t('Context'), t('Actions'));
+
+ // Retrieve all information items
+ $info_items = array();
+ foreach (module_implements('hierarchical_select_config_info') as $module) {
+ $info_items = array_merge_recursive($info_items, module_invoke($module, 'hierarchical_select_config_info'));
+ }
+
+ // Process the retrieved information into rows.
+ $rows = array();
+ foreach ($info_items as $id => $item) {
+ $config_id = $item['config_id'];
+
+ $rows[$id] = array(
+ $item['hierarchy type'],
+ $item['hierarchy'],
+ $item['entity type'],
+ $item['bundle'],
+ $item['context type'],
+ $item['context'],
+ theme('links', array('links' => array(
+ array(
+ 'title' => t('Edit'),
+ 'href' => $item['edit link'],
+ 'fragment' => "hierarchical-select-config-form-$config_id",
+ ),
+ array(
+ 'title' => t('Export'),
+ 'href' => "admin/config/content/hierarchical_select/export/$config_id",
+ ),
+ array(
+ 'title' => t('Import'),
+ 'href' => "admin/config/content/hierarchical_select/import/$config_id",
+ ),
+ ))),
+ );
+ }
+
+ return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array(), 'caption' => t('Overview of all Hierarchical Select configurations.')));
+}
+
+/**
+ * Menu callback; a table that lists all Hierarchical Select implementations
+ * and the features they support.
+ */
+function hierarchical_select_admin_implementations() {
+ $output = '';
+ $header = array(t('Implementation (module)'), t('Hierarchy type'), t('Entity type'), t('Create new items'), t('Entity count'));
+
+ // Retrieve all information items
+ $rows = array();
+ foreach (module_implements('hierarchical_select_root_level') as $module) {
+ $filename = db_query("SELECT filename FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => $module))->fetchField();
+ $module_info = drupal_parse_info_file(dirname($filename) . "/$module.info");
+ // Try to extract the hierarchy type from the optional hook_hierarchical_select_config_info().
+ $hierarchy_type = $entity_type = t('unknown');
+ if (module_hook($module, 'hierarchical_select_implementation_info')) {
+ $implementation = module_invoke($module, 'hierarchical_select_implementation_info');
+ $hierarchy_type = $implementation['hierarchy type'];
+ $entity_type = $implementation['entity type'];
+ }
+
+ $rows[] = array(
+ $module_info['name'],
+ $hierarchy_type,
+ $entity_type,
+ (module_hook($module, 'hierarchical_select_create_item')) ? t('Yes') : t('No'),
+ (module_hook($module, 'hierarchical_select_entity_count')) ? t('Yes') : t('No'),
+ );
+ }
+
+ $output .= '
';
+ $output .= t('
+ The table below allows you to find out which Hierarchical Select
+ features are supported by the implementations of the Hierarchical
+ Select API.
+ It is not a reflection of some settings.
+ ');
+ $output .= '
';
+
+ $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array(), 'caption' => t('Overview of all installed Hierarchical Select implementations.')));
+
+ return $output;
+}
+
+/**
+ * Form definition; config export form.
+ */
+function hierarchical_select_admin_export($form, &$form_state, $config_id) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $config = hierarchical_select_common_config_get($config_id);
+ $code = _hierarchical_select_create_export_code($config);
+
+ drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css');
+ drupal_add_js('$(document).ready(function() { $(".hierarchical-select-code").focus(); });', array('type' => 'inline', 'scope' => JS_DEFAULT));
+
+ $lines = substr_count($code, "\n") + 1;
+ $form['config'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Hierarchical Select configuration %config_id', array('%config_id' => $config_id)),
+ '#default_value' => $code,
+ '#rows' => $lines,
+ '#attributes' => array('class' => array('hierarchical-select-config-code')),
+ );
+
+ return $form;
+}
+
+/**
+ * Form definition; config import form.
+ */
+function hierarchical_select_admin_import($form, &$form_state, $config_id) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css');
+ drupal_add_js('$(document).ready(function() { $(".hierarchical-select-code").focus(); });', array('type' => 'inline', 'scope' => JS_DEFAULT));
+
+ $form['config'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Import Hierarchical Select configuration code'),
+ '#cols' => 60,
+ '#rows' => 15,
+ '#description' => t('Copy and paste the results of an exported
+ Hierarchical Select configuration here. This will override the
+ current Hierarchical Select configuration for %config_id.',
+ array('%config_id' => $config_id)
+ ),
+ '#attributes' => array('class' => array('hierarchical-select-config-code')),
+ );
+ $form['interpreted_config'] = array('#type' => 'value', '#value' => NULL);
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t("Import"),
+ );
+ $form_state['#redirect'] = NULL;
+ return $form;
+}
+
+/**
+ * Validate callback; config import form.
+ */
+function hierarchical_select_admin_import_validate($form, &$form_state) {
+ ob_start();
+ eval($form_state['values']['config']);
+ ob_end_clean();
+
+ form_set_value($form['interpreted_config'], serialize($config), $form_state);
+
+ if (empty($form_state['values']['config'])) {
+ form_error($form['config'], t('You did not enter anything.'));
+ }
+ elseif ($config == NULL) {
+ form_error($form['config'], t('There is a syntax error in the Hierarchical Select configuration you entered.'));
+ }
+ elseif (!isset($config['config_id']) || empty($config['config_id'])) {
+ form_error($form['config'], t('Unable to import this configuration, because no Hierarchical Select config id is set.'));
+ }
+}
+
+/**
+ * Submit callback; config import form.
+ */
+function hierarchical_select_admin_import_submit($form, &$form_state) {
+ $config = unserialize($form_state['values']['interpreted_config']);
+ $config_id = $config['config_id'];
+ hierarchical_select_common_config_set($config_id, $config);
+ drupal_set_message(t('Hierarchical Select configuration for %config_id imported!', array('%config_id' => $config_id)));
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Given a config array, create the export code for it.
+ *
+ * @param array $config
+ * A Hierarchical Select config array, as described in API.txt
+ * @return string
+ * The code as it would appear in an editor.
+ */
+function _hierarchical_select_create_export_code($config) {
+ $output = _hierarchical_select_create_code_from_array($config);
+ $output = '$config = ' . $output . ";\n";
+ return $output;
+}
+
+/**
+ * Given a array, create the export code for it.
+ *
+ * This functions is a refactoring of features_var_export() to use with the
+ * hierarchical select module.
+ *
+ * @param mixed $config
+ * A value to export as code.
+ * @param string $prefix
+ * Padding for nested array.
+ * @param boolean $init
+ * Indicator of the first level of the export.
+ * @return string
+ * The code as it would appear in an editor.
+ */
+function _hierarchical_select_create_code_from_array($var, $prefix = '', $init = TRUE) {
+ $output = "";
+ $type = gettype($var);
+ switch ($type) {
+ case 'array':
+ if (empty($var)) {
+ $output = "array()";
+ }
+ else {
+ $output = "array(\n";
+ foreach ($var as $key => $value) {
+ $value = _hierarchical_select_create_code_from_array($value, ' ', FALSE);
+ $output .= " '$key' => " . $value . ",\n";
+ }
+ $output .= ')';
+ }
+ break;
+ case 'string':
+ $var = str_replace("\n", "***BREAK***", $var);
+ $output = var_export($var, TRUE);
+ break;
+ case 'boolean':
+ $var = empty($var) ? 'FALSE' : 'TRUE';
+ $output = var_export($var, TRUE);
+ break;
+ default:
+ $output = var_export($var, TRUE);
+ }
+ if ($prefix) {
+ $output = str_replace("\n", "\n$prefix", $output);
+ }
+ if ($init) {
+ $output = str_replace("***BREAK***", "\n", $output);
+ }
+ return $output;
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.css b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.css
new file mode 100644
index 00000000..5f20a75e
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.css
@@ -0,0 +1,226 @@
+
+
+/* The hierarchical select. */
+.hierarchical-select-wrapper .hierarchical-select .selects {
+ float: left; /* If a block is floated, it won't consume as much width as
+ available, only just enough. This allows the grippie to
+ perfectly scale with the with consumed by the selects. */
+}
+
+.hierarchical-select-wrapper .hierarchical-select .selects .grippie {
+ clear: left;
+ height: 9px;
+ overflow: hidden;
+ background: #eee url(images/grippie.png) no-repeat center 2px;
+ border: 1px solid #ddd;
+ border-top-width: 0;
+ cursor: s-resize;
+ margin-right: 0.5em; /* Give the grippie the same margin as each select. */
+ min-width: 50px; /* Hack for IE, makes the grip usable, but not yet the same as in other browsers. */
+}
+
+.hierarchical-select-wrapper .hierarchical-select select,
+.hierarchical-select-wrapper .hierarchical-select .add-to-dropbox,
+.hierarchical-select-wrapper .hierarchical-select .create-new-item {
+ margin: 0;
+ margin-right: .5em;
+ margin-bottom: 3px;
+ float: left;
+}
+
+
+/* The flat select (only used in GET forms). */
+.hierarchical-select-wrapper .flat-select {
+ display: none;
+}
+
+
+/* The pseudo-modal window for creating a new item or new level. */
+.hierarchical-select-wrapper .hierarchical-select .create-new-item {
+ padding: .7em;
+ border: 2px outset gray;
+}
+
+.hierarchical-select-wrapper .hierarchical-select .create-new-item {
+ width: 11em;
+}
+
+.hierarchical-select-wrapper .hierarchical-select .create-new-item-create,
+.hierarchical-select-wrapper .hierarchical-select .create-new-item-cancel {
+ float: right;
+ margin: 0;
+ margin-left: .4em;
+}
+
+.hierarchical-select-wrapper .hierarchical-select .create-new-item-input {
+ width: 10.5em;
+ margin: 0;
+ margin-bottom: 1em;
+ float: left;
+ clear: right;
+}
+
+
+/* Level labels styles. */
+.hierarchical-select-level-labels-style-bold .hierarchical-select select option.level-label {
+ font-weight: bold;
+}
+
+.hierarchical-select-level-labels-style-inversed .hierarchical-select select option.level-label {
+ background-color: #000000;
+ color: #FFFFFF;
+}
+
+.hierarchical-select-level-labels-style-underlined .hierarchical-select select option.level-label {
+ text-decoration: underline;
+}
+
+
+/* Child level indicator. */
+.hierarchical-select-wrapper .hierarchical-select option.has-children {
+ background: url(images/arrow.png) no-repeat right center;
+ padding-right: 20px;
+}
+
+
+/* Dropbox limit warning.*/
+p.hierarchical-select-dropbox-limit-warning {
+ padding: 0;
+ color: #F7A54F;
+ font-size: 110%;
+ padding-left: .5em;
+}
+
+
+/* The dropbox table. */
+.hierarchical-select-wrapper .dropbox-title {
+ font-size: 115%;
+ color: #898989;
+ margin-bottom: 0.2em;
+}
+
+.hierarchical-select-wrapper .dropbox {
+ display: inline-block;
+ margin: .5em 0;
+}
+
+.hierarchical-select-wrapper .dropbox table {
+ margin: 0;
+ width: auto;
+ max-width: 100%;
+ min-width: 20em;
+ color: gray;
+ font-size: 90%;
+ border: 1px solid gray;
+}
+
+tr.dropbox-entry {
+ line-height: 1.3em;
+ padding: .3em .6em;
+}
+
+tr.dropbox-entry.even {
+ background-color: transparent;
+ border-bottom: 1px solid #CCCCCC;
+}
+
+tr.dropbox-entry.odd {
+ background-color: #EDF5FA;
+ border-bottom: 1px solid #CCCCCC;
+}
+
+tr.dropbox-entry.first {
+ border-top: 1px solid gray;
+}
+
+tr.dropbox-entry.last {
+ border-bottom: 1px solid gray;
+}
+
+.dropbox-selected-item {
+ font-weight: bold;
+}
+
+.hierarchical-select-item-separator {
+ padding-left: .5em;
+ padding-right: .5em;
+}
+
+td.dropbox-remove *,
+td.dropbox-remove a:link,
+td.dropbox-remove a:visited {
+ color: #F7A54F;
+ text-decoration: none;
+}
+
+td.dropbox-remove a:hover {
+ text-decoration: underline;
+}
+
+tr.dropbox-is-empty {
+ padding: .5em 1em;
+}
+
+
+/* The "Update" button and help text (used when Javascript is disabled). */
+.hierarchical-select-wrapper .nojs .update-button {
+ margin: 0 0 1em;
+}
+
+.hierarchical-select-wrapper .nojs .help-text {
+ font-size: 90%;
+ color: transparent;
+ display: block;
+ border: 1px dotted black;
+ overflow: hidden;
+ width: 34em;
+ height: 1.2em;
+ padding: .6em;
+ line-height: normal;
+}
+
+.hierarchical-select-wrapper .nojs .help-text:hover {
+ height: auto;
+ width: auto;
+ min-width: 25em;
+ max-width: 45em;
+ color: gray;
+}
+
+.hierarchical-select-wrapper .nojs .help-text .ask-to-hover {
+ color: gray;
+ font-style: italic;
+}
+
+.hierarchical-select-wrapper .nojs .help-text:hover .ask-to-hover {
+ display: none;
+}
+
+.hierarchical-select-wrapper .nojs .help-text .highlight {
+ text-decoration: underline;
+}
+
+.hierarchical-select-wrapper .nojs .help-text .warning {
+ color: red;
+}
+
+.hierarchical-select-wrapper .nojs .help-text .solutions {
+ margin: 0;
+ padding: 0;
+}
+
+
+/* The 'waiting' class is set dynamically, during a callback to the server. */
+.hierarchical-select-wrapper.waiting {
+ opacity: 0.5;
+
+ /* IE doesn't support CSS 2 properly. */
+ zoom: 1;
+ filter: alpha(opacity=50);
+}
+
+
+/* Use a monospace font for the import/export config code text areas. */
+.hierarchical-select-config-code {
+ font-family: 'Monaco', 'Lucida Console', 'Consolas', monospace;
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.features.inc b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.features.inc
new file mode 100644
index 00000000..301e187c
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.features.inc
@@ -0,0 +1,99 @@
+ $config) {
+ $dependencies[$config_id] = $module;
+ }
+ }
+
+ // Add features and dependencies.
+ foreach ($data as $config_id) {
+ $export['features']['hierarchical_select'][$config_id] = $config_id;
+ if (isset($dependencies[$config_id])) {
+ $module = $dependencies[$config_id];
+ $export['dependencies'][$module] = $module;
+ }
+ }
+
+ return array();
+}
+
+/**
+ * Implements hook_features_export_options().
+ */
+function hierarchical_select_features_export_options() {
+ // Retrieve all information items.
+ $info_items = array();
+ foreach (module_implements('hierarchical_select_config_info') as $module) {
+ $info_items = array_merge_recursive($info_items, module_invoke($module, 'hierarchical_select_config_info'));
+ }
+
+ // Process the retrieved information into options.
+ $options = array();
+ foreach ($info_items as $id => $item) {
+ $config_id = $item['config_id'];
+ $options[$config_id] = $item['hierarchy type'] . ': ' . $item['hierarchy'] . ' - ' . $item['context type'] . (!empty($item['context']) ? ': ' . $item['context'] : '');
+ }
+
+ return $options;
+}
+
+/**
+ * Implements hook_features_export_render().
+ */
+function hierarchical_select_features_export_render($module, $data) {
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ module_load_include('inc', 'hierarchical_select', 'hierarchical_select.admin');
+
+ $code = array();
+ $code[] = '$configs = array();';
+ foreach ($data as $config_id) {
+ $config = hierarchical_select_common_config_get($config_id);
+ $config['config_id'] = $config_id;
+
+ $code[] = _hierarchical_select_create_export_code($config);
+ $code[] = "\$configs['{$config_id}'] = \$config;";
+ }
+ $code[] = "return \$configs;";
+ $code = implode("\n", $code);
+
+ return array('hierarchical_select_default_configs' => $code);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function hierarchical_select_features_revert($module) {
+ hierarchical_select_features_rebuild($module);
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function hierarchical_select_features_rebuild($module) {
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ $configs = features_get_default('hierarchical_select', $module);
+ if (!empty($configs)) {
+ // Apply the configuration.
+ require_once(drupal_get_path('module', 'hierarchical_select') .'/includes/common.inc');
+
+ foreach ($configs as $config_id => $config) {
+ hierarchical_select_common_config_set($config_id, $config);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.info b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.info
new file mode 100644
index 00000000..2d82da5e
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.info
@@ -0,0 +1,14 @@
+name = Hierarchical Select
+description = Simplifies the selection of one or multiple items in a hierarchical tree.
+package = Form Elements
+
+core = 7.x
+configure = admin/config/content/hierarchical_select
+files[] = tests/internals.test
+
+; Information added by Drupal.org packaging script on 2017-02-15
+version = "7.x-3.0-beta8"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1487167708"
+
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.install b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.install
new file mode 100644
index 00000000..825c2a00
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.install
@@ -0,0 +1,84 @@
+condition('name', 'hs_config_%', 'LIKE')
+ ->execute();
+
+ db_delete('variable')
+ ->condition('name', 'hierarchical_select_%', 'LIKE')
+ ->execute();
+}
+
+
+//----------------------------------------------------------------------------
+// Updates.
+
+/**
+ * Update Hierarchical Select to Drupal 7. Basically remove a lot of cruft.
+ */
+function hierarchical_select_update_7001() {
+ // Drop Hierarchical Select's cache table, which is now obsolete.
+ db_drop_table('cache_hierarchical_select');
+
+ // Undo Hierarchical Select module weight changes, because they're no longer
+ // necessary.
+ db_update('system')
+ ->fields(array(
+ 'weight' => 0,
+ ))
+ ->condition('name', 'hierarchical_select')
+ ->execute();
+}
+
+/**
+ * Update Hierarchical Select config to support improved "entity count".
+ */
+function hierarchical_select_update_7002() {
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ // Retrieve all information items.
+ $info_items = array();
+ foreach (module_implements('hierarchical_select_config_info') as $module) {
+ $info_items = array_merge_recursive($info_items, module_invoke($module, 'hierarchical_select_config_info'));
+ }
+ foreach ($info_items as $info_item) {
+ // Load config.
+ $config = hierarchical_select_common_config_get($info_item['config_id']);
+
+ // Move old settings to new location.
+ $config['entity_count'] = array(
+ 'enabled' => $config['entity_count'],
+ 'require_entity' => $config['require_entity'],
+ );
+
+ // Remove old setting.
+ unset($config['require_entity']);
+
+ // Add entity types settings.
+ $entity_info = entity_get_info();
+ foreach ($entity_info as $entity => $entity_info) {
+ if (!empty($entity_info['bundles']) && $entity_info['fieldable'] === TRUE) {
+ foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
+ if ($entity == 'node') {
+ $config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle] = $bundle;
+ }
+ else {
+ $config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle] = 0;
+ }
+ }
+ }
+ }
+
+ // Save new config.
+ hierarchical_select_common_config_set($info_item['config_id'], $config);
+ }
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.js b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.js
new file mode 100644
index 00000000..df98f65a
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.js
@@ -0,0 +1,702 @@
+
+(function($) {
+
+Drupal.behaviors.HierarchicalSelect = {
+ attach: function (context) {
+ $('.hierarchical-select-wrapper:not(.hierarchical-select-wrapper-processed)', context)
+ .addClass('hierarchical-select-wrapper-processed').each(function() {
+ var hsid = $(this).attr('id').replace(/^hierarchical-select-(.+)-wrapper$/, "$1");
+ Drupal.HierarchicalSelect.initialize(hsid);
+ });
+ }
+};
+
+Drupal.HierarchicalSelect = {};
+
+Drupal.HierarchicalSelect.state = [];
+
+Drupal.HierarchicalSelect.context = function() {
+ return $("form .hierarchical-select-wrapper");
+};
+
+Drupal.HierarchicalSelect.initialize = function(hsid) {
+ // Prevent JS errors when Hierarchical Select is loaded dynamically.
+ if (undefined == Drupal.settings.HierarchicalSelect || undefined == Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]) {
+ return false;
+ }
+
+ // If you set Drupal.settings.HierarchicalSelect.pretendNoJS to *anything*,
+ // and as such, Hierarchical Select won't initialize its Javascript! It
+ // will seem as if your browser had Javascript disabled.
+ if (undefined != Drupal.settings.HierarchicalSelect.pretendNoJS) {
+ return false;
+ }
+
+ var form = $('#hierarchical-select-'+ hsid +'-wrapper').parents('form');
+
+ // Pressing the 'enter' key on a form that contains an HS widget, depending
+ // on which browser, usually causes the first submit button to be pressed
+ // (likely an HS button). This results in unpredictable behaviour. There is
+ // no way to determine the 'real' submit button, so disable the enter key.
+ form.find('input').keypress(function(event) {
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ return false;
+ }
+ });
+
+ // Turn off Firefox' autocomplete feature. This causes Hierarchical Select
+ // form items to be disabled after a hard refresh.
+ // See http://drupal.org/node/453048 and
+ // http://www.ryancramer.com/journal/entries/radio_buttons_firefox/
+ if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
+ form.attr('autocomplete', 'off');
+ }
+
+ // Enable *all* submit buttons in this form, as well as all input-related
+ // elements of the current hierarchical select, in case we reloaded while
+ // they were disabled.
+ form.add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select')
+ .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select input')
+ .attr('disabled', false);
+
+ if (this.cache != null) {
+ this.cache.initialize();
+ }
+
+ Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['updatesEnabled'] = true;
+ if (undefined == Drupal.HierarchicalSelect.state["hs-" + hsid]) {
+ Drupal.HierarchicalSelect.state["hs-" + hsid] = {};
+ }
+
+ this.transform(hsid);
+ if (Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].resizable) {
+ this.resizable(hsid);
+ }
+ Drupal.HierarchicalSelect.attachBindings(hsid);
+
+ if (this.cache != null && this.cache.status()) {
+ this.cache.load(hsid);
+ }
+
+ Drupal.HierarchicalSelect.log(hsid);
+};
+
+Drupal.HierarchicalSelect.log = function(hsid, messages) {
+ // Only perform logging if logging is enabled.
+ if (Drupal.settings.HierarchicalSelect.initialLog == undefined || Drupal.settings.HierarchicalSelect.initialLog["hs-" + hsid] == undefined) {
+ return;
+ }
+ else {
+ Drupal.HierarchicalSelect.state["hs-" + hsid].log = [];
+ }
+
+ // Store the log messages. The first call to this function may not contain a
+ // message: the initial log included in the initial HTML rendering should be
+ // used instead..
+ if (Drupal.HierarchicalSelect.state["hs-" + hsid].log.length == 0) {
+ Drupal.HierarchicalSelect.state["hs-" + hsid].log.push(Drupal.settings.HierarchicalSelect.initialLog["hs-" + hsid]);
+ }
+ else {
+ Drupal.HierarchicalSelect.state["hs-" + hsid].log.push(messages);
+ }
+
+ // Print the log messages.
+ console.log("HIERARCHICAL SELECT " + hsid);
+ var logIndex = Drupal.HierarchicalSelect.state["hs-" + hsid].log.length - 1;
+ for (var i = 0; i < Drupal.HierarchicalSelect.state["hs-" + hsid].log[logIndex].length; i++) {
+ console.log(Drupal.HierarchicalSelect.state["hs-" + hsid].log[logIndex][i]);
+ }
+ console.log(' ');
+};
+
+Drupal.HierarchicalSelect.transform = function(hsid) {
+ var removeString = $('#hierarchical-select-'+ hsid +'-wrapper .dropbox .dropbox-remove:first', Drupal.HierarchicalSelect.context).text();
+
+ $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context)
+ // Remove the .nojs div.
+ .find('.nojs').hide().end()
+ // Find all .dropbox-remove cells in the dropbox table.
+ .find('.dropbox .dropbox-remove')
+ // Hide the children of these table cells. We're not removing them because
+ // we want to continue to use the "Remove" checkboxes.
+ .find('*').css('display', 'none').end() // We can't use .hide() because of collapse.js: http://drupal.org/node/351458#comment-1258303.
+ // Put a "Remove" link there instead.
+ .append(''+ removeString +'');
+};
+
+Drupal.HierarchicalSelect.resizable = function(hsid) {
+ var $selectsWrapper = $('#hierarchical-select-' + hsid + '-wrapper .hierarchical-select .selects', Drupal.HierarchicalSelect.context);
+
+ // No select wrapper present: the user is creating a new item.
+ if ($selectsWrapper.length == 0) {
+ return;
+ }
+
+ // Append the drag handle ("grippie").
+ $selectsWrapper.append($(''));
+
+ // jQuery object that contains all selects in the hierarchical select, to
+ // speed up DOM manipulation during dragging.
+ var $selects = $selectsWrapper.find('select');
+
+ var defaultPadding = parseInt($selects.slice(0, 1).css('padding-top').replace(/^(\d+)px$/, "$1")) + parseInt($selects.slice(0, 1).css('padding-bottom').replace(/^(\d+)px$/, "$1"));
+ var defaultHeight = Drupal.HierarchicalSelect.state["hs-" + hsid].defaultHeight = $selects.slice(0, 1).height() + defaultPadding;
+ var defaultSize = Drupal.HierarchicalSelect.state["hs-" + hsid].defaultSize = $selects.slice(0, 1).attr('size');
+ defaultSize = (defaultSize == 0) ? 1 : defaultSize;
+ var margin = Drupal.HierarchicalSelect.state["hs-" + hsid].margin = parseInt($selects.slice(0, 1).css('margin-bottom').replace(/^(\d+)px$/, "$1"));
+
+ // Bind the drag event.
+ $('.grippie', $selectsWrapper)
+ .mousedown(startDrag)
+ .dblclick(function() {
+ if (Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight == undefined) {
+ Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight = defaultHeight;
+ }
+ var resizedHeight = Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight = (Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight > defaultHeight + 2) ? defaultHeight : 4.6 / defaultSize * defaultHeight;
+ Drupal.HierarchicalSelect.resize($selects, defaultHeight, resizedHeight, defaultSize, margin);
+ });
+
+ function startDrag(e) {
+ staticOffset = $selects.slice(0, 1).height() - e.pageY;
+ $selects.css('opacity', 0.25);
+ $(document).mousemove(performDrag).mouseup(endDrag);
+ return false;
+ }
+
+ function performDrag(e) {
+ var resizedHeight = staticOffset + e.pageY;
+ Drupal.HierarchicalSelect.resize($selects, defaultHeight, resizedHeight, defaultSize, margin);
+ return false;
+ }
+
+ function endDrag(e) {
+ var height = $selects.slice(0, 1).height();
+
+ $(document).unbind("mousemove", performDrag).unbind("mouseup", endDrag);
+ $selects.css('opacity', 1);
+ if (height != Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight) {
+ Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight = (height > defaultHeight) ? height : defaultHeight;
+ }
+ }
+};
+
+Drupal.HierarchicalSelect.resize = function($selects, defaultHeight, resizedHeight, defaultSize, margin) {
+ if (resizedHeight == undefined) {
+ resizedHeight = defaultHeight;
+ }
+
+ $selects
+ .attr('size', (resizedHeight > defaultHeight) ? 2 : defaultSize)
+ .height(Math.max(defaultHeight + margin, resizedHeight)); // Without the margin component, the height() method would allow the select to be sized to low: defaultHeight - margin.
+};
+
+Drupal.HierarchicalSelect.disableForm = function(hsid) {
+ // Disable *all* submit buttons in this form, as well as all input-related
+ // elements of the current hierarchical select.
+ $('form:has(#hierarchical-select-' + hsid +'-wrapper) :submit')
+ .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select')
+ .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select :input')
+ .attr('disabled', true);
+
+ // Add the 'waiting' class. Default style: make everything transparent.
+ $('#hierarchical-select-' + hsid +'-wrapper').addClass('waiting');
+
+ // Indicate that the user has to wait.
+ $('body').css('cursor', 'wait');
+};
+
+Drupal.HierarchicalSelect.enableForm = function(hsid) {
+ // This method undoes everything the disableForm() method did.
+
+ $e = $('form:has(#hierarchical-select-' + hsid +'-wrapper) :submit')
+ .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select :input:not(:submit)');
+
+ // Don't enable the selects again if they've been disabled because the
+ // dropbox limit was exceeded.
+ dropboxLimitExceeded = $('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select-dropbox-limit-warning').length > 0;
+ if (!dropboxLimitExceeded) {
+ $e = $e.add($('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select'));
+ }
+ $e.removeAttr("disabled");
+
+ // Don't enable the 'Add' button again if it's been disabled because the
+ // dropbox limit was exceeded.
+ if (dropboxLimitExceeded) {
+ $('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select :submit')
+ .attr('disabled', true);
+ }
+
+ $('#hierarchical-select-' + hsid +'-wrapper').removeClass('waiting');
+
+ $('body').css('cursor', 'auto');
+};
+
+Drupal.HierarchicalSelect.throwError = function(hsid, message) {
+ // Show the error to the user.
+ alert(message);
+
+ // Log the error.
+ Drupal.HierarchicalSelect.log(hsid, [ message ]);
+
+ // Re-enable the form to allow the user to retry, but reset the selection to
+ // the level label if possible, otherwise the "" option if possible.
+ var $select = $('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select:first');
+ var levelLabelOption = $('option[value^=label_]', $select).val();
+ if (levelLabelOption !== undefined) {
+ $select.val(levelLabelOption);
+ }
+ else {
+ var noneOption = $('option[value=none]', $select).val();
+ if (noneOption !== undefined) {
+ $select.val(noneOption);
+ }
+ }
+ Drupal.HierarchicalSelect.enableForm(hsid);
+};
+
+Drupal.HierarchicalSelect.prepareGETSubmit = function(hsid) {
+ // Remove the name attributes of all form elements that end up in GET,
+ // except for the "flat select" form element.
+ $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context)
+ .find('input, select')
+ .not('.flat-select')
+ .removeAttr('name');
+
+ // Update the name attribute of the "flat select" form element
+ var $flatSelect = $('#hierarchical-select-'+ hsid +'-wrapper .flat-select', Drupal.HierarchicalSelect.context);
+ var newName = $flatSelect.attr('name').replace(/^([a-zA-Z0-9_\-]*)(?:\[flat_select\]){1}(\[\])?$/, "$1$2");
+ $flatSelect.attr('name', newName);
+
+ Drupal.HierarchicalSelect.triggerEvents(hsid, 'prepared-GET-submit', {});
+};
+
+Drupal.HierarchicalSelect.attachBindings = function(hsid) {
+ var updateOpString = $('#hierarchical-select-'+ hsid +'-wrapper .update-button').val();
+ var addOpString = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .add-to-dropbox', Drupal.HierarchicalSelect.context).val();
+ var createNewItemOpString = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .create-new-item-create', Drupal.HierarchicalSelect.context).val();
+ var cancelNewItemOpString = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .create-new-item-cancel', Drupal.HierarchicalSelect.context).val();
+
+ var data = {};
+ data.hsid = hsid;
+
+ $('#hierarchical-select-'+ hsid +'-wrapper', this.context)
+ // "disable-updates" event
+ .unbind('disable-updates').bind('disable-updates', data, function(e) {
+ Drupal.settings.HierarchicalSelect.settings["hs-" + e.data.hsid]['updatesEnabled'] = false;
+ })
+
+ // "enforce-update" event
+ .unbind('enforce-update').bind('enforce-update', data, function(e, extraPost) {
+ Drupal.HierarchicalSelect.update(e.data.hsid, 'enforced-update', { opString: updateOpString, extraPost: extraPost });
+ })
+
+ // "prepare-GET-submit" event
+ .unbind('prepare-GET-submit').bind('prepare-GET-submit', data, function(e) {
+ Drupal.HierarchicalSelect.prepareGETSubmit(e.data.hsid);
+ })
+
+ // "update-hierarchical-select" event
+ .find('.hierarchical-select .selects select').unbind().change(function(_hsid) {
+ return function() {
+ if (Drupal.settings.HierarchicalSelect.settings["hs-" + _hsid]['updatesEnabled']) {
+ Drupal.HierarchicalSelect.update(_hsid, 'update-hierarchical-select', { opString: updateOpString, select_id : $(this).attr('id') });
+ }
+ };
+ }(hsid)).end()
+
+ // "create-new-item" event
+ .find('.hierarchical-select .create-new-item .create-new-item-create').unbind().click(function(_hsid) {
+ return function() {
+ Drupal.HierarchicalSelect.update(_hsid, 'create-new-item', { opString : createNewItemOpString });
+ return false; // Prevent the browser from POSTing the page.
+ };
+ }(hsid)).end()
+
+ // "cancel-new-item" event"
+ .find('.hierarchical-select .create-new-item .create-new-item-cancel').unbind().click(function(_hsid) {
+ return function() {
+ Drupal.HierarchicalSelect.update(_hsid, 'cancel-new-item', { opString : cancelNewItemOpString });
+ return false; // Prevent the browser from POSTing the page (in case of the "Cancel" button).
+ };
+ }(hsid)).end()
+
+ // "add-to-dropbox" event
+ .find('.hierarchical-select .add-to-dropbox').unbind().click(function(_hsid) {
+ return function() {
+ Drupal.HierarchicalSelect.update(_hsid, 'add-to-dropbox', { opString : addOpString });
+ return false; // Prevent the browser from POSTing the page.
+ };
+ }(hsid)).end()
+
+ // "remove-from-dropbox" event
+ // (anchors in the .dropbox-remove cells in the .dropbox table)
+ .find('.dropbox .dropbox-remove a').unbind().click(function(_hsid) {
+ return function() {
+ var isDisabled = $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context).attr('disabled');
+
+ // If the hierarchical select is disabled, then ignore this click.
+ if (isDisabled) {
+ return false;
+ }
+
+ // Check the (hidden, because JS is enabled) checkbox that marks this
+ // dropbox entry for removal.
+ $(this).parent().find('input[type=checkbox]').attr('checked', true);
+ Drupal.HierarchicalSelect.update(_hsid, 'remove-from-dropbox', { opString: updateOpString });
+ return false; // Prevent the browser from POSTing the page.
+ };
+ }(hsid));
+};
+
+Drupal.HierarchicalSelect.preUpdateAnimations = function(hsid, updateType, lastUnchanged, callback) {
+ switch (updateType) {
+ case 'update-hierarchical-select':
+ // Drop out the selects of the levels deeper than the select of the
+ // level that just changed.
+ var animationDelay = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['animationDelay'];
+ var $animatedSelects = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context).slice(lastUnchanged);
+ if ($animatedSelects.size() > 0) {
+ $animatedSelects.hide();
+ for (var i = 0; i < $animatedSelects.size(); i++) {
+ if (i < $animatedSelects.size() - 1) {
+ $animatedSelects.slice(i, i + 1).hide("drop", { direction: "left" }, animationDelay);
+ }
+ else {
+ $animatedSelects.slice(i, i + 1).hide("drop", { direction: "left" }, animationDelay, callback);
+ }
+ }
+ }
+ else if (callback) {
+ callback();
+ }
+ break;
+ default:
+ if (callback) {
+ callback();
+ }
+ break;
+ }
+};
+
+Drupal.HierarchicalSelect.postUpdateAnimations = function(hsid, updateType, lastUnchanged, callback) {
+ if (Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].resizable) {
+ // Restore the resize.
+ Drupal.HierarchicalSelect.resize(
+ $('#hierarchical-select-' + hsid + '-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context),
+ Drupal.HierarchicalSelect.state["hs-" + hsid].defaultHeight,
+ Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight,
+ Drupal.HierarchicalSelect.state["hs-" + hsid].defaultSize,
+ Drupal.HierarchicalSelect.state["hs-" + hsid].margin
+ );
+ }
+
+ switch (updateType) {
+ case 'update-hierarchical-select':
+ var $createNewItemInput = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .create-new-item-input', Drupal.HierarchicalSelect.context);
+ // Hide the loaded selects after the one that was just changed, then
+ // drop them in.
+ var animationDelay = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['animationDelay'];
+ var $animatedSelects = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context).slice(lastUnchanged);
+ if ($animatedSelects.size() > 0) {
+ $animatedSelects.hide();
+ for (var i = 0; i < $animatedSelects.size(); i++) {
+ if (i < $animatedSelects.size() - 1) {
+ $animatedSelects.slice(i, i + 1).show("drop", { direction: "left" }, animationDelay);
+ }
+ else {
+ $animatedSelects.slice(i, i + 1).show("drop", { direction: "left" }, animationDelay, callback);
+ }
+ }
+ }
+ else if (callback) {
+ callback();
+ }
+ if ($createNewItemInput.size() == 0) {
+ // Give focus to the level below the one that has changed, if it
+ // exists.
+ setTimeout(
+ function() {
+ $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context)
+ .slice(lastUnchanged, lastUnchanged + 1)
+ .focus();
+ },
+ animationDelay + 100
+ );
+ }
+ else {
+ // Give focus to the input field of the "create new item/level"
+ // section, if it exists, and also select the existing text.
+ $createNewItemInput.focus();
+ $createNewItemInput[0].select();
+ }
+ break;
+
+ case 'create-new-item':
+ // Make sure that other Hierarchical Selects that represent the same
+ // hierarchy are also updated, to make sure that they have the newly
+ // created item!
+ var cacheId = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].cacheId;
+ for (var otherHsid in Drupal.settings.HierarchicalSelect.settings) {
+ if (Drupal.settings.HierarchicalSelect.settings[otherHsid].cacheId == cacheId) {
+ $('#hierarchical-select-'+ otherHsid +'-wrapper')
+ .trigger('enforce-update');
+ }
+ }
+ // TRICKY: NO BREAK HERE!
+
+ case 'cancel-new-item':
+ // After an item/level has been created/cancelled, reset focus to the
+ // beginning of the hierarchical select.
+ $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context)
+ .slice(0, 1)
+ .focus();
+
+ if (callback) {
+ callback();
+ }
+ break;
+
+ default:
+ if (callback) {
+ callback();
+ }
+ break;
+ }
+};
+
+Drupal.HierarchicalSelect.triggerEvents = function(hsid, updateType, settings) {
+ $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context)
+ .trigger(updateType, [ hsid, settings ]);
+};
+
+Drupal.HierarchicalSelect.update = function(hsid, updateType, settings) {
+ var post = $('form:has(#hierarchical-select-' + hsid +'-wrapper)', Drupal.HierarchicalSelect.context).formToArray();
+ var hs_current_language = Drupal.settings.HierarchicalSelect.hs_current_language;
+
+ // Pass the hierarchical_select id via POST.
+ post.push({ name : 'hsid', value : hsid });
+ // Send the current language so we can use the same language during the AJAX callback.
+ post.push({ name : 'hs_current_language', value : hs_current_language});
+ // Emulate the AJAX data sent normally so that we get the same theme.
+ post.push({ name : 'ajax_page_state[theme]', value : Drupal.settings.ajaxPageState.theme });
+ post.push({ name : 'ajax_page_state[theme_token]', value : Drupal.settings.ajaxPageState.theme_token });
+
+ // If a cache system is installed, let the server know if it's running
+ // properly. If it is running properly, the server will send back additional
+ // information to maintain a lazily-loaded cache.
+ if (Drupal.HierarchicalSelect.cache != null) {
+ post.push({ name : 'client_supports_caching', value : Drupal.HierarchicalSelect.cache.status() });
+ }
+
+ // updateType is one of:
+ // - 'none' (default)
+ // - 'update-hierarchical-select'
+ // - 'enforced-update'
+ // - 'create-new-item'
+ // - 'cancel-new-item'
+ // - 'add-to-dropbox'
+ // - 'remove-from-dropbox'
+ switch (updateType) {
+ case 'update-hierarchical-select':
+ var value = $('#'+ settings.select_id).val();
+ var lastUnchanged = parseInt(settings.select_id.replace(/^.*-hierarchical-select-selects-(\d+)/, "$1")) + 1;
+ var optionClass = $('#'+ settings.select_id).find('option[value="'+ value +'"]').attr('class');
+
+ // Don't do anything (also no callback to the server!) when the selected
+ // item is:
+ // - the '' option and the renderFlatSelect setting is disabled, or
+ // - a level label, or
+ // - an option of class 'has-no-children', and
+ // (the renderFlatSelect setting is disabled or the dropbox is enabled)
+ // and
+ // (the createNewLevels setting is disabled).
+ if ((value == 'none' && Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['renderFlatSelect'] == false)
+ || value.match(/^label_\d+$/)
+ || (optionClass == 'has-no-children'
+ &&
+ (
+ (Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['renderFlatSelect'] == false
+ || $('#hierarchical-select-'+ hsid +'-wrapper .dropbox').length > 0
+ )
+ &&
+ Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['createNewLevels'] == false
+ )
+ )
+ )
+ {
+ Drupal.HierarchicalSelect.preUpdateAnimations(hsid, updateType, lastUnchanged, function() {
+ // Remove the sublevels.
+ $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context)
+ .slice(lastUnchanged)
+ .remove();
+
+ // The selection of this hierarchical select has changed!
+ Drupal.HierarchicalSelect.triggerEvents(hsid, 'change-hierarchical-select', settings);
+ });
+ return;
+ }
+ post.push({ name : 'op', value : settings.opString });
+ break;
+
+ case 'enforced-update':
+ post.push({ name : 'op', value : settings.opString });
+ post = post.concat(settings.extraPost);
+ break;
+
+ case 'create-new-item':
+ case 'cancel-new-item':
+ case 'add-to-dropbox':
+ case 'remove-from-dropbox':
+ post.push({ name : 'op', value : settings.opString });
+ break;
+
+ default:
+ break;
+ }
+
+ // Construct the URL the request should be made to.
+ var url = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].ajax_url;
+
+ // Construct the object that contains the options for a callback to the
+ // server. If a client-side cache is found however, it's possible that this
+ // won't be used.
+ var ajaxOptions = $.extend({}, Drupal.ajax.prototype, {
+ url: url,
+ type: 'POST',
+ dataType: 'json',
+ data: post,
+ effect: 'fade',
+ wrapper: '#hierarchical-select-' + hsid + '-wrapper',
+ beforeSend: function() {
+ Drupal.HierarchicalSelect.triggerEvents(hsid, 'before-' + updateType, settings);
+ Drupal.HierarchicalSelect.disableForm(hsid);
+ },
+ error: function (XMLHttpRequest, textStatus, errorThrown) {
+ // When invalid HTML is received in Safari, jQuery calls this function.
+ Drupal.HierarchicalSelect.throwError(hsid, Drupal.t('Received an invalid response from the server.'));
+ },
+ success: function(response, status) {
+ // An invalid response may be returned by the server, in case of a PHP
+ // error. Detect this and let the user know.
+ if (response === null || response.length == 0) {
+ Drupal.HierarchicalSelect.throwError(hsid, Drupal.t('Received an invalid response from the server.'));
+ return;
+ }
+
+ // Execute all AJAX commands in the response. But pass an additional
+ // hsid parameter, which is then only used by the commands written
+ // for Hierarchical Select.
+
+ // This is another hack because of the non-Drupal ajax implementation
+ // of this module, one of the response that can come from a drupal
+ // ajax command is insert, which expects a Drupal.ajax object as the first
+ // arguments and assumes that certain functions/settings are available.
+ // Because we are calling a Drupal.ajax.command but providing the regular
+ // jQuery ajax object itself, we are allowing Drupal.ajax.prototype.commands
+ // to misserably fail.
+ //
+ // This hack attempts to fix one issue with an insert command,
+ // @see https://www.drupal.org/node/2393695, allowing it to work properly
+ // Other hacks might be necessary for other ajax commands if they are added
+ // by external modules.
+ this.effect = 'none';
+ this.getEffect = Drupal.ajax.prototype.getEffect;
+
+ for (var i in response) {
+ if (response[i]['command'] && Drupal.ajax.prototype.commands[response[i]['command']]) {
+ Drupal.ajax.prototype.commands[response[i]['command']](this, response[i], status, hsid);
+ }
+ }
+
+ // Attach behaviors. This is just after the HTML has been updated, so
+ // it's as soon as we can.
+ Drupal.attachBehaviors($('#hierarchical-select-' + hsid + '-wrapper').parents('div.form-type-hierarchical-select')[0]);
+
+ // Transform the hierarchical select and/or dropbox to the JS variant,
+ // make it resizable again and re-enable the disabled form items.
+ Drupal.HierarchicalSelect.enableForm(hsid);
+
+ Drupal.HierarchicalSelect.postUpdateAnimations(hsid, updateType, lastUnchanged, function() {
+ // Update the client-side cache when:
+ // - information for in the cache is provided in the response, and
+ // - the cache system is available, and
+ // - the cache system is running.
+ if (response.cache != null && Drupal.HierarchicalSelect.cache != null && Drupal.HierarchicalSelect.cache.status()) {
+ Drupal.HierarchicalSelect.cache.sync(hsid, response.cache);
+ }
+
+ if (response.log != undefined) {
+ Drupal.HierarchicalSelect.log(hsid, response.log);
+ }
+
+ Drupal.HierarchicalSelect.triggerEvents(hsid, updateType, settings);
+
+ if (updateType == 'update-hierarchical-select') {
+ // The selection of this hierarchical select has changed!
+ Drupal.HierarchicalSelect.triggerEvents(hsid, 'change-hierarchical-select', settings);
+ }
+ });
+ }
+ });
+
+ // Use the client-side cache to update the hierarchical select when:
+ // - the hierarchical select is being updated (i.e. no add/remove), and
+ // - the renderFlatSelect setting is disabled, and
+ // - the createNewItems setting is disabled, and
+ // - the cache system is available, and
+ // - the cache system is running.
+ // Otherwise, perform a normal dynamic form submit.
+ if (updateType == 'update-hierarchical-select'
+ && Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['renderFlatSelect'] == false
+ && Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['createNewItems'] == false
+ && Drupal.HierarchicalSelect.cache != null
+ && Drupal.HierarchicalSelect.cache.status())
+ {
+ Drupal.HierarchicalSelect.cache.updateHierarchicalSelect(hsid, value, settings, lastUnchanged, ajaxOptions);
+ }
+ else {
+ Drupal.HierarchicalSelect.preUpdateAnimations(hsid, updateType, lastUnchanged, function() {
+ // Adding current theme to prevent conflicts, @see ajax.js
+ // @TODO, try converting to use Drupal.ajax instead.
+
+ // Prevent duplicate HTML ids in the returned markup.
+ // @see drupal_html_id()
+ var ids = [];
+ $('[id]').each(function () {
+ ids.push(this.id);
+ });
+
+ ajaxOptions.data.push({ name : 'ajax_html_ids[]', value : ids });
+
+ ajaxOptions.data.push({ name : 'ajax_page_state[theme]', value : Drupal.settings.ajaxPageState.theme });
+ ajaxOptions.data.push({ name : 'ajax_page_state[theme_token]', value : Drupal.settings.ajaxPageState.theme_token });
+ for (var key in Drupal.settings.ajaxPageState.css) {
+ ajaxOptions.data.push({ name : 'ajax_page_state[css][' + key + ']', value : 1});
+ }
+ for (var key in Drupal.settings.ajaxPageState.js) {
+ ajaxOptions.data.push({ name : 'ajax_page_state[js][' + key + ']', value : 1});
+ }
+
+ // Make it work with jquery update
+ if (Drupal.settings.ajaxPageState.jquery_version) {
+ ajaxOptions.data.push({ name : 'ajax_page_state[jquery_version]', value : Drupal.settings.ajaxPageState.jquery_version });
+ }
+
+ $.ajax(ajaxOptions);
+ });
+ }
+};
+
+Drupal.ajax.prototype.commands.hierarchicalSelectUpdate = function(ajax, response, status, hsid) {
+ // Replace the old HTML with the (relevant part of) retrieved HTML.
+ $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context)
+ .parent('.form-item')
+ .replaceWith($(response.output));
+};
+
+Drupal.ajax.prototype.commands.hierarchicalSelectSettingsUpdate = function(ajax, response, status, hsid) {
+ Drupal.settings.HierarchicalSelect.settings["hs-" + response.hsid] = response.settings;
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.module b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.module
new file mode 100644
index 00000000..6c53379e
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select.module
@@ -0,0 +1,2367 @@
+ 'hierarchical_select_ajax',
+ 'delivery callback' => 'ajax_deliver',
+ 'access arguments' => array('access content'),
+ 'theme callback' => 'ajax_base_page_theme',
+ 'type' => MENU_CALLBACK,
+ );
+
+ $items['admin/config/content/hierarchical_select'] = array(
+ 'title' => 'Hierarchical Select',
+ 'description' => 'Configure site-wide settings for the Hierarchical Select form element.',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('hierarchical_select_admin_settings'),
+ 'type' => MENU_NORMAL_ITEM,
+ 'file' => 'hierarchical_select.admin.inc',
+ );
+ $items['admin/config/content/hierarchical_select/settings'] = array(
+ 'title' => 'Site-wide settings',
+ 'access arguments' => array('administer site configuration'),
+ 'weight' => -10,
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'file' => 'hierarchical_select.admin.inc',
+ );
+ $items['admin/config/content/hierarchical_select/configs'] = array(
+ 'title' => 'Configurations',
+ 'description' => 'All available Hierarchical Select configurations.',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'hierarchical_select_admin_configs',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'hierarchical_select.admin.inc',
+ );
+ $items['admin/config/content/hierarchical_select/implementations'] = array(
+ 'title' => 'Implementations',
+ 'description' => 'Features of each Hierarchical Select implementation.',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'hierarchical_select_admin_implementations',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'hierarchical_select.admin.inc',
+ );
+ $items['admin/config/content/hierarchical_select/export/%hierarchical_select_config_id'] = array(
+ 'title' => 'Export',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('hierarchical_select_admin_export', 5),
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'hierarchical_select.admin.inc',
+ );
+ $items['admin/config/content/hierarchical_select/import/%hierarchical_select_config_id'] = array(
+ 'title' => 'Import',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('hierarchical_select_admin_import', 5),
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'hierarchical_select.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_element_info().
+ */
+function hierarchical_select_element_info() {
+ $types['hierarchical_select'] = array(
+ '#input' => TRUE,
+ '#process' => array('form_hierarchical_select_process'),
+ '#theme' => array('hierarchical_select'),
+ '#theme_wrappers' => array('form_element'),
+ '#config' => array(
+ 'module' => 'some_module',
+ 'params' => array(),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ 'labels' => array(),
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'title' => t('All selections'),
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ 'sort' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'animation_delay' => variable_get('hierarchical_select_animation_delay', 400),
+ 'special_items' => array(),
+ 'render_flat_select' => 0,
+ ),
+ '#default_value' => -1,
+ );
+ $types['hierarchical_select_item_separator'] = array(
+ '#theme' => 'hierarchical_select_item_separator',
+ );
+
+ return $types;
+}
+
+/**
+ * Implements hook_requirements().
+ */
+function hierarchical_select_requirements($phase) {
+ $requirements = array();
+
+ if ($phase == 'runtime') {
+ // Check if all hook_update_n() hooks have been executed.
+ require_once DRUPAL_ROOT . '/' . 'includes/install.inc';
+ drupal_load_updates();
+ $updates = drupal_get_schema_versions('hierarchical_select');
+ $current = drupal_get_installed_schema_version('hierarchical_select');
+
+ $up_to_date = (end($updates) == $current);
+
+ $hierarchical_select_weight = db_query("SELECT weight FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => 'hierarchical_select'))->fetchField();
+ $core_overriding_modules = array('hs_book', 'hs_menu', 'hs_taxonomy');
+ $path_errors = array();
+ foreach ($core_overriding_modules as $module) {
+ $filename = db_query("SELECT filename FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => $module))->fetchField();
+ if (strpos($filename, 'modules/') === 0) {
+ $module_info = drupal_parse_info_file(dirname($filename) . "/$module.info");
+ $path_errors[] = t('!module', array('!module' => $module_info['name']));
+ }
+ }
+
+ if ($up_to_date && !count($path_errors)) {
+ $value = t('All updates installed. HS API implementation modules correctly installed.');
+ $description = '';
+ $severity = REQUIREMENT_OK;
+ }
+ elseif ($path_errors) {
+ $value = t('Modules incorrectly installed!');
+ $description = t(
+ "The following modules implement Hierarchical Select module for Drupal
+ core modules, but are installed in the wrong location. They're
+ installed in core's modules directory, but should be
+ installed in either the sites/all/modules directory or a
+ sites/yoursite.com/modules directory"
+ ) . ':' . theme('item_list', array('items' => $path_errors));
+ $severity = REQUIREMENT_ERROR;
+ }
+ else {
+ $value = t('Not all updates installed!');
+ $description = t('Please run update.php to install the latest updates!
+ You have installed update !installed_update, but the latest update is
+ !latest_update!',
+ array(
+ '!installed_update' => $current,
+ '!latest_update' => end($updates),
+ )
+ );
+ $severity = REQUIREMENT_ERROR;
+ }
+
+ $requirements['hierarchical_select'] = array(
+ 'title' => t('Hierarchical Select'),
+ 'value' => $value,
+ 'description' => $description,
+ 'severity' => $severity,
+ );
+ }
+
+ return $requirements;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function hierarchical_select_theme() {
+ return array(
+ 'hierarchical_select_form_element' => array(
+ 'file' => 'includes/theme.inc',
+ 'variables' => array('element' => NULL, 'value' => NULL),
+ ),
+ 'hierarchical_select' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'element',
+ ),
+ 'hierarchical_select_selects_container' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'element',
+ ),
+ 'hierarchical_select_select' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'element',
+ ),
+ 'hierarchical_select_item_separator' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'element',
+ ),
+ 'hierarchical_select_special_option' => array(
+ 'file' => 'includes/theme.inc',
+ 'variables' => array('option' => NULL),
+ ),
+ 'hierarchical_select_dropbox_table' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'element',
+ ),
+ 'hierarchical_select_common_config_form_level_labels' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'form',
+ ),
+ 'hierarchical_select_common_config_form_editability' => array(
+ 'file' => 'includes/theme.inc',
+ 'render element' => 'form',
+ ),
+ 'hierarchical_select_selection_as_lineages' => array(
+ 'file' => 'includes/theme.inc',
+ 'variables' => array(
+ 'selection' => NULL,
+ 'config' => NULL,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_features_api().
+ */
+function hierarchical_select_features_api() {
+ return array(
+ 'hierarchical_select' => array(
+ 'name' => t('Hierarchical select configs'),
+ 'feature_source' => TRUE,
+ 'default_hook' => 'hierarchical_select_default_configs',
+ 'default_file' => FEATURES_DEFAULTS_INCLUDED,
+ 'file' => drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.features.inc',
+ ),
+ );
+}
+
+/**
+ * Implements hook_select_menu_site_status_alter().
+ *
+ * This will run straight after the bootstrap/hook_init(), and override the
+ * interface language there determined with the interface language from the
+ * previous request on the HS AJAX callback. We want the language to remain
+ * the same between requests so we can determine the "triggering element"
+ * correctly. If the button value changed because of a language change (as
+ * can happen with the admin_language module), the whole form would submit.
+ */
+function hierarchical_select_menu_site_status_alter(&$menu_site_status, $path) {
+ global $language;
+ // Make sure we are on the AJAX callback.
+ if (0 === strpos($_GET['q'], 'hierarchical_select_ajax') && !empty($_POST['hs_current_language'])) {
+ $languages = language_list();
+ if (isset($languages[$_POST['hs_current_language']])) {
+ // Override the language set during bootstrap with the language from the
+ // previous request.
+ $language = $languages[$_POST['hs_current_language']];
+ }
+ }
+}
+
+
+//----------------------------------------------------------------------------
+// Menu system callbacks.
+
+/**
+ * Wildcard loader for Hierarchical Select config ID's.
+ */
+function hierarchical_select_config_id_load($config_id) {
+ $config = variable_get('hs_config_' . $config_id, FALSE);
+ return ($config !== FALSE) ? $config['config_id'] : FALSE;
+}
+
+
+//----------------------------------------------------------------------------
+// Forms API callbacks.
+
+/**
+ * Ajax callback to render the select form elements.
+ *
+ * @see file_ajax_upload(), upon which this is strongly inspired.
+ * @see ajax_form_callback()
+ */
+function hierarchical_select_ajax() {
+ $form_parents = func_get_args();
+ list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
+
+ // Process user input. $form and $form_state are modified in the process.
+ drupal_process_form($form['#form_id'], $form, $form_state);
+ $element = drupal_array_get_nested_value($form, $form_parents);
+
+ // Render the output.
+ $output = theme('status_messages') . drupal_render($element);
+
+ // Send AJAX command to update the Hierarchical Select.
+ $commands[] = array(
+ 'command' => 'hierarchicalSelectUpdate',
+ 'output' => $output,
+ );
+
+ $new_settings = _hs_new_setting_ajax(FALSE);
+ foreach ($new_settings as $new_setting) {
+ $commands[] = array(
+ 'command' => 'hierarchicalSelectSettingsUpdate',
+ 'hsid' => $new_setting['hsid'],
+ 'settings' => $new_setting['settings'],
+ );
+ }
+
+ $context = array(
+ 'form' => $form,
+ 'form_state' => $form_state,
+ 'element' => $element,
+ );
+ drupal_alter('hierarchical_select_ajax_commands', $commands, $context);
+ return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+function _hs_process_determine_hsid($element, &$form_state) {
+ // Determine the HSID to use: either the existing one that is received, or
+ // generate a new one based on the last HSID used (which is
+ // stored in form state storage).
+ if (!isset($element['#value']) || !is_array($element['#value']) || !array_key_exists('hsid', $element['#value'])) {
+ $hsid = uniqid();
+ }
+ else {
+ $hsid = check_plain($element['#value']['hsid']);
+ }
+
+ return $hsid;
+}
+
+// Get the config and convert the 'special_items' setting to a more easily
+// accessible format.
+function _hs_process_shortcut_special_items($config) {
+ $special_items = array();
+ if (isset($config['special_items'])) {
+ $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
+ $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
+ }
+ return $special_items;
+}
+
+function _hs_process_attach_css_js($element, $hsid, &$form_state, $complete_form) {
+ global $language;
+ // Set up Javascript and add settings specifically for the current
+ // hierarchical select.
+ $element['#attached']['library'][] = array('system', 'ui');
+ $element['#attached']['library'][] = array('system', 'drupal.ajax');
+ $element['#attached']['library'][] = array('system', 'jquery.form');
+ $element['#attached']['library'][] = array('system', 'effects');
+ $element['#attached']['library'][] = array('system', 'effects.drop');
+ $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css';
+ $element['#attached']['js'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.js';
+ if (variable_get('hierarchical_select_js_cache_system', 0) == 1) {
+ $element['#attached']['js'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select_cache.js';
+ }
+
+ if (!isset($form_state['storage']['hs']['js_settings_sent'])) {
+ $form_state['storage']['hs']['js_settings_sent'] = array();
+ }
+
+ // Form was submitted; this is a newly loaded page, thus ensure that all JS
+ // settings are resent.
+ if ($form_state['process_input'] === TRUE) {
+ $form_state['storage']['hs']['js_settings_sent'] = array();
+ }
+
+ if (!isset($form_state['storage']['hs']['js_settings_sent'][$hsid]) || (isset($form_state['storage']['hs']['js_settings_sent'][$hsid]) && (isset($form_state['triggering_element']) && $form_state['triggering_element']['#type'] == 'submit'))) {
+ $config = _hierarchical_select_inherit_default_config($element['#config']);
+ $settings = array(
+ 'HierarchicalSelect' => array(
+ // Save language in settings so we can use the same language during the AJAX callback.
+ 'hs_current_language' => $language->language,
+ 'settings' => array(
+ "hs-$hsid" => array(
+ 'animationDelay' => ($config['animation_delay'] == 0) ? (int) variable_get('hierarchical_select_animation_delay', 400) : $config['animation_delay'],
+ 'cacheId' => $config['module'] . '_' . md5(serialize($config['params'])),
+ 'renderFlatSelect' => (isset($config['render_flat_select'])) ? (int) $config['render_flat_select'] : 0,
+ 'createNewItems' => (isset($config['editability']['status'])) ? (int) $config['editability']['status'] : 0,
+ 'createNewLevels' => (isset($config['editability']['allow_new_levels'])) ? (int) $config['editability']['allow_new_levels'] : 0,
+ 'resizable' => (isset($config['resizable'])) ? (int) $config['resizable'] : 0,
+ 'ajax_url' => url('hierarchical_select_ajax/' . implode('/', $element['#array_parents'])),
+ ),
+ ),
+ )
+ );
+
+ if (!isset($_POST['hsid'])) {
+ $element['#attached']['js'][] = array(
+ 'type' => 'setting',
+ 'data' => $settings,
+ );
+ }
+ else {
+ $element['#attached']['_hs_new_setting_ajax'][] = array($hsid, $settings['HierarchicalSelect']['settings']["hs-$hsid"]);
+ }
+
+ $form_state['storage']['hs']['js_settings_sent'][$hsid] = TRUE;
+ }
+
+ return $element;
+}
+
+function _hs_new_setting_ajax($hsid = FALSE, $settings = NULL) {
+ static $hs_settings = array();
+
+ if ($hsid !== FALSE) {
+ $hs_settings[] = array('hsid' => $hsid, 'settings' => $settings);
+ }
+
+ return $hs_settings;
+}
+
+// Basic config validation and diagnostics.
+function _hs_process_developer_mode_log_diagnostics(&$element) {
+ if (HS_DEVELOPER_MODE) {
+ $config = $element['#config'];
+ $diagnostics = array();
+ if (!isset($config['module']) || empty($config['module'])) {
+ $diagnostics[] = t("'module is not set!");
+ }
+ elseif (!module_exists($config['module'])) {
+ $diagnostics[] = t('the module that should be used (module) is not installed!', array('%module' => $config['module']));
+ }
+ else {
+ $required_params = module_invoke($config['module'], 'hierarchical_select_params');
+ $missing_params = array_diff($required_params, array_keys($config['params']));
+ if (!empty($missing_params)) {
+ $diagnostics[] = t("'params' is missing values for: ") . implode(', ', $missing_params) . '.';
+ }
+ }
+ $config_id = (isset($config['config_id']) && is_string($config['config_id'])) ? $config['config_id'] : 'none';
+ if (empty($diagnostics)) {
+ _hierarchical_select_log("Config diagnostics (config id: $config_id): no problems found!");
+ }
+ else {
+ $diagnostics_string = print_r($diagnostics, TRUE);
+ $message = "Config diagnostics (config id: $config_id): $diagnostics_string";
+ _hierarchical_select_log($message);
+ $title = $element['#title'];
+ $element = array();
+ $element['#type'] = 'item';
+ $element['#title'] = $title;
+ $element['#markup'] = '
Fix the indicated errors in the #config property first! ' . nl2br($message) . '
';
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+function _hs_process_developer_mode_log_selections($config, $hs_selection, $db_selection) {
+ if (HS_DEVELOPER_MODE) {
+ _hierarchical_select_log("Calculated hierarchical select selection:");
+ _hierarchical_select_log($hs_selection);
+
+ if ($config['dropbox']['status']) {
+ _hierarchical_select_log("Calculated dropbox selection:");
+ _hierarchical_select_log($db_selection);
+ }
+ }
+}
+
+function _hs_process_developer_mode_log_hierarchy_and_dropbox($config, $hierarchy, $dropbox) {
+ if (HS_DEVELOPER_MODE) {
+ _hierarchical_select_log('Generated hierarchy in ' . $hierarchy->build_time['total'] . ' ms:');
+ _hierarchical_select_log($hierarchy);
+
+ if ($config['dropbox']['status']) {
+ _hierarchical_select_log('Generated dropbox in ' . $dropbox->build_time . ' ms: ');
+ _hierarchical_select_log($dropbox);
+ }
+ }
+}
+
+function _hs_process_developer_mode_send_log_js($element, $hsid) {
+ if (HS_DEVELOPER_MODE) {
+ $log = _hierarchical_select_log(NULL, TRUE);
+ $settings = array(
+ 'HierarchicalSelect' => array(
+ 'initialLog' => array(
+ "hs-$hsid" => $log,
+ ),
+ ),
+ );
+ $element['#attached']['js'][] = array(
+ 'type' => 'setting',
+ 'data' => $settings,
+ );
+ }
+
+ return $element;
+}
+
+function _hs_process_exclusive_lineages($element, $hs_selection, $db_selection) {
+ $config = $element['#config'];
+ $special_items = _hs_process_shortcut_special_items($config);
+
+ // If:
+ // - the special_items setting has been configured
+ // - at least one special item has the 'exclusive' property
+ // - the dropbox is enabled
+ // then do the necessary processing to make exclusive lineages possible.
+ if (!empty($special_items) && count($special_items['exclusive']) && $config['dropbox']['status']) {
+ // When the form is first loaded, $db_selection will contain the selection
+ // that we should check, but in updates, $hs_selection will.
+ $selection = (!empty($hs_selection)) ? $hs_selection : $db_selection;
+
+ // If the current selection of the hierarchical select matches one of the
+ // configured exclusive items, then disable the dropbox (to ensure an
+ // exclusive selection).
+ $exclusive_item = array_intersect($selection, $special_items['exclusive']);
+ if (count($exclusive_item)) {
+ // By also updating the configuration stored in $element, we ensure that
+ // the validation step, which extracts the configuration again, also gets
+ // the updated config.
+ $element['#config']['dropbox']['status'] = 0;
+
+ // Set the hierarchical select to the exclusive item and make the
+ // dropbox empty.
+ $hs_selection = array(0 => reset($exclusive_item));
+ $db_selection = array();
+ }
+ }
+
+ return array($element, $hs_selection, $db_selection);
+}
+
+function _hs_process_render_create_new_item($element, $hierarchy) {
+ $creating_new_item = FALSE;
+
+ // This container and the "Create" / "Cancel" buttons must always be part of
+ // the form, even when HS is not in create mode, in order for AJAX submit
+ // callbacks on the "Create" and "Cancel" buttons to be processed correctly.
+ //
+ // Basically, FAPI looks through each of the buttons in the form to determine
+ // which one was clicked. If it can't find the responsible button, it
+ // assumes it was the first button in the form. This is problematic when the
+ // user clicks on the "Create" or "Cancel" buttons because we only want them
+ // to show up when HS is in create mode. To fix this, we always render the
+ // buttons as part of the form, then disable access to them in an
+ // "#after_build" callback.
+ //
+ // This might not be necessary if we used D7's native AJAX callback function,
+ // ajax_form_callback().
+ $element['hierarchical_select']['create_new_item'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#after_build' => array('hierarchical_select_create_new_item_after_build'),
+ );
+
+ // @todo Port to use built-in D7 AJAX callback?
+ $element['hierarchical_select']['create_new_item']['create'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create'),
+ '#attributes' => array('class' => array('create-new-item-create')),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#validate' => array(),
+ '#submit' => array('hierarchical_select_ajax_update_submit'),
+ );
+
+ $element['hierarchical_select']['create_new_item']['cancel'] = array(
+ '#type' => 'submit',
+ '#value' => t('Cancel'),
+ '#attributes' => array('class' => array('create-new-item-cancel')),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#validate' => array(),
+ '#submit' => array('hierarchical_select_ajax_update_submit'),
+ );
+
+ if (isset($element['#value']['hierarchical_select']['selects'])) {
+ foreach ($element['#value']['hierarchical_select']['selects'] as $depth => $value) {
+ if ($value == 'create_new_item' && _hierarchical_select_create_new_item_is_allowed($element['#config'], $depth)) {
+ $creating_new_item = TRUE;
+
+ // We want to override the select in which the "create_new_item"
+ // option was selected and hide all selects after that, if they exist.
+ // If depth == 0, then that means all selects should be hidden.
+ if ($depth == 0) {
+ unset($element['hierarchical_select']['selects']);
+ }
+ else {
+ for ($i = $depth; $i < count($hierarchy->lineage); $i++) {
+ unset($element['hierarchical_select']['selects'][$i]);
+ }
+ }
+
+ $item_type_depth = ($value == 'create_new_item') ? $depth : $depth + 1;
+ $item_type = (count($element['#config']['editability']['item_types']) == $item_type_depth)
+ ? t($element['#config']['editability']['item_types'][$item_type_depth])
+ : t('item');
+
+ $element['hierarchical_select']['create_new_item']['input'] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#maxlength' => 255,
+ '#default_value' => t('new @item', array('@item' => $item_type)),
+ '#attributes' => array(
+ 'title' => t('new @item', array('@item' => $item_type)),
+ 'class' => array('create-new-item-input'),
+ ),
+ // Prevent the textfield from being wrapped in a div. This
+ // simplifies the CSS and JS code.
+ '#theme_wrappers' => array(),
+ // Place the textfield above the "Create" / "Cancel" buttons.
+ '#weight' => -1,
+ );
+ }
+ }
+ }
+ $element['hierarchical_select']['create_new_item']['#creating_new_item'] = $creating_new_item;
+
+ return array($element, $creating_new_item);
+}
+
+/**
+ * Render API callback: Controls access to the create_new_item form.
+ *
+ * Only allows access to the create_new_item form if creating a new item.
+ *
+ * This function is assigned as an #after_build callback in
+ * _hs_process_render_create_new_item().
+ */
+function hierarchical_select_create_new_item_after_build(array $element) {
+ $element['#access'] = $element['#creating_new_item'];
+
+ return $element;
+}
+
+function _hs_process_render_dropbox($element, $hsid, $creating_new_item, $dropbox, $form_state) {
+ $config = $element['#config'];
+
+ if ($config['dropbox']['status']) {
+ if (!$creating_new_item) {
+ // Append an "Add" button to the selects.
+ $element['hierarchical_select']['dropbox_add'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add'),
+ '#attributes' => array('class' => array('add-to-dropbox')),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#validate' => array(),
+ '#submit' => array('hierarchical_select_ajax_update_submit'),
+ );
+ }
+
+ if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
+ if (count($dropbox->lineages) >= $config['dropbox']['limit']) {
+ $element['dropbox_limit_warning'] = array(
+ '#markup' => t("You've reached the maximum number of items you can select."),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // Disable all child form elements of $element['hierarchical_select].
+ // _hierarchical_select_mark_as_disabled($element['hierarchical_select']);
+
+ // TODO: make the above work again. Currently, we're just disabling
+ // the "Add" button. #disabled can't be used for the same reasons as
+ // described in _hierarchical_select_mark_as_disabled().
+ $element['hierarchical_select']['dropbox_add']['#attributes']['disabled'] = TRUE;
+ }
+ }
+
+ // Store the currently selected lineages of the dropbox in the form state's
+ // storage section.
+ if (isset($dropbox->lineages_selections)) {
+ $form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] = $dropbox->lineages_selections;
+ }
+
+ // Add the dropbox-as-a-table that will be visible to the user.
+ $element['dropbox']['visible'] = _hs_process_render_db_table($hsid, $dropbox);
+ }
+
+ return array($element, $form_state);
+}
+
+function _hs_process_render_nojs($element, $config) {
+ // This button and accompanying help text will be hidden when Javascript is
+ // enabled.
+ $element['nojs'] = array(
+ '#prefix' => '
',
+ );
+
+ return $element;
+}
+
+/**
+ * Hierarchical select form element type #process callback.
+ */
+function form_hierarchical_select_process($element, &$form_state, $complete_form) {
+ if (arg(0) != 'hierarchical_select_ajax') {
+ // Get unique identifier using parents of the field.
+ $cid = isset($element['#parents']) ? implode("-", $element['#parents']) : implode("-", $element['#field_parents']);
+
+ // Verify if hsid is present.
+ $elhsid = drupal_array_get_nested_value($element, array('#value', 'hsid'));
+
+ if (!isset($elhsid)) {
+ // Retrieve previous element from form_state.
+ $cached = drupal_array_get_nested_value($form_state, array('storage', 'hs', 'hs_fields', $cid));
+ }
+ if (empty($cached)) {
+ $docache = TRUE;
+ }
+ else {
+ // Switch current element with the "cached".
+ return $cached;
+ }
+ }
+
+ // Determine the HSID.
+ $hsid = _hs_process_determine_hsid($element, $form_state);
+
+ // Config.
+ $config = $element['#config'];
+
+ // Attach CSS/JS files and JS settings.
+ $element = _hs_process_attach_css_js($element, $hsid, $form_state, $complete_form);
+
+ // Developer mode diagnostics, return immediately in case of a config error.
+ if (!_hs_process_developer_mode_log_diagnostics($element)) {
+ return $element;
+ }
+
+ // Calculate the selections in both the hierarchical select and the dropbox,
+ // we need these before we can render anything.
+ $hs_selection = $db_selection = array();
+ list($hs_selection, $db_selection) = _hierarchical_select_process_calculate_selections($element, $hsid, $form_state);
+
+ // Developer mode logging: log selections.
+ _hs_process_developer_mode_log_selections($config, $hs_selection, $db_selection);
+
+ // Dynamically disable the dropbox when an exclusive item has been selected.
+ // When this happens, the configuration is dynamically altered. Hence, we
+ // need to update $config.
+ list($element, $hs_selection, $db_selection) = _hs_process_exclusive_lineages($element, $hs_selection, $db_selection);
+ $config = $element['#config'];
+
+ // Generate the $hierarchy and $dropbox objects using the selections that
+ // were just calculated.
+ $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection);
+ $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox);
+
+ // Developer mode logging: log $hierarchy and $dropbox objects.
+ _hs_process_developer_mode_log_hierarchy_and_dropbox($config, $hierarchy, $dropbox);
+
+ // Finally, calculate the return value of this hierarchical_select form
+ // element. This will be set in _hierarchical_select_validate(). (If we'd
+ // set it now, it would be overridden again.)
+ $element['#return_value'] = _hierarchical_select_process_calculate_return_value($hierarchy, ($config['dropbox']['status']) ? $dropbox : FALSE, $config['module'], $config['params'], $config['save_lineage']);
+ if (!is_array($element['#return_value'])) {
+ $element['#return_value'] = array($element['#return_value']);
+ }
+
+ // Add a validate callback, which will:
+ // - validate that the dropbox limit was not exceeded.
+ // - set the return value of this form element.
+ // Also make sure it is the *first* validate callback.
+ $element['#element_validate'] = (isset($element['#element_validate'])) ? $element['#element_validate'] : array();
+ $element['#element_validate'] = array_merge(array('_hierarchical_select_validate'), $element['#element_validate']);
+
+ // Ensure the form is cached, for AJAX to work.
+ $form_state['cache'] = TRUE;
+
+ //
+ // Rendering.
+ //
+
+ // Ensure that #tree is enabled!
+ $element['#tree'] = TRUE;
+
+ // Store the HSID in a hidden form element; when an AJAX callback comes in,
+ // we'll know which HS was updated.
+ $element['hsid'] = array('#type' => 'hidden', '#value' => $hsid);
+
+
+ // If render_flat_select is enabled, render a flat select.
+ if ($config['render_flat_select']) {
+ $element['flat_select'] = _hs_process_render_flat_select($hierarchy, $dropbox, $config);
+ // See https://www.drupal.org/node/994820
+ if (empty($element['flat_select']['#options'])) {
+ unset($element['flat_select']);
+ }
+ }
+
+ // Render the hierarchical select.
+ $element['hierarchical_select'] = array(
+ '#theme' => 'hierarchical_select_selects_container',
+ );
+ $size = isset($element['#size']) ? $element['#size'] : 0;
+ $element['hierarchical_select']['selects'] = _hs_process_render_hs_selects($hsid, $hierarchy, $size);
+
+ // When the special "create_new_item" value is passed in a level, replace
+ // that level with an inline modal form to create a new item, and hide all
+ // subsequent selects.
+ list($element, $creating_new_item) = _hs_process_render_create_new_item($element, $hierarchy);
+
+ // Render the dropbox, if enabled.
+ // Automatically hides the "Add" button when creating a new item.
+ // Automatically disables HS' selects when reaching the dropbox limit.
+ // Stores the currently selected lineages of the dropbox in storage.
+ list($element, $form_state) = _hs_process_render_dropbox($element, $hsid, $creating_new_item, $dropbox, $form_state);
+
+ // Render the HTML that allows for graceful degradation.
+ $element = _hs_process_render_nojs($element, $config);
+
+ // Ensure the render order is correct.
+ $element['hierarchical_select']['#weight'] = 0;
+ $element['dropbox_limit_warning']['#weight'] = 1;
+ $element['dropbox']['#weight'] = 2;
+ $element['nojs']['#weight'] = 3;
+
+ // If the form item is marked as disabled, disable all child form items as
+ // well.
+ if (isset($element['#disabled']) && $element['#disabled']) {
+ _hierarchical_select_mark_as_disabled($element);
+ }
+
+ // This prevents values from in $form_state['input'] to be used instead of
+ // the generated default values (#default_value).
+ // For example: $element['hierarchical_select']['selects']['0']['#default_value']
+ // is set to 'label_0' after an "Add" operation. When $form_state['input']
+ // is NOT erased, the corresponding value in $form_state['input'] will be
+ // used instead of the default value that was set. This would result in
+ // undesired behavior.
+ // This, however, must not be called on node preview, becuase in that case
+ // the node will be rebuilt and we need the values inside $form_state['input']
+ // to recreate the edited form properly.
+ // @TODO: If the form is rebuilt by some other action than a node preview, we
+ // might lose data again, we should see if there's any way to prevent this from
+ // happening by setting this value after the form has been flagged to be rebuilt,
+ // but as far as I checked, there's not.
+ // Another option might be to rework the need of this function to prevent
+ // the undesired behaviors of not having it with some other logic that would
+ // work as well if the form is rebuilt.
+ if (empty($docache)) {
+ if (!isset($form_state['triggering_element']) || ($form_state['triggering_element']['#value'] != t('Preview') && $form_state['triggering_element']['#value'] != t('View changes'))) {
+ if (isset($form_state['input']) && is_array($form_state['input'])) {
+ drupal_array_set_nested_value($form_state['input'], $element['#array_parents'], array());
+ }
+ }
+ }
+ else {
+ // Store new element in cache.
+ $form_state['storage']['hs']['hs_fields'][$cid] = $element;
+ }
+
+ // Send the collected developer mode logs (by using #attached JS).
+ $element = _hs_process_developer_mode_send_log_js($element, $hsid);
+
+ return $element;
+}
+
+/**
+ * Submit callback; only sets no_redirect to TRUE (which already)
+ */
+function hierarchical_select_ajax_update_submit($form, &$form_state) {
+ $form_state['no_redirect'] = TRUE;
+}
+
+
+/**
+ * Hierarchical select form element #element_validate callback.
+ */
+function _hierarchical_select_validate(&$element, &$form_state) {
+ // If the dropbox is enabled and a dropbox limit is configured, check if
+ // this limit is not exceeded.
+ $hsid = $element['hsid']['#value'];
+ $config = _hierarchical_select_inherit_default_config($element['#config']);
+ if ($config['dropbox']['status']) {
+ if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
+ // TRICKY: #element_validate is not called upon the initial rendering
+ // (i.e. it is assumed that the default value is valid). However,
+ // Hierarchical Select's config can influence the validity (i.e. how
+ // many selections may be added to the dropbox). This means it's
+ // possible the user has actually selected too many items without being
+ // notified of this.
+ $lineage_count = count($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections']);
+ if ($lineage_count > $config['dropbox']['limit']) {
+ // TRICKY: this should propagate the error down to the children, but
+ // this doesn't seem to happen, since for example the selects of the
+ // hierarchical select don't get the error class set. Further
+ // investigation needed.
+ form_error(
+ $element,
+ t("You've selected %lineage-count items, but you're only allowed to select %dropbox-limit items.",
+ array(
+ '%lineage-count' => $lineage_count,
+ '%dropbox-limit' => $config['dropbox']['limit'],
+ )
+ )
+ );
+ _hierarchical_select_form_set_error_class($element);
+ }
+ }
+ }
+
+ // Set the proper return value. I.e. instead of returning all the values
+ // that are used for making the hierarchical_select form element type work,
+ // we pass a flat array of item ids. e.g. for the taxonomy module, this will
+ // be an array of term ids. If a single item is selected, this will not be
+ // an array.
+ // If the form item is disabled, set the default value as the return value,
+ // because otherwise nothing would be returned (disabled form items are not
+ // submitted, as described in the HTML standard).
+ if (isset($element['#disabled']) && $element['#disabled']) {
+ $element['#return_value'] = $element['#default_value'];
+ }
+
+ $element['#value'] = $element['#return_value'];
+ form_set_value($element, $element['#value'], $form_state);
+
+ // We have to check again for errors. This line is taken litterally from
+ // form.inc, so it works in an identical way.
+ if ($element['#required'] &&
+ (!isset($form_state['submit_handlers'][0]) || $form_state['submit_handlers'][0] !== 'hierarchical_select_ajax_update_submit') &&
+ (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0))) {
+ form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
+ _hierarchical_select_form_set_error_class($element);
+ }
+}
+
+
+//----------------------------------------------------------------------------
+// Forms API #process callback:
+// Calculation of hierarchical select and dropbox selection.
+
+/**
+ * Get the current (flat) selection of the hierarchical select.
+ *
+ * This selection is updatable by the user, because the values are retrieved
+ * from the selects in $element['hierarchical_select']['selects'].
+ *
+ * @param array $element
+ * A hierarchical_select form element.
+ * @return array
+ * An array (bag) containing the ids of the selected items in the
+ * hierarchical select.
+ */
+function _hierarchical_select_process_get_hs_selection($element) {
+ $hs_selection = array();
+ $config = _hierarchical_select_inherit_default_config($element['#config']);
+
+ if (!empty($element['#value']['hierarchical_select']['selects'])) {
+ if ($config['save_lineage']) {
+ foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
+ $hs_selection[] = $value;
+ }
+ }
+ else {
+ foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
+ $hs_selection[] = $value;
+ }
+ $hs_selection = _hierarchical_select_hierarchy_validate($hs_selection, $config['module'], $config['params']);
+
+ // Get the last valid value. (Only the deepest item gets saved). Make
+ // sure $hs_selection is an array at all times.
+ $hs_selection = ($hs_selection != -1) ? array(end($hs_selection)) : array();
+ }
+ }
+
+ return $hs_selection;
+}
+
+/**
+ * Get the current (flat) selection of the dropbox.
+ *
+ * This selection is not updatable by the user, because the values are
+ * retrieved from the hidden values in
+ * $element['dropbox']['hidden']['lineages_selections']. This selection can
+ * only be updated by the server, i.e. when the user clicks the "Add" button.
+ * But this selection can still be reduced in size if the user has marked
+ * dropbox entries (lineages) for removal.
+ *
+ * @param $element
+ * A hierarchical_select form element.
+ * @param $form_state
+ * The $form_state array. We need to look at
+ * $form_state['storage']['hs'][$hsid]['dropbox_lineages_selections']
+ * to know what to remove.
+ * @return
+ * An array (bag) containing the ids of the selected items in the
+ * dropbox.
+ */
+function _hierarchical_select_process_get_db_selection($element, $hsid, &$form_state) {
+ $db_selection = array();
+
+ if (!empty($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'])) {
+ // Check which lineages have been marked for removal by the user.
+ $remove_from_db_selection = array();
+ if (isset($element['#value']['dropbox']['visible']['lineages'])) {
+ foreach ($element['#value']['dropbox']['visible']['lineages'] as $x => $remove_value) {
+ if ($remove_value['remove'] === '1') {
+ // $x is of the form "lineage-". Extract the number.
+ $remove_from_db_selection[] = substr($x, 8);
+ // By removing the input (POST) reference to the remove checkbox,
+ // we make sure that on a form rebuild the same remove checkbox,
+ // which is accessed by index, is not set, preventing a double removal.
+ // @see https://www.drupal.org/node/1566878#comment-9226261
+ $elm = &$form_state['input'];
+ foreach ($element['#parents'] as $parent) {
+ $elm = &$elm[$parent];
+ }
+ unset($elm['dropbox']['visible']['lineages'][$x]['remove']);
+ }
+ }
+ }
+
+ // Add all selections to the dropbox selection, except for the ones that
+ // are scheduled for removal.
+ foreach ($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] as $x => $selection) {
+ if (!in_array($x, $remove_from_db_selection)) {
+ $db_selection = array_merge($db_selection, $selection);
+ }
+ }
+
+ // Ensure that the last item of each selection that was scheduled for
+ // removal is completely absent from the dropbox selection.
+ // In case of a tree with multiple parents, the same item can exist in
+ // different entries, and thus it would stay in the selection. When the
+ // server then reconstructs all lineages, the lineage we're removing, will
+ // also be reconstructed: it will seem as if the removing didn't work!
+ // This will not break removing dropbox entries for hierarchies without
+ // multiple parents, since items at the deepest level are always unique to
+ // that specific lineage.
+ // Easier explanation at http://drupal.org/node/221210#comment-733715.
+ foreach ($remove_from_db_selection as $key => $x) {
+ $item = end($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'][$x]);
+ $position = array_search($item, $db_selection);
+ if ($position) {
+ unset($db_selection[$position]);
+ }
+ }
+ $db_selection = array_unique($db_selection);
+ }
+
+ return $db_selection;
+}
+
+/**
+ * Calculates the flat selections of both the hierarchical select and the
+ * dropbox.
+ *
+ * @param $element
+ * A hierarchical_select form element.
+ * @param $form_state
+ * The $form_state array. We need to look at $form_state['input']['op'], to
+ * know which operation has occurred.
+ * @return
+ * An array of the following structure:
+ * array(
+ * $hierarchical_select_selection = array(), // Flat list of selected ids.
+ * $dropbox_selection = array(),
+ * )
+ * with both of the subarrays flat lists of selected ids. The
+ * _hierarchical_select_hierarchy_generate() and
+ * _hierarchical_select_dropbox_generate() functions should be applied on
+ * these respective subarrays.
+ *
+ * @see _hierarchical_select_hierarchy_generate()
+ * @see _hierarchical_select_dropbox_generate()
+ */
+function _hierarchical_select_process_calculate_selections(&$element, $hsid, &$form_state) {
+ $hs_selection = array(); // hierarchical select selection
+ $db_selection = array(); // dropbox selection
+
+ $config = _hierarchical_select_inherit_default_config($element['#config']);
+ $dropbox = (bool) $config['dropbox']['status'];
+
+ // When:
+ // - no input data was provided (through POST nor GET)
+ // - or #value is set directly and not by a Hierarchical Select POST (and
+ // therefor set either manually or by another module),
+ // then use the value of #default_value, or when available, of #value.
+ if (empty($form_state['input']) || (!isset($element['#value']['hierarchical_select']) && !isset($element['#value']['dropbox']))) {
+ $value = (!empty($element['#value'])) ? $element['#value'] : $element['#default_value'];
+ $value = (is_array($value)) ? $value : array($value);
+ if ($dropbox) {
+ $db_selection = $value;
+ }
+ else {
+ $hs_selection = $value;
+ }
+ }
+ else {
+ $op = (isset($form_state['input']['op']) && isset($form_state['input']['hsid']) && $form_state['input']['hsid'] == $hsid) ? $form_state['input']['op'] : NULL;
+ if ($dropbox && $op == t('Add')) {
+ $hs_selection = _hierarchical_select_process_get_hs_selection($element);
+ $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state);
+
+ // Add $hs_selection to $db_selection.
+ $db_selection = array_unique(array_merge($db_selection, $hs_selection));
+
+ // Only reset $hs_selection if the user has configured it that way.
+ if ((bool) $config['dropbox']['reset_hs']) {
+ $hs_selection = array();
+ }
+ }
+ else if ($op == t('Create')) {
+ // This code handles both the creation of a new item in an existing
+ // level and the creation of an item that also creates a new level.
+ $label = trim($element['#value']['hierarchical_select']['create_new_item']['input']);
+ $selects = isset($element['#value']['hierarchical_select']['selects']) ? $element['#value']['hierarchical_select']['selects'] : array();
+ $depth = count($selects);
+ $parent = ($depth > 0) ? end($selects) : 0;
+
+ // Disallow items with empty labels; allow the user again to create a
+ // (proper) new item.
+ if (empty($label)) {
+ $element['#value']['hierarchical_select']['selects'][count($selects)] = 'create_new_item';
+ }
+ // Ensure that this new item will not violate the max_levels and
+ // allowed_levels settings.
+ else if (
+ (count(module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']))
+ || $config['editability']['max_levels'] == 0
+ || $depth < $config['editability']['max_levels']
+ )
+ &&
+ (_hierarchical_select_create_new_item_is_allowed($config, $depth))
+ ) {
+ // Create the new item in the hierarchy and retrieve its value.
+ $value = module_invoke($config['module'], 'hierarchical_select_create_item', check_plain($label), $parent, $config['params']);
+
+ // Ensure the newly created item will be selected after rendering.
+ if ($value) {
+ // Pretend there was a select where the "create new item" section
+ // was, and assign it the value of the item that was just created.
+ $element['#value']['hierarchical_select']['selects'][count($selects)] = $value;
+ }
+ }
+
+ $hs_selection = _hierarchical_select_process_get_hs_selection($element);
+ if ($dropbox) {
+ $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state);
+ }
+ }
+ else {
+ // This handles the cases of:
+ // - $op == t('Update')
+ // - $op == t('Cancel') (used when creating a new item or a new level)
+ // - any other submit button, e.g. the "Preview" button
+ $hs_selection = _hierarchical_select_process_get_hs_selection($element);
+ if ($dropbox) {
+ $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state);
+ }
+ }
+ }
+
+ // Prevent doubles in either array.
+ $hs_selection = array_unique($hs_selection, SORT_REGULAR);
+ $db_selection = array_unique($db_selection, SORT_REGULAR);
+
+ return array($hs_selection, $db_selection);
+}
+
+
+//----------------------------------------------------------------------------
+// Forms API #process callback:
+// Rendering (generation of FAPI code) of hierarchical select and dropbox.
+
+/**
+ * Render the selects in the hierarchical select.
+ *
+ * @param $hsid
+ * A hierarchical select id.
+ * @param $hierarchy
+ * A hierarchy object.
+ * @param $size
+ * The $size to render each select with.
+ * @return
+ * A structured array for use in the Forms API.
+ */
+function _hs_process_render_hs_selects($hsid, $hierarchy, $size) {
+ $form['#tree'] = TRUE;
+ $form['#prefix'] = '
';
+ $form['#suffix'] = '
';
+
+ foreach ($hierarchy->lineage as $depth => $selected_item) {
+ $form[$depth] = array(
+ '#type' => 'select',
+ '#options' => $hierarchy->levels[$depth],
+ '#default_value' => $selected_item,
+ '#size' => $size,
+ // Prevent the select from being wrapped in a div. This simplifies the
+ // CSS and JS code.
+ '#theme_wrappers' => array(),
+ // This alternative to theme_select ets a special class on the level
+ // label option, if any, to make level label styles possible.
+ '#theme' => 'hierarchical_select_select',
+ // Add child information. When a child has no children, its
+ // corresponding "option" element will be marked as such.
+ '#childinfo' => (isset($hierarchy->childinfo[$depth])) ? $hierarchy->childinfo[$depth] : NULL,
+ // Drupal 7's Forms API insists on validating "select" form elements,
+ // despite the fact that this form element is merely part of a larger
+ // whole, with its own #element_validate callback. This disables that
+ // validation.
+ '#validated' => TRUE,
+ );
+ }
+ return $form;
+}
+
+/**
+ * Render the visible part of the dropbox.
+ *
+ * @param $hsid
+ * A hierarchical select id.
+ * @param $dropbox
+ * A dropbox object.
+ * @return
+ * A structured array for use in the Forms API.
+ */
+function _hs_process_render_db_table($hsid, $dropbox) {
+ $element['#tree'] = TRUE;
+ $element['#theme'] = 'hierarchical_select_dropbox_table';
+
+
+ // This information is necessary for the #theme callback.
+ $element['title'] = array('#type' => 'value', '#value' => t($dropbox->title));
+ $element['separator'] = array('#type' => 'value', '#value' => '›');
+ $element['is_empty'] = array('#type' => 'value', '#value' => empty($dropbox->lineages));
+
+
+ if (!empty($dropbox->lineages)) {
+ foreach ($dropbox->lineages as $x => $lineage) {
+
+ // Store position information for the lineage. This will be used in the
+ // #theme callback.
+ $element['lineages']["lineage-$x"] = array(
+ '#zebra' => (($x + 1) % 2 == 0) ? 'even' : 'odd',
+ '#first' => ($x == 0) ? 'first' : '',
+ '#last' => ($x == count($dropbox->lineages) - 1) ? 'last' : '',
+ );
+
+ // Create a 'markup' element for each item in the lineage.
+ foreach ($lineage as $depth => $item) {
+ // The item is selected when save_lineage is enabled (i.e. each item
+ // will be selected), or when the item is the last item in the current
+ // lineage.
+ $is_selected = $dropbox->save_lineage || ($depth == count($lineage) - 1);
+
+ $element['lineages']["lineage-$x"][$depth] = array(
+ '#markup' => $item['label'],
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+ }
+
+ // Finally, create a "Remove" checkbox for the lineage.
+ $element['lineages']["lineage-$x"]['remove'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Remove'),
+ );
+ }
+ }
+
+ return $element;
+}
+
+/**
+ * Render a flat select version of a hierarchical_select form element. This is
+ * necessary for backwards compatibility (together with some Javascript code)
+ * in case of GET forms.
+ *
+ * @param $hierarchy
+ * A hierarchy object.
+ * @param $dropbox
+ * A dropbox object.
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - params
+ * - dropbox
+ * - status
+ * @return
+ * A structured array for use in the Forms API.
+ */
+function _hs_process_render_flat_select($hierarchy, $dropbox, $config) {
+ $selection = array();
+ if ($config['dropbox']['status']) {
+ foreach ($dropbox->lineages_selections as $lineage_selection) {
+ $selection = array_merge($selection, $lineage_selection);
+ }
+ }
+ else {
+ $selection = $hierarchy->lineage;
+ }
+
+ $options = array();
+ foreach ($selection as $value) {
+ $is_valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $value, $config['params']);
+ if ($is_valid) {
+ $options[$value] = $value;
+ }
+ }
+
+ $element = array(
+ '#type' => 'select',
+ '#multiple' => ($config['save_lineage'] || $config['dropbox']['status']),
+ '#options' => $options,
+ '#value' => array_keys($options),
+ // Use a #theme callback to prevent the select from being wrapped in a
+ // div. This simplifies the CSS and JS code.
+ '#theme' => 'hierarchical_select_select',
+ '#attributes' => array('class' => array('flat-select')),
+ );
+
+ return $element;
+}
+
+/**
+ * Calculate the return value of a hierarchical_select form element, based on
+ * the $hierarchy and $dropbox objects. We have to set a return value, because
+ * the values set and used by this form element ($element['#value]) are not
+ * easily usable in the Forms API; we want to return a flat list of item ids.
+ *
+ * @param $hierarchy
+ * A hierarchy object.
+ * @param $dropbox
+ * Optional. A dropbox object.
+ * @param $module
+ * The module that should be used for HS hooks.
+ * @param $params
+ * Optional. An array of parameters, which may be necessary for some
+ * implementations.
+ * @param $save_lineage
+ * Whether the save_lineage setting is enabled or not.
+ * @return
+ * A single item id or a flat array of item ids.
+ */
+function _hierarchical_select_process_calculate_return_value($hierarchy, $dropbox = FALSE, $module, $params, $save_lineage) {
+ if (!$dropbox) {
+ $return_value = _hierarchical_select_hierarchy_validate($hierarchy->lineage, $module, $params);
+ // If the save_lineage setting is disabled, keep only the deepest item.
+ if (!$save_lineage) {
+ $return_value = (is_array($return_value)) ? end($return_value) : NULL;
+ }
+
+ // Prevent a return value of -1. -1 is used for HS' internal system and
+ // means "nothing selected", but to Drupal it *will* seam like a valid
+ // value. Therefore, we set it to NULL.
+ $return_value = ($return_value != -1) ? $return_value : NULL;
+ }
+ else {
+ $return_value = array();
+ foreach ($dropbox->lineages_selections as $x => $selection) {
+ if (!$save_lineage) {
+ // An entry in the dropbox when the save_lineage setting is disabled
+ // is only the deepest item of the generated lineage.
+ $return_value[] = end($selection);
+ }
+ else {
+ // An entry in the dropbox when the save_lineage setting is enabled is
+ // the entire generated lineage, if it's valid (i.e. if the user has
+ // not tampered with it).
+ $lineage = _hierarchical_select_hierarchy_validate($selection, $module, $params);
+ $return_value = array_merge($return_value, $lineage);
+ }
+ }
+ $return_value = array_unique($return_value);
+ }
+
+ return $return_value;
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Inherit the default config from Hierarchical Selects' hook_elements().
+ *
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - params
+ * @return
+ * An updated config array.
+ */
+function _hierarchical_select_inherit_default_config($config, $defaults_override = array()) {
+ // Set defaults for unconfigured settings. Get the defaults from our
+ // hook_elements() implementation. Default properties from this hook are
+ // applied automatically, but properties inside properties, such as is the
+ // case for Hierarchical Select's #config property, aren't applied.
+ $type = hierarchical_select_element_info();
+ $defaults = $type['hierarchical_select']['#config'];
+ // Don't inherit the module and params settings.
+ unset($defaults['module']);
+ unset($defaults['params']);
+
+ // Allow the defaults to be overridden.
+ $defaults = array_smart_merge($defaults, $defaults_override);
+
+ // Apply the defaults to the config.
+ $config = array_smart_merge($defaults, $config);
+
+ return $config;
+}
+
+/**
+ * Convert a hierarchy object into an array of arrays that can be used for
+ * caching an entire hierarchy in a client-side database.
+ *
+ * @param $hierarchy
+ * A hierarchy object.
+ * @return
+ * An array of arrays.
+ */
+function _hierarchical_select_json_convert_hierarchy_to_cache($hierarchy) {
+ // Convert the hierarchy object to an array of values like these:
+ // array('value' => $term_id, 'label => $term_name, 'parent' => $term_id)
+ $cache = array();
+ foreach ($hierarchy->levels as $depth => $items) {
+ $weight = 0;
+ foreach ($items as $value => $label) {
+ $weight++;
+ $cache[] = array(
+ 'value' => $value,
+ 'label' => $label,
+ 'parent' => ($depth == 0) ? 0 : $hierarchy->lineage[$depth - 1],
+ 'weight' => $weight,
+ );
+ }
+ }
+
+ // The last item in the lineage never has any children.
+ $value = end($hierarchy->lineage);
+ $cache[] = array(
+ 'value' => $value . '-has-no-children', // Construct a pseudo-value (will never be actually used).
+ 'label' => '',
+ 'parent' => $value,
+ 'weight' => 0,
+ );
+
+ return $cache;
+}
+
+/**
+ * Helper function that marks every element in the given element as disabled.
+ *
+ * @param &$element
+ * The element of which we want to mark all elements as disabled.
+ * @return
+ * A structured array for use in the Forms API.
+ */
+function _hierarchical_select_mark_as_disabled(&$element) {
+ // Setting $element['#disabled'] = TRUE resulted in undesired side-effects:
+ // when the dropbox limit would be reached after pressing the "Add" button,
+ // then the *entire form* would be submitted. Using #attributes instead does
+ // not trigger this behavior.
+ // Based on documentation of @see _form_builder_handle_input_element():
+ // "If a form wants to start a control off with one of these attributes
+ // for UI purposes only, but still allow input to be processed if it's
+ // sumitted, it can set the desired attribute in #attributes directly
+ // rather than using #disabled."
+ // #disabled prevents #value from containing values for disabled elements,
+ // but using #attributes circumvents this. Most likely, Form API thinks that
+ // because HS' selects are disabled, that the whole of HS is disabled (which
+ // is of course a wrong assumption). Hence it thinks the 'op' that is being
+ // passed ('Add') is wrong and is forcefully being set through JS (which is
+ // also a wrong assumption). Hence it reverts to the main form's default
+ // submit handler.
+ $element['#attributes']['disabled'] = TRUE;
+
+ // Recurse through all children:
+ foreach (element_children($element) as $key) {
+ if (isset($element[$key]) && $element[$key]) {
+ _hierarchical_select_mark_as_disabled($element[$key]);
+ }
+ }
+}
+
+/**
+ * Helper function to determine whether a given depth (i.e. the depth of a
+ * level) is allowed by the allowed_levels setting.
+ *
+ * @param $config
+ * A config array with at least the following settings:
+ * - editability
+ * - allowed_levels
+ * @param $depth
+ * A depth, starting from 0.
+ * @return
+ * 0 or 1 if it allowed_levels is set for the given depth, 1 otherwise.
+ */
+function _hierarchical_select_create_new_item_is_allowed($config, $depth) {
+ return (isset($config['editability']['allowed_levels'][$depth])) ? $config['editability']['allowed_levels'][$depth] : 1;
+}
+
+/**
+ * Helper function that generates the help text is that is displayed to the
+ * user when Javascript is disabled.
+ *
+ * @param $dropbox_is_enabled
+ * Indicates if the dropbox is enabled or not, the help text will be
+ * adjusted depending on this value.
+ * @return
+ * The generated help text (in HTML).
+ */
+function _hierarchical_select_nojs_helptext($dropbox_is_enabled) {
+ $output = '';
+
+ return $output;
+}
+
+/**
+ * Set the 'error' class on the appropriate part of Hierarchical Select,
+ * depending on its configuration.
+ *
+ * @param $element
+ * A Hierarchical Select form item.
+ */
+function _hierarchical_select_form_set_error_class(&$element) {
+ $config = _hierarchical_select_inherit_default_config($element['#config']);
+
+ if ($config['dropbox']['status']) {
+ form_error($element['dropbox']['visible']);
+ }
+ else {
+ for ($i = 0; $i < count(element_children($element['hierarchical_select']['selects'])); $i++) {
+ form_error($element['hierarchical_select']['selects'][$i]);
+ }
+ }
+}
+
+/**
+ * Append messages to Hierarchical Select's log. Used when in developer mode.
+ *
+ * @param $item
+ * Either a message (string) or an array.
+ * @param $reset
+ * Reset the stored log.
+ * @return
+ * Only when the log is being reset, the stored log is returned.
+ */
+function _hierarchical_select_log($item, $reset = FALSE) {
+ static $log;
+
+ if ($reset) {
+ $copy_of_log = $log;
+ $log = array();
+ return $copy_of_log;
+ }
+
+ $log[] = $item;
+}
+
+
+//----------------------------------------------------------------------------
+// Hierarchy object generation functions.
+
+/**
+ * Generate the hierarchy object.
+ *
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - params
+ * - enforce_deepest
+ * - save_lineage
+ * - level_labels
+ * - status
+ * - labels
+ * - editability
+ * - status
+ * - allow_new_levels
+ * - max_levels
+ * @param $selection
+ * The selection based on which a HS should be rendered.
+ * @param $required
+ * Whether the form element is required or not. (#required in Forms API)
+ * @param $dropbox
+ * A dropbox object, or FALSE.
+ * @return
+ * A hierarchy object.
+ */
+function _hierarchical_select_hierarchy_generate($config, $selection, $required, $dropbox = FALSE) {
+ $hierarchy = new stdClass();
+
+ // Convert the 'special_items' setting to a more easily accessible format.
+ if (isset($config['special_items'])) {
+ $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
+ $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
+ }
+
+
+ //
+ // Build the lineage.
+ //
+
+ $start_lineage = microtime();
+
+ // If save_linage is enabled, reconstruct the lineage. This is necessary
+ // because e.g. the taxonomy module stores the terms by order of weight and
+ // lexicography, rather than by hierarchy.
+ if ($config['save_lineage'] && is_array($selection) && count($selection) >= 2) {
+ // Ensure the item in the root level is the first item in the selection.
+ $root_level = array_keys(module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']));
+
+ for ($i = 0; $i < count($selection); $i++) {
+ if (in_array($selection[$i], $root_level)) {
+ if ($i != 0) { // Don't swap if it's already the first item.
+ list($selection[0], $selection[$i]) = array($selection[$i], $selection[0]);
+ }
+ break;
+ }
+ }
+ // Reconstruct all sublevels.
+ for ($i = 0; $i < count($selection); $i++) {
+ $children = array_keys(module_invoke($config['module'], 'hierarchical_select_children', $selection[$i], $config['params']));
+
+ // Ensure the next item in the selection is a child of the current item.
+ for ($j = $i + 1; $j < count($selection); $j++) {
+ if (in_array($selection[$j], $children)) {
+ list($selection[$j], $selection[$i + 1]) = array($selection[$i + 1], $selection[$j]);
+ }
+ }
+ }
+ }
+
+ // Validate the hierarchy.
+ $selection = _hierarchical_select_hierarchy_validate($selection, $config['module'], $config['params']);
+
+ // When nothing is currently selected, set the root level to:
+ // - "" (or its equivalent special item) when:
+ // - enforce_deepest is enabled *and* level labels are enabled *and*
+ // no root level label is set (1), or
+ // - the dropbox is enabled *and* at least one selection has been added
+ // to the dropbox (2)
+ // - "label_0" (the root level label) in all other cases.
+ if ($selection == -1) {
+ $root_level = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']);
+ $first_case = $config['enforce_deepest'] && $config['level_labels']['status'] && !isset($config['level_labels']['labels'][0]);
+ $second_case = $dropbox && count($dropbox->lineages) > 0;
+
+ // If
+ // - the special_items setting has been configured, and
+ // - one special item has the 'none' property
+ // then we'll use the special item instead of the normal "" option.
+ $none_option = (isset($special_items) && count($special_items['none'])) ? $special_items['none'][0] : 'none';
+
+ // Set "" option (or its equivalent special item), or "label_0".
+ $hierarchy->lineage[0] = ($first_case || $second_case) ? $none_option : 'label_0';
+ }
+ else {
+ // If save_lineage setting is enabled, then the selection *is* a lineage.
+ // If it's disabled, we have to generate one ourselves based on the
+ // (deepest) selected item.
+ if ($config['save_lineage']) {
+ // When the form element is optional, the "" setting can be
+ // selected, thus only the first level will be displayed. As a result,
+ // we won't receive an array as the selection, but only a single item.
+ // We convert this into an array.
+ $hierarchy->lineage = (is_array($selection)) ? $selection : array(0 => $selection);
+ }
+ else {
+ $selection = (is_array($selection)) ? $selection[0] : $selection;
+ if (module_invoke($config['module'], 'hierarchical_select_valid_item', $selection, $config['params'])) {
+ $hierarchy->lineage = module_invoke($config['module'], 'hierarchical_select_lineage', $selection, $config['params']);
+ }
+ else {
+ // If the selected item is invalid, then start with an empty lineage.
+ $hierarchy->lineage = array();
+ }
+ }
+ }
+
+ // If enforce_deepest is enabled, ensure that the lineage goes as deep as
+ // possible: append values of items that will be selected by default.
+ if ($config['enforce_deepest'] && !in_array($hierarchy->lineage[0], array('none', 'label_0'))) {
+ $hierarchy->lineage = _hierarchical_select_hierarchy_enforce_deepest($hierarchy->lineage, $config['module'], $config['params']);
+ }
+
+ $end_lineage = microtime();
+
+
+ //
+ // Build the levels.
+ //
+
+ $start_levels = microtime();
+
+ // Start building the levels, initialize with the root level.
+ $hierarchy->levels[0] = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']);
+ $hierarchy->levels[0] = _hierarchical_select_apply_entity_settings($hierarchy->levels[0], $config);
+
+ // Prepend a "" option to the root level when:
+ // - the editability setting is enabled, and
+ // - the hook is implemented (this is an optional hook), and
+ // - the logged in user has permission to edit terms in this vocabulary, and
+ // - the allowed_levels setting allows to create new items at this level.
+ if (!empty($config['editability']['status'])
+ && module_hook($config['module'], 'hierarchical_select_create_item')
+ && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
+ && _hierarchical_select_create_new_item_is_allowed($config, 0)
+ ) {
+ $item_type = (isset($config['editability']['item_types']) && count($config['editability']['item_types']) > 0)
+ ? t($config['editability']['item_types'][0])
+ : t('item');
+ $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
+ $hierarchy->levels[0] = array('create_new_item' => $option) + $hierarchy->levels[0];
+ }
+
+ // Prepend a "" option to the root level when:
+ // - the form element is optional (1), or
+ // - enforce_deepest is enabled (2), or
+ // - the dropbox is enabled *and* at least one selection has been added to
+ // the dropbox (3)
+ // except when:
+ // - level labels are enabled
+ // - the special_items setting has been configured, and
+ // - one special item has the 'none' property
+ $first_case = !$required;
+ $second_case = $config['enforce_deepest'];
+ $third_case = $dropbox && count($dropbox->lineages) > 0;
+ if (($first_case || $second_case || $third_case) && (!$config['level_labels']['status'] && isset($special_items) && !count($special_items['none']))) {
+ $option = theme('hierarchical_select_special_option', array('option' => t('none')));
+ $hierarchy->levels[0] = array('none' => $option) + $hierarchy->levels[0];
+ }
+
+ // Calculate the lineage's depth (starting from 0).
+ $max_depth = count($hierarchy->lineage) - 1;
+
+ // Build all sublevels, based on the lineage.
+ for ($depth = 1; $depth <= $max_depth; $depth++) {
+ $hierarchy->levels[$depth] = module_invoke($config['module'], 'hierarchical_select_children', $hierarchy->lineage[$depth - 1], $config['params']);
+ $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config);
+ }
+
+ if ($config['enforce_deepest']) {
+ // Prepend a "" option to each level below the root level
+ // when:
+ // - the editability setting is enabled, and
+ // - the hook is implemented (this is an optional hook), and
+ // - the allowed_levels setting allows to create new items at this level.
+ if (!empty($config['editability']['status'])
+ && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
+ && module_hook($config['module'], 'hierarchical_select_create_item')) {
+ for ($depth = 1; $depth <= $max_depth; $depth++) {
+ $item_type = (count($config['editability']['item_types']) >= $depth)
+ ? t($config['editability']['item_types'][$depth])
+ : t('item');
+ $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
+ if (_hierarchical_select_create_new_item_is_allowed($config, $depth)) {
+ $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth];
+ }
+ }
+ }
+
+ // If level labels are enabled and the root label is set, prepend it.
+ if ($config['level_labels']['status'] && isset($config['level_labels']['labels'][0])) {
+ $hierarchy->levels[0] = array('label_0' => t($config['level_labels']['labels'][0])) + $hierarchy->levels[0];
+ }
+ }
+ else if (!$config['enforce_deepest']) {
+ // Prepend special options to every level.
+ for ($depth = 0; $depth <= $max_depth; $depth++) {
+ // Prepend a "" option to the current level when:
+ // - this is not the root level (the root level already has this), and
+ // - the editability setting is enabled, and
+ // - the hook is implemented (this is an optional hook), and
+ // - the logged in user has permission to edit terms in this vocabulary, and
+ // - the allowed_levels setting allows to create new items at this level.
+ if ($depth > 0
+ && !empty($config['editability']['status'])
+ && module_hook($config['module'], 'hierarchical_select_create_item')
+ && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
+ && _hierarchical_select_create_new_item_is_allowed($config, $depth)
+ ) {
+ $item_type = (count($config['editability']['item_types']) == $depth)
+ ? t($config['editability']['item_types'][$depth])
+ : t('item');
+ $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
+ $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth];
+ }
+ // Level label: set an empty level label if they've been disabled.
+ $label = ($config['level_labels']['status'] && isset($config['level_labels']['labels'][$depth])) ? t($config['level_labels']['labels'][$depth]) : '';
+ $hierarchy->levels[$depth] = array('label_' . $depth => $label) + $hierarchy->levels[$depth];
+ }
+
+ // If the root level label is empty and the none option is present, remove
+ // the root level label because it's conceptually identical.
+ if ($hierarchy->levels[0]['label_0'] == '' && isset($hierarchy->levels[0]['none'])) {
+ unset($hierarchy->levels[0]['label_0']);
+ // Update the selected lineage when necessary to prevent an item that
+ // doesn't exist from being "selected" internally.
+ if ($hierarchy->lineage[0] == 'label_0') {
+ $hierarchy->lineage[0] = 'none';
+ }
+ }
+
+ // Add one more level if appropriate.
+ $parent = $hierarchy->lineage[$max_depth];
+ if (module_invoke($config['module'], 'hierarchical_select_valid_item', $parent, $config['params'])) {
+ $children = module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']);
+ if (count($children)) {
+ // We're good, let's add one level!
+ $depth = $max_depth + 1;
+
+ $hierarchy->levels[$depth] = array();
+
+ // Prepend a "" option to the current level when:
+ // - the editability setting is enabled, and
+ // - the hook is implemented (this is an optional hook), and
+ // - the logged in user has permission to edit terms in this vocabulary, and
+ // - the allowed_levels setting allows to create new items at this level.
+ if (!empty($config['editability']['status'])
+ && module_hook($config['module'], 'hierarchical_select_create_item')
+ && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
+ && _hierarchical_select_create_new_item_is_allowed($config, $depth)
+ ) {
+ $item_type = (count($config['editability']['item_types']) >= $depth)
+ ? t($config['editability']['item_types'][$depth])
+ : t('item');
+ $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
+ $hierarchy->levels[$depth] = array('create_new_item' => $option);
+ }
+
+ // Level label: set an empty level label if they've been disabled.
+ $hierarchy->lineage[$depth] = 'label_' . $depth;
+ $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : '';
+ $hierarchy->levels[$depth] = array('label_' . $depth => $label) + $hierarchy->levels[$depth] + $children;
+
+ $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config);
+ }
+ }
+ }
+
+ // Add an extra level with only a level label and a ""
+ // option, if:
+ // - the editability setting is enabled
+ // - the allow_new_levels setting is enabled
+ // - an additional level is permitted by the max_levels setting
+ // - the logged in user has permission to edit terms in this vocabulary
+ // - the deepest item of the lineage is a valid item
+ // NOTE: this uses an optional hook, so we also check if it's implemented.
+ if (!empty($config['editability']['status'])
+ && !empty($config['editability']['allow_new_levels'])
+ && ($config['editability']['max_levels'] == 0 || count($hierarchy->lineage) < $config['editability']['max_levels'])
+ && module_invoke($config['module'], 'hierarchical_select_valid_item', end($hierarchy->lineage), $config['params'])
+ && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
+ && module_hook($config['module'], 'hierarchical_select_create_item')
+ ) {
+ $depth = $max_depth + 1;
+
+ // Level label: set an empty level label if they've been disabled.
+ $hierarchy->lineage[$depth] = 'label_' . $depth;
+ $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : '';
+
+ // Item type.
+ $item_type = (count($config['editability']['item_types']) >= $depth)
+ ? t($config['editability']['item_types'][$depth])
+ : t('item');
+
+ // The new level with only a level label and a "" option.
+ $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
+ $hierarchy->levels[$depth] = array(
+ 'label_' . $depth => $label,
+ 'create_new_item' => $option,
+ );
+ }
+
+ // Calculate the time it took to generate the levels.
+ $end_levels = microtime();
+
+ // Add child information.
+ $start_childinfo = microtime();
+ $hierarchy = _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config);
+ $end_childinfo = microtime();
+
+ // Calculate the time it took to build the hierarchy object.
+ $hierarchy->build_time['total'] = ($end_childinfo - $start_lineage) * 1000;
+ $hierarchy->build_time['lineage'] = ($end_lineage - $start_lineage) * 1000;
+ $hierarchy->build_time['levels'] = ($end_levels - $start_levels) * 1000;
+ $hierarchy->build_time['childinfo'] = ($end_childinfo - $start_childinfo) * 1000;
+
+ return $hierarchy;
+}
+
+/**
+ * Given a level, apply the entity_count and require_entity settings.
+ *
+ * @param $level
+ * A level in the hierarchy.
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - params
+ * - entity_count
+ * - require_entity
+ * @return
+ * The updated level
+ */
+function _hierarchical_select_apply_entity_settings($level, $config) {
+ if (isset($config['special_items'])) {
+ $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
+ $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
+ }
+
+ // Only do something when the entity_count or the require_entity (or both)
+ // settings are enabled.
+ // NOTE: this uses the optional "hierarchical_select_entity_count" hook, so
+ // we also check if it's implemented.
+ if (isset($config['entity_count']['enabled']) && ($config['entity_count']['enabled'] || $config['entity_count']['require_entity']) && module_hook($config['module'], 'hierarchical_select_entity_count')) {
+ foreach ($level as $item => $label) {
+ // We don't want to alter internal or special items.
+ if (!preg_match('/(none|label_\d+|create_new_item)/', $item)
+ && !in_array($item, $special_items['exclusive'])
+ && !in_array($item, $special_items['none'])
+ ) {
+ // Add our entity count settings to the parameters.
+ $config['params'] += array(
+ 'entity_count' => array(
+ 'settings' => array(
+ 'count_children' => $config['entity_count']['settings']['count_children'],
+ 'entity_types' => $config['entity_count']['settings']['entity_types'],
+ ),
+ ),
+ );
+ $entity_count = module_invoke($config['module'], 'hierarchical_select_entity_count', $item, $config['params']);
+
+ // When the require_entity setting is enabled and the entity count is
+ // zero, then remove the item from the level.
+ // When the item is not removed from the level due to the above and
+ // the entity_count setting is enabled, update the label of the item
+ // to include the entity count.
+ if ($config['entity_count']['require_entity'] && $entity_count == 0) {
+ unset($level[$item]);
+ }
+ elseif ($config['entity_count']['enabled']) {
+ $level[$item] = "$label ($entity_count)";
+ }
+ }
+ }
+ }
+
+ return $level;
+}
+
+/**
+ * Extends a hierarchy object with child information: for each item in the
+ * hierarchy, the child count will be retrieved and stored in the hierarchy
+ * object, in the "childinfo" property. Items are grouped per level.
+ *
+ * @param $hierarchy
+ * A hierarchy object with the "levels" property set.
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - params
+ * @return
+ * An updated hierarchy object with the "childinfo" property set.
+ */
+function _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config) {
+ foreach ($hierarchy->levels as $depth => $level) {
+ foreach (array_keys($level) as $item) {
+ if (!preg_match('/(none|label_\d+|create_new_item)/', $item)) {
+ $hierarchy->childinfo[$depth][$item] = count(module_invoke($config['module'], 'hierarchical_select_children', $item, $config['params']));
+ }
+ }
+ }
+
+ return $hierarchy;
+}
+
+/**
+ * Reset the selection if no valid item was selected. The first item in the
+ * array corresponds to the first selected term. As soon as an invalid item
+ * is encountered, the lineage from that level to the deeper levels should be
+ * unset. This is so to ignore selection of a level label.
+ *
+ * @param $selection
+ * Either a single item id or an array of item ids.
+ * @param $module
+ * The module that should be used for HS hooks.
+ * @param $params
+ * The module that should be passed to HS hooks.
+ * @return
+ * The updated selection.
+ */
+function _hierarchical_select_hierarchy_validate($selection, $module, $params) {
+ $valid = TRUE;
+ $selection_levels = count($selection);
+ for ($i = 0; $i < $selection_levels; $i++) {
+ // As soon as one invalid item has been found, we'll stop validating; all
+ // subsequently selected items will be removed from the selection.
+ if ($valid) {
+ $valid = module_invoke($module, 'hierarchical_select_valid_item', $selection[$i], $params);
+ if ($i > 0) {
+ $parent = $selection[$i - 1];
+ $child = $selection[$i];
+ $children = array_keys(module_invoke($module, 'hierarchical_select_children', $parent, $params));
+ $valid = $valid && in_array($child, $children);
+ }
+ }
+ if (!$valid) {
+ unset($selection[$i]);
+ }
+ }
+
+ if (empty($selection)) {
+ $selection = -1;
+ }
+ if (is_array($selection)) {
+ // This is needed because we may have unset some values and we don't want
+ // any gaps in the indexes (ie. the indexes would be 0,1,3 if we did
+ // "$selection[] = X" after unsetting #2).
+ $selection = array_values($selection);
+ }
+
+ return $selection;
+}
+
+/**
+ * Helper function to update the lineage of the hierarchy to ensure that the
+ * user selects an item in the deepest level of the hierarchy.
+ *
+ * @param $lineage
+ * The lineage up to the deepest selection the user has made so far.
+ * @param $module
+ * The module that should be used for HS hooks.
+ * @param $params
+ * The params that should be passed to HS hooks.
+ * @return
+ * The updated lineage.
+ */
+function _hierarchical_select_hierarchy_enforce_deepest($lineage, $module, $params) {
+ // Use the deepest item as the first parent. Then apply this algorithm:
+ // 1) get the parent's children, stop if no children
+ // 2) choose the first child as the option that is selected by default, by
+ // adding it to the lineage of the hierarchy
+ // 3) make this child the parent, go to step 1.
+ $parent = end($lineage); // The last item in the lineage is the deepest one.
+ $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
+ while (count($children)) {
+ $keys = array_keys($children);
+ $first_child = $keys[0];
+ $lineage[] = $first_child;
+ $parent = $first_child;
+ $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
+ }
+
+ return $lineage;
+}
+
+
+//----------------------------------------------------------------------------
+// Dropbox object generation functions.
+
+/**
+ * Generate the dropbox object.
+ *
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - save_lineage
+ * - params
+ * - dropbox
+ * - title
+ * @param $selection
+ * The selection based on which a dropbox should be generated.
+ * @return
+ * A dropbox object.
+ */
+function _hierarchical_select_dropbox_generate($config, $selection) {
+ $dropbox = new stdClass();
+ $start = microtime();
+
+ $dropbox->title = (!empty($config['dropbox']['title'])) ? filter_xss_admin($config['dropbox']['title']) : t('All selections');
+ $dropbox->lineages = array();
+ $dropbox->lineages_selections = array();
+
+ // Clean selection.
+ foreach ($selection as $key => $item) {
+ if (!module_invoke($config['module'], 'hierarchical_select_valid_item', $item, $config['params'])) {
+ unset($selection[$key]);
+ }
+ }
+
+ if (!empty($selection)) {
+ // Store the "save lineage" setting, needed in the rendering layer.
+ $dropbox->save_lineage = $config['save_lineage'];
+ if ($config['save_lineage']) {
+ $dropbox->lineages = _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($config['module'], $selection, $config['params']);
+ }
+ else {
+ // Retrieve the lineage of each item.
+ foreach ($selection as $item) {
+ $dropbox->lineages[] = module_invoke($config['module'], 'hierarchical_select_lineage', $item, $config['params']);
+ }
+
+ // We will also need the labels of each item in the rendering layer.
+ foreach ($dropbox->lineages as $id => $lineage) {
+ foreach ($lineage as $level => $item) {
+ $dropbox->lineages[$id][$level] = array('value' => $item, 'label' => module_invoke($config['module'], 'hierarchical_select_item_get_label', $item, $config['params']));
+ }
+ }
+ }
+
+ // Sanitize the labels.
+ foreach ($dropbox->lineages as $id => $lineage) {
+ foreach ($lineage as $level => $item) {
+ $dropbox->lineages[$id][$level]['label'] = check_plain($dropbox->lineages[$id][$level]['label']);
+ }
+ }
+
+ if (!isset($config['dropbox']['sort']) || $config['dropbox']['sort']){
+ usort($dropbox->lineages, '_hierarchical_select_dropbox_sort');
+ }
+
+ // Now store each lineage's selection too. This is needed on the client side
+ // to enable the remove button to let the server know which selected items
+ // should be removed.
+ foreach ($dropbox->lineages as $id => $lineage) {
+ if ($config['save_lineage']) {
+ // Store the entire lineage.
+ $dropbox->lineages_selections[$id] = array_map('_hierarchical_select_dropbox_lineage_item_get_value', $lineage);
+ }
+ else {
+ // Store only the last (aka the deepest) value of the lineage.
+ $dropbox->lineages_selections[$id][0] = $lineage[count($lineage) - 1]['value'];
+ }
+ }
+ }
+
+ // Calculate the time it took to build the dropbox object.
+ $dropbox->build_time = (microtime() - $start) * 1000;
+
+ return $dropbox;
+}
+
+/**
+ * Helper function to reconstruct the lineages given a set of selected items
+ * and the fact that the "save lineage" setting is enabled.
+ *
+ * Note that it's impossible to predict how many lineages if we know the
+ * number of selected items, exactly because the "save lineage" setting is
+ * enabled.
+ *
+ * Worst case time complexity is O(n^3), optimizations are still possible.
+ *
+ * @param $module
+ * The module that should be used for HS hooks.
+ * @param $selection
+ * The selection based on which a dropbox should be generated.
+ * @param $params
+ * Optional. An array of parameters, which may be necessary for some
+ * implementations.
+ * @return
+ * An array of dropbox lineages.
+ */
+function _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($module, $selection, $params) {
+ // We have to reconstruct all lineages from the given set of selected items.
+ // That means: we have to reconstruct every possible combination!
+ $lineages = array();
+ $root_level = module_invoke($module, 'hierarchical_select_root_level', $params);
+
+ foreach ($selection as $key => $item) {
+ // Create new lineage if the item can be found in the root level.
+ if (array_key_exists($item, $root_level)) {
+ $lineages[][0] = array('value' => $item, 'label' => $root_level[$item]);
+ unset($selection[$key]);
+ }
+ }
+
+ // Keep on trying as long as at least one lineage has been extended.
+ $at_least_one = TRUE;
+ for ($level = 0; $at_least_one; $level++) {
+ $at_least_one = FALSE;
+ $num = count($lineages);
+
+ // Try to extend every lineage. Make sure we don't iterate over
+ // possibly new lineages.
+ for ($id = 0; $id < $num; $id++) {
+ // Only try to extend a lineage if it has an item at the current level.
+ if (!isset($lineages[$id][$level])) {
+ continue;
+ }
+ $children = module_invoke($module, 'hierarchical_select_children', $lineages[$id][$level]['value'], $params);
+
+ $child_added_to_lineage = FALSE;
+ foreach (array_keys($children) as $child) {
+ if (in_array($child, $selection)) {
+ if (!$child_added_to_lineage) {
+ // Add the child to the lineage.
+ $lineages[$id][$level + 1] = array('value' => $child, 'label' => $children[$child]);
+ $child_added_to_lineage = TRUE;
+ $at_least_one = TRUE;
+ }
+ else {
+ // Create new lineage based on current one and add the child.
+ $lineage = $lineages[$id];
+ $lineage[$level + 1] = array('value' => $child, 'label' => $children[$child]);
+
+ // Add the new lineage to the set of lineages
+ $lineages[] = $lineage;
+ }
+ }
+ }
+ }
+ }
+
+ return $lineages;
+}
+
+/**
+ * Dropbox lineages sorting callback.
+ *
+ * @param $lineage_a
+ * The first lineage.
+ * @param $lineage_b
+ * The second lineage.
+ * @return
+ * An integer that determines which of the two lineages comes first.
+ */
+function _hierarchical_select_dropbox_sort($lineage_a, $lineage_b) {
+ $string_a = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_a));
+ $string_b = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_b));
+ return strcmp($string_a, $string_b);
+}
+
+/**
+ * Helper function needed for the array_map() call in the dropbox sorting
+ * callback.
+ *
+ * @param $item
+ * An item in a dropbox lineage.
+ * @return
+ * The value associated with the "label" key of the item.
+ */
+function _hierarchical_select_dropbox_lineage_item_get_label($item) {
+ return t($item['label']);
+}
+
+/**
+ * Helper function needed for the array_map() call in the dropbox lineages
+ * selections creation.
+ *
+ * @param $item
+ * An item in a dropbox lineage.
+ * @return
+ * The value associated with the "value" key of the item.
+ */
+function _hierarchical_select_dropbox_lineage_item_get_value($item) {
+ return $item['value'];
+}
+
+/**
+ * Smarter version of array_merge_recursive: overwrites scalar values.
+ *
+ * From: http://www.php.net/manual/en/function.array-merge-recursive.php#82976.
+ */
+if (!function_exists('array_smart_merge')) {
+ function array_smart_merge($array, $override) {
+ if (is_array($array) && is_array($override)) {
+ foreach ($override as $k => $v) {
+ if (isset($array[$k]) && is_array($v) && is_array($array[$k])) {
+ $array[$k] = array_smart_merge($array[$k], $v);
+ }
+ else {
+ $array[$k] = $v;
+ }
+ }
+ }
+ return $array;
+ }
+}
+
+/**
+ * Helper function needed for the array_filter() call to filter the items
+ * marked with the 'exclusive' property
+ *
+ * @param $item
+ * An item in the 'special_items' setting.
+ * @return
+ * TRUE if it's marked with the 'exclusive' property, FALSE otherwise.
+ */
+function _hierarchical_select_special_item_exclusive($item) {
+ return in_array('exclusive', $item);
+}
+
+/**
+ * Helper function needed for the array_filter() call to filter the items
+ * marked with the 'none' property
+ *
+ * @param $item
+ * An item in the 'special_items' setting.
+ * @return
+ * TRUE if it's marked with the 'none' property, FALSE otherwise.
+ */
+function _hierarchical_select_special_item_none($item) {
+ return in_array('none', $item);
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select_cache.js b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select_cache.js
new file mode 100644
index 00000000..dbac146d
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select_cache.js
@@ -0,0 +1,229 @@
+
+/**
+ * @file
+ * Cache system for Hierarchical Select.
+ * This cache system takes advantage of the HTML 5 client-side database
+ * storage specification to reduce the number of queries to the server. A lazy
+ * loading strategy is used.
+ */
+
+
+/**
+ * Note: this cache system can be replaced by another one, as long as you
+ * provide the following methods:
+ * - initialize()
+ * - status()
+ * - load()
+ * - sync()
+ * - updateHierarchicalSelect()
+ *
+ * TODO: better documentation
+ */
+
+(function ($) {
+
+Drupal.HierarchicalSelect.cache = {};
+
+Drupal.HierarchicalSelect.cache.initialize = function() {
+ try {
+ if (window.openDatabase) {
+ this.db = openDatabase("Hierarchical Select", "3.x", "Hierarchical Select cache", 200000);
+
+ this.db
+ // Create the housekeeping table if it doesn't exist yet.
+ .transaction(function(tx) {
+ tx.executeSql("SELECT COUNT(*) FROM hierarchical_select", [], null, function(tx, error) {
+ tx.executeSql("CREATE TABLE hierarchical_select (table_name TEXT UNIQUE, expires REAL)", []);
+ console.log("Created housekeeping table.");
+ });
+ })
+ // Empty tables that have expired, based on the information in the
+ // housekeeping table.
+ .transaction(function(tx) {
+ tx.executeSql("SELECT table_name FROM hierarchical_select WHERE expires < ?", [ new Date().getTime() ], function(tx, resultSet) {
+ for (var i = 0; i < resultSet.rows.length; i++) {
+ var row = resultSet.rows.item(i);
+ var newExpiresTimestamp = new Date().getTime() + 86400;
+
+ tx.executeSql("DELETE * FROM " + row.table_name);
+ tx.executeSql("UPDATE hierarchical_select SET expires = ? WHERE table_name = ?", [ newExpiresTimestamp, row.table_name ]);
+
+ console.log("Table "+ row.table_name +" was expired: emptied it. Will expire again in "+ (newExpiresTimestamp - new Date().getTime()) / 3600 +" hours.");
+ }
+ });
+ });
+ }
+ else {
+ this.db = false;
+ }
+ }
+ catch(err) { }
+};
+
+Drupal.HierarchicalSelect.cache.status = function() {
+ return Drupal.HierarchicalSelect.cache.db !== false;
+};
+
+Drupal.HierarchicalSelect.cache.table = function(hsid) {
+ return Drupal.settings.HierarchicalSelect.settings[hsid].cacheId;
+};
+
+Drupal.HierarchicalSelect.cache.load = function(hsid) {
+ // If necessary, create the cache table for the given Hierarchical Select.
+ Drupal.HierarchicalSelect.cache.db.transaction(function(tx) {
+ var table = Drupal.HierarchicalSelect.cache.table(hsid);
+
+ tx.executeSql("SELECT value FROM "+ table, [], function(tx, resultSet) {
+ console.log("" + resultSet.rows.length + " cached items in the " + table + " table.");
+ }, function(tx, error) {
+ var expiresTimestamp = new Date().getTime() + 86400;
+
+ tx.executeSql("CREATE TABLE "+ table +" (parent REAL, value REAL UNIQUE, label REAL, weight REAL)");
+ tx.executeSql("INSERT INTO hierarchical_select (table_name, expires) VALUES (?, ?)", [ table, expiresTimestamp ]);
+
+ console.log("Created table "+ table +", will expire in "+ (expiresTimestamp - new Date().getTime()) / 3600 +" hours.");
+ });
+ });
+};
+
+Drupal.HierarchicalSelect.cache.insertOnDuplicateKeyUpdate = function(table, row) {
+// console.log("storing: value: "+ row.value +", label: "+ row.label +", parent: "+ row.parent +", weight: "+ row.weight);
+ Drupal.HierarchicalSelect.cache.db.transaction(function(tx) {
+ tx.executeSql("INSERT INTO "+ table +" (parent, value, label, weight) VALUES (?, ?, ?, ?)", [ row.parent, row.value, row.label, row.weight ], null, function(tx, error) {
+// console.log("UPDATING value: "+ row.value +", label: "+ row.label +", parent: "+ row.parent +", weight: "+ row.weight);
+ tx.executeSql("UPDATE "+ table +" SET parent = ?, label = ?, weight = ? WHERE value = ?", [ row.parent, row.label, row.weight, row.value ], null, function(tx, error) {
+// console.log("sql error: " + error.message);
+ });
+ });
+ });
+};
+
+Drupal.HierarchicalSelect.cache.sync = function(hsid, info) {
+ var table = Drupal.HierarchicalSelect.cache.table(hsid);
+ for (var id in info) {
+ var closure = function(_info, id) {
+ Drupal.HierarchicalSelect.cache.insertOnDuplicateKeyUpdate(table, _info[id]);
+ } (info, id);
+ }
+};
+
+Drupal.HierarchicalSelect.cache.hasChildren = function(hsid, value, successCallback, failCallback) {
+ var table = Drupal.HierarchicalSelect.cache.table(hsid);
+ Drupal.HierarchicalSelect.cache.db.transaction(function(tx) {
+ tx.executeSql("SELECT * FROM "+ table +" WHERE parent = ?", [ value ], function(tx, resultSet) {
+ if (resultSet.rows.length > 0) {
+ successCallback();
+ }
+ else {
+ failCallback();
+ }
+ });
+ });
+};
+
+Drupal.HierarchicalSelect.cache.getSubLevels = function(hsid, value, callback, previousSubLevels) {
+ var table = Drupal.HierarchicalSelect.cache.table(hsid);
+
+ var subLevels = new Array();
+ if (previousSubLevels != undefined) {
+ subLevels = previousSubLevels;
+ }
+
+ Drupal.HierarchicalSelect.cache.db.transaction(function(tx) {
+ tx.executeSql("SELECT value, label FROM "+ table +" WHERE parent = ? ORDER BY weight", [ value ], function(tx, resultSet) {
+ var numChildren = resultSet.rows.length;
+
+ // If there's only one child, check if it has the dummy "-has-no-children" value.
+ if (numChildren == 1) {
+ var valueOfFirstRow = String(resultSet.rows.item(0).value);
+ var isDummy = valueOfFirstRow.match(/^.*-has-no-children$/);
+ }
+
+ // Only pass the children if there are any (and not a fake one either).
+ if (numChildren && !isDummy) {
+ var level = new Array();
+ for (var i = 0; i < resultSet.rows.length; i++) {
+ var row = resultSet.rows.item(i);
+ level[i] = { 'value' : row.value, 'label' : row.label };
+ console.log("child of "+ value +": ("+ row.value +", "+ row.label +")");
+ }
+
+ subLevels.push(level);
+
+ Drupal.HierarchicalSelect.cache.getSubLevels(hsid, level[0].value, callback, subLevels);
+ }
+ else {
+ if (subLevels.length > 0) {
+ callback(subLevels);
+ }
+ else {
+ callback(false);
+ }
+ }
+ });
+ });
+};
+
+Drupal.HierarchicalSelect.cache.createAndUpdateSelects = function(hsid, subLevels, lastUnchanged) {
+ // Remove all levels below the level in which a value was selected, if they
+ // exist.
+ // Note: the root level can never change because of this!
+ $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select').slice(lastUnchanged).remove();
+
+ // Create the new sublevels, by cloning the root level and then modifying
+ // that clone.
+ var $rootSelect = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select:first');
+ for (var depth in subLevels) {
+ var optionElements = $.map(subLevels[depth], function(item) { return ''; });
+
+ var level = parseInt(lastUnchanged) + parseInt(depth);
+
+ $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select:last').after(
+ $rootSelect.clone()
+ // Update the name attribute.
+ .attr('name', $rootSelect.attr('name').replace(/(.*)\d+\]$/, "$1"+ level +"]"))
+ // Update the id attribute.
+ .attr('id', $rootSelect.attr('id').replace(/(.*-hierarchical-select-selects-)\d+/, "$1"+ level))
+ // Remove the existing options and set the new ones.
+ .empty().append(optionElements.join(''))
+ );
+ }
+};
+
+Drupal.HierarchicalSelect.cache.updateHierarchicalSelect = function(hsid, value, settings, lastUnchanged, ajaxOptions) {
+ // If the selected value has children
+ Drupal.HierarchicalSelect.cache.hasChildren(hsid, value, function() {
+ console.log("Cache hit.");
+ Drupal.HierarchicalSelect.cache.getSubLevels(hsid, value, function(subLevels) {
+ Drupal.HierarchicalSelect.preUpdateAnimations(hsid, 'update-hierarchical-select', lastUnchanged, function() {
+ if (subLevels !== false) {
+ Drupal.HierarchicalSelect.cache.createAndUpdateSelects(hsid, subLevels, lastUnchanged);
+ }
+ else {
+ // Nothing must happen: the user selected a value that doesn't
+ // have any subLevels.
+ $('#hierarchical-select-' + hsid + '-wrapper .hierarchical-select .selects select').slice(lastUnchanged).remove();
+ }
+
+ Drupal.HierarchicalSelect.postUpdateAnimations(hsid, 'update-hierarchical-select', lastUnchanged, function() {
+ // Reattach the bindings.
+ Drupal.HierarchicalSelect.attachBindings(hsid);
+
+ Drupal.HierarchicalSelect.triggerEvents(hsid, 'update-hierarchical-select', settings);
+
+ // The selection of this hierarchical select has changed!
+ Drupal.HierarchicalSelect.triggerEvents(hsid, 'change-hierarchical-select', settings);
+ });
+ });
+ });
+ }, function() {
+ // This item was not yet requested before, so we still have to perform
+ // the dynamic form submit.
+ console.log("Cache miss. Querying the server.");
+ Drupal.HierarchicalSelect.preUpdateAnimations(hsid, 'update-hierarchical-select', lastUnchanged, function() {
+ $.ajax(ajaxOptions);
+ });
+ });
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select_formtoarray.js b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select_formtoarray.js
new file mode 100644
index 00000000..62af3ec8
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/hierarchical_select_formtoarray.js
@@ -0,0 +1,95 @@
+
+/**
+ * @file
+ * Contains the formToArray method and the method it depends on. Taken from
+ * jQuery Form Plugin 2.12. (http://www.malsup.com/jquery/form/)
+ */
+
+(function ($) {
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property. An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic) {
+ var a = [];
+ if (this.length == 0) return a;
+
+ var form = this[0];
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
+ if (!els) return a;
+ for(var i=0, max=els.length; i < max; i++) {
+ var el = els[i];
+ var n = el.name;
+ if (!n) continue;
+
+ if (semantic && form.clk && el.type == "image") {
+ // handle image inputs on the fly when semantic == true
+ if(!el.disabled && form.clk == el)
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ continue;
+ }
+
+ var v = $.fieldValue(el, true);
+ if (v && v.constructor == Array) {
+ for(var j=0, jmax=v.length; j < jmax; j++)
+ a.push({name: n, value: v[j]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: n, value: v});
+ }
+
+ if (!semantic && form.clk) {
+ // input type=='image' are not found in elements array! handle them here
+ var inputs = form.getElementsByTagName("input");
+ for(var i=0, max=inputs.length; i < max; i++) {
+ var input = inputs[i];
+ var n = input.name;
+ if(n && !input.disabled && input.type == "image" && form.clk == input)
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ }
+ return a;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+ if (typeof successful == 'undefined') successful = true;
+
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+ tag == 'select' && el.selectedIndex == -1))
+ return null;
+
+ if (tag == 'select') {
+ var index = el.selectedIndex;
+ if (index < 0) return null;
+ var a = [], ops = el.options;
+ var one = (t == 'select-one');
+ var max = (one ? index+1 : ops.length);
+ for(var i=(one ? index : 0); i < max; i++) {
+ var op = ops[i];
+ if (op.selected) {
+ // extra pain for IE...
+ var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value;
+ if (one) return v;
+ a.push(v);
+ }
+ }
+ return a;
+ }
+ return el.value;
+};
+
+})(jQuery);
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/images/arrow-rtl.png b/sites/all/modules/contrib/fields/hierarchical_select/images/arrow-rtl.png
new file mode 100644
index 00000000..2908c38b
Binary files /dev/null and b/sites/all/modules/contrib/fields/hierarchical_select/images/arrow-rtl.png differ
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/images/arrow.png b/sites/all/modules/contrib/fields/hierarchical_select/images/arrow.png
new file mode 100644
index 00000000..cb901e2b
Binary files /dev/null and b/sites/all/modules/contrib/fields/hierarchical_select/images/arrow.png differ
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/images/grippie.png b/sites/all/modules/contrib/fields/hierarchical_select/images/grippie.png
new file mode 100644
index 00000000..6524d416
Binary files /dev/null and b/sites/all/modules/contrib/fields/hierarchical_select/images/grippie.png differ
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/includes/common.inc b/sites/all/modules/contrib/fields/hierarchical_select/includes/common.inc
new file mode 100644
index 00000000..151873f8
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/includes/common.inc
@@ -0,0 +1,441 @@
+ $strings['item'],
+ '!items' => $strings['items'],
+ '!entity' => $strings['entity'],
+ '!entities' => $strings['entities'],
+ '!hierarchy' => $strings['hierarchy'],
+ '!hierarchies' => $strings['hierarchies']
+ );
+
+ $form = array(
+ '#tree' => TRUE,
+ '#type' => 'fieldset',
+ '#title' => t('Hierarchical Select configuration'),
+ '#attributes' => array(
+ 'class' => array('hierarchical-select-config-form'),
+ 'id' => 'hierarchical-select-config-form-' . $config_id,
+ ),
+ '#attached' => array(
+ 'css' => array(
+ drupal_get_path('module', 'hierarchical_select') . '/includes/common_config_form.css'
+ ),
+ 'js' => array(
+ array(
+ 'type' => 'file',
+ 'data' => drupal_get_path('module', 'hierarchical_select') . '/includes/common_config_form.js',
+ ),
+ array(
+ 'type' => 'setting',
+ 'data' => array('HierarchicalSelect' => array('configForm' => array($config_id))),
+ ),
+ ),
+ )
+ );
+
+ $form['config_id'] = array('#type' => 'value', '#value' => $config_id);
+
+ // TODO: really make this a *live* preview, i.e. refresh the preview on each
+ // change in the form. This cannot be done easily in Drupal 5 or 6, so let's
+ // do so in Drupal 7. See cfg.livePreview in common_config_form.js.
+ $form['live_preview'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Preview'),
+ '#description' => t('This is what the Hierarchical Select will look like with your current configuration.'),
+ '#collapsible' => FALSE,
+ '#attributes' => array('class' => array('live-preview')),
+ );
+ $form['live_preview']['example'] = array(
+ '#type' => 'hierarchical_select',
+ '#required' => $preview_is_required,
+ '#title' => t('Preview'),
+ '#description' => t('The description.'),
+ // Skip al validation for this form element: the data collected through it
+ // is always discarded, it's merely here for illustrative purposes.
+ '#validated' => TRUE,
+ );
+ hierarchical_select_common_config_apply($form['live_preview']['example'], $config_id, array_merge($defaults_override, array('module' => $module, 'params' => $params)));
+
+ $form['save_lineage'] = array(
+ '#type' => 'radios',
+ '#title' => t('Save lineage'),
+ '#options' => array(
+ 1 => t('Save !item lineage', $args),
+ 0 => t('Save only the deepest !item', $args),
+ ),
+ '#default_value' => (isset($config['save_lineage'])) ? $config['save_lineage'] : NULL,
+ '#description' => t(
+ 'Saving the !item lineage means saving the the !item itself and all
+ its ancestors.',
+ $args
+ ),
+ );
+
+ $form['enforce_deepest'] = array(
+ '#type' => 'radios',
+ '#title' => t('Level choice'),
+ '#options' => array(
+ 1 => t('Force the user to choose a !item from a deepest level', $args),
+ 0 => t('Allow the user to choose a !item from any level', $args),
+ ),
+ '#default_value' => (isset($config['enforce_deepest'])) ? $config['enforce_deepest'] : NULL,
+ '#description' => t(
+ 'This setting determines from which level in the !hierarchy tree a
+ user can select a !item.',
+ $args
+ ),
+ '#attributes' => array('class' => array('enforce-deepest')),
+ );
+
+ $form['resizable'] = array(
+ '#type' => 'radios',
+ '#title' => t('Resizable'),
+ '#description' => t(
+ "When enabled, a handle appears below the Hierarchical Select to allow
+ the user to dynamically resize it. Double clicking will toggle between
+ the smallest and a sane 'big size'."
+ ),
+ '#options' => array(
+ 0 => t('Disabled'),
+ 1 => t('Enabled'),
+ ),
+ '#default_value' => (isset($config['resizable'])) ? $config['resizable'] : NULL,
+ '#attributes' => array('class' => array('resizable')),
+ );
+
+ $form['level_labels'] = array(
+ '#tree' => TRUE,
+ '#type' => 'fieldset',
+ '#title' => t('Level labels'),
+ '#description' => t(
+ 'When the user is allowed to choose a !item from any level in the
+ Level choice setting, you can enter a label for each
+ level.
+ However, when the user is only allowed to choose a !item from the
+ deepest level, then you can only enter a label for the root
+ level.',
+ $args
+ ),
+ '#collapsible' => TRUE,
+ );
+ $form['level_labels']['status'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable level labels'),
+ '#default_value' => (isset($config['level_labels']['status'])) ? $config['level_labels']['status'] : NULL,
+ '#attributes' => array('class' => array('level-labels-status')),
+ );
+ for ($depth = 0; $depth <= $max_hierarchy_depth; $depth++) {
+ $form['level_labels']['labels'][$depth] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#maxlength' => 255,
+ '#default_value' => (isset($config['level_labels']['labels'][$depth])) ? $config['level_labels']['labels'][$depth] : NULL,
+ '#attributes' => array('class' => array('level-label')),
+ );
+ }
+ $form['level_labels']['#theme'] = 'hierarchical_select_common_config_form_level_labels';
+ $form['level_labels']['#strings'] = $strings;
+
+ $form['dropbox'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Dropbox settings'),
+ '#description' => t('The dropbox allows the user to make multiple selections.'),
+ '#collapsible' => TRUE,
+ );
+ $form['dropbox']['status'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable the dropbox'),
+ '#default_value' => (isset($config['dropbox']['status'])) ? $config['dropbox']['status'] : NULL,
+ '#attributes' => array('class' => array('dropbox-status')),
+ );
+ $form['dropbox']['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#description' => t('The title you enter here appears above the dropbox.'),
+ '#size' => 20,
+ '#maxlength' => 255,
+ '#default_value' => (isset($config['dropbox']['title'])) ? $config['dropbox']['title'] : NULL,
+ '#attributes' => array('class' => array('dropbox-title')),
+ );
+ $form['dropbox']['limit'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Limit the number of selections'),
+ '#description' => t(
+ 'Limits the number of selections that can be added to the dropbox.
+ 0 means no limit.
+
+ Note: the "Save !item lineage" option has no effect on this, even if
+ a lineage consists of 3 !items, this will count as only one selection
+ in the dropbox.',
+ $args
+ ),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#default_value' => (isset($config['dropbox']['limit'])) ? $config['dropbox']['limit'] : NULL,
+ '#attributes' => array('class' => array('dropbox-limit')),
+ );
+ $form['dropbox']['reset_hs'] = array(
+ '#type' => 'radios',
+ '#title' => t('Reset selection of hierarchical select'),
+ '#description' => t(
+ 'This setting determines what will happen to the hierarchical select
+ when the user has added a selection to the dropbox.'
+ ),
+ '#options' => array(
+ 0 => t('Disabled'),
+ 1 => t('Enabled'),
+ ),
+ '#default_value' => (isset($config['dropbox']['reset_hs'])) ? $config['dropbox']['reset_hs'] : NULL,
+ '#attributes' => array('class' => array('dropbox-reset-hs')),
+ );
+ $form['dropbox']['sort'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Sort dropbox items'),
+ '#description' => t('Automatically sort items added to the dropbox. If unchecked new items will be added to the end of the dropbox list.'),
+ '#default_value' => (isset($config['dropbox']['sort'])) ? $config['dropbox']['sort'] : 1,
+ '#attributes' => array('class' => array('dropbox-sort')),
+ );
+ if (module_hook($module, 'hierarchical_select_create_item')) {
+ $form['editability'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Editability settings'),
+ '#description' => t(
+ 'You can allow the user to add new !items to this
+ !hierarchythrough Hierarchical Select.',
+ $args
+ ),
+ '#collapsible' => TRUE,
+ );
+ $form['editability']['status'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Allow creation of new !items', $args),
+ '#options' => array(
+ 0 => t('Disabled'),
+ 1 => t('Enabled'),
+ ),
+ '#default_value' => (isset($config['editability']['status'])) ? $config['editability']['status'] : NULL,
+ '#attributes' => array('class' => array('editability-status')),
+ );
+ for ($depth = 0; $depth <= $max_hierarchy_depth; $depth++) {
+ $form['editability']['item_types'][$depth] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#maxlength' => 255,
+ '#default_value' => (isset($config['editability']['item_types'][$depth])) ? $config['editability']['item_types'][$depth] : NULL,
+ '#attributes' => array('class' => array('editability-item-type')),
+ );
+ }
+ for ($depth = 0; $depth <= $max_hierarchy_depth; $depth++) {
+ $form['editability']['allowed_levels'][$depth] = array(
+ '#type' => 'checkbox',
+ '#default_value' => (isset($config['editability']['allowed_levels'][$depth])) ? $config['editability']['allowed_levels'][$depth] : 1,
+ );
+ }
+ $form['editability']['allow_new_levels'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Allow creation of new levels'),
+ '#default_value' => $config['editability']['allow_new_levels'],
+ '#description' => t(
+ 'Allow the user to create child !items for !items that do not yet have
+ children.',
+ $args
+ ),
+ '#attributes' => array('class' => array('editability-allow-new-levels')),
+ );
+ $form['editability']['max_levels'] = array(
+ '#type' => 'select',
+ '#title' => t('Maximum number of levels allowed'),
+ '#options' => array(
+ 0 => t('0 (no limit)'), 1, 2, 3, 4, 5, 6, 7, 8, 9
+ ),
+ '#default_value' => (isset($config['editability']['max_levels'])) ? $config['editability']['max_levels'] : NULL,
+ '#description' => t(
+ 'When the user is allowed to create new levels, this option prevents
+ the user from creating extremely deep !hierarchies.',
+ $args
+ ),
+ '#attributes' => array('class' => array('editability-max-levels')),
+ );
+
+ $form['editability']['#theme'] = 'hierarchical_select_common_config_form_editability';
+ $form['editability']['#strings'] = $strings;
+ }
+
+ if (module_hook($module, 'hierarchical_select_entity_count')) {
+ $form['entity_count'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Entity Count'),
+ '#collapsible' => TRUE,
+ );
+
+ $form['entity_count']['enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display number of entities'),
+ '#description' => t('Display the number of entities associated with the !item. Do not forget to check which entities should be counted.', $args),
+ '#default_value' => isset($config['entity_count']['enabled']) ? $config['entity_count']['enabled'] : FALSE,
+ '#weight' => -1,
+ '#attributes' => array('class' => array('entity-count-enabled')),
+ );
+
+ $form['entity_count']['require_entity'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require associated entity'),
+ '#description' => t('If checked only !items that have at least one entity associated with them will be displayed.', $args),
+ '#default_value' => (isset($config['entity_count']['require_entity'])) ? $config['entity_count']['require_entity'] : FALSE,
+ );
+
+ $form['entity_count']['settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Entity count settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#weight' => -1,
+ '#attributes' => array('class' => array('entity-count-settings')),
+ );
+
+ $form['entity_count']['settings']['count_children'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Also count children of !item.', $args),
+ '#description' => t('If checked this will result in a larger number because the children will be counted also.'),
+ '#default_value' => isset($config['entity_count']['settings']['count_children']) ? $config['entity_count']['settings']['count_children'] : FALSE,
+ );
+
+ $form['entity_count']['settings']['entity_types'] = array(
+ '#type' => 'item',
+ '#title' => t('Select entities that should be counted.'),
+ '#description' => t('Select entity type or one of it\'s bundles that should be counted'),
+ );
+
+ $entity_info = entity_get_info();
+ foreach ($entity_info as $entity => $entity_info) {
+ if (!empty($entity_info['bundles']) && $entity_info['fieldable'] === TRUE) {
+ $options = array();
+ $default_values = array();
+
+ $form['entity_count']['settings']['entity_types'][$entity] = array(
+ '#type' => 'fieldset',
+ '#title' => check_plain($entity_info['label']),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+
+ foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
+ $options[$bundle] = check_plain($bundle_info['label']);
+ $default_values[$entity][$bundle] = isset($config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle]) ? $config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle] : 0;
+ }
+
+ $form['entity_count']['settings']['entity_types'][$entity]['count_' . $entity] = array(
+ '#type' => 'checkboxes',
+ '#options' => $options,
+ '#default_value' => $default_values[$entity],
+ );
+ }
+ }
+ }
+
+ return $form;
+}
+
+/**
+ * Submit callback for the hierarchical_select_common_config_form form.
+ */
+function hierarchical_select_common_config_form_submit($form, &$form_state) {
+ $config = _hierarchical_select_get_form_item_by_parents($form_state['values'], $form['#hs_common_config_form_parents']);
+
+ // Don't include the value of the live preview in the config.
+ unset($config['live_preview']);
+
+ hierarchical_select_common_config_set($config['config_id'], $config);
+}
+
+/**
+ * Get the form element of a form that has a certain lineage of parents.
+ *
+ * @param $form
+ * A structured array for use in the Forms API.
+ * @param $parents
+ * An array of parent form element names.
+ * @return
+ * The form element that has the specified lineage of parents.
+ */
+function _hierarchical_select_get_form_item_by_parents($form, $parents) {
+ if (count($parents)) {
+ $parent = array_shift($parents);
+ return _hierarchical_select_get_form_item_by_parents($form[$parent], $parents);
+ }
+ else {
+ return $form;
+ }
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/includes/common_config_form.css b/sites/all/modules/contrib/fields/hierarchical_select/includes/common_config_form.css
new file mode 100644
index 00000000..d72c2771
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/includes/common_config_form.css
@@ -0,0 +1,11 @@
+
+.hierarchical-select-config-form .live-preview {
+ margin-left: auto;
+ margin-right: auto;
+ width: 25em;
+ float: right;
+}
+
+.hierarchical-select-config-form fieldset {
+ clear: right;
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/includes/common_config_form.js b/sites/all/modules/contrib/fields/hierarchical_select/includes/common_config_form.js
new file mode 100644
index 00000000..36233a6c
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/includes/common_config_form.js
@@ -0,0 +1,148 @@
+
+Drupal.HierarchicalSelectConfigForm = {};
+
+(function ($, cfg) {
+
+cfg.context = function(configId) {
+ if (configId === undefined) {
+ return $('.hierarchical-select-config-form > *').not('.live-preview');
+ }
+ else {
+ return $('#hierarchical-select-config-form-'+ configId + ' > *').not('.live-preview');
+ }
+};
+
+cfg.levelLabels = function(configId) {
+ var $status = $('.level-labels-status', cfg.context(configId));
+ var $enforceDeepest = $('.enforce-deepest input', cfg.context(configId));
+
+ var showHide = function(speed) {
+ $affected = $('.level-labels-settings', cfg.context(configId));
+ if (!$status.is(':checked')) {
+ $affected.hide(speed);
+ }
+ else {
+ // For showing/hiding rows, I'm relying on setting the style
+ // "display: none" and removing it again. jQuery's show()/hide() leave
+ // "display: block" behind and are thereby messing up the table layout.
+ if ($enforceDeepest.slice(1, 2).is(':checked')) {
+ $affected.find('tr').removeAttr('style');
+ }
+ else {
+ // We need to take special measures if sticky headers are enabled, so
+ // handle the show/hide separately when it's enabled.
+ if ($affected.find('table.sticky-header').length == 0) {
+ $affected.find('tr').slice(0, 2).removeAttr('style'); // Show header tr and root level tr.
+ $affected.find('tr').slice(2).attr('style', 'display: none'); // Hide all other tr's.
+ }
+ else {
+ $affected.find('table').show(speed); // Show both tables (the one with the sticky headers and the one with the actual content).
+ $affected.find('table').slice(1).find('tr').slice(2).attr('style', 'display: none'); // Show all tr's after the header tr and root level tr of the 2nd table (the one with the actual content).
+ }
+ }
+
+ // If $status was unchecked previously, the entire div would have been
+ // hidden!
+ if ($affected.css('display') == 'none') {
+ $affected.show(speed);
+ }
+ }
+ };
+
+ $status.click(function() { showHide(200); });
+ $enforceDeepest.click(function() { showHide(200); });
+ showHide(0);
+};
+
+cfg.dropbox = function(configId) {
+ var $status = $('.dropbox-status', cfg.context(configId));
+
+ var showHide = function(speed) {
+ var $affected = $('.dropbox-title, .dropbox-limit, .dropbox-reset-hs', cfg.context(configId)).parent();
+ if ($status.is(':checked')) {
+ $affected.show(speed);
+ }
+ else {
+ $affected.hide(speed);
+ }
+ };
+
+ $status.click(function() { showHide(200); });
+ showHide(0);
+};
+
+cfg.editability = function(configId) {
+ var $status = $('.editability-status', cfg.context(configId));
+ var $allowNewLevels = $('.editability-allow-new-levels', cfg.context(configId));
+
+ var showHide = function(speed) {
+ var $affected = $('.editability-per-level-settings, .form-item:has(.editability-allow-new-levels)', cfg.context(configId));
+ var $maxLevels = $('.form-item:has(.editability-max-levels)', cfg.context(configId));
+ if ($status.is(':checked')) {
+ if ($allowNewLevels.is(':checked')) {
+ $affected.add($maxLevels).show(speed);
+ }
+ else {
+ $affected.show(speed);
+ }
+ }
+ else {
+ $affected.add($maxLevels).hide(speed);
+ }
+ };
+
+ var showHideMaxLevels = function(speed) {
+ $affected = $('.editability-max-levels', cfg.context(configId)).parent();
+ if ($allowNewLevels.is(':checked')) {
+ $affected.show(speed);
+ }
+ else {
+ $affected.hide(speed);
+ }
+ };
+
+ $status.click(function() { showHide(200); });
+ $allowNewLevels.click(function() { showHideMaxLevels(200); });
+ showHideMaxLevels(0);
+ showHide(0);
+};
+
+cfg.entityCount = function(configId) {
+ var $status = $('.entity-count-enabled', cfg.context(configId));
+
+ var showHide = function(speed) {
+ var $affected = $('.entity-count-settings', cfg.context(configId));
+ if ($status.is(':checked')) {
+ $affected.show(speed);
+ }
+ else {
+ $affected.hide(speed);
+ }
+ };
+
+ $status.click(function() { showHide(200); });
+ showHide(0);
+};
+
+cfg.livePreview = function(configId) {
+ // React on changes to any input, except the ones in the live preview.
+ $updateLivePreview = $('input', cfg.context(configId))
+ .filter(':not(.create-new-item-input):not(.create-new-item-create):not(.create-new-item-cancel)')
+ .change(function() {
+ // TODO: Do an AJAX submit of the entire form.
+ });
+};
+
+$(document).ready(function() {
+ for (var id in Drupal.settings.HierarchicalSelect.configForm) {
+ var configId = Drupal.settings.HierarchicalSelect.configForm.id;
+
+ cfg.levelLabels(configId);
+ cfg.dropbox(configId);
+ cfg.editability(configId);
+ cfg.entityCount(configId);
+ //cfg.livePreview(configId);
+ }
+});
+
+})(jQuery, Drupal.HierarchicalSelectConfigForm);
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/includes/theme.inc b/sites/all/modules/contrib/fields/hierarchical_select/includes/theme.inc
new file mode 100644
index 00000000..7c376679
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/includes/theme.inc
@@ -0,0 +1,413 @@
+\n";
+ $required = !empty($element['#required']) ? '*' : '';
+
+ if (!empty($element['#title'])) {
+ $title = $element['#title'];
+ if (!empty($element['#id'])) {
+ $output .= ' \n";
+ }
+ else {
+ $output .= ' \n";
+ }
+ }
+
+ $output .= " $value\n";
+
+ if (!empty($element['#description'])) {
+ $output .= '
' . $element['#description'] . "
\n";
+ }
+
+ $output .= "
\n";
+
+ return $output;
+}
+
+/**
+ * Format a hierarchical select.
+ *
+ * @param array $variables
+ * An associative array containing the properties of the element.
+ * @return string
+ * A themed HTML string representing the form element.
+ */
+function theme_hierarchical_select($variables) {
+ $element = $variables['element'];
+ $output = '';
+
+ // Update $element['#attributes']['class'].
+ if (!isset($element['#attributes']['class'])) {
+ $element['#attributes']['class'] = array();
+ }
+ $hsid = $element['hsid']['#value'];
+ $level_labels_style = variable_get('hierarchical_select_level_labels_style', 'none');
+ $classes = array(
+ 'hierarchical-select-wrapper',
+ "hierarchical-select-level-labels-style-$level_labels_style",
+ // Classes that make it possible to override the styling of specific
+ // instances of Hierarchical Select, based on either the ID of the form
+ // element or the config that it uses.
+ 'hierarchical-select-wrapper-for-name-' . $element['#id'],
+ (isset($element['#config']['config_id'])) ? 'hierarchical-select-wrapper-for-config-' . $element['#config']['config_id'] : NULL,
+ );
+ $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $classes);
+ $element['#attributes']['id'] = "hierarchical-select-$hsid-wrapper";
+ $element['#id'] = "hierarchical-select-$hsid-wrapper"; // This ensures the label's for attribute is correct.
+
+ return '
' . drupal_render_children($element) . '
';
+}
+
+/**
+ * Format the container for all selects in the hierarchical select.
+ *
+ * @param array $variables
+ * An associative array containing the properties of the element.
+ * @return string
+ * A themed HTML string representing the form element.
+ */
+function theme_hierarchical_select_selects_container($variables) {
+ $element = $variables['element'];
+ $output = '';
+ $output .= '
';
+ return $output;
+}
+
+/**
+ * Format a select in the .hierarchial-select div: prevent it from being
+ * wrapped in a div. This simplifies the CSS and JS code.
+ *
+ * @param array $variables
+ * An associative array containing the properties of the element.
+ * @return string
+ * A themed HTML string representing the form element.
+ */
+function theme_hierarchical_select_select($variables) {
+ $element = $variables['element'];
+ element_set_attributes($element, array('id', 'name', 'size'));
+ _form_set_class($element, array('form-select'));
+
+ return '';
+}
+
+/**
+ * Format an item separator (for use in a lineage).
+ */
+function theme_hierarchical_select_item_separator($variables) {
+ $output = '';
+ $output .= '';
+ $output .= '›';
+ $output .= '';
+ return $output;
+}
+
+/**
+ * Format a special option in a Hierarchical Select select. For example the
+ * "none" option or the "create new item" option. This theme function allows
+ * you to change how a special option is indicated textually.
+ *
+ * @param array $variables
+ * A special option.
+ * @return string
+ * A textually indicated special option.
+ */
+function theme_hierarchical_select_special_option($variables) {
+ $option = $variables['option'];
+ return '<' . $option . '>';
+}
+
+/**
+ * Forms API theming callback for the dropbox. Renders the dropbox as a table.
+ *
+ * @param array $variables
+ * An element for which the #theme property was set to this function.
+ * @return string
+ * A themed HTML string.
+ */
+function theme_hierarchical_select_dropbox_table($variables) {
+ $element = $variables['element'];
+ $output = '';
+
+ $class = 'dropbox';
+ if (form_get_error($element) === '') {
+ $class .= ' error';
+ }
+
+ $title = $element['title']['#value'];
+ $separator = $element['separator']['#value'];
+ $is_empty = $element['is_empty']['#value'];
+
+ $separator_html = '' . $separator . '';
+
+ $output .= '
';
+ $output .= '
';
+ $output .= '
' . $title . '
';
+ $output .= '';
+
+ if (!$is_empty) {
+ // Each lineage in the dropbox corresponds to an entry in the dropbox table.
+ $lineage_count = count(element_children($element['lineages']));
+ for ($x = 0; $x < $lineage_count; $x++) {
+ $db_entry = $element['lineages']["lineage-$x"];
+ $zebra = $db_entry['#zebra'];
+ $first = $db_entry['#first'];
+ $last = $db_entry['#last'];
+ // The deepest level is the number of child levels minus one. This "one"
+ // is the element for the "Remove" checkbox.
+ $deepest_level = count(element_children($db_entry)) - 1;
+
+ $output .= '
';
+ $output .= '
';
+ // Each item in a lineage is separated by the separator string.
+ for ($depth = 0; $depth < $deepest_level; $depth++) {
+ $output .= drupal_render($db_entry[$depth]);
+
+ if ($depth < $deepest_level - 1) {
+ $output .= $separator_html;
+ }
+ }
+ $output .= '
';
+ $output .= '
' . drupal_render($db_entry['remove']) . '
';
+ $output .= '
';
+ }
+ }
+ else {
+ $output .= '
';
+ $output .= t('Nothing has been selected.');
+ $output .= '
';
+ }
+
+ $output .= '';
+ $output .= '
';
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Themeing function to render the level_labels settings as a table.
+ */
+// TODO: rename $form to $element for consistency (and update hook_theme() after that), make the comment consistent.
+/**
+ * @todo Please document this function.
+ * @see http://drupal.org/node/1354
+ */
+function theme_hierarchical_select_common_config_form_level_labels($variables) {
+ $form = $variables['form'];
+ // Recover the stored strings.
+ $strings = $form['#strings'];
+
+ $output = '';
+ $header = array(t('Level'), t('Label'));
+ $rows = array();
+
+ $output .= drupal_render($form['status']);
+
+ $output .= '
';
+ $output .= t(
+ 'The %item_type you enter for each level is what will be used in
+ each level to replace a "<create new item>" option with a
+ "<create new %item_type>" option, which is often more
+ intuitive.',
+ array(
+ '%item_type' => $strings['item_type'],
+ )
+ );
+ $output .= '
';
+ }
+ else {
+ // No levels exist yet in the hierarchy!
+ $output .= '
';
+ $output .= t('There are no levels yet in this !hierarchy!', array('!hierarchy' => $strings['hierarchy']));
+ $output .= '
';
+ }
+ $output .= '
';
+
+ // Render the remaining form items.
+ $output .= drupal_render_children($form);
+
+ return $output;
+}
+
+/**
+ * Themeing function to render a selection (of items) according to a given
+ * Hierarchical Select configuration as one or more lineages.
+ *
+ * @param $selection
+ * A selection of items of a hierarchy.
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - save_lineage
+ * - params
+ */
+function theme_hierarchical_select_selection_as_lineages($variables) {
+ $selection = $variables['selection'];
+ $config = $variables['config'];
+ $output = '';
+
+ $selection = (!is_array($selection)) ? array($selection) : $selection;
+
+ // Generate a dropbox out of the selection. This will automatically
+ // calculate all lineages for us.
+ $selection = array_keys($selection);
+ $dropbox = _hierarchical_select_dropbox_generate($config, $selection);
+
+ // Actual formatting.
+ foreach ($dropbox->lineages as $id => $lineage) {
+ if ($id > 0) {
+ $output .= ' ';
+ }
+
+ $items = array();
+ foreach ($lineage as $level => $item) {
+ $items[] = $item['label'];
+ }
+ $output .= implode('›', $items);
+ }
+
+ // Add the CSS.
+ drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css');
+
+ return $output;
+}
+
+/**
+ * @} End of "ingroup themeable".
+ */
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * This is an altered clone of form_select_options(). The reason: I need to be
+ * able to set a class on an option element if it contains a level label, to
+ * allow for level label styles.
+ * TODO: rename to _hierarchical_select_select_options().
+ */
+function _hierarchical_select_options($element) {
+ if (!isset($choices)) {
+ $choices = $element['#options'];
+ }
+ // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
+ // isset() fails in this situation.
+ $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
+ $value_is_array = isset($element['#value']) && is_array($element['#value']);
+ $options = '';
+ foreach ($choices as $key => $choice) {
+ $key = (string) $key;
+ if ($value_valid && (!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value'])))) {
+ $selected = ' selected="selected"';
+ }
+ else {
+ $selected = '';
+ }
+
+ // If an option DOES NOT have child info, then it's a special option:
+ // - label_\d+ (level label)
+ // - none ("")
+ // - create_new_item ("")
+ // Only when it's a level label, we have to add a class to this option.
+ if (!isset($element['#childinfo'][$key])) {
+ $class = (preg_match('/label_\d+/', $key)) ? ' level-label' : '';
+ }
+ else {
+ $class = ($element['#childinfo'][$key] == 0) ? 'has-no-children' : 'has-children';
+ }
+
+ $options .= '';
+ }
+ return $options;
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/includes/views.js b/sites/all/modules/contrib/fields/hierarchical_select/includes/views.js
new file mode 100644
index 00000000..d5b5e3eb
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/includes/views.js
@@ -0,0 +1,29 @@
+
+
+/**
+ * @file
+ * Make Hierarchical Select work in Views' exposed filters form.
+ *
+ * Views' exposed filters form is a GET form, but since Hierarchical Select
+ * really is a combination of various form items, this will result in a very
+ * ugly and unnecessarily long GET URL, which also breaks the exposed filters.
+ * This piece of JavaScript is a necessity to make it work again, but it will
+ * of course only work when JavaScript is enabled!
+ */
+
+
+if (Drupal.jsEnabled) {
+ $(document).ready(function(){
+ $('.view-filters form').submit(function() {
+ // Remove the Hierarchical Select form build id and the form id, to
+ // prevent them from ending up in the GET URL.
+ $('#edit-hs-form-build-id').remove();
+
+ // Prepare the hierarchical select form elements that are used as
+ // exposed filters for a GET submit.
+ $('.view-filters form')
+ .find('.hierarchical-select-wrapper')
+ .trigger('prepare-GET-submit');
+ });
+ });
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_flatlist.info b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_flatlist.info
new file mode 100644
index 00000000..c1eeb5a1
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_flatlist.info
@@ -0,0 +1,12 @@
+name = Hierarchical Select Flat List
+description = Allows Hierarchical Select's dropbox to be used for selecting multiple items in a flat list of options.
+dependencies[] = hierarchical_select
+package = Form Elements
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2017-02-15
+version = "7.x-3.0-beta8"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1487167708"
+
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_flatlist.module b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_flatlist.module
new file mode 100644
index 00000000..d2fa4de6
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_flatlist.module
@@ -0,0 +1,66 @@
+ t('None: flat list'),
+ 'entity type' => t('N/A'),
+ );
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.info b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.info
new file mode 100644
index 00000000..5eb6432f
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.info
@@ -0,0 +1,13 @@
+name = Hierarchical Select Menu
+description = Use Hierarchical Select for menu parent selection.
+dependencies[] = hierarchical_select
+dependencies[] = menu
+package = Form Elements
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2017-02-15
+version = "7.x-3.0-beta8"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1487167708"
+
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.install b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.install
new file mode 100644
index 00000000..3b4deb5a
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.install
@@ -0,0 +1,27 @@
+fields(array('weight' => 1))
+ ->condition('name', 'hs_menu')
+ ->execute();
+}
+
+/**
+ * Implementats hook_uninstall().
+ */
+function hs_menu_uninstall() {
+ db_delete('variable')
+ ->condition('name', 'hs_menu_%', 'LIKE')
+ ->execute();
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.module b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.module
new file mode 100644
index 00000000..b5893a1c
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_menu.module
@@ -0,0 +1,326 @@
+ 'Menu',
+ 'description' => 'Hierarchical Select configuration for Menu',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('hs_menu_admin_settings'),
+ 'type' => MENU_LOCAL_TASK,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the node form's menu form.
+ */
+function hs_menu_form_node_form_alter(&$form, &$form_state) {
+ $active_types = array_filter(variable_get('hs_menu_content_types', array()));
+ $active = empty($active_types) || in_array($form_state['node']->type, $active_types);
+ if ($active && isset($form['menu']['link']['parent']) && isset($form['menu']['#access']) && $form['menu']['#access']) {
+ unset($form['menu']['link']['parent']['#options']);
+ $form['menu']['link']['parent']['#type'] = 'hierarchical_select';
+ // Get menu name, needed to exclude current node.
+ $menu_name = explode(':', $form['menu']['link']['parent']['#default_value']);
+ _hs_menu_apply_config($form['menu']['link']['parent'], array(
+ 0 => $menu_name[0],
+ 1 => $form['menu']['link']['mlid']['#value'],
+ 'type' => $form['type']['#value'],
+ ));
+
+ // Set custom submit callback.
+ array_unshift($form['#submit'], 'hs_menu_node_form_submit');
+ // Change the loaded default value into an array so we can populate the
+ // Hierarchical Select element.
+ $form['menu']['link']['parent']['#default_value'] = array($form['menu']['link']['parent']['#default_value']);
+ }
+}
+
+/**
+ * Implements hook_form_BASE_FORMID_alter().
+ *
+ * Alter the widget type form; dynamically add the Hierarchical Select
+ * Configuration form when it is needed.
+ */
+function hs_menu_form_menu_edit_item_alter(&$form, &$form_state) {
+ unset($form['parent']['#options']);
+ $original_item = $form['original_item']['#value'];
+ $form['parent']['#type'] = 'hierarchical_select';
+ _hs_menu_apply_config($form['parent'], array('exclude' => array(
+ $original_item['menu_name'],
+ $original_item['mlid'],
+ )));
+
+ // Set custom submit callback.
+ array_unshift($form['#submit'], 'hs_menu_menu_edit_item_form_submit');
+}
+
+
+//----------------------------------------------------------------------------
+// Form API callbacks.
+
+/**
+ * Submit callback; menu edit item form.
+ */
+function hs_menu_menu_edit_item_form_submit(&$form, &$form_state) {
+ // Don't return an array, but a single item.
+ $form_state['values']['parent'] = $form_state['values']['parent'][0];
+}
+
+/**
+ * Submit callback; node edit form.
+ */
+function hs_menu_node_form_submit(&$form, &$form_state) {
+ // Don't return an array, but a single item.
+ $form_state['values']['menu']['parent'] = $form_state['values']['menu']['parent'][0];
+}
+
+//----------------------------------------------------------------------------
+// Menu callbacks.
+
+/**
+ * Form definition; admin settings.
+ */
+function hs_menu_admin_settings() {
+ $form['hs_menu_resizable'] = array(
+ '#type' => 'radios',
+ '#title' => t('Resizable'),
+ '#description' => t(
+ "When enabled, a handle appears below the Hierarchical Select to allow
+ the user to dynamically resize it. Double clicking will toggle between
+ the smallest and a sane 'big size'."
+ ),
+ '#options' => array(
+ 0 => t('Disabled'),
+ 1 => t('Enabled'),
+ ),
+ '#default_value' => variable_get('hs_menu_resizable', 1),
+ );
+
+ $form['hs_menu_content_types'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Content types'),
+ '#description' => t("Select the content types to use Hierarchical Select Menu on. If no content types are selected, then it will apply to all content types."),
+ '#options' => node_type_get_names(),
+ '#default_value' => variable_get('hs_menu_content_types', array()),
+ );
+
+ return system_settings_form($form);
+}
+
+
+//----------------------------------------------------------------------------
+// Hierarchical Select hooks.
+
+/**
+ * Implements hook_hierarchical_select_params().
+ */
+function hs_menu_hierarchical_select_params() {
+ $params = array(
+ 'exclude', // The menu_name and mlid (in an array) of a menu link that should be excluded from the hierarchy.
+ );
+ return $params;
+}
+
+/**
+ * Implements hook_hierarchical_select_root_level().
+ */
+function hs_menu_hierarchical_select_root_level($params) {
+ $menus = array();
+
+ $result = db_query("SELECT menu_name, title FROM {menu_custom} ORDER BY title");
+ // If the type is set, respect the core menu options setting.
+ if (isset($params['type'])) {
+ $type_menus = variable_get('menu_options_' . $params['type'], array('main-menu' => 'main-menu'));
+ while ($menu = $result->fetchObject()) {
+ if (in_array($menu->menu_name, $type_menus)) {
+ $menus[$menu->menu_name . ':0'] = $menu->title;
+ }
+ }
+ }
+ // Fall back to the legacy approach, show all menu's.
+ else {
+ while ($menu = $result->fetchObject()) {
+ $menus[$menu->menu_name . ':0'] = $menu->title;
+ }
+ }
+
+ return $menus;
+}
+
+/**
+ * Implements hook_hierarchical_select_children().
+ */
+function hs_menu_hierarchical_select_children($parent, $params) {
+ $children = array();
+ list($menu_name, $plid) = explode(':', $parent);
+ $tree = menu_tree_all_data($menu_name, NULL);
+ return _hs_menu_children($tree, $menu_name, $plid, $params['exclude']);
+}
+
+/**
+ * Implements hook_hierarchical_select_lineage().
+ */
+function hs_menu_hierarchical_select_lineage($item, $params) {
+ $lineage = array($item);
+
+ list($menu_name, $mlid) = explode(':', $item);
+
+ // If the initial mlid is zero, then this is the root level, so we don't
+ // have to get the lineage.
+ if ($mlid > 0) {
+ // Prepend each parent mlid (i.e. plid) to the lineage.
+ do {
+ $plid = db_query("SELECT plid FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchField();
+ array_unshift($lineage, "$menu_name:$plid");
+ if ($mlid == $plid) {
+ // Somehow we have an infinite loop situation. Bail out of the loop.
+ break;
+ }
+ $mlid = $plid;
+ } while ($plid > 0);
+ }
+
+ return $lineage;
+}
+
+/**
+ * Implements hook_hierarchical_select_valid_item().
+ */
+function hs_menu_hierarchical_select_valid_item($item, $params) {
+ $parts = explode(':', $item);
+
+ $valid = TRUE;
+
+ // Validate menu name.
+ $valid = (array_key_exists($parts[0], menu_get_menus()));
+
+ // Validate hierarchy of mlids.
+ for ($i = 1; $valid && $i < count($parts); $i++) {
+ $valid = $valid && is_numeric($parts[$i]);
+ }
+
+ // Ensure that this isn't the excluded menu link.
+ $valid = $valid && $item != $params['exclude'][0] . $params['exclude'][1];
+
+ return $valid;
+}
+
+/**
+ * Implements hook_hierarchical_select_item_get_label().
+ */
+function hs_menu_hierarchical_select_item_get_label($item, $params) {
+ static $labels = array();
+
+ $parts = explode(':', $item);
+ if (count($parts) == 1) { // Get the menu name.
+ $menu_name = $parts[0];
+ $labels[$item] = db_query("SELECT title FROM {menu_custom} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField();
+ }
+ else { // Get the menu link title.
+ $mlid = end($parts);
+ $menu_link = menu_link_load($mlid);
+ $labels[$item] = $menu_link['title'];
+ }
+
+ return $labels[$item];
+}
+
+/**
+ * Implements hook_hierarchical_select_implementation_info().
+ */
+function hs_menu_hierarchical_select_implementation_info() {
+ return array(
+ 'hierarchy type' => t('Menu'),
+ 'entity type' => t('N/A'),
+ );
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Recursive helper function for hs_menu_hierarchical_select_children().
+ */
+function _hs_menu_children($tree, $menu_name, $plid = 0, $exclude = FALSE) {
+ $children = array();
+
+ foreach ($tree as $data) {
+ if ($data['link']['plid'] == $plid && $data['link']['hidden'] >= 0) {
+ if ($exclude && $data['link']['menu_name'] === $exclude[0] && $data['link']['mlid'] == $exclude[1]) {
+ continue;
+ }
+
+ $title = truncate_utf8($data['link']['title'], 30, TRUE, FALSE);
+ if ($data['link']['hidden']) {
+ $title .= ' (' . t('disabled') . ')';
+ }
+ $children[$menu_name . ':' . $data['link']['mlid']] = $title;
+ if ($data['below']) {
+ $children += _hs_menu_children($data['below'], $menu_name, $plid, $exclude);
+ }
+ }
+ elseif ($data['below']) {
+ $children += _hs_menu_children($data['below'], $menu_name, $plid, $exclude);
+ }
+ }
+
+ return $children;
+}
+
+/**
+ * Helper function to apply the HS config to a form item.
+ */
+function _hs_menu_apply_config(&$form, $params) {
+ // The following is to ensure via javascript self is not listed.
+ if (!empty($params['exclude'])) {
+ $params['exclude'] = $params['exclude'][0] .':'. $params['exclude'][1];
+ drupal_add_js('jQuery(document).ready(function () {
+ jQuery("[value*=\"' . $params['exclude'] . '\"]").hide();
+ });', 'inline');
+ }
+ $form['#config'] = array(
+ 'module' => 'hs_menu',
+ 'params' => array(
+ 'exclude' => isset($params['exclude']) ? $params['exclude'] : NULL,
+ 'type' => isset($params['type']) ? $params['type'] : NULL,
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => variable_get('hs_menu_resizable', 1),
+ 'level_labels' => array(
+ 'status' => 0,
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'render_flat_select' => 0,
+ );
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_smallhierarchy.info b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_smallhierarchy.info
new file mode 100644
index 00000000..7ec8105d
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_smallhierarchy.info
@@ -0,0 +1,12 @@
+name = Hierarchical Select Small Hierarchy
+description = Allows Hierarchical Select to be used for a hardcoded hierarchy. When it becomes to slow, you should move the hierarchy into the database and write a proper implementation.
+dependencies[] = hierarchical_select
+package = Form Elements
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2017-02-15
+version = "7.x-3.0-beta8"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1487167708"
+
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_smallhierarchy.module b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_smallhierarchy.module
new file mode 100644
index 00000000..be4c7ebb
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_smallhierarchy.module
@@ -0,0 +1,214 @@
+ t('Custom'),
+ 'entity type' => t('N/A'),
+ );
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Automatically transform a given hierarchy with this format:
+ * array(
+ * 'win' => array(
+ * 'label' => 'Windows',
+ * 'children' => array(
+ * 'xp' => array('label' => 'XP'),
+ * 'vista' => array(
+ * 'label' => 'Vista',
+ * 'children' => array(
+ * 'x86' => array('label' => '32-bits'),
+ * 'x64' => array('label' => '64-bits'),
+ * ),
+ * ),
+ * ),
+ * ),
+ * )
+ *
+ * to one with this format:
+ * array(
+ * 'root' => array(
+ * 'children' => array(
+ * 'xp',
+ * 'vista',
+ * ),
+ * ),
+ * 'win' => array(
+ * 'label' => 'Windows',
+ * 'children' => array(
+ * 'win|xp',
+ * 'win|vista',
+ * ),
+ * ),
+ * 'win|xp' => array(
+ * 'label' => 'XP',
+ * ),
+ * 'win|vista' => array(
+ * 'label' => 'Vista',
+ * 'children' => array(
+ * 'win|vista|x86',
+ * 'win|vista|x64',
+ * ),
+ * ),
+ * 'win|vista|x86' => array(
+ * 'label' => '32-bits',
+ * ),
+ * 'win|vista|x64' => array(
+ * 'label' => '64-bits',
+ * ),
+ * )
+ *
+ * This new format:
+ * - ensures unique identifiers for each item
+ * - makes it very easy to find the parent of a given item.
+ * - makes it very easy to find the label and children of a given item.
+ *
+ * @params $hierarchy
+ * The hierarchy.
+ * @params $id
+ * A unique identifier for the hierarchy, for caching purposes.
+ * @params $separator
+ * The separator to use.
+ */
+function _hs_smallhierarchy_transform($hierarchy, $id, $separator = '|') {
+ // Make sure each hierarchy is only transformed once.
+ if (!isset($hs_hierarchy[$id])) {
+ $hs_hierarchy[$id] = array();
+
+ // Build the root level.
+ foreach ($hierarchy as $item => $children) {
+ $hs_hierarchy[$id]['root']['children'][] = $item;
+ $hs_hierarchy[$id][$item]['label'] = $children['label'];
+
+ // Build the subsequent levels.
+ if (isset($children['children'])) {
+ _hs_smallhierarchy_transform_recurse($item, $hs_hierarchy[$id], $children['children'], $separator);
+ }
+ }
+ }
+
+ return $hs_hierarchy[$id];
+}
+
+/**
+ * Helper function for _hs_smallhierarchy_transform().
+ *
+ * @params $parent
+ * The parent item of the current level.
+ * @params $hs_hierarchy
+ * The HS hierarchy.
+ * @params $relative_hierarchy
+ * The hierarchy relative to the current level.
+ * @params $separator
+ * The separator to use.
+ */
+function _hs_smallhierarchy_transform_recurse($parent, &$hs_hierarchy, $relative_hierarchy, $separator = '|') {
+ foreach ($relative_hierarchy as $item => $children) {
+ $generated_item = $parent . $separator . $item;
+ $hs_hierarchy[$parent]['children'][] = $generated_item;
+ $hs_hierarchy[$generated_item]['label'] = $children['label'];
+
+ // Build the subsequent levels.
+ if (isset($children['children'])) {
+ _hs_smallhierarchy_transform_recurse($generated_item, $hs_hierarchy, $children['children'], $separator);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.info b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.info
new file mode 100644
index 00000000..71ee7e20
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.info
@@ -0,0 +1,14 @@
+name = Hierarchical Select Taxonomy
+description = Use Hierarchical Select for Taxonomy.
+dependencies[] = hierarchical_select
+dependencies[] = taxonomy
+package = Form Elements
+
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2017-02-15
+version = "7.x-3.0-beta8"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1487167708"
+
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.install b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.install
new file mode 100644
index 00000000..72712c35
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.install
@@ -0,0 +1,123 @@
+condition('name', 'taxonomy_hierarchical_select_%', 'LIKE')
+ ->execute();
+ variable_del('taxonomy_override_selector');
+}
+
+/**
+ * Implementation of hook_enable().
+ */
+function hs_taxonomy_enable() {
+ variable_set('taxonomy_override_selector', TRUE);
+ drupal_set_message(t("Drupal core's taxonomy selects are now overridden on the
+ Taxonomy Term form. They've been replaced by
+ Hierarchical Selects for better scalability.
+ You can configure it to
+ be used on node forms too!",
+ array(
+ '!configure-url' => url('admin/config/content/hierarchical_select/configs'),
+ )));
+}
+
+/**
+ * Implementation of hook_disable().
+ */
+function hs_taxonomy_disable() {
+ variable_set('taxonomy_override_selector', FALSE);
+ drupal_set_message(t("Drupal core's taxonomy selects are now restored.
+ Please remember that they're not scalable!."),
+ 'warning');
+}
+
+
+//----------------------------------------------------------------------------
+// Schema updates.
+
+/**
+ * Upgrade path from Drupal 6 to Drupal 7 version of Hierarchical Select:
+ * - delete the taxonomy_override_selector variable if it exists.
+ */
+function hs_taxonomy_update_7300() {
+ variable_del('taxonomy_override_selector');
+}
+
+/**
+ * Apparently, taxonomy_override_selector still exists in *one* location in
+ * Drupal 7 core: on the form_taxonomy_form_term form (where you can create or
+ * edit a Taxonomy term).
+ */
+function hs_taxonomy_update_7301() {
+ variable_set('taxonomy_override_selector', TRUE);
+}
+
+/**
+ * Convert Taxonomy vocabulary config IDs to use machine name instead of serial
+ * vocabulary ID.
+ */
+function hs_taxonomy_update_7302() {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $vocabularies = taxonomy_vocabulary_get_names();
+
+ foreach ($vocabularies as $machine_name => $vocabulary) {
+ $old_config_id = "taxonomy-{$vocabulary->vid}";
+ $new_config_id = "taxonomy-{$machine_name}";
+
+ $old_config = variable_get('hs_config_' . $old_config_id, NULL);
+
+ if (!empty($old_config)) {
+ hierarchical_select_common_config_set($new_config_id, $old_config);
+ hierarchical_select_common_config_del($old_config_id);
+ }
+ }
+}
+
+/**
+ * Convert Taxonomy vocabulary config IDs to use field name.
+ */
+function hs_taxonomy_update_7303() {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ foreach (field_info_instances() as $entity_type => $bundles) {
+ foreach ($bundles as $bundle => $field_list) {
+ foreach ($field_list as $field_name => $instance) {
+ if ($instance['widget']['type'] == 'taxonomy_hs') {
+ $field_info = field_info_field($field_name);
+ $allowed_value = $field_info['settings']['allowed_values'][0];
+ $vocabulary_name = $allowed_value['vocabulary'];
+ $old_config_id = "taxonomy-{$vocabulary_name}";
+ $new_config_id = "taxonomy-{$field_name}";
+ $old_config = hierarchical_select_common_config_get($old_config_id);
+ if (!empty($old_config)) {
+ hierarchical_select_common_config_set($new_config_id, $old_config);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Cleanup old-named configs.
+ */
+function hs_taxonomy_update_7304() {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $vocabularies = taxonomy_get_vocabularies();
+ foreach ($vocabularies as $vid => $vocabulary) {
+ $old_config_id = "taxonomy-{$vocabulary->machine_name}";
+ hierarchical_select_common_config_del($old_config_id);
+ }
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.module b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.module
new file mode 100644
index 00000000..3308030d
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hs_taxonomy.module
@@ -0,0 +1,1277 @@
+ array(
+ 'variables' => array('lineage' => array()),
+ ),
+ );
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the Hierarchical Select admin settings form to add a checkbox to
+ * disable Hierarchical Select for taxonomy term edit forms.
+ */
+function hs_taxonomy_form_hierarchical_select_admin_settings_alter(&$form, &$form_state) {
+ $form['taxonomy_override_selector'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable for taxonomy term edit forms'),
+ '#description' => t(
+ 'If this is checked then the "Relations > Parent terms" field on taxonomy term edit pages will use hierarchical select.'
+ ),
+ '#default_value' => variable_get('taxonomy_override_selector', FALSE),
+ );
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the widget type form; dynamically add the Hierarchical Select
+ * Configuration form when it is needed.
+ */
+function hs_taxonomy_form_field_ui_widget_type_form_alter(&$form, &$form_state) {
+ form_load_include($form_state, 'inc', 'hierarchical_select', 'includes/common');
+
+ // Alter the widget type select: configure #ajax so that we can respond to
+ // changes in its value: whenever it is set to "taxonomy_hs", we add the HS
+ // config UI.
+ $form['basic']['widget_type']['#ajax'] = array(
+ 'event' => 'change',
+ 'callback' => 'hs_taxonomy_field_ui_widget_settings_ajax',
+ 'wrapper' => 'hs-config-replace',
+ 'method' => 'replace'
+ );
+
+ $current_widget_type = (isset($form_state['input']['widget_type'])) ? $form_state['input']['widget_type'] : $form_state['build_info']['args'][0]['widget']['type'];
+ if ($current_widget_type == 'taxonomy_hs') {
+ $field = field_info_field($form['#field_name']);
+ if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+ }
+
+ $vid = isset($vocabulary->vid) ? $vocabulary->vid : NULL;
+ $save_lineage = isset($vocabulary->hierarchy) ? (int) ($vocabulary->hierarchy == 2) : 0;
+ $instance = field_info_instance($form['#entity_type'], $form['#field_name'], $form['#bundle']);
+
+ // Add the Hierarchical Select config form.
+ $module = 'hs_taxonomy';
+ $params = array(
+ 'vid' => $vid,
+ 'exclude_tid' => NULL,
+ 'root_term' => NULL,
+ );
+ $config_id = hs_taxonomy_get_config_id($form['#field_name']);
+ $defaults = array(
+ // Enable the save_lineage setting by default if the multiple parents
+ // vocabulary option is enabled.
+ 'save_lineage' => $save_lineage,
+ 'editability' => array(
+ 'max_levels' => _hs_taxonomy_hierarchical_select_get_depth($vid),
+ ),
+ );
+ $strings = array(
+ 'hierarchy' => t('taxonomy_vocabulary'),
+ 'hierarchies' => t('vocabularies'),
+ 'item' => t('term'),
+ 'items' => t('terms'),
+ 'item_type' => t('term type'),
+ 'entity' => t('node'),
+ 'entities' => t('nodes'),
+ );
+ $max_hierarchy_depth = _hs_taxonomy_hierarchical_select_get_depth($vid);
+ $preview_is_required = ($instance['required'] == 1);
+ $form['hs'] = hierarchical_select_common_config_form($module, $params, $config_id, $defaults, $strings, $max_hierarchy_depth, $preview_is_required);
+
+ if (!module_exists('taxonomy_entity_index')) {
+ // Only allow person to select nodes.
+ $form['hs']['entity_count']['settings']['entity_types'] = array('node' => $form['hs']['entity_count']['settings']['entity_types']['node']);
+ $form['hs']['entity_count']['settings']['#collapsed'] = FALSE;
+ $form['hs']['entity_count']['settings']['entity_types']['node']['#collapsed'] = FALSE;
+ $form['hs']['entity_count']['#description'] = '
' . t('You can extend this functionality to other entities if you install !taxonomy_entity_index.', array('!taxonomy_entity_index' => l('Taxonomy entity index', 'https://www.drupal.org/project/taxonomy_entity_index'))) . '
';
+ }
+
+ // Make the config form AJAX-updateable.
+ $form['hs'] += array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // Add the submit handler for the Hierarchical Select config form. Make
+ // sure it is executed first.
+ $form['#hs_common_config_form_parents'] = array('hs');
+ array_unshift($form['#submit'], 'hierarchical_select_common_config_form_submit');
+
+ // Add a submit handler for HS Taxonomy that will update the field
+ // settings when necessary.
+ // @see hs_taxonomy_field_settings_submit() for details.
+ $form['#submit'][] = 'hs_taxonomy_field_settings_submit';
+ }
+ else {
+ $form['hs'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ }
+}
+
+/**
+ * Submit callback; updates the field settings (i.e. sets the cardinality of
+ * the field to unlimited) whenever either the dropbox or "save lineage" is
+ * enabled.
+ */
+function hs_taxonomy_field_settings_submit(&$form, &$form_state) {
+ $field = field_info_field($form['#field_name']);
+ $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field_name']));
+
+ if ($config['dropbox']['status'] || $config['save_lineage']) {
+ $field = field_info_field($form['#field_name']);
+ $field['cardinality'] = -1; // -1 = unlimited
+ field_update_field($field);
+
+ drupal_set_message(t("Updated this field's cardinality to unlimited."));
+ }
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the field settings form; dynamically disable the "cardinality" (or
+ * "Number of values" in the UI) setting on the form when either the dropbox
+ * or "save lineage" is enabled.
+ */
+function hs_taxonomy_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
+ if (isset($form['#field']['type']) && $form['#field']['type'] === 'taxonomy_term_reference' && $form['#instance']['widget']['type'] == 'taxonomy_hs') {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field']['field_name']));
+
+ if ($config['dropbox']['status'] || $config['save_lineage']) {
+ $form['field']['cardinality']['#disabled'] = TRUE;
+ $form['field']['cardinality']['#description'] .= ' ' . t('This setting is now managed by the Hierarchical Select configuration.') . '';
+ }
+ }
+}
+
+function hs_taxonomy_form_taxonomy_form_term_alter(&$form, &$form_state) {
+ // Don't alter the form when taxonomy_override_selector is not TRUE (or 1).
+ if (!variable_get('taxonomy_override_selector', FALSE)) {
+ return;
+ }
+
+ // Don't alter the form when it's in confirmation mode.
+ if (isset($form_state['confirm_delete']) || isset($form_state['confirm_parents'])) {
+ return;
+ }
+
+ // Build an appropriate config.
+ $vocabulary = $form['#vocabulary'];
+ $vid = $vocabulary->vid;
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ $config = array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vid,
+ 'exclude_tid' => isset($form['#term']['tid']) ? $form['#term']['tid'] : NULL,
+ 'root_term' => TRUE,
+ ),
+ 'enforce_deepest' => 0,
+ 'save_lineage' => 0,
+ 'level_labels' => array('status' => FALSE, 'labels' => array()),
+ 'dropbox' => array(
+ 'status' => variable_get('hs_taxonomy_enable_dropbox_on_term_form', 0),
+ 'limit' => 0,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ ),
+ 'entity_count' => array(
+ 'enable' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'render_flat_select' => 0,
+ );
+
+ // Use Hierarchical Select for selecting the parent term(s).
+ $parent_tid = array_keys(taxonomy_get_parents($form['#term']['tid']));
+ $parent = !empty($parent_tid) ? $parent_tid : array(0);
+ $form['relations']['parent'] = array(
+ '#type' => 'hierarchical_select',
+ '#title' => t('Parents'),
+ '#required' => TRUE,
+ '#default_value' => $parent,
+ '#config' => $config,
+ );
+ $form['relations']['parent']['#config']['dropbox']['title'] = t('All parent terms');
+}
+
+/**
+ * Implements hook_field_delete().
+ *
+ * This enables us to delete HS configs when fields are deleted.
+ */
+function hs_taxonomy_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+ hierarchical_select_common_config_del(hs_taxonomy_get_config_id($field['field_name']));
+}
+
+//----------------------------------------------------------------------------
+// FAPI callbacks.
+
+/**
+ * AJAX callback; field UI widget settings form.
+ */
+function hs_taxonomy_field_ui_widget_settings_ajax($form, &$form_state) {
+ return $form['hs'];
+}
+
+
+//----------------------------------------------------------------------------
+// Field API widget hooks.
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function hs_taxonomy_field_widget_info() {
+ return array(
+ 'taxonomy_hs' => array(
+ 'label' => t('Hierarchical Select'),
+ 'field types' => array('taxonomy_term_reference'),
+ 'settings' => array(), // All set in hs_taxonomy_field_widget_form().
+ 'behaviors' => array(
+ // TODO: figure out how to map the "dropbox" behavior to Field API's
+ // "multiple values" system.
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function hs_taxonomy_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+ }
+
+ // Build an array of existing term IDs.
+ $tids = array();
+ foreach ($items as $delta => $item) {
+ if (!empty($item['tid']) && $item['tid'] != 'autocreate') {
+ $tids[] = $item['tid'];
+ }
+ }
+
+ $element += array(
+ '#type' => 'hierarchical_select',
+ '#config' => array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => isset($vocabulary->vid) ? (int) $vocabulary->vid : NULL,
+ 'exclude_tid' => NULL,
+ 'root_term' => isset($field['settings']['allowed_values'][0]['parent']) ? (int) $field['settings']['allowed_values'][0]['parent'] : NULL,
+ ),
+ ),
+ '#default_value' => $tids,
+ );
+
+ hierarchical_select_common_config_apply($element, hs_taxonomy_get_config_id($field['field_name']));
+
+ // Append another #process callback that transforms #return_value to the
+ // format that Field API/Taxonomy Field expects.
+ // However, HS' default #process callback has not yet been set, since this
+ // typically happens automatically during FAPI processing. To ensure the
+ // order is right, we already set HS' own #process callback here explicitly.
+ $element_info = element_info('hierarchical_select');
+ $element['#process'] = array_merge($element_info['#process'], array('hs_taxonomy_widget_process'));
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function hs_taxonomy_field_widget_settings_form($field, $instance) {
+ // This poorly integrates with the Field UI. Hence we alter the
+ // field_ui_widget_type_form, to provide a more appropriate integration.
+ // @see hs_taxonomy_form_field_ui_widget_type_form_alter.
+ $form = array();
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function hs_taxonomy_field_widget_error($element, $error, $form, &$form_state) {
+ form_error($element, $error['message']);
+}
+
+/**
+ * #process callback that runs after HS' #process callback, to transform
+ * #return_value to the format that Field API/Taxonomy Field expects.
+ */
+function hs_taxonomy_widget_process($element, &$form_state, $complete_form) {
+ $tids = $element['#return_value'];
+
+ // If #return_value is array(NULL), then nothing was selected!
+ if (count($tids) == 1 && $tids[0] === NULL) {
+ $element['#return_value'] = array();
+ return $element;
+ }
+
+ $items = array();
+ foreach ($tids as $tid) {
+ $items[] = array('tid' => $tid);
+ }
+
+ $element['#return_value'] = $items;
+
+ return $element;
+}
+
+
+//----------------------------------------------------------------------------
+// Field API formatter hooks.
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function hs_taxonomy_field_formatter_info() {
+ return array(
+ 'hs_taxonomy_term_reference_hierarchical_text' => array(
+ 'label' => t('Hierarchical text'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_hierarchical_links' => array(
+ 'label' => t('Hierarchical links'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_hierarchical_links_last_text' => array(
+ 'label' => t('Hierarchical links last text'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_hierarchical_text_last_link' => array(
+ 'label' => t('Hierarchical text last link'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_last_link_only' => array(
+ 'label' => t('Last link only'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_last_text_only' => array(
+ 'label' => t('Last text only'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_prepare_view().
+ */
+function hs_taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+ // Extract required field information.
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+ $vid = $vocabulary->vid;
+
+ // Get the config for this field.
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ $config_id = hs_taxonomy_get_config_id($field['field_name']);
+ $config = hierarchical_select_common_config_get($config_id);
+ $config += array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vid,
+ ),
+ );
+
+ // Collect every possible term attached to any of the fieldable entities.
+ // Copied from taxonomy_field_formatter_prepare_view().
+ foreach ($entities as $id => $entity) {
+ $selection = array();
+
+ foreach ($items[$id] as $delta => $item) {
+ // Force the array key to prevent duplicates.
+ if ($item['tid'] != 'autocreate') {
+ $key = in_array($item['tid'], $selection) ? array_search($item['tid'], $selection) : $delta;
+ $selection[$key] = $item['tid'];
+ }
+ }
+
+ // Generate a dropbox out of the selection. This will automatically
+ // calculate all lineages for us.
+ $dropbox = _hierarchical_select_dropbox_generate($config, $selection);
+
+ // Store additional information in each item that's required for
+ // Hierarchical Select's custom formatters that are compatible with the
+ // save_lineage functionality.
+ if (!empty($dropbox->lineages)) {
+ foreach (array_keys($dropbox->lineages) as $lineage) {
+ foreach ($dropbox->lineages[$lineage] as $level => $details) {
+ $tid = $details['value'];
+
+ // Look up where this term (tid) is stored in the items array.
+ $key = array_search($tid, $selection);
+
+ // Store the additional information. One term can occur in multiple
+ // lineages: when Taxonomy's "multiple parents" functionality is
+ // being used.
+ $items[$id][$key]['hs_lineages'][] = array(
+ 'lineage' => $lineage,
+ 'level' => $level,
+ 'label' => $details['label'],
+ 'tid' => $tid,
+ );
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function hs_taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ // Extract required field information.
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+
+ // Extract the lineage information from the items (this was added by
+ // hs_taxonomy_field_formatter_prepare_view()).
+ $lineages = array();
+ foreach ($items as $delta => $item) {
+ if (!empty($item['hs_lineages'])) {
+ $metadata = $item['hs_lineages'];
+
+ for ($i = 0; $i < count($metadata); $i++) {
+ $term = new StdClass();
+ $term->tid = $metadata[$i]['tid'];
+ $term->vid = $vocabulary->vid;
+ $term->vocabulary_machine_name = $vocabulary->machine_name;
+ $term->name = $metadata[$i]['label'];
+
+ $lineages[$metadata[$i]['lineage']][$metadata[$i]['level']] = $term;
+ }
+ }
+ }
+
+ // Actual formatting.
+ $element = array();
+ switch ($display['type']) {
+ case 'hs_taxonomy_term_reference_hierarchical_text':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]); $level++) {
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][$level] = array(
+ '#markup' => $term->name,
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_hierarchical_links':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]); $level++) {
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $uri['options']['html'] = TRUE;
+ $element[$l]['#lineage'][$level] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_hierarchical_links_last_text':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]) - 1; $level++) {
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $element[$l]['#lineage'][$level] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][$level] = array(
+ '#markup' => $term->name,
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_hierarchical_text_last_link':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]) - 1; $level++) {
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][$level] = array(
+ '#markup' => $term->name,
+ );
+ }
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $element[$l]['#lineage'][$level] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_last_text_only':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][0] = array(
+ '#markup' => $term->name,
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_last_link_only':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $element[$l]['#lineage'][0] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ }
+ break;
+ }
+
+ if (!empty($element)) {
+ $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css';
+ }
+
+ return $element;
+}
+
+
+//----------------------------------------------------------------------------
+// Hierarchical Select hooks.
+
+/**
+ * Implementation of hook_hierarchical_select_params().
+ */
+function hs_taxonomy_hierarchical_select_params() {
+ $params = array(
+ 'vid',
+ 'exclude_tid', // Allows a term to be excluded (necessary for the taxonomy_form_term form).
+ 'root_term', // Displays a fake "" term in the root level (necessary for the taxonomy_form-term form).
+ 'entity_count_for_node_type', // Restrict the entity count to a specific node type.
+ );
+ return $params;
+}
+
+/**
+ * Implementation of hook_hierarchical_select_root_level().
+ */
+function hs_taxonomy_hierarchical_select_root_level($params) {
+ if (!isset($params['vid'])) {
+ return array();
+ }
+ // TODO: support multiple parents, i.e. support "save lineage".
+ $vocabulary = taxonomy_vocabulary_load($params['vid']);
+ $terms = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1);
+
+ // If the root_term parameter is enabled, then prepend a fake "" term.
+ if (isset($params['root_term']) && $params['root_term'] === TRUE) {
+ $root_term = new StdClass();
+ $root_term->tid = 0;
+ $root_term->name = '<' . t('root') . '>';
+ $terms = array_merge(array($root_term), $terms);
+ }
+
+ // Unset the term that's being excluded, if it is among the terms.
+ if (isset($params['exclude_tid'])) {
+ foreach ($terms as $key => $term) {
+ if ($term->tid == $params['exclude_tid']) {
+ unset($terms[$key]);
+ }
+ }
+ }
+
+ // If the Term Permissions module is installed, honor its settings.
+ if (function_exists('term_permissions_allowed')) {
+ global $user;
+ foreach ($terms as $key => $term) {
+ if (!term_permissions_allowed($term->tid, $user) ) {
+ unset($terms[$key]);
+ }
+ }
+ }
+
+ return _hs_taxonomy_hierarchical_select_terms_to_options($terms);
+}
+
+/**
+ * Implementation of hook_hierarchical_select_children().
+ */
+function hs_taxonomy_hierarchical_select_children($parent, $params) {
+ if (isset($params['root_term']) && $params['root_term'] && $parent == 0) {
+ return array();
+ }
+
+ $terms = taxonomy_get_children($parent, $params['vid']);
+
+ // Unset the term that's being excluded, if it is among the children.
+ if (isset($params['exclude_tid'])) {
+ unset($terms[$params['exclude_tid']]);
+ }
+
+ // If the Term Permissions module is installed, honor its settings.
+ if (function_exists('term_permissions_allowed')) {
+ global $user;
+ foreach ($terms as $key => $term) {
+ if (!term_permissions_allowed($term->tid, $user) ) {
+ unset($terms[$key]);
+ }
+ }
+ }
+
+ return _hs_taxonomy_hierarchical_select_terms_to_options($terms);
+}
+
+/**
+ * Implementation of hook_hierarchical_select_lineage().
+ */
+function hs_taxonomy_hierarchical_select_lineage($item, $params) {
+ $lineage = array();
+
+ if (isset($params['root_term']) && $params['root_term'] && $item == 0) {
+ return array(0);
+ }
+
+ $terms = array_reverse(hs_taxonomy_get_parents_all($item));
+ foreach ($terms as $term) {
+ $lineage[] = $term->tid;
+ }
+ return $lineage;
+}
+
+/**
+ * Alternative version of taxonomy_get_parents_all(): instead of using all
+ * parents of a term (i.e. when multiple parents are being used), only the
+ * first is kept.
+ */
+function hs_taxonomy_get_parents_all($tid) {
+ $parents = array();
+ if ($term = taxonomy_term_load($tid)) {
+ $parents[] = $term;
+ $n = 0;
+ while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
+ $parents = array_merge($parents, array(reset($parent)));
+ $n++;
+ }
+ }
+ return $parents;
+}
+
+/**
+ * Implementation of hook_hierarchical_select_valid_item().
+ */
+function hs_taxonomy_hierarchical_select_valid_item($item, $params) {
+ if (isset($params['root_term']) && $params['root_term'] && $item == 0) {
+ return TRUE;
+ }
+
+ if (!is_numeric($item) || $item < 1 || (isset($params['exclude_tid']) && $item == $params['exclude_tid'])) {
+ return FALSE;
+ }
+
+ $term = taxonomy_term_load($item);
+ if (!$term) {
+ return FALSE;
+ }
+
+ // If the Term Permissions module is installed, honor its settings.
+ if (function_exists('term_permissions_allowed')) {
+ global $user;
+ if (!term_permissions_allowed($term->tid, $user)) {
+ return FALSE;
+ }
+ }
+
+ return ($term->vid == $params['vid']);
+}
+
+/**
+ * Implementation of hook_hierarchical_select_item_get_label().
+ */
+function hs_taxonomy_hierarchical_select_item_get_label($item, $params) {
+ static $labels = array();
+
+ if (!isset($labels[$item])) {
+ if ($item === 0 && isset($params['root_term']) && $params['root_term'] === TRUE) {
+ $term = new StdClass();
+ $term->name = '<' . t('root') . '>';
+ }
+ else {
+ $term = taxonomy_term_load($item);
+ // Try to translate the label if the i18n module is available.
+ if (module_exists('i18n_taxonomy')) {
+ $term->name = i18n_taxonomy_term_name($term);
+ }
+ }
+ $labels[$item] = $term->name;
+ }
+
+ return $labels[$item];
+}
+
+/**
+ * Implementation of hook_hierarchical_select_create_item().
+ */
+function hs_taxonomy_hierarchical_select_create_item($label, $parent, $params) {
+ $term = new StdClass();
+ $term->vid = $params['vid'];
+ $term->name = html_entity_decode($label, ENT_QUOTES);
+ $term->description = '';
+ $term->parent = $parent;
+
+ $status = taxonomy_term_save($term);
+
+ if ($status !== FALSE) {
+ // Reset the cached tree.
+ _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1, TRUE);
+
+ // Retrieve the tid.
+ $children = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], $parent, 1);
+ foreach ($children as $term) {
+ if ($term->name == $label) {
+ return $term->tid;
+ }
+ }
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implementation of hook_hierarchical_select_entity_count().
+ */
+function hs_taxonomy_hierarchical_select_entity_count($item, $params) {
+
+ $num_entities = 0;
+ $selected_bundles = $params['entity_count']['settings']['entity_types'];
+ $count_children = $params['entity_count']['settings']['count_children'];
+
+ // Maybe this needs some more caching and value-updates on entity_save()/
+ // _update()/delete().
+ if (empty($num_entities)) {
+ $index_table = 'taxonomy_index';
+ if (module_exists('taxonomy_entity_index')) {
+ $index_table = 'taxonomy_entity_index';
+ }
+
+ // Count entities associated to this term.
+ $query = db_select($index_table, 'ti');
+ $query->fields('ti');
+ $query->condition('ti.tid', $item);
+ if (module_exists('taxonomy_entity_index')) {
+ _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles);
+ }
+ $result = $query->execute();
+
+ $num_entities = $result->rowCount();
+
+ if ($count_children) {
+ $tids = array();
+ $tree = taxonomy_get_tree($params['vid'], $item);
+ foreach ($tree as $child_term) {
+ $tids[] = $child_term->tid;
+ }
+ if (count($tids)) {
+ // Count entities associated to child terms.
+ $query = db_select($index_table, 'ti');
+ $query->fields('ti');
+ $query->condition('ti.tid', $tids, 'IN');
+ if (module_exists('taxonomy_entity_index')) {
+ _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles);
+ }
+ $result = $query->execute();
+
+ $num_entities += $result->rowCount();
+ }
+ }
+ }
+
+ return $num_entities;
+}
+
+/**
+ * Implementation of hook_hierarchical_select_implementation_info().
+ */
+function hs_taxonomy_hierarchical_select_implementation_info() {
+ return array(
+ 'hierarchy type' => t('Taxonomy'),
+ 'entity type' => t('Node'),
+ );
+}
+
+/**
+ * Implementation of hook_hierarchical_select_config_info().
+ */
+function hs_taxonomy_hierarchical_select_config_info() {
+ static $config_info;
+
+ if (!isset($config_info)) {
+ $config_info = array();
+ $fields = field_info_fields();
+ foreach ($fields as $field_name => $field) {
+ foreach ($field['bundles'] as $entity_type => $bundles) {
+ $bundle_links = array();
+ foreach ($bundles as $bundle) {
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+ if ($instance['widget']['type'] == 'taxonomy_hs') {
+ $bundles_info = field_info_bundles($entity_type);
+ $bundle_links[] = l($bundles_info[$bundle]['label'], $bundles_info[$bundle]['admin']['real path']);
+ $entity_info = entity_get_info($entity_type);
+ $machine_name = $field['settings']['allowed_values'][0]['vocabulary'];
+ $vocabulary = taxonomy_vocabulary_machine_name_load($machine_name);
+ $config_id = hs_taxonomy_get_config_id($field_name);
+ $config_info[$config_id] = array(
+ 'config_id' => $config_id,
+ 'hierarchy type' => t('Taxonomy'),
+ 'hierarchy' => t('Vocabulary') . ': ' . l(t($vocabulary->name), "admin/structure/taxonomy/$machine_name")
+ . ' ' . t('Field label') . ': ' . $instance['label']
+ . ' ' . t('Field machine name') . ': ' . $field_name . '',
+ 'entity type' => $entity_info['label'],
+ 'bundle' => implode(' ', $bundle_links),
+ 'context type' => '',
+ 'context' => '',
+ 'edit link' => isset($bundles_info[$bundle]['admin']['real path']) ? $bundles_info[$bundle]['admin']['real path'] . "/fields/$field_name/widget-type" : $bundles_info[$bundle]['admin']['path'] . "/fields/$field_name/widget-type",
+ );
+ }
+ }
+ }
+ }
+ }
+
+ return $config_info;
+}
+
+
+//----------------------------------------------------------------------------
+// Token hooks.
+
+/**
+ * Implementation of hook_token_values().
+ */
+/*
+// TODO: port this to D7.
+function hs_taxonomy_token_values($type, $object = NULL, $options = array()) {
+ static $hs_vids;
+ static $all_vids;
+
+ $separator = variable_get('hs_taxonomy_separator', variable_get('pathauto_separator', '-'));
+
+ $values = array();
+ switch ($type) {
+ case 'node':
+ $node = $object;
+
+ // Default values.
+ $values['save-lineage-termpath'] = $values['save-lineage-termpath-raw'] = '';
+
+ // If $node->taxonomy doesn't exist, these tokens cannot be created!
+ if (!is_object($node) || !isset($node->taxonomy) || !is_array($node->taxonomy)) {
+ return $values;
+ }
+
+ // Find out which vocabularies are using Hierarchical Select.
+ if (!isset($hs_vids)) {
+ $hs_vids = array();
+ // TODO Please convert this statement to the D7 database API syntax.
+ $result = db_query("SELECT SUBSTRING(name, 30, 3) AS vid FROM {variable} WHERE name LIKE 'taxonomy_hierarchical_select_%' AND value LIKE 'i:1\;';");
+ while ($o = db_fetch_object($result)) {
+ $hs_vids[] = $o->vid;
+ }
+ }
+
+ // Get a list of all existent vids, so we can generate an empty token
+ // when a token is requested for a vocabulary that's not associated with
+ // the current content type.
+ if (!isset($all_vids)) {
+ $all_vids = array();
+ $result = db_query("SELECT vid FROM {taxonomy_vocabulary}");
+ while ($row = db_fetch_object($result)) {
+ $all_vids[] = $row->vid;
+ }
+ }
+
+ // Generate the per-vid "save-lineage-termpath" tokens.
+ foreach ($all_vids as $vid) {
+ $terms = array();
+ if (in_array($vid, $hs_vids) && isset($node->taxonomy[$vid])) {
+ $selection = $node->taxonomy[$vid];
+ $terms = _hs_taxonomy_token_termpath_for_vid($selection, $vid);
+ }
+
+ $terms_raw = $terms;
+ $terms = array_map('check_plain', $terms);
+ $values["save-lineage-termpath:$vid"] = !empty($options['pathauto']) ? $terms : implode($separator, $terms);
+ $values["save-lineage-termpath-raw:$vid"] = !empty($options['pathauto']) ? $terms_raw : implode($separator, $terms_raw);
+ }
+
+ // We use the terms of the first vocabulary that uses Hierarchical
+ // Select for the default "save-lineage-termpath" tokens.
+ $vids = array_intersect(array_keys($node->taxonomy), $hs_vids);
+ if (!empty($vids)) {
+ $vid = $vids[0];
+ $values['save-lineage-termpath'] = $values["save-lineage-termpath:$vid"];
+ $values['save-lineage-termpath-raw'] = $values["save-lineage-termpath-raw:$vid"];
+ }
+ break;
+ }
+
+ return $values;
+}
+*/
+
+/**
+ * Implementation of hook_token_list().
+ */
+/*
+// TODO: port this to D7.
+function hs_taxonomy_token_list($type = 'all') {
+ if ($type == 'node' || $type == 'all') {
+ $tokens['node']['save-lineage-termpath'] = t('Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.');
+ $tokens['node']['save-lineage-termpath-raw'] = t('As [save-linage-termpath]. WARNING - raw user input.');
+
+ $tokens['node']['save-lineage-termpath:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.');
+ $tokens['node']['save-lineage-termpath-raw:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. As [save-linage-termpath]. WARNING - raw user input.');
+
+ return $tokens;
+ }
+}
+*/
+
+/**
+ * Helper function for hs_taxonomy_token_values().
+ */
+function _hs_taxonomy_token_termpath_for_vid($selection, $vid) {
+ $terms = array();
+ $selection = (is_array($selection)) ? $selection : array($selection);
+
+ // Generate the part we'll need of the Hierarchical Select configuration.
+ $config = array(
+ 'module' => 'hs_taxonomy',
+ 'save_lineage' => 1,
+ 'params' => array(
+ 'vid' => $vid,
+ 'exclude_tid' => NULL,
+ 'root_term' => NULL,
+ ),
+ );
+
+ // Validate all items in the selection, if any.
+ if (!empty($selection)) {
+ foreach ($selection as $key => $item) {
+ $valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $selection[$key], $config['params']);
+ if (!$valid) {
+ unset($selection[$key]);
+ }
+ }
+ }
+
+ // Generate a dropbox out of the selection. This will automatically
+ // calculate all lineages for us.
+ // If the selection is empty, then the tokens will be as well.
+ if (!empty($selection)) {
+ $dropbox = _hierarchical_select_dropbox_generate($config, $selection);
+
+ // If no lineages could be generated, these tokens cannot be created!
+ if (empty($dropbox->lineages)) {
+ return $terms;
+ }
+
+ // We pick the first lineage.
+ $lineage = $dropbox->lineages[0];
+
+ // Finally, we build the tokens.
+ foreach ($lineage as $item) {
+ $terms[] = $item['label'];
+ }
+ }
+
+ return $terms;
+}
+
+
+//----------------------------------------------------------------------------
+// Theme functions.
+
+/**
+ * Format a lineage for one of HS Taxonomy's custom Term reference formatters.
+ */
+function theme_hs_taxonomy_formatter_lineage($variables) {
+ $output = '';
+ $lineage = $variables['lineage'];
+ $separator = theme('hierarchical_select_item_separator');
+
+ // Render each item within a lineage.
+ $items = array();
+ foreach ($lineage as $level => $item ) {
+ $line = '';
+ $line .= drupal_render($item);
+ $line .= '';
+ $items[] = $line;
+ }
+ $output .= implode($separator, $items);
+
+ return $output;
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Drupal core's taxonomy_get_tree() doesn't allow us to reset the cached
+ * trees, which obviously causes problems when you create new items between
+ * two calls to it.
+ */
+function _hs_taxonomy_hierarchical_select_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL, $reset = FALSE) {
+ static $children, $parents, $terms;
+
+ if ($reset) {
+ $children = $parents = $terms = array();
+ }
+
+ $tree = array();
+
+ if (!is_array($parent)) {
+ $parent = array($parent);
+ }
+
+ $max_depth = (is_null($max_depth)) ? 99999999 : $max_depth;
+ $depth++;
+
+ // We cache trees, so it's not CPU-intensive to call get_tree() on a term
+ // and its children, too.
+ if ($max_depth <= $depth) {
+ return $tree;
+ }
+ // Prepare queue for the "IN ( .. )" part of query.
+ $queue = array();
+ foreach ($parent as $single_parent) {
+ // Queue branch for processing if it's not cached yet.
+ if (!isset($children[$vid][$single_parent])) {
+ $queue[] = $single_parent;
+ // Use an empty array to distinguish between a stub (without children)
+ // term and a branch that is not loaded yet.
+ $children[$vid][$single_parent] = array();
+ }
+ }
+ if (!empty($queue)) {
+
+ $query = db_select('taxonomy_term_data', 't');
+ $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
+ $result = $query
+ ->addTag('translatable')
+ ->addTag('term_access')
+ ->addTag('hs_taxonomy_tree')
+ ->fields('t')
+ ->fields('h', array('parent'))
+ ->condition('t.vid', $vid)
+ ->condition('parent', array_merge(array($vid), $queue), 'IN')
+ ->orderBy('t.weight')
+ ->orderBy('t.name')
+ ->execute();
+
+ foreach ($result as $term) {
+ $children[$vid][$term->parent][] = $term->tid;
+ $parents[$vid][$term->tid][] = $term->parent;
+ $terms[$vid][$term->tid] = $term;
+ }
+ }
+
+ // Provide support for Title module. If Title module is enabled and this
+ // vocabulary uses translated term names we want output those terms with their
+ // translated version. Therefore a full taxonomy term entity load is required,
+ // similar to taxonomy_get_tree().
+ if (!empty($terms) && module_exists('title')) {
+ $vocabulary = taxonomy_vocabulary_load($vid);
+ if (title_field_replacement_enabled('taxonomy_term', $vocabulary->machine_name, 'name')) {
+ $term_entities = taxonomy_term_load_multiple(array_keys($terms[$vid]));
+ }
+ }
+
+ $next_parent = array();
+ foreach ($parent as $single_parent) {
+ foreach ($children[$vid][$single_parent] as $child) {
+ $term = isset($term_entities[$child]) ? $term_entities[$child] : $terms[$vid][$child];
+ $term = clone $term;
+ $term->depth = $depth;
+ // The "parent" attribute is not useful, as it would show one parent only.
+ unset($term->parent);
+ $term->parents = $parents[$vid][$child];
+ $tree[] = $term;
+ // Need more steps ?
+ if ($max_depth > $depth + 1) {
+ // Queue children for the next step down the tree. Do not process
+ // children which we already know as stub ones.
+ if (!isset($children[$vid][$child]) || !empty($children[$vid][$child])) {
+ $next_parent[] = $child;
+ }
+ }
+ }
+ }
+ if (!empty($next_parent)) {
+ // Process multiple children together i.e. next level.
+ $tree = array_merge($tree, _hs_taxonomy_hierarchical_select_get_tree($vid, $next_parent, $depth, $max_depth));
+ }
+
+ return isset($tree) ? $tree : array();
+}
+
+/**
+ * Returns the configuration ID that would be used for the specified field.
+ *
+ * @param string $field_name
+ * The field machine name.
+ *
+ * @return string
+ * The config id for the provided field.
+ */
+function hs_taxonomy_get_config_id($field_name) {
+ return "taxonomy-{$field_name}";
+}
+
+/**
+ * Drupal core's taxonomy_term_count_nodes() is buggy. See
+ * http://drupal.org/node/144969#comment-843000.
+ */
+function hs_taxonomy_term_count_nodes($tid, $type = 0) {
+ static $count;
+
+ $tids = array($tid);
+ if ($term = taxonomy_term_load($tid)) {
+ $tree = _hs_taxonomy_hierarchical_select_get_tree($term->vid, $tid);
+ foreach ($tree as $descendant) {
+ $tids[] = $descendant->tid;
+ }
+ }
+
+ if (!isset($count[$type][$tid])) {
+ $query = db_select('taxonomy_term_node','t');
+ $query->join('node', 'n', 't.nid = n.nid');
+ $query->addExpression('COUNT(DISTINCT(n.nid))', 'count')
+ ->condition('n.status', 1)
+ ->condition('t,tid', $tids);
+
+ if (!is_numeric($type)) {
+ $query->condition('n.type', $type);
+ }
+
+ $query->addTag('hs_taxonomy_term_count_nodes');
+ $query->addTag('term_access');
+ $result = $query->execute();
+
+ $count[$type][$tid] = $result->fetchField();
+ }
+ return $count[$type][$tid];
+}
+
+/**
+ * Transform an array of terms into an associative array of options, for use
+ * in a select form item.
+ *
+ * @param $terms
+ * An array of term objects.
+ * @return
+ * An associative array of options, keys are tids, values are term names.
+ */
+function _hs_taxonomy_hierarchical_select_terms_to_options($terms) {
+ $options = array();
+ $use_i18n = module_exists('i18n_taxonomy');
+ foreach ($terms as $key => $term) {
+ // Use the translated term when available!
+ $options[$term->tid] = $use_i18n && isset($term->vid) ? i18n_taxonomy_term_name($term) : $term->name;
+ }
+ return $options;
+}
+
+/**
+ * Get the depth of a vocabulary's tree.
+ *
+ * @param $vid
+ * A vocabulary id.
+ * @return
+ * The depth of the vocabulary's tree.
+ */
+function _hs_taxonomy_hierarchical_select_get_depth($vid) {
+ $depth = -99999;
+ $tree = _hs_taxonomy_hierarchical_select_get_tree($vid);
+ foreach ($tree as $term) {
+ if ($term->depth > $depth) {
+ $depth = $term->depth;
+ }
+ }
+ return $depth;
+}
+
+/**
+ * Helper function to add entity_type and bundles to count query in form of a
+ * and/or combination.
+ *
+ * @param object $query
+ * A db_select query object.
+ * @param array $selected_bundles
+ * An associative array of entities that contain bundles.
+ */
+function _hs_taxonomy_add_entity_bundles_condition_to_query(&$query, $selected_bundles) {
+ $db_or = db_or();
+ foreach ($selected_bundles as $entity_type => $bundles) {
+ $db_and = db_and();
+ $db_and->condition('ti.entity_type', $entity_type);
+ $db_and->condition('ti.bundle', array_shift($bundles) , 'IN');
+ $db_or->condition($db_and);
+ }
+ $query->condition($db_or);
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/README.txt b/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/README.txt
new file mode 100644
index 00000000..4eb1737f
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/README.txt
@@ -0,0 +1,8 @@
+This module allows you to use hierarchical_select (version 7.x-3.x) as a widget
+for a taxonomy-based entityreference field.
+
+To use it, create an entityreference field, with the Hierarchical Select widget,
+select "Taxonomy term" as the target type, and select one vocabulary (you must
+choose exactly one) as the target bundle.
+
+Credit: John Morahan, iO1 and iVillage.
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/hser.info b/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/hser.info
new file mode 100644
index 00000000..14cdc58b
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/hser.info
@@ -0,0 +1,18 @@
+name = Hierarchical Select Entity Reference
+description = Use the hierarchical select widget for entity reference fields, using taxonomy to provide hierarchy if appropriate, otherwise flat.
+package = Form Elements
+core = 7.x
+dependencies[] = hierarchical_select
+dependencies[] = hs_taxonomy
+dependencies[] = entityreference
+dependencies[] = entity
+dependencies[] = ctools
+dependencies[] = options
+dependencies[] = field
+
+; Information added by Drupal.org packaging script on 2017-02-15
+version = "7.x-3.0-beta8"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1487167708"
+
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/hser.module b/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/hser.module
new file mode 100644
index 00000000..eaaf47d0
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/modules/hser/hser.module
@@ -0,0 +1,132 @@
+ array(
+ 'label' => t('Hierarchical Select'),
+ 'field types' => array('entityreference'),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ 'settings' => array(
+ 'editable' => FALSE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function hser_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'] + field_info_widget_settings($widget['type']);
+
+ $form = array();
+
+ if ($widget['type'] == 'hser_hierarchy') {
+ $form['editable'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Editable'),
+ '#default_value' => $settings['editable'],
+ '#description' => t('Select this to allow users to use the hierarchical select widget to create new terms in the selected vocabulary.'),
+ );
+ }
+
+ return $form;
+}
+
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function hser_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ if ($field['settings']['target_type'] == 'taxonomy_term') {
+ $vocabularies = $field['settings']['handler_settings']['target_bundles'];
+ if ((count($vocabularies) == 1) && ($vocabulary = taxonomy_vocabulary_machine_name_load(reset($vocabularies)))) {
+ $default_value = array();
+ foreach ($items as $item) {
+ $default_value[] = $item['target_id'];
+ }
+ $element += array(
+ '#type' => 'hierarchical_select',
+ '#size' => 1,
+ '#default_value' => $default_value,
+ '#element_validate' => array('_hser_element_validate'),
+ '#required' => $instance['required'],
+ '#config' => array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vocabulary->vid,
+ ),
+ 'save_lineage' => FALSE,
+ 'enforce_deepest' => FALSE,
+ 'resizable' => FALSE,
+ 'level_labels' => array('status' => FALSE),
+ 'dropbox' => array(
+ 'status' => ($field['cardinality'] != 1),
+ 'limit' => $field['cardinality'],
+ ),
+ 'editability' => array(
+ 'status' => $instance['widget']['settings']['editable'],
+ 'allow_new_levels' => TRUE,
+ 'max_levels' => 0,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'render_flat_select' => FALSE,
+ ),
+ );
+ return $element;
+ }
+ }
+ // If we reach this point, we decided that hierarchical_select would not be
+ // appropriate for some reason (not taxonomy, no vocabulary selected, etc).
+ // So instead we fall back to a normal select widget.
+ $instance['widget']['type'] = 'options_select';
+ return options_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
+}
+
+/**
+ * Element validation callback for field widget hierarchical select element.
+ */
+function _hser_element_validate($element, &$form_state, $form) {
+ $value = array();
+ foreach ($element['#value'] as $delta => $target_id) {
+ $value[$delta]['target_id'] = $target_id;
+ }
+ form_set_value($element, $value, $form_state);
+
+ if ($element['#required'] && (!isset($form_state['submit_handlers'][0]) || $form_state['submit_handlers'][0] !== 'hierarchical_select_ajax_update_submit')) {
+ if (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0) || (array_key_exists(0, $element['#value'])) && !$element['#value'][0]) {
+ form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
+ _hierarchical_select_form_set_error_class($element);
+ }
+ }
+}
+
+/**
+ * Implements hook_node_validate().
+ *
+ * Temporary workaround for https://drupal.org/node/1293166 - remove when that
+ * bug is fixed.
+ */
+function hser_node_validate($node, $form, &$form_state) {
+ if (arg(0) == 'hierarchical_select_ajax') {
+ form_set_error('');
+ }
+}
diff --git a/sites/all/modules/contrib/fields/hierarchical_select/tests/internals.test b/sites/all/modules/contrib/fields/hierarchical_select/tests/internals.test
new file mode 100644
index 00000000..e7341241
--- /dev/null
+++ b/sites/all/modules/contrib/fields/hierarchical_select/tests/internals.test
@@ -0,0 +1,433 @@
+ array(
+ 'label' => LABEL_EURO,
+ 'children' => array(
+ EURO_BE => array(
+ 'label' => LABEL_EURO_BE,
+ 'children' => array(
+ EURO_BE_BRU => array(
+ 'label' => LABEL_EURO_BE_BRU,
+ ),
+ EURO_BE_HAS => array(
+ 'label' => LABEL_EURO_BE_HAS,
+ ),
+ ),
+ ),
+ EURO_FR => array(
+ 'label' => LABEL_EURO_FR,
+ ),
+ ),
+ ),
+ ASIA => array(
+ 'label' => LABEL_ASIA,
+ 'children' => array(
+ ASIA_CH => array(
+ 'label' => LABEL_ASIA_CH,
+ ),
+ ASIA_JP => array(
+ 'label' => LABEL_ASIA_JP,
+ 'children' => array(
+ ASIA_JP_TOK => array(
+ 'label' => LABEL_ASIA_JP_TOK,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+
+ /**
+ * Implementation of getInfo().
+ */
+ public function getInfo() {
+ return array(
+ 'name' => 'Internals',
+ 'description' => 'Checks whether all internals are working: the
+ building of the hierarchy and dropbox objects.',
+ 'group' => 'Hierarchical Select',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ public function setUp() {
+ parent::setUp('hierarchical_select', 'hs_smallhierarchy');
+ }
+
+ // In this test, all settings are disabled.
+ public function testAllSettingsOff() {
+ // Generate form item.
+ $form_item = array(
+ '#required' => FALSE,
+ '#config' => array(
+ 'module' => 'hs_smallhierarchy',
+ 'params' => array(
+ 'hierarchy' => $this->small_hierarchy,
+ 'id' => 'driverpack_platforms',
+ 'separator' => '|',
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'animation_delay' => 400,
+ 'exclusive_lineages' => array(),
+ 'render_flat_select' => 0,
+ ),
+ );
+
+ // No selection
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(), array());
+ $reference = new stdClass();
+ $reference->lineage = array(
+ 0 => 'none',
+ );
+ $reference->levels = array(
+ 0 => array(
+ 'none' => '',
+ LINEAGE_EURO => LABEL_EURO,
+ LINEAGE_ASIA => LABEL_ASIA,
+ ),
+ );
+ $reference->childinfo = array(
+ 0 => array(
+ LINEAGE_EURO => 2,
+ LINEAGE_ASIA => 2,
+ ),
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => 'label_1',
+ );
+ $reference->levels[1] = array(
+ 'label_1' => '',
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > France
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_FR), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_FR,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > Belgium
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_BE), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_BE,
+ 2 => 'label_2',
+ );
+ $reference->levels[1] = array(
+ 'label_1' => '',
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->levels[2] = array(
+ 'label_2' => '',
+ LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU,
+ LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_EURO_BE_BRU => 0,
+ LINEAGE_EURO_BE_HAS => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => 'label_1',
+ );
+ $reference->levels[1] = array(
+ 'label_1' => '',
+ LINEAGE_ASIA_CH => LABEL_ASIA_CH,
+ LINEAGE_ASIA_JP => LABEL_ASIA_JP,
+ );
+ unset($reference->levels[2]);
+ $reference->childinfo[1] = array(
+ LINEAGE_ASIA_CH => 0,
+ LINEAGE_ASIA_JP => 1,
+ );
+ unset($reference->childinfo[2]);
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia > Japan > Tokyo
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA_JP_TOK), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => LINEAGE_ASIA_JP,
+ 2 => LINEAGE_ASIA_JP_TOK,
+ );
+ $reference->levels[2] = array(
+ 'label_2' => '',
+ LINEAGE_ASIA_JP_TOK => LABEL_ASIA_JP_TOK,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_ASIA_JP_TOK => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+ }
+
+ // In this test, only enforce_deepest enabled.
+ public function testEnforceDeepest() {
+ // Generate form item.
+ $form_item = array(
+ '#required' => FALSE,
+ '#config' => array(
+ 'module' => 'hs_smallhierarchy',
+ 'params' => array(
+ 'hierarchy' => $this->small_hierarchy,
+ 'id' => 'driverpack_platforms',
+ 'separator' => '|',
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 1,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'animation_delay' => 400,
+ 'exclusive_lineages' => array(),
+ 'render_flat_select' => 0,
+ ),
+ );
+
+ // No selection
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(), array());
+ $reference = new stdClass();
+ $reference->lineage = array(
+ 0 => 'label_0',
+ );
+ $reference->levels = array(
+ 0 => array(
+ 'none' => '',
+ LINEAGE_EURO => LABEL_EURO,
+ LINEAGE_ASIA => LABEL_ASIA,
+ ),
+ );
+ $reference->childinfo = array(
+ 0 => array(
+ LINEAGE_EURO => 2,
+ LINEAGE_ASIA => 2,
+ ),
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_BE,
+ 2 => LINEAGE_EURO_BE_BRU,
+ );
+ $reference->levels[1] = array(
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->levels[2] = array(
+ LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU,
+ LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_EURO_BE_BRU => 0,
+ LINEAGE_EURO_BE_HAS => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > France
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_FR), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_FR,
+ );
+ unset($reference->levels[2]);
+ unset($reference->childinfo[2]);
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > Belgium
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_BE), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_BE,
+ 2 => LINEAGE_EURO_BE_BRU,
+ );
+ $reference->levels[1] = array(
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->levels[2] = array(
+ LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU,
+ LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_EURO_BE_BRU => 0,
+ LINEAGE_EURO_BE_HAS => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => LINEAGE_ASIA_CH,
+ );
+ $reference->levels[1] = array(
+ LINEAGE_ASIA_CH => LABEL_ASIA_CH,
+ LINEAGE_ASIA_JP => LABEL_ASIA_JP,
+ );
+ unset($reference->levels[2]);
+ $reference->childinfo[1] = array(
+ LINEAGE_ASIA_CH => 0,
+ LINEAGE_ASIA_JP => 1,
+ );
+ unset($reference->childinfo[2]);
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia > Japan > Tokyo
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA_JP_TOK), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => LINEAGE_ASIA_JP,
+ 2 => LINEAGE_ASIA_JP_TOK,
+ );
+ $reference->levels[2] = array(
+ LINEAGE_ASIA_JP_TOK => LABEL_ASIA_JP_TOK,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_ASIA_JP_TOK => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+ }
+
+
+ //--------------------------------------------------------------------------
+ // Private methods.
+
+ private function generate($element, $hs_selection, $db_selection, $op = 'Update') {
+ $config = $element['#config'];
+
+ // Generate the $hierarchy and $dropbox objects using the selections that
+ // were just calculated.
+ $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection);
+ $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox);
+
+ return array($hierarchy, $dropbox);
+ }
+
+ private function assertHierarchy($hierarchy, $reference) {
+ $this->assertIdentical($hierarchy->lineage, $reference->lineage, 'Hierarchy lineage is correct.');
+ $this->assertIdentical($hierarchy->levels, $reference->levels, 'Hierarchy levels is correct.');
+ $this->assertIdentical($hierarchy->childinfo, $reference->childinfo, 'Hierarchy child info is correct.');
+ }
+}
diff --git a/sites/all/modules/contrib/fields/prepopulate/LICENSE.txt b/sites/all/modules/contrib/fields/prepopulate/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/fields/prepopulate/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/fields/prepopulate/README.txt b/sites/all/modules/contrib/fields/prepopulate/README.txt
new file mode 100644
index 00000000..8757fc25
--- /dev/null
+++ b/sites/all/modules/contrib/fields/prepopulate/README.txt
@@ -0,0 +1,23 @@
+
+PREPOPULATE MODULE
+==================
+By ea.Farris, based on an idea from chx.
+Maintained by Addison Berry (add1sun).
+
+Prepopulate is an attempt to solve the problem that resulted from
+the discussion at http://www.drupal.org/node/27155 where the $node object,
+it was (correctly, I believe) decided, should
+not be prefilled from the $_GET variables, and instead, the power of the
+FormsAPI should be used to modify the #default_value of the form
+elements themselves.
+
+This functionality will make things like bookmarklets easier to write,
+since it basically allows forms to be prefilled from the URL, using a
+syntax like:
+
+http://www.example.com/node/add/blog?edit[title]=this is the title&edit[body]=body goes here
+
+Refer to the online handbook at http://drupal.org/node/228167 for more examples.
+
+Please report any bugs or feature requests to the Prepopulate issue queue:
+http://drupal.org/project/issues/prepopulate
diff --git a/sites/all/modules/contrib/fields/prepopulate/prepopulate.info b/sites/all/modules/contrib/fields/prepopulate/prepopulate.info
new file mode 100644
index 00000000..d7c7875f
--- /dev/null
+++ b/sites/all/modules/contrib/fields/prepopulate/prepopulate.info
@@ -0,0 +1,10 @@
+name = Prepopulate
+description = Allows form elements to be prepopulated from the URL.
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2016-03-02
+version = "7.x-2.1"
+core = "7.x"
+project = "prepopulate"
+datestamp = "1456898940"
+
diff --git a/sites/all/modules/contrib/fields/prepopulate/prepopulate.install b/sites/all/modules/contrib/fields/prepopulate/prepopulate.install
new file mode 100644
index 00000000..1d18588a
--- /dev/null
+++ b/sites/all/modules/contrib/fields/prepopulate/prepopulate.install
@@ -0,0 +1,12 @@
+
+ * Based on an idea from chx, from the conversation at
+ * http://www.drupal.org/node/27155.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function prepopulate_help($path, $arg) {
+ switch ($path) {
+ case 'admin/modules#description':
+ return t('Pre-populates forms with HTTP GET or POST data');
+ }
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function prepopulate_form_alter(&$form, $form_state, $form_id) {
+ // If this is a subsequent step of a multi-step form, the prepopulate values
+ // have done their work, and the user may have modified them: bail.
+ if (!empty($form_state['rebuild'])) {
+ return;
+ }
+ if (isset($_REQUEST['edit'])) {
+ $form['#after_build'][] = 'prepopulate_after_build';
+ }
+}
+
+/**
+ * An #after_build function to set the values prepopulated in the request.
+ */
+function prepopulate_after_build($form, &$form_state) {
+ if (isset($_REQUEST['edit'])) {
+ $request = (array) $_REQUEST['edit'];
+ _prepopulate_request_walk($form, $request);
+ }
+ return $form;
+}
+
+/**
+ * Internal helper to set element values from the $_REQUEST variable.
+ *
+ * @param array &$form
+ * A form element.
+ * @param mixed &$request_slice
+ * String or array. Value(s) to be applied to the element.
+ */
+function _prepopulate_request_walk(&$form, &$request_slice) {
+ $limited_types = array(
+ 'actions',
+ 'button',
+ 'container',
+ 'token',
+ 'value',
+ 'hidden',
+ 'image_button',
+ 'password',
+ 'password_confirm',
+ 'text_format',
+ 'markup',
+ );
+ if (is_array($request_slice)) {
+ foreach (array_keys($request_slice) as $request_variable) {
+ if (element_child($request_variable) && !empty($form[$request_variable]) &&
+ (!isset($form[$request_variable]['#type']) || !in_array($form[$request_variable]['#type'], $limited_types))) {
+ if (!isset($form[$request_variable]['#access']) || $form[$request_variable]['#access'] != FALSE) {
+ _prepopulate_request_walk($form[$request_variable], $request_slice[$request_variable]);
+ }
+ }
+ }
+ if (!empty($form['#default_value']) && is_array($form['#default_value'])) {
+ $form['#default_value'] = array_merge($form['#default_value'], $request_slice);
+ }
+ }
+ else {
+ if ($form['#type'] == 'markup' || empty($form['#type'])) {
+ $form['#value'] = check_plain($request_slice);
+ }
+ else {
+ $form['#value'] = $request_slice;
+ }
+ if ($form['#type'] == 'checkboxes' || $form['#type'] == 'checkbox') {
+ if (!empty($form['#value'])) {
+ $form['#checked'] = TRUE;
+ }
+ else {
+ $form['#checked'] = FALSE;
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/entity_translation_tabs/LICENSE.txt b/sites/all/modules/contrib/localisation/entity_translation_tabs/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation_tabs/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/localisation/entity_translation_tabs/README.txt b/sites/all/modules/contrib/localisation/entity_translation_tabs/README.txt
new file mode 100644
index 00000000..af95e7dc
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation_tabs/README.txt
@@ -0,0 +1,34 @@
+-- SUMMARY --
+
+Creates tabs on nodes to redirect users to the appropriate transltion edit
+page.
+
+For a full description of the module, visit the project page:
+ http://drupal.org/project/entity_translation_tabs
+To submit bug reports and feature suggestions, or to track changes:
+ http://drupal.org/project/issues/entity_translation_tabs
+
+
+-- REQUIREMENTS --
+
+entity_translation, and entity translation enabled on your nodes.
+
+-- INSTALLATION --
+
+* Install as usual, see http://drupal.org/node/70151 for further information.
+
+-- CONFIGURATION --
+
+* Then configure your nodes to use entity_translation
+
+-- USAGE --
+
+* Go to a node page, where the edit tab was you will now see an edit tab
+ for each language in addition to a "Source" tab (which is the renamed
+ 'Edit' tab).
+
+-- CONTACT --
+
+Current maintainers:
+* Ryan Weal (Ryan Weal) - http://drupal.org/user/412402
+
diff --git a/sites/all/modules/contrib/localisation/entity_translation_tabs/entity_translation_tabs.info b/sites/all/modules/contrib/localisation/entity_translation_tabs/entity_translation_tabs.info
new file mode 100644
index 00000000..f6762961
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation_tabs/entity_translation_tabs.info
@@ -0,0 +1,14 @@
+name = Entity translation tabs
+description = Creates a translation tab for each language on nodes.
+core = 7.x
+package = Multilingual
+dependencies[] = locale
+dependencies[] = entity_translation
+files[] = entity_translation_tabs.module
+
+; Information added by Drupal.org packaging script on 2016-04-09
+version = "7.x-1.1"
+core = "7.x"
+project = "entity_translation_tabs"
+datestamp = "1460204042"
+
diff --git a/sites/all/modules/contrib/localisation/entity_translation_tabs/entity_translation_tabs.module b/sites/all/modules/contrib/localisation/entity_translation_tabs/entity_translation_tabs.module
new file mode 100644
index 00000000..98e6856a
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/entity_translation_tabs/entity_translation_tabs.module
@@ -0,0 +1,82 @@
+ $value) {
+ $items['node/%node/' . "$key"] = array(
+ 'title' => 'Edit' . ' [' . $value->name . ']',
+ 'page callback' => 'entity_translation_tabs_switcher',
+ 'page arguments' => array(1, 2),
+ 'access callback' => 'node_access',
+ 'access arguments' => array('update', 1),
+ 'weight' => 0,
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+ }
+ return $items;
+}
+
+/**
+ * Redirection logic to determine where the tabs will take you.
+ */
+function entity_translation_tabs_switcher($node, $lang) {
+
+ // This switcher currently does a drupal_goto for each appropriate case, but
+ // the intended functionality is to load the entity translation form directly
+ // on each applicable tab. It will require loading the appropraite inc files
+ // and then to load the entity_translation_edit_form with all the parameters.
+ $nid = $node->nid;
+ $und = FALSE;
+ $result = db_query(
+ "SELECT language, source FROM {entity_translation} WHERE entity_id=:eid",
+ array(
+ ':eid' => $nid,
+ ));
+ foreach ($result as $record) {
+ // Source is not set, therefore it is source.
+ if (sizeof($record->source) == 1) {
+ $source = $record->language;
+ }
+ // Source is undefined, therefore edit original.
+ if ($record->language == 'und') {
+ $und = TRUE;
+ }
+ // Translation exists, let's go there.
+ if ($record->language == $lang) {
+ drupal_goto('node/' . $nid . '/edit/' . $lang);
+ }
+ }
+ if ($und != TRUE) {
+ drupal_set_message(t('This is a new translation, please translate it now.'), 'warning');
+ drupal_goto('node/' . $nid . '/edit/add/' . $source . "/" . $lang);
+ }
+ else {
+ drupal_set_message(t('This content is set to display on all languages.
+ Set the language of this page to make it translatable.'), 'warning');
+ drupal_goto('node/' . $nid . '/edit');
+ }
+ return;
+}
diff --git a/sites/all/modules/contrib/localisation/i18n_access/LICENSE.txt b/sites/all/modules/contrib/localisation/i18n_access/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/localisation/i18n_access/entity_translation-2211649-5.patch b/sites/all/modules/contrib/localisation/i18n_access/entity_translation-2211649-5.patch
new file mode 100644
index 00000000..3df12433
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/entity_translation-2211649-5.patch
@@ -0,0 +1,288 @@
+diff --git a/i18n_access.module b/i18n_access.module
+index 5f4aa56..a76370d 100644
+--- a/i18n_access.module
++++ b/i18n_access.module
+@@ -105,18 +105,21 @@ function i18n_access_permission() {
+ function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) {
+
+ if (isset($form['language']['#options'])) {
+- // Remove inaccessible languages from the select box
+- // don't do it for admininstrators
++ // Remove inaccessible languages from the select box
++ // don't do it for administrators
+ if (!user_access('administer nodes')) {
+ $perms = i18n_access_load_permissions();
+ foreach ($form['language']['#options'] as $key => $value) {
+ $perm_key = ($key == '') ? I18N_ACCESS_LANGUAGE_NEUTRAL : $key;
+- if ($key!='en' && empty($perms[$perm_key])) {
++
++ // remove english from here, we treat english the same as any language
++ // if ($key!='en' && empty($perms[$perm_key])) {
++ if (empty($perms[$perm_key])) {
+ unset($form['language']['#options']["$key"]);
+ }
+ }
+ }
+- unset($form['#after_build']['0']);
++ // unset($form['#after_build']['0']);
+ }
+ }
+
+@@ -125,7 +128,7 @@ function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) {
+ */
+ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
+
+- //Configuring translation edit form to limit it to allowed language
++ // Configuring translation edit form to limit it to allowed language
+ if ($form_id == 'i18n_node_select_translation' && !user_access('administer nodes')) {
+
+ $perms = i18n_access_load_permissions();
+@@ -149,7 +152,7 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
+ }
+
+ // Add i18n_access things to user/edit /user/add
+- if ($form_id == 'user_register_form' || $form_id == 'user_profile_form' ) {
++ if ($form_id == 'user_register_form' || $form_id == 'user_profile_form') {
+
+ $form['i18n_access'] = array(
+ '#type' => 'fieldset',
+@@ -171,67 +174,100 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
+ *
+ * @see node_access()
+ */
+-function i18n_access_node_access($node, $op, $account = NULL) {
++function i18n_access_node_access($node, $op, $account = NULL, $langcode = NULL) {
++ // big re-work here. discarded entire original function-- replaced with our own
+ if (is_object($node)) {
+-
+- global $user;
+-
+- // If no user object is supplied, the access check is for the current user.
+- if (empty($account)) {
+- $account = $user;
++ // make sure that site administrators always have access
++ $permissions = i18n_access_load_permissions($user);
++ if (user_access('site administrator', $account)) {
++ return TRUE;
+ }
++ // if langcode is null it means the user is not accessing by translation overview, we throw access deny and allow to hard deny sneaky people and keep unpermitted tabs out of the menu system for the user
++ elseif ($langcode == NULL) {
++ global $language;
++ $langcode = $language->language;
++
++ switch ($op) {
++ case 'view':
++ return NODE_ACCESS_ALLOW;
++ break;
++ case 'update':
++ if (empty($permissions[$langcode])) {
++ return NODE_ACCESS_DENY;
++ }
++ else {
++ return NODE_ACCESS_ALLOW;
++ }
++ break;
++ case 'create':
++ if (empty($permissions[$langcode])) {
++ return NODE_ACCESS_DENY;
++ }
++ else {
++ return NODE_ACCESS_ALLOW;
++ }
++ break;
+
+- // Bypass completely if node_access returns false.
+- //TODO $access = node_access($node, $op, $account);
+-
+- /* TODO if (!$access) {
+- return FALSE;
+- } */
+-
+- // This module doesn't deal with view permissions
+- if ($op == 'view') {
+- return NODE_ACCESS_IGNORE;
++ }
+ }
+-
+- // make sure that administrators always have access
+- if (user_access('administer nodes', $account)) {
+- return TRUE;
++ //if they are accessing by translation overview, the language code gets passed by the translation overview, we send true or false here
++ else {
++ switch ($op) {
++ case 'view':
++ return TRUE;
++ break;
++ case 'update':
++ if (empty($permissions[$langcode])) {
++ return FALSE;
++ }
++ else {
++ return TRUE;
++ }
++ break;
++ case 'create':
++ if (empty($permissions[$langcode])) {
++ return FALSE;
++ }
++ else {
++ return TRUE;
++ }
++ break;
++ }
+ }
+-
+- $perms = i18n_access_load_permissions($account->uid);
+-
+- // Make sure to use the language neutral constant if node language is empty
+- $langcode = $node->language ? $node->language : I18N_ACCESS_LANGUAGE_NEUTRAL;
+-
+- //return isset($perms[$langcode]) ? (bool) $perms[$langcode] : NODE_ACCESS_DENY;
+- return isset($perms[$langcode]) ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
+ }
+ }
+
+ /**
+ * Implements hook_menu_alter().
+ */
+-function i18n_access_menu_alter(&$items) {
++
++//make function name i18n_access_node_menu_alter
++function i18n_access_node_menu_alter(&$items) {
++
++ // due to hook_module_implementation_alter calling entity translation last, we can't change the callback here, i've done it in entity_translation.node.inc - consider calling it here?
+ // Replace the translation overview page since we can't hook it.
+ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
++
+ }
+
+ function i18n_access_translation_node_overview($node) {
+
+ include_once DRUPAL_ROOT . '/includes/language.inc';
+
+- if (!empty($node->tnid)) {
+- // Already part of a set, grab that set.
+- $tnid = $node->tnid;
+- $translations = translation_node_get_translations($node->tnid);
+- }
+- else {
+- // We have no translation source nid, this could be a new set, emulate that.
+- $tnid = $node->nid;
+- $translations = array($node->language => $node);
++ // include functions from i18n_node.pages.inc
++ include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'i18n_node') . '/i18n_node.pages.inc';
++
++ // this is the part where this thing sorts out how to build a list of existing translations for this node
++ // since we use entity translation, the tnid isn't what we're using to build the translation list. we're using node->translations->data[keys]
++ $available_translations = $node->translations->data;
++
++ // iterate over each available translation and add its key (which is the 2 letter language code) to the array we call $translations with the node object as the value
++ foreach ($available_translations as $key => $value) {
++ $translations[$key] = $node;
+ }
+
+ $type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE);
++
+ $header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
+
+ //added from i18n/i18n_node/i18n_node.pages.inc function
+@@ -240,9 +276,9 @@ function i18n_access_translation_node_overview($node) {
+ $perms = i18n_access_load_permissions($account->uid);
+ //end
+
+-
+ // Modes have different allowed languages
+ foreach (i18n_node_language_list($node) as $langcode => $language_name) {
++
+ if ($langcode == LANGUAGE_NONE) {
+ // Never show language neutral on the overview.
+ continue;
+@@ -253,16 +289,24 @@ function i18n_access_translation_node_overview($node) {
+ // We load the full node to check whether the user can edit it.
+ $translation_node = node_load($translations[$langcode]->nid);
+ $path = 'node/' . $translation_node->nid;
+- $title = i18n_node_translation_link($translation_node->title, $path, $langcode);
+- if (node_access('update', $translation_node)) {
++
++ // Account for title field module:
++ if (isset($translation_node->title_field) && isset($translation_node->title_field[$langcode])) {
++ $title = i18n_node_translation_link($translation_node->title_field[$langcode][0]['value'], $path, $langcode);
++ }
++ else {
++ $title = i18n_node_translation_link($translation_node->title, $path, $langcode);
++ }
++
++ if (i18n_access_node_access($translation_node, 'update', $user, $langcode)) {
+ $text = t('edit');
+ $path = 'node/' . $translation_node->nid . '/edit';
+ $options[] = i18n_node_translation_link($text, $path, $langcode);
+ }
+ $status = $translation_node->status ? t('Published') : t('Not published');
+- $status .= $translation_node->translate ? ' - ' . t('outdated') . '' : '';
++ $status .= $translation_node->translate ? ' - ' . t('outdated') . '' : '';
+ if ($translation_node->nid == $tnid) {
+- $language_name = t('@language_name (source)', array('@language_name' => $language_name));
++ $language_name = t('@language_name (source)', array('@language_name' => $language_name));
+ }
+ }
+ else {
+@@ -316,6 +360,52 @@ function i18n_access_menu() {
+ }
+
+ /**
++ * Node-specific menu alterations.
++ */
++function i18n_access_menu_alter(&$items, $backup) {
++ if (isset($backup['node'])) {
++ $item = $backup['node'];
++ // Preserve the menu router item defined by other modules.
++ $callback['page callback'] = $item['page callback'];
++ $callback['file'] = $item['file'];
++ $callback['module'] = $item['module'];
++ $access_arguments = array_merge(array(1, $item['access callback']), $item['access arguments']);
++ }
++ else {
++ $access_arguments = array(1);
++ }
++
++ // Point the 'translate' tab to point to the i18n_access version of the translation overview page
++ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
++
++ // There are 3 page arguments for the entity translation overview, only one for i18n_access:
++ $items['node/%node/translate']['page arguments'] = array(1);
++
++ // Pass in the i18n_access permissions
++ $items['node/%node/translate']['access arguments'] = $access_arguments;
++
++ // Point to i18n_access's include for the callback
++ $items['node/%node/translate']['file'] = 'i18n_access.module';
++
++ // Point to i18n_access module
++ $items['node/%node/translate']['module'] = 'i18n_access';
++}
++
++/**
++ * Implements hook_module_implements_alter().
++ */
++function i18n_access_module_implements_alter(&$implementations, $hook) {
++ switch ($hook) {
++ case 'menu_alter':
++ // Move our hook_menu_alter implementation to the end of the list.
++ $group = $implementations['i18n_access'];
++ unset($implementations['i18n_access']);
++ $implementations['i18n_access'] = $group;
++ break;
++ }
++}
++
++/**
+ * Admin settings form
+ */
+ function i18n_access_admin_settings() {
+@@ -330,4 +420,4 @@ function i18n_access_admin_settings() {
+ );
+
+ return system_settings_form($form);
+-}
+\ No newline at end of file
++}
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.2298475-6.patch b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.2298475-6.patch
new file mode 100644
index 00000000..72182da1
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.2298475-6.patch
@@ -0,0 +1,619 @@
+diff --git a/i18n_access.info b/i18n_access.info
+index e19050a..074cec2 100644
+--- a/i18n_access.info
++++ b/i18n_access.info
+@@ -2,6 +2,9 @@ name = Translation Access
+ description = Control access to creating content in different languages.
+ package = Multilanguage
+ core = 7.x
++configure = admin/config/regional/language/access
+
+ dependencies[] = locale
+ dependencies[] = translation
++dependencies[] = i18n_node
++files[] = i18n_access.test
+diff --git a/i18n_access.install b/i18n_access.install
+index 24aefb4..95c935c 100644
+--- a/i18n_access.install
++++ b/i18n_access.install
+@@ -42,4 +42,5 @@ function i18n_access_install() {
+ * Implements hook_uninstall().
+ */
+ function i18n_access_uninstall() {
++ variable_del('i18n_access_languages');
+ }
+diff --git a/i18n_access.module b/i18n_access.module
+index 5f4aa56..3b68458 100644
+--- a/i18n_access.module
++++ b/i18n_access.module
+@@ -2,32 +2,14 @@
+
+ /**
+ * @file
+- * file_description
++ * i18n_access.module
+ */
+
+-define('I18N_ACCESS_LANGUAGE_NEUTRAL', 'NEUTRAL');
+-
+ /**
+ * Implements hook_user_insert().
+ */
+ function i18n_access_user_insert(&$edit, &$account, $category = NULL) {
+- if ($category == 'account') {
+- // see user_admin_perm_submit()
+- if (isset($edit['i18n_access'])) {
+- db_delete('i18n_access')
+- ->condition('uid', $account->uid)
+- ->execute();
+- $edit['i18n_access'] = array_filter($edit['i18n_access']);
+- if (count($edit['i18n_access'])) {
+- db_insert('i18n_access')
+- ->fields(array(
+- 'uid' => $account->uid,
+- 'perm' => implode(', ', array_keys($edit['i18n_access'])),
+- ))->execute();
+- }
+- unset($edit['i18n_access']);
+- }
+- }
++ i18n_access_user_update($edit, $account, $category);
+ }
+
+ /**
+@@ -54,10 +36,19 @@ function i18n_access_user_update(&$edit, &$account, $category = NULL) {
+ }
+
+ /**
++ * Implements hook_user_delete().
++ */
++function i18n_access_user_delete($account) {
++ db_delete('i18n_access')
++ ->condition('uid', $account->uid)
++ ->execute();
++}
++
++/**
+ * Load the language permissions for a given user
+ */
+ function i18n_access_load_permissions($uid = NULL) {
+- static $perms = array();
++ $perms = &drupal_static(__FUNCTION__);
+
+ // use the global user id if none is passed
+ if (!isset($uid)) {
+@@ -94,7 +85,8 @@ function i18n_access_permission() {
+ return array(
+ 'access selected languages' => array(
+ 'title' => t('Access selected languages'),
+- 'description' => t('access selected languages.'),
++ 'description' => t('This permission gives this role edit/delete access to all content which are in the selected language. View/create access needs a different access level.', array('!url' => url('admin/config/regional/language/access'))),
++ 'restrict access' => TRUE,
+ ),
+ );
+ }
+@@ -102,34 +94,39 @@ function i18n_access_permission() {
+ /**
+ * Implements hook_form_node_form_alter().
+ */
+-function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) {
+-
+- if (isset($form['language']['#options'])) {
+- // Remove inaccessible languages from the select box
+- // don't do it for admininstrators
+- if (!user_access('administer nodes')) {
+- $perms = i18n_access_load_permissions();
+- foreach ($form['language']['#options'] as $key => $value) {
+- $perm_key = ($key == '') ? I18N_ACCESS_LANGUAGE_NEUTRAL : $key;
+- if ($key!='en' && empty($perms[$perm_key])) {
+- unset($form['language']['#options']["$key"]);
+- }
++function i18n_access_form_node_form_alter(&$form) {
++ $form['#after_build'][] = '_i18n_access_form_node_form_alter';
++}
++
++/**
++ * Unset's languages from language options if user does not have permission to
++ * use.
++ *
++ * @param $form
++ * @param $form_state
++ * @return mixed
++ */
++function _i18n_access_form_node_form_alter($form, &$form_state) {
++ if (isset($form['language']['#options']) && !user_access('bypass node access')) {
++ $perms = i18n_access_load_permissions();
++ foreach ($form['language']['#options'] as $key => $value) {
++ if (empty($perms[$key])) {
++ unset($form['language']['#options'][$key]);
+ }
+ }
+- unset($form['#after_build']['0']);
+ }
++
++ return $form;
+ }
+
+ /**
+ * Implements hook_form_alter().
+ */
+ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
+-
+ //Configuring translation edit form to limit it to allowed language
+- if ($form_id == 'i18n_node_select_translation' && !user_access('administer nodes')) {
++ if ($form_id == 'i18n_node_select_translation' && !user_access('bypass node access')) {
+
+ $perms = i18n_access_load_permissions();
+-
+ foreach ($form['translations']['nid'] as $language => $translation) {
+ if (!isset($perms[$language]) && $language != '#tree') {
+ unset($form['translations']['nid'][$language]);
+@@ -159,17 +156,15 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
+ );
+ $form['i18n_access']['i18n_access'] = array(
+ '#type' => 'checkboxes',
+- '#options' => array(I18N_ACCESS_LANGUAGE_NEUTRAL => t('Language neutral')) + locale_language_list('name'),
++ '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
+ '#default_value' => i18n_access_load_permissions($form['#user']->uid),
+- '#description' => t('Select the languages that this user should have permission to create and edit content for.'),
++ '#description' => t('The user get edit, delete access to all content which are in this enabled languages. Create, view access needs a different access level.'),
+ );
+ }
+ }
+
+ /**
+- * Wrapper around node_access() with additional checks for language permissions.
+- *
+- * @see node_access()
++ * Implements hook_node_access().
+ */
+ function i18n_access_node_access($node, $op, $account = NULL) {
+ if (is_object($node)) {
+@@ -181,29 +176,16 @@ function i18n_access_node_access($node, $op, $account = NULL) {
+ $account = $user;
+ }
+
+- // Bypass completely if node_access returns false.
+- //TODO $access = node_access($node, $op, $account);
+-
+- /* TODO if (!$access) {
+- return FALSE;
+- } */
+-
+ // This module doesn't deal with view permissions
+ if ($op == 'view') {
+ return NODE_ACCESS_IGNORE;
+ }
+
+- // make sure that administrators always have access
+- if (user_access('administer nodes', $account)) {
+- return TRUE;
+- }
+-
+ $perms = i18n_access_load_permissions($account->uid);
+
+ // Make sure to use the language neutral constant if node language is empty
+- $langcode = $node->language ? $node->language : I18N_ACCESS_LANGUAGE_NEUTRAL;
++ $langcode = $node->language ? $node->language : LANGUAGE_NONE;
+
+- //return isset($perms[$langcode]) ? (bool) $perms[$langcode] : NODE_ACCESS_DENY;
+ return isset($perms[$langcode]) ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
+ }
+ }
+@@ -212,14 +194,26 @@ function i18n_access_node_access($node, $op, $account = NULL) {
+ * Implements hook_menu_alter().
+ */
+ function i18n_access_menu_alter(&$items) {
+- // Replace the translation overview page since we can't hook it.
+- $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
++ if (isset($items['node/%node/translate'])) {
++ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
++ }
+ }
+
++/**
++ * Most logic comes from translation/i18n_node module.
++ *
++ * We removes here only the "add translation" links for languages which are not your selected language.
++ *
++ * @see translation_node_overview
++ * @see i18n_node_translation_overview
++ *
++ * @param object $node
++ *
++ * @return array.
++ */
+ function i18n_access_translation_node_overview($node) {
+
+ include_once DRUPAL_ROOT . '/includes/language.inc';
+-
+ if (!empty($node->tnid)) {
+ // Already part of a set, grab that set.
+ $tnid = $node->tnid;
+@@ -231,16 +225,12 @@ function i18n_access_translation_node_overview($node) {
+ $translations = array($node->language => $node);
+ }
+
+- $type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE);
+ $header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
+-
+- //added from i18n/i18n_node/i18n_node.pages.inc function
++ $rows = array();
+ global $user;
+- $account = $user;
+- $perms = i18n_access_load_permissions($account->uid);
++ $perms = i18n_access_load_permissions($user->uid);
+ //end
+
+-
+ // Modes have different allowed languages
+ foreach (i18n_node_language_list($node) as $langcode => $language_name) {
+ if ($langcode == LANGUAGE_NONE) {
+@@ -268,15 +258,11 @@ function i18n_access_translation_node_overview($node) {
+ else {
+ // No such translation in the set yet: help user to create it.
+ $title = t('n/a');
+- if (node_access('create', $node)) {
++ if (node_access('create', $node->type) && (!empty($perms[$langcode]) || user_access('bypass node access'))) {
+ $text = t('add translation');
+ $path = 'node/add/' . str_replace('_', '-', $node->type);
+ $query = array('query' => array('translation' => $node->nid, 'target' => $langcode));
+-
+- //condition added from i18n/i18n_node/i18n_node.pages.inc
+- if (in_array($langcode, $perms)) {
+- $options[] = i18n_node_translation_link($text, $path, $langcode, $query);
+- }
++ $options[] = i18n_node_translation_link($text, $path, $langcode, $query);
+ }
+ $status = t('Not translated');
+ }
+@@ -301,9 +287,7 @@ function i18n_access_translation_node_overview($node) {
+ * Implements hook_menu().
+ */
+ function i18n_access_menu() {
+- $items = array();
+-
+- $items['admin/settings/language/access'] = array(
++ $items['admin/config/regional/language/access'] = array(
+ 'title' => 'Access',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('i18n_access_admin_settings'),
+@@ -311,23 +295,20 @@ function i18n_access_menu() {
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 10,
+ );
+-
+ return $items;
+ }
+
+ /**
+- * Admin settings form
++ * Admin settings form.
+ */
+-function i18n_access_admin_settings() {
+-
++function i18n_access_admin_settings($form) {
+ $form['i18n_access_languages'] = array(
+ '#title' => t('Select the default access languages'),
+ '#type' => 'select',
+- '#multiple' => 'true',
+- '#options' => array(I18N_ACCESS_LANGUAGE_NEUTRAL => t('Language neutral')) + locale_language_list('name'),
++ '#multiple' => TRUE,
++ '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
+ '#default_value' => variable_get('i18n_access_languages', array()),
+ '#description' => t("This selection of languages will be connected with the 'access selected languages' permission which you can use to grant a role access to these languages at once.")
+ );
+-
+ return system_settings_form($form);
+-}
+\ No newline at end of file
++}
+diff --git a/i18n_access.test b/i18n_access.test
+index d32dbf4..5b2f443 100644
+--- a/i18n_access.test
++++ b/i18n_access.test
+@@ -6,10 +6,17 @@
+ */
+
+ class i18nAccessTestCase extends DrupalWebTestCase {
++
++ protected $admin_user;
++
++ protected $translator;
++
++ protected $visitor;
++
+ /**
+ * Implementation of getInfo().
+ */
+- function getInfo() {
++ public static function getInfo() {
+ return array(
+ 'name' => t('Translation Access'),
+ 'description' => t('Test suite for the i18n_access module.'),
+@@ -20,22 +27,21 @@ class i18nAccessTestCase extends DrupalWebTestCase {
+ /**
+ * Implementation of setUp().
+ */
+- function setUp() {
+- parent::setUp('locale', 'translation', 'i18n_access');
++ public function setUp() {
++ parent::setUp(array('locale', 'translation', 'i18n_access', 'i18n_node'));
+
+- $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer nodes', 'administer users'));
+- $this->translator = $this->drupalCreateUser(array('create story content', 'edit own story content', 'translate content'));
++ $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer users', 'bypass node access', 'translate content'));
++ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
+ $this->visitor = $this->drupalCreateUser(array('access content'));
+ $this->drupalLogin($this->admin_user);
++
+ $this->addLanguage('fr');
+ $this->addLanguage('de');
+- $this->setLanguagePermissions($this->translator, array('en', 'fr'));
+
+ // Set Story content type to use multilingual support with translation.
+- $edit = array();
+ $edit['language_content_type'] = 2;
+- $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type'));
+- $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Story')), t('Story content type has been updated.'));
++ $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
++ $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Story content type has been updated.');
+
+ }
+
+@@ -47,26 +53,27 @@ class i18nAccessTestCase extends DrupalWebTestCase {
+ */
+ function addLanguage($language_code) {
+ // Check to make sure that language has not already been installed.
+- $this->drupalGet('admin/settings/language');
++ $this->drupalGet('admin/config/regional/language');
+
+ if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) {
+ // Doesn't have language installed so add it.
+ $edit = array();
+ $edit['langcode'] = $language_code;
+- $this->drupalPost('admin/settings/language/add', $edit, t('Add language'));
++ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+
+- $languages = language_list('language', TRUE); // Make sure not using cached version.
+- $this->assertTrue(array_key_exists($language_code, $languages), t('Language was installed successfully.'));
++ drupal_static_reset('language_list'); // Make sure not using cached version.
++ $languages = language_list('language');
++ $this->assertTrue(array_key_exists($language_code, $languages), 'Language was installed successfully.');
+
+ if (array_key_exists($language_code, $languages)) {
+- $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), t('Language has been created.'));
++ $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), 'Language has been created.');
+ }
+ }
+ else {
+ // Ensure that it is enabled.
+ $this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration'));
+
+- $this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.'));
++ $this->assertRaw(t('Configuration saved.'), 'Language successfully enabled.');
+ }
+ }
+
+@@ -80,8 +87,9 @@ class i18nAccessTestCase extends DrupalWebTestCase {
+ * An array of language codes to give permission for
+ */
+ function setLanguagePermissions($account, $languages = array()) {
+- $this->assertTrue(user_access('administer users'), t('User has permission to administer users'));
+-
++ $this->assertTrue(user_access('administer users'), 'User has permission to administer users');
++ $expected = array();
++ $edit = array();
+ foreach ($languages as $langcode) {
+ $key = 'i18n_access[' . $langcode . ']';
+ $edit[$key] = $langcode;
+@@ -90,7 +98,31 @@ class i18nAccessTestCase extends DrupalWebTestCase {
+ $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
+
+ $actual = i18n_access_load_permissions($account->uid);
+- $this->assertEqual($expected, $actual, t('Language permissions set correctly.'), 'i18n_access');
++ $this->assertEqual($expected, $actual, 'Language permissions set correctly.', 'i18n_access');
++ }
++
++ /**
++ * Unsets the language permission for the specified user. Must be logged in as
++ * an 'administer users' privileged user before calling this.
++ *
++ * @param $account
++ * The user account to modify
++ * @param $languages
++ * An array of language codes to remove permission for
++ */
++ function unsetLanguagePermissions($account, $languages = array()) {
++ $this->assertTrue(user_access('administer users'), 'User has permission to administer users');
++ $expected = array();
++ $edit = array();
++ foreach ($languages as $langcode) {
++ $key = 'i18n_access[' . $langcode . ']';
++ $edit[$key] = FALSE;
++ }
++ $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
++ drupal_static_reset('i18n_access_load_permissions');
++ drupal_static_reset('node_access');
++ $actual = i18n_access_load_permissions($account->uid);
++ $this->assertEqual($expected, $actual, 'Language permissions unset correctly.', 'i18n_access');
+ }
+
+ /**
+@@ -109,7 +141,6 @@ class i18nAccessTestCase extends DrupalWebTestCase {
+ function assertLanguageOption($langcode, $message, $group = 'Other') {
+ $xpath = '//select[@name="language"]/option';
+ $fields = $this->xpath($xpath);
+-
+ // If value specified then check array for match.
+ $found = TRUE;
+ if (isset($langcode)) {
+@@ -157,52 +188,145 @@ class i18nAccessTestCase extends DrupalWebTestCase {
+ return $this->assertFalse($fields && $found, $message, $group);
+ }
+
+- function dsm($object) {
+- $this->error('
' . check_plain(print_r($object, 1)) . '
');
+- }
+-
+ /**
+- * Test translator user. User with 'create story content' and 'edit own story
+- * content' permissions should be able to create and edit story nodes only in
++ * Test translator user. User with 'create article content' permission
++ * should be able to create and edit article nodes only in/for
+ * the languages that they have permissions for.
+ */
+ function testTranslatorUser() {
++ $this->_testTranslatorNodeAccess();
++ $this->_testTranslatorNodeAccess(TRUE);
++ }
++
++
++ function _testTranslatorNodeAccess($via_role = FALSE) {
++ $this->drupalLogin($this->admin_user);
++ if (!$via_role) {
++ $this->setLanguagePermissions($this->translator, array('en', 'fr'));
++ }
++ else{
++ $edit = array(
++ 'i18n_access_languages[]' => array('en', 'fr'),
++ );
++ $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
++
++ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
++ }
++
+ $this->drupalLogin($this->translator);
+
+- $this->drupalGet('node/add/story');
+- $this->assertField('language', t('Found language selector.'));
++ $this->drupalGet('node/add/article');
++ $this->assertField('language', 'Found language selector.');
+
+ $perms = i18n_access_load_permissions($this->translator->uid);
+ $languages = language_list();
+- $languages[I18N_ACCESS_LANGUAGE_NEUTRAL] = (object)array('language' => '', 'name' => 'Language Neutral');
++ $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
+ foreach ($languages as $key => $language) {
+ // TODO: Add in check for language neutral
+ if (isset($perms[$key]) && $perms[$key]) {
+- $this->assertLanguageOption($language->language, t('Option found for %language in language selector.', array('%language' => $language->name)));
++ $this->assertLanguageOption($language->language, format_string('Option found for %language in language selector.', array('%language' => $language->name)));
+ }
+ else {
+- $this->assertNoLanguageOption($language->language, t('Option not found for %language in language selector.', array('%language' => $language->name)));
++ $this->assertNoLanguageOption($language->language, format_string('Option not found for %language in language selector.', array('%language' => $language->name)));
+ }
+ }
+- }
++ $this->drupalLogin($this->admin_user);
++ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
++
++ $this->drupalLogin($this->translator);
++ $this->assertFalse(node_access('update', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/edit');
++ $this->assertResponse(403);
++
++ $this->assertFalse(node_access('delete', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/delete');
++ $this->assertResponse(403);
++
++ $this->drupalLogin($this->admin_user);
++ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'fr', 'body' => array('fr' => array(array()))));
++
++ $this->drupalLogin($this->translator);
++ $this->assertTrue(node_access('update', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/edit');
++ $this->assertResponse(200);
++
++ $this->assertTrue(node_access('delete', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/delete');
++ $this->assertResponse(200);
++
++ $this->drupalGet('node/' . $node->nid . '/translate');
++ $query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
++ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
++ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
++ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
++ $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
++
++ $this->drupalLogin($this->admin_user);
++ if (!$via_role) {
++ $this->unsetLanguagePermissions($this->translator, array('fr', 'en'));
++ }
++ else{
++ $edit = array(
++ 'i18n_access_languages[]' => array(),
++ );
++ $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
++ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
++ drupal_static_reset('i18n_access_load_permissions');
++ drupal_static_reset('node_access');
++ }
++
++ $this->drupalLogin($this->translator);
++ $this->assertFalse(node_access('update', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/edit');
++ $this->assertResponse(403);
++
++ $this->assertFalse(node_access('delete', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/delete');
++ $this->assertResponse(403);
++
++ $this->drupalGet('node/' . $node->nid . '/translate');
++ $query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
++ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
++ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
++ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
++ $this->assertNoRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
++
++ }
+
+ /**
+- * Test admin user. User with 'administer nodes' permission should be able to
+- * create and edit nodes regardless of the language
++ * Test admin user. User with 'bypass node access' permission should be able to
++ * update, delete nodes regardless of the language.
+ */
+ function testAdminUser() {
+ $this->drupalLogin($this->admin_user);
++ $this->drupalGet('node/add/article');
++ $this->assertField('language', 'Found language selector.');
+
+- $this->drupalGet('node/add/story');
+- $this->assertField('language', t('Found language selector.'));
+-
+- $perms = i18n_access_load_permissions($this->admin_user->uid);
+ $languages = language_list();
+- $languages[I18N_ACCESS_LANGUAGE_NEUTRAL] = (object)array('language' => '', 'name' => 'Language Neutral');
++ $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
+ foreach ($languages as $language) {
+- // TODO: Add in check for language neutral
+- $this->assertLanguageOption($language->language, t('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name)));
++ $this->assertLanguageOption($language->language, format_string('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name)));
+ }
++ $this->drupalLogin($this->translator);
++ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
++
++ $this->drupalLogin($this->admin_user);
++
++ $this->assertTrue(node_access('update', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/edit');
++ $this->assertResponse(200);
++
++ $this->assertTrue(node_access('delete', $node, $this->loggedInUser));
++ $this->drupalGet('node/' . $node->nid . '/delete');
++ $this->assertResponse(200);
++
++ $this->drupalGet('node/' . $node->nid . '/translate');
++
++ $query = array('query' => array('translation' => $node->nid, 'target' => 'fr'));
++ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'fr', $query));
++ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
++ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
++ $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'de'));
+ }
+
+-}
+\ No newline at end of file
++}
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info
new file mode 100644
index 00000000..3a188657
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info
@@ -0,0 +1,14 @@
+name = Translation Access
+description = Control access to creating content in different languages.
+package = Multilanguage
+core = 7.x
+
+dependencies[] = locale
+dependencies[] = translation
+
+; Information added by drupal.org packaging script on 2013-09-30
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "i18n_access"
+datestamp = "1380582441"
+
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info.orig b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info.orig
new file mode 100644
index 00000000..3a188657
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info.orig
@@ -0,0 +1,14 @@
+name = Translation Access
+description = Control access to creating content in different languages.
+package = Multilanguage
+core = 7.x
+
+dependencies[] = locale
+dependencies[] = translation
+
+; Information added by drupal.org packaging script on 2013-09-30
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "i18n_access"
+datestamp = "1380582441"
+
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info.rej b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info.rej
new file mode 100644
index 00000000..c1ad21cb
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.info.rej
@@ -0,0 +1,12 @@
+--- i18n_access.info
++++ i18n_access.info
+@@ -2,6 +2,9 @@ name = Translation Access
+ description = Control access to creating content in different languages.
+ package = Multilanguage
+ core = 7.x
++configure = admin/config/regional/language/access
+
+ dependencies[] = locale
+ dependencies[] = translation
++dependencies[] = i18n_node
++files[] = i18n_access.test
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.install b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.install
new file mode 100644
index 00000000..95c935cd
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.install
@@ -0,0 +1,46 @@
+ 'Store language permissions per user',
+ 'fields' => array(
+ 'uid' => array(
+ 'description' => 'The primary identifier for a user.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'perm' => array(
+ 'description' => 'List of languages that the user has permission for.',
+ 'type' => 'text',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ ),
+ ),
+ 'primary key' => array('uid'),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function i18n_access_install() {
+ // Set module weight for it to run after core and i18n modules
+ db_query("UPDATE {system} SET weight = 20 WHERE name = 'i18n_access' AND type = 'module'");
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function i18n_access_uninstall() {
+ variable_del('i18n_access_languages');
+}
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.module b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.module
new file mode 100644
index 00000000..cd96e8a6
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.module
@@ -0,0 +1,405 @@
+condition('uid', $account->uid)
+ ->execute();
+ $edit['i18n_access'] = array_filter($edit['i18n_access']);
+ if (count($edit['i18n_access'])) {
+ db_insert('i18n_access')
+ ->fields(array(
+ 'uid' => $account->uid,
+ 'perm' => implode(', ', array_keys($edit['i18n_access'])),
+ ))->execute();
+ }
+ unset($edit['i18n_access']);
+ }
+ }
+}
+
+/**
+ * Implements hook_user_delete().
+ */
+function i18n_access_user_delete($account) {
+ db_delete('i18n_access')
+ ->condition('uid', $account->uid)
+ ->execute();
+}
+
+/**
+ * Load the language permissions for a given user
+ */
+function i18n_access_load_permissions($uid = NULL) {
+ $perms = &drupal_static(__FUNCTION__);
+
+ // use the global user id if none is passed
+ if (!isset($uid)) {
+ $uid = $GLOBALS['user']->uid;
+ $account = NULL;
+ }
+ else {
+ $account = user_load($uid);
+ }
+
+ if (!isset($perms[$uid])) {
+ $perm_string = db_query('SELECT perm FROM {i18n_access} WHERE uid = :uid', array(':uid' => $uid))->fetchField();
+
+ if ($perm_string) {
+ $perms[$uid] = drupal_map_assoc(explode(', ', $perm_string));
+ }
+ else {
+ $perms[$uid] = array();
+ }
+ }
+
+ // adding the default languages if permission has been granted
+ if (user_access('access selected languages', $account)) {
+ $perms[$uid] = array_merge($perms[$uid], drupal_map_assoc(variable_get('i18n_access_languages', array())));
+ }
+
+ return $perms[$uid];
+}
+
+/**
+ * Implements hook_permission().
+ */
+function i18n_access_permission() {
+ return array(
+ 'access selected languages' => array(
+ 'title' => t('Access selected languages'),
+ 'description' => t('This permission gives this role edit/delete access to all content which are in the selected language. View/create access needs a different access level.', array('!url' => url('admin/config/regional/language/access'))),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_form_node_form_alter().
+ */
+function i18n_access_form_node_form_alter(&$form) {
+ $form['#after_build'][] = '_i18n_access_form_node_form_alter';
+}
+
+/**
+ * Unset's languages from language options if user does not have permission to
+ * use.
+ *
+ * @param $form
+ * @param $form_state
+ * @return mixed
+ */
+function _i18n_access_form_node_form_alter($form, &$form_state) {
+ if (isset($form['language']['#options']) && !user_access('bypass node access')) {
+ $perms = i18n_access_load_permissions();
+ foreach ($form['language']['#options'] as $key => $value) {
+ if (empty($perms[$key])) {
+ unset($form['language']['#options'][$key]);
+ }
+ }
+ }
+
+ return $form;
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function i18n_access_form_alter(&$form, &$form_state, $form_id) {
+ //Configuring translation edit form to limit it to allowed language
+ if ($form_id == 'i18n_node_select_translation' && !user_access('bypass node access')) {
+
+ $perms = i18n_access_load_permissions();
+ foreach ($form['translations']['nid'] as $language => $translation) {
+ if (!isset($perms[$language]) && $language != '#tree') {
+ unset($form['translations']['nid'][$language]);
+ }
+ }
+ foreach ($form['translations']['language'] as $language => $translation) {
+ if (!isset($perms[$language]) && $language != '#tree') {
+ unset($form['translations']['language'][$language]);
+ }
+ }
+ foreach ($form['translations']['node'] as $language => $translation) {
+ if (!isset($perms[$language]) && $language != '#tree') {
+ unset($form['translations']['node'][$language]);
+ }
+ }
+
+ }
+
+ // Add i18n_access things to user/edit /user/add
+ if ($form_id == 'user_register_form' || $form_id == 'user_profile_form' ) {
+
+ $form['i18n_access'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Translation access'),
+ '#tree' => 0,
+ '#access' => user_access('administer users'),
+ );
+ $form['i18n_access']['i18n_access'] = array(
+ '#type' => 'checkboxes',
+ '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
+ '#default_value' => i18n_access_load_permissions($form['#user']->uid),
+ '#description' => t('The user get edit, delete access to all content which are in this enabled languages. Create, view access needs a different access level.'),
+ );
+ }
+}
+
+/**
+ * Implements hook_node_access().
+ */
+function i18n_access_node_access($node, $op, $account = NULL, $langcode = NULL) {
+ // big re-work here. discarded entire original function-- replaced with our own
+ if (is_object($node)) {
+ // make sure that site administrators always have access
+ $permissions = i18n_access_load_permissions($user);
+ if (user_access('site administrator', $account)) {
+ return TRUE;
+ }
+ // if langcode is null it means the user is not accessing by translation overview, we throw access deny and allow to hard deny sneaky people and keep unpermitted tabs out of the menu system for the user
+ elseif ($langcode == NULL) {
+ global $language;
+ $langcode = $language->language;
+
+ switch ($op) {
+ case 'view':
+ return NODE_ACCESS_ALLOW;
+ break;
+ case 'update':
+ if (empty($permissions[$langcode])) {
+ return NODE_ACCESS_DENY;
+ }
+ else {
+ return NODE_ACCESS_ALLOW;
+ }
+ break;
+ case 'create':
+ if (empty($permissions[$langcode])) {
+ return NODE_ACCESS_DENY;
+ }
+ else {
+ return NODE_ACCESS_ALLOW;
+ }
+ break;
+ }
+ }
+ //if they are accessing by translation overview, the language code gets passed by the translation overview, we send true or false here
+ else {
+ switch ($op) {
+ case 'view':
+ return TRUE;
+ break;
+ case 'update':
+ if (empty($permissions[$langcode])) {
+ return FALSE;
+ }
+ else {
+ return TRUE;
+ }
+ break;
+ case 'create':
+ if (empty($permissions[$langcode])) {
+ return FALSE;
+ }
+ else {
+ return TRUE;
+ }
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_menu_alter().
+ */
+function i18n_access_node_menu_alter(&$items) {
+ // due to hook_module_implementation_alter calling entity translation last, we can't change the callback here, i've done it in entity_translation.node.inc - consider calling it here?
+ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
+}
+
+/**
+ * Most logic comes from translation/i18n_node module.
+ *
+ * We removes here only the "add translation" links for languages which are not your selected language.
+ *
+ * @see translation_node_overview
+ * @see i18n_node_translation_overview
+ *
+ * @param object $node
+ *
+ * @return array.
+ */
+function i18n_access_translation_node_overview($node) {
+
+ include_once DRUPAL_ROOT . '/includes/language.inc';
+
+ // include functions from i18n_node.pages.inc
+ include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'i18n_node') . '/i18n_node.pages.inc';
+
+ // this is the part where this thing sorts out how to build a list of existing translations for this node
+ // since we use entity translation, the tnid isn't what we're using to build the translation list. we're using node->translations->data[keys]
+ $available_translations = $node->translations->data;
+ // iterate over each available translation and add its key (which is the 2 letter language code) to the array we call $translations with the node object as the value
+ foreach ($available_translations as $key => $value) {
+ $translations[$key] = $node;
+ }
+
+ $header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
+ $rows = array();
+ global $user;
+ $perms = i18n_access_load_permissions($user->uid);
+ //end
+
+ // Modes have different allowed languages
+ foreach (i18n_node_language_list($node) as $langcode => $language_name) {
+ if ($langcode == LANGUAGE_NONE) {
+ // Never show language neutral on the overview.
+ continue;
+ }
+ $options = array();
+ if (isset($translations[$langcode])) {
+ // Existing translation in the translation set: display status.
+ // We load the full node to check whether the user can edit it.
+ $translation_node = node_load($translations[$langcode]->nid);
+ $path = 'node/' . $translation_node->nid;
+
+ // Account for title field module:
+ if (isset($translation_node->title_field) && isset($translation_node->title_field[$langcode])) {
+ $title = i18n_node_translation_link($translation_node->title_field[$langcode][0]['value'], $path, $langcode);
+ }
+ else {
+ $title = i18n_node_translation_link($translation_node->title, $path, $langcode);
+ }
+ if (i18n_access_node_access($translation_node, 'update', $user, $langcode)) {
+ $text = t('edit');
+ $path = 'node/' . $translation_node->nid . '/edit';
+ $options[] = i18n_node_translation_link($text, $path, $langcode);
+ }
+ $status = $translation_node->status ? t('Published') : t('Not published');
+ $status .= $translation_node->translate ? ' - ' . t('outdated') . '' : '';
+ if ($translation_node->nid == $tnid) {
+ $language_name = t('@language_name (source)', array('@language_name' => $language_name));
+ }
+ }
+ else {
+ // No such translation in the set yet: help user to create it.
+ $title = t('n/a');
+ if (node_access('create', $node->type) && (!empty($perms[$langcode]) || user_access('bypass node access'))) {
+ $text = t('add translation');
+ $path = 'node/add/' . str_replace('_', '-', $node->type);
+ $query = array('query' => array('translation' => $node->nid, 'target' => $langcode));
+ $options[] = i18n_node_translation_link($text, $path, $langcode, $query);
+ }
+ $status = t('Not translated');
+ }
+ $rows[] = array($language_name, $title, $status, implode(" | ", $options));
+ }
+
+ drupal_set_title(t('Translations of %title', array('%title' => $node->title)), PASS_THROUGH);
+
+ $build['translation_node_overview'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ );
+
+ if (user_access('administer content translations')) {
+ $build['translation_node_select'] = drupal_get_form('i18n_node_select_translation', $node, $translations);
+ }
+ return $build;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function i18n_access_menu() {
+ $items['admin/config/regional/language/access'] = array(
+ 'title' => 'Access',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('i18n_access_admin_settings'),
+ 'access arguments' => array('administer site configuration'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 10,
+ );
+ return $items;
+}
+
+/**
+* Node-specific menu alterations.
+*/
+function i18n_access_menu_alter(&$items, $backup) {
+ if (isset($backup['node'])) {
+ $item = $backup['node'];
+ // Preserve the menu router item defined by other modules.
+ $callback['page callback'] = $item['page callback'];
+ $callback['file'] = $item['file'];
+ $callback['module'] = $item['module'];
+ $access_arguments = array_merge(array(1, $item['access callback']), $item['access arguments']);
+ }
+ else {
+ $access_arguments = array(1);
+ }
+
+ // Point the 'translate' tab to point to the i18n_access version of the translation overview page
+ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
+
+ // There are 3 page arguments for the entity translation overview, only one for i18n_access:
+ $items['node/%node/translate']['page arguments'] = array(1);
+
+ // Pass in the i18n_access permissions
+ $items['node/%node/translate']['access arguments'] = $access_arguments;
+
+ // Point to i18n_access's include for the callback
+ $items['node/%node/translate']['file'] = 'i18n_access.module';
+
+ // Point to i18n_access module
+ $items['node/%node/translate']['module'] = 'i18n_access';
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ */
+function i18n_access_module_implements_alter(&$implementations, $hook) {
+ switch ($hook) {
+ case 'menu_alter':
+ // Move our hook_menu_alter implementation to the end of the list.
+ $group = $implementations['i18n_access'];
+ unset($implementations['i18n_access']);
+ $implementations['i18n_access'] = $group;
+ break;
+ }
+}
+
+/**
+ * Admin settings form.
+ */
+function i18n_access_admin_settings($form) {
+ $form['i18n_access_languages'] = array(
+ '#title' => t('Select the default access languages'),
+ '#type' => 'select',
+ '#multiple' => TRUE,
+ '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
+ '#default_value' => variable_get('i18n_access_languages', array()),
+ '#description' => t("This selection of languages will be connected with the 'access selected languages' permission which you can use to grant a role access to these languages at once.")
+ );
+ return system_settings_form($form);
+}
diff --git a/sites/all/modules/contrib/localisation/i18n_access/i18n_access.test b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.test
new file mode 100644
index 00000000..5b2f4430
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/i18n_access/i18n_access.test
@@ -0,0 +1,332 @@
+ t('Translation Access'),
+ 'description' => t('Test suite for the i18n_access module.'),
+ 'group' => t('i18n'),
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ public function setUp() {
+ parent::setUp(array('locale', 'translation', 'i18n_access', 'i18n_node'));
+
+ $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer users', 'bypass node access', 'translate content'));
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
+ $this->visitor = $this->drupalCreateUser(array('access content'));
+ $this->drupalLogin($this->admin_user);
+
+ $this->addLanguage('fr');
+ $this->addLanguage('de');
+
+ // Set Story content type to use multilingual support with translation.
+ $edit['language_content_type'] = 2;
+ $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
+ $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Story content type has been updated.');
+
+ }
+
+ /**
+ * Enable the specified language if it has not been already.
+ *
+ * @param string $language_code
+ * The language code to enable.
+ */
+ function addLanguage($language_code) {
+ // Check to make sure that language has not already been installed.
+ $this->drupalGet('admin/config/regional/language');
+
+ if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) {
+ // Doesn't have language installed so add it.
+ $edit = array();
+ $edit['langcode'] = $language_code;
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+
+ drupal_static_reset('language_list'); // Make sure not using cached version.
+ $languages = language_list('language');
+ $this->assertTrue(array_key_exists($language_code, $languages), 'Language was installed successfully.');
+
+ if (array_key_exists($language_code, $languages)) {
+ $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), 'Language has been created.');
+ }
+ }
+ else {
+ // Ensure that it is enabled.
+ $this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration'));
+
+ $this->assertRaw(t('Configuration saved.'), 'Language successfully enabled.');
+ }
+ }
+
+ /**
+ * Sets the language permission for the specified user. Must be logged in as
+ * an 'administer users' privileged user before calling this.
+ *
+ * @param $account
+ * The user account to modify
+ * @param $languages
+ * An array of language codes to give permission for
+ */
+ function setLanguagePermissions($account, $languages = array()) {
+ $this->assertTrue(user_access('administer users'), 'User has permission to administer users');
+ $expected = array();
+ $edit = array();
+ foreach ($languages as $langcode) {
+ $key = 'i18n_access[' . $langcode . ']';
+ $edit[$key] = $langcode;
+ $expected[$langcode] = $langcode;
+ }
+ $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
+
+ $actual = i18n_access_load_permissions($account->uid);
+ $this->assertEqual($expected, $actual, 'Language permissions set correctly.', 'i18n_access');
+ }
+
+ /**
+ * Unsets the language permission for the specified user. Must be logged in as
+ * an 'administer users' privileged user before calling this.
+ *
+ * @param $account
+ * The user account to modify
+ * @param $languages
+ * An array of language codes to remove permission for
+ */
+ function unsetLanguagePermissions($account, $languages = array()) {
+ $this->assertTrue(user_access('administer users'), 'User has permission to administer users');
+ $expected = array();
+ $edit = array();
+ foreach ($languages as $langcode) {
+ $key = 'i18n_access[' . $langcode . ']';
+ $edit[$key] = FALSE;
+ }
+ $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
+ drupal_static_reset('i18n_access_load_permissions');
+ drupal_static_reset('node_access');
+ $actual = i18n_access_load_permissions($account->uid);
+ $this->assertEqual($expected, $actual, 'Language permissions unset correctly.', 'i18n_access');
+ }
+
+ /**
+ * Assert that a language option exists in the language select field on the
+ * current page.
+
+ * @param $langcode
+ * Value of the language option to assert.
+ * @param $message
+ * Message to display.
+ * @param $group
+ * The group this message belongs to.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertLanguageOption($langcode, $message, $group = 'Other') {
+ $xpath = '//select[@name="language"]/option';
+ $fields = $this->xpath($xpath);
+ // If value specified then check array for match.
+ $found = TRUE;
+ if (isset($langcode)) {
+ $found = FALSE;
+ if ($fields) {
+ foreach ($fields as $field) {
+ if ($field['value'] == $langcode) {
+ $found = TRUE;
+ }
+ }
+ }
+ }
+ return $this->assertTrue($fields && $found, $message, $group);
+ }
+
+ /**
+ * Assert that a language option does not exist in the language select field
+ * on the current page.
+
+ * @param $langcode
+ * Value of the language option to assert.
+ * @param $message
+ * Message to display.
+ * @param $group
+ * The group this message belongs to.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertNoLanguageOption($langcode, $message, $group = 'Other') {
+ $xpath = '//select[@name="language"]/option';
+ $fields = $this->xpath($xpath);
+
+ // If value specified then check array for match.
+ $found = TRUE;
+ if (isset($langcode)) {
+ $found = FALSE;
+ if ($fields) {
+ foreach ($fields as $field) {
+ if ($field['value'] == $langcode) {
+ $found = TRUE;
+ }
+ }
+ }
+ }
+ return $this->assertFalse($fields && $found, $message, $group);
+ }
+
+ /**
+ * Test translator user. User with 'create article content' permission
+ * should be able to create and edit article nodes only in/for
+ * the languages that they have permissions for.
+ */
+ function testTranslatorUser() {
+ $this->_testTranslatorNodeAccess();
+ $this->_testTranslatorNodeAccess(TRUE);
+ }
+
+
+ function _testTranslatorNodeAccess($via_role = FALSE) {
+ $this->drupalLogin($this->admin_user);
+ if (!$via_role) {
+ $this->setLanguagePermissions($this->translator, array('en', 'fr'));
+ }
+ else{
+ $edit = array(
+ 'i18n_access_languages[]' => array('en', 'fr'),
+ );
+ $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
+
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
+ }
+
+ $this->drupalLogin($this->translator);
+
+ $this->drupalGet('node/add/article');
+ $this->assertField('language', 'Found language selector.');
+
+ $perms = i18n_access_load_permissions($this->translator->uid);
+ $languages = language_list();
+ $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
+ foreach ($languages as $key => $language) {
+ // TODO: Add in check for language neutral
+ if (isset($perms[$key]) && $perms[$key]) {
+ $this->assertLanguageOption($language->language, format_string('Option found for %language in language selector.', array('%language' => $language->name)));
+ }
+ else {
+ $this->assertNoLanguageOption($language->language, format_string('Option not found for %language in language selector.', array('%language' => $language->name)));
+ }
+ }
+ $this->drupalLogin($this->admin_user);
+ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
+
+ $this->drupalLogin($this->translator);
+ $this->assertFalse(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(403);
+
+ $this->assertFalse(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(403);
+
+ $this->drupalLogin($this->admin_user);
+ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'fr', 'body' => array('fr' => array(array()))));
+
+ $this->drupalLogin($this->translator);
+ $this->assertTrue(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(200);
+
+ $this->assertTrue(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(200);
+
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
+ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
+ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
+ $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
+
+ $this->drupalLogin($this->admin_user);
+ if (!$via_role) {
+ $this->unsetLanguagePermissions($this->translator, array('fr', 'en'));
+ }
+ else{
+ $edit = array(
+ 'i18n_access_languages[]' => array(),
+ );
+ $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
+ drupal_static_reset('i18n_access_load_permissions');
+ drupal_static_reset('node_access');
+ }
+
+ $this->drupalLogin($this->translator);
+ $this->assertFalse(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(403);
+
+ $this->assertFalse(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(403);
+
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
+ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
+ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
+ $this->assertNoRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
+
+ }
+
+ /**
+ * Test admin user. User with 'bypass node access' permission should be able to
+ * update, delete nodes regardless of the language.
+ */
+ function testAdminUser() {
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet('node/add/article');
+ $this->assertField('language', 'Found language selector.');
+
+ $languages = language_list();
+ $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
+ foreach ($languages as $language) {
+ $this->assertLanguageOption($language->language, format_string('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name)));
+ }
+ $this->drupalLogin($this->translator);
+ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
+
+ $this->drupalLogin($this->admin_user);
+
+ $this->assertTrue(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(200);
+
+ $this->assertTrue(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(200);
+
+ $this->drupalGet('node/' . $node->nid . '/translate');
+
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'fr'));
+ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'fr', $query));
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
+ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
+ $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'de'));
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/LICENSE.txt b/sites/all/modules/contrib/localisation/tmgmt/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/localisation/tmgmt/README.txt b/sites/all/modules/contrib/localisation/tmgmt/README.txt
new file mode 100644
index 00000000..4c34c2af
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/README.txt
@@ -0,0 +1,180 @@
+Translation Management Tool (tmgmt)
+-------------------------------------
+
+A collection of tools to facilitate the translation of text elements in Drupal.
+
+Requirements
+------------------
+
+Translation Management Tool was built for Drupal 7. There will be no backport.
+
+To use Translation Management Tool you need to install and activate the
+following modules:
+
+ * Entity API
+ * Views
+ * Chaos Tools (Required for Views)
+ * Views Bulk Operations
+ * Content Translation
+ * Locale
+ * Rules
+
+Optional dependencies:
+ * Internationalization/i18n
+ (Only necessary for i18n_string translation)
+ * Entity Translation (only for entity sources)
+
+Basic concepts
+------------------
+
+With TMGMT installed, the 'translate' tab of a node changes. You can choose
+one or more languages to translate the node to and 'Request a translation' with
+the corresponding button.
+
+A translation job is created for each language chosen. It will run through the
+following states:
+
+Unprocessed Translation requested in the 'translate' tab of a node.
+ Settings of the job (label set, translator chosen) defined.
+ The job was saved.
+Active The job is in the process of being translated. Depending on
+ the chosen translator, the actual translation happens auto-
+ matically or by a human being.
+ In all cases the job is returned to the job queue for review.
+ When the review is done, the status of the job item goes from
+ 'needs review' to 'accepted'.
+Finished The job has been accepted and the translated node was created
+
+The project also provides overviews for the supported sources that allow to
+translate multiple pieces of content (job items) in a single job and see the
+current translation status for your site content.
+
+Getting started
+------------------
+
+The first simple translation job using Microsoft's translation service.
+
+1) Preparation
+
+- Make sure you have downloaded all of the listed dependencies.
+- Define a second language using locale
+- Modify one content type to be multilingual. Choose 'Enabled, with translation'
+ from the Publishing Options / Multilingual support.
+
+2) Set up Translation Management Tool
+
+- Download tmgmt module
+- Download tmgmt_microsoft module
+- Enable the following modules, this will also include all dependencies
+ - Translation Management UI
+ - Content translation Source UI
+ - Microsoft Translator
+- A translator has been automatically created. Go to the Translator management
+ page at:
+
+ Configuration > Regional and language > Translation Management Translators
+
+ Adjust the label to your liking and get a client ID and client secret using
+ the provided link in the settings. Then save the updated translator.
+
+- Adjust the Auto Acceptance settings to your liking. You can choose to accept
+ jobs without review by checking 'Auto accept finished translations' for each
+ of your translators individually.
+
+3) Translate
+
+- Create a new piece of content of the multilingual content type defined before.
+ Make sure to choose a language.
+- Once the node has been saved, click on the "Translate" tab.
+- Choose the language you want to translate the node to with the checkbox.
+- Click on 'Request Translation' and the foreign language version of the node
+ will be created immediately.
+- If the auto acceptance is not set, find the job in the jobs queue and choose
+ the 'review' link. Accept the translation and the translated node is created.
+- Check the translated node!
+
+For further options, see the documentation on http://drupal.org/node/1445790.
+
+Features
+----------
+
+This projects consists of 3 major parts. The starting point are the sources,
+which expose translatable content like nodes, other entities and i18n strings.
+
+On the other side are the so called translators, which are responsible for
+getting the requested sources translated.
+
+The core system combine these two parts and provide the ability to create,
+manage and review translation jobs.
+
+The main features of the core system include:
+
+- Creation of translations and managing their progress
+- Review of returned translations, ability to request revisions and communicate
+ with the translator if supported.
+- Translation overviews that allow to see which content is available in which
+ language and what translation jobs are currently ongoing.
+- The same information is provided on the translate tab of the supported
+ sources.
+- A suggestions system that makes recommendations about related content that
+ could be translated with the same job.
+- Sources can declare which parts of a source text should not be translated,
+ for example placeholders for user interface strings.
+
+The following sources are currently supported:
+
+- Content Translation
+ Integrates with the core translation module to translate nodes.
+
+- Entity Translation
+ Integrates with the entity_translation module that allows to translate fields
+ on any entity type.
+
+- Internationalization (i18n)
+ Integrates with the i18n project (http://drupal.org/project/i18n) and allows
+ to translate various configuration elements of a site: blocks, terms, fields,
+ node types, contact categories and many more.
+
+- Locale
+ Allows to translate locale strings. Currently limited to the default
+ textgroup (user interface strings passed through t()).
+
+Two translators are included in the project:
+
+- File translator
+ Allows to export jobs into files and import them once they have been
+ translated. Contains a pluggable system to support various file formats,
+ currently XLIFF and HTML.
+
+- Local Translator
+ The local translator allows to manage translators on your own site so that
+ they can translate your content in a central place and defined workflows.
+ Together with the TMGMT Server, it can be used to build your own translation
+ server. Check the Hermes installation profile for more information:
+ http://drupal.org/project/hermes
+
+Translators in separate projects:
+
+- Microsoft Translator
+ Machine translation using Microsoft's Bing translation.
+ http://drupal.org/project/tmgmt_microsoft.
+
+- Google
+ Machine translation using Google Translate.
+ Moved to http://drupal.org/project/tmgmt_google.
+
+- Gengo (Previously named MyGengo)
+ Human translation that integrates with http://www.gengo.com.
+ http://drupal.org/project/tmgmt_mygengo.
+
+- Supertext
+ Human translation that integrates with http://www.supertext.ch.
+ http://drupal.org/project/tmgmt_supertext.
+
+- Nativy
+ Human translation that integrates with http://www.nativy.com.
+ http://drupal.org/project/tmgmt_nativy.
+
+- One Hour Translation
+ Human translation that integrates with http://www.onehourtranslation.com.
+ http://drupal.org/project/tmgmt_oht
diff --git a/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.job.inc b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.job.inc
new file mode 100644
index 00000000..6140e2d7
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.job.inc
@@ -0,0 +1,60 @@
+changed = REQUEST_TIME;
+ return parent::save($entity, $transaction);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($ids, $transaction = NULL) {
+ parent::delete($ids, $transaction);
+ // Since we are deleting one or multiple jobs here we also need to delete
+ // the attached job items and messages.
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
+ ->propertyCondition('tjid', $ids)
+ ->execute();
+ if (!empty($result['tmgmt_job_item'])) {
+ $controller = entity_get_controller('tmgmt_job_item');
+ // We need to directly query the entity controller so we can pass on
+ // the transaction object.
+ $controller->delete(array_keys($result['tmgmt_job_item']), $transaction);
+ }
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_message')
+ ->propertyCondition('tjid', $ids)
+ ->execute();
+ if (!empty($result['tmgmt_message'])) {
+ $controller = entity_get_controller('tmgmt_message');
+ // We need to directly query the entity controller so we can pass on
+ // the transaction object.
+ $controller->delete(array_keys($result['tmgmt_message']), $transaction);
+ }
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_remote')
+ ->propertyCondition('tjid', $ids)
+ ->execute();
+ if (!empty($result['tmgmt_remote'])) {
+ $controller = entity_get_controller('tmgmt_remote');
+ $controller->delete(array_keys($result['tmgmt_remote']), $transaction);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.job_item.inc b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.job_item.inc
new file mode 100644
index 00000000..e583d20c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.job_item.inc
@@ -0,0 +1,71 @@
+changed = REQUEST_TIME;
+ if (!empty($entity->tjid)) {
+ $entity->recalculateStatistics();
+ }
+ return parent::save($entity, $transaction);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($ids, $transaction = NULL) {
+ parent::delete($ids, $transaction);
+ // Since we are deleting one or multiple job items here we also need to
+ // delete the attached messages.
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_message')
+ ->propertyCondition('tjiid', $ids)
+ ->execute();
+ if (!empty($result['tmgmt_message'])) {
+ $controller = entity_get_controller('tmgmt_message');
+ // We need to directly query the entity controller so we can pass on
+ // the transaction object.
+ $controller->delete(array_keys($result['tmgmt_message']), $transaction);
+ }
+
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_remote')
+ ->propertyCondition('tjiid', $ids)
+ ->execute();
+ if (!empty($result['tmgmt_remote'])) {
+ $controller = entity_get_controller('tmgmt_remote');
+ $controller->delete(array_keys($result['tmgmt_remote']), $transaction);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function invoke($hook, $entity) {
+ // We need to check whether the state of the job is affected by this
+ // deletion.
+ if ($hook == 'delete' && $job = $entity->getJob()) {
+ // We only care for active jobs.
+ if ($job->isActive() && tmgmt_job_check_finished($job->tjid)) {
+ // Mark the job as finished.
+ $job->finished();
+ }
+ }
+ parent::invoke($hook, $entity);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.remote.inc b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.remote.inc
new file mode 100644
index 00000000..7fa8015d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.remote.inc
@@ -0,0 +1,97 @@
+remote_data)) {
+ $entity->remote_data = unserialize($entity->remote_data);
+ }
+ }
+
+ return $entities;
+ }
+
+ /**
+ * Loads remote mappings based on local data.
+ *
+ * @param int $tjid
+ * Translation job id.
+ * @param int $tjiid
+ * Translation job item id.
+ * @param int $data_item_key
+ * Data item key.
+ *
+ * @return array
+ * Array of TMGMTRemote entities.
+ */
+ function loadByLocalData($tjid = NULL, $tjiid = NULL, $data_item_key = NULL) {
+ $data_item_key = tmgmt_ensure_keys_string($data_item_key);
+
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_remote');
+
+ if (!empty($tjid)) {
+ $query->propertyCondition('tjid', $tjid);
+ }
+ if (!empty($tjiid)) {
+ $query->propertyCondition('tjiid', $tjiid);
+ }
+ if (!empty($data_item_key)) {
+ $query->propertyCondition('data_item_key', $data_item_key);
+ }
+
+ $result = $query->execute();
+
+ if (isset($result['tmgmt_remote'])) {
+ return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+ }
+
+ return array();
+ }
+
+ /**
+ * Loads remote mapping entities based on remote identifier.
+ *
+ * @param int $remote_identifier_1
+ * @param int $remote_identifier_2
+ * @param int $remote_identifier_3
+ *
+ * @return array
+ * Array of TMGMTRemote entities.
+ */
+ function loadByRemoteIdentifier($remote_identifier_1 = NULL, $remote_identifier_2 = NULL, $remote_identifier_3 = NULL) {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_remote');
+
+ if ($remote_identifier_1 !== NULL) {
+ $query->propertyCondition('remote_identifier_1', $remote_identifier_1);
+ }
+ if ($remote_identifier_2 !== NULL) {
+ $query->propertyCondition('remote_identifier_2', $remote_identifier_2);
+ }
+ if ($remote_identifier_3 !== NULL) {
+ $query->propertyCondition('remote_identifier_3', $remote_identifier_3);
+ }
+
+ $result = $query->execute();
+
+ if (isset($result['tmgmt_remote'])) {
+ return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+ }
+
+ return array();
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.translator.inc b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.translator.inc
new file mode 100644
index 00000000..b5c18527
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/controller/tmgmt.controller.translator.inc
@@ -0,0 +1,62 @@
+condition('plugin', array_keys($plugins));
+ }
+ else {
+ // Don't return any translators if no plugin exists.
+ $query->where('1 = 0');
+ }
+ // Sort by the weight of the translator.
+ $query->orderBy('weight');
+ return $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($ids, DatabaseTransaction $transaction = NULL) {
+ $cids = array();
+ // We are never going to have many entities here, so we can risk a loop.
+ foreach ($ids as $key => $name) {
+ if (tmgmt_translator_busy($key)) {
+ // The translator can't be deleted because it is currently busy. Remove
+ // it from the ids so it wont get deleted in the parent implementation.
+ unset($ids[$key]);
+ }
+ else {
+ $cids[$key] = 'language:' . $key;
+ }
+ }
+ // Clear the language cache for the deleted translators.
+ cache_clear_all($cids, 'cache_tmgmt');
+ parent::delete($ids, $transaction);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($entity, DatabaseTransaction $transaction = NULL) {
+ $return = parent::save($entity, $transaction);
+ // Clear the languages cache.
+ cache_clear_all('language:' . $entity->name, 'cache_tmgmt');
+ return $return;
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.info b/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.info
new file mode 100644
index 00000000..baf7b790
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.info
@@ -0,0 +1,18 @@
+name = Translation Management Demo
+description = All elements for a functioning demo.
+package = Translation Management
+core = 7.x
+hidden = TRUE
+
+dependencies[] = tmgmt_ui
+dependencies[] = tmgmt_node_ui
+dependencies[] = tmgmt_file
+dependencies[] = tmgmt_local
+dependencies[] = google_chart_tools
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.install b/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.install
new file mode 100644
index 00000000..7399b5a5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.install
@@ -0,0 +1,89 @@
+ 'translatable',
+ 'name' => 'Translation Demo Type',
+ 'base' => 'node_content',
+ 'custom' => 1,
+ 'modified' => 1,
+ 'locked' => 0,
+ );
+
+ $type = node_type_set_defaults($type);
+ node_type_save($type);
+ node_add_body_field($type);
+ variable_set('language_content_type_translatable', TRUE);
+ variable_set('comment_translatable', '0');
+ }
+
+ // Add language skills to the admin user.
+ $user = user_load(1);
+
+ $edit = array(
+ 'tmgmt_translation_skills' => array(
+ 'und' => array(
+ 0 => array(
+ 'language_from' => 'de',
+ 'language_to' => 'en',
+ ),
+ 1 => array(
+ 'language_from' => 'en',
+ 'language_to' => 'de',
+ ),
+ ),
+ ),
+ );
+
+ user_save($user, $edit);
+
+ // Add demo content.
+ $node = new stdClass();
+ $node->title = 'Second node';
+ $node->type = 'translatable';
+ node_object_prepare($node);
+ $node->language = 'en';
+ $node->body[LANGUAGE_NONE][0]['value'] = 'Have another try. This text can be
+ translated as well';
+ $node->uid = $user->uid;
+ node_save($node);
+
+ $node = new stdClass();
+ $node->title = 'First node';
+ $node->type = 'translatable';
+ node_object_prepare($node);
+ $node->language = 'en';
+ $node->body[LANGUAGE_NONE][0]['value'] = 'This text can be translated with TMGMT.
+ Use the "translate" Tab and choose "Request Translation" to get started';
+ $node->uid = $user->uid;
+ node_save($node);
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function tmgmt_demo_uninstall() {
+ // Remove the content type created by the demo module.
+ if (array_key_exists('translatable', node_type_get_names())) {
+ node_type_delete('translatable');
+ variable_del('node_preview_translatable');
+ node_types_rebuild();
+ menu_rebuild();
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.module b/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.module
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/demo/tmgmt_demo.module
@@ -0,0 +1 @@
+tjid)) {
+ $this->created = REQUEST_TIME;
+ }
+ if (!isset($this->state)) {
+ $this->state = TMGMT_JOB_STATE_UNPROCESSED;
+ }
+ }
+
+ /**
+ * Clones job as unprocessed.
+ */
+ public function cloneAsUnprocessed() {
+ $clone = clone $this;
+ $clone->tjid = NULL;
+ $clone->uid = NULL;
+ $clone->changed = NULL;
+ $clone->reference = NULL;
+ $clone->created = REQUEST_TIME;
+ $clone->state = TMGMT_JOB_STATE_UNPROCESSED;
+ return $clone;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultLabel() {
+ // In some cases we might have a user-defined label.
+ if (!empty($this->label)) {
+ return $this->label;
+ }
+
+ $items = $this->getItems();
+ $count = count($items);
+ if ($count > 0) {
+ $source_label = reset($items)->getSourceLabel();
+ $t_args = array('!title' => $source_label, '!more' => $count - 1);
+ $label = format_plural($count, '!title', '!title and !more more', $t_args);
+
+ // If the label length exceeds maximum allowed then cut off exceeding
+ // characters from the title and use it to recreate the label.
+ if (strlen($label) > TMGMT_JOB_LABEL_MAX_LENGTH) {
+ $max_length = strlen($source_label) - (strlen($label) - TMGMT_JOB_LABEL_MAX_LENGTH);
+ $source_label = truncate_utf8($source_label, $max_length, TRUE);
+ $t_args['!title'] = $source_label;
+ $label = format_plural($count, '!title', '!title and !more more', $t_args);
+ }
+ }
+ else {
+ $wrapper = entity_metadata_wrapper($this->entityType, $this);
+ $source = $wrapper->source_language->label();
+ if (empty($source)) {
+ $source = '?';
+ }
+ $target = $wrapper->target_language->label();
+ if (empty($target)) {
+ $target = '?';
+ }
+ $label = t('From !source to !target', array('!source' => $source, '!target' => $target));
+ }
+
+ return $label;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultUri() {
+ return array('path' => 'admin/tmgmt/jobs/' . $this->tjid);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildContent($view_mode = 'full', $langcode = NULL) {
+ $content = array();
+ if (module_exists('tmgmt_ui')) {
+ $content = entity_ui_get_form('tmgmt_job', $this);
+ }
+ return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+ }
+
+ /**
+ * Adds an item to the translation job.
+ *
+ * @param $plugin
+ * The plugin name.
+ * @param $item_type
+ * The source item type.
+ * @param $item_id
+ * The source item id.
+ *
+ * @return TMGMTJobItem
+ * The job item that was added to the job or FALSE if it couldn't be saved.
+ * @throws TMGMTException
+ * On zero item word count.
+ */
+ public function addItem($plugin, $item_type, $item_id) {
+
+ $transaction = db_transaction();
+ $is_new = FALSE;
+
+ if (empty($this->tjid)) {
+ $this->save();
+ $is_new = TRUE;
+ }
+
+ $item = tmgmt_job_item_create($plugin, $item_type, $item_id, array('tjid' => $this->tjid));
+ $item->save();
+
+ if ($item->getWordCount() == 0) {
+ $transaction->rollback();
+
+ // In case we got word count 0 for the first job item, NULL tjid so that
+ // if there is another addItem() call the rolled back job object will get
+ // persisted.
+ if ($is_new) {
+ $this->tjid = NULL;
+ }
+
+ throw new TMGMTException('Job item @label (@type) has no translatable content.',
+ array('@label' => $item->label(), '@type' => $item->getSourceType()));
+ }
+
+ return $item;
+ }
+
+ /**
+ * Add a given TMGMTJobItem to this job.
+ *
+ * @param TMGMTJobItem $job
+ * The job item to add.
+ */
+ function addExistingItem(TMGMTJobItem &$item) {
+ $item->tjid = $this->tjid;
+ $item->save();
+ }
+
+ /**
+ * Add a log message for this job.
+ *
+ * @param $message
+ * The message to store in the log. Keep $message translatable by not
+ * concatenating dynamic values into it! Variables in the message should be
+ * added by using placeholder strings alongside the variables argument to
+ * declare the value of the placeholders. See t() for documentation on how
+ * $message and $variables interact.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ * @param $type
+ * (Optional) The type of the message. Can be one of 'status', 'error',
+ * 'warning' or 'debug'. Messages of the type 'debug' will not get printed
+ * to the screen.
+ */
+ public function addMessage($message, $variables = array(), $type = 'status') {
+ // Save the job if it hasn't yet been saved.
+ if (!empty($this->tjid) || $this->save()) {
+ $message = tmgmt_message_create($message, $variables, array(
+ 'tjid' => $this->tjid,
+ 'type' => $type,
+ 'uid' => $GLOBALS['user']->uid,
+ ));
+ if ($message->save()) {
+ return $message;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns all job items attached to this job.
+ *
+ * @param array $conditions
+ * Additional conditions to pass into EFQ.
+ *
+ * @return TMGMTJobItem[]
+ * An array of translation job items.
+ */
+ public function getItems($conditions = array()) {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_job_item');
+ $query->propertyCondition('tjid', $this->tjid);
+ foreach ($conditions as $key => $condition) {
+ if (is_array($condition)) {
+ $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+ $query->propertyCondition($key, $condition['value'], $operator);
+ }
+ else {
+ $query->propertyCondition($key, $condition);
+ }
+ }
+ $results = $query->execute();
+ if (!empty($results['tmgmt_job_item'])) {
+ return entity_load('tmgmt_job_item', array_keys($results['tmgmt_job_item']));
+ }
+ return array();
+ }
+
+ /**
+ * Returns all job messages attached to this job.
+ *
+ * @return array
+ * An array of translation job messages.
+ */
+ public function getMessages($conditions = array()) {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_message');
+ $query->propertyCondition('tjid', $this->tjid);
+ foreach ($conditions as $key => $condition) {
+ if (is_array($condition)) {
+ $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+ $query->propertyCondition($key, $condition['value'], $operator);
+ }
+ else {
+ $query->propertyCondition($key, $condition);
+ }
+ }
+ $results = $query->execute();
+ if (!empty($results['tmgmt_message'])) {
+ return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
+ }
+ return array();
+ }
+
+ /**
+ * Returns all job messages attached to this job with timestamp newer than
+ * $time.
+ *
+ * @param $time
+ * (Optional) Messages need to have a newer timestamp than $time. Defaults
+ * to REQUEST_TIME.
+ *
+ * @return array
+ * An array of translation job messages.
+ */
+ public function getMessagesSince($time = NULL) {
+ $time = isset($time) ? $time : REQUEST_TIME;
+ $conditions = array('created' => array('value' => $time, 'operator' => '>='));
+ return $this->getMessages($conditions);
+ }
+
+ /**
+ * Retrieves a setting value from the job settings. Pulls the default values
+ * (if defined) from the plugin controller.
+ *
+ * @param $name
+ * The name of the setting.
+ *
+ * @return
+ * The setting value or $default if the setting value is not set. Returns
+ * NULL if the setting does not exist at all.
+ */
+ public function getSetting($name) {
+ if (isset($this->settings[$name])) {
+ return $this->settings[$name];
+ }
+ // The translator might provide default settings.
+ if ($translator = $this->getTranslator()) {
+ if (($setting = $translator->getSetting($name)) !== NULL) {
+ return $setting;
+ }
+ }
+ if ($controller = $this->getTranslatorController()) {
+ $defaults = $controller->defaultSettings();
+ if (isset($defaults[$name])) {
+ return $defaults[$name];
+ }
+ }
+ }
+
+ /**
+ * Returns the translator for this job.
+ *
+ * @return TMGMTTranslator
+ * The translator entity or FALSE if there was a problem.
+ */
+ public function getTranslator() {
+ if (isset($this->translator)) {
+ return tmgmt_translator_load($this->translator);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns the state of the job. Can be one of the job state constants.
+ *
+ * @return integer
+ * The state of the job or NULL if it hasn't been set yet.
+ */
+ public function getState() {
+ // We don't need to check if the state is actually set because we always set
+ // it in the constructor.
+ return $this->state;
+ }
+
+ /**
+ * Updates the state of the job.
+ *
+ * @param $state
+ * The new state of the job. Has to be one of the job state constants.
+ * @param $message
+ * (Optional) The log message to be saved along with the state change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * @return int
+ * The updated state of the job if it could be set.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
+ // Return TRUE if the state could be set. Return FALSE otherwise.
+ if (array_key_exists($state, tmgmt_job_states())) {
+ $this->state = $state;
+ $this->save();
+ // If a message is attached to this state change add it now.
+ if (!empty($message)) {
+ $this->addMessage($message, $variables, $type);
+ }
+ }
+ return $this->state;
+ }
+
+ /**
+ * Checks whether the passed value matches the current state.
+ *
+ * @param $state
+ * The value to check the current state against.
+ *
+ * @return boolean
+ * TRUE if the passed state matches the current state, FALSE otherwise.
+ */
+ public function isState($state) {
+ return $this->getState() == $state;
+ }
+
+ /**
+ * Checks whether the user described by $account is the author of this job.
+ *
+ * @param $account
+ * (Optional) A user object. Defaults to the currently logged in user.
+ */
+ public function isAuthor($account = NULL) {
+ $account = isset($account) ? $account : $GLOBALS['user'];
+ return $this->uid == $account->uid;
+ }
+
+ /**
+ * Returns whether the state of this job is 'unprocessed'.
+ *
+ * @return boolean
+ * TRUE if the state is 'unprocessed', FALSE otherwise.
+ */
+ public function isUnprocessed() {
+ return $this->isState(TMGMT_JOB_STATE_UNPROCESSED);
+ }
+
+ /**
+ * Returns whether the state of this job is 'aborted'.
+ *
+ * @return boolean
+ * TRUE if the state is 'aborted', FALSE otherwise.
+ */
+ public function isAborted() {
+ return $this->isState(TMGMT_JOB_STATE_ABORTED);
+ }
+
+ /**
+ * Returns whether the state of this job is 'active'.
+ *
+ * @return boolean
+ * TRUE if the state is 'active', FALSE otherwise.
+ */
+ public function isActive() {
+ return $this->isState(TMGMT_JOB_STATE_ACTIVE);
+ }
+
+ /**
+ * Returns whether the state of this job is 'rejected'.
+ *
+ * @return boolean
+ * TRUE if the state is 'rejected', FALSE otherwise.
+ */
+ public function isRejected() {
+ return $this->isState(TMGMT_JOB_STATE_REJECTED);
+ }
+
+ /**
+ * Returns whether the state of this jon is 'finished'.
+ *
+ * @return boolean
+ * TRUE if the state is 'finished', FALSE otherwise.
+ */
+ public function isFinished() {
+ return $this->isState(TMGMT_JOB_STATE_FINISHED);
+ }
+
+ /**
+ * Checks whether a job is translatable.
+ *
+ * @return boolean
+ * TRUE if the job can be translated, FALSE otherwise.
+ */
+ public function isTranslatable() {
+ if ($translator = $this->getTranslator()) {
+ if ($translator->canTranslate($this)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Checks whether a job is abortable.
+ *
+ * @return boolean
+ * TRUE if the job can be aborted, FALSE otherwise.
+ */
+ public function isAbortable() {
+ // Only non-submitted translation jobs can be aborted.
+ return $this->isActive();
+ }
+
+ /**
+ * Checks whether a job is submittable.
+ *
+ * @return boolean
+ * TRUE if the job can be submitted, FALSE otherwise.
+ */
+ public function isSubmittable() {
+ return $this->isUnprocessed() || $this->isRejected();
+ }
+
+ /**
+ * Checks whether a job is deletable.
+ *
+ * @return boolean
+ * TRUE if the job can be deleted, FALSE otherwise.
+ */
+ public function isDeletable() {
+ return !$this->isActive();
+ }
+
+ /**
+ * Set the state of the job to 'submitted'.
+ *
+ * @param $message
+ * The log message to be saved along with the state change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * @return TMGMTJob
+ * The job entity.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function submitted($message = NULL, $variables = array(), $type = 'status') {
+ if (!isset($message)) {
+ $message = 'The translation job has been submitted.';
+ }
+ $this->setState(TMGMT_JOB_STATE_ACTIVE, $message, $variables, $type);
+ }
+
+ /**
+ * Set the state of the job to 'finished'.
+ *
+ * @param $message
+ * The log message to be saved along with the state change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * @return TMGMTJob
+ * The job entity.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function finished($message = NULL, $variables = array(), $type = 'status') {
+ if (!isset($message)) {
+ $message = 'The translation job has been finished.';
+ }
+ return $this->setState(TMGMT_JOB_STATE_FINISHED, $message, $variables, $type);
+ }
+
+ /**
+ * Sets the state of the job to 'aborted'.
+ *
+ * @param $message
+ * The log message to be saved along with the state change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * Use TMGMTJob::abortTranslation() to abort a translation.
+ *
+ * @return TMGMTJob
+ * The job entity.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function aborted($message = NULL, $variables = array(), $type = 'status') {
+ if (!isset($message)) {
+ $message = 'The translation job has been aborted.';
+ }
+ /** @var TMGMTJobItem $item */
+ foreach ($this->getItems() as $item) {
+ $item->setState(TMGMT_JOB_ITEM_STATE_ABORTED);
+ }
+ return $this->setState(TMGMT_JOB_STATE_ABORTED, $message, $variables, $type);
+ }
+
+ /**
+ * Sets the state of the job to 'rejected'.
+ *
+ * @param $message
+ * The log message to be saved along with the state change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * @return TMGMTJob
+ * The job entity.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function rejected($message = NULL, $variables = array(), $type = 'error') {
+ if (!isset($message)) {
+ $message = 'The translation job has been rejected by the translation provider.';
+ }
+ return $this->setState(TMGMT_JOB_STATE_REJECTED, $message, $variables, $type);
+ }
+
+ /**
+ * Request the translation of a job from the translator.
+ *
+ * @return integer
+ * The updated job status.
+ */
+ public function requestTranslation() {
+ if (!$this->isTranslatable() || !$controller = $this->getTranslatorController()) {
+ return FALSE;
+ }
+ // We don't know if the translator plugin already processed our
+ // translation request after this point. That means that the plugin has to
+ // set the 'submitted', 'needs review', etc. states on its own.
+ $controller->requestTranslation($this);
+ }
+
+ /**
+ * Attempts to abort the translation job. Already accepted jobs can not be
+ * aborted, submitted jobs only if supported by the translator plugin.
+ * Always use this method if you want to abort a translation job.
+ *
+ * @return boolean
+ * TRUE if the translation job was aborted, FALSE otherwise.
+ */
+ public function abortTranslation() {
+ if (!$this->isAbortable() || !$controller = $this->getTranslatorController()) {
+ return FALSE;
+ }
+ // We don't know if the translator plugin was able to abort the translation
+ // job after this point. That means that the plugin has to set the
+ // 'aborted' state on its own.
+ return $controller->abortTranslation($this);
+ }
+
+ /**
+ * Returns the translator plugin controller of the translator of this job.
+ *
+ * @return TMGMTTranslatorPluginControllerInterface
+ * The controller of the translator plugin.
+ */
+ public function getTranslatorController() {
+ if ($translator = $this->getTranslator($this)) {
+ return $translator->getController();
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns the source data of all job items.
+ *
+ * @param $key
+ * If present, only the subarray identified by key is returned.
+ * @param $index
+ * Optional index of an attribute below $key.
+ * @return array
+ * A nested array with the source data where the most upper key is the job
+ * item id.
+ */
+ public function getData(array $key = array(), $index = NULL) {
+ $data = array();
+ if (!empty($key)) {
+ $tjiid = array_shift($key);
+ $item = entity_load_single('tmgmt_job_item', $tjiid);
+ if ($item) {
+ $data[$tjiid] = $item->getData($key, $index);
+ // If not set, use the job item label as the data label.
+ if (!isset($data[$tjiid]['#label'])) {
+ $data[$tjiid]['#label'] = $item->getSourceLabel();
+ }
+ }
+ }
+ else {
+ foreach ($this->getItems() as $tjiid => $item) {
+ $data[$tjiid] = $item->getData();
+ // If not set, use the job item label as the data label.
+ if (!isset($data[$tjiid]['#label'])) {
+ $data[$tjiid]['#label'] = $item->getSourceLabel();
+ }
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Sums up all pending counts of this jobs job items.
+ *
+ * @return
+ * The sum of all pending counts
+ */
+ public function getCountPending() {
+ return tmgmt_job_statistic($this, 'count_pending');
+ }
+
+ /**
+ * Sums up all translated counts of this jobs job items.
+ *
+ * @return
+ * The sum of all translated counts
+ */
+ public function getCountTranslated() {
+ return tmgmt_job_statistic($this, 'count_translated');
+ }
+
+ /**
+ * Sums up all accepted counts of this jobs job items.
+ *
+ * @return
+ * The sum of all accepted data items.
+ */
+ public function getCountAccepted() {
+ return tmgmt_job_statistic($this, 'count_accepted');
+ }
+
+ /**
+ * Sums up all accepted counts of this jobs job items.
+ *
+ * @return
+ * The sum of all accepted data items.
+ */
+ public function getCountReviewed() {
+ return tmgmt_job_statistic($this, 'count_reviewed');
+ }
+
+ /**
+ * Sums up all word counts of this jobs job items.
+ *
+ * @return
+ * The total word count of this job.
+ */
+ public function getWordCount() {
+ return tmgmt_job_statistic($this, 'word_count');
+ }
+
+ /**
+ * Store translated data back into the items.
+ *
+ * @param $data
+ * Partially or complete translated data, the most upper key needs to be
+ * the translation job item id.
+ * @param $key
+ * (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
+ * one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
+ * means that it will replace the whole translated data array. The most
+ * upper key entry needs to be the job id (tjiid).
+ */
+ public function addTranslatedData($data, $key = NULL) {
+ $key = tmgmt_ensure_keys_array($key);
+ $items = $this->getItems();
+ // If there is a key, get the specific item and forward the call.
+ if (!empty($key)) {
+ $item_id = array_shift($key);
+ if (isset($items[$item_id])) {
+ $items[$item_id]->addTranslatedData($data, $key);
+ }
+ }
+ else {
+ foreach ($data as $key => $value) {
+ if (isset($items[$key])) {
+ $items[$key]->addTranslatedData($value);
+ }
+ }
+ }
+ }
+
+ /**
+ * Propagates the returned job item translations to the sources.
+ *
+ * @return boolean
+ * TRUE if we were able to propagate the translated data, FALSE otherwise.
+ */
+ public function acceptTranslation() {
+ foreach ($this->getItems() as $item) {
+ $item->acceptTranslation();
+ }
+ }
+
+ /**
+ * Gets remote mappings for current job.
+ *
+ * @return array
+ * List of TMGMTRemote entities.
+ */
+ public function getRemoteMappings() {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_remote');
+ $query->propertyCondition('tjid', $this->tjid);
+ $result = $query->execute();
+
+ if (isset($result['tmgmt_remote'])) {
+ return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+ }
+
+ return array();
+ }
+
+ /**
+ * Invoke the hook 'hook_tmgmt_source_suggestions' to get all suggestions.
+ *
+ * @param arary $conditions
+ * Conditions to pass only some and not all items to the hook.
+ *
+ * @return array
+ * An array with all additional translation suggestions.
+ * - job_item: A TMGMTJobItem instance.
+ * - referenced: A string which indicates where this suggestion comes from.
+ * - from_job: The main TMGMTJob-ID which suggests this translation.
+ */
+ public function getSuggestions(array $conditions = array()) {
+ $suggestions = module_invoke_all('tmgmt_source_suggestions', $this->getItems($conditions), $this);
+
+ // Each TMGMTJob needs a job id to be able to count the words, because the
+ // source-language is stored in the job and not the item.
+ foreach ($suggestions as &$suggestion) {
+ $jobItem = $suggestion['job_item'];
+ $jobItem->tjid = $this->tjid;
+ $jobItem->recalculateStatistics();
+ }
+ return $suggestions;
+ }
+
+ /**
+ * Removes all suggestions from the given list which should not be processed.
+ *
+ * This function removes all suggestions from the given list which are already
+ * assigned to a translation job or which should not be processed because
+ * there are no words, no translation is needed, ...
+ *
+ * @param array &$suggestions
+ * Associative array of translation suggestions. It must contain at least:
+ * - tmgmt_job: An instance of a TMGMTJobItem.
+ */
+ public function cleanSuggestionsList(array &$suggestions) {
+ foreach ($suggestions as $k => $suggestion) {
+ if (is_array($suggestion) && isset($suggestion['job_item']) && ($suggestion['job_item'] instanceof TMGMTJobItem)) {
+ $jobItem = $suggestion['job_item'];
+
+ // Items with no words to translate should not be presented.
+ if ($jobItem->getWordCount() <= 0) {
+ unset($suggestions[$k]);
+ continue;
+ }
+
+ // Check if there already exists a translation job for this item in the
+ // current language.
+ $items = tmgmt_job_item_load_all_latest($jobItem->plugin, $jobItem->item_type, $jobItem->item_id, $this->source_language);
+ if ($items && isset($items[$this->target_language])) {
+ unset($suggestions[$k]);
+ continue;
+ }
+ } else {
+ unset($suggestions[$k]);
+ continue;
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.job_item.inc b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.job_item.inc
new file mode 100644
index 00000000..b1e7114f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.job_item.inc
@@ -0,0 +1,1000 @@
+state)) {
+ $this->state = TMGMT_JOB_ITEM_STATE_ACTIVE;
+ }
+ }
+
+ /**
+ * Clones as active.
+ */
+ public function cloneAsActive() {
+ $clone = clone $this;
+ $clone->data = NULL;
+ $clone->tjid = NULL;
+ $clone->tjiid = NULL;
+ $clone->changed = NULL;
+ $clone->word_count = NULL;
+ $clone->count_accepted = NULL;
+ $clone->count_pending = NULL;
+ $clone->count_translated = NULL;
+ $clone->count_reviewed = NULL;
+ $clone->state = TMGMT_JOB_ITEM_STATE_ACTIVE;
+ return $clone;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultLabel() {
+ if ($controller = $this->getSourceController()) {
+ $label = $controller->getLabel($this);
+ }
+ else {
+ $label = parent::defaultLabel();
+ }
+
+ if (strlen($label) > TMGMT_JOB_LABEL_MAX_LENGTH) {
+ $label = truncate_utf8($label, TMGMT_JOB_LABEL_MAX_LENGTH, TRUE);
+ }
+
+ return $label;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @see _tmgmt_ui_breadcrumb()
+ */
+ public function defaultUri() {
+ // The path of a job item is not directly below the job that it belongs to.
+ // Having to maintain two unknowns / wildcards (job and job item) in the
+ // path is more complex than it has to be. Instead we just append the
+ // additional breadcrumb pieces manually with _tmgmt_ui_breadcrumb().
+ return array('path' => 'admin/tmgmt/items/' . $this->tjiid);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildContent($view_mode = 'full', $langcode = NULL) {
+ $content = array();
+ if (module_exists('tmgmt_ui')) {
+ $content = tmgmt_ui_job_item_review($this);
+ }
+ return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+ }
+
+ /**
+ * Add a log message for this job item.
+ *
+ * @param $message
+ * The message to store in the log. Keep $message translatable by not
+ * concatenating dynamic values into it! Variables in the message should be
+ * added by using placeholder strings alongside the variables argument to
+ * declare the value of the placeholders. See t() for documentation on how
+ * $message and $variables interact.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ * @param $type
+ * (Optional) The type of the message. Can be one of 'status', 'error',
+ * 'warning' or 'debug'. Messages of the type 'debug' will not get printed
+ * to the screen.
+ */
+ public function addMessage($message, $variables = array(), $type = 'status') {
+ // Save the job item if it hasn't yet been saved.
+ if (!empty($this->tjiid) || $this->save()) {
+ $message = tmgmt_message_create($message, $variables, array(
+ 'tjid' => $this->tjid,
+ 'tjiid' => $this->tjiid,
+ 'uid' => $GLOBALS['user']->uid,
+ 'type' => $type,
+ ));
+ if ($message->save()) {
+ return $message;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Retrieves the label of the source object via the source controller.
+ *
+ * @return
+ * The label of the source object.
+ */
+ public function getSourceLabel() {
+ if ($controller = $this->getSourceController()) {
+ return $controller->getLabel($this);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Retrieves the path to the source object via the source controller.
+ *
+ * @return
+ * The path to the source object.
+ */
+ public function getSourceUri() {
+ if ($controller = $this->getSourceController()) {
+ return $controller->getUri($this);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns the user readable type of job item.
+ *
+ * @param string
+ * A type that describes the job item.
+ */
+ public function getSourceType() {
+ if ($controller = $this->getSourceController()) {
+ return $controller->getType($this);
+ }
+ return ucfirst($this->item_type);
+ }
+
+ /**
+ * Loads the job entity that this job item is attached to.
+ *
+ * @return TMGMTJob
+ * The job entity that this job item is attached to or FALSE if there was
+ * a problem.
+ */
+ public function getJob() {
+ if (!empty($this->tjid)) {
+ return tmgmt_job_load($this->tjid);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns the translator for this job item.
+ *
+ * @return TMGMTTranslator
+ * The translator entity or FALSE if there was a problem.
+ */
+ public function getTranslator() {
+ if ($job = $this->getJob()) {
+ return $job->getTranslator();
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns the translator plugin controller of the translator of this job item.
+ *
+ * @return TMGMTTranslatorPluginControllerInterface
+ * The controller of the translator plugin or FALSE if there was a problem.
+ */
+ public function getTranslatorController() {
+ if ($job = $this->getJob()) {
+ return $job->getTranslatorController();
+ }
+ return FALSE;
+ }
+
+ /**
+ * Array of the data to be translated.
+ *
+ * The structure is similar to the form API in the way that it is a possibly
+ * nested array with the following properties whose presence indicate that the
+ * current element is a text that might need to be translated.
+ *
+ * - #text: The text to be translated.
+ * - #label: (Optional) The label that might be shown to the translator.
+ * - #comment: (Optional) A comment with additional information.
+ * - #translate: (Optional) If set to FALSE the text will not be translated.
+ * - #translation: The translated data. Set by the translator plugin.
+ * - #escape: (Optional) List of arrays with a required string key, keyed by
+ * the position key. Translators must use this list to prevent translation
+ * of these strings if possible.
+ *
+ *
+ * @todo: Move data item documentation to a new, separate api group.
+ *
+ * The key can be an alphanumeric string.
+ * @param $key
+ * If present, only the subarray identified by key is returned.
+ * @param $index
+ * Optional index of an attribute below $key.
+ *
+ * @return array
+ * A structured data array.
+ */
+ public function getData(array $key = array(), $index = NULL) {
+ if (empty($this->data) && !empty($this->tjid)) {
+ // Load the data from the source if it has not been set yet.
+ $this->data = $this->getSourceData();
+ $this->save();
+ }
+ if (empty($key)) {
+ return $this->data;
+ }
+ if ($index) {
+ $key = array_merge($key, array($index));
+ }
+ return drupal_array_get_nested_value($this->data, $key);
+ }
+
+ /**
+ * Loads the structured source data array from the source.
+ */
+ public function getSourceData() {
+ if ($controller = $this->getSourceController()) {
+ return $controller->getData($this);
+ }
+ return array();
+ }
+
+ /**
+ * Returns the plugin controller of the configured plugin.
+ *
+ * @return TMGMTSourcePluginControllerInterface
+ */
+ public function getSourceController() {
+ if (!empty($this->plugin)) {
+ return tmgmt_source_plugin_controller($this->plugin);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Count of all pending data items
+ *
+ * @return
+ * Pending counts
+ */
+ public function getCountPending() {
+ return $this->count_pending;
+ }
+
+ /**
+ * Count of all translated data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountTranslated() {
+ return $this->count_translated;
+ }
+
+ /**
+ * Count of all accepted data items.
+ *
+ * @return
+ * Accepted count
+ */
+ public function getCountAccepted() {
+ return $this->count_accepted;
+ }
+
+ /**
+ * Count of all accepted data items.
+ *
+ * @return
+ * Accepted count
+ */
+ public function getCountReviewed() {
+ return $this->count_reviewed;
+ }
+
+ /**
+ * Word count of all data items.
+ *
+ * @return
+ * Word count
+ */
+ public function getWordCount() {
+ return (int)$this->word_count;
+ }
+
+ /**
+ * Sets the state of the job item to 'needs review'.
+ */
+ public function needsReview($message = NULL, $variables = array(), $type = 'status') {
+ if (!isset($message)) {
+ $uri = $this->getSourceUri();
+ $message = 'The translation for !source needs to be reviewed.';
+ $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
+ }
+ $return = $this->setState(TMGMT_JOB_ITEM_STATE_REVIEW, $message, $variables, $type);
+ // Auto accept the trganslation if the translator is configured for it.
+ if ($this->getTranslator()->getSetting('auto_accept')) {
+ $this->acceptTranslation();
+ }
+ return $return;
+ }
+
+ /**
+ * Sets the state of the job item to 'accepted'.
+ */
+ public function accepted($message = NULL, $variables = array(), $type = 'status') {
+ if (!isset($message)) {
+ $uri = $this->getSourceUri();
+ $message = 'The translation for !source has been accepted.';
+ $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
+ }
+ $return = $this->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED, $message, $variables, $type);
+ // Check if this was the last unfinished job item in this job.
+ if (tmgmt_job_check_finished($this->tjid) && $job = $this->getJob()) {
+ // Mark the job as finished.
+ $job->finished();
+ }
+ return $return;
+ }
+
+ /**
+ * Sets the state of the job item to 'active'.
+ */
+ public function active($message = NULL, $variables = array(), $type = 'status') {
+ if (!isset($message)) {
+ $uri = $this->getSourceUri();
+ $message = 'The translation for !source is now being processed.';
+ $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
+ }
+ return $this->setState(TMGMT_JOB_ITEM_STATE_ACTIVE, $message, $variables, $type);
+ }
+
+ /**
+ * Updates the state of the job item.
+ *
+ * @param $state
+ * The new state of the job item. Has to be one of the job state constants.
+ * @param $message
+ * (Optional) The log message to be saved along with the state change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * @return int
+ * The updated state of the job if it could be set.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
+ // Return TRUE if the state could be set. Return FALSE otherwise.
+ if (array_key_exists($state, tmgmt_job_item_states()) && $this->state != $state) {
+ $this->state = $state;
+ $this->save();
+ // If a message is attached to this state change add it now.
+ if (!empty($message)) {
+ $this->addMessage($message, $variables, $type);
+ }
+ }
+ return $this->state;
+ }
+
+ /**
+ * Returns the state of the job item. Can be one of the job item state
+ * constants.
+ *
+ * @return integer
+ * The state of the job item.
+ */
+ public function getState() {
+ // We don't need to check if the state is actually set because we always set
+ // it in the constructor.
+ return $this->state;
+ }
+
+ /**
+ * Checks whether the passed value matches the current state.
+ *
+ * @param $state
+ * The value to check the current state against.
+ *
+ * @return boolean
+ * TRUE if the passed state matches the current state, FALSE otherwise.
+ */
+ public function isState($state) {
+ return $this->getState() == $state;
+ }
+
+ /**
+ * Checks whether the state of this transaction is 'accepted'.
+ *
+ * @return boolean
+ * TRUE if the state is 'accepted', FALSE otherwise.
+ */
+ public function isAccepted() {
+ return $this->isState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
+ }
+
+ /**
+ * Checks whether the state of this transaction is 'active'.
+ *
+ * @return boolean
+ * TRUE if the state is 'active', FALSE otherwise.
+ */
+ public function isActive() {
+ return $this->isState(TMGMT_JOB_ITEM_STATE_ACTIVE);
+ }
+
+ /**
+ * Checks whether the state of this transaction is 'needs review'.
+ *
+ * @return boolean
+ * TRUE if the state is 'needs review', FALSE otherwise.
+ */
+ public function isNeedsReview() {
+ return $this->isState(TMGMT_JOB_ITEM_STATE_REVIEW);
+ }
+
+ /**
+ * Checks whether the state of this transaction is 'aborted'.
+ *
+ * @return boolean
+ * TRUE if the state is 'aborted', FALSE otherwise.
+ */
+ public function isAborted() {
+ return $this->isState(TMGMT_JOB_ITEM_STATE_ABORTED);
+ }
+
+ /**
+ * Recursively writes translated data to the data array of a job item.
+ *
+ * While doing this the #status of each data item is set to
+ * TMGMT_DATA_ITEM_STATE_TRANSLATED.
+ *
+ * @param $translation
+ * Nested array of translated data. Can either be a single text entry, the
+ * whole data structure or parts of it.
+ * @param $key
+ * (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
+ * one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
+ * means that it will replace the whole translated data array.
+ */
+ protected function addTranslatedDataRecursive($translation, $key = array()) {
+ if (isset($translation['#text'])) {
+ $data = $this->getData(tmgmt_ensure_keys_array($key));
+ if (empty($data['#status']) || $data['#status'] != TMGMT_DATA_ITEM_STATE_ACCEPTED) {
+
+ // In case the origin is not set consider it to be remote.
+ if (!isset($translation['#origin'])) {
+ $translation['#origin'] = 'remote';
+ }
+
+ // If we already have a translation text and it hasn't changed, don't
+ // update anything unless the origin is remote.
+ if (!empty($data['#translation']['#text']) && $data['#translation']['#text'] == $translation['#text'] && $translation['#origin'] != 'remote') {
+ return;
+ }
+
+ // In case the timestamp is not set consider it to be now.
+ if (!isset($translation['#timestamp'])) {
+ $translation['#timestamp'] = REQUEST_TIME;
+ }
+ // If we have a translation text and is different from new one create
+ // revision.
+ if (!empty($data['#translation']['#text']) && $data['#translation']['#text'] != $translation['#text']) {
+
+ // Copy into $translation existing revisions.
+ if (!empty($data['#translation']['#text_revisions'])) {
+ $translation['#text_revisions'] = $data['#translation']['#text_revisions'];
+ }
+
+ // If current translation was created locally and the incoming one is
+ // remote, do not override the local, just create a new revision.
+ if (isset($data['#translation']['#origin']) && $data['#translation']['#origin'] == 'local' && $translation['#origin'] == 'remote') {
+ $translation['#text_revisions'][] = array(
+ '#text' => $translation['#text'],
+ '#origin' => $translation['#origin'],
+ '#timestamp' => $translation['#timestamp'],
+ );
+ $this->addMessage('Translation for customized @key received. Revert your changes if you wish to use it.', array('@key' => tmgmt_ensure_keys_string($key)));
+ // Unset text and origin so that the current translation does not
+ // get overridden.
+ unset($translation['#text'], $translation['#origin'], $translation['#timestamp']);
+ }
+ // Do the same if the translation was already reviewed and origin is
+ // remote.
+ elseif ($translation['#origin'] == 'remote' && !empty($data['#status']) && $data['#status'] == TMGMT_DATA_ITEM_STATE_REVIEWED) {
+ $translation['#text_revisions'][] = array(
+ '#text' => $translation['#text'],
+ '#origin' => $translation['#origin'],
+ '#timestamp' => $translation['#timestamp'],
+ );
+ $this->addMessage('Translation for already reviewed @key received and stored as a new revision. Revert to it if you wish to use it.', array('@key' => tmgmt_ensure_keys_string($key)));
+ // Unset text and origin so that the current translation does not
+ // get overridden.
+ unset($translation['#text'], $translation['#origin'], $translation['#timestamp']);
+ }
+ else {
+ $translation['#text_revisions'][] = array(
+ '#text' => $data['#translation']['#text'],
+ '#origin' => isset($data['#translation']['#origin']) ? $data['#translation']['#origin'] : 'remote',
+ '#timestamp' => isset($data['#translation']['#timestamp']) ? $data['#translation']['#timestamp'] : $this->changed,
+ );
+ // Add a message if the translation update is from remote.
+ if ($translation['#origin'] == 'remote') {
+ $diff = drupal_strlen($translation['#text']) - drupal_strlen($data['#translation']['#text']);
+ $this->addMessage('Updated translation for key @key, size difference: @diff characters.', array('@key' => tmgmt_ensure_keys_string($key), '@diff' => $diff));
+ }
+ }
+ }
+
+ $values = array(
+ '#translation' => $translation,
+ '#status' => TMGMT_DATA_ITEM_STATE_TRANSLATED,
+ );
+ $this->updateData($key, $values);
+ }
+ return;
+ }
+
+ foreach (element_children($translation) as $item) {
+ $this->addTranslatedDataRecursive($translation[$item], array_merge($key, array($item)));
+ }
+ }
+
+ /**
+ * Reverts data item translation to the latest existing revision.
+ *
+ * @param array $key
+ * Data item key that should be reverted.
+ *
+ * @return bool
+ * Result of the revert action.
+ */
+ public function dataItemRevert(array $key) {
+ $data = $this->getData($key);
+ if (!empty($data['#translation']['#text_revisions'])) {
+
+ $prev_revision = end($data['#translation']['#text_revisions']);
+ $data['#translation']['#text_revisions'][] = array(
+ '#text' => $data['#translation']['#text'],
+ '#timestamp' => $data['#translation']['#timestamp'],
+ '#origin' => $data['#translation']['#origin'],
+ );
+ $data['#translation']['#text'] = $prev_revision['#text'];
+ $data['#translation']['#origin'] = $prev_revision['#origin'];
+ $data['#translation']['#timestamp'] = $prev_revision['#timestamp'];
+
+ $this->updateData($key, $data);
+ $this->addMessage('Translation for @key reverted to the latest version.', array('@key' => tmgmt_ensure_keys_string($key)));
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * Updates the values for a specific substructure in the data array.
+ *
+ * The values are either set or updated but never deleted.
+ *
+ * @param $key
+ * Key pointing to the item the values should be applied.
+ * The key can be either be an array containing the keys of a nested array
+ * hierarchy path or a string with '][' or '|' as delimiter.
+ * @param $values
+ * Nested array of values to set.
+ */
+ public function updateData($key, $values = array()) {
+ foreach ($values as $index => $value) {
+ // In order to preserve existing values, we can not aplly the values array
+ // at once. We need to apply each containing value on its own.
+ // If $value is an array we need to advance the hierarchy level.
+ if (is_array($value)) {
+ $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+ }
+ // Apply the value.
+ else {
+ drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+ }
+ }
+ }
+
+ /**
+ * Adds translated data to a job item.
+ *
+ * This function calls for TMGMTJobItem::addTranslatedDataRecursive() which
+ * sets the status of each added data item to TMGMT_DATA_ITEM_STATE_TRANSLATED.
+ *
+ * Following rules apply while adding translated data:
+ *
+ * 1) Updated are only items that are changed. In case there is local
+ * modification the translation is added as a revision with a message stating
+ * this fact.
+ *
+ * 2) Merging happens at the data items level, so updating only those that are
+ * changed. If a data item is in review/reject status and is being updated
+ * with translation originating from remote the status is updated to
+ * 'translated' no matter if it is changed or not.
+ *
+ * 3) Each time a data item is updated the previous translation becomes a
+ * revision.
+ *
+ * If all data items are translated, the status of the job item is updated to
+ * needs review.
+ *
+ * @todo
+ * To update the job item status to needs review we could take advantage of
+ * the TMGMTJobItem::getCountPending() and TMGMTJobItem::getCountTranslated().
+ * The catch is, that this counter gets updated while saveing which not yet
+ * hapened.
+ *
+ * @param $translation
+ * Nested array of translated data. Can either be a single text entry, the
+ * whole data structure or parts of it.
+ * @param $key
+ * (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
+ * one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
+ * means that it will replace the whole translated data array.
+ */
+ public function addTranslatedData($translation, $key = array()) {
+ $this->addTranslatedDataRecursive($translation, $key);
+ // Check if the job item has all the translated data that it needs now.
+ // Only attempt to change the status to needs review if it is currently
+ // active.
+ if ($this->isActive()) {
+ $data = tmgmt_flatten_data($this->getData());
+ $data = array_filter($data, '_tmgmt_filter_data');
+ $finished = TRUE;
+ foreach ($data as $item) {
+ if (empty($item['#status']) || $item['#status'] == TMGMT_DATA_ITEM_STATE_PENDING) {
+ $finished = FALSE;
+ break;
+ }
+ }
+ if ($finished) {
+ // There are no unfinished elements left.
+ if ($this->getJob()->getTranslator()->getSetting('auto_accept')) {
+ // If the job item is going to be auto-accepted, set to review without
+ // a message.
+ $this->needsReview(FALSE);
+ }
+ else {
+ // Otherwise, create a message that contains source label, target
+ // language and links to the review form.
+ $uri = $this->uri();
+ $job_uri = $this->getJob()->uri();
+ $variables = array(
+ '!source' => l($this->getSourceLabel(), $uri['path']),
+ '@language' => entity_metadata_wrapper('tmgmt_job', $this->getJob())->target_language->label(),
+ '!review_url' => url($uri['path'], array('query' => array('destination' => $job_uri['path']))),
+ );
+ $this->needsReview('The translation of !source to @language is finished and can now be reviewed.', $variables);
+ }
+ }
+ }
+ $this->save();
+ }
+
+ /**
+ * Propagates the returned job item translations to the sources.
+ *
+ * @return boolean
+ * TRUE if we were able to propagate the translated data and the item could
+ * be saved, FALSE otherwise.
+ */
+ public function acceptTranslation() {
+ if (!$this->isNeedsReview() || !$controller = $this->getSourceController()) {
+ return FALSE;
+ }
+ // We don't know if the source plugin was able to save the translation after
+ // this point. That means that the plugin has to set the 'accepted' states
+ // on its own.
+ $controller->saveTranslation($this);
+ }
+
+ /**
+ * Returns all job messages attached to this job item.
+ *
+ * @return array
+ * An array of translation job messages.
+ */
+ public function getMessages($conditions = array()) {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_message');
+ $query->propertyCondition('tjiid', $this->tjiid);
+ foreach ($conditions as $key => $condition) {
+ if (is_array($condition)) {
+ $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+ $query->propertyCondition($key, $condition['value'], $operator);
+ }
+ else {
+ $query->propertyCondition($key, $condition);
+ }
+ }
+ $results = $query->execute();
+ if (!empty($results['tmgmt_message'])) {
+ return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
+ }
+ return array();
+ }
+
+ /**
+ * Retrieves all siblings of this job item.
+ *
+ * @return array
+ * An array of job items that are the siblings of this job item.
+ */
+ public function getSiblings() {
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
+ ->propertyCondition('tjiid', $this->tjiid, '<>')
+ ->propertyCondition('tjid', $this->tjid)
+ ->execute();
+ if (!empty($result['tmgmt_job_item'])) {
+ return entity_load('tmgmt_job_item', array_keys($result['tmgmt_job_item']));
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns all job messages attached to this job item with timestamp newer
+ * than $time.
+ *
+ * @param $timestamp
+ * (Optional) Messages need to have a newer timestamp than $time. Defaults
+ * to REQUEST_TIME.
+ *
+ * @return array
+ * An array of translation job messages.
+ */
+ public function getMessagesSince($time = NULL) {
+ $time = isset($time) ? $time : REQUEST_TIME;
+ $conditions = array('created' => array('value' => $time, 'operator' => '>='));
+ return $this->getMessages($conditions);
+ }
+
+ /**
+ * Adds remote mapping entity to this job item.
+ *
+ * @param string $data_item_key
+ * Job data item key.
+ * @param int $remote_identifier_1
+ * Array of remote identifiers. In case you need to save
+ * remote_identifier_2/3 set it into $mapping_data argument.
+ * @param array $mapping_data
+ * Additional data to be added.
+ *
+ * @return int|bool
+ * @throws TMGMTException
+ */
+ public function addRemoteMapping($data_item_key = NULL, $remote_identifier_1 = NULL, $mapping_data = array()) {
+
+ if (empty($remote_identifier_1) && !isset($mapping_data['remote_identifier_2']) && !isset($remote_mapping['remote_identifier_3'])) {
+ throw new TMGMTException('Cannot create remote mapping without remote identifier.');
+ }
+
+ $data = array(
+ 'tjid' => $this->tjid,
+ 'tjiid' => $this->tjiid,
+ 'data_item_key' => $data_item_key,
+ 'remote_identifier_1' => $remote_identifier_1,
+ );
+
+ if (!empty($mapping_data)) {
+ $data += $mapping_data;
+ }
+
+ $remote_mapping = entity_create('tmgmt_remote', $data);
+
+ return entity_get_controller('tmgmt_remote')->save($remote_mapping);
+ }
+
+ /**
+ * Gets remote mappings for current job item.
+ *
+ * @return array
+ * List of TMGMTRemote entities.
+ */
+ public function getRemoteMappings() {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_remote');
+ $query->propertyCondition('tjiid', $this->tjiid);
+ $result = $query->execute();
+
+ if (isset($result['tmgmt_remote'])) {
+ return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+ }
+
+ return array();
+ }
+
+ /**
+ * Gets language code of the job item source.
+ *
+ * @return string
+ * Language code.
+ */
+ public function getSourceLangCode() {
+ return $this->getSourceController()->getSourceLangCode($this);
+ }
+
+ /**
+ * Gets existing translation language codes of the job item source.
+ *
+ * @return array
+ * Array of language codes.
+ */
+ public function getExistingLangCodes() {
+ return $this->getSourceController()->getExistingLangCodes($this);
+ }
+
+ /**
+ * Recalculate statistical word-data: pending, translated, reviewed, accepted.
+ */
+ public function recalculateStatistics() {
+ // Set translatable data from the current entity to calculate words.
+ if (empty($this->data)) {
+ $this->data = $this->getSourceData();
+ }
+
+ // Consider everything accepted when the job item is accepted.
+ if ($this->isAccepted()) {
+ $this->count_pending = 0;
+ $this->count_translated = 0;
+ $this->count_reviewed = 0;
+ $this->count_accepted = count(array_filter(tmgmt_flatten_data($this->data), '_tmgmt_filter_data'));
+ }
+ // Count the data item states.
+ else {
+ // Reset counter values.
+ $this->count_pending = 0;
+ $this->count_translated = 0;
+ $this->count_reviewed = 0;
+ $this->count_accepted = 0;
+ $this->word_count = 0;
+ $this->count($this->data);
+ }
+ }
+
+ /**
+ * Parse all data items recursively and sums up the counters for
+ * accepted, translated and pending items.
+ *
+ * @param $item
+ * The current data item.
+ */
+ protected function count(&$item) {
+ if (!empty($item['#text'])) {
+ if (_tmgmt_filter_data($item)) {
+
+ // Count words of the data item.
+ $this->word_count += tmgmt_word_count($item['#text']);
+
+ // Set default states if no state is set.
+ if (!isset($item['#status'])) {
+ // Translation is present.
+ if (!empty($item['#translation'])) {
+ $item['#status'] = TMGMT_DATA_ITEM_STATE_TRANSLATED;
+ }
+ // No translation present.
+ else {
+ $item['#status'] = TMGMT_DATA_ITEM_STATE_PENDING;
+ }
+ }
+ switch ($item['#status']) {
+ case TMGMT_DATA_ITEM_STATE_REVIEWED:
+ $this->count_reviewed++;
+ break;
+ case TMGMT_DATA_ITEM_STATE_TRANSLATED:
+ $this->count_translated++;
+ break;
+ default:
+ $this->count_pending++;
+ break;
+ }
+ }
+ }
+ elseif (is_array($item)) {
+ foreach (element_children($item) as $key) {
+ $this->count($item[$key]);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.message.inc b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.message.inc
new file mode 100644
index 00000000..588f9428
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.message.inc
@@ -0,0 +1,143 @@
+created)) {
+ $this->created = REQUEST_TIME;
+ }
+ if (empty($this->type)) {
+ $this->type = 'status';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultLabel() {
+ $created = format_date($this->created);
+ switch ($this->type) {
+ case 'error':
+ return t('Error message from @time', array('@time' => $created));
+ case 'status':
+ return t('Status message from @time', array('@time' => $created));
+ case 'warning':
+ return t('Warning message from @time', array('@time' => $created));
+ case 'debug':
+ return t('Debug message from @time', array('@time' => $created));
+ }
+ }
+
+ /**
+ * Returns the translated message.
+ *
+ * @return
+ * The translated message.
+ */
+ public function getMessage() {
+ $text = $this->message;
+ if (is_array($this->variables) && !empty($this->variables)) {
+ $text = t($text, $this->variables);
+ }
+ return $text;
+ }
+
+ /**
+ * Loads the job entity that this job message is attached to.
+ *
+ * @return TMGMTJob
+ * The job entity that this job message is attached to or FALSE if there was
+ * a problem.
+ */
+ public function getJob() {
+ if (!empty($this->tjid)) {
+ return tmgmt_job_load($this->tjid);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Loads the job entity that this job message is attached to.
+ *
+ * @return TMGMTJobItem
+ * The job item entity that this job message is attached to or FALSE if
+ * there was a problem.
+ */
+ public function getJobItem() {
+ if (!empty($this->tjiid)) {
+ return tmgmt_job_item_load($this->tjiid);
+ }
+ return FALSE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.remote.inc b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.remote.inc
new file mode 100644
index 00000000..eb0254b1
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.remote.inc
@@ -0,0 +1,155 @@
+tjid);
+ }
+
+ /**
+ * Gets translation job item.
+ *
+ * @return TMGMTJobItem
+ */
+ function getJobItem() {
+ if (!empty($this->tjiid)) {
+ return tmgmt_job_item_load($this->tjiid);
+ }
+ return NULL;
+ }
+
+ /**
+ * Adds data to the remote_data storage.
+ *
+ * @param string $key
+ * Key through which the data will be accessible.
+ * @param $value
+ * Value to store.
+ */
+ function addRemoteData($key, $value) {
+ $this->remote_data[$key] = $value;
+ }
+
+ /**
+ * Gets data from remote_data storage.
+ *
+ * @param string $key
+ * Access key for the data.
+ *
+ * @return mixed
+ * Stored data.
+ */
+ function getRemoteData($key) {
+ return $this->remote_data[$key];
+ }
+
+ /**
+ * Removes data from remote_data storage.
+ *
+ * @param string $key
+ * Access key for the data that are to be removed.
+ */
+ function removeRemoteData($key) {
+ unset($this->remote_data[$key]);
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.translator.inc b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.translator.inc
new file mode 100644
index 00000000..21845f56
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/entity/tmgmt.entity.translator.inc
@@ -0,0 +1,299 @@
+plugin)) {
+ return tmgmt_translator_plugin_controller($this->plugin);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns the supported target languages for this translator.
+ *
+ * @return array
+ * An array of supported target languages in ISO format.
+ */
+ public function getSupportedTargetLanguages($source_language) {
+ if ($controller = $this->getController()) {
+ if (isset($this->pluginInfo['cache languages']) && empty($this->pluginInfo['cache languages'])) {
+ // This plugin doesn't support language caching.
+ return $controller->getSupportedTargetLanguages($this, $source_language);
+ }
+ else {
+ // Retrieve the supported languages from the cache.
+ if (empty($this->languageCache) && $cache = cache_get('languages:' . $this->name, 'cache_tmgmt')) {
+ $this->languageCache = $cache->data;
+ }
+ // Even if we successfully queried the cache it might not have an entry
+ // for our source language yet.
+ if (!isset($this->languageCache[$source_language])) {
+ $this->languageCache[$source_language] = $controller->getSupportedTargetLanguages($this, $source_language);
+ $this->languageCacheOutdated = TRUE;
+ }
+ }
+ return $this->languageCache[$source_language];
+ }
+ }
+
+ /**
+ * Gets the supported language pairs for this translator.
+ *
+ * @return array
+ * List of language pairs where a pair is an associative array of
+ * source_language and target_language.
+ * Example:
+ * array(
+ * array('source_language' => 'en-US', 'target_language' => 'de-DE'),
+ * array('source_language' => 'en-US', 'target_language' => 'de-CH'),
+ * )
+ */
+ public function getSupportedLanguagePairs() {
+ if ($controller = $this->getController()) {
+ if (isset($this->pluginInfo['cache languages']) && empty($this->pluginInfo['cache languages'])) {
+ // This plugin doesn't support language caching.
+ return $controller->getSupportedLanguagePairs($this);
+ }
+ else {
+ // Retrieve the supported languages from the cache.
+ if (empty($this->languagePairsCache) && $cache = cache_get('language_pairs:' . $this->name, 'cache_tmgmt')) {
+ $this->languagePairsCache = $cache->data;
+ }
+ // Even if we successfully queried the cache data might not be yet
+ // available.
+ if (empty($this->languagePairsCache)) {
+ $this->languagePairsCache = $controller->getSupportedLanguagePairs($this);
+ $this->languageCacheOutdated = TRUE;
+ }
+ }
+ return $this->languagePairsCache;
+ }
+ }
+
+ /**
+ * Clears the language cache for this translator.
+ */
+ public function clearLanguageCache() {
+ cache_clear_all('languages:' . $this->name, 'cache_tmgmt');
+ cache_clear_all('language_pairs:' . $this->name, 'cache_tmgmt');
+ }
+
+ /**
+ * Check whether this translator can handle a particular translation job.
+ *
+ * @param $job
+ * The TMGMTJob entity that should be translated.
+ *
+ * @return boolean
+ * TRUE if the job can be processed and translated, FALSE otherwise.
+ */
+ public function canTranslate(TMGMTJob $job) {
+ if ($controller = $this->getController()) {
+ return $controller->canTranslate($this, $job);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Checks whether a translator is available.
+ *
+ * @return boolean
+ * TRUE if the translator plugin is available, FALSE otherwise.
+ */
+ public function isAvailable() {
+ if ($controller = $this->getController()) {
+ return $controller->isAvailable($this);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns if the plugin has any settings for this job.
+ */
+ public function hasCheckoutSettings(TMGMTJob $job) {
+ if ($controller = $this->getController()) {
+ return $controller->hasCheckoutSettings($job);
+ }
+ return FALSE;
+ }
+
+ /**
+ * @todo Remove this once http://drupal.org/node/1420364 is done.
+ */
+ public function getNotAvailableReason() {
+ if ($controller = $this->getController()) {
+ return $controller->getNotAvailableReason($this);
+ }
+ return FALSE;
+ }
+
+ /**
+ * @todo Remove this once http://drupal.org/node/1420364 is done.
+ */
+ public function getNotCanTranslateReason(TMGMTJob $job) {
+ if ($controller = $this->getController()) {
+ return $controller->getNotCanTranslateReason($job);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Retrieves a setting value from the translator settings. Pulls the default
+ * values (if defined) from the plugin controller.
+ *
+ * @param $name
+ * The name of the setting.
+ *
+ * @return
+ * The setting value or $default if the setting value is not set. Returns
+ * NULL if the setting does not exist at all.
+ */
+ public function getSetting($name) {
+ if (isset($this->settings[$name])) {
+ return $this->settings[$name];
+ }
+ elseif ($controller = $this->getController()) {
+ $defaults = $controller->defaultSettings();
+ if (isset($defaults[$name])) {
+ return $defaults[$name];
+ }
+ }
+ }
+
+ /**
+ * Maps local language to remote language.
+ *
+ * @param $language
+ * Local language code.
+ *
+ * @return string
+ * Remote language code.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function mapToRemoteLanguage($language) {
+ return $this->getController()->mapToRemoteLanguage($this, $language);
+ }
+
+ /**
+ * Maps remote language to local language.
+ *
+ * @param $language
+ * Remote language code.
+ *
+ * @return string
+ * Local language code.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function mapToLocalLanguage($language) {
+ return $this->getController()->mapToLocalLanguage($this, $language);
+ }
+
+ /**
+ * Updates the language cache if it has changed.
+ */
+ public function __destruct() {
+ if ($controller = $this->getController()) {
+ $info = $controller->pluginInfo();
+ if (!isset($info['language cache']) || !empty($info['language cache']) && !empty($this->languageCacheOutdated)) {
+ cache_set('languages:' . $this->name, $this->languageCache, 'cache_tmgmt');
+ cache_set('language_pairs:' . $this->name, $this->languagePairsCache, 'cache_tmgmt');
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/includes/tmgmt.exception.inc b/sites/all/modules/contrib/localisation/tmgmt/includes/tmgmt.exception.inc
new file mode 100644
index 00000000..09d0518e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/includes/tmgmt.exception.inc
@@ -0,0 +1,17 @@
+type);
+ $properties = &$info[$this->type]['properties'];
+
+ // Make the created and changed property appear as date.
+ $properties['changed']['type'] = $properties['created']['type'] = 'date';
+
+ // Use the defined entity label callback instead of the custom label directly.
+ $properties['label']['getter callback'] = 'entity_class_label';
+
+ // Allow to change the properties.
+ foreach (array('target_language', 'source_language', 'translator') as $property) {
+ $properties[$property]['setter callback'] = 'entity_property_verbatim_set';
+ }
+
+ // Add the options list for the available languages.
+ $properties['target_language']['options list'] = $properties['source_language']['options list'] = 'entity_metadata_language_list';
+
+ // Add the options list for the defined state constants.
+ $properties['state']['options list'] = 'tmgmt_job_states';
+
+ // Add the options list for all available translator plugins.
+ $properties['translator']['type'] = 'tmgmt_translator';
+ $properties['translator']['options list'] = 'tmgmt_translator_labels';
+
+ // Link the author property to the corresponding user entity.
+ $properties['author'] = array(
+ 'label' => t('Author'),
+ 'type' => 'user',
+ 'description' => t('The author of the translation job.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer tmgmt',
+ 'required' => TRUE,
+ 'schema field' => 'uid',
+ );
+
+ return $info;
+ }
+
+}
+
+/**
+ * Metadata controller for the job item entity.
+ */
+class TMGMTJobItemMetadataController extends EntityDefaultMetadataController {
+
+ public function entityPropertyInfo() {
+ $info = parent::entityPropertyInfo();
+ $info = _tmgmt_override_property_description($info, $this->type);
+ $properties = &$info[$this->type]['properties'];
+
+ // Make the created and changed property appear as date.
+ $properties['changed']['type'] = 'date';
+
+ // Add the options list for the defined state constants.
+ $properties['state']['options list'] = 'tmgmt_job_item_states';
+
+ // Link the job id property to the corresponding job entity.
+ $properties['tjid'] = array(
+ 'description' => t('Corresponding job entity.'),
+ 'type' => 'tmgmt_job',
+ ) + $properties['tjid'];
+
+ // Add the options list for all available source plugins.
+ $properties['plugin']['options list'] = 'tmgmt_source_plugin_labels';
+
+ $properties['word_count']['label'] = t('Word count');
+
+ return $info;
+ }
+
+}
+
+/**
+ * Metadata controller for the job message entity.
+ */
+class TMGMTMessageMetadataController extends EntityDefaultMetadataController {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function entityPropertyInfo() {
+ $info = parent::entityPropertyInfo();
+ $info = _tmgmt_override_property_description($info, $this->type);
+ $properties = &$info[$this->type]['properties'];
+
+ // Make the created property appear as date.
+ $properties['created']['type'] = 'date';
+
+ // Link the job id property to the corresponding job entity.
+ $properties['tjid'] = array(
+ 'description' => t('Corresponding job entity.'),
+ 'type' => 'tmgmt_job',
+ ) + $properties['tjid'];
+
+ // Link the job item id property to the corresponding job item entity.
+ $properties['tjiid'] = array(
+ 'description' => t('Corresponding job item entity.'),
+ 'type' => 'tmgmt_job_item',
+ ) + $properties['tjiid'];
+
+ // Link user, was added in an update so make sure that it doesn't explode
+ // if the schema cache was not cleared.
+ $properties['uid'] = array(
+ 'type' => 'user',
+ 'description' => t('User associated with TMGMT Job Message entity.'),
+ ) + (isset($properties['uid']) ? $properties['uid'] : array());
+
+ return $info;
+ }
+
+}
+
+/**
+ * Metadata controller for the translator entity.
+ */
+class TMGMTTranslatorMetadataController extends EntityDefaultMetadataController {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function entityPropertyInfo() {
+ $info = parent::entityPropertyInfo();
+ $info = _tmgmt_override_property_description($info, $this->type);
+ $properties = &$info[$this->type]['properties'];
+
+ // Options list callback for the translator plugin labels.
+ $properties['plugin']['options list'] = 'tmgmt_translator_plugin_labels';
+
+ return $info;
+ }
+
+}
+
+/**
+ * Populates all entity property descriptions based on the schema definition.
+ *
+ * @param $info
+ * Entity propety info array.
+ *
+ * @return
+ * The altered entity properties array.
+ */
+function _tmgmt_override_property_description($info, $entity_type) {
+ // Load tmgmt.install so we can access the schema.
+ module_load_install('tmgmt');
+ $entity_info = entity_get_info($entity_type);
+ $schema = tmgmt_schema();
+ $fields = $schema[$entity_info['base table']]['fields'];
+ $properties = &$info[$entity_type]['properties'];
+ foreach ($properties as $name => $property_info) {
+ if (isset($fields[$name]['description'])) {
+ $properties[$name]['description'] = $fields[$name]['description'];
+ }
+ }
+ return $info;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.base.inc b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.base.inc
new file mode 100644
index 00000000..d14a6320
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.base.inc
@@ -0,0 +1,38 @@
+pluginType = $plugin;
+ $this->pluginInfo = _tmgmt_plugin_info($type, $plugin);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pluginInfo() {
+ return $this->pluginInfo;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pluginType() {
+ return $this->pluginType;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.interface.base.inc b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.interface.base.inc
new file mode 100644
index 00000000..74c2f705
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.interface.base.inc
@@ -0,0 +1,35 @@
+ remote language codes.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function getDefaultRemoteLanguagesMappings();
+
+ /**
+ * Gets all supported languages of the translator.
+ *
+ * This list of all language codes used by the remote translator is then used
+ * for example in the translator settings form to select which remote language
+ * code correspond to which local language code.
+ *
+ * @param TMGMTTranslator $translator
+ * Translator entity for which to get supported languages.
+ *
+ * @return array
+ * An array of language codes which are provided by the translator
+ * (remote language codes).
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function getSupportedRemoteLanguages(TMGMTTranslator $translator);
+
+ /**
+ * Gets existing remote languages mappings.
+ *
+ * This method is responsible to provide all local to remote language pairs.
+ *
+ * @param TMGMTTranslator $translator
+ * Translator entity for which to get mappings.
+ *
+ * @return array
+ * An array of local => remote language codes.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function getRemoteLanguagesMappings(TMGMTTranslator $translator);
+
+ /**
+ * Maps local language to remote language.
+ *
+ * @param TMGMTTranslator $translator
+ * Translator entity for which to get remote language.
+ * @param $language
+ * Local language code.
+ *
+ * @return string
+ * Remote language code.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function mapToRemoteLanguage(TMGMTTranslator $translator, $language);
+
+ /**
+ * Maps remote language to local language.
+ *
+ * @param TMGMTTranslator $translator
+ * Translator entity for which to get local language.
+ * @param $language
+ * Remote language code.
+ *
+ * @return string
+ * Local language code.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function mapToLocalLanguage(TMGMTTranslator $translator, $language);
+
+ /**
+ * Returns all available target languages that are supported by this service
+ * when given a source language.
+ *
+ * @param TMGMTTranslator $translator
+ * The translator entity.
+ * @param $source_language
+ * The source language.
+ *
+ * @return array
+ * An array of remote languages in ISO format.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language);
+
+ /**
+ * Returns supported language pairs.
+ *
+ * This info may be used by other plugins to find out what language pairs
+ * can handle the translator.
+ *
+ * @param TMGMTTranslator $translator
+ * The translator entity.
+ *
+ * @return array
+ * List of language pairs where a pair is an associative array of
+ * source_language and target_language.
+ * Example:
+ * array(
+ * array('source_language' => 'en-US', 'target_language' => 'de-DE'),
+ * array('source_language' => 'en-US', 'target_language' => 'de-CH'),
+ * )
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function getSupportedLanguagePairs(TMGMTTranslator $translator);
+
+ /**
+ * @abstract
+ *
+ * Submits the translation request and sends it to the translation provider.
+ *
+ * @param TMGMTJob $job
+ * The job that should be submitted.
+ *
+ * @ingroup tmgmt_remote_languages_mapping
+ */
+ public function requestTranslation(TMGMTJob $job);
+
+ /**
+ * Aborts a translation job.
+ *
+ * @param TMGMTJob $job
+ * The job that should have its translation aborted.
+ *
+ * @return boolean
+ * TRUE if the job could be aborted, FALSE otherwise.
+ */
+ public function abortTranslation(TMGMTJob $job);
+
+ /**
+ * Defines default settings.
+ *
+ * @return array
+ * An array of default settings.
+ */
+ public function defaultSettings();
+
+ /**
+ * Returns if the translator has any settings for the passed job.
+ */
+ public function hasCheckoutSettings(TMGMTJob $job);
+
+ /**
+ * Accept a single data item.
+ *
+ * @todo Using job item breaks the current convention which uses jobs.
+ *
+ * @param $job_item
+ * The Job item the accepted data item belongs to.
+ * @param $key
+ * The key of the accepted data item.
+ * The key is an array containing the keys of a nested array hierarchy path.
+ *
+ * @return
+ * TRUE if the approving was succesfull, FALSE otherwise.
+ * In case of an error, it is the responsibility of the translator to
+ * provide informations about the failure by adding a message to the job
+ * item.
+ */
+ public function acceptetDataItem(TMGMTJobItem $job_item, array $key);
+
+ /**
+ * Returns the escaped #text of a data item.
+ *
+ * @param array $data_item
+ * A data item with a #text and optional #escape definitions.
+ *
+ * @return string
+ * The text of the data item with translator-specific escape patterns
+ * applied.
+ */
+ public function escapeText(array $data_item);
+
+ /**
+ * Removes escape patterns from an escaped text.
+ *
+ * @param string $text
+ * The text from which escape patterns should be removed.
+ *
+ * @return string
+ * The unescaped text.
+ */
+ public function unescapeText($text);
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.source.inc b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.source.inc
new file mode 100644
index 00000000..77456298
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.source.inc
@@ -0,0 +1,64 @@
+ $this->pluginInfo['label'], '@item' => $job_item->item_type . ':' . $job_item->item_id));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUri(TMGMTJobItem $job_item) {
+ return array(
+ 'path' => '',
+ 'options' => array(),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemTypes() {
+ return isset($this->pluginInfo['item types']) ? $this->pluginInfo['item types'] : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemTypeLabel($type) {
+ $types = $this->getItemTypes();
+ if (isset($types[$type])) {
+ return $types[$type];
+ }
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType(TMGMTJobItem $job_item) {
+ return ucfirst($job_item->item_type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLangCodes(TMGMTJobItem $job_item) {
+ return array();
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.translator.inc b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.translator.inc
new file mode 100644
index 00000000..ba1bc815
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.plugin.translator.inc
@@ -0,0 +1,246 @@
+isAvailable($translator) && array_key_exists($job->target_language, $translator->getSupportedTargetLanguages($job->source_language))) {
+ // We can only translate this job if the target language of the job is in
+ // one of the supported languages.
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function abortTranslation(TMGMTJob $job) {
+ // Assume that we can abort a translation job at any time.
+ $job->aborted();
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultRemoteLanguagesMappings() {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedRemoteLanguages(TMGMTTranslator $translator) {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemoteLanguagesMappings(TMGMTTranslator $translator) {
+ if (!empty($this->remoteLanguagesMappings)) {
+ return $this->remoteLanguagesMappings;
+ }
+
+ foreach (language_list() as $language => $info) {
+ $this->remoteLanguagesMappings[$language] = $this->mapToRemoteLanguage($translator, $language);
+ }
+
+ return $this->remoteLanguagesMappings;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mapToRemoteLanguage(TMGMTTranslator $translator, $language) {
+ if (!tmgmt_provide_remote_languages_mappings($translator)) {
+ return $language;
+ }
+
+ if (!empty($translator->settings['remote_languages_mappings'][$language])) {
+ return $translator->settings['remote_languages_mappings'][$language];
+ }
+
+ $default_mappings = $this->getDefaultRemoteLanguagesMappings();
+
+ if (isset($default_mappings[$language])) {
+ return $default_mappings[$language];
+ }
+
+ return $language;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mapToLocalLanguage(TMGMTTranslator $translator, $language) {
+ if (!tmgmt_provide_remote_languages_mappings($translator)) {
+ return $language;
+ }
+
+ if (isset($translator->settings['remote_languages_mappings']) && is_array($translator->settings['remote_languages_mappings'])) {
+ $mappings = $translator->settings['remote_languages_mappings'];
+ }
+ else {
+ $mappings = $this->getDefaultRemoteLanguagesMappings();
+ }
+
+ if ($remote_language = array_search($language, $mappings)) {
+ return $remote_language;
+ }
+
+ return $language;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
+ $languages = entity_metadata_language_list();
+ unset($languages[LANGUAGE_NONE], $languages[$source_language]);
+ return drupal_map_assoc(array_keys($languages));
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Default implementation that gets target languages for each remote language.
+ * This approach is ineffective and therefore it is advised that a plugin
+ * should provide own implementation.
+ */
+ public function getSupportedLanguagePairs(TMGMTTranslator $translator) {
+ $language_pairs = array();
+
+ foreach ($this->getSupportedRemoteLanguages($translator) as $source_language) {
+ foreach ($this->getSupportedTargetLanguages($translator, $source_language) as $target_language) {
+ $language_pairs[] = array('source_language' => $source_language, 'target_language' => $target_language);
+ }
+ }
+
+ return $language_pairs;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNotCanTranslateReason(TMGMTJob $job) {
+ $wrapper = entity_metadata_wrapper('tmgmt_job', $job);
+ return t('@translator can not translate from @source to @target.', array('@translator' => $job->getTranslator()->label(), '@source' => $wrapper->source_language->label(), '@target' => $wrapper->target_language->label()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNotAvailableReason(TMGMTTranslator $translator) {
+ return t('@translator is not available. Make sure it is properly !configured.', array('@translator' => $this->pluginInfo['label'], '!configured' => l(t('configured'), 'admin/config/regional/tmgmt_translator/manage/' . $translator->name)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultSettings() {
+ $defaults = array('auto_accept' => FALSE);
+ // Check if any default settings are defined in the plugin info.
+ if (isset($this->pluginInfo['default settings'])) {
+ return array_merge($defaults, $this->pluginInfo['default settings']);
+ }
+ return $defaults;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCheckoutSettings(TMGMTJob $job) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function acceptetDataItem(TMGMTJobItem $job_item, array $key) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function escapeText(array $data_item) {
+ if (empty($data_item['#escape'])) {
+ return $data_item['#text'];
+ }
+
+ $text = $data_item['#text'];
+ $escape = $data_item['#escape'];
+
+ // Sort them in reverse order based/ on the position and process them,
+ // so that positions don't change.
+ krsort($escape, SORT_NUMERIC);
+
+ foreach ($escape as $position => $info) {
+ $text = substr_replace($text, $this->getEscapedString($info['string']), $position, strlen($info['string']));
+ }
+
+ return $text;
+ }
+
+ /**
+ * Returns the escaped string.
+ *
+ * Defaults to use the escapeStart and escapeEnd properties but can be
+ * overriden if a non-static replacement pattern is used.
+ *
+ * @param string $string
+ * String that should be escaped.
+ * @return string
+ */
+ protected function getEscapedString($string) {
+ return $this->escapeStart . $string . $this->escapeEnd;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function unescapeText($text) {
+ return preg_replace('/' . preg_quote($this->escapeStart, '/') . '(.+)' . preg_quote($this->escapeEnd, '/') . '/U', '$1', $text);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.ui.interface.source.inc b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.ui.interface.source.inc
new file mode 100644
index 00000000..9e2fe602
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.ui.interface.source.inc
@@ -0,0 +1,52 @@
+pluginType)) {
+ $defaults = array(
+ 'page callback' => 'drupal_get_form',
+ 'access callback' => 'tmgmt_job_access',
+ 'access arguments' => array('create'),
+ );
+ if (isset($this->pluginInfo['file'])) {
+ $defaults['file'] = $this->pluginInfo['file'];
+ }
+ if (isset($this->pluginInfo['file path'])) {
+ $defaults['file path'] = $this->pluginInfo['file path'];
+ }
+ foreach ($types as $type => $name) {
+ $items['admin/tmgmt/sources/' . $this->pluginType . '_' . $type] = $defaults + array(
+ 'title' => check_plain($name),
+ 'page arguments' => array('tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form', $this->pluginType, $type),
+ 'type' => MENU_LOCAL_TASK,
+ );
+ }
+ }
+ return $items;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hook_forms() {
+ $info = array();
+ if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
+ foreach (array_keys($types) as $type) {
+ $info['tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form'] = array(
+ 'callback' => 'tmgmt_ui_source_overview_form',
+ 'wrapper_callback' => 'tmgmt_ui_source_overview_form_defaults',
+ );
+ }
+ }
+ return $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hook_views_default_views() {
+ return array();
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.ui.translator.inc b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.ui.translator.inc
new file mode 100644
index 00000000..ec48a88b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/plugin/tmgmt.ui.translator.inc
@@ -0,0 +1,128 @@
+plugin)) {
+ $controller = tmgmt_translator_plugin_controller($translator->plugin);
+ }
+
+ // If current translator is configured to provide remote language mapping
+ // provide the form to configure mappings, unless it does not exists yet.
+ if (!empty($controller) && tmgmt_provide_remote_languages_mappings($translator)) {
+
+ $form['remote_languages_mappings'] = array(
+ '#tree' => TRUE,
+ '#type' => 'fieldset',
+ '#title' => t('Remote languages mappings'),
+ '#description' => t('Here you can specify mappings of your local language codes to the translator language codes.'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+
+ $options = array();
+ foreach ($controller->getSupportedRemoteLanguages($translator) as $language) {
+ $options[$language] = $language;
+ }
+
+ ksort($options);
+
+ foreach ($controller->getRemoteLanguagesMappings($translator) as $local_language => $remote_language) {
+ $form['remote_languages_mappings'][$local_language] = array(
+ '#type' => 'textfield',
+ '#title' => tmgmt_language_label($local_language) . ' (' . $local_language . ')',
+ '#default_value' => $remote_language,
+ '#size' => 6,
+ );
+
+ if (!empty($options)) {
+ $form['remote_languages_mappings'][$local_language]['#type'] = 'select';
+ $form['remote_languages_mappings'][$local_language]['#options'] = $options;
+ $form['remote_languages_mappings'][$local_language]['#empty_option'] = ' - ';
+ unset($form['remote_languages_mappings'][$local_language]['#size']);
+ }
+ }
+ }
+
+ if (!element_children($form)) {
+ $form['#description'] = t("The @plugin plugin doesn't provide any settings.", array('@plugin' => $this->pluginInfo['label']));
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
+ if (!element_children($form)) {
+ $form['#description'] = t("The @translator translator doesn't provide any checkout settings.", array('@translator' => $job->getTranslator()->label()));
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkoutInfo(TMGMTJob $job) {
+ return array();
+ }
+
+ /**
+ * Provides a simple wrapper for the checkout info fieldset.
+ *
+ * @param TMGMTJob $job
+ * Translation job object.
+ * @param $form
+ * Partial form structure to be wrapped in the fieldset.
+ *
+ * @return
+ * The provided form structure wrapped in a collapsed fieldset.
+ */
+ public function checkoutInfoWrapper(TMGMTJob $job, $form) {
+ $label = $job->getTranslator()->label();
+ $form += array(
+ '#title' => t('@translator translation job information', array('@translator' => $label)),
+ '#type' => 'fieldset',
+ '#collapsible' => TRUE,
+ );
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reviewDataItemElement($form, &$form_state, $data_item_key, $parent_key, array $data_item, TMGMTJobItem $item) {
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
+ // Nothing to do here by default.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
+ // Nothing to do here by default.
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/css/tmgmt_entity.admin.entity_source_search_form.css b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/css/tmgmt_entity.admin.entity_source_search_form.css
new file mode 100644
index 00000000..4840f5ae
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/css/tmgmt_entity.admin.entity_source_search_form.css
@@ -0,0 +1,8 @@
+.tmgmt-entity-sources-wrapper .form-item {
+ float: left;
+ margin: 0 10px 0 0;
+}
+
+.tmgmt-entity-sources-wrapper #edit-search-submit {
+ margin: 26px 10px 0 10px;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tests/tmgmt_entity_test.info b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tests/tmgmt_entity_test.info
new file mode 100644
index 00000000..f9821209
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tests/tmgmt_entity_test.info
@@ -0,0 +1,12 @@
+name = "Entity source plugin tests"
+description = "Support module for entity source testing."
+package = Testing
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tests/tmgmt_entity_test.module b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tests/tmgmt_entity_test.module
new file mode 100644
index 00000000..98ad050f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tests/tmgmt_entity_test.module
@@ -0,0 +1,20 @@
+ array(
+ 'base path' => 'taxonomy/term/%taxonomy_term',
+ 'alias' => TRUE,
+ ),
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.api.php b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.api.php
new file mode 100644
index 00000000..2fff6e27
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.api.php
@@ -0,0 +1,25 @@
+entityCondition('type', array('article', 'page'));
+}
+
+/**
+ * @} End of "addtogroup tmgmt_source".
+ */
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.info b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.info
new file mode 100644
index 00000000..01e37fb5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.info
@@ -0,0 +1,27 @@
+name = Entity Source
+description = Entity source plugin for the Translation Management system.
+package = Translation Management
+core = 7.x
+
+dependencies[] = tmgmt
+dependencies[] = tmgmt_field
+dependencies[] = entity
+dependencies[] = entity_translation
+
+test_dependencies[] = pathauto
+test_dependencies[] = file_entity
+test_dependencies[] = entityreference
+
+files[] = tmgmt_entity.source.test
+files[] = tmgmt_entity.source.none.test
+files[] = tmgmt_entity.pathauto.test
+files[] = tmgmt_entity.suggestions.test
+files[] = tmgmt_entity.plugin.inc
+files[] = tmgmt_entity.ui.inc
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.module b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.module
new file mode 100644
index 00000000..1abbc609
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.module
@@ -0,0 +1,332 @@
+ t('Entity'),
+ 'description' => t('Source handler for entities.'),
+ 'plugin controller class' => 'TMGMTEntitySourcePluginController',
+ 'item types' => array(),
+ );
+
+ $entity_types = array_filter(variable_get('entity_translation_entity_types', array()));
+
+ foreach ($entity_types as $entity_key) {
+ $entity_info = entity_get_info($entity_key);
+ $info['entity']['item types'][$entity_key] = $entity_info['label'];
+ }
+
+ return $info;
+}
+
+/**
+ * Implements hook_form_ID_alter().
+ *
+ * Alters comment node type select box to filter out comment types that belongs
+ * to non entity translatable node types.
+ */
+function tmgmt_entity_form_tmgmt_ui_entity_source_comment_overview_form_alter(&$form, &$form_state) {
+
+ if (!isset($form['search_wrapper']['search']['node_type'])) {
+ return;
+ }
+
+ // Change the select name to "type" as in the query submitted value will be
+ // passed into node.type condition.
+ $form['search_wrapper']['search']['type'] = $form['search_wrapper']['search']['node_type'];
+ unset($form['search_wrapper']['search']['node_type']);
+
+ // Set new default value.
+ $form['search_wrapper']['search']['type']['#default_value'] = isset($_GET['type']) ? $_GET['type'] : NULL;
+
+}
+
+/**
+ * Helper function to get entity translatable bundles.
+ *
+ * Note that for comment entity type it will return the same as for node as
+ * comment bundles have no use (i.e. in queries).
+ *
+ * @param string $entity_type
+ * Drupal entity type.
+ *
+ * @return array
+ * Array of key => values, where key is type and value its label.
+ */
+function tmgmt_entity_get_translatable_bundles($entity_type) {
+
+ // If given entity type does not have entity translations enabled, no reason
+ // to continue.
+ if (!in_array($entity_type, variable_get('entity_translation_entity_types', array()))) {
+ return array();
+ }
+
+ $entity_info = entity_get_info($entity_type);
+ $translatable_bundle_types = array();
+
+ foreach ($entity_info['bundles'] as $bundle_type => $bundle_definition) {
+
+ if ($entity_type == 'comment') {
+ $bundle_type = str_replace('comment_node_', '', $bundle_type);
+ if (variable_get('language_content_type_' . $bundle_type) == ENTITY_TRANSLATION_ENABLED) {
+ $translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
+ }
+ }
+ elseif ($entity_type == 'node') {
+ if (variable_get('language_content_type_' . $bundle_type) == ENTITY_TRANSLATION_ENABLED) {
+ $translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
+ }
+ }
+ else {
+ $translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
+ }
+ }
+
+ return $translatable_bundle_types;
+}
+
+/**
+ * Gets translatable entities of a given type.
+ *
+ * Additionally you can specify entity property conditions, pager and limit.
+ *
+ * @param string $entity_type
+ * Drupal entity type.
+ * @param array $property_conditions
+ * Entity properties. There is no value processing so caller must make sure
+ * the provided entity property exists for given entity type and its value
+ * is processed.
+ * @param bool $pager
+ * Flag to determine if pager will be used.
+ *
+ * @return array
+ * Array of translatable entities.
+ */
+function tmgmt_entity_get_translatable_entities($entity_type, $property_conditions = array(), $pager = FALSE) {
+
+ if (!in_array($entity_type, variable_get('entity_translation_entity_types', array()))) {
+ return array();
+ }
+
+ $languages = drupal_map_assoc(array_keys(language_list()));
+
+ $entity_info = entity_get_info($entity_type);
+ $label_key = isset($entity_info['entity keys']['label']) ? $entity_info['entity keys']['label'] : NULL;
+
+ $id_key = $entity_info['entity keys']['id'];
+ $query = db_select($entity_info['base table'], 'e');
+ $query->addTag('tmgmt_entity_get_translatable_entities');
+ $query->addField('e', $id_key);
+
+ // Language neutral entities are not translatable. Filter them out. To do
+ // that: join {entity_translation} table, but only records with source column
+ // empty. The {entity_translation}.language will represent the original entity
+ // language in that case.
+ $source_table_alias = $query->leftJoin('entity_translation', NULL, "%alias.entity_type = :entity_type AND %alias.entity_id = e.$id_key AND %alias.source = ''", array(':entity_type' => $entity_type));
+ $query->condition("$source_table_alias.language", LANGUAGE_NONE, '<>');
+
+ // Searching for sources with missing translation.
+ if (!empty($property_conditions['target_status']) && !empty($property_conditions['target_language']) && in_array($property_conditions['target_language'], $languages)) {
+
+ $translation_table_alias = db_escape_field('et_' . $property_conditions['target_language']);
+ $query->leftJoin('entity_translation', $translation_table_alias, "%alias.entity_type = :entity_type AND %alias.entity_id = e.$id_key AND %alias.language = :language",
+ array(':entity_type' => $entity_type, ':language' => $property_conditions['target_language']));
+
+ // Exclude entities with having source language same as the target language
+ // we search for.
+ $query->condition('e.language', $property_conditions['target_language'], '<>');
+
+ if ($property_conditions['target_status'] == 'untranslated_or_outdated') {
+ $or = db_or();
+ $or->isNull("$translation_table_alias.language");
+ $or->condition("$translation_table_alias.translate", 1);
+ $query->condition($or);
+ }
+ elseif ($property_conditions['target_status'] == 'outdated') {
+ $query->condition("$translation_table_alias.translate", 1);
+ }
+ elseif ($property_conditions['target_status'] == 'untranslated') {
+ $query->isNull("$translation_table_alias.language");
+ }
+ }
+
+ // Remove the condition so we do not try to add it again below.
+ unset($property_conditions['target_language']);
+ unset($property_conditions['target_status']);
+
+ // Searching for the source label.
+ if (!empty($label_key) && isset($property_conditions[$label_key])) {
+ $search_tokens = explode(' ', $property_conditions[$label_key]);
+ $or = db_or();
+
+ foreach ($search_tokens as $search_token) {
+ $search_token = trim($search_token);
+ if (strlen($search_token) > 2) {
+ $or->condition($label_key, "%$search_token%", 'LIKE');
+ }
+ }
+
+ if ($or->count() > 0) {
+ $query->condition($or);
+ }
+
+ unset($property_conditions[$label_key]);
+ }
+
+ // Searching by taxonomy bundles - we need to switch to vid as the bundle key.
+ if ($entity_type == 'taxonomy_term' && !empty($property_conditions['vocabulary_machine_name'])) {
+ $property_name = 'vid';
+ $vocabulary = taxonomy_vocabulary_machine_name_load($property_conditions['vocabulary_machine_name']);
+ $property_value = $vocabulary->vid;
+ $query->condition('e.' . $property_name, $property_value);
+ // Remove the condition so we do not try to add it again below.
+ unset($property_conditions['vocabulary_machine_name']);
+ }
+ // Searching by the node bundles - that applies for node entities as well as
+ // comment.
+ elseif (in_array($entity_type, array('comment', 'node'))) {
+ $node_table_alias = 'e';
+
+ // For comments join node table so that we can filter based on type.
+ if ($entity_type == 'comment') {
+ $query->join('node', 'n', 'e.nid = n.nid');
+ $node_table_alias = 'n';
+ }
+
+ // Get translatable node types and check if it is worth to continue.
+ $translatable_node_types = array_keys(tmgmt_entity_get_translatable_bundles('node'));
+ if (empty($translatable_node_types)) {
+ return array();
+ }
+
+ // If we have type property add condition.
+ if (isset($property_conditions['type'])) {
+ $query->condition($node_table_alias . '.type', $property_conditions['type']);
+ // Remove the condition so we do not try to add it again below.
+ unset($property_conditions['type']);
+ }
+ // If not, query db only for translatable node types.
+ else {
+ $query->condition($node_table_alias . '.type', $translatable_node_types);
+ }
+ }
+
+ // Add remaining query conditions which are expected to be handled in a
+ // generic way.
+ foreach ($property_conditions as $property_name => $property_value) {
+ $query->condition('e.' . $property_name, $property_value);
+ }
+
+ if ($pager) {
+ $query = $query->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
+ }
+ else {
+ $query->range(0, variable_get('tmgmt_source_list_limit', 20));
+ }
+
+ $query->orderBy($entity_info['entity keys']['id'], 'DESC');
+ $entity_ids = $query->execute()->fetchCol();
+ $entities = array();
+
+ if (!empty($entity_ids)) {
+ $entities = entity_load($entity_type, $entity_ids);
+ }
+
+ return $entities;
+}
+
+/**
+ * Implements hook_tmgmt_source_suggestions()
+ */
+function tmgmt_entity_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
+ $suggestions = array();
+ // Get all translatable entity types.
+ $entity_types = array_filter(variable_get('entity_translation_entity_types', array()));
+
+ foreach ($items as $item) {
+ if (($item instanceof TMGMTJobItem) && ($item->plugin == 'entity') || ($item->plugin == 'node')) {
+ // Load the entity and extract the bundle name to get all fields from the
+ // current entity.
+ $entity = entity_load_single($item->item_type, $item->item_id);
+ list(, , $bundle) = entity_extract_ids($item->item_type, $entity);
+ $field_instances = field_info_instances($item->item_type, $bundle);
+
+
+ // Loop over all fields, check if they are NOT translatable. Only if a
+ // field is not translatable we may suggest a referenced entity. If so,
+ // check for a supported field type (image and file currently here).
+ foreach ($field_instances as $instance) {
+ $field = field_info_field($instance['field_name']);
+ $field_type = $field['type'];
+ $field_name = $field['field_name'];
+ switch ($field_type) {
+ case 'file':
+ case 'image':
+ // 'File' (and images) must be translatable entity types.
+ // Other files we not suggest here. Get all field items from the
+ // current field and suggest them as translatable.
+ if (isset($entity_types['file']) && ($field_items = field_get_items($item->item_type, $entity, $field_name))) {
+ // Add all files as a suggestion.
+ foreach ($field_items as $field_item) {
+ $file_entity = entity_load_single('file', $field_item['fid']);
+
+ // Check if there is already a translation available for this
+ // file. If so, just continue with the next file.
+ $handler = entity_translation_get_handler('file', $file_entity);
+ if ($handler instanceof EntityTranslationHandlerInterface) {
+ $translations = $handler->getTranslations();
+ if (isset($translations->data[$job->target_language])) {
+ continue;
+ }
+ }
+
+ // Add the translation as a suggestion.
+ $suggestions[] = array(
+ 'job_item' => tmgmt_job_item_create('entity', 'file', $file_entity->fid),
+ 'reason' => t('Field @label', array('@label' => $instance['label'])),
+ 'from_item' => $item->tjiid,
+ );
+ }
+ }
+ break;
+
+ case 'entityreference':
+ $target_type = $field['settings']['target_type'];
+ // Make sure only tranlatable entity types are suggested.
+ if (isset($entity_types[$target_type]) && ($field_items = field_get_items($item->item_type, $entity, $field_name))) {
+ // Add all referenced entities as suggestion.
+ foreach ($field_items as $field_item) {
+ $ref_entity = entity_load_single($target_type, $field_item['target_id']);
+
+ // Check if there is already a translation available for this
+ // entity. If so, just continue with the next one.
+ $handler = entity_translation_get_handler($target_type, $ref_entity);
+ if ($handler instanceof EntityTranslationHandlerInterface) {
+ $translations = $handler->getTranslations();
+ if (isset($translations->data[$job->target_language])) {
+ continue;
+ }
+ }
+
+ // Add suggestion.
+ $suggestions[] = array(
+ 'job_item' => tmgmt_job_item_create('entity', $target_type, $field_item['target_id']),
+ 'reason' => t('Field @label', array('@label' => $instance['label'])),
+ 'from_item' => $item->tjiid,
+ );
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ return $suggestions;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.pathauto.test b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.pathauto.test
new file mode 100644
index 00000000..801784ac
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.pathauto.test
@@ -0,0 +1,55 @@
+ 'Entity Source Pathauto tests',
+ 'description' => 'Verifies that the correct aliases are generated for entity transations',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('entity_translation', 'pathauto'),
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_entity', 'entity_translation', 'pathauto'));
+ $this->loginAsAdmin();
+ $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
+ }
+
+ /**
+ * Tests that pathauto aliases are correctly created.
+ */
+ function testAliasCreation() {
+ $this->setEnvironment('de');
+
+ // Create a translation job.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ // Create a node.
+ $node = $this->createNode('article');
+ // Create a job item for this node and add it to the job.
+ $job->addItem('entity', 'node', $node->nid);
+
+ // Translate the job.
+ $job->requestTranslation();
+
+ // Check the translated job items.
+ foreach ($job->getItems() as $item) {
+ $item->acceptTranslation();
+ }
+
+ // Make sure that the correct url aliases were created.
+ $aliases = db_query('SELECT * FROM {url_alias} where source = :source', array(':source' => 'node/' . $node->nid))->fetchAllAssoc('language');
+ $this->assertEqual(2, count($aliases));
+ $this->assertTrue(isset($aliases['en']), 'English alias created.');
+ $this->assertTrue(isset($aliases['de']), 'German alias created.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.plugin.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.plugin.inc
new file mode 100644
index 00000000..90cf76e9
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.plugin.inc
@@ -0,0 +1,110 @@
+item_type, $job_item->item_id)) {
+ return entity_label($job_item->item_type, $entity);
+ }
+ }
+
+ public function getUri(TMGMTJobItem $job_item) {
+ if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
+ return entity_uri($job_item->item_type, $entity);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Returns the data from the fields as a structure that can be processed by
+ * the Translation Management system.
+ */
+ public function getData(TMGMTJobItem $job_item) {
+ $entity = entity_load_single($job_item->item_type, $job_item->item_id);
+ if (!$entity) {
+ throw new TMGMTException(t('Unable to load entity %type with id %id', array('%type' => $job_item->item_type, $job_item->item_id)));
+ }
+ if (entity_language($job_item->item_type, $entity) == LANGUAGE_NONE) {
+ throw new TMGMTException(t('Entity %entity could not be translated because it is language neutral', array('%entity' => entity_label($job_item->item_type, $entity))));
+ }
+ return tmgmt_field_get_source_data($job_item->item_type, $entity, $job_item->getJob()->source_language, TRUE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveTranslation(TMGMTJobItem $job_item) {
+ $entity = entity_load_single($job_item->item_type, $job_item->item_id);
+ $job = tmgmt_job_load($job_item->tjid);
+
+ tmgmt_field_populate_entity($job_item->item_type, $entity, $job->target_language, $job_item->getData());
+
+ // Change the active language of the entity to the target language.
+ $handler = entity_translation_get_handler($job_item->item_type, $entity);
+ $handler->setFormLanguage($job_item->getJob()->target_language);
+
+ entity_save($job_item->item_type, $entity);
+
+ $translation = array(
+ // @todo Improve hardcoded values.
+ 'translate' => 0,
+ 'status' => TRUE,
+ 'language' => $job_item->getJob()->target_language,
+ 'source' => $job_item->getJob()->source_language,
+ );
+ $handler->setTranslation($translation);
+ $handler->saveTranslations();
+ $job_item->accepted();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType(TMGMTJobItem $job_item) {
+ if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
+ $bundles = tmgmt_entity_get_translatable_bundles($job_item->item_type);
+ $info = entity_get_info($job_item->item_type);
+ list(, , $bundle) = entity_extract_ids($job_item->item_type, $entity);
+ // Display entity type and label if we have one and the bundle isn't
+ // the same as the entity type.
+ if (isset($bundles[$bundle]) && $bundle != $job_item->item_type) {
+ return t('@type (@bundle)', array('@type' => $info['label'], '@bundle' => $bundles[$bundle]));
+ }
+ // Otherwise just display the entity type label.
+ elseif (isset($info['label'])) {
+ return $info['label'];
+ }
+ return parent::getType($job_item);
+ }
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSourceLangCode(TMGMTJobItem $job_item) {
+ $entity = entity_load_single($job_item->item_type, $job_item->item_id);
+ return isset($entity->translations->original) ? $entity->translations->original : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLangCodes(TMGMTJobItem $job_item) {
+ if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
+ $entity_info = entity_get_info($job_item->item_type);
+ if (isset($entity_info['entity keys']['translations'])){
+ $translations_key = $entity_info['entity keys']['translations'];
+ return array_keys($entity->{$translations_key}->data);
+ }
+ }
+
+ return array();
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.source.none.test b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.source.none.test
new file mode 100644
index 00000000..34a0150f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.source.none.test
@@ -0,0 +1,101 @@
+ 'Entity Source Neutral tests',
+ 'description' => 'Tests that LANGUAGE_NONE entities can not be translated',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('entity_translation'),
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_entity', 'taxonomy', 'entity_translation'));
+
+ // Admin user to perform settings on setup.
+ $this->loginAsAdmin(array('administer entity translation'));
+
+ $this->vocabulary = $this->createTaxonomyVocab(strtolower($this->randomName()), $this->randomName(), array(FALSE, TRUE, TRUE, TRUE));
+
+ // Enable entity translations for taxonomy.
+ $edit['entity_translation_entity_types[taxonomy_term]'] = 1;
+ $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
+ }
+
+ /**
+ * Test if language neutral entities are not allowed for translation.
+ *
+ * That behaviour is described in the entity_translation documentation:
+ * https://www.drupal.org/node/1280934
+ */
+ function testLanguageNeutral() {
+ $this->setEnvironment('de');
+
+ // Structure: array({entity-type} => array({source-langcode} => {entity}))
+ $test_data = array();
+
+ $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
+ $test_data['node'][LANGUAGE_NONE] = $this->createNode('article', LANGUAGE_NONE);
+ $test_data['node']['en'] = $this->createNode('article', 'en');
+ $test_data['node']['de'] = $this->createNode('article', 'de');
+
+ $test_data['taxonomy_term'][LANGUAGE_NONE] = $this->createTaxonomyTerm($this->vocabulary, LANGUAGE_NONE);
+ $test_data['taxonomy_term']['en'] = $this->createTaxonomyTerm($this->vocabulary, 'en');
+ $test_data['taxonomy_term']['de'] = $this->createTaxonomyTerm($this->vocabulary, 'de');
+
+ // Test if tmgmt_entity_get_translatable_entities() function excludes
+ // language neutral entities.
+ foreach ($test_data as $entity_type => $entities) {
+ $translatable_entities = tmgmt_entity_get_translatable_entities($entity_type);
+ foreach ($entities as $langcode => $entity) {
+ list($id, , ) = entity_extract_ids($entity_type, $entity);
+ if ($langcode == LANGUAGE_NONE) {
+ $this->assert(!isset($translatable_entities[$id]), "Language neutral $entity_type entity does not exist in the translatable entities list.");
+ }
+ else {
+ $this->assert(isset($translatable_entities[$id]), "$langcode $entity_type entity exists in the translatable entities list.");
+ }
+ }
+ }
+
+ // Test if language neutral entities can't be added to a translation job.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+ foreach ($test_data as $entity_type => $entities) {
+ foreach ($entities as $langcode => $entity) {
+ list($id, , ) = entity_extract_ids($entity_type, $entity);
+ try {
+ $job->addItem('entity', $entity_type, $id);
+ if ($langcode == LANGUAGE_NONE) {
+ $this->fail("Adding of language neutral $entity_type entity to a translation job did not fail.");
+ }
+ else {
+ $this->pass("Adding of $langcode $entity_type entity node to a translation job did not fail.");
+ }
+ }
+ catch (TMGMTException $e) {
+ if ($langcode == LANGUAGE_NONE) {
+ $this->pass("Adding of language neutral $entity_type entity to a translation job did fail.");
+ }
+ else {
+ $this->fail("Adding of $langcode $entity_type entity node to a translation job did fail.");
+ }
+ }
+ }
+ }
+
+ $GLOBALS['TMGMT_DEBUG'] = FALSE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.source.test b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.source.test
new file mode 100644
index 00000000..cd81fe54
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.source.test
@@ -0,0 +1,212 @@
+ 'Entity Source tests',
+ 'description' => 'Exporting source data from entities and saving translations back to entities.',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('entity_translation'),
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_entity', 'taxonomy', 'entity_translation'));
+
+ // Admin user to perform settings on setup.
+ $this->loginAsAdmin(array('administer entity translation'));
+
+ $this->vocabulary = $this->createTaxonomyVocab(strtolower($this->randomName()), $this->randomName(), array(FALSE, TRUE, TRUE, TRUE));
+
+ // Enable entity translations for taxonomy.
+ $edit['entity_translation_entity_types[taxonomy_term]'] = 1;
+ $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
+ }
+
+ /**
+ * Tests nodes field translation.
+ */
+ function testEntitySourceNode() {
+ $this->setEnvironment('de');
+
+ $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
+
+ // Create a translation job.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ // Create some nodes.
+ for ($i = 1; $i <= 5; $i++) {
+ $node = $this->createNode('article');
+ // Create a job item for this node and add it to the job.
+ $item = $job->addItem('entity', 'node', $node->nid);
+ $this->assertEqual(t('@type (@bundle)', array('@type' => t('Node'), '@bundle' => 'Article')), $item->getSourceType());
+ }
+
+ // Translate the job.
+ $job->requestTranslation();
+
+ // Check the translated job items.
+ foreach ($job->getItems() as $item) {
+ // The source is available only for en.
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+ $item->acceptTranslation();
+ $this->assertTrue($item->isAccepted());
+ $entity = entity_load_single($item->item_type, $item->item_id);
+ $data = $item->getData();
+ $this->checkTranslatedData($entity, $data, 'de');
+ $this->checkUntranslatedData($entity, $this->field_names['node']['article'], $data, 'de');
+ // The source is now available for both en and de.
+ $this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
+ }
+ }
+
+ /**
+ * Tests taxonomy terms field translation.
+ */
+ function testEntitySourceTerm() {
+ $this->setEnvironment('de');
+
+ // Create the job.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $term = NULL;
+
+ //Create some terms.
+ for ($i = 1; $i <= 5; $i++) {
+ $term = $this->createTaxonomyTerm($this->vocabulary);
+ // Create the item and assign it to the job.
+ $item = $job->addItem('entity', 'taxonomy_term', $term->tid);
+ $this->assertEqual(t('@type (@bundle)', array('@type' => t('Taxonomy term'), '@bundle' => $this->vocabulary->name)), $item->getSourceType());
+ }
+ // Request the translation and accept it.
+ $job->requestTranslation();
+
+ // Check if the fields were translated.
+ foreach ($job->getItems() as $item) {
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+ $item->acceptTranslation();
+ $entity = entity_load_single($item->item_type, $item->item_id);
+ $data = $item->getData();
+ $this->checkTranslatedData($entity, $data, 'de');
+ $this->checkUntranslatedData($entity, $this->field_names['taxonomy_term'][$this->vocabulary->machine_name], $data, 'de');
+ $this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
+ }
+ }
+
+ function testAddingJobItemsWithEmptySourceText() {
+ $this->setEnvironment('de');
+
+ // Create term with empty texts.
+ $empty_term = new stdClass();
+ $empty_term->name = $this->randomName();
+ $empty_term->description = $this->randomName();
+ $empty_term->vid = $this->vocabulary->vid;
+ taxonomy_term_save($empty_term);
+
+ // Create the job.
+ $job = tmgmt_job_create('en', NULL);
+ try {
+ $job->addItem('entity', 'taxonomy_term', $empty_term->tid);
+ $this->fail('Job item added with empty source text.');
+ }
+ catch (TMGMTException $e) {
+ $this->assert(empty($job->tjid), 'After adding a job item with empty source text its tjid has to be unset.');
+ }
+
+ // Create term with populated source content.
+ $populated_content_term = $this->createTaxonomyTerm($this->vocabulary);
+
+ // Lets reuse the last created term with populated source content.
+ $job->addItem('entity', 'taxonomy_term', $populated_content_term->tid);
+ $this->assert(!empty($job->tjid), 'After adding another job item with populated source text its tjid must be set.');
+ }
+
+ /**
+ * Test if the source is able to pull content in requested language.
+ */
+ function testRequestDataForSpecificLanguage() {
+ $this->setEnvironment('de');
+ $this->setEnvironment('cs');
+
+ $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
+
+ // Create a translation job.
+ $job = $this->createJob('en', 'de');
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $node = $this->createNode('article', 'cs');
+ $node->body['en'][0]['value'] = 'en translation';
+ node_save($node);
+ $job->addItem('entity', 'node', $node->nid);
+
+ $data = $job->getData();
+ $this->assertEqual($data[1]['body'][0]['value']['#text'], 'en translation');
+ }
+
+ /**
+ * Compares the data from an entity with the translated data.
+ *
+ * @param $tentity
+ * The translated entity object.
+ * @param $data
+ * An array with the translated data.
+ * @param $langcode
+ * The code of the target language.
+ */
+ function checkTranslatedData($tentity, $data, $langcode) {
+ foreach (element_children($data) as $field_name) {
+ foreach (element_children($data[$field_name]) as $delta) {
+ foreach (element_children($data[$field_name][$delta]) as $column) {
+ $column_value = $data[$field_name][$delta][$column];
+ if (!empty($column_value['#translate'])) {
+ $this->assertEqual($tentity->{$field_name}[$langcode][$delta][$column], $column_value['#translation']['#text'], format_string('The field %field:%delta has been populated with the proper translated data.', array('%field' => $field_name, 'delta' => $delta)));
+ }
+ else {
+ $this->assertEqual($tentity->{$field_name}[$langcode][$delta][$column], $column_value['#text'], format_string('The field %field:%delta has been populated with the proper untranslated data.', array('%field' => $field_name, 'delta' => $delta)));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks the fields that should not be translated.
+ *
+ * @param $tentity
+ * The translated entity object.
+ * @param $fields
+ * An array with the field names to check.
+ * @param $translation
+ * An array with the translated data.
+ * @param $langcode
+ * The code of the target language.
+ */
+ function checkUntranslatedData($tentity, $fields, $data, $langcode) {
+ foreach ($fields as $field_name) {
+ $field_info = field_info_field($field_name);
+ if (!$field_info['translatable']) {
+ // Avoid some PHP warnings.
+ if (isset($data[$field_name])) {
+ $this->assertNull($data[$field_name]['#translation']['#text'], 'The not translatable field was not translated.');
+ }
+ if (isset($tentity->{$field_name}[$langcode])) {
+ $this->assertNull($tentity->{$field_name}[$langcode], 'The entity has translated data in a field that is translatable.');
+ }
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.suggestions.test b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.suggestions.test
new file mode 100644
index 00000000..f92b7cc9
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.suggestions.test
@@ -0,0 +1,274 @@
+ 'Entity Suggestions tests',
+ 'description' => 'Tests suggestion implementation for the entity source plugin',
+ 'group' => 'Translation Management',
+ 'dependencies' => array(
+ 'file_entity',
+ 'entityreference',
+ ),
+ );
+ }
+
+ public function setUp() {
+ parent::setUp(array('file_entity', 'tmgmt_entity', 'tmgmt_ui', 'entityreference', 'tmgmt_i18n_string', 'i18n_menu'));
+ $this->loginAsAdmin(array('administer entity translation'));
+
+ $this->setEnvironment('de');
+
+ // Enable entity translations for nodes and comments.
+ $edit = array();
+ $edit['entity_translation_entity_types[node]'] = 1;
+ $edit['entity_translation_entity_types[file]'] = 1;
+ $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
+ }
+
+ /**
+ * Prepare a node to get suggestions from.
+ *
+ * Creates a node with two file fields. The first one is not translatable,
+ * the second one is. Both fields got two files attached, where one has
+ * translatable content (title and atl-text) and the other one not.
+ *
+ * @return object
+ * The node which is prepared with all needed fields for the suggestions.
+ */
+ protected function prepareTranslationSuggestions() {
+ // Create a content type with fields.
+ // Only the first field is a translatable reference.
+ $type = $this->drupalCreateContentType();
+
+ $field1 = field_create_field(array(
+ 'field_name' => 'field1',
+ 'type' => 'file',
+ 'cardinality' => -1,
+ ));
+ $field2 = field_create_field(array(
+ 'field_name' => 'field2',
+ 'type' => 'file',
+ 'cardinality' => -1,
+ 'translatable' => TRUE,
+ ));
+ $field3 = field_create_field(array(
+ 'field_name' => 'field3',
+ 'type' => 'entityreference',
+ 'cardinality' => -1,
+ 'settings' => array(
+ 'target_type' => 'node',
+ 'handler' => 'base',
+ 'handler_settings' => array(
+ 'target_bundles' => array($type->type => $type->type),
+ 'sort' => array('type' => 'none'),
+ ),
+ ),
+ ));
+
+ // Create field instances on the content type.
+ field_create_instance(array(
+ 'field_name' => $field1['field_name'],
+ 'entity_type' => 'node',
+ 'bundle' => $type->type,
+ 'label' => 'Field 1',
+ 'widget' => array('type' => 'file'),
+ 'settings' => array(),
+ ));
+ field_create_instance(array(
+ 'field_name' => $field2['field_name'],
+ 'entity_type' => 'node',
+ 'bundle' => $type->type,
+ 'label' => 'Field 2',
+ 'widget' => array('type' => 'file'),
+ 'settings' => array(),
+ ));
+ field_create_instance(array(
+ 'field_name' => $field3['field_name'],
+ 'entity_type' => 'node',
+ 'bundle' => $type->type,
+ 'label' => 'Field 3',
+ 'settings' => array(),
+ 'widget' => array('type' => 'entityreference_autocomplete_tags'),
+ ));
+
+ // Make the body field translatable from node.
+ $info = field_info_field('body');
+ $info['translatable'] = TRUE;
+ field_update_field($info);
+
+ // Make the file entity fields translatable.
+ $info = field_info_field('field_file_image_alt_text');
+ $info['translatable'] = TRUE;
+ field_update_field($info);
+
+ $info = field_info_field('field_file_image_title_text');
+ $info['translatable'] = TRUE;
+ field_update_field($info);
+
+ // Create and save files - two with some text and two with no text.
+ list($file1, $file2, $file3, $file4) = $this->drupalGetTestFiles('image');
+ $file2->field_file_image_alt_text['en'][0] = array(
+ 'value' => $this->randomName(),
+ 'type' => 'plain_text',
+ );
+ $file2->field_file_image_title_text['en'][0] = array(
+ 'value' => $this->randomName() . ' ' . $this->randomName(),
+ 'type' => 'plain_text',
+ );
+
+ $file4->field_file_image_alt_text['en'][0] = array(
+ 'value' => $this->randomName(),
+ 'type' => 'plain_text',
+ );
+ $file4->field_file_image_title_text['en'][0] = array(
+ 'value' => $this->randomName() . ' ' . $this->randomName(),
+ 'type' => 'plain_text',
+ );
+
+ file_save($file1);
+ file_save($file2);
+ file_save($file3);
+ file_save($file4);
+
+ // Create a dummy node that will be referenced
+ $referenced_node = $this->drupalCreateNode(array(
+ 'type' => $type->type,
+ 'language' => 'en',
+ 'body' => array(
+ 'en' => array(
+ array('value' => $this->randomName() . ' ' . $this->randomName()),
+ ),
+ ),
+ ));
+
+ // Create a node with two translatable and two non-translatable files.
+ $node = $this->drupalCreateNode(array(
+ 'type' => $type->type,
+ 'language' => 'en',
+ 'body' => array('en' => array(
+ array(
+ 'value' => $this->randomName(),
+ ),
+ )),
+ $field1['field_name'] => array(LANGUAGE_NONE => array(
+ array(
+ 'fid' => $file1->fid,
+ 'display' => 1,
+ 'description' => '',
+ ),
+ array(
+ 'fid' => $file2->fid,
+ 'display' => 1,
+ 'description' => '',
+ ),
+ )),
+ $field2['field_name'] => array(LANGUAGE_NONE => array(
+ array(
+ 'fid' => $file3->fid,
+ 'display' => 1,
+ 'description' => '',
+ ),
+ array(
+ 'fid' => $file4->fid,
+ 'display' => 1,
+ 'description' => '',
+ ),
+ )),
+ $field3['field_name'] => array(LANGUAGE_NONE => array(
+ array('target_id' => $referenced_node->nid),
+ )),
+ ));
+
+ // Create a translatable menu.
+ $config = array(
+ 'menu_name' => 'translatable-menu',
+ 'title' => 'Translatable menu',
+ 'description' => $this->randomName(),
+ 'i18n_mode' => I18N_MODE_MULTIPLE,
+ );
+ menu_save($config);
+ $menu = menu_load($config['menu_name']);
+
+ // Create a menu link for the node.
+ $menu_link = array(
+ 'link_path' => 'node/' . $node->nid,
+ 'link_title' => 'Menu link one',
+ // i18n_menu_link::get_title() uses the title, set that too.
+ 'title' => 'Menu link one',
+ 'menu_name' => $menu['menu_name'],
+ 'customized' => TRUE,
+ );
+ $node->link = menu_link_load(menu_link_save($menu_link));
+
+ return $node;
+ }
+
+ /**
+ * Test suggested entities from a translation job.
+ */
+ public function testSuggestions() {
+ // Prepare a job and a node for testing.
+ $job = $this->createJob();
+ $node = $this->prepareTranslationSuggestions();
+ $item = $job->addItem('entity', 'node', $node->nid);
+
+ // Get all suggestions and clean the list.
+ $suggestions = $job->getSuggestions();
+ $job->cleanSuggestionsList($suggestions);
+
+ // Check for suggestions.
+ $this->assertEqual(count($suggestions), 4, 'Found four suggestions.');
+
+ // Check for valid attributes on the suggestions.
+ foreach ($suggestions as $suggestion) {
+ switch ($suggestion['reason']) {
+ case 'Field Field 1':
+ $this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_type, 'file', 'Got a file in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_id, $node->field1[LANGUAGE_NONE][1]['fid'], 'File id match between node and suggestion.');
+ break;
+ case 'Field Field 2':
+ $this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_type, 'file', 'Got a file in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_id, $node->field2[LANGUAGE_NONE][1]['fid'], 'File id match between node and suggestion.');
+ break;
+ case 'Field Field 3':
+ $this->assertEqual($suggestion['job_item']->getWordCount(), 2, 'Two translatable words in the suggestion');
+ $this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_type, 'node', 'Got a node in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_id, $node->field3[LANGUAGE_NONE][0]['target_id'], 'File id match between node and suggestion.');
+ break;
+ case 'Menu link Menu link one':
+ $this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion' . $suggestion['job_item']->plugin . $suggestion['job_item']->item_type);
+ $this->assertEqual($suggestion['job_item']->plugin, 'i18n_string', 'Got a string as plugin in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_type, 'menu_link', 'Got a menu link in the suggestion.');
+ $this->assertEqual($suggestion['job_item']->item_id, 'menu:item:'. $node->link['mlid'], 'Menu link id match between menu link and suggestion.');
+ break;
+ default:
+ $this->fail('Found an invalid suggestion.');
+ break;
+ }
+ $this->assertEqual($suggestion['from_item'], $item->tjiid);
+ $job->addExistingItem($suggestion['job_item']);
+ }
+
+ // Re-get all suggestions.
+ $suggestions = $job->getSuggestions();
+ $job->cleanSuggestionsList($suggestions);
+
+ // Check for no more suggestions.
+ $this->assertEqual(count($suggestions), 0, 'Found no more suggestion.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.ui.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.ui.inc
new file mode 100644
index 00000000..4b4941b3
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/tmgmt_entity.ui.inc
@@ -0,0 +1,257 @@
+ $value pairs passed into
+ * tmgmt_entity_get_translatable_entities() as the second parameter.
+ *
+ * @return array
+ * Array of entities.
+ */
+ public function getEntitiesTranslationData($type, $property_conditions = array()) {
+
+ $return_value = array();
+ $entities = tmgmt_entity_get_translatable_entities($type, $property_conditions, TRUE);
+
+ $entity_info = entity_get_info($type);
+ $bundles = tmgmt_entity_get_translatable_bundles($type);
+
+ // For retrieved entities add translation specific data.
+ foreach ($entities as $entity) {
+
+ list($entity_id, , $bundle) = entity_extract_ids($type, $entity);
+ $entity_uri = entity_uri($type, $entity);
+
+ // This occurs on user entity type.
+ if (empty($entity_id)) {
+ continue;
+ }
+
+ /**
+ * @var EntityTranslationDefaultHandler $handler
+ */
+ $handler = entity_translation_get_handler($type, $entity);
+
+ // Get existing translations and current job items for the entity
+ // to determine translation statuses
+ $translations = $handler->getTranslations();
+ $source_lang = entity_language($type, $entity);
+ $current_job_items = tmgmt_job_item_load_latest('entity', $type, $entity_id, $source_lang);
+
+ // Load basic entity data.
+ $return_value[$entity_id] = array(
+ 'entity_type' => $type,
+ 'entity_id' => $entity_id,
+ 'entity_label' => entity_label($type, $entity),
+ 'entity_uri' => $entity_uri['path'],
+ );
+
+ if (count($bundles) > 1) {
+ $return_value[$entity_id]['bundle'] = isset($bundles[$bundle]) ? $bundles[$bundle] : t('Unknown');
+ }
+
+ // Load entity translation specific data.
+ foreach (language_list() as $langcode => $language) {
+
+ $translation_status = 'current';
+
+ if ($langcode == $source_lang) {
+ $translation_status = 'original';
+ }
+ elseif (!isset($translations->data[$langcode])) {
+ $translation_status = 'missing';
+ }
+ elseif (!empty($translations->data[$langcode]['translate'])) {
+ $translation_status = 'outofdate';
+ }
+
+ $return_value[$entity_id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode]: NULL;
+ $return_value[$entity_id]['translation_statuses'][$langcode] = $translation_status;
+ }
+ }
+
+ return $return_value;
+ }
+
+ /**
+ * Builds search form for entity sources overview.
+ *
+ * @param array $form
+ * Drupal form array.
+ * @param $form_state
+ * Drupal form_state array.
+ * @param $type
+ * Entity type.
+ *
+ * @return array
+ * Drupal form array.
+ */
+ public function overviewSearchFormPart($form, &$form_state, $type) {
+
+ // Add search form specific styling.
+ drupal_add_css(drupal_get_path('module', 'tmgmt_entity') . '/css/tmgmt_entity.admin.entity_source_search_form.css');
+
+ $form = array();
+ // Add entity type value into form array so that it is available in
+ // the form alter hook.
+ $form_state['entity_type'] = $type;
+
+ $form['search_wrapper'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#weight' => -15,
+ );
+ $form['search_wrapper']['search'] = array(
+ '#tree' => TRUE,
+ );
+
+ $form['search_wrapper']['search_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Search'),
+ '#weight' => 10,
+ );
+ $form['search_wrapper']['search_cancel'] = array(
+ '#type' => 'submit',
+ '#value' => t('Cancel'),
+ '#weight' => 11,
+ );
+
+ $entity_info = entity_get_info($type);
+
+ $label_key = isset($entity_info['entity keys']['label']) ? $entity_info['entity keys']['label'] : NULL;
+
+ if (!empty($label_key)) {
+ $form['search_wrapper']['search'][$label_key] = array(
+ '#type' => 'textfield',
+ '#title' => t('@entity_name title', array('@entity_name' => $entity_info['label'])),
+ '#size' => 25,
+ '#default_value' => isset($_GET[$label_key]) ? $_GET[$label_key] : NULL,
+ );
+ }
+
+ $language_options = array();
+ foreach (language_list() as $langcode => $language) {
+ $language_options[$langcode] = $language->name;
+ }
+
+ $form['search_wrapper']['search']['language'] = array(
+ '#type' => 'select',
+ '#title' => t('Source Language'),
+ '#options' => $language_options,
+ '#empty_option' => t('All'),
+ '#default_value' => isset($_GET['language']) ? $_GET['language'] : NULL,
+ );
+
+ $bundle_key = $entity_info['entity keys']['bundle'];
+ $bundle_options = tmgmt_entity_get_translatable_bundles($type);
+
+ if (count($bundle_options) > 1) {
+ $form['search_wrapper']['search'][$bundle_key] = array(
+ '#type' => 'select',
+ '#title' => t('@entity_name type', array('@entity_name' => $entity_info['label'])),
+ '#options' => $bundle_options,
+ '#empty_option' => t('All'),
+ '#default_value' => isset($_GET[$bundle_key]) ? $_GET[$bundle_key] : NULL,
+ );
+ }
+ // In case entity translation is not enabled for any of bundles
+ // display appropriate message.
+ elseif (count($bundle_options) == 0) {
+ drupal_set_message(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'), 'warning');
+ unset($form['search_wrapper']);
+ }
+
+ $options = array();
+ foreach (language_list() as $langcode => $language) {
+ $options[$langcode] = $language->name;
+ }
+
+ $form['search_wrapper']['search']['target_language'] = array(
+ '#type' => 'select',
+ '#title' => t('Target language'),
+ '#options' => $options,
+ '#empty_option' => t('Any'),
+ '#default_value' => isset($_GET['target_language']) ? $_GET['target_language'] : NULL,
+ );
+ $form['search_wrapper']['search']['target_status'] = array(
+ '#type' => 'select',
+ '#title' => t('Target status'),
+ '#options' => array(
+ 'untranslated_or_outdated' => t('Untranslated or outdated'),
+ 'untranslated' => t('Untranslated'),
+ 'outdated' => t('Outdated'),
+ ),
+ '#default_value' => isset($_GET['target_status']) ? $_GET['target_status'] : NULL,
+ '#states' => array(
+ 'invisible' => array(
+ ':input[name="search[target_language]"]' => array('value' => ''),
+ ),
+ ),
+ );
+
+ return $form;
+ }
+
+ /**
+ * Performs redirect with search params appended to the uri.
+ *
+ * In case of triggering element is edit-search-submit it redirects to
+ * current location with added query string containing submitted search form
+ * values.
+ *
+ * @param array $form
+ * Drupal form array.
+ * @param $form_state
+ * Drupal form_state array.
+ * @param $type
+ * Entity type.
+ */
+ public function overviewSearchFormRedirect($form, &$form_state, $type) {
+ if ($form_state['triggering_element']['#id'] == 'edit-search-cancel') {
+ drupal_goto($_GET['q']);
+ }
+ elseif ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
+
+ $query = array();
+
+ foreach ($form_state['values']['search'] as $key => $value) {
+ $query[$key] = $value;
+ }
+
+ drupal_goto($_GET['q'], array('query' => $query));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hook_menu() {
+ $items = parent::hook_menu();
+ if (isset($items['admin/tmgmt/sources/entity_node'])) {
+ // We assume that nodes are the most important overview if enabled, so
+ // make sure they show up first.
+ $items['admin/tmgmt/sources/entity_node']['weight'] = -20;
+ }
+ return $items;
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.info b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.info
new file mode 100644
index 00000000..e21c54dd
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.info
@@ -0,0 +1,20 @@
+name = Entity Source User Interface
+description = User Interface for the entity translation source plugin.
+package = Translation Management
+core = 7.x
+
+dependencies[] = tmgmt_entity
+dependencies[] = tmgmt_ui
+dependencies[] = views_bulk_operations
+
+files[] = tmgmt_entity_ui.test
+files[] = tmgmt_entity_ui.list.test
+files[] = tmgmt_entity_ui.ui.inc
+
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.list.test b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.list.test
new file mode 100644
index 00000000..a8e42911
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.list.test
@@ -0,0 +1,268 @@
+ 'Entity Source List tests',
+ 'description' => 'Tests the user interface for entity translation lists.',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_entity_ui', 'translation', 'comment', 'taxonomy'));
+ $this->loginAsAdmin(array('administer entity translation'));
+
+ $this->setEnvironment('de');
+ $this->setEnvironment('fr');
+
+ // Enable entity translations for nodes and comments.
+ $edit = array();
+ $edit['entity_translation_entity_types[comment]'] = 1;
+ $edit['entity_translation_entity_types[node]'] = 1;
+ $edit['entity_translation_entity_types[taxonomy_term]'] = 1;
+ $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
+
+ $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
+ $this->createNodeType('page', 'Page', TRANSLATION_ENABLED);
+
+ // Create nodes that will be used during tests.
+ // NOTE that the order matters as results are read by xpath based on
+ // position in the list.
+ $this->nodes['page']['en'][] = $this->createNode('page');
+ $this->nodes['article']['de'][0] = $this->createNode('article', 'de');
+ $this->nodes['article']['fr'][0] = $this->createNode('article', 'fr');
+ $this->nodes['article']['en'][3] = $this->createNode('article', 'en');
+ $this->nodes['article']['en'][2] = $this->createNode('article', 'en');
+ $this->nodes['article']['en'][1] = $this->createNode('article', 'en');
+ $this->nodes['article']['en'][0] = $this->createNode('article', 'en');
+ }
+
+ /**
+ * Tests that the term bundle filter works.
+ */
+ function testTermBundleFilter() {
+
+ $vocabulary1 = entity_create('taxonomy_vocabulary', array(
+ 'machine_name' => 'vocab1',
+ 'name' => $this->randomName(),
+ ));
+ taxonomy_vocabulary_save($vocabulary1);
+
+ $term1 = entity_create('taxonomy_term', array(
+ 'name' => $this->randomName(),
+ 'vid' => $vocabulary1->vid,
+ ));
+ taxonomy_term_save($term1);
+
+ $vocabulary2 = (object) array(
+ 'machine_name' => 'vocab2',
+ 'name' => $this->randomName(),
+ );
+ taxonomy_vocabulary_save($vocabulary2);
+
+ $term2 = entity_create('taxonomy_term', array(
+ 'name' => $this->randomName(),
+ 'vid' => $vocabulary2->vid,
+ ));
+ taxonomy_term_save($term2);
+
+ $this->drupalGet('admin/tmgmt/sources/entity_taxonomy_term');
+ // Both terms should be displayed with their bundle.
+ $this->assertText($term1->name);
+ $this->assertText($term2->name);
+ $this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
+ $this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
+
+ // Limit to the first vocabulary.
+ $edit = array();
+ $edit['search[vocabulary_machine_name]'] = $vocabulary1->machine_name;
+ $this->drupalPost(NULL, $edit, t('Search'));
+ // Only term 1 should be displayed now.
+ $this->assertText($term1->name);
+ $this->assertNoText($term2->name);
+ $this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
+ $this->assertFalse($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
+
+ }
+
+ function testAvailabilityOfEntityLists() {
+
+ $this->drupalGet('admin/tmgmt/sources/entity_comment');
+ // Check if we are at comments page.
+ $this->assertText(t('Comment overview (Entity)'));
+ // No comments yet - empty message is expected.
+ $this->assertText(t('No entities matching given criteria have been found.'));
+
+ $this->drupalGet('admin/tmgmt/sources/entity_node');
+ // Check if we are at nodes page.
+ $this->assertText(t('Node overview (Entity)'));
+ // We expect article title as article node type is entity translatable.
+ $this->assertText($this->nodes['article']['en'][0]->title);
+ // Page node type should not be listed as it is not entity translatable.
+ $this->assertNoText($this->nodes['page']['en'][0]->title);
+ }
+
+ function testTranslationStatuses() {
+
+ // Test statuses: Source, Missing.
+ $this->drupalGet('admin/tmgmt/sources/entity_node');
+ $langstatus_en = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-en"]');
+ $langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
+
+ $this->assertEqual($langstatus_en[0]->div['title'], t('Source language'));
+ $this->assertEqual($langstatus_de[0]->div['title'], t('Not translated'));
+
+ // Test status: Active job item.
+ $job = $this->createJob('en', 'de');
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $job->addItem('entity', 'node', $this->nodes['article']['en'][0]->nid);
+ $job->requestTranslation();
+
+ $this->drupalGet('admin/tmgmt/sources/entity_node');
+ $langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]/a');
+
+ $items = $job->getItems();
+ $wrapper = entity_metadata_wrapper('tmgmt_job_item', array_shift($items));
+ $label = t('Active job item: @state', array('@state' => $wrapper->state->label()));
+
+ $this->assertEqual($langstatus_de[0]->div['title'], $label);
+
+ // Test status: Current
+ foreach ($job->getItems() as $job_item) {
+ $job_item->acceptTranslation();
+ }
+
+ $this->drupalGet('admin/tmgmt/sources/entity_node');
+ $langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
+
+ $this->assertEqual($langstatus_de[0]->div['title'], t('Translation up to date'));
+ }
+
+ function testTranslationSubmissions() {
+
+ // Simple submission.
+ $nid = $this->nodes['article']['en'][0]->nid;
+ $edit = array();
+ $edit["items[$nid]"] = 1;
+ $this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
+ $this->assertText(t('One job needs to be checked out.'));
+
+ // Submission of two entities of the same source language.
+ $nid1 = $this->nodes['article']['en'][0]->nid;
+ $nid2 = $this->nodes['article']['en'][1]->nid;
+ $edit = array();
+ $edit["items[$nid1]"] = 1;
+ $edit["items[$nid2]"] = 1;
+ $this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
+ $this->assertText(t('One job needs to be checked out.'));
+
+ // Submission of several entities of different source languages.
+ $nid1 = $this->nodes['article']['en'][0]->nid;
+ $nid2 = $this->nodes['article']['en'][1]->nid;
+ $nid3 = $this->nodes['article']['en'][2]->nid;
+ $nid4 = $this->nodes['article']['en'][3]->nid;
+ $nid5 = $this->nodes['article']['de'][0]->nid;
+ $nid6 = $this->nodes['article']['fr'][0]->nid;
+ $edit = array();
+ $edit["items[$nid1]"] = 1;
+ $edit["items[$nid2]"] = 1;
+ $edit["items[$nid3]"] = 1;
+ $edit["items[$nid4]"] = 1;
+ $edit["items[$nid5]"] = 1;
+ $edit["items[$nid6]"] = 1;
+ $this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
+ $this->assertText(t('@count jobs need to be checked out.', array('@count' => '3')));
+ }
+
+ function testNodeEntityListings() {
+
+ // Turn off the entity translation.
+ $edit = array();
+ $edit['language_content_type'] = TRANSLATION_ENABLED;
+ $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
+
+ // Check if we have appropriate message in case there are no entity
+ // translatable content types.
+ $this->drupalGet('admin/tmgmt/sources/entity_node');
+ $this->assertText(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'));
+
+ // Turn on the entity translation for both - article and page - to test
+ // search form.
+ $edit = array();
+ $edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED;
+ $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
+ $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
+ // Create page node after entity translation is enabled.
+ $page_node_translatable = $this->createNode('page');
+
+ $this->drupalGet('admin/tmgmt/sources/entity_node');
+ // We have both listed - one of articles and page.
+ $this->assertText($this->nodes['article']['en'][0]->title);
+ $this->assertText($page_node_translatable->title);
+
+ // Try the search by content type.
+ $edit = array();
+ $edit['search[type]'] = 'article';
+ $this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Search'));
+ // There should be article present.
+ $this->assertText($this->nodes['article']['en'][0]->title);
+ // The page node should not be listed.
+ $this->assertNoText($page_node_translatable->title);
+
+ // Try cancel button - despite we do post content type search value
+ // we should get nodes of botch content types.
+ $this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Cancel'));
+ $this->assertText($this->nodes['article']['en'][0]->title);
+ $this->assertText($page_node_translatable->title);
+ }
+
+ function testEntitySourceListSearch() {
+
+ // We need a node with title composed of several words to test
+ // "any words" search.
+ $title_part_1 = $this->randomName('4');
+ $title_part_2 = $this->randomName('4');
+ $title_part_3 = $this->randomName('4');
+
+ $this->nodes['article']['en'][0]->title = "$title_part_1 $title_part_2 $title_part_3";
+ node_save($this->nodes['article']['en'][0]);
+
+ // Submit partial node title and see if we have a result.
+ $edit = array();
+ $edit['search[title]'] = "$title_part_1 $title_part_3";
+ $this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Search'));
+ $this->assertText("$title_part_1 $title_part_2 $title_part_3", 'Searching on partial node title must return the result.');
+
+ // Check if there is only one result in the list.
+ $search_result_rows = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr');
+ $this->assert(count($search_result_rows) == 1, 'The search result must return only one row.');
+
+ // To test if other entity types work go for simple comment search.
+ $comment = new stdClass();
+ $comment->comment_body[LANGUAGE_NONE][0]['value'] = $this->randomName();
+ $comment->subject = $this->randomName();
+ // We need to associate the comment with entity translatable node object.
+ $comment->nid = $this->nodes['article']['en'][0]->nid;
+ // Set defaults - without these we will get Undefined property notices.
+ $comment->is_anonymous = TRUE;
+ $comment->cid = 0;
+ $comment->pid = 0;
+ $comment->uid = 0;
+ // Will add further comment variables.
+ $comment = comment_submit($comment);
+ comment_save($comment);
+ // Do search for the comment.
+ $edit = array();
+ $edit['search[subject]'] = $comment->subject;
+ $this->drupalPost('admin/tmgmt/sources/entity_comment', $edit, t('Search'));
+ $this->assertText($comment->subject, 'Searching for a comment subject.');
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.module b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.module
new file mode 100644
index 00000000..8fa16e7d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.module
@@ -0,0 +1,42 @@
+ 'tableselect',
+ '#header' => $overview['#header'],
+ '#options' => array(),
+ );
+ $languages = language_list();
+ // Check if there is a job / job item that references this translation.
+ $entity_language = entity_language($form_state['entity_type'], $form_state['entity']);
+ $items = tmgmt_job_item_load_latest('entity', $form_state['entity_type'], $id, $entity_language);
+ foreach ($languages as $langcode => $language) {
+ if ($langcode == LANGUAGE_NONE) {
+ // Never show language neutral on the overview.
+ continue;
+ }
+ // Since the keys are numeric and in the same order we can shift one element
+ // after the other from the original non-form rows.
+ $option = array_shift($overview['#rows']);
+ if ($langcode == $entity_language) {
+ $additional = '' . t('Source') . '';
+ // This is the source object so we disable the checkbox for this row.
+ $form['languages'][$langcode] = array(
+ '#type' => 'checkbox',
+ '#disabled' => TRUE,
+ );
+ }
+ elseif (isset($items[$langcode])) {
+ $item = $items[$langcode];
+ $uri = $item->uri();
+ $wrapper = entity_metadata_wrapper('tmgmt_job_item', $item);
+ $additional = l($wrapper->state->label(), $uri['path'], array('query' => array('destination' => current_path())));
+ // Disable the checkbox for this row since there is already a translation
+ // in progress that has not yet been finished. This way we make sure that
+ // we don't stack multiple active translations for the same item on top
+ // of each other.
+ $form['languages'][$langcode] = array(
+ '#type' => 'checkbox',
+ '#disabled' => TRUE,
+ );
+ }
+ else {
+ // There is no translation job / job item for this target language.
+ $additional = t('None');
+ }
+ // Inject the additional column into the array.
+
+ // The generated form structure has changed, support both an additional
+ // 'data' key (that is not supported by tableselect) and the old version
+ // without.
+ if (isset($option['data'])) {
+ array_splice($option['data'], -1, 0, array($additional));
+ // Append the current option array to the form.
+ $form['languages']['#options'][$langcode] = $option['data'];
+ }
+ else {
+ array_splice($option, -1, 0, array($additional));
+ // Append the current option array to the form.
+ $form['languages']['#options'][$langcode] = $option;
+ }
+ }
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['request'] = array(
+ '#type' => 'submit',
+ '#value' => t('Request translation'),
+ '#submit' => array('tmgmt_entity_ui_translate_form_submit'),
+ '#validate' => array('tmgmt_entity_ui_translate_form_validate'),
+ );
+ return $form;
+}
+
+/**
+ * Validation callback for the entity translation overview form.
+ */
+function tmgmt_entity_ui_translate_form_validate($form, &$form_state) {
+ $selected = array_filter($form_state['values']['languages']);
+ if (empty($selected)) {
+ form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
+ }
+}
+
+/**
+ * Submit callback for the entity translation overview form.
+ */
+function tmgmt_entity_ui_translate_form_submit($form, &$form_state) {
+ $entity = $form_state['entity'];
+ $entity_type = $form_state['entity_type'];
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+ $values = $form_state['values'];
+ $jobs = array();
+ foreach (array_keys(array_filter($values['languages'])) as $langcode) {
+ // Create the job object.
+ $job = tmgmt_job_create(entity_language($entity_type, $entity), $langcode, $GLOBALS['user']->uid);
+ try {
+ // Add the job item.
+ $job->addItem('entity', $entity_type, $id);
+ // Append this job to the array of created jobs so we can redirect the user
+ // to a multistep checkout form if necessary.
+ $jobs[$job->tjid] = $job;
+ }
+ catch (TMGMTException $e) {
+ watchdog_exception('tmgmt', $e);
+ $languages = language_list();
+ $target_lang_name = $languages[$langcode]->language;
+ drupal_set_message(t('Unable to add job item for target language %name. Make sure the source content is not empty.', array('%name' => $target_lang_name)), 'error');
+ }
+ }
+ tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.test b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.test
new file mode 100644
index 00000000..8762192b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.test
@@ -0,0 +1,328 @@
+ 'Entity Source UI tests',
+ 'description' => 'Tests the user interface for entity translation sources.',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('entity_translation'),
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_entity_ui', 'block', 'comment'));
+ variable_set('language_content_type_page', ENTITY_TRANSLATION_ENABLED);
+ variable_set('language_content_type_article', ENTITY_TRANSLATION_ENABLED);
+
+ $this->loginAsAdmin(array(
+ 'create translation jobs',
+ 'submit translation jobs',
+ 'accept translation jobs',
+ 'administer blocks',
+ 'administer entity translation',
+ 'toggle field translatability',
+ ));
+
+ $this->setEnvironment('de');
+ $this->setEnvironment('fr');
+ $this->setEnvironment('es');
+ $this->setEnvironment('el');
+
+ $this->createNodeType('page', st('Page'), ENTITY_TRANSLATION_ENABLED);
+ $this->createNodeType('article', st('Article'), ENTITY_TRANSLATION_ENABLED);
+
+ // Enable path locale detection.
+ $edit = array(
+ 'language[enabled][locale-url]' => TRUE,
+ 'language_content[enabled][locale-interface]' => TRUE,
+ );
+ $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
+
+ // @todo Re-enable this when switching to testing profile.
+ // Enable the main page content block for hook_page_alter() to work.
+ $edit = array(
+ 'blocks[system_main][region]' => 'content',
+ );
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ }
+
+ /**
+ * Test the translate tab for a single checkout.
+ */
+ function testNodeTranslateTabSingleCheckout() {
+
+ $this->loginAsTranslator(array('translate node entities'));
+
+ // Create an english source node.
+ $node = $this->createNode('page', 'en');
+ // Create a nodes that will not be translated to test the missing
+ // translation filter.
+ $node_not_translated = $this->createNode('page', 'en');
+ $node_german = $this->createNode('page', 'de');
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertText(t('One job needs to be checked out.'));
+ $this->assertText($node->title);
+
+ // Submit.
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
+
+ // Verify that the pending translation is shown.
+ $this->clickLink(t('Needs review'));
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+
+ $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
+
+ // German node should now be listed and be clickable.
+ // @todo Improve detection of the link, e.g. use xpath on the table or the
+ // title module to get a better title.
+ $this->clickLink('view', 1);
+ $this->assertText('de_' . $node->body['en'][0]['value']);
+
+ // Test that the destination query argument does not break the redirect
+ // and we are redirected back to the correct page.
+ $this->drupalGet('node/' . $node->nid . '/translate', array('query' => array('destination' => 'node')));
+
+ // Request a spanish translation.
+ $edit = array(
+ 'languages[es]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the checkout page.
+ $this->assertText(t('One job needs to be checked out.'));
+ $this->assertText($node->title);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the originally defined destination URL.
+ $this->assertEqual(url('node', array('absolute' => TRUE)), $this->getUrl());
+
+ // Test the missing translation filter.
+ $this->drupalGet('admin/tmgmt/sources');
+ $this->assertText($node->title);
+ $this->assertText($node_not_translated->title);
+ $this->drupalPost(NULL, array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'untranslated',
+ ), t('Search'));
+ $this->assertNoText($node->title);
+ $this->assertNoText($node_german->title);
+ $this->assertText($node_not_translated->title);
+ // Update the the translate flag of the translated node and test if it is
+ // listed among sources with missing translation.
+ db_update('entity_translation')->fields(array('translate' => 1))
+ ->condition('entity_type', 'node')->condition('entity_id', $node->nid)->execute();
+ $this->drupalPost(NULL, array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'outdated',
+ ), t('Search'));
+ $this->assertText($node->title);
+ $this->assertNoText($node_german->title);
+ $this->assertNoText($node_not_translated->title);
+
+ $this->drupalPost(NULL, array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'untranslated_or_outdated',
+ ), t('Search'));
+ $this->assertText($node->title);
+ $this->assertNoText($node_german->title);
+ $this->assertText($node_not_translated->title);
+ }
+
+ /**
+ * Test the translate tab for a single checkout.
+ */
+ function testNodeTranslateTabMultipeCheckout() {
+ // Allow auto-accept.
+ $default_translator = tmgmt_translator_load('test_translator');
+ $default_translator->settings = array(
+ 'auto_accept' => TRUE,
+ );
+ $default_translator->save();
+
+ $this->loginAsTranslator(array('translate node entities'));
+
+ // Create an english source node.
+ $node = $this->createNode('page', 'en');
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ 'languages[es]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertText(t('2 jobs need to be checked out.'));
+
+ // Submit all jobs.
+ $this->assertText($node->title);
+ $this->drupalPost(NULL, array(), t('Submit to translator and continue'));
+ $this->assertText($node->title);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
+ $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
+
+ // Translated nodes should now be listed and be clickable.
+ // @todo Use links on translate tab.
+ $this->drupalGet('de/node/' . $node->nid);
+ $this->assertText('de_' . $node->body['en'][0]['value']);
+
+ $this->drupalGet('es/node/' . $node->nid);
+ $this->assertText('es_' . $node->body['en'][0]['value']);
+ }
+
+ /**
+ * Test translating comments.
+ *
+ * @todo: Disabled pending resolution of http://drupal.org/node/1760270.
+ */
+ function dtestCommentTranslateTab() {
+
+ // Login as admin to be able to submit config page.
+ $this->loginAsAdmin(array('administer entity translation'));
+ // Enable comment translation.
+ $edit = array(
+ 'entity_translation_entity_types[comment]' => TRUE
+ );
+ $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
+
+ // Change comment_body field to be translatable.
+ $comment_body = field_info_field('comment_body');
+ $comment_body['translatable'] = TRUE;
+ field_update_field($comment_body);
+
+ // Create a user that is allowed to translate comments.
+ $permissions = array('translate comment entities', 'create translation jobs', 'submit translation jobs', 'accept translation jobs', 'post comments', 'skip comment approval', 'edit own comments', 'access comments');
+ $entity_translation_permissions = entity_translation_permission();
+ // The new translation edit form of entity_translation requires a new
+ // permission that does not yet exist in older versions. Add it
+ // conditionally.
+ if (isset($entity_translation_permissions['edit original values'])) {
+ $permissions[] = 'edit original values';
+ }
+ $this->loginAsTranslator($permissions, TRUE);
+
+ // Create an english source term.
+ $node = $this->createNode('article', 'en');
+
+ // Add a comment.
+ $this->drupalGet('node/' . $node->nid);
+ $edit = array(
+ 'subject' => $this->randomName(),
+ 'comment_body[en][0][value]' => $this->randomName(),
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertText(t('Your comment has been posted.'));
+
+ // Go to the translate tab.
+ $this->clickLink('edit');
+ $this->assertTrue(preg_match('|comment/(\d+)/edit$|', $this->getUrl(), $matches), 'Comment found');
+ $comment = comment_load($matches[1]);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $comment->subject)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ 'languages[es]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertText(t('2 jobs need to be checked out.'));
+
+ // Submit all jobs.
+ $this->assertText($comment->subject);
+ $this->drupalPost(NULL, array(), t('Submit to translator and continue'));
+ $this->assertText($comment->subject);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('comment/' . $comment->cid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $comment->subject, '@language' => t('Spanish'))));
+ $this->assertText(t('The translation for @title has been accepted.', array('@title' => $comment->subject)));
+
+ // @todo Use links on translate tab.
+ $this->drupalGet('de/comment/' . $comment->cid);
+ $this->assertText('de_' . $comment->comment_body['en'][0]['value']);
+
+ // @todo Use links on translate tab.
+ $this->drupalGet('es/node/' . $comment->cid);
+ $this->assertText('es_' . $comment->comment_body['en'][0]['value']);
+ }
+
+ /**
+ * Test the entity source specific cart functionality.
+ */
+ function testCart() {
+ $this->loginAsTranslator(array('translate node entities'));
+
+ $nodes = array();
+ for ($i = 0; $i < 4; $i++) {
+ $nodes[$i] = $this->createNode('page');
+ }
+
+ // Test the source overview.
+ $this->drupalGet('admin/tmgmt/sources/entity');
+ $this->drupalPost('admin/tmgmt/sources/entity', array(
+ 'items[' . $nodes[1]->nid . ']' => TRUE,
+ 'items[' . $nodes[2]->nid . ']' => TRUE,
+ ), t('Add to cart'));
+
+ $this->drupalGet('admin/tmgmt/cart');
+ $this->assertText($nodes[1]->title);
+ $this->assertText($nodes[2]->title);
+
+ // Test the translate tab.
+ $this->drupalGet('node/' . $nodes[3]->nid . '/translate');
+ $this->assertRaw(t('There are @count items in the translation cart.',
+ array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
+
+ $this->drupalPost(NULL, array(), t('Add to cart'));
+ $this->assertRaw(t('@count content source was added into the cart.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
+ $this->assertRaw(t('There are @count items in the translation cart including the current item.',
+ array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.ui.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.ui.inc
new file mode 100644
index 00000000..1a6c7b4c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/entity/ui/tmgmt_entity_ui.ui.inc
@@ -0,0 +1,174 @@
+ $language) {
+ $languages['langcode-' . $langcode] = array(
+ 'data' => check_plain($language->name),
+ );
+ }
+
+ $entity_info = entity_get_info($type);
+
+ $header = array(
+ 'title' => array('data' => t('Title (in source language)')),
+ );
+
+ // Show the bundle if there is more than one for this entity type.
+ if (count(tmgmt_entity_get_translatable_bundles($type)) > 1) {
+ $header['bundle'] = array('data' => t('@entity_name type', array('@entity_name' => $entity_info['label'])));
+ }
+
+ $header += $languages;
+
+ return $header;
+ }
+
+ /**
+ * Builds a table row for overview form.
+ *
+ * @param array $data
+ * Data needed to build the list row.
+ *
+ * @return array
+ */
+ public function overviewRow($data) {
+ $label = $data['entity_label'] ? $data['entity_label'] : t('@type: @id', array('@type' => $data['entity_type'], '@id' => $data['entity_id']));
+
+ $row = array(
+ 'id' => $data['entity_id'],
+ 'title' => l($label, $data['entity_uri']),
+ );
+
+ if (isset($data['bundle'])) {
+ $row['bundle'] = $data['bundle'];
+ }
+
+ foreach (language_list() as $langcode => $language) {
+ $row['langcode-' . $langcode] = array(
+ 'data' => theme('tmgmt_ui_translation_language_status_single', array(
+ 'translation_status' => $data['translation_statuses'][$langcode],
+ 'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
+ )),
+ 'class' => array('langstatus-' . $langcode),
+ );
+ }
+
+ return $row;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function overviewForm($form, &$form_state, $type) {
+
+ $form += $this->overviewSearchFormPart($form, $form_state, $type);
+
+ $form['items'] = array(
+ '#type' => 'tableselect',
+ '#header' => $this->overviewFormHeader($type),
+ '#empty' => t('No entities matching given criteria have been found.'),
+ '#attributes' => array('id' => 'tmgmt-entities-list'),
+ );
+
+ // Load search property params which will be passed into
+ $search_property_params = array();
+ $exclude_params = array('q', 'page');
+ foreach ($_GET as $key => $value) {
+ // Skip exclude params, and those that have empty values, as these would
+ // make it into query condition instead of being ignored.
+ if (in_array($key, $exclude_params) || $value === '') {
+ continue;
+ }
+ $search_property_params[$key] = $value;
+ }
+
+ foreach ($this->getEntitiesTranslationData($type, $search_property_params) as $data) {
+ $form['items']['#options'][$data['entity_id']] = $this->overviewRow($data);
+ }
+
+ $form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function overviewFormValidate($form, &$form_state, $type) {
+ if (!empty($form_state['values']['search']['target_language']) && $form_state['values']['search']['language'] == $form_state['values']['search']['target_language']) {
+ form_set_error('search[target_language]', t('The source and target languages must not be the same.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function overviewFormSubmit($form, &$form_state, $type) {
+
+ // Handle search redirect.
+ $this->overviewSearchFormRedirect($form, $form_state, $type);
+
+ $jobs = array();
+ $entities = entity_load($type, $form_state['values']['items']);
+ $source_lang_registry = array();
+
+ // Loop through entities and create individual jobs for each source language.
+ foreach ($entities as $entity) {
+ /**
+ * @var EntityTranslationDefaultHandler $handler
+ */
+ $handler = entity_translation_get_handler($type, $entity);
+ $source_lang = entity_language($type, $entity);
+ list($entity_id, ,) = entity_extract_ids($type, $entity);
+
+ try {
+
+ // For given source lang no job exists yet.
+ if (!isset($source_lang_registry[$source_lang])) {
+ // Create new job.
+ $job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
+ // Add initial job item.
+ $job->addItem('entity', $type, $entity_id);
+ // Add job identifier into registry
+ $source_lang_registry[$source_lang] = $job->tjid;
+ // Add newly created job into jobs queue.
+ $jobs[$job->tjid] = $job;
+ }
+ // We have a job for given source lang, so just add new job item for the
+ // existing job.
+ else {
+ $jobs[$source_lang_registry[$source_lang]]->addItem('entity', $type, $entity_id);
+ }
+ }
+ catch (TMGMTException $e) {
+ watchdog_exception('tmgmt', $e);
+ $entity_label = entity_label($type, $entity);
+ drupal_set_message(t('Unable to add job item for entity %name: %error.', array('%name' => $entity_label, '%error' => $e->getMessage())), 'error');
+ }
+ }
+
+ // If necessary, do a redirect.
+ $redirects = tmgmt_ui_job_checkout_multiple($jobs);
+ if ($redirects) {
+ tmgmt_ui_redirect_queue_set($redirects, current_path());
+ $form_state['redirect'] = tmgmt_ui_redirect_queue_dequeue();
+
+ drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/field/tmgmt_field.api.php b/sites/all/modules/contrib/localisation/tmgmt/sources/field/tmgmt_field.api.php
new file mode 100644
index 00000000..7c54078d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/field/tmgmt_field.api.php
@@ -0,0 +1,106 @@
+ $value) {
+ $structure[$delta]['#label'] = t('Delta #@delta', array('@delta' => $delta));
+ $structure[$delta]['value'] = array(
+ '#label' => $structure['#label'],
+ '#text' => $value['value'],
+ '#translate' => TRUE,
+ );
+ // Add format.
+ $structure[$delta]['format'] = array(
+ '#label' => '',
+ '#text' => $value['format'],
+ '#translate' => FALSE,
+ );
+ if ($field['type'] == 'text_with_summary' && !empty($value['summary'])) {
+ $structure[$delta]['summary'] = array(
+ '#label' => t('Summary'),
+ '#text' => $value['summary'],
+ '#translate' => TRUE,
+ );
+ }
+ }
+ }
+ return $structure;
+}
+
+/**
+ * Helper function for retrieving all translatable field values from an entity.
+ *
+ * @param $entity_type
+ * The entity type.
+ * @param $entity
+ * An entity object.
+ * @param $langcode
+ * The language of retrieved field values.
+ * @param $only_translatable
+ * If TRUE, only the fields which are flagged as translatable are returned.
+ * Defaults to FALSE, which is usually used for node translation, where the
+ * field translatability does not matter.
+ *
+ * @return array
+ * The structured field data for all translatable fields
+ */
+function tmgmt_field_get_source_data($entity_type, $entity, $langcode, $only_translatable = FALSE) {
+ try {
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+ }
+ catch (Exception $e) {
+ watchdog_exception('tmgmt field', $e);
+ return array();
+ }
+
+ $fields = array();
+ foreach (field_info_instances($entity_type, $bundle) as $field_name => $instance) {
+ $field = field_info_field($field_name);
+ $items = field_get_items($entity_type, $entity, $field_name, $langcode);
+ if ((!$only_translatable || $field['translatable']) && $items) {
+ if ($data = module_invoke($field['module'], 'tmgmt_source_translation_structure', $entity_type, $entity, $field, $instance, $langcode, $items)) {
+ $fields[$field_name] = $data;
+ }
+ }
+ }
+
+ drupal_alter('tmgmt_field_source_data', $fields, $entity_type, $entity, $langcode);
+ return $fields;
+}
+
+/**
+ * Populates a field on an object with the provided field values.
+ *
+ * @param $entity_type
+ * The type of $entity.
+ * @param $entity
+ * The object to be populated.
+ * @param $langcode
+ * The field language.
+ * @param $data
+ * An array of values.
+ * @param $use_field_translation
+ * TRUE if field translation is being used.
+ */
+function tmgmt_field_populate_entity($entity_type, $entity, $langcode, $data, $use_field_translation = TRUE) {
+ drupal_alter('tmgmt_field_pre_populate_entity', $data, $entity, $entity_type, $langcode);
+
+ foreach (element_children($data) as $field_name) {
+ if ($field = field_info_field($field_name)) {
+ $function = $field['module'] . '_field_type_tmgmt_populate_entity';
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+ if (function_exists($function)) {
+ $function($entity_type, $entity, $field, $instance, $langcode, $data, $use_field_translation);
+ }
+ else {
+ $field_langcode = $field['translatable'] ? $langcode : LANGUAGE_NONE;
+ // When not using field translation, make sure we're not storing
+ // multiple languages.
+ if (!$use_field_translation) {
+ $entity->{$field_name} = array($field_langcode => array());
+ }
+
+ foreach (element_children($data[$field_name]) as $delta) {
+ $columns = array();
+ foreach (element_children($data[$field_name][$delta]) as $column) {
+ if (isset($data[$field_name][$delta][$column]['#translation']['#text'])) {
+ $columns[$column] = $data[$field_name][$delta][$column]['#translation']['#text'];
+ }
+ // For elements which are not translatable, keep using the original
+ // value.
+ elseif (isset($data[$field_name][$delta][$column]['#translate']) && $data[$field_name][$delta][$column]['#translate'] == FALSE) {
+ $columns[$column] = $data[$field_name][$delta][$column]['#text'];
+ }
+ }
+ // Make sure the array_merge() gets an array as a first parameter.
+ if (!isset($entity->{$field_name}[$field_langcode][$delta])) {
+ $entity->{$field_name}[$field_langcode][$delta] = array();
+ }
+ $entity->{$field_name}[$field_langcode][$delta] = array_merge($entity->{$field_name}[$field_langcode][$delta], $columns);
+ }
+ }
+ }
+ }
+
+ drupal_alter('tmgmt_field_post_populate_entity', $entity, $entity_type, $data, $langcode);
+}
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.info b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.info
new file mode 100644
index 00000000..161c4d7b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.info
@@ -0,0 +1,20 @@
+name = i18n String Source
+description = i18n String source plugin for the Translation Management system.
+package = Translation Management
+core = 7.x
+dependencies[] = tmgmt_ui
+dependencies[] = i18n_string
+# List variable as a dependency so that it gets picked up testbot.
+# See http://drupal.org/node/1440484
+dependencies[] = i18n
+dependencies[] = variable
+files[] = tmgmt_i18n_string.plugin.inc
+files[] = tmgmt_i18n_string.test
+files[] = tmgmt_i18n_string.ui.inc
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.module b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.module
new file mode 100644
index 00000000..09d5a2ad
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.module
@@ -0,0 +1,342 @@
+ t('i18n String'),
+ 'description' => t('Source handler for i18n strings.'),
+ 'plugin controller class' => 'TMGMTI18nStringSourcePluginController',
+ 'ui controller class' => 'TMGMTI18nStringDefaultSourceUIController',
+ );
+ foreach (i18n_object_info() as $object_type => $object_info) {
+ // Only consider object types that have string translation information.
+ if (isset($object_info['string translation'])) {
+ $info['i18n_string']['item types'][$object_type] = $object_info['title'];
+ }
+ }
+ return $info;
+}
+
+/**
+ * Gets i18n strings for given type and label.
+ *
+ * @param string $type
+ * i18n object type.
+ * @param string $search_label
+ * Label to search for.
+ * @param string $target_language
+ * Target language.
+ * @param string $target_status
+ * Target status.
+ *
+ * @return array
+ * List of i18n strings data.
+ */
+function tmgmt_i18n_string_get_strings($type, $search_label = NULL, $target_language = NULL, $target_status = 'untranslated_or_outdated') {
+ $info = i18n_object_info($type);
+
+ $languages = drupal_map_assoc(array_keys(language_list()));
+ $select = db_select('i18n_string', 'i18n_s');
+
+ $select->addTag('tmgmt_sources_search');
+ $select->addMetaData('plugin', 'i18n_string');
+ $select->addMetaData('type', $type);
+
+ $select->condition('i18n_s.textgroup', $info['string translation']['textgroup']);
+
+ if (!empty($target_language) && in_array($target_language, $languages)) {
+ if ($target_status == 'untranslated_or_outdated') {
+ $or = db_or();
+ $or->isNull("lt_$target_language.language");
+ $or->condition("lt_$target_language.i18n_status", I18N_STRING_STATUS_UPDATE);
+ $select->condition($or);
+ }
+ elseif ($target_status == 'outdated') {
+ $select->condition("lt_$target_language.i18n_status", I18N_STRING_STATUS_UPDATE);
+ }
+ elseif ($target_status == 'untranslated') {
+ $select->isNull("lt_$target_language.language");
+ }
+ }
+
+ if (isset($info['string translation']['type'])) {
+ $select->condition('i18n_s.type', $info['string translation']['type']);
+ }
+ elseif ($type == 'field' || $type == 'field_instance') {
+ // Fields and field instances share the same textgroup. Use list of bundles
+ // to include/exclude field_instances.
+ $bundles = array();
+ foreach (entity_get_info() as $entity_info) {
+ $bundles = array_merge($bundles, array_keys($entity_info['bundles']));
+ }
+ $select->condition('i18n_s.objectid', $bundles, $type == 'field_instance' ? 'IN' : 'NOT IN');
+ }
+
+ $select->join('locales_source', 'ls', 'ls.lid = i18n_s.lid');
+ $select->addField('ls', 'source');
+ if (!empty($search_label)) {
+ $select->condition('ls.source', "%$search_label%", 'LIKE');
+ }
+
+ foreach ($languages as $langcode) {
+ $langcode = str_replace('-', '', $langcode);
+ $select->leftJoin('locales_target', "lt_$langcode", "i18n_s.lid = %alias.lid AND %alias.language = '$langcode'");
+ $select->addField("lt_$langcode", 'language', "lang_$langcode");
+ }
+ $select->fields("i18n_s", array('lid', 'textgroup', 'context', 'type', 'objectid'));
+
+ $select->addExpression("concat(i18n_s.textgroup, ':', i18n_s.type, ':', i18n_s.objectid)", 'job_item_id');
+
+ $select->orderBy('i18n_s.context');
+
+ $select->groupBy('type');
+ $select->groupBy('objectid');
+
+ $select = $select->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
+
+ return $select->execute()->fetchAll();
+}
+
+/**
+ * Implements hook_form_ID_alter().
+ *
+ * Adds request translation capabilities into i18n translate tab.
+ */
+function tmgmt_i18n_string_form_i18n_string_translate_page_overview_form_alter(&$form, &$form_state) {
+ $object = $form['object']['#value'];
+
+ // Create the id: textgroup:type:objectid.
+ $id = $object->get_textgroup() . ':' . implode(':', $object->get_string_context());
+ $source_language = variable_get_value('i18n_string_source_language');
+
+ $existing_items = tmgmt_job_item_load_latest('i18n_string', $object->get_type(), $id, $source_language);
+
+ $form['top_actions']['#type'] = 'actions';
+ $form['top_actions']['#weight'] = -10;
+ tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'i18n_string', $object->get_type(), $id);
+
+ $form['languages']['#type'] = 'tableselect';
+
+ // Append lang code so that we can use it
+ foreach ($form['languages']['#rows'] as $lang => $row) {
+
+ if (isset($existing_items[$lang])) {
+
+ $states = tmgmt_job_item_states();
+ $row['status'] = $states[$existing_items[$lang]->state];
+
+ if ($existing_items[$lang]->isNeedsReview()) {
+ $row['operations'] .= ' | ' . l(t('review'), 'admin/tmgmt/items/' . $existing_items[$lang]->tjiid, array('query' => array('destination' => $_GET['q'])));
+ }
+ elseif ($existing_items[$lang]->isActive()) {
+ $row['operations'] .= ' | ' . l(t('in progress'), 'admin/tmgmt/items/' . $existing_items[$lang]->tjiid, array('query' => array('destination' => $_GET['q'])));
+ }
+ }
+
+ $form['languages']['#options'][$id . ':' . $lang] = $row;
+
+ if ($lang == $source_language || isset($existing_items[$lang])) {
+ $form['languages'][$id . ':' . $lang] = array(
+ '#type' => 'checkbox',
+ '#disabled' => TRUE,
+ );
+ }
+ }
+
+ unset($form['languages']['#rows'], $form['languages']['#theme']);
+
+ $form['actions']['request_translation'] = array(
+ '#type' => 'submit',
+ '#value' => t('Request translation'),
+ '#submit' => array('tmgmt_i18n_string_translate_form_submit'),
+ '#validate' => array('tmgmt_i18n_string_translate_form_validate'),
+ );
+}
+
+/**
+ * Validation callback for the entity translation overview form.
+ */
+function tmgmt_i18n_string_translate_form_validate($form, &$form_state) {
+ $selected = array_filter($form_state['values']['languages']);
+ if (empty($selected)) {
+ form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
+ }
+}
+
+function tmgmt_i18n_string_translate_form_submit($form, &$form_state) {
+
+ $items = array_filter($form_state['values']['languages']);
+ $type = $form_state['values']['object']->get_type();
+ $source_lang = variable_get_value('i18n_string_source_language');
+
+ $jobs = array();
+ $target_lang_registry = array();
+
+ // Loop through entities and create individual jobs for each source language.
+ foreach ($items as $item) {
+
+ $item_parts = explode(':', $item);
+ $target_lang = array_pop($item_parts);
+ $key = implode(':', $item_parts);
+
+ // For given source lang no job exists yet.
+ if (!isset($target_lang_registry[$target_lang])) {
+ // Create new job.
+ $job = tmgmt_job_create($source_lang, $target_lang, $GLOBALS['user']->uid);
+ // Add initial job item.
+ $job->addItem('i18n_string', $type, $key);
+ // Add job identifier into registry
+ $target_lang_registry[$target_lang] = $job->tjid;
+ // Add newly created job into jobs queue.
+ $jobs[$job->tjid] = $job;
+ }
+ // We have a job for given source lang, so just add new job item for the
+ // existing job.
+ else {
+ $jobs[$target_lang_registry[$target_lang]]->addItem('i18n_string', $type, $key);
+ }
+ }
+ tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
+}
+
+/**
+ * Implements hook_i18n_object_info_alter().
+ */
+function tmgmt_i18n_string_i18n_object_info_alter(&$info) {
+ $entity_info = entity_get_info();
+ // Add a entity key to the object info if neither load callback nor entity
+ // keys are set and the object is an entity_type.
+ // @todo: Add this as default in EntityDefaultI18nStringController.
+ foreach ($info as $name => &$object) {
+ if (!isset($object['load callback']) && !isset($object['entity']) && isset($entity_info[$name])) {
+ $object['entity'] = $name;
+ }
+ }
+}
+
+/**
+ * Returns the i18n wrapper object.
+ *
+ * I18N objects with one or two keys are supported.
+ *
+ * @param string $type
+ * I18n object type.
+ * @param object $i18n_string
+ * Object with type and objectid properties.
+ *
+ * @return i18n_string_object_wrapper
+ */
+function tmgmt_i18n_string_get_wrapper($type, $i18n_string) {
+ $object_key = i18n_object_info($type, 'key');
+
+ // Special handling for i18nviews.
+ if ($type == 'views') {
+ // The construct method needs the full view object.
+ $view = views_get_view($i18n_string->objectid);
+ $wrapper = i18n_get_object($type, $i18n_string->objectid, $view);
+ return $wrapper;
+ }
+
+ // Special handling for i18n_panels.
+ $panels_objects = array(
+ 'pane_configuration' => 'panels_pane',
+ 'display_configuration' => 'panels_display',
+ );
+ if (in_array($type, array_keys($panels_objects))) {
+ ctools_include('export');
+ $wrapper = FALSE;
+ switch ($type) {
+ case 'display_configuration':
+ $object_array = ctools_export_load_object($panels_objects[$type], 'conditions', array('uuid' => $i18n_string->objectid));
+ $wrapper = i18n_get_object($type, $i18n_string->objectid, $object_array[$i18n_string->objectid]);
+ break;
+
+ case 'pane_configuration':
+ $obj = db_query("SELECT * FROM {panels_pane} WHERE uuid = :uuid", array(':uuid' => $i18n_string->objectid))->fetchObject();
+ if ($obj) {
+ $pane = ctools_export_unpack_object($panels_objects[$type], $obj);
+ $translation = i18n_panels_get_i18n_translation_object($pane);
+ $translation->uuid = $pane->uuid;
+ $wrapper = i18n_get_object($type, $i18n_string->objectid, $translation);
+ }
+ break;
+
+ default:
+ break;
+ }
+ return $wrapper;
+ }
+
+ // Special handling if the object has two keys. Assume that they
+ // mean type and object id.
+ if ($type == 'field') {
+ // Special case for fields which expect the type to be the identifier.
+ $wrapper = i18n_get_object($type, $i18n_string->type);
+ return $wrapper;
+ }
+ elseif ($type == 'field_instance') {
+ // Special case for field instances, which use the field name as type and
+ // bundle as object id. We don't know the entity_type, so we loop over all
+ // entity_types to search for the bundle. This will clash if different
+ // entity types have bundles with the same names.
+ foreach (entity_get_info() as $entity_type => $entity_info) {
+ if (isset($entity_info['bundles'][$i18n_string->objectid])) {
+ list($type_key, $objectid_key) = $object_key;
+ $wrapper = i18n_get_object($type, array(
+ $type_key => $i18n_string->type,
+ $objectid_key => $i18n_string->objectid
+ ), field_info_instance($entity_type, $i18n_string->type, $i18n_string->objectid));
+ return $wrapper;
+ }
+ }
+ }
+ elseif (count($object_key) == 2) {
+ list($type_key, $objectid_key) = $object_key;
+ $wrapper = i18n_get_object($type, array(
+ $type_key => $i18n_string->type,
+ $objectid_key => $i18n_string->objectid
+ ));
+ return $wrapper;
+ }
+ else {
+ // Otherwise, use the object id.
+ $wrapper = i18n_get_object($type, $i18n_string->objectid);
+ return $wrapper;
+ }
+}
+
+/**
+ * Implements hook_tmgmt_source_suggestions()
+ */
+function tmgmt_i18n_string_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
+ $suggestions = array();
+
+ foreach ($items as $item) {
+ if (($item instanceof TMGMTJobItem) && ($item->item_type == 'node')) {
+ // Load translatable menu items related to this node.
+ $query = db_select('menu_links', 'ml')
+ ->condition('ml.link_path', 'node/' . $item->item_id)
+ ->fields('ml', array('mlid'));
+ $query->join('menu_custom', 'mc', 'ml.menu_name = mc.menu_name AND mc.i18n_mode = ' . I18N_MODE_MULTIPLE);
+ $results = $query->execute()->fetchAllAssoc('mlid');
+ foreach ($results as $result) {
+ $menu_link = menu_link_load($result->mlid);
+ // Add suggestion.
+ $suggestions[] = array(
+ 'job_item' => tmgmt_job_item_create('i18n_string', 'menu_link', "menu:item:{$result->mlid}"),
+ 'reason' => t('Menu link @title', array('@title' => $menu_link['link_title'])),
+ 'from_item' => $item->tjiid,
+ );
+ }
+ }
+ }
+
+ return $suggestions;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.plugin.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.plugin.inc
new file mode 100644
index 00000000..5266d02e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.plugin.inc
@@ -0,0 +1,157 @@
+getI18nObjectWrapper($job_item);
+ $structure = array();
+ $languages = language_list();
+
+ if ($i18n_object instanceof i18n_string_object_wrapper) {
+ $i18n_strings = $i18n_object->get_strings();
+ $source_language = $job_item->getJob()->source_language;
+ foreach ($i18n_strings as $string_id => $string) {
+ // If the job source language is different from the i18n source language
+ // try to load an existing translation for the language and use it as
+ // the source.
+ if ($source_language != i18n_string_source_language()) {
+ $translation = $string->get_translation($source_language);
+
+ if (empty($translation)) {
+ throw new TMGMTException(t('Unable to load %language translation for the string %title',
+ array('%language' => $languages[$source_language]->name, '%title' => $string->title)));
+ }
+ // If '#label' is empty theme_tmgmt_ui_translator_review_form() fails.
+ $structure[$string_id] = array(
+ '#label' => !empty($string->title) ? $string->title : $string->property,
+ '#text' => $translation,
+ '#translate' => TRUE
+ );
+ }
+ else {
+ // If '#label' is empty theme_tmgmt_ui_translator_review_form() fails.
+ $structure[$string_id] = array(
+ '#label' => !empty($string->title) ? $string->title : $string->property,
+ '#text' => $string->string,
+ '#translate' => TRUE
+ );
+ }
+ }
+ }
+ return $structure;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveTranslation(TMGMTJobItem $job_item) {
+ $job = tmgmt_job_load($job_item->tjid);
+ $data = array_filter(tmgmt_flatten_data($job_item->getData()), '_tmgmt_filter_data');
+ foreach ($data as $i18n_string => $item) {
+ if (isset($item['#translation']['#text'])) {
+ i18n_string_translation_update($i18n_string, $item['#translation']['#text'], $job->target_language);
+ }
+ }
+
+ // We just saved the translation, set the state of the job item to
+ // 'finished'.
+ $job_item->accepted();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel(TMGMTJobItem $job_item) {
+ if ($i18n_object = $this->getI18nObjectWrapper($job_item)) {
+ // Get the label, default to the get_title() method, fall back to the
+ // first string if that is empty.
+ $title = t('Unknown');
+ if ($i18n_object->get_title()) {
+ $title = $i18n_object->get_title();
+ }
+ elseif ($strings = $i18n_object->get_strings(array('empty' => TRUE))) {
+ $title = reset($strings)->get_string();
+ }
+ return t('@title (@id)', array('@title' => strip_tags(drupal_substr($title, 0, 64)), '@id' => $job_item->item_id));
+ }
+ return parent::getLabel($job_item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUri(TMGMTJobItem $job_item) {
+ if ($wrapper = $this->getI18nObjectWrapper($job_item)) {
+ return array(
+ 'path' => $wrapper->get_path(),
+ 'options' => array(),
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType(TMGMTJobItem $job_item) {
+ if ($label = $this->getItemTypeLabel($job_item->item_type)) {
+ return $label;
+ }
+ return parent::getType($job_item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSourceLangCode(TMGMTJobItem $job_item) {
+ return i18n_string_source_language();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLangCodes(TMGMTJobItem $job_item) {
+ $existing_lang_codes = array();
+ $languages = language_list();
+
+ if ($object = $this->getI18nObjectWrapper($job_item)) {
+ $existing_lang_codes = array_keys($languages);
+ foreach ($object->load_strings() as $string) {
+ foreach ($languages as $language) {
+ if ($language->language == $this->getSourceLangCode($job_item)) {
+ continue;
+ }
+ // Remove languages for which we fail to find translation.
+ if (in_array($language->language, $existing_lang_codes) && !$string->get_translation($language->language)) {
+ $existing_lang_codes = array_diff($existing_lang_codes, array($language->language));
+ }
+ }
+ }
+ }
+
+ return $existing_lang_codes;
+ }
+
+ /**
+ * Helper function to get i18n_object_wrapper for given job item.
+ *
+ * @param TMGMTJobItem $job_item
+ *
+ * @return i18n_string_object_wrapper
+ */
+ protected function getI18nObjectWrapper(TMGMTJobItem $job_item) {
+ list(, $type, $object_id) = explode(':', $job_item->item_id, 3);
+ return tmgmt_i18n_string_get_wrapper($job_item->item_type, (object) array('type' => $type, 'objectid' => $object_id));
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.test b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.test
new file mode 100644
index 00000000..de772de7
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.test
@@ -0,0 +1,496 @@
+ 'i18n String Source tests',
+ 'description' => 'Exporting source data from i18n string and saving translations back',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('i18n_string'),
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_ui', 'tmgmt_i18n_string', 'taxonomy', 'i18n_taxonomy', 'i18n_block', 'i18n_field', 'list', 'i18n_menu'));
+ $this->setEnvironment('de');
+ $this->translator = $this->createTranslator();
+ }
+
+ function testI18nStringSourceTaxonomy() {
+ // Test translation of a vocabulary.
+ /////////////////////////////////////
+ $config = array(
+ 'name' => $this->randomName(),
+ 'machine_name' => 'test_vocab',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ );
+ $vocabulary = entity_create('taxonomy_vocabulary', $config);
+ taxonomy_vocabulary_save($vocabulary);
+
+ $string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
+ $source_text = $vocabulary->name;
+
+ // Create the new job and job item.
+ $job = $this->createJob();
+ $job->translator = $this->translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $item1 = $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
+ $this->assertEqual(t('Vocabulary'), $item1->getSourceType());
+ $job->requestTranslation();
+
+ foreach ($job->getItems() as $item) {
+ /* @var $item TMGMTJobItem */
+ $item->acceptTranslation();
+ }
+
+ // Check the structure of the imported data.
+ $this->assertEqual($item1->item_id, $string_object_name, 'i18n Strings object correctly saved');
+
+ // Check string translation.
+ $this->assertEqual(i18n_string_translate('taxonomy:vocabulary:' . $vocabulary->vid . ':name', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
+
+ // Test translation of a taxonomy term.
+ /////////////////////////////////////
+ $term = entity_create('taxonomy_term', array(
+ 'vid' => $vocabulary->vid,
+ 'name' => $this->randomName(),
+ 'description' => $this->randomName(),
+ ));
+ taxonomy_term_save($term);
+
+ $string_object_name = "taxonomy:term:" . $term->tid;
+ $source_text_name = $term->name;
+ $source_text_description = $term->description;
+
+ // Create the new job and job item.
+ $job = $this->createJob();
+ $job->translator = $this->translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $item1 = $job->addItem('i18n_string', 'taxonomy_term', $string_object_name);
+ $this->assertEqual(t('Taxonomy term'), $item1->getSourceType());
+ $job->requestTranslation();
+
+ /* @var $item TMGMTJobItem */
+ foreach ($job->getItems() as $item) {
+ // The source is available only in en.
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+ $item->acceptTranslation();
+ // The source should be now available in de and en.
+ $this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
+ }
+
+ // Check the structure of the imported data.
+ $this->assertEqual($item1->item_id, $string_object_name);
+
+ // Check string translation.
+ $this->assertEqual(i18n_string_translate('taxonomy:term:' . $term->tid . ':name', $source_text_name,
+ array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text_name);
+ $this->assertEqual(i18n_string_translate('taxonomy:term:' . $term->tid . ':description', $source_text_description,
+ array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text_description);
+
+ }
+
+ /**
+ * Test if the source is able to pull content in requested language.
+ */
+ function testRequestDataForSpecificLanguage() {
+ $this->setEnvironment('es');
+ $this->setEnvironment('cs');
+
+ $config = array(
+ 'name' => $this->randomName(),
+ 'machine_name' => 'test_vocab',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ );
+ $vocabulary = entity_create('taxonomy_vocabulary', $config);
+ taxonomy_vocabulary_save($vocabulary);
+
+ $string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
+
+ i18n_string_translation_update($string_object_name . ':name', 'de translation', 'de');
+
+ // Create new job item with a source language for which the translation
+ // exits.
+ $job = $this->createJob('de', 'cs');
+ $job->save();
+ $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
+
+ $data = $job->getData();
+ $this->assertEqual($data[1][$string_object_name . ':name']['#text'], 'de translation');
+
+ // Create new job item with a source language for which the translation
+ // does not exit.
+ $job = $this->createJob('es', 'cs');
+ $job->save();
+ try {
+ $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
+ $this->fail('The job item should not be added as there is no translation for language "es"');
+ }
+ catch (TMGMTException $e) {
+ $languages = language_list();
+ $this->assertEqual(t('Unable to load %language translation for the string %title',
+ array('%language' => $languages['es']->name, '%title' => 'Name')), $e->getMessage());
+ }
+ }
+
+ function testI18nStringSourceMenu() {
+ drupal_static_reset('_tmgmt_plugin_info');
+ drupal_static_reset('_tmgmt_plugin_controller');
+
+ // Test translation of a menu.
+ /////////////////////////////////////
+ $config = array(
+ 'menu_name' => $this->randomName(),
+ 'title' => $this->randomName(),
+ 'description' => $this->randomName(),
+ 'i18n_mode' => I18N_MODE_MULTIPLE,
+ );
+ menu_save($config);
+ $menu = menu_load($config['menu_name']);
+
+ $source_text = $menu['title'];
+ $string_name = 'menu:menu:' . $menu['menu_name'];
+
+ // Create the new job and job item.
+ $job = $this->createJob();
+ $job->translator = $this->translator->name;
+ $job->settings = array();
+
+ $item1 = $job->addItem('i18n_string', 'menu', $string_name);
+ $this->assertEqual(t('Menu'), $item1->getSourceType());
+ $job->requestTranslation();
+ /* @var $item TMGMTJobItem */
+ foreach ($job->getItems() as $item) {
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+ $item->acceptTranslation();
+ $this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
+ }
+
+ $data = $item1->getData();
+ $this->assertEqual($data['menu:menu:' . $menu['menu_name'] . ':title']['#text'], $config['title']);
+ $this->assertEqual($data['menu:menu:' . $menu['menu_name'] . ':description']['#text'], $config['description']);
+
+ // Check the structure of the imported data.
+ $this->assertEqual($item1->item_id, $string_name, 'String is correctly saved');
+
+ // Check string translation.
+ $this->assertEqual(i18n_string_translate($string_name . ':title', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
+
+ // Test translation of a menu item.
+ /////////////////////////////////////
+ $source_text = $this->randomName();
+ $menu_link = array(
+ 'link_path' => '',
+ 'link_title' => $source_text,
+ // i18n_menu_link::get_title() uses the title, set that too.
+ 'title' => $source_text,
+ 'menu_name' => $menu['menu_name'],
+ 'customized' => TRUE,
+ );
+ menu_link_save($menu_link);
+ $string_name = 'menu:item:' . $menu_link['mlid'];
+
+ // Create the new job and job item.
+ $job = $this->createJob();
+ $job->translator = $this->translator->name;
+ $job->settings = array();
+
+ $item1 = $job->addItem('i18n_string', 'menu_link', $string_name);
+ $this->assertEqual(t('Menu link'), $item1->getSourceType());
+ $job->requestTranslation();
+ /* @var $item TMGMTJobItem */
+ foreach ($job->getItems() as $item) {
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+ $item->acceptTranslation();
+ $this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
+ }
+
+ $data = $item1->getData();
+ $this->assertEqual($data[$string_name . ':title']['#text'], $source_text);
+
+ // Check the structure of the imported data.
+ $this->assertEqual($item1->item_id, $string_name);
+
+ // Check string translation.
+ $this->assertEqual(i18n_string_translate($string_name . ':title', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
+
+ }
+
+ function testI18nStringSourceLangCodes() {
+ $config = array(
+ 'name' => $this->randomName(),
+ 'description' => 'description_' . $this->randomName(),
+ 'machine_name' => 'test_vocab',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ );
+ $vocabulary = entity_create('taxonomy_vocabulary', $config);
+ taxonomy_vocabulary_save($vocabulary);
+
+ $string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
+
+ // Create the new job and job item.
+ $job = $this->createJob();
+ $job->translator = $this->translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $item = $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+
+ i18n_string_translation_update($string_object_name . ':description', 'de_' . $config['description'], 'de');
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+
+ i18n_string_translation_update($string_object_name . ':name', 'de_' . $config['name'], 'de');
+ $this->assertJobItemLangCodes($item, 'en', array('en', 'de'));
+ }
+
+ function testI18nStringPluginUI() {
+
+ $this->loginAsAdmin(array('administer taxonomy', 'translate interface', 'translate user-defined strings'));
+
+ $vocab_data = array(
+ 'name' => $this->randomName(),
+ 'machine_name' => 'test_vocab',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ );
+ $term_data = array(
+ 'name' => $this->randomName(),
+ );
+ $vocab_data_not_translated = array(
+ 'name' => $this->randomName(),
+ 'machine_name' => 'test_vocab3',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ );
+
+ // Configure taxonomy and create vocab + term.
+ $this->drupalPost('admin/structure/taxonomy/add', $vocab_data, t('Save'));
+ $this->drupalGet('admin/structure/taxonomy');
+ $this->clickLink(t('add terms'));
+ $this->drupalPost(NULL, $term_data, t('Save'));
+ $this->drupalPost('admin/structure/taxonomy/add', $vocab_data_not_translated, t('Save'));
+
+ $this->drupalGet('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary');
+ $this->assertText($vocab_data['name']);
+
+ // Request translation via i18n source tab
+ $this->drupalPost(NULL, array('items[taxonomy:vocabulary:1]' => 1), t('Request translation'));
+ // Test for the job checkout url.
+ $this->assertTrue(strpos($this->getUrl(), 'admin/tmgmt/jobs') !== FALSE);
+ entity_get_controller('tmgmt_job')->resetCache();
+ $jobs = entity_load('tmgmt_job', FALSE);
+ /** @var TMGMTJob $job */
+ $job = array_pop($jobs);
+ $this->assertFieldByName('label', $job->label());
+
+ // Request translation via translate tab of i18n.
+ $this->drupalPost('admin/structure/taxonomy/test_vocab/translate', array('languages[taxonomy:vocabulary:1:de]' => 1), t('Request translation'));
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Verify that the job item status is shown.
+ $this->assertText(t('Needs review'));
+ $this->clickLink(t('review'));
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+ $this->assertText(t('The translation for @label has been accepted.', array('@label' => $job->label())));
+
+ // Test the missing translation filter.
+ $this->drupalGet('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary');
+ // Check that the source language has been removed from the target language
+ // select box.
+ $elements = $this->xpath('//select[@name=:name]//option[@value=:option]', array(':name' => 'search[target_language]', ':option' => i18n_string_source_language()));
+ $this->assertTrue(empty($elements));
+ $edit = array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'untranslated',
+ );
+ $this->drupalPost('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary', $edit, t('Search'));
+ // The vocabulary name is translated to "de" therefore it must not show up
+ // in the list.
+ $this->assertNoText($vocab_data['name']);
+ $this->assertText($vocab_data_not_translated['name']);
+
+ $edit = array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'untranslated',
+ );
+ $this->drupalPost(NULL, $edit, t('Search'));
+ $this->assertNoText($vocab_data['name']);
+ $this->assertText($vocab_data_not_translated['name']);
+
+ // Update the string status to I18N_STRING_STATUS_UPDATE.
+ $lid = db_select('locales_source', 's')->fields('s', array('lid'))->condition('source', $vocab_data['name'])->execute()->fetchField();
+ db_update('locales_target')->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))->condition('lid', $lid)->execute();
+
+ $edit = array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'outdated',
+ );
+ $this->drupalPost(NULL, $edit, t('Search'));
+ $this->assertText($vocab_data['name']);
+ $this->assertNoText($vocab_data_not_translated['name']);
+
+ $edit = array(
+ 'search[target_language]' => 'de',
+ 'search[target_status]' => 'untranslated_or_outdated',
+ );
+ $this->drupalPost(NULL, $edit, t('Search'));
+ $this->assertText($vocab_data['name']);
+ $this->assertText($vocab_data_not_translated['name']);
+ }
+
+ /**
+ * Tests translation of blocks through the user interface.
+ */
+ function testI18nStringPluginUIBlock() {
+
+ $this->loginAsAdmin(array('administer blocks', 'translate interface', 'translate user-defined strings'));
+
+ // Make some blocks translatable.
+ $navigation_edit = array(
+ 'title' => $this->randomName(),
+ 'i18n_mode' => 1,
+ );
+ $this->drupalPost('admin/structure/block/manage/system/navigation/configure', $navigation_edit, t('Save block'));
+ $powered_edit = array(
+ 'title' => $this->randomName(),
+ 'i18n_mode' => 1,
+ );
+ $this->drupalPost('admin/structure/block/manage/system/powered-by/configure', $powered_edit, t('Save block'));
+
+ $this->drupalGet('admin/tmgmt/sources/i18n_string_block');
+ $this->assertText($navigation_edit['title']);
+ $this->assertText($powered_edit['title']);
+
+ // Request translation via i18n source tab.
+ $edit = array(
+ 'items[blocks:system:powered-by]' => 1,
+ 'items[blocks:system:navigation]' => 1,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+ $this->assertText($navigation_edit['title']);
+ $this->assertText($powered_edit['title']);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ $this->assertRaw(t('Active job item: Needs review'));
+ }
+
+ /**
+ * Tests translation of fields through the user interface.
+ */
+ function testI18nStringPluginUIField() {
+ $this->loginAsAdmin(array('translate interface', 'translate user-defined strings'));
+ $type = $this->drupalCreateContentType(array('type' => $type = $this->randomName()));
+
+ // Create a field.
+ $field = array(
+ 'field_name' => 'list_test',
+ 'type' => 'list_text',
+ );
+ for ($i = 0; $i < 5; $i++) {
+ $field['settings']['allowed_values'][$this->randomName()] = $this->randomString();
+ }
+ field_create_field($field);
+
+ // Create an instance of the previously created field.
+ $instance = array(
+ 'field_name' => 'list_test',
+ 'entity_type' => 'node',
+ 'bundle' => $type->type,
+ 'label' => $this->randomName(10),
+ 'description' => $this->randomString(30),
+ );
+ field_create_instance($instance);
+
+ // The body field doesn't have anything that can be translated on the field
+ // level, so it shouldn't show up in the field overview.
+ $this->drupalGet('admin/tmgmt/sources/i18n_string_field');
+ $this->assertNoText(t('Body'));
+ // @todo: Label doesn't work here?
+ $this->assertText('field:list_test:#allowed_values');
+
+ $this->drupalGet('admin/tmgmt/sources/i18n_string_field_instance');
+ $this->assertUniqueText(t('Body'));
+ $this->assertUniqueText($instance['label']);
+
+ // Request translation.
+ $edit = array(
+ 'items[field:body:' . $type->type . ']' => 1,
+ 'items[field:list_test:' . $type->type . ']' => 1,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+ $this->assertText(t('Body'));
+ $this->assertText($instance['label']);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ $this->assertRaw(t('Active job item: Needs review'));
+
+ // Review the first item.
+ $this->clickLink(t('reviewed'));
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+
+ // The overview should now have a translated field and a pending job item.
+ $this->drupalGet('admin/tmgmt/sources/i18n_string_field_instance');
+ $this->assertRaw(t('Translation up to date'));
+ $this->assertRaw(t('Active job item: Needs review'));
+
+ }
+
+ /**
+ * Test the i18n specific cart functionality.
+ */
+ function testCart() {
+ $vocabulary1 = entity_create('taxonomy_vocabulary', array(
+ 'name' => $this->randomName(),
+ 'description' => 'description_' . $this->randomName(),
+ 'machine_name' => 'test_vocab1',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ ));
+ taxonomy_vocabulary_save($vocabulary1);
+ $string1 = "taxonomy:vocabulary:" . $vocabulary1->vid;
+
+ $vocabulary2 = entity_create('taxonomy_vocabulary', array(
+ 'name' => $this->randomName(),
+ 'description' => 'description_' . $this->randomName(),
+ 'machine_name' => 'test_vocab2',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ ));
+ taxonomy_vocabulary_save($vocabulary2);
+ $string2 = "taxonomy:vocabulary:" . $vocabulary2->vid;
+
+ $vocabulary3 = entity_create('taxonomy_vocabulary', array(
+ 'name' => $this->randomName(),
+ 'description' => 'description_' . $this->randomName(),
+ 'machine_name' => 'test_vocab3',
+ 'i18n_mode' => I18N_MODE_LOCALIZE,
+ ));
+ taxonomy_vocabulary_save($vocabulary3);
+
+ $this->loginAsAdmin(array_merge($this->translator_permissions, array('translate interface', 'translate user-defined strings')));
+
+ // Test source overview.
+ $this->drupalPost('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary', array(
+ 'items[' . $string1 . ']' => TRUE,
+ 'items[' . $string2 . ']' => TRUE,
+ ), t('Add to cart'));
+ $this->drupalGet('admin/tmgmt/cart');
+ $this->assertText($vocabulary1->name);
+ $this->assertText($vocabulary2->name);
+
+ // Test translate tab.
+ $this->drupalGet('admin/structure/taxonomy/test_vocab3/translate');
+ $this->assertRaw(t('There are @count items in the translation cart.',
+ array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
+
+ $this->drupalPost(NULL, array(), t('Add to cart'));
+ $this->assertRaw(t('@count content source was added into the cart.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
+ $this->assertRaw(t('There are @count items in the translation cart including the current item.',
+ array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.ui.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.ui.inc
new file mode 100644
index 00000000..3b49269d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/i18n_string/tmgmt_i18n_string.ui.inc
@@ -0,0 +1,295 @@
+ $language) {
+ $langcode = str_replace('-', '', $langcode);
+ $languages['langcode-' . $langcode] = array(
+ 'data' => check_plain($language->name),
+ );
+ }
+
+ $header = array(
+ 'title' => array('data' => t('Label (in source language)')),
+ 'type' => array('data' => t('Type')),
+ ) + $languages;
+
+ return $header;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function overviewForm($form, &$form_state, $type) {
+ $form += $this->overviewSearchFormPart($form, $form_state, $type);
+
+ $form['items'] = array(
+ '#type' => 'tableselect',
+ '#header' => $this->overviewFormHeader($type),
+ '#empty' => t('No strings matching given criteria have been found.')
+ );
+
+ $search_data = $this->getSearchFormSubmittedParams();
+
+ $i18n_strings = tmgmt_i18n_string_get_strings($type, $search_data['label'], $search_data['target_language'], $search_data['target_status']);
+
+ foreach ($this->getTranslationData($i18n_strings, $form_state['item_type']) as $id => $data) {
+ $form['items']['#options'][$id] = $this->overviewRow($type, $data);
+ }
+
+ $form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
+
+ return $form;
+ }
+
+ /**
+ * Helper function to create translation data list for the sources page list.
+ *
+ * @param array $i18n_strings
+ * Result of the search query returned by tmgmt_i18n_string_get_strings().
+ * @param string $type
+ * I18n object type.
+ *
+ * @return array
+ * Structured array with translation data.
+ */
+ protected function getTranslationData($i18n_strings, $type) {
+ $objects = array();
+ $source_language = variable_get_value('i18n_string_source_language');
+
+ foreach ($i18n_strings as $i18n_string) {
+ $wrapper = tmgmt_i18n_string_get_wrapper($type, $i18n_string);
+
+ if ($wrapper instanceof i18n_string_object_wrapper) {
+ $id = $i18n_string->job_item_id;
+
+ // Get existing translations and current job items for the entity
+ // to determine translation statuses
+ $current_job_items = tmgmt_job_item_load_latest('i18n_string', $wrapper->get_type(), $id, $source_language);
+
+ $objects[$id] = array(
+ 'id' => $id,
+ 'object' => $wrapper->get_strings(array('empty' => TRUE)),
+ 'wrapper' => $wrapper,
+ );
+ // Load entity translation specific data.
+ foreach (language_list() as $langcode => $language) {
+ $langcode = str_replace('-', '', $langcode);
+
+ $translation_status = 'current';
+
+ if ($langcode == $source_language) {
+ $translation_status = 'original';
+ }
+ elseif ($i18n_string->{'lang_' . $langcode} === NULL) {
+ $translation_status = 'missing';
+ }
+
+ $objects[$id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL;
+ $objects[$id]['translation_statuses'][$langcode] = $translation_status;
+ }
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * Builds search form for entity sources overview.
+ *
+ * @param array $form
+ * Drupal form array.
+ * @param $form_state
+ * Drupal form_state array.
+ * @param $type
+ * Entity type.
+ *
+ * @return array
+ * Drupal form array.
+ */
+ public function overviewSearchFormPart($form, &$form_state, $type) {
+
+ $options = array();
+ foreach (language_list() as $langcode => $language) {
+ $options[$langcode] = $language->name;
+ }
+
+ $default_values = $this->getSearchFormSubmittedParams();
+
+ $form['search_wrapper'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#weight' => -15,
+ );
+ $form['search_wrapper']['search'] = array(
+ '#tree' => TRUE,
+ );
+ $form['search_wrapper']['search']['label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Label in source language'),
+ '#default_value' => isset($default_values['label']) ? $default_values['label'] : NULL,
+ );
+
+ // Unset the source language as it should not be listed among target
+ // languages.
+ unset($options[i18n_string_source_language()]);
+
+ $form['search_wrapper']['search']['target_language'] = array(
+ '#type' => 'select',
+ '#title' => t('Target language'),
+ '#options' => $options,
+ '#empty_option' => t('Any'),
+ '#default_value' => isset($default_values['target_language']) ? $default_values['target_language'] : NULL,
+ );
+ $form['search_wrapper']['search']['target_status'] = array(
+ '#type' => 'select',
+ '#title' => t('Target status'),
+ '#options' => array(
+ 'untranslated_or_outdated' => t('Untranslated or outdated'),
+ 'untranslated' => t('Untranslated'),
+ 'outdated' => t('Outdated'),
+ ),
+ '#default_value' => isset($default_values['target_status']) ? $default_values['target_status'] : NULL,
+ '#states' => array(
+ 'invisible' => array(
+ ':input[name="search[target_language]"]' => array('value' => ''),
+ ),
+ ),
+ );
+ $form['search_wrapper']['search_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Search'),
+ );
+
+ return $form;
+ }
+
+ /**
+ * Gets submitted search params.
+ *
+ * @return array
+ */
+ public function getSearchFormSubmittedParams() {
+ $params = array(
+ 'label' => NULL,
+ 'target_language' => NULL,
+ 'target_status' => NULL,
+ );
+
+ if (isset($_GET['label'])) {
+ $params['label'] = $_GET['label'];
+ }
+ if (isset($_GET['target_language'])) {
+ $params['target_language'] = $_GET['target_language'];
+ }
+ if (isset($_GET['target_status'])) {
+ $params['target_status'] = $_GET['target_status'];
+ }
+
+ return $params;
+ }
+
+ /**
+ * Builds a table row for overview form.
+ *
+ * @param string $type
+ * i18n type.
+ * @param array $data
+ * Data needed to build the list row.
+ *
+ * @return array
+ */
+ public function overviewRow($type, $data) {
+ // Set the default item key, assume it's the first.
+ $item_title = reset($data['object']);
+
+ $type_label = i18n_object_info($type, 'title');
+
+ $row = array(
+ 'id' => $data['id'],
+ 'title' => $item_title->get_string() ? t('@title (@id)', array('@title' => $item_title->get_string(), '@id' => $data['id'])) : $data['id'],
+ 'type' => empty($type_label) ? t('Unknown') : $type_label,
+ );
+
+ foreach (language_list() as $langcode => $language) {
+ $langcode = str_replace('-', '', $langcode);
+ $row['langcode-' . $langcode] = theme('tmgmt_ui_translation_language_status_single', array(
+ 'translation_status' => $data['translation_statuses'][$langcode],
+ 'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
+ ));
+ }
+
+ return $row;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function overviewFormSubmit($form, &$form_state, $type) {
+
+ // Handle search redirect.
+ $this->overviewSearchFormRedirect($form, $form_state, $type);
+ $items = array_filter($form_state['values']['items']);
+ $type = $form_state['item_type'];
+
+ $source_lang = variable_get_value('i18n_string_source_language');
+
+ // Create only single job for all items as the source language is just
+ // the same for all.
+ $job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
+
+ // Loop through entities and create individual jobs for each source language.
+ foreach ($items as $item) {
+ $job->addItem('i18n_string', $type, $item);
+ }
+
+ $form_state['redirect'] = array('admin/tmgmt/jobs/' . $job->tjid,
+ array('query' => array('destination' => current_path())));
+ drupal_set_message(t('One job needs to be checked out.'));
+ }
+
+ /**
+ * Performs redirect with search params appended to the uri.
+ *
+ * In case of triggering element is edit-search-submit it redirects to
+ * current location with added query string containing submitted search form
+ * values.
+ *
+ * @param array $form
+ * Drupal form array.
+ * @param $form_state
+ * Drupal form_state array.
+ * @param $type
+ * Entity type.
+ */
+ public function overviewSearchFormRedirect($form, &$form_state, $type) {
+ if ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
+
+ $query = array();
+
+ foreach ($form_state['values']['search'] as $key => $value) {
+ $query[$key] = $value;
+ }
+
+ drupal_goto($_GET['q'], array('query' => $query));
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tests/test.xx.po b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tests/test.xx.po
new file mode 100644
index 00000000..4b435a13
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tests/test.xx.po
@@ -0,0 +1,13 @@
+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 "Hello World"
+msgstr "Hallo Welt"
+
+msgid "Example"
+msgstr "Beispiel"
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.info b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.info
new file mode 100644
index 00000000..230a3780
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.info
@@ -0,0 +1,17 @@
+name = Locales Source
+description = Locales source plugin for the Translation Management system.
+package = Translation Management
+core = 7.x
+dependencies[] = tmgmt
+dependencies[] = locale
+files[] = tmgmt_locale.plugin.inc
+files[] = tmgmt_locale.test
+files[] = tmgmt_locale.ui.inc
+files[] = tmgmt_locale.ui.test
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.install b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.install
new file mode 100644
index 00000000..94ccf552
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.install
@@ -0,0 +1,28 @@
+condition('ji.plugin', 'locale')
+ ->condition('ji.state', TMGMT_JOB_ITEM_STATE_ACCEPTED);
+ $query->innerJoin('tmgmt_job', 'j', 'j.tjid = ji.tjid');
+ $query->addField('ji', 'item_id', 'lid');
+ $query->addField('j', 'target_language', 'language');
+ foreach ($query->execute() as $row) {
+ db_update('locales_target')
+ ->condition('lid', $row->lid)
+ ->condition('language', $row->language)
+ ->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
+ ->execute();
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.module b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.module
new file mode 100644
index 00000000..3f089105
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.module
@@ -0,0 +1,25 @@
+ t('Locale source'),
+ 'description' => t('Source handler for locale strings.'),
+ 'plugin controller class' => 'TMGMTLocaleSourcePluginController',
+ 'ui controller class' => 'TMGMTLocaleSourceUIController',
+ 'item types' => array(
+ 'default' => t('Locale'),
+ ),
+ );
+
+ return $info;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.plugin.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.plugin.inc
new file mode 100644
index 00000000..a0d210f4
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.plugin.inc
@@ -0,0 +1,245 @@
+ $lid, ':language' => $target_language))
+ ->fetchField();
+
+ $fields = array(
+ 'translation' => $translation,
+ );
+ if (module_exists('l10n_update')) {
+ module_load_include('inc', 'l10n_update');
+ $fields += array(
+ 'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
+ );
+ }
+
+ // @todo Only singular strings are managed here, we should take care of
+ // plural information of processed string.
+ if (!$exists) {
+ $fields += array(
+ 'lid' => $lid,
+ 'language' => $target_language,
+ );
+ db_insert('locales_target')
+ ->fields($fields)
+ ->execute();
+ }
+ else {
+ db_update('locales_target')
+ ->fields($fields)
+ ->condition('lid', $lid)
+ ->condition('language', $target_language)
+ ->execute();
+ }
+ // Clear locale caches.
+ _locale_invalidate_js($target_language);
+ cache_clear_all('locale:' . $target_language, 'cache');
+ return TRUE;
+ }
+
+ /**
+ * Helper function to obtain a locale object for given job item.
+ *
+ * @param TMGMTJobItem $job_item
+ *
+ * @return locale object
+ */
+ protected function getLocaleObject(TMGMTJobItem $job_item) {
+ $locale_lid = $job_item->item_id;
+
+ // Check existence of assigned lid.
+ $exists = db_query("SELECT COUNT(lid) FROM {locales_source} WHERE lid = :lid", array(':lid' => $locale_lid))->fetchField();
+ if (!$exists) {
+ throw new TMGMTException(t('Unable to load locale with id %id', array('%id' => $job_item->item_id)));
+ }
+
+ // This is necessary as the method is also used in the getLabel() callback
+ // and for that case the job is not available in the cart.
+ if (!empty($job_item->tjid)) {
+ $source_language = $job_item->getJob()->source_language;
+ }
+ else {
+ $source_language = $job_item->getSourceLangCode();
+ }
+
+ if ($source_language == 'en') {
+ $query = db_select('locales_source', 'ls');
+ $query
+ ->fields('ls')
+ ->condition('ls.lid', $locale_lid);
+ $locale_object = $query
+ ->execute()
+ ->fetchObject();
+
+ $locale_object->language = 'en';
+
+ if (empty($locale_object)) {
+ return null;
+ }
+
+ $locale_object->origin = 'source';
+ }
+ else {
+ $query = db_select('locales_target', 'lt');
+ $query->join('locales_source', 'ls', 'ls.lid = lt.lid');
+ $query
+ ->fields('lt')
+ ->fields('ls')
+ ->condition('lt.lid', $locale_lid)
+ ->condition('lt.language', $source_language);
+ $locale_object = $query
+ ->execute()
+ ->fetchObject();
+
+ if (empty($locale_object)) {
+ return null;
+ }
+
+ $locale_object->origin = 'target';
+ }
+
+ return $locale_object;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel(TMGMTJobItem $job_item) {
+ if ($locale_object = $this->getLocaleObject($job_item)) {
+ if ($locale_object->origin == 'source') {
+ $label = $locale_object->source;
+ }
+ else {
+ $label = $locale_object->translation;
+ }
+ return truncate_utf8(strip_tags($label), 30, FALSE, TRUE);
+ }
+ }
+
+ /**
+ * [@inheritdoc}
+ */
+ public function getType(TMGMTJobItem $job_item) {
+ return $this->getItemTypeLabel($job_item->item_type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getData(TMGMTJobItem $job_item) {
+ $locale_object = $this->getLocaleObject($job_item);
+ if (empty($locale_object)) {
+ $languages = language_list();
+ throw new TMGMTException(t('Unable to load %language translation for the locale %id',
+ array('%language' => $languages[$job_item->getJob()->source_language]->name, '%id' => $job_item->item_id)));
+ }
+
+ if ($locale_object->origin == 'source') {
+ $text = $locale_object->source;
+ }
+ else {
+ $text = $locale_object->translation;
+ }
+
+ // Identify placeholders that need to be escaped. Assume that placeholders
+ // consist of alphanumeric characters and _,- only and are delimited by
+ // non-alphanumeric characters. There are cases that don't match, for
+ // example appended SI units like "@valuems", there only @value is the
+ // actual placeholder.
+ $escape = array();
+ if (preg_match_all('/([@!%][a-zA-Z0-9_-]+)/', $text, $matches, PREG_OFFSET_CAPTURE)) {
+ foreach ($matches[0] as $match) {
+ $escape[$match[1]]['string'] = $match[0];
+ }
+ }
+ $structure['singular'] = array(
+ '#label' => t('Singular'),
+ '#text' => (string) $text,
+ '#translate' => TRUE,
+ '#escape' => $escape,
+ );
+ return $structure;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveTranslation(TMGMTJobItem $job_item) {
+ $job = tmgmt_job_load($job_item->tjid);
+ $data = $job_item->getData();
+ if (isset($data['singular'])) {
+ $translation = $data['singular']['#translation']['#text'];
+ // Update the locale string in the system.
+ // @todo: Send error message to user if update fails.
+ if ($this->updateTranslation($job_item->item_id, $job->target_language, $translation)) {
+ $job_item->accepted();
+ }
+ }
+
+ // @todo: Temporary backwards compability with existing jobs, remove in next
+ // release.
+ if (isset($data[$job_item->item_id])) {
+ $translation = $data[$job_item->item_id]['#translation']['#text'];
+ // Update the locale string in the system.
+ // @todo: Send error message to user if update fails.
+ if ($this->updateTranslation($job_item->item_id, $job->target_language, $translation)) {
+ $job_item->accepted();
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSourceLangCode(TMGMTJobItem $job_item) {
+ // For the locale source English is always the source language.
+ return 'en';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLangCodes(TMGMTJobItem $job_item) {
+ $query = db_select('locales_target', 'lt');
+ $query->fields('lt', array('language'));
+ $query->condition('lt.lid', $job_item->item_id);
+
+ $existing_lang_codes = array('en');
+ foreach ($query->execute() as $language) {
+ $existing_lang_codes[] = $language->language;
+ }
+
+ return $existing_lang_codes;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.test b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.test
new file mode 100644
index 00000000..917b1c9f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.test
@@ -0,0 +1,231 @@
+ 'Locale Source tests',
+ 'description' => 'Exporting source data from locale and saving translations back',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_locale'));
+ $this->langcode = 'de';
+ $this->context = 'default';
+ $file = new stdClass();
+ $file->uri = drupal_realpath(drupal_get_path('module', 'tmgmt_locale') . '/tests/test.xx.po');
+ $this->pofile = file_save($file);
+ $this->setEnvironment($this->langcode);
+ $this->setEnvironment('es');
+ }
+
+ /**
+ * Tests translation of a locale singular term.
+ */
+ function testSingularTerm() {
+ // Load PO file to create a locale structure in the database.
+ _locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
+
+ // Obtain one locale string with translation.
+ $locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
+ $source_text = $locale_object->source;
+
+ // Create the new job and job item.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $item1 = $job->addItem('locale', 'default', $locale_object->lid);
+
+ // Check the structure of the imported data.
+ $this->assertEqual($item1->item_id, $locale_object->lid, 'Locale Strings object correctly saved');
+ $this->assertEqual('Locale', $item1->getSourceType());
+ $this->assertEqual('Hello World', $item1->getSourceLabel());
+ $job->requestTranslation();
+
+ foreach ($job->getItems() as $item) {
+ /* @var $item TMGMTJobItem */
+ $item->acceptTranslation();
+ $this->assertTrue($item->isAccepted());
+ // The source is now available in en and de.
+ $this->assertJobItemLangCodes($item, 'en', array('en', 'de'));
+ }
+
+ // Check string translation.
+ $expected_translation = $job->target_language . '_' . $source_text;
+ $this->assertTranslation($locale_object->lid, 'de', $expected_translation);
+
+ // Translate the german translation to spanish.
+ $target_langcode = 'es';
+ $job = $this->createJob('de', $target_langcode);
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $item1 = $job->addItem('locale', 'default', $locale_object->lid);
+ $this->assertEqual('Locale', $item1->getSourceType());
+ $this->assertEqual($expected_translation, $item1->getSourceLabel());
+ $job->requestTranslation();
+
+ foreach ($job->getItems() as $item) {
+ /* @var $item TMGMTJobItem */
+ $item->acceptTranslation();
+ $this->assertTrue($item->isAccepted());
+
+ // The source should be now available for en, de and es languages.
+ $this->assertJobItemLangCodes($item, 'en', array('en', 'de', 'es'));
+ }
+
+ // Check string translation.
+ $this->assertTranslation($locale_object->lid, $target_langcode, $job->target_language . '_' . $expected_translation);
+ }
+
+ /**
+ * Test if the source is able to pull content in requested language.
+ */
+ function testRequestDataForSpecificLanguage() {
+ $this->setEnvironment('cs');
+
+ _locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
+ $locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
+
+ $plugin = new TMGMTLocaleSourcePluginController('locale', 'locale');
+ $reflection_plugin = new ReflectionClass('TMGMTLocaleSourcePluginController');
+ $updateTranslation = $reflection_plugin->getMethod('updateTranslation');
+ $updateTranslation->setAccessible(TRUE);
+
+ $updateTranslation->invoke($plugin, $locale_object->lid, 'de', 'de translation');
+
+ // Create the new job and job item.
+ $job = $this->createJob('de', 'cs');
+ $job->save();
+ $job->addItem('locale', 'default', $locale_object->lid);
+
+ $data = $job->getData();
+ $this->assertEqual($data[1]['singular']['#text'], 'de translation');
+
+ // Create new job item with a source language for which the translation
+ // does not exit.
+ $job = $this->createJob('es', 'cs');
+ $job->save();
+ try {
+ $job->addItem('locale', 'default', $locale_object->lid);
+ $this->fail('The job item should not be added as there is no translation for language "es"');
+ }
+ catch (TMGMTException $e) {
+ $languages = language_list();
+ $this->assertEqual(t('Unable to load %language translation for the locale %id',
+ array('%language' => $languages['es']->name, '%id' => $locale_object->lid)), $e->getMessage());
+ }
+ }
+
+ /**
+ * Verifies that strings that need escaping are correctly identified.
+ */
+ function testEscaping() {
+ $lid = db_insert('locales_source')
+ ->fields(array(
+ 'source' => '@place-holders need %to be !esc_aped.',
+ 'textgroup' => 'default',
+ 'context' => '',
+ ))
+ ->execute();
+ $job = $this->createJob('en', 'de');
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ $item = $job->addItem('locale', 'default', $lid);
+ $data = $item->getData();
+ $expected_escape = array(
+ 0 => array('string' => '@place-holders'),
+ 20 => array('string' => '%to'),
+ 27 => array('string' => '!esc_aped'),
+ );
+ $this->assertEqual($data['singular']['#escape'], $expected_escape);
+
+ // Invalid patterns that should be ignored.
+ $lid = db_insert('locales_source')
+ ->fields(array(
+ 'source' => '@ % ! example',
+ 'textgroup' => 'default',
+ 'context' => '',
+ ))
+ ->execute();
+
+ $item = $job->addItem('locale', 'default', $lid);
+ $data = $item->getData();
+ $this->assertTrue(empty($data[$lid]['#escape']));
+
+ }
+
+ /**
+ * Tests that system behaves correctly with an non-existing locales.
+ */
+ function testInexistantSource() {
+ // Create inexistant locale object.
+ $locale_object = new stdClass();
+ $locale_object->lid = 0;
+
+ // Create the job.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ // Create the job item.
+ try {
+ $job->addItem('locale', 'default', $locale_object->lid);
+ $this->fail('Job item add with an inexistant locale.');
+ }
+ catch (TMGMTException $e) {
+ $this->pass('Exception thrown when trying to translate non-existing locale string');
+ }
+
+ // Try to translate a source string without translation from german to
+ // spanish.
+ $lid = db_insert('locales_source')
+ ->fields(array(
+ 'source' => 'No translation',
+ 'textgroup' => 'default',
+ 'context' => '',
+ ))
+ ->execute();
+ $job = $this->createJob('de', 'fr');
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ try {
+ $job->addItem('locale', 'default', $lid);
+ $this->fail('Job item add with an non-existing locale did not fail.');
+ }
+ catch (TMGMTException $e) {
+ $this->pass('Job item add with an non-existing locale did fail.');
+ }
+ }
+
+ /**
+ * Asserts a locale translation.
+ *
+ * @param int $lid
+ * The locale source id.
+ * @param string $target_langcode
+ * The target language code.
+ * @param string $expected_translation
+ * The expected translation.
+ */
+ public function assertTranslation($lid, $target_langcode, $expected_translation) {
+ $actual_translation = db_query('SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language', array(
+ ':lid' => $lid,
+ ':language' => $target_langcode
+ ))->fetchField();
+ $this->assertEqual($actual_translation, $expected_translation);
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.ui.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.ui.inc
new file mode 100644
index 00000000..4b579719
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.ui.inc
@@ -0,0 +1,316 @@
+fields('ls', array('lid', 'source'));
+
+ $select->addTag('tmgmt_sources_search');
+ $select->addMetaData('plugin', 'locale');
+ $select->addMetaData('type', $textgroup);
+
+ $select->condition('ls.textgroup', $textgroup);
+ if (!empty($search_label)) {
+ $select->condition('ls.source', "%$search_label%", 'LIKE');
+ }
+ if (!empty($missing_target_language) && in_array($missing_target_language, $languages)) {
+ $select->isNull("lt_$missing_target_language.language");
+ }
+
+ // Join locale targets for each language.
+ // We want all joined fields to be named as langcodes, but langcodes could
+ // contain hyphens in their names, which is not allowed by the most database
+ // engines. So we create a langcode-to-filed_alias map, and rename fields
+ // later.
+ $langcode_to_filed_alias_map = array();
+ foreach ($languages as $langcode) {
+ $table_alias = $select->leftJoin('locales_target', db_escape_field("lt_$langcode"), "ls.lid = %alias.lid AND %alias.language = '$langcode'");
+ $langcode_to_filed_alias_map[$langcode] = $select->addField($table_alias, 'language');
+ }
+
+ $select = $select->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
+ $rows = $select->execute()->fetchAll();
+ foreach ($rows as $row) {
+ foreach ($langcode_to_filed_alias_map as $langcode => $field_alias) {
+ $row->{$langcode} = $row->{$field_alias};
+ unset($row->{$field_alias});
+ }
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Gets overview form header.
+ *
+ * @return array
+ * Header array definition as expected by theme_tablesort().
+ */
+ public function overviewFormHeader() {
+ $languages = array();
+ foreach (language_list() as $langcode => $language) {
+ $languages['langcode-' . $langcode] = array(
+ 'data' => check_plain($language->name),
+ );
+ }
+
+ $header = array(
+ 'source' => array('data' => t('Source text')),
+ ) + $languages;
+
+ return $header;
+ }
+
+ /**
+ * Implements TMGMTSourceUIControllerInterface::overviewForm().
+ */
+ public function overviewForm($form, &$form_state, $type) {
+ $form += $this->overviewSearchFormPart($form, $form_state, $type);
+
+ $form['items'] = array(
+ '#type' => 'tableselect',
+ '#header' => $this->overviewFormHeader($type),
+ '#empty' => t('No strings matching given criteria have been found.')
+ );
+
+ $search_data = $this->getSearchFormSubmittedParams();
+
+ $strings = $this->getStrings($type, $search_data['label'], $search_data['missing_target_language']);
+
+ foreach ($this->getTranslationData($strings, $type) as $id => $data) {
+ $form['items']['#options'][$id] = $this->overviewRow($type, $data);
+ }
+
+ $form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
+
+ return $form;
+ }
+
+ /**
+ * Helper function to create translation data list for the sources page list.
+ *
+ * @param array $strings
+ * Result of the search query returned by tmgmt_i18n_string_get_strings().
+ * @param string $type
+ * I18n object type.
+ *
+ * @return array
+ * Structured array with translation data.
+ */
+ protected function getTranslationData($strings, $type) {
+ $objects = array();
+ // Source language of locale strings is always english.
+ $source_language = 'en';
+
+ foreach ($strings as $string) {
+ $id = $string->lid;
+
+ // Get existing translations and current job items for the entity
+ // to determine translation statuses
+ $current_job_items = tmgmt_job_item_load_latest('locale', $type, $id, $source_language);
+
+ $objects[$id] = array(
+ 'id' => $id,
+ 'object' => $string
+ );
+ // Load entity translation specific data.
+ foreach (language_list() as $langcode => $language) {
+ $translation_status = 'current';
+
+ if ($langcode == $source_language) {
+ $translation_status = 'original';
+ }
+ elseif ($string->{$langcode} === NULL) {
+ $translation_status = 'missing';
+ }
+
+ $objects[$id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL;
+ $objects[$id]['translation_statuses'][$langcode] = $translation_status;
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * Builds search form for entity sources overview.
+ *
+ * @param array $form
+ * Drupal form array.
+ * @param $form_state
+ * Drupal form_state array.
+ * @param $type
+ * Entity type.
+ *
+ * @return array
+ * Drupal form array.
+ */
+ public function overviewSearchFormPart($form, &$form_state, $type) {
+
+ $options = array();
+ foreach (language_list() as $langcode => $language) {
+ $options[$langcode] = $language->name;
+ }
+
+ $default_values = $this->getSearchFormSubmittedParams();
+
+ $form['search_wrapper'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#weight' => -15,
+ );
+ $form['search_wrapper']['search'] = array(
+ '#tree' => TRUE,
+ );
+ $form['search_wrapper']['search']['label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Source text'),
+ '#default_value' => isset($default_values['label']) ? $default_values['label'] : NULL,
+ );
+
+ // Unset English as it is the source language for all locale strings.
+ unset($options['en']);
+
+ $form['search_wrapper']['search']['missing_target_language'] = array(
+ '#type' => 'select',
+ '#title' => t('Not translated to'),
+ '#options' => $options,
+ '#empty_option' => '--',
+ '#default_value' => isset($default_values['missing_target_language']) ? $default_values['missing_target_language'] : NULL,
+ );
+ $form['search_wrapper']['search_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Search'),
+ );
+
+ return $form;
+ }
+
+ /**
+ * Gets submitted search params.
+ *
+ * @return array
+ */
+ public function getSearchFormSubmittedParams() {
+ $params = array(
+ 'label' => NULL,
+ 'missing_target_language' => NULL,
+ );
+
+ if (isset($_GET['label'])) {
+ $params['label'] = $_GET['label'];
+ }
+ if (isset($_GET['missing_target_language'])) {
+ $params['missing_target_language'] = $_GET['missing_target_language'];
+ }
+
+ return $params;
+ }
+
+ /**
+ * Builds a table row for overview form.
+ *
+ * @param string $type
+ * i18n type.
+ * @param array $data
+ * Data needed to build the list row.
+ *
+ * @return array
+ */
+ public function overviewRow($type, $data) {
+ // Set the default item key, assume it's the first.
+ $source = $data['object'];
+
+ $row = array(
+ 'id' => $data['id'],
+ 'source' => check_plain($source->source),
+ );
+
+ foreach (language_list() as $langcode => $language) {
+ $row['langcode-' . $langcode] = theme('tmgmt_ui_translation_language_status_single', array(
+ 'translation_status' => $data['translation_statuses'][$langcode],
+ 'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
+ ));
+ }
+
+ return $row;
+ }
+
+ /**
+ * Implements TMGMTSourceUIControllerInterface::overviewFormSubmit().
+ */
+ public function overviewFormSubmit($form, &$form_state, $type) {
+ // Handle search redirect.
+ $this->overviewSearchFormRedirect($form, $form_state, $type);
+ $items = array_filter($form_state['values']['items']);
+ $type = $form_state['item_type'];
+
+ $source_lang = 'en';
+
+ // Create only single job for all items as the source language is just
+ // the same for all.
+ $job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
+
+ // Loop through entities and create individual jobs for each source language.
+ foreach ($items as $item) {
+ $job->addItem('locale', $type, $item);
+ }
+
+ $form_state['redirect'] = array('admin/tmgmt/jobs/' . $job->tjid,
+ array('query' => array('destination' => current_path())));
+ drupal_set_message(t('One job needs to be checked out.'));
+ }
+
+ /**
+ * Performs redirect with search params appended to the uri.
+ *
+ * In case of triggering element is edit-search-submit it redirects to
+ * current location with added query string containing submitted search form
+ * values.
+ *
+ * @param array $form
+ * Drupal form array.
+ * @param $form_state
+ * Drupal form_state array.
+ * @param $type
+ * Entity type.
+ */
+ public function overviewSearchFormRedirect($form, &$form_state, $type) {
+ if ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
+
+ $query = array();
+
+ foreach ($form_state['values']['search'] as $key => $value) {
+ $query[$key] = $value;
+ }
+
+ drupal_goto($_GET['q'], array('query' => $query));
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.ui.test b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.ui.test
new file mode 100644
index 00000000..d479425c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/locale/tmgmt_locale.ui.test
@@ -0,0 +1,105 @@
+ 'Locale Source UI tests',
+ 'description' => 'Tests the locale source overview',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_locale', 'tmgmt_ui'));
+ $this->langcode = 'de';
+ $this->context = 'default';
+ $file = new stdClass();
+ $file->uri = drupal_realpath(drupal_get_path('module', 'tmgmt_locale') . '/tests/test.xx.po');
+ $this->pofile = file_save($file);
+ $this->setEnvironment($this->langcode);
+ $this->setEnvironment('gsw-berne');
+ }
+
+
+
+ public function testOverview() {
+ // Load PO file to create a locale structure in the database.
+ _locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
+
+ $this->loginAsTranslator();
+ $this->drupalGet('admin/tmgmt/sources/locale_default');
+
+ $this->assertText('Hello World');
+ $this->assertText('Example');
+ $rows = $this->xpath('//tbody/tr');
+ foreach ($rows as $row) {
+ if ($row->td[1] == 'Hello World') {
+ $this->assertEqual((string) $row->td[3]->div['title'], t('Translation up to date'));
+ $this->assertEqual((string) $row->td[4]->div['title'], t('Not translated'));
+ }
+ }
+
+ // Filter on the label.
+ $edit = array('search[label]' => 'Hello');
+ $this->drupalPost(NULL, $edit, t('Search'));
+
+ $this->assertText('Hello World');
+ $this->assertNoText('Example');
+
+ $locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
+
+ // First add source to the cart to test its functionality.
+ $edit = array(
+ 'items[' . $locale_object->lid . ']' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Add to cart'));
+ $this->assertRaw(t('@count content source was added into the cart.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
+ $edit['target_language[]'] = array('gsw-berne');
+ $this->drupalPost('admin/tmgmt/cart', $edit, t('Request translation'));
+
+ // Assert that the job item is displayed.
+ $this->assertText('Hello World');
+ $this->assertText(t('Locale'));
+ $this->assertText('2');
+ $this->drupalPost(NULL, array('target_language' => 'gsw-berne'), t('Submit to translator'));
+
+ // Test for the translation flag title.
+ $this->drupalGet('admin/tmgmt/sources/locale_default');
+ $this->assertRaw(t('Active job item: Needs review'));
+
+ // Review and accept the job item.
+ $job_items = tmgmt_job_item_load_latest('locale', 'default', $locale_object->lid, 'en');
+ $this->drupalGet('admin/tmgmt/items/' . $job_items['gsw-berne']->tjiid);
+ $this->assertRaw('gsw-berne_Hello World');
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+ $this->drupalGet('admin/tmgmt/sources/locale_default');
+
+ $this->assertNoRaw(t('Active job item: Needs review'));
+ $rows = $this->xpath('//tbody/tr');
+ foreach ($rows as $row) {
+ if ($row->td[1] == 'Hello World') {
+ $this->assertEqual((string) $row->td[3]->div['title'], t('Translation up to date'));
+ $this->assertEqual((string) $row->td[4]->div['title'], t('Translation up to date'));
+ }
+ }
+
+ // Test the missing translation filter.
+ $this->drupalGet('admin/tmgmt/sources/locale_default');
+ // Check that the source language (en) has been removed from the target language
+ // select box.
+ $elements = $this->xpath('//select[@name=:name]//option[@value=:option]', array(':name' => 'search[target_language]', ':option' => 'en'));
+ $this->assertTrue(empty($elements));
+
+ // Filter on the "Not translated to".
+ $edit = array('search[missing_target_language]' => 'gsw-berne');
+ $this->drupalPost(NULL, $edit, t('Search'));
+ // Hello World is translated to "gsw-berne" therefore it must not show up in the
+ // list.
+ $this->assertNoText('Hello World');
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.api.php b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.api.php
new file mode 100644
index 00000000..f032c628
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.api.php
@@ -0,0 +1,21 @@
+revision = 1;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.info b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.info
new file mode 100644
index 00000000..5c47b283
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.info
@@ -0,0 +1,26 @@
+name = Content Source
+description = Content Translation source plugin for the Translation Management system.
+package = Translation Management
+core = 7.x
+
+dependencies[] = tmgmt
+dependencies[] = tmgmt_field
+dependencies[] = translation
+
+files[] = tmgmt_node.plugin.inc
+files[] = tmgmt_node.ui.inc
+files[] = tmgmt_node.test
+
+; Views integration and handlers
+files[] = views/tmgmt_node.views.inc
+files[] = views/handlers/tmgmt_node_handler_field_translation_language_status.inc
+files[] = views/handlers/tmgmt_node_handler_field_translation_language_status_single.inc
+files[] = views/handlers/tmgmt_node_handler_filter_node_translatable_types.inc
+files[] = views/handlers/tmgmt_node_handler_filter_missing_translation.inc
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.module b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.module
new file mode 100644
index 00000000..0ed6a539
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.module
@@ -0,0 +1,35 @@
+ t('Node'),
+ 'description' => t('Source handler for nodes.'),
+ 'plugin controller class' => 'TMGMTNodeSourcePluginController',
+ 'ui controller class' => 'TMGMTNodeSourceUIController',
+ 'views controller class' => 'TMGMTNodeSourceViewsController',
+ 'item types' => array(),
+ );
+ foreach (node_type_get_names() as $type => $name) {
+ if (translation_supported_type($type)) {
+ $info['node']['item types'][$type] = $name;
+ }
+ }
+ return $info;
+}
+
+/**
+ * Form element validator for the missing target language views field handler.
+ */
+function tmgmt_node_views_exposed_target_language_validate($form, &$form_state) {
+ if (!empty($form_state['values']['tmgmt_node_missing_translation']) && $form_state['values']['language_1'] == $form_state['values']['tmgmt_node_missing_translation']) {
+ form_set_error('tmgmt_node_missing_translation', t('The source and target languages must not be the same.'));
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.plugin.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.plugin.inc
new file mode 100644
index 00000000..b72d9c52
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.plugin.inc
@@ -0,0 +1,152 @@
+item_id);
+ $source_language = $job_item->getJob()->source_language;
+ $languages = language_list();
+
+ // If the node language is not the same as the job source language try to
+ // load its translation for the job source language.
+ if ($node->language != $source_language) {
+ $translation_loaded = FALSE;
+ foreach (translation_node_get_translations($node->nid) as $language => $translation) {
+ if ($language == $source_language) {
+ $node = node_load($translation->nid);
+ $translation_loaded = TRUE;
+ break;
+ }
+ }
+
+ if (!$translation_loaded) {
+ throw new TMGMTException(t('Unable to load %language translation for the node %title',
+ array('%language' => $languages[$source_language]->name, '%title' => $node->title)));
+ }
+ }
+
+ $type = node_type_get_type($node);
+ // Get all the fields that can be translated and arrange their values into
+ // a specific structure.
+ $structure = tmgmt_field_get_source_data('node', $node, $job_item->getJob()->source_language);
+ $structure['node_title']['#label'] = $type->title_label;
+ $structure['node_title']['#text'] = $node->title;
+ return $structure;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveTranslation(TMGMTJobItem $job_item) {
+ if ($node = node_load($job_item->item_id)) {
+ $job = $job_item->getJob();
+ if (empty($node->tnid)) {
+ // We have no translation source nid, this is a new set, so create it.
+ $node->tnid = $node->nid;
+ node_save($node);
+ }
+ $translations = translation_node_get_translations($node->tnid);
+ if (isset($translations[$job->target_language])) {
+ // We have already a translation for the source node for the target
+ // language, so load it.
+ $tnode = node_load($translations[$job->target_language]->nid);
+ }
+ else {
+ // We don't have a translation for the source node yet, so create one.
+ $tnode = clone $node;
+ unset($tnode->nid, $tnode->vid, $tnode->uuid, $tnode->vuuid);
+ $tnode->language = $job->target_language;
+ $tnode->translation_source = $node;
+ }
+
+ // Allow modules and translator plugins to alter, for example in the
+ // case of creating revisions for translated nodes, or altering
+ // properties of the tnode before saving.
+ drupal_alter('tmgmt_before_update_node_translation', $tnode, $node, $job_item);
+
+ // Time to put the translated data into the node.
+ $data = $job_item->getData();
+ // Special case for the node title.
+ if (isset($data['node_title']['#translation']['#text'])) {
+ $tnode->title = $data['node_title']['#translation']['#text'];
+ unset($data['node_title']);
+ }
+ tmgmt_field_populate_entity('node', $tnode, $job->target_language, $data, FALSE);
+ // Reset translation field, which determines outdated status.
+ $tnode->translation['status'] = 0;
+ node_save($tnode);
+
+ // We just saved the translation, set the sate of the job item to
+ // 'finished'.
+ $job_item->accepted();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel(TMGMTJobItem $job_item) {
+ if ($node = node_load($job_item->item_id)) {
+ return entity_label('node', $node);
+ }
+ return parent::getLabel($job_item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUri(TMGMTJobItem $job_item) {
+ if ($node = node_load($job_item->item_id)) {
+ return entity_uri('node', $node);
+ }
+ return parent::getUri($job_item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType(TMGMTJobItem $job_item) {
+ if ($node = node_load($job_item->item_id)) {
+ return node_type_get_name($node);
+ }
+ return parent::getType($job_item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSourceLangCode(TMGMTJobItem $job_item) {
+ if ($node = node_load($job_item->item_id)) {
+ return entity_language('node', $node);
+ }
+
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLangCodes(TMGMTJobItem $job_item) {
+ $existing_lang_codes = array();
+ if ($node = node_load($job_item->item_id)) {
+ $existing_lang_codes = array(entity_language('node', $node));
+ }
+ if ($translations = translation_node_get_translations($job_item->item_id)) {
+ $existing_lang_codes = array_unique(array_merge($existing_lang_codes, array_keys($translations)));
+ }
+
+ return $existing_lang_codes;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.test b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.test
new file mode 100644
index 00000000..077ddf7e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.test
@@ -0,0 +1,147 @@
+ 'Node Source tests',
+ 'description' => 'Exporting source data from nodes and saving translations back to nodes',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_node', 'translation'));
+ $this->loginAsAdmin();
+ $this->setEnvironment('de');
+ $this->createNodeType('page', 'Basic page', TRANSLATION_ENABLED, FALSE);
+ $this->attachFields('node', 'page', array(TRUE, TRUE, FALSE, FALSE));
+ }
+
+ /**
+ * Tests nodes field translation.
+ */
+ function testNodeSource() {
+
+ // Create a translation job.
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+
+ for ($i = 0; $i < 2; $i++) {
+ $node = $this->createNode('page');
+ // Create a job item for this node and add it to the job.
+ $item = $job->addItem('node', 'node', $node->nid);
+ $this->assertEqual('Basic page', $item->getSourceType());
+ }
+
+ // Translate the job.
+ $job->requestTranslation();
+
+ foreach ($job->getItems() as $item) {
+ // The source is only available in en.
+ $this->assertJobItemLangCodes($item, 'en', array('en'));
+
+ $item->acceptTranslation();
+ $node = node_load($item->item_id);
+ // Check if the tnid attribute is bigger than 0.
+ $this->assertTrue($node->tnid > 0, 'The source node is part of a translation set.');
+ // The translations may be statically cached, so make make sure
+ // to reset the cache before loading the node translations.
+ $cached_translations = & drupal_static('translation_node_get_translations', array());
+ unset($cached_translations[$node->tnid]);
+ // Load the translation set of the source node.
+ $translations = translation_node_get_translations($node->tnid);
+ $this->assertNotNull($translations['de'], 'Translation found for the source node.');
+ if (isset($translations['de'])) {
+ $tnode = node_load($translations['de']->nid, NULL, TRUE);
+ $this->checkTranslatedData($tnode, $item->getData(), 'de');
+ }
+
+ // The source should be now available for en and de.
+ $this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
+ }
+ }
+
+ /**
+ * Test if the source is able to pull content in requested language.
+ */
+ function testRequestDataForSpecificLanguage() {
+ $this->setEnvironment('sk');
+ $this->setEnvironment('es');
+ $content_type = $this->drupalCreateContentType();
+
+ $node = $this->drupalCreateNode(array(
+ 'title' => $this->randomName(),
+ 'language' => 'sk',
+ 'body' => array('sk' => array(array())),
+ 'type' => $content_type->type,
+ ));
+
+ $this->drupalCreateNode(array(
+ 'title' => 'en translation',
+ 'language' => 'en',
+ 'tnid' => $node->nid,
+ 'body' => array('en' => array(array())),
+ 'type' => $content_type->type,
+ ));
+
+ // Create a translation job.
+ $job = $this->createJob('en', 'de');
+ $job->save();
+ $job->addItem('node', 'node', $node->nid);
+
+ $data = $job->getData();
+ $this->assertEqual($data[1]['node_title']['#text'], 'en translation');
+
+ // Create new job item with a source language for which the translation
+ // does not exit.
+ $job = $this->createJob('es', 'cs');
+ $job->save();
+ try {
+ $job->addItem('node', 'node', $node->nid);
+ $this->fail('The job item should not be added as there is no translation for language "es"');
+ }
+ catch (TMGMTException $e) {
+ $languages = language_list();
+ $this->assertEqual(t('Unable to load %language translation for the node %title',
+ array('%language' => $languages['es']->name, '%title' => $node->title)), $e->getMessage());
+ }
+ }
+
+ /**
+ * Compares the data from an entity with the translated data.
+ *
+ * @param $node
+ * The translated node object.
+ * @param $data
+ * An array with the translated data.
+ * @param $langcode
+ * The code of the target language.
+ */
+ function checkTranslatedData($node, $data, $langcode) {
+ foreach (element_children($data) as $field_name) {
+ if ($field_name == 'node_title') {
+ $this->assertEqual($node->title, $data['node_title']['#translation']['#text'], 'The title of the translated node matches the translated data.');
+ continue;
+ }
+ foreach (element_children($data[$field_name]) as $delta) {
+ $field_langcode = field_is_translatable('node', field_info_field($field_name)) ? $langcode : LANGUAGE_NONE;
+ foreach (element_children($data[$field_name][$delta]) as $column) {
+ $column_value = $data[$field_name][$delta][$column];
+ if (!isset($column_value['#translate']) || $column_value['#translate']) {
+ $this->assertEqual($node->{$field_name}[$field_langcode][$delta][$column], $column_value['#translation']['#text'], format_string('The translatable field %field:%delta has been populated with the proper translated data.', array(
+ '%field' => $field_name,
+ 'delta' => $delta
+ )));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.ui.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.ui.inc
new file mode 100644
index 00000000..cd87fe5e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/tmgmt_node.ui.inc
@@ -0,0 +1,35 @@
+ array(
+ 'type' => 'node',
+ 'label' => t('Request translations'),
+ 'configurable' => false,
+ 'aggregate' => true
+ )
+ );
+}
+
+/**
+ * Action to do multistep checkout for translations.
+ *
+ * @param array $nodes
+ * Array of Drupal nodes.
+ * @param $info
+ * Action info - not used.
+ *
+ */
+function tmgmt_node_ui_checkout_multiple_action($nodes, $info) {
+ $jobs = array();
+ $source_lang_registry = array();
+
+ // Loop through entities and create individual jobs for each source language.
+ foreach ($nodes as $node) {
+
+ try {
+
+ // For given source lang no job exists yet.
+ if (!isset($source_lang_registry[$node->language])) {
+ // Create new job.
+ $job = tmgmt_job_create($node->language, NULL, $GLOBALS['user']->uid);
+ // Add initial job item.
+ $job->addItem('node', 'node', $node->nid);
+ // Add job identifier into registry
+ $source_lang_registry[$node->language] = $job->tjid;
+ // Add newly created job into jobs queue.
+ $jobs[$job->tjid] = $job;
+ }
+ // We have a job for given source lang, so just add new job item for the
+ // existing job.
+ else {
+ $jobs[$source_lang_registry[$node->language]]->addItem('node', 'node', $node->nid);
+ }
+ }
+ catch (TMGMTException $e) {
+ watchdog_exception('tmgmt', $e);
+ drupal_set_message(t('Unable to add job item for node %name. Make sure the source content is not empty.', array('%name' => $node->title)), 'error');
+ }
+ }
+
+ // If necessary, do a redirect.
+ $redirects = tmgmt_ui_job_checkout_multiple($jobs);
+ if ($redirects) {
+ tmgmt_ui_redirect_queue_set($redirects, current_path());
+ drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
+ drupal_goto(tmgmt_ui_redirect_queue_dequeue());
+ }
+}
+
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.overview.test b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.overview.test
new file mode 100644
index 00000000..c5f22467
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.overview.test
@@ -0,0 +1,160 @@
+ 'Node Source UI Overview tests',
+ 'description' => 'Tests the user interface for node overviews.',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('rules'),
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_node_ui'));
+
+ $this->loginAsAdmin();
+
+ $this->setEnvironment('de');
+ $this->setEnvironment('fr');
+ $this->setEnvironment('es');
+ $this->setEnvironment('el');
+
+ $this->createNodeType('page', 'Page', TRANSLATION_ENABLED, FALSE);
+ // 1 means that the node type can have a language but is not translatable.
+ $this->createNodeType('untranslated', 'Untranslated', 1, FALSE);
+
+ $this->checkPermissions(array(), TRUE);
+
+ // Allow auto-accept.
+ $default_translator = tmgmt_translator_load('test_translator');
+ $default_translator->settings = array(
+ 'auto_accept' => TRUE,
+ );
+ $default_translator->save();
+ }
+
+ /**
+ * Tests translating through the content source overview.
+ */
+ function testNodeSourceOverview() {
+
+ // Login as translator to translate nodes.
+ $this->loginAsTranslator(array(
+ 'translate content',
+ 'edit any page content',
+ 'create page content',
+ ));
+
+ // Create a bunch of english nodes.
+ $node1 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+ $node2 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+ $node3 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+ $node4 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Create a node with an undefined language.
+ $node5 = $this->drupalCreateNode(array('type' => 'page'));
+
+ // Create a node of an untranslatable content type.
+ $node6 = $this->drupalCreateNode(array('type' => 'untranslated', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Go to the overview page and make sure the nodes are there.
+ $this->drupalGet('admin/tmgmt/sources/node');
+
+ // Make sure that valid nodes are shown.
+ $this->assertText($node1->title);
+ $this->assertText($node2->title);
+ $this->assertText($node3->title);
+ $this->assertText($node4->title);
+ // Nodes without a language must not be shown.
+ $this->assertNoText($node5->title);
+ // Node with a type that is not enabled for translation must not be shown.
+ $this->assertNoText($node6->title);
+
+ // Now translate them.
+ $edit = array(
+ 'views_bulk_operations[0]' => TRUE,
+ 'views_bulk_operations[1]' => TRUE,
+ 'views_bulk_operations[2]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translations'));
+
+ // Some assertions on the submit form.
+ $this->assertText(t('@title and 2 more (English to ?, Unprocessed)', array('@title' => $node1->title)));
+ $this->assertText($node1->title);
+ $this->assertText($node2->title);
+ $this->assertText($node3->title);
+ $this->assertNoText($node4->title);
+
+ // Translate
+ $edit = array(
+ 'target_language' => 'de',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node1->title, '@language' => t('German'))));
+ $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
+ $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node2->title, '@language' => t('German'))));
+ $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
+ $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node3->title, '@language' => t('German'))));
+ $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
+
+ // Check the translated node.
+ $this->clickLink($node1->title);
+ $this->clickLink(t('Translate'));
+ $this->assertText('de_' . $node1->title);
+
+ // Test for the source list limit set in the views export.
+ $view = views_get_view('tmgmt_node_source_overview');
+ $view->execute_display('default');
+ $this->assertEqual($view->get_items_per_page(), variable_get('tmgmt_source_list_limit', 20));
+
+ // Test the missing translation filter.
+
+ // Create nodes needed to test the missing translation filter here so that
+ // VBO order is not affected.
+ $node_not_translated = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+ $node_de = $this->drupalCreateNode(array('type' => 'page', 'language' => 'de', 'body' => array('de' => array(array()))));
+
+ $this->drupalGet('admin/tmgmt/sources/node');
+ $this->assertText($node1->title);
+ $this->assertText($node_not_translated->title);
+ $this->assertText($node_de->title);
+
+ // Submitting the search form will not work. After the form submission the
+ // page does gets redirected to url without query parameters. So we simply
+ // access the page with desired query.
+ $this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
+ 'tmgmt_node_missing_translation' => 'de',
+ 'target_status' => 'untranslated',
+ )));
+ $this->assertNoText($node1->title);
+ $this->assertText($node_not_translated->title);
+ $this->assertNoText($node_de->title);
+
+ // Update the the translate flag of the translated node and test if it is
+ // listed among sources with missing translation.
+ db_update('node')->fields(array('translate' => 1))
+ ->condition('nid', $node1->nid)->execute();
+
+ $this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
+ 'tmgmt_node_missing_translation' => 'de',
+ 'target_status' => 'outdated',
+ )));
+ $this->assertText($node1->title);
+ $this->assertNoText($node_not_translated->title);
+ $this->assertNoText($node_de->title);
+
+ $this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
+ 'tmgmt_node_missing_translation' => 'de',
+ 'target_status' => 'untranslated_or_outdated',
+ )));
+ $this->assertText($node1->title);
+ $this->assertText($node_not_translated->title);
+ $this->assertNoText($node_de->title);
+ }
+}
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.pages.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.pages.inc
new file mode 100644
index 00000000..684a4bf6
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.pages.inc
@@ -0,0 +1,116 @@
+nid);
+
+ // Inject our additional column into the header.
+ array_splice($original['#header'], -1, 0, array(t('Pending Translations')));
+ // Make this a tableselect form.
+ $form['languages'] = array(
+ '#type' => 'tableselect',
+ '#header' => $original['#header'],
+ '#options' => array(),
+ );
+ $languages = module_exists('i18n_node') ? i18n_node_language_list($node) : language_list();
+ // Check if there is a job / job item that references this translation.
+ $items = tmgmt_job_item_load_latest('node', 'node', $node->nid, $node->language);
+ foreach ($languages as $langcode => $language) {
+ if ($langcode == LANGUAGE_NONE) {
+ // Never show language neutral on the overview.
+ continue;
+ }
+ // Since the keys are numeric and in the same order we can shift one element
+ // after the other from the original non-form rows.
+ $option = array_shift($original['#rows']);
+ if ($langcode == $node->language) {
+ $additional = '' . t('Source') . '';
+ // This is the source object so we disable the checkbox for this row.
+ $form['languages'][$langcode] = array(
+ '#type' => 'checkbox',
+ '#disabled' => TRUE,
+ );
+ }
+ elseif (isset($items[$langcode])) {
+ /** @var TMGMTJobItem $item */
+ $item = $items[$langcode];
+ if ($item->getJob()->isUnprocessed()) {
+ $uri = $item->getJob()->uri();
+ $additional = l(t('Unprocessed'), $uri['path']);
+ }
+ else {
+ $wrapper = entity_metadata_wrapper('tmgmt_job_item', $item);
+ $uri = $item->uri();
+ $additional = l($wrapper->state->label(), $uri['path']);
+ }
+ // Disable the checkbox for this row since there is already a translation
+ // in progress that has not yet been finished. This way we make sure that
+ // we don't stack multiple active translations for the same item on top
+ // of each other.
+ $form['languages'][$langcode] = array(
+ '#type' => 'checkbox',
+ '#disabled' => TRUE,
+ );
+ }
+ else {
+ // There is no translation job / job item for this target language.
+ $additional = t('None');
+ }
+ // Inject the additional column into the array.
+ array_splice($option, -1, 0, array($additional));
+ // Append the current option array to the form.
+ $form['languages']['#options'][$langcode] = $option;
+ }
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['request'] = array(
+ '#type' => 'submit',
+ '#value' => t('Request translation'),
+ '#submit' => array('tmgmt_node_ui_translate_form_submit'),
+ '#validate' => array('tmgmt_node_ui_translate_form_validate'),
+ );
+ return $form;
+}
+
+/**
+ * Validation callback for the node translation overview form.
+ */
+function tmgmt_node_ui_translate_form_validate($form, &$form_state) {
+ $selected = array_filter($form_state['values']['languages']);
+ if (empty($selected)) {
+ form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
+ }
+}
+
+/**
+ * Submit callback for the node translation overview form.
+ */
+function tmgmt_node_ui_translate_form_submit($form, &$form_state) {
+ $node = $form_state['node'];
+ $values = $form_state['values'];
+ $jobs = array();
+ foreach (array_keys(array_filter($values['languages'])) as $langcode) {
+ // Create the job object.
+ $job = tmgmt_job_create($node->language, $langcode, $GLOBALS['user']->uid);
+ // Add the job item.
+ $job->addItem('node', 'node', $node->nid);
+ // Append this job to the array of created jobs so we can redirect the user
+ // to a multistep checkout form if necessary.
+ $jobs[$job->tjid] = $job;
+ }
+ tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.rules_defaults.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.rules_defaults.inc
new file mode 100644
index 00000000..578355a9
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.rules_defaults.inc
@@ -0,0 +1,49 @@
+name] = $rule;
+ return $configs;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.source_overview.css b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.source_overview.css
new file mode 100644
index 00000000..69bddb98
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.source_overview.css
@@ -0,0 +1,15 @@
+#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status,
+#edit-tmgmt-node-missing-translation-wrapper .form-item-tmgmt-node-missing-translation {
+ float: left;
+}
+
+#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status {
+ margin: 0 0 0 55px;
+ padding: 0;
+ position: relative;
+ top: -19px;
+}
+
+#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status select {
+ margin-top: 9px;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.test b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.test
new file mode 100644
index 00000000..233643d9
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/ui/tmgmt_node_ui.test
@@ -0,0 +1,450 @@
+ 'Node Source UI tests',
+ 'description' => 'Tests the user interface for node translation sources.',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_node_ui', 'block'));
+
+ // We need the administer blocks permission.
+ $this->loginAsAdmin(array('administer blocks'));
+
+ $this->setEnvironment('de');
+ $this->setEnvironment('fr');
+ $this->setEnvironment('es');
+ $this->setEnvironment('el');
+
+ // @todo Re-enable this when switching to testing profile.
+ // Enable the main page content block for hook_page_alter() to work.
+ $edit = array(
+ 'blocks[system_main][region]' => 'content',
+ );
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+
+ $this->createNodeType('page', 'Page', TRANSLATION_ENABLED, FALSE);
+ }
+
+ /**
+ * Tests the create, submit and accept permissions.
+ */
+ function testPermissions() {
+
+ $no_permissions = $this->drupalCreateUser();
+ $this->drupalLogin($no_permissions);
+ $this->drupalGet('admin/tmgmt');
+ $this->assertResponse(403);
+
+ // Test with a user that is only allowed to create jobs.
+ $create_user = $this->drupalCreateUser(array('access administration pages', 'translate content', 'create translation jobs'));
+ $this->drupalLogin($create_user);
+
+ // Create an english source node.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+ $this->assertText(t('One job has been created.'));
+ // Verify that we are still on the translate tab.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+
+ // The job is unprocessed, check the status flag in the source list.
+ $this->drupalGet('admin/tmgmt/sources');
+ $links = $this->xpath('//a[contains(@title, :title)]', array(':title' => t('Active job item: @state', array('@state' => t('Unprocessed')))));
+ $attributes = $links[0]->attributes();
+ // Check if the found link points to the job checkout page instead of the
+ // job item review form.
+ $this->assertEqual($attributes['href'], url('admin/tmgmt/jobs/1', array('query' => array('destination' => 'admin/tmgmt/sources'))));
+
+ $this->drupalGet('admin/tmgmt');
+ $this->assertResponse(200);
+ $this->assertLink(t('manage'));
+ $this->assertNoLink(t('submit'));
+ $this->assertNoLink(t('delete'));
+ $this->assertText(t('@title', array('@title' => $node->title)));
+ $this->clickLink(t('manage'));
+ $this->assertResponse(200);
+ $this->assertNoRaw(t('Submit to translator'));
+
+ // Try to access the delete page directly.
+ $this->drupalGet($this->getUrl() . '/delete');
+ $this->assertResponse(403);
+
+ // Log in as user with only submit permission.
+ $submit_user = $this->drupalCreateUser(array('access administration pages', 'translate content', 'submit translation jobs'));
+ $this->drupalLogin($submit_user);
+
+ // Go to the translate tab, verify that there is no request translation
+ // button.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+ $this->assertNoRaw(t('Request translation'));
+
+ // Go to the overview and submit the job.
+ $this->drupalGet('admin/tmgmt');
+ $this->assertResponse(200);
+ $this->assertLink(t('submit'));
+ $this->assertNoLink(t('manage'));
+ $this->assertNoLink(t('delete'));
+ $this->assertText(t('@title', array('@title' => $node->title)));
+
+ // Check VBO actions - "submit translation job" has the right to cancel
+ // translation only.
+ $element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
+ $options = array();
+ foreach ($element as $option) {
+ $options[] = (string) $option;
+ }
+ $this->assertTrue(in_array('rules_component::rules_tmgmt_job_abort_translation', $options));
+
+ // Go to the job checkout page and submit it.
+ $this->clickLink('submit');
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+ // After submit the redirect goes back to the job overview.
+ $this->assertUrl('admin/tmgmt');
+
+ // Make sure that the job is active now.
+ $this->assertText(t('Active'));
+ // Click abort link and check if we are at the job abort confirm page.
+ $this->clickLink(t('abort'));
+ $this->assertText(t('This will send a request to the translator to abort the job. After the action the job translation process will be aborted and only remaining action will be resubmitting it.'));
+ // Return back to job overview and test the manage link.
+ $this->drupalGet('admin/tmgmt');
+ $this->clickLink(t('manage'));
+ $this->assertText(t('Needs review'));
+ $this->assertNoLink(t('review'));
+
+ // Now log in as user with only accept permission and review the job.
+ $accept_user = $this->drupalCreateUser(array('access administration pages', 'accept translation jobs'));
+ $this->drupalLogin($accept_user);
+
+ $this->drupalGet('admin/tmgmt');
+
+ // Check VBO actions - "accept translation jobs" has the right to accept
+ // translation only.
+ $element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
+ $options = array();
+ foreach ($element as $option) {
+ $options[] = (string) $option;
+ }
+ $this->assertTrue(in_array('rules_component::rules_tmgmt_job_accept_translation', $options));
+
+ $this->clickLink('manage');
+ $this->clickLink('review');
+
+ $this->drupalPost(NULL, array(), '✓');
+ // Verify that the accepted character is shown.
+ $this->assertText('☑');
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+ $this->assertText(t('Accepted'));
+ $this->assertText('1/0/0');
+
+ $create_user = $this->loginAsAdmin();
+ $this->drupalLogin($create_user);
+
+ $this->drupalGet('admin/tmgmt');
+ // Check VBO actions - "administer tmgmt" has rights for all actions.
+ $element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
+ $options = array();
+ foreach ($element as $option) {
+ $options[] = (string) $option;
+ }
+ $this->assertTrue(in_array('rules_component::rules_tmgmt_job_accept_translation', $options));
+ $this->assertTrue(in_array('rules_component::rules_tmgmt_job_abort_translation', $options));
+ $this->assertTrue(in_array('rules_component::rules_tmgmt_job_delete', $options));
+
+ // Go to the translate tab, verify that there is no request translation
+ // button.
+ //$this->drupalGet('node/' . $node->nid);
+ //$this->clickLink('Translate');
+ //$this->assertNoRaw(t('Request translation'));
+ }
+
+ /**
+ * Test the translate tab for a single checkout.
+ */
+ function testTranslateTabSingleCheckout() {
+
+ // Create a user that is allowed to translate nodes.
+ $translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
+ $this->drupalLogin($translater);
+
+ // Create an english source node.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertText(t('One job needs to be checked out.'));
+ $this->assertText($node->title);
+
+ // Go to the translate tab and check if the pending translation label is
+ // "Unprocessed" and links to the job checkout page.
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $this->assertLink(t('Unprocessed'));
+ $this->clickLink(t('Unprocessed'));
+
+ // Submit.
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
+
+ // Review.
+ $this->clickLink(t('Needs review'));
+
+ // @todo Review job throuh the UI.
+ $items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
+ $items['de']->acceptTranslation();
+
+ // German node should now be listed and be clickable.
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $this->clickLink('de_' . $node->title);
+
+ // Test that the destination query argument does not break the redirect
+ // and we are redirected back to the correct page.
+ $this->drupalGet('node/' . $node->nid . '/translate', array('query' => array('destination' => 'node')));
+
+ // Request a spanish translation.
+ $edit = array(
+ 'languages[es]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the checkout page.
+ $this->assertText(t('One job needs to be checked out.'));
+ $this->assertText($node->title);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the originally defined destination URL.
+ $this->assertEqual(url('node', array('absolute' => TRUE)), $this->getUrl());
+ }
+
+ /**
+ * Test the translate tab for a single checkout.
+ */
+ function testTranslateTabMultipeCheckout() {
+ // Create a user that is allowed to translate nodes.
+ $translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
+ $this->drupalLogin($translater);
+
+ // Create an english source node.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ 'languages[es]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertText(t('2 jobs need to be checked out.'));
+
+ // Submit all jobs.
+ $this->assertText($node->title);
+ $this->drupalPost(NULL, array(), t('Submit to translator and continue'));
+ $this->assertText($node->title);
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
+
+ // Review.
+ $this->clickLink(t('Needs review'));
+
+ // @todo Review job throuh the UI.
+ $items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
+ $items['de']->acceptTranslation();
+ $items['es']->acceptTranslation();
+
+ // Translated nodes should now be listed and be clickable.
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $this->clickLink('de_' . $node->title);
+
+ // Translated nodes should now be listed and be clickable.
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $this->clickLink('es_' . $node->title);
+ }
+
+ /**
+ * Test the translate tab for a single checkout.
+ */
+ function testTranslateTabAutomatedCheckout() {
+ // Hide settings on the test translator.
+ $default_translator = tmgmt_translator_load('test_translator');
+ $default_translator->settings = array(
+ 'expose_settings' => FALSE,
+ );
+ $default_translator->save();
+
+ // Create a user that is allowed to translate nodes.
+ $translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
+ $this->drupalLogin($translater);
+
+ // Create an english source node.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertNoText(t('One job needs to be checked out.'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
+
+ // Review.
+ $this->clickLink(t('Needs review'));
+
+ // @todo Review job throuh the UI.
+ $items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
+ $items['de']->acceptTranslation();
+
+ // German node should now be listed and be clickable.
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $this->clickLink('de_' . $node->title);
+ }
+
+ /**
+ * Test the translate tab for a single checkout.
+ */
+ function testTranslateTabDisabledQuickCheckout() {
+ variable_set('tmgmt_quick_checkout', FALSE);
+
+ // Hide settings on the test translator.
+ $default_translator = tmgmt_translator_load('test_translator');
+ $default_translator->settings = array(
+ 'expose_settings' => FALSE,
+ );
+ $default_translator->save();
+
+ // Create a user that is allowed to translate nodes.
+ $translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
+ $this->drupalLogin($translater);
+
+ // Create an english source node.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+ // Go to the translate tab.
+ $this->drupalGet('node/' . $node->nid);
+ $this->clickLink('Translate');
+
+ // Assert some basic strings on that page.
+ $this->assertText(t('Translations of @title', array('@title' => $node->title)));
+ $this->assertText(t('Pending Translations'));
+
+ // Request a translation for german.
+ $edit = array(
+ 'languages[de]' => TRUE,
+ );
+ $this->drupalPost(NULL, $edit, t('Request translation'));
+
+ // Verify that we are on the translate tab.
+ $this->assertText(t('One job needs to be checked out.'));
+ $this->assertText($node->title);
+
+ // Submit.
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // Make sure that we're back on the translate tab.
+ $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
+ $this->assertText(t('Test translation created.'));
+ $this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
+
+ // Review.
+ $this->clickLink(t('Needs review'));
+
+ // @todo Review job throuh the UI.
+ $items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
+ $items['de']->acceptTranslation();
+
+ // German node should now be listed and be clickable.
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $this->clickLink('de_' . $node->title);
+ }
+
+ /**
+ * Test the node source specific cart functionality.
+ */
+ function testCart() {
+ $nodes = array();
+ for ($i = 0; $i < 4; $i++) {
+ $nodes[] = $this->createNode('page');
+ }
+
+ $this->loginAsAdmin(array_merge($this->translator_permissions, array('translate content')));
+
+ // Test the source overview.
+ $this->drupalPost('admin/tmgmt/sources/node', array(
+ 'views_bulk_operations[0]' => TRUE,
+ 'views_bulk_operations[1]' => TRUE,
+ ), t('Add to cart'));
+ $this->drupalGet('admin/tmgmt/cart');
+ $this->assertText($nodes[0]->title);
+ $this->assertText($nodes[1]->title);
+
+ // Test the translate tab.
+ $this->drupalGet('node/' . $nodes[3]->nid . '/translate');
+ $this->assertRaw(t('There are @count items in the translation cart.',
+ array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
+
+ $this->drupalPost(NULL, array(), t('Add to cart'));
+ $this->assertRaw(t('@count content source was added into the cart.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
+ $this->assertRaw(t('There are @count items in the translation cart including the current item.',
+ array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_jobs.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_jobs.inc
new file mode 100644
index 00000000..9dd16f28
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_jobs.inc
@@ -0,0 +1,26 @@
+get_value($row);
+ $nids[] = $nid;
+ }
+
+ $select = db_select('tmgmt_job', 'tj');
+ $select->join('tmgmt_job_item', 'tji', "tj.id = %alias.tjid");
+ $select->join('node', 'n', "tji.item_type = 'node' AND tji.plugin = 'node' AND tji.item_id = node.nid");
+ $select->addField('n', 'nid');
+ $select->addExpression('MAX(tj.id)');
+
+ dpq($select);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_translation_language_status.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_translation_language_status.inc
new file mode 100644
index 00000000..9ddb4a62
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_translation_language_status.inc
@@ -0,0 +1,80 @@
+view->init_style();
+ $this->additional_fields['nid'] = 'nid';
+
+ /**
+ * Dynamically add new fields so they are used
+ */
+ $languages = language_list('language');
+ foreach ($languages as $langcode => $lang_info) {
+ $handler = views_get_handler($this->table, $this->field . '_single', 'field');
+ if ($handler) {
+ $id = $options['id'] . '_single_' . $langcode;
+ $this->view->display_handler->handlers['field'][$id] = $handler;
+ $info = array(
+ 'id' => $id,
+ 'table' => $this->table,
+ 'field' => $this->field . '_single',
+ 'label' => $lang_info->name,
+ );
+ $handler->langcode = $langcode;
+ $handler->main_field = $options['id'];
+ $handler->init($this->view, $info);
+ $this->language_handlers[$langcode] = $handler;
+ }
+ }
+ }
+
+ function pre_render(&$values) {
+ $nids = array();
+ foreach ($values as $value) {
+ $tnid = $this->get_value($value);
+ $tnid = !empty($tnid) ? $tnid : $this->get_value($value, 'nid');
+ $this->active_job_items[$tnid] = tmgmt_job_item_load_latest('node', 'node', $tnid, $value->node_language);
+ $nids[] = $tnid;
+ }
+ if ($nodes = node_load_multiple($nids)) {
+ $result = db_select('node', 'n')
+ ->fields('n', array('tnid', 'language', 'translate'))
+ ->condition('tnid', $nids)
+ ->execute()
+ ->fetchAll();
+
+ $this->language_items = array();
+ foreach ($result as $tnode) {
+ // The translate flag is set if the translation node is outdated, revert
+ // to have FALSE for outdated translations.
+ $this->language_items[$tnode->tnid][$tnode->language] = !$tnode->translate;
+ }
+
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_translation_language_status_single.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_translation_language_status_single.inc
new file mode 100644
index 00000000..be42b931
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_field_translation_language_status_single.inc
@@ -0,0 +1,62 @@
+additional_fields['nid'] = array(
+ 'table' => 'node',
+ 'field' => 'nid',
+ );
+ }
+
+ function render($values) {
+ $nid = $values->nid;
+ $langcode = $this->langcode;
+
+ // Check if this is the source language.
+ if ($langcode == $values->node_language) {
+ $translation_status = 'original';
+ }
+ // Check if there is a translation.
+ elseif (!isset($this->view->field[$this->main_field]->language_items[$nid][$langcode])) {
+ $translation_status = 'missing';
+ }
+ // Check if the translation is outdated.
+ elseif (!$this->view->field[$this->main_field]->language_items[$nid][$langcode]) {
+ $translation_status = 'outofdate';
+ }
+ else {
+ $translation_status = 'current';
+ }
+
+ $job_item = NULL;
+
+ if (isset($this->view->field[$this->main_field]->active_job_items[$nid][$langcode])) {
+ $job_item = $this->view->field[$this->main_field]->active_job_items[$nid][$langcode];
+ }
+
+ return theme('tmgmt_ui_translation_language_status_single', array(
+ 'translation_status' => $translation_status,
+ 'job_item' => $job_item,
+ ));
+ }
+
+ function query() {
+ $this->add_additional_fields();
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_filter_missing_translation.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_filter_missing_translation.inc
new file mode 100644
index 00000000..b9f7a4bd
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_filter_missing_translation.inc
@@ -0,0 +1,108 @@
+ensure_my_table();
+
+ // Don't do anything if no language was selected.
+ if (!$this->value) {
+ return;
+ }
+
+ $join = new views_join();
+ $join->definition['left_table'] = $this->table_alias;
+ $join->definition['left_field'] = $this->real_field;
+ $join->definition['table'] = 'node';
+ $join->definition['field'] = 'tnid';
+ $join->definition['type'] = 'LEFT';
+ $join->construct();
+
+ $join->extra = array(array(
+ 'field' => 'language',
+ 'value' => $this->value,
+ ));
+
+ $table_alias = $this->query->add_table('node', $this->relationship, $join);
+
+ $this->query->add_where_expression($this->options['group'], "{$this->table_alias}.language != :language", array(':language' => $this->value));
+
+ if ($this->target_status == 'untranslated_or_outdated') {
+ $this->query->add_where_expression($this->options['group'], "($table_alias.nid IS NULL OR {$this->table_alias}.translate = 1)");
+ }
+ elseif ($this->target_status == 'outdated') {
+ $this->query->add_where_expression($this->options['group'], "{$this->table_alias}.translate = 1");
+ }
+ elseif ($this->target_status == 'untranslated') {
+ $this->query->add_where_expression($this->options['group'], "$table_alias.nid IS NULL");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function value_form(&$form, &$form_state) {
+ $options = array();
+ foreach (language_list() as $langcode => $language) {
+ $options[$langcode] = $language->name;
+ }
+
+ $identifier = $this->options['expose']['identifier'];
+
+ $form['value'][$identifier] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#empty_option' => t('Any'),
+ '#id' => 'tmgmt_node_missing_target_language',
+ '#element_validate' => array('tmgmt_node_views_exposed_target_language_validate'),
+ );
+ // Attach css to style the target_status element inline.
+ $form['#attached']['css'][] = drupal_get_path('module', 'tmgmt_node_ui') . '/tmgmt_node_ui.source_overview.css';
+ $form['value']['target_status'] = array(
+ '#type' => 'select',
+ '#title' => t('Target status'),
+ '#options' => array(
+ 'untranslated_or_outdated' => t('Untranslated or outdated'),
+ 'untranslated' => t('Untranslated'),
+ 'outdated' => t('Outdated'),
+ ),
+ '#states' => array(
+ 'invisible' => array(
+ ':input[id="tmgmt_node_missing_target_language"]' => array('value' => ''),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function accept_exposed_input($input) {
+ $return = parent::accept_exposed_input($input);
+ if ($return && isset($input['target_status'])) {
+ $this->target_status = $input['target_status'];
+ }
+ return $return;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_filter_node_translatable_types.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_filter_node_translatable_types.inc
new file mode 100644
index 00000000..fe08b665
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/handlers/tmgmt_node_handler_filter_node_translatable_types.inc
@@ -0,0 +1,32 @@
+ensure_my_table();
+ $valid_types = array_keys(tmgmt_source_translatable_item_types('node'));
+ if ($valid_types) {
+ $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", array_values($valid_types), 'IN');
+ }
+ else {
+ // There are no valid translatable node types, do not return any results.
+ $this->query->add_where_expression($this->options['group'], '1 = 0');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function admin_summary() { }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/tmgmt_node.views.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/tmgmt_node.views.inc
new file mode 100644
index 00000000..bb145c24
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/tmgmt_node.views.inc
@@ -0,0 +1,140 @@
+ t('Content'),
+ 'help' => t('Content that is associated with this job item.'),
+ 'real field' => 'item_id',
+ 'relationship' => array(
+ 'label' => t('Content'),
+ 'base' => 'node',
+ 'base field' => 'vid',
+ 'relationship field' => 'item_id',
+ 'extra' => array(
+ array(
+ 'table' => 'tmgmt_job_item',
+ 'field' => 'item_type',
+ 'operator' => '=',
+ 'value' => 'node',
+ ),
+ array(
+ 'table' => 'tmgmt_job_item',
+ 'field' => 'plugin',
+ 'operator' => '=',
+ 'value' => 'node',
+ ),
+ ),
+ ),
+ );
+ $data['node']['node_to_job_item'] = array(
+ 'title' => t('Translation job item'),
+ 'help' => t('Job items of this node.'),
+ 'relationship' => array(
+ 'real field' => 'vid',
+ 'label' => t('Translation job item'),
+ 'base' => 'tmgmt_job_item',
+ 'base field' => 'item_id',
+ 'extra' => array(
+ array(
+ 'field' => 'item_type',
+ 'operator' => '=',
+ 'value' => 'node',
+ ),
+ array(
+ 'field' => 'plugin',
+ 'operator' => '=',
+ 'value' => 'node',
+ ),
+ ),
+ ),
+ );
+ $data['node']['tmgmt_translatable_types_all'] = array(
+ 'group' => t('Content translation'),
+ 'title' => t('All translatable types'),
+ 'help' => t('Enforces that only nodes from node types which are translatable are '),
+ 'filter' => array(
+ 'handler' => 'tmgmt_node_ui_handler_filter_node_translatable_types',
+ 'real field' => 'type',
+ ),
+ );
+ $data['node']['tmgmt_node_missing_translation'] = array(
+ 'group' => t('Content translation'),
+ 'title' => t('Missing translation'),
+ 'help' => t('Enables the search for nodes with missing translation ofr the specified language'),
+ 'filter' => array(
+ 'handler' => 'tmgmt_node_handler_filter_missing_translation',
+ 'real field' => 'nid',
+ ),
+ );
+ $data['node']['tmgmt_jobs'] = array(
+ 'title' => t('Translation jobs'),
+ 'help' => t('Shows all translation jobs which contains this node'),
+ 'field' => array(
+ 'handler' => 'tmgmt_node_ui_handler_field_jobs',
+ 'real field' => 'nid',
+ ),
+ );
+ $data['node']['tmgmt_job_item'] = array(
+ 'title' => t('Job item'),
+ 'real field' => 'vid',
+ 'relationship' => array(
+ 'title' => t('Translation job item'),
+ 'label' => t('Translation job item'),
+ 'base' => 'tmgmt_job_item',
+ 'base field' => 'item_id',
+ 'extra' => array(
+ array(
+ 'field' => 'item_type',
+ 'operator' => '=',
+ 'value' => 'node',
+ ),
+ array(
+ 'field' => 'plugin',
+ 'operator' => '=',
+ 'value' => 'node',
+ ),
+ ),
+ ),
+ );
+ $data['node']['translation_language_status'] = array(
+ 'group' => t('Content translation'),
+ 'title' => t('All translation languages'),
+ 'help' => t('Display all target lanuages.'),
+ 'real field' => 'tnid',
+ 'field' => array(
+ 'handler' => 'tmgmt_node_handler_field_translation_language_status',
+ ),
+ );
+ $data['node']['translation_language_status_single'] = array(
+ 'title' => t('All translation languages (single)'),
+ 'help' => t("Don't use this in the user interface."),
+ 'field' => array(
+ 'handler' => 'tmgmt_node_handler_field_translation_language_status_single',
+ ),
+ );
+ $data['node']['tmgmt_translatable_types_select'] = array(
+ 'group' => t('Content translation'),
+ 'title' => t('Select translatable content types'),
+ 'help' => t('Allows to filter on specific translatable types.'),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_in_operator',
+ 'real field' => 'type',
+ 'options callback' => 'tmgmt_source_translatable_item_types',
+ 'options arguments' => array($this->pluginType),
+ ),
+ );
+ return $data;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/tmgmt_node_source_overview.view.inc b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/tmgmt_node_source_overview.view.inc
new file mode 100644
index 00000000..c5b6669e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/sources/node/views/tmgmt_node_source_overview.view.inc
@@ -0,0 +1,340 @@
+name = 'tmgmt_node_source_overview';
+$view->description = 'Node source overview for bulk operations.';
+$view->tag = 'Translation Management';
+$view->base_table = 'node';
+$view->human_name = 'Node Source Overview';
+$view->core = 7;
+$view->api_version = '3.0';
+$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+/* Display: Master */
+$handler = $view->new_display('default', 'Master', 'default');
+$handler->display->display_options['title'] = 'Content overview';
+$handler->display->display_options['use_more_always'] = FALSE;
+$handler->display->display_options['group_by'] = TRUE;
+$handler->display->display_options['access']['type'] = 'perm';
+$handler->display->display_options['access']['perm'] = 'create translation jobs';
+$handler->display->display_options['cache']['type'] = 'none';
+$handler->display->display_options['query']['type'] = 'views_query';
+$handler->display->display_options['query']['options']['query_comment'] = FALSE;
+$handler->display->display_options['exposed_form']['type'] = 'basic';
+$handler->display->display_options['exposed_form']['options']['submit_button'] = 'Search';
+$handler->display->display_options['pager']['type'] = 'full';
+$handler->display->display_options['pager']['options']['items_per_page'] = variable_get('tmgmt_source_list_limit', 20);
+$handler->display->display_options['pager']['options']['offset'] = '0';
+$handler->display->display_options['pager']['options']['id'] = '0';
+$handler->display->display_options['pager']['options']['quantity'] = '9';
+$handler->display->display_options['style_plugin'] = 'table';
+$handler->display->display_options['style_options']['columns'] = array(
+ 'title' => 'title',
+);
+$handler->display->display_options['style_options']['default'] = '-1';
+$handler->display->display_options['style_options']['info'] = array(
+ 'title' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+);
+/* No results behavior: Global: Text area */
+$handler->display->display_options['empty']['area']['id'] = 'area';
+$handler->display->display_options['empty']['area']['table'] = 'views';
+$handler->display->display_options['empty']['area']['field'] = 'area';
+$handler->display->display_options['empty']['area']['content'] = 'There are no nodes that match the specified filter criteria.';
+$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+/* Relationship: User */
+$handler->display->display_options['relationships']['uid']['id'] = 'uid';
+$handler->display->display_options['relationships']['uid']['table'] = 'node';
+$handler->display->display_options['relationships']['uid']['field'] = 'uid';
+$handler->display->display_options['relationships']['uid']['ui_name'] = 'User';
+$handler->display->display_options['relationships']['uid']['label'] = 'User';
+/* Field: Bulk operations */
+$handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations';
+$handler->display->display_options['fields']['views_bulk_operations']['table'] = 'node';
+$handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations';
+$handler->display->display_options['fields']['views_bulk_operations']['ui_name'] = 'Bulk operations';
+$handler->display->display_options['fields']['views_bulk_operations']['label'] = '';
+$handler->display->display_options['fields']['views_bulk_operations']['element_label_colon'] = FALSE;
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '1';
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1;
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0;
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10';
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array(
+ 'rules_component::tmgmt_node_ui_tmgmt_nodes_add_items_to_cart' => array(
+ 'selected' => 1,
+ 'skip_confirmation' => 1,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_assign_owner_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::views_bulk_operations_delete_item' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::views_bulk_operations_script_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_make_sticky_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_make_unsticky_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::views_bulk_operations_modify_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ 'settings' => array(
+ 'show_all_tokens' => 1,
+ 'display_values' => array(
+ '_all_' => '_all_',
+ ),
+ ),
+ ),
+ 'action::views_bulk_operations_argument_selector_action' => array(
+ 'selected' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ 'settings' => array(
+ 'url' => '',
+ ),
+ ),
+ 'action::node_promote_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_publish_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_unpromote_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'rules_component::tmgmt_node_ui_request_translation' => array(
+ 'selected' => 0,
+ 'skip_confirmation' => 1,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::tmgmt_node_ui_checkout_multiple_action' => array(
+ 'selected' => 1,
+ 'skip_confirmation' => 1,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_save_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::system_send_email_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_unpublish_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::node_unpublish_by_keyword_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+);
+/* Field: Title */
+$handler->display->display_options['fields']['title']['id'] = 'title';
+$handler->display->display_options['fields']['title']['table'] = 'node';
+$handler->display->display_options['fields']['title']['field'] = 'title';
+$handler->display->display_options['fields']['title']['ui_name'] = 'Title';
+$handler->display->display_options['fields']['title']['label'] = 'Title (in source language)';
+$handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE;
+$handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE;
+/* Field: Type */
+$handler->display->display_options['fields']['type']['id'] = 'type';
+$handler->display->display_options['fields']['type']['table'] = 'node';
+$handler->display->display_options['fields']['type']['field'] = 'type';
+$handler->display->display_options['fields']['type']['ui_name'] = 'Type';
+/* Field: All translation languages */
+$handler->display->display_options['fields']['translation_language_status_1']['id'] = 'translation_language_status_1';
+$handler->display->display_options['fields']['translation_language_status_1']['table'] = 'node';
+$handler->display->display_options['fields']['translation_language_status_1']['field'] = 'translation_language_status';
+$handler->display->display_options['fields']['translation_language_status_1']['ui_name'] = 'All translation languages';
+$handler->display->display_options['fields']['translation_language_status_1']['exclude'] = TRUE;
+/* Field: Author */
+$handler->display->display_options['fields']['name']['id'] = 'name';
+$handler->display->display_options['fields']['name']['table'] = 'users';
+$handler->display->display_options['fields']['name']['field'] = 'name';
+$handler->display->display_options['fields']['name']['relationship'] = 'uid';
+$handler->display->display_options['fields']['name']['ui_name'] = 'Author';
+$handler->display->display_options['fields']['name']['label'] = 'Author';
+/* Field: Updated date */
+$handler->display->display_options['fields']['changed']['id'] = 'changed';
+$handler->display->display_options['fields']['changed']['table'] = 'node';
+$handler->display->display_options['fields']['changed']['field'] = 'changed';
+$handler->display->display_options['fields']['changed']['ui_name'] = 'Updated date';
+$handler->display->display_options['fields']['changed']['date_format'] = 'short';
+/* Sort criterion: Post date */
+$handler->display->display_options['sorts']['created']['id'] = 'created';
+$handler->display->display_options['sorts']['created']['table'] = 'node';
+$handler->display->display_options['sorts']['created']['field'] = 'created';
+$handler->display->display_options['sorts']['created']['ui_name'] = 'Post date';
+$handler->display->display_options['sorts']['created']['order'] = 'DESC';
+/* Filter criterion: Content: Title */
+$handler->display->display_options['filters']['title']['id'] = 'title';
+$handler->display->display_options['filters']['title']['table'] = 'node';
+$handler->display->display_options['filters']['title']['field'] = 'title';
+$handler->display->display_options['filters']['title']['operator'] = 'word';
+$handler->display->display_options['filters']['title']['group'] = 1;
+$handler->display->display_options['filters']['title']['exposed'] = TRUE;
+$handler->display->display_options['filters']['title']['expose']['operator_id'] = 'title_op';
+$handler->display->display_options['filters']['title']['expose']['label'] = 'Node title';
+$handler->display->display_options['filters']['title']['expose']['operator'] = 'title_op';
+$handler->display->display_options['filters']['title']['expose']['identifier'] = 'title';
+/* Filter criterion: Published */
+$handler->display->display_options['filters']['status']['id'] = 'status';
+$handler->display->display_options['filters']['status']['table'] = 'node';
+$handler->display->display_options['filters']['status']['field'] = 'status';
+$handler->display->display_options['filters']['status']['ui_name'] = 'Published';
+$handler->display->display_options['filters']['status']['value'] = '1';
+$handler->display->display_options['filters']['status']['group'] = 1;
+$handler->display->display_options['filters']['status']['exposed'] = TRUE;
+$handler->display->display_options['filters']['status']['expose']['operator_id'] = '';
+$handler->display->display_options['filters']['status']['expose']['label'] = 'Published';
+$handler->display->display_options['filters']['status']['expose']['operator'] = 'status_op';
+$handler->display->display_options['filters']['status']['expose']['identifier'] = 'status';
+$handler->display->display_options['filters']['status']['expose']['required'] = TRUE;
+/* Filter criterion: Source translation */
+$handler->display->display_options['filters']['source_translation']['id'] = 'source_translation';
+$handler->display->display_options['filters']['source_translation']['table'] = 'node';
+$handler->display->display_options['filters']['source_translation']['field'] = 'source_translation';
+$handler->display->display_options['filters']['source_translation']['ui_name'] = 'Source translation';
+$handler->display->display_options['filters']['source_translation']['operator'] = '1';
+$handler->display->display_options['filters']['source_translation']['group'] = 1;
+/* Filter criterion: Content: Language */
+$handler->display->display_options['filters']['language']['id'] = 'language';
+$handler->display->display_options['filters']['language']['table'] = 'node';
+$handler->display->display_options['filters']['language']['field'] = 'language';
+$handler->display->display_options['filters']['language']['operator'] = 'not in';
+$handler->display->display_options['filters']['language']['value'] = array(
+ 'und' => 'und',
+);
+$handler->display->display_options['filters']['language']['group'] = 1;
+/* Filter criterion: Content: Language */
+$handler->display->display_options['filters']['language_1']['id'] = 'language_1';
+$handler->display->display_options['filters']['language_1']['table'] = 'node';
+$handler->display->display_options['filters']['language_1']['field'] = 'language';
+$handler->display->display_options['filters']['language_1']['group'] = 1;
+$handler->display->display_options['filters']['language_1']['exposed'] = TRUE;
+$handler->display->display_options['filters']['language_1']['expose']['operator_id'] = 'language_1_op';
+$handler->display->display_options['filters']['language_1']['expose']['label'] = 'Source language';
+$handler->display->display_options['filters']['language_1']['expose']['operator'] = 'language_1_op';
+$handler->display->display_options['filters']['language_1']['expose']['identifier'] = 'language_1';
+/* Filter criterion: Content translation: Select translatable content types */
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['id'] = 'tmgmt_translatable_types_select';
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['table'] = 'node';
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['field'] = 'tmgmt_translatable_types_select';
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['exposed'] = TRUE;
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['operator_id'] = 'tmgmt_translatable_types_select_op';
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['label'] = 'Content type';
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['operator'] = 'tmgmt_translatable_types_select_op';
+$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['identifier'] = 'tmgmt_translatable_types_select';
+/* Filter criterion: Content translation: All translatable types */
+$handler->display->display_options['filters']['tmgmt_translatable_types_all']['id'] = 'tmgmt_translatable_types_all';
+$handler->display->display_options['filters']['tmgmt_translatable_types_all']['table'] = 'node';
+$handler->display->display_options['filters']['tmgmt_translatable_types_all']['field'] = 'tmgmt_translatable_types_all';
+/* Filter criterion: Content translation: Missing translation */
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['id'] = 'tmgmt_node_missing_translation';
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['table'] = 'node';
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['field'] = 'tmgmt_node_missing_translation';
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['exposed'] = TRUE;
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['operator_id'] = 'tmgmt_node_missing_translation_op';
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['label'] = 'Target language';
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['operator'] = 'tmgmt_node_missing_translation_op';
+$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['identifier'] = 'tmgmt_node_missing_translation';
+
+/* Display: Page */
+$handler = $view->new_display('page', 'Page', 'page');
+$handler->display->display_options['path'] = 'admin/tmgmt/sources/node';
+$handler->display->display_options['menu']['type'] = 'tab';
+$handler->display->display_options['menu']['title'] = 'Content';
+$handler->display->display_options['menu']['weight'] = -20;
+$handler->display->display_options['menu']['context'] = 0;
+$translatables['tmgmt_node_source_overview'] = array(
+ t('Master'),
+ t('Content overview'),
+ t('more'),
+ t('Search'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('There are no nodes that match the specified filter criteria.'),
+ t('User'),
+ t(''),
+ t('Title (in source language)'),
+ t('Type'),
+ t('All translation languages'),
+ t('Author'),
+ t('Updated date'),
+ t('Node title'),
+ t('Published'),
+ t('Source language'),
+ t('Content type'),
+ t('Page'),
+);
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/testing_html/sample.html b/sites/all/modules/contrib/localisation/tmgmt/tests/testing_html/sample.html
new file mode 100644
index 00000000..9fadd133
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/testing_html/sample.html
@@ -0,0 +1,6 @@
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.base.entity.test b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.base.entity.test
new file mode 100644
index 00000000..fcf8001b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.base.entity.test
@@ -0,0 +1,245 @@
+field_names['node']['YOUR_BUNDLE_NAME'] to access them.
+ *
+ * @param string $machine_name
+ * Machine name of the node type.
+ * @param string $human_name
+ * Human readable name of the node type.
+ * @param int $language_content_type
+ * Either 0 (disabled), 1 (language enabled but no translations),
+ * TRANSLATION_ENABLED or ENTITY_TRANSLATION_ENABLED.
+ * pparam bool $attach_fields
+ * (optional) If fields with the same translatability should automatically
+ * be attached to the node type.
+ */
+ function createNodeType($machine_name, $human_name, $language_content_type = 0, $attach_fields = TRUE) {
+
+ // Create new bundle.
+ $type = array(
+ 'type' => $machine_name,
+ 'name' => $human_name,
+ 'base' => 'node_content',
+ 'description' => '',
+ 'custom' => 1,
+ 'modified' => 1,
+ 'locked' => 0,
+ );
+ $type = node_type_set_defaults($type);
+ node_type_save($type);
+ node_add_body_field($type);
+ node_types_rebuild();
+
+ // Set content type to be translatable as specified by
+ // $language_content_type.
+ $edit = array();
+ $edit['language_content_type'] = $language_content_type;
+ $this->drupalPost('admin/structure/types/manage/' . $machine_name, $edit, t('Save content type'));
+
+ $translatable = FALSE;
+ if (defined('ENTITY_TRANSLATION_ENABLED') && $language_content_type == ENTITY_TRANSLATION_ENABLED) {
+ $translatable = TRUE;
+ }
+
+ // Push in also the body field.
+ $this->field_names['node'][$machine_name][] = 'body';
+
+ if ($attach_fields) {
+ $this->attachFields('node', $machine_name, $translatable);
+ }
+
+ // Change body field to be translatable.
+ $body = field_info_field('body');
+ $body['translatable'] = $translatable;
+ field_update_field($body);
+ }
+
+ /**
+ * Creates taxonomy vocabulary with custom fields.
+ *
+ * To create and attach fields it internally calls
+ * TMGMTEntityTestCaseUtility::attachFields(). You can than access these
+ * fields calling $this->field_names['node']['YOUR_BUNDLE_NAME'].
+ *
+ * @param string $machine_name
+ * Vocabulary machine name.
+ * @param string $human_name
+ * Vocabulary human readable name.
+ * @param bool|array $fields_translatable
+ * Flag or definition array to determine which or all fields should be
+ * translatable.
+ *
+ * @return stdClass
+ * Created vocabulary object.
+ */
+ function createTaxonomyVocab($machine_name, $human_name, $fields_translatable = TRUE) {
+ $vocabulary = new stdClass();
+ $vocabulary->name = $human_name;
+ $vocabulary->machine_name = $machine_name;
+ taxonomy_vocabulary_save($vocabulary);
+
+ $this->attachFields('taxonomy_term', $vocabulary->machine_name, $fields_translatable);
+
+ return $vocabulary;
+ }
+
+ /**
+ * Creates fields of type text and text_with_summary of different cardinality.
+ *
+ * It will attach created fields to provided entity name and bundle.
+ *
+ * Field names will be stored in $this->field_names['entity']['bundle']
+ * through which you can access them.
+ *
+ * @param string $entity_name
+ * Entity name to which fields should be attached.
+ * @param string $bundle
+ * Bundle name to which fields should be attached.
+ * @param bool|array $translatable
+ * Flag or definition array to determine which or all fields should be
+ * translatable.
+ */
+ function attachFields($entity_name, $bundle, $translatable = TRUE) {
+ // Create several text fields.
+ $field_types = array('text', 'text_with_summary');
+
+ for ($i = 0 ; $i <= 5; $i++) {
+ $field_type = $field_types[array_rand($field_types, 1)];
+ $field_name = drupal_strtolower($this->randomName());
+
+ // Create a field.
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => $field_type,
+ 'cardinality' => mt_rand(1, 5),
+ 'translatable' => is_array($translatable) && isset($translatable[$i]) ? $translatable[$i] : (boolean) $translatable,
+ );
+ field_create_field($field);
+
+ // Create an instance of the previously created field.
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => $entity_name,
+ 'bundle' => $bundle,
+ 'label' => $this->randomName(10),
+ 'description' => $this->randomString(30),
+ 'widget' => array(
+ 'type' => $field_type == 'text' ? 'text_textfield' : 'text_textarea_with_summary',
+ 'label' => $this->randomString(10),
+ ),
+ );
+ field_create_instance($instance);
+
+ // Store field names in case there are needed outside this method.
+ $this->field_names[$entity_name][$bundle][] = $field_name;
+ }
+ }
+
+ /**
+ * Creates a node of a given bundle.
+ *
+ * It uses $this->field_names to populate content of attached fields.
+ *
+ * @param string $bundle
+ * Node type name.
+ * @param string $sourcelang
+ * Source lang of the node to be created.
+ *
+ * @return object
+ * Newly created node object.
+ */
+ function createNode($bundle, $sourcelang = 'en') {
+ $node = array(
+ 'type' => $bundle,
+ 'language' => $sourcelang,
+ // Ensure that the body field is defined for the node language.
+ 'body' => array($sourcelang => array(0 => array())),
+ );
+
+ foreach ($this->field_names['node'][$bundle] as $field_name) {
+ $field_info = field_info_field($field_name);
+ $cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
+ $field_langcode = field_is_translatable('node', $field_info) ? $sourcelang : LANGUAGE_NONE;
+
+ // Create two deltas for each field.
+ for ($delta = 0; $delta <= $cardinality; $delta++) {
+ $node[$field_name][$field_langcode][$delta]['value'] = $this->randomName(20);
+ if ($field_info['type'] == 'text_with_summary') {
+ $node[$field_name][$field_langcode][$delta]['summary'] = $this->randomName(10);
+ }
+ }
+ }
+
+ return $this->drupalCreateNode($node);
+ }
+
+ /**
+ * Creates a taxonomy term of a given vocabulary.
+ *
+ * It uses $this->field_names to populate content of attached fields. You can
+ * access fields values using
+ * $this->field_names['taxonomy_term'][$vocabulary->machine_name].
+ *
+ * @param object $vocabulary
+ * Vocabulary object for which the term should be created.
+ *
+ * @param string $langcode
+ * The language code to be set as the entity source language.
+ *
+ * @return object
+ * Newly created node object.
+ */
+ function createTaxonomyTerm($vocabulary, $langcode = 'en') {
+
+ // When an entity is being saved, the entity_translation module initializes
+ // a translation fetching the language from an entity. But the taxonomy
+ // terms have no entity language key, so its langcode will be the set to the
+ // default one.
+ /* @see entity_translation_field_attach_insert() */
+ /* @see EntityTranslationDefaultHandler::initTranslations() */
+ /* @see EntityTranslationDefaultHandler::getLanguage() */
+ $settings_variable_name = 'entity_translation_settings_taxonomy_term__' . $vocabulary->machine_name;
+ variable_set($settings_variable_name, array('default_language' => $langcode));
+
+ $term = new stdClass();
+ $term->name = $this->randomName();
+ $term->description = $this->randomName();
+ $term->vid = $vocabulary->vid;
+
+ foreach ($this->field_names['taxonomy_term'][$vocabulary->machine_name] as $field_name) {
+ $field_info = field_info_field($field_name);
+ $cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
+ $field_lang = $field_info['translatable'] ? $langcode : LANGUAGE_NONE;
+
+ // Create two deltas for each field.
+ for ($delta = 0; $delta <= $cardinality; $delta++) {
+ $term->{$field_name}[$field_lang][$delta]['value'] = $this->randomName(20);
+ if ($field_info['type'] == 'text_with_summary') {
+ $term->{$field_name}[$field_lang][$delta]['summary'] = $this->randomName(10);
+ }
+ }
+ }
+
+ taxonomy_term_save($term);
+ return taxonomy_term_load($term->tid);
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.base.test b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.base.test
new file mode 100644
index 00000000..cac283e5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.base.test
@@ -0,0 +1,219 @@
+default_translator = tmgmt_translator_load('test_translator');
+
+ // Load default admin permissions.
+ $this->admin_permissions = array(
+ 'administer languages',
+ 'access administration pages',
+ 'administer content types',
+ 'administer tmgmt',
+ );
+
+ // Load default translator user permissions.
+ $this->translator_permissions = array(
+ 'create translation jobs',
+ 'submit translation jobs',
+ 'accept translation jobs',
+ );
+ }
+
+ /**
+ * Will create a user with admin permissions and log it in.
+ *
+ * @param array $additional_permissions
+ * Additional permissions that will be granted to admin user.
+ * @param boolean $reset_permissions
+ * Flag to determine if default admin permissions will be replaced by
+ * $additional_permissions.
+ *
+ * @return object
+ * Newly created and logged in user object.
+ */
+ function loginAsAdmin($additional_permissions = array(), $reset_permissions = FALSE) {
+ $permissions = $this->admin_permissions;
+
+ if ($reset_permissions) {
+ $permissions = $additional_permissions;
+ }
+ elseif (!empty($additional_permissions)) {
+ $permissions = array_merge($permissions, $additional_permissions);
+ }
+
+ $this->admin_user = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->admin_user);
+ return $this->admin_user;
+ }
+
+ /**
+ * Will create a user with translator permissions and log it in.
+ *
+ * @param array $additional_permissions
+ * Additional permissions that will be granted to admin user.
+ * @param boolean $reset_permissions
+ * Flag to determine if default admin permissions will be replaced by
+ * $additional_permissions.
+ *
+ * @return object
+ * Newly created and logged in user object.
+ */
+ function loginAsTranslator($additional_permissions = array(), $reset_permissions = FALSE) {
+ $permissions = $this->translator_permissions;
+
+ if ($reset_permissions) {
+ $permissions = $additional_permissions;
+ }
+ elseif (!empty($additional_permissions)) {
+ $permissions = array_merge($permissions, $additional_permissions);
+ }
+
+ $this->translator_user = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->translator_user);
+ return $this->translator_user;
+ }
+
+ /**
+ * Creates, saves and returns a translator.
+ *
+ * @return TMGMTTranslator
+ */
+ function createTranslator() {
+ $translator = new TMGMTTranslator();
+ $translator->name = strtolower($this->randomName());
+ $translator->label = $this->randomName();
+ $translator->plugin = 'test_translator';
+ $translator->settings = array(
+ 'key' => $this->randomName(),
+ 'another_key' => $this->randomName(),
+ );
+ $this->assertEqual(SAVED_NEW, $translator->save());
+
+ // Assert that the translator was assigned a tid.
+ $this->assertTrue($translator->tid > 0);
+ return $translator;
+ }
+
+ /**
+ * Creates, saves and returns a translation job.
+ *
+ * @return TMGMTJob
+ */
+ function createJob($source = 'en', $target = 'de', $uid = 1) {
+ $job = tmgmt_job_create($source, $target, $uid);
+ $this->assertEqual(SAVED_NEW, $job->save());
+
+ // Assert that the translator was assigned a tid.
+ $this->assertTrue($job->tjid > 0);
+ return $job;
+ }
+
+
+ /**
+ * Sets the proper environment.
+ *
+ * Currently just adds a new language.
+ *
+ * @param string $langcode
+ * The language code.
+ */
+ function setEnvironment($langcode) {
+ // Add the language.
+ locale_add_language($langcode);
+ }
+
+ /**
+ * Asserts job item language codes.
+ *
+ * @param TMGMTJobItem $job_item
+ * Job item to check.
+ * @param string $expected_source_lang
+ * Expected source language.
+ * @param array $actual_lang_codes
+ * Expected existing language codes (translations).
+ */
+ function assertJobItemLangCodes(TMGMTJobItem $job_item, $expected_source_lang, array $actual_lang_codes) {
+ $this->assertEqual($job_item->getSourceLangCode(), $expected_source_lang);
+ $existing = $job_item->getExistingLangCodes();
+ sort($existing);
+ sort($actual_lang_codes);
+ $this->assertEqual($existing, $actual_lang_codes);
+ }
+
+ /**
+ * Adds languages as admin user and switches to a translator user.
+ */
+ protected function createLanguagesLoginTranslator($permissions = NULL) {
+ // Login as admin to be able to set environment variables.
+ $this->loginAsAdmin();
+ $this->setEnvironment('de');
+ $this->setEnvironment('es');
+ $this->setEnvironment('el');
+
+ $base_permissions = array(
+ 'access administration pages',
+ 'create translation jobs',
+ 'submit translation jobs',
+ );
+ $permissions = $permissions ? array_merge($permissions, $base_permissions) : $base_permissions;
+ // Login as translator only with limited permissions to run these tests.
+ $this->loginAsTranslator($permissions, TRUE);
+ }
+
+}
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.crud.test b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.crud.test
new file mode 100644
index 00000000..d452f199
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.crud.test
@@ -0,0 +1,532 @@
+ 'CRUD tests',
+ 'description' => 'Basic crud operations for jobs and translators',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ /**
+ * Test crud operations of translators.
+ */
+ function testTranslators() {
+ $translator = $this->createTranslator();
+
+ $loaded_translator = tmgmt_translator_load($translator->tid);
+
+ $this->assertEqual($translator->name, $loaded_translator->name);
+ $this->assertEqual($translator->label, $loaded_translator->label);
+ $this->assertEqual($translator->settings, $loaded_translator->settings);
+
+ // Update the settings.
+ $translator->settings['new_key'] = $this->randomString();
+ $this->assertEqual(SAVED_UPDATED, $translator->save());
+
+ $loaded_translator = tmgmt_translator_load($translator->tid);
+
+ $this->assertEqual($translator->name, $loaded_translator->name);
+ $this->assertEqual($translator->label, $loaded_translator->label);
+ $this->assertEqual($translator->settings, $loaded_translator->settings);
+
+ // Delete the translator, make sure the translator is gone.
+ $translator->delete();
+ $this->assertFalse(tmgmt_translator_load($translator->tid));
+ }
+
+ /**
+ * Test crud operations of jobs.
+ */
+ function testJobs() {
+ $job = $this->createJob();
+
+ $loaded_job = tmgmt_job_load($job->tjid);
+
+ $this->assertEqual($job->source_language, $loaded_job->source_language);
+ $this->assertEqual($job->target_language, $loaded_job->target_language);
+
+ // Assert that the created and changed information has been set to the
+ // default value.
+ $this->assertTrue($loaded_job->created > 0);
+ $this->assertTrue($loaded_job->changed > 0);
+ $this->assertEqual(0, $loaded_job->state);
+
+ // Update the settings.
+ $job->reference = 7;
+ $this->assertEqual(SAVED_UPDATED, $job->save());
+
+ $loaded_job = tmgmt_job_load($job->tjid);
+
+ $this->assertEqual($job->reference, $loaded_job->reference);
+
+ // Test the job items.
+ $item1 = $job->addItem('test_source', 'type', 5);
+ $item2 = $job->addItem('test_source', 'type', 4);
+
+ // Load and compare the items.
+ $items = $job->getItems();
+ $this->assertEqual(2, count($items));
+
+ $this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
+ $this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
+ $this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
+ $this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
+ $this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
+ $this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
+
+ // Delete the job and make sure it is gone.
+ $job->delete();
+ $this->assertFalse(tmgmt_job_load($job->tjid));
+ }
+
+ function testRemoteMappings() {
+
+ $data_key = '5][test_source][type';
+
+ $translator = $this->createTranslator();
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->save();
+ $item1 = $job->addItem('test_source', 'type', 5);
+ $item2 = $job->addItem('test_source', 'type', 4);
+
+ $mapping_data = array(
+ 'remote_identifier_2' => 'id12',
+ 'remote_identifier_3' => 'id13',
+ 'amount' => 1043,
+ 'currency' => 'EUR',
+ );
+
+ $result = $item1->addRemoteMapping($data_key, 'id11', $mapping_data);
+ $this->assertEqual($result, SAVED_NEW);
+
+ $job_mappings = $job->getRemoteMappings();
+ $item_mappings = $item1->getRemoteMappings();
+
+ $job_mapping = array_shift($job_mappings);
+ $item_mapping = array_shift($item_mappings);
+
+ $_job = $job_mapping->getJob();
+ $this->assertEqual($job->tjid, $_job->tjid);
+
+ $_job = $item_mapping->getJob();
+ $this->assertEqual($job->tjid, $_job->tjid);
+
+ $_item1 = $item_mapping->getJobItem();
+ $this->assertEqual($item1->tjiid, $_item1->tjiid);
+
+ /**
+ * @var TMGMTRemoteController $remote_mapping_controller
+ */
+ $remote_mapping_controller = entity_get_controller('tmgmt_remote');
+ $remote_mappings = $remote_mapping_controller->loadByRemoteIdentifier('id11', 'id12', 'id13');
+ $remote_mapping = array_shift($remote_mappings);
+ $this->assertEqual($remote_mapping->tjiid, $item1->tjiid);
+ $this->assertEqual($remote_mapping->amount, $mapping_data['amount']);
+ $this->assertEqual($remote_mapping->currency, $mapping_data['currency']);
+
+ $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11')), 1);
+ $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', '')), 0);
+ $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', NULL, '')), 0);
+ $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier(NULL, NULL, 'id13')), 1);
+
+ // Test remote data.
+ $item_mapping->addRemoteData('test_data', 'test_value');
+ entity_save('tmgmt_remote', $item_mapping);
+ $item_mapping = entity_load_single('tmgmt_remote', $item_mapping->trid);
+ $this->assertEqual($item_mapping->getRemoteData('test_data'), 'test_value');
+
+ // Add mapping to the other job item as well.
+ $item2->addRemoteMapping($data_key, 'id21', array('remote_identifier_2' => 'id22', 'remote_identifier_3' => 'id23'));
+
+ // Test deleting.
+
+ // Delete item1.
+ entity_get_controller('tmgmt_job_item')->delete(array($item1->tjiid));
+ // Test if mapping for item1 has been removed as well.
+
+ $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item1->tjiid)), 0);
+
+ // We still should have mapping for item2.
+ $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 1);
+
+ // Now delete the job and see if remaining mappings were removed as well.
+ entity_get_controller('tmgmt_job')->delete(array($job->tjid));
+ $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 0);
+ }
+
+ /**
+ * Test crud operations of job items.
+ */
+ function testJobItems() {
+ $job = $this->createJob();
+
+ // Add some test items.
+ $item1 = $job->addItem('test_source', 'type', 5);
+ $item2 = $job->addItem('test_source', 'test_with_long_label', 4);
+
+ // Test single load callback.
+ $item = tmgmt_job_item_load($item1->tjiid);
+ $this->assertEqual($item1->plugin, $item->plugin);
+ $this->assertEqual($item1->item_type, $item->item_type);
+ $this->assertEqual($item1->item_id, $item->item_id);
+
+ // Test multiple load callback.
+ $items = tmgmt_job_item_load_multiple(array($item1->tjiid, $item2->tjiid));
+
+ $this->assertEqual(2, count($items));
+
+ $this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
+ $this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
+ $this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
+ $this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
+ $this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
+ $this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
+ // Test the second item label length - it must not exceed the
+ // TMGMT_JOB_LABEL_MAX_LENGTH.
+ $this->assertTrue(TMGMT_JOB_LABEL_MAX_LENGTH >= strlen($items[$item2->tjiid]->label()));
+ }
+
+ /**
+ * Tests adding translated data and revision handling.
+ */
+ function testAddingTranslatedData() {
+ $translator = $this->createTranslator();
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->save();
+
+ // Add some test items.
+ $item1 = $job->addItem('test_source', 'test_with_long_label', 5);
+ // Test the job label - it must not exceed the TMGMT_JOB_LABEL_MAX_LENGTH.
+ $this->assertTrue(TMGMT_JOB_LABEL_MAX_LENGTH >= strlen($job->label()));
+
+ $key = array('dummy', 'deep_nesting');
+
+ $translation['dummy']['deep_nesting']['#text'] = 'translated 1';
+ $item1->addTranslatedData($translation);
+ $data = $item1->getData($key);
+
+ // Check job messages.
+ $messages = $job->getMessages();
+ $this->assertEqual(count($messages), 1);
+ $last_message = end($messages);
+ $this->assertEqual($last_message->message, 'The translation of !source to @language is finished and can now be reviewed.');
+
+ // Initial state - translation has been received for the first time.
+ $this->assertEqual($data['#translation']['#text'], 'translated 1');
+ $this->assertTrue(empty($data['#translation']['#text_revisions']));
+ $this->assertEqual($data['#translation']['#origin'], 'remote');
+ $this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME);
+
+ // Set status back to pending as if the data item was rejected.
+ $item1->updateData(array('dummy', 'deep_nesting'), array('#status' => TMGMT_DATA_ITEM_STATE_PENDING));
+ // Add same translation text.
+ $translation['dummy']['deep_nesting']['#text'] = 'translated 1';
+ $item1->addTranslatedData($translation);
+ $data = $item1->getData($key);
+ // Check if the status has been updated back to translated.
+ $this->assertEqual($data['#status'], TMGMT_DATA_ITEM_STATE_TRANSLATED);
+
+ // Add translation, however locally customized.
+ $translation['dummy']['deep_nesting']['#text'] = 'translated 2';
+ $translation['dummy']['deep_nesting']['#origin'] = 'local';
+ $translation['dummy']['deep_nesting']['#timestamp'] = REQUEST_TIME - 5;
+ $item1->addTranslatedData($translation);
+ $data = $item1->getData($key);
+
+ // The translation text is updated.
+ $this->assertEqual($data['#translation']['#text'], 'translated 2');
+ $this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME - 5);
+
+ // Previous translation is among text_revisions.
+ $this->assertEqual($data['#translation']['#text_revisions'][0]['#text'], 'translated 1');
+ $this->assertEqual($data['#translation']['#text_revisions'][0]['#origin'], 'remote');
+ $this->assertEqual($data['#translation']['#text_revisions'][0]['#timestamp'], REQUEST_TIME);
+ // Current translation origin is local.
+ $this->assertEqual($data['#translation']['#origin'], 'local');
+
+ // Check job messages.
+ $messages = $job->getMessages();
+ $this->assertEqual(count($messages), 1);
+
+ // Add translation - not local.
+ $translation['dummy']['deep_nesting']['#text'] = 'translated 3';
+ unset($translation['dummy']['deep_nesting']['#origin']);
+ unset($translation['dummy']['deep_nesting']['#timestamp']);
+ $item1->addTranslatedData($translation);
+ $data = $item1->getData($key);
+
+ // The translation text is NOT updated.
+ $this->assertEqual($data['#translation']['#text'], 'translated 2');
+ $this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME - 5);
+ // Received translation is the latest revision.
+ $last_revision = end($data['#translation']['#text_revisions']);
+ $this->assertEqual($last_revision['#text'], 'translated 3');
+ $this->assertEqual($last_revision['#origin'], 'remote');
+ $this->assertEqual($last_revision['#timestamp'], REQUEST_TIME);
+ // Current translation origin is local.
+ $this->assertEqual($data['#translation']['#origin'], 'local');
+
+ // Check job messages.
+ $messages = $job->getMessages();
+ $this->assertEqual(count($messages), 2);
+ $last_message = end($messages);
+ $this->assertEqual($last_message->message, 'Translation for customized @key received. Revert your changes if you wish to use it.');
+
+ // Revert to previous revision which is the latest received translation.
+ $item1->dataItemRevert($key);
+ $data = $item1->getData($key);
+
+ // The translation text is updated.
+ $this->assertEqual($data['#translation']['#text'], 'translated 3');
+ $this->assertEqual($data['#translation']['#origin'], 'remote');
+ $this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME);
+ // Latest revision is now the formerly added local translation.
+ $last_revision = end($data['#translation']['#text_revisions']);
+ $this->assertTrue($last_revision['#text'], 'translated 2');
+ $this->assertTrue($last_revision['#origin'], 'remote');
+ $this->assertEqual($last_revision['#timestamp'], REQUEST_TIME - 5);
+
+ // Check job messages.
+ $messages = $job->getMessages();
+ $this->assertEqual(count($messages), 3);
+ $last_message = end($messages);
+ $this->assertEqual($last_message->message, 'Translation for @key reverted to the latest version.');
+
+ // There should be three revisions now.
+ $this->assertEqual(count($data['#translation']['#text_revisions']), 3);
+
+ // Attempt to update the translation with the same text, this should not
+ // lead to a new revision.
+ $translation['dummy']['deep_nesting']['#text'] = 'translated 3';
+ //unset($translation['dummy']['deep_nesting']['#origin']);
+ //unset($translation['dummy']['deep_nesting']['#timestamp']);
+ $item1->addTranslatedData($translation);
+ $data = $item1->getData($key);
+ $this->assertEqual(count($data['#translation']['#text_revisions']), 3);
+
+ // Mark the translation as reviewed, a new translation should not update the
+ // existing one but create a new translation.
+ $item1->updateData($key, array('#status' => TMGMT_DATA_ITEM_STATE_REVIEWED));
+ $translation['dummy']['deep_nesting']['#text'] = 'translated 4';
+ $item1->addTranslatedData($translation);
+ $data = $item1->getData($key);
+
+ // The translation text is NOT updated.
+ $this->assertEqual($data['#translation']['#text'], 'translated 3');
+ // Received translation is the latest revision.
+ $this->assertEqual(count($data['#translation']['#text_revisions']), 4);
+ $last_revision = end($data['#translation']['#text_revisions']);
+ $this->assertEqual($last_revision['#text'], 'translated 4');
+ $this->assertEqual($last_revision['#origin'], 'remote');
+ $this->assertEqual($last_revision['#timestamp'], REQUEST_TIME);
+
+ // Check job messages.
+ $messages = $job->getMessages();
+ $this->assertEqual(count($messages), 4);
+ $last_message = end($messages);
+ $this->assertEqual($last_message->message, 'Translation for already reviewed @key received and stored as a new revision. Revert to it if you wish to use it.');
+ }
+
+ /**
+ * Test the calculations of the counters.
+ */
+ function testJobItemsCounters() {
+ $job = $this->createJob();
+
+ // Some test data items.
+ $data1 = array(
+ '#text' => 'The text to be translated.',
+ );
+ $data2 = array(
+ '#text' => 'The text to be translated.',
+ '#translation' => '',
+ );
+ $data3 = array(
+ '#text' => 'The text to be translated.',
+ '#translation' => 'The translated data. Set by the translator plugin.',
+ );
+ $data4 = array(
+ '#text' => 'Another, longer text to be translated.',
+ '#translation' => 'The translated data. Set by the translator plugin.',
+ '#status' => TMGMT_DATA_ITEM_STATE_REVIEWED,
+ );
+ $data5 = array(
+ '#label' => 'label',
+ 'data1' => $data1,
+ 'data4' => $data4,
+ );
+
+ // No data items.
+ $this->assertEqual(0, $job->getCountPending());
+ $this->assertEqual(0, $job->getCountTranslated());
+ $this->assertEqual(0, $job->getCountReviewed());
+ $this->assertEqual(0, $job->getCountAccepted());
+ $this->assertEqual(0, $job->getWordCount());
+
+ // Add a test items.
+ $job_item1 = tmgmt_job_item_create('plugin', 'type', 4, array('tjid' => $job->tjid));
+ $job_item1->save();
+
+ // No pending, translated and confirmed data items.
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(0, $job_item1->getCountPending());
+ $this->assertEqual(0, $job_item1->getCountTranslated());
+ $this->assertEqual(0, $job_item1->getCountReviewed());
+ $this->assertEqual(0, $job_item1->getCountAccepted());
+ $this->assertEqual(0, $job->getCountPending());
+ $this->assertEqual(0, $job->getCountTranslated());
+ $this->assertEqual(0, $job->getCountReviewed());
+ $this->assertEqual(0, $job->getCountAccepted());
+
+ // Add an untranslated data item.
+ $job_item1->data['data_item1'] = $data1;
+ $job_item1->save();
+
+ // One pending data items.
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(1, $job_item1->getCountPending());
+ $this->assertEqual(0, $job_item1->getCountTranslated());
+ $this->assertEqual(0, $job_item1->getCountReviewed());
+ $this->assertEqual(5, $job_item1->getWordCount());
+ $this->assertEqual(1, $job->getCountPending());
+ $this->assertEqual(0, $job->getCountReviewed());
+ $this->assertEqual(0, $job->getCountTranslated());
+ $this->assertEqual(5, $job->getWordCount());
+
+
+ // Add another untranslated data item.
+ // Test with an empty translation set.
+ $job_item1->data['data_item1'] = $data2;
+ $job_item1->save();
+
+ // One pending data items.
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(1, $job_item1->getCountPending());
+ $this->assertEqual(0, $job_item1->getCountTranslated());
+ $this->assertEqual(0, $job_item1->getCountReviewed());
+ $this->assertEqual(5, $job_item1->getWordCount());
+ $this->assertEqual(1, $job->getCountPending());
+ $this->assertEqual(0, $job->getCountTranslated());
+ $this->assertEqual(0, $job->getCountReviewed());
+ $this->assertEqual(5, $job->getWordCount());
+
+ // Add a translated data item.
+ $job_item1->data['data_item1'] = $data3;
+ $job_item1->save();
+
+ // One translated data items.
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(0, $job_item1->getCountPending());
+ $this->assertEqual(1, $job_item1->getCountTranslated());
+ $this->assertEqual(0, $job_item1->getCountReviewed());
+ $this->assertEqual(0, $job->getCountPending());
+ $this->assertEqual(0, $job->getCountReviewed());
+ $this->assertEqual(1, $job->getCountTranslated());
+
+ // Add a confirmed data item.
+ $job_item1->data['data_item1'] = $data4;
+ $job_item1->save();
+
+ // One reviewed data item.
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(1, $job_item1->getCountReviewed());
+ $this->assertEqual(1, $job->getCountReviewed());
+
+ // Add a translated and an untranslated and a confirmed data item
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+ $job_item1->data['data_item1'] = $data1;
+ $job_item1->data['data_item2'] = $data3;
+ $job_item1->data['data_item3'] = $data4;
+ $job_item1->save();
+
+ // One pending and translated data items each.
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(1, $job->getCountPending());
+ $this->assertEqual(1, $job->getCountTranslated());
+ $this->assertEqual(1, $job->getCountReviewed());
+ $this->assertEqual(16, $job->getWordCount());
+
+ // Add nested data items.
+ $job_item1->data['data_item1'] = $data5;
+ $job_item1->save();
+
+ // One pending data items.
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+ $this->assertEqual('label', $job_item1->data['data_item1']['#label']);
+ $this->assertEqual(3, count($job_item1->data['data_item1']));
+
+ // Add a greater number of data items
+ for ($index = 1; $index <= 3; $index++) {
+ $job_item1->data['data_item' . $index] = $data1;
+ }
+ for ($index = 4; $index <= 10; $index++) {
+ $job_item1->data['data_item' . $index] = $data3;
+ }
+ for ($index = 11; $index <= 15; $index++) {
+ $job_item1->data['data_item' . $index] = $data4;
+ }
+ $job_item1->save();
+
+ // 3 pending and 7 translated data items each.
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(3, $job->getCountPending());
+ $this->assertEqual(7, $job->getCountTranslated());
+ $this->assertEqual(5, $job->getCountReviewed());
+
+ // Add several job items
+ $job_item2 = tmgmt_job_item_create('plugin', 'type', 5, array('tjid' => $job->tjid));
+ for ($index = 1; $index <= 4; $index++) {
+ $job_item2->data['data_item' . $index] = $data1;
+ }
+ for ($index = 5; $index <= 12; $index++) {
+ $job_item2->data['data_item' . $index] = $data3;
+ }
+ for ($index = 13; $index <= 16; $index++) {
+ $job_item2->data['data_item' . $index] = $data4;
+ }
+ $job_item2->save();
+
+ // 3 pending and 7 translated data items each.
+ $job = entity_load_single('tmgmt_job', $job->tjid);
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(7, $job->getCountPending());
+ $this->assertEqual(15, $job->getCountTranslated());
+ $this->assertEqual(9, $job->getCountReviewed());
+
+ // Accept the job items.
+ foreach ($job->getItems() as $item) {
+ // Set the state directly to avoid triggering translator and source
+ // controllers that do not exist.
+ $item->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
+ $item->save();
+ }
+ drupal_static_reset('tmgmt_job_statistics_load');
+ $this->assertEqual(0, $job->getCountPending());
+ $this->assertEqual(0, $job->getCountTranslated());
+ $this->assertEqual(0, $job->getCountReviewed());
+ $this->assertEqual(31, $job->getCountAccepted());
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.helper.test b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.helper.test
new file mode 100644
index 00000000..92bfeab4
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.helper.test
@@ -0,0 +1,123 @@
+ 'Helper functions Test case',
+ 'description' => 'Helper functions for other modules',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ /**
+ * Tests tmgmt_job_match_item()
+ *
+ * @see tmgmt_job_match_item
+ */
+ function testTMGTJobMatchItem() {
+ $this->loginAsAdmin();
+ $this->setEnvironment('fr');
+ $this->setEnvironment('es');
+
+ // Add a job from en to fr and en to sp.
+ $job_en_fr = $this->createJob('en', 'fr');
+ $job_en_sp = $this->createJob('en', 'es');
+
+ // Add a job which has existing source-target combinations.
+ $this->assertEqual($job_en_fr->tjid, tmgmt_job_match_item('en', 'fr')->tjid);
+ $this->assertEqual($job_en_sp->tjid, tmgmt_job_match_item('en', 'es')->tjid);
+
+ // Add a job which has no existing source-target combination.
+ $this->assertTrue(tmgmt_job_match_item('fr', 'es'));
+ }
+
+ /**
+ * Tests the tmgmt_data_item_label() function.
+ *
+ * @todo: Move into a unit test case once available.
+ */
+ function testDataIemLabel() {
+ $no_label = array(
+ '#text' => 'No label',
+ );
+ $this->assertEqual(tmgmt_data_item_label($no_label), 'No label');
+ $this->assertEqual(tmgmt_data_item_label($no_label, 6), 'No ...');
+ $label = array(
+ '#parent_label' => array(),
+ '#label' => 'A label',
+ );
+ $this->assertEqual(tmgmt_data_item_label($label), 'A label');
+ $this->assertEqual(tmgmt_data_item_label($label, 6), 'A l...');
+ $parent_label = array(
+ '#parent_label' => array('Parent label', 'Sub label'),
+ '#label' => 'A label',
+ );
+ $this->assertEqual(tmgmt_data_item_label($parent_label), 'Parent label > Sub label');
+ $this->assertEqual(tmgmt_data_item_label($parent_label, 18), 'Pare... > Sub ...');
+ $nested = array(
+ '#parent_label' => array('Parent label', 'Sub label', 'Sub-sub label'),
+ '#label' => 'A label',
+ );
+ $this->assertEqual(tmgmt_data_item_label($nested), 'Parent label > Sub label > Sub-sub label');
+ $this->assertEqual(tmgmt_data_item_label($nested, 28), 'Pare... > Sub ... > Sub-...');
+ $long_label = array(
+ '#parent_label' => array('Loooooooooooong label', 'Short'),
+ '#label' => 'A label',
+ );
+ $this->assertEqual(tmgmt_data_item_label($long_label), 'Loooooooooooong label > Short');
+ $this->assertEqual(tmgmt_data_item_label($long_label, 30), 'Loooooooooooong label > Short');
+ $node_example = array(
+ '#parent_label' => array('This is a very loooong title, so looong', 'Body', 'Delta #0', 'Body'),
+ '#label' => 'A label',
+ );
+ $this->assertEqual(tmgmt_data_item_label($node_example), 'This is a very loooong title, so looong > Body > Delta #0 > Body');
+ $this->assertEqual(tmgmt_data_item_label($node_example, 56), 'This is a very loooong title... > Body > Delta #0 > Body');
+ }
+
+ function testWordCount() {
+ $unit_tests = array(
+ 'empty' => array(
+ 'text' => '',
+ 'count' => 0,
+ ),
+ 'latin' => array(
+ 'text' => 'Drupal is the best!',
+ 'count' => 4,
+ ),
+ 'non-latin' => array(
+ 'text' => 'Друпал лучший!',
+ 'count' => 2,
+ ),
+ 'complex punctuation' => array(
+ 'text' => '<[({-!ReAd@*;: ,?+MoRe...})]>\\|/',
+ 'count' => 2,
+ 'exclude_tags' => FALSE,
+ ),
+ 'repeat' => array(
+ 'text' => 'repeat repeat',
+ 'count' => 2,
+ ),
+ 'strip tags' => array(
+ 'text' => 'link text plain text ',
+ 'count' => 4,
+ ),
+ );
+ foreach ($unit_tests as $id => $test_data) {
+ // Set the exclude_tags flag. In case not provided the TRUE is default.
+ $test_data += array('exclude_tags' => TRUE);
+ if (variable_get('tmgmt_word_count_exclude_tags', TRUE) != $test_data['exclude_tags']) {
+ variable_set('tmgmt_word_count_exclude_tags', $test_data['exclude_tags']);
+ }
+ $this->assertEqual($real_count = tmgmt_word_count($test_data['text']), $desirable_count = $test_data['count'], t('!test_id: Real count (=!real_count) should be equal to desirable (=!desirable_count)', array('!test_id' => $id, '!real_count' => $real_count, '!desirable_count' => $desirable_count)));
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.plugin.test b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.plugin.test
new file mode 100644
index 00000000..67748e7f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.plugin.test
@@ -0,0 +1,230 @@
+ 'Plugin tests',
+ 'description' => 'Verifies basic functionality of source and translator plugins',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function createJob($source = 'en', $target = 'de', $uid = 1) {
+ $job = parent::createJob();
+
+ for ($i = 1; $i < 3; $i++) {
+ if ($i == 3) {
+ // Explicitly define the data for the third item.
+ $data['data'] = array(
+ 'dummy' => array(
+ 'deep_nesting' => array(
+ '#text' => 'Stored data',
+ ),
+ ),
+ );
+ $job->addItem('test_source', 'test', $i, array($data));
+ }
+ $job->addItem('test_source', 'test', $i);
+ }
+
+ // Manually specify the translator for now.
+ $job->translator = $this->default_translator->name;
+
+ return $job;
+ }
+
+ function testBasicWorkflow() {
+ // Submit a translation job.
+ $submit_job = $this->createJob();
+ $submit_job->settings = array('action' => 'submit');
+ $submit_job->requestTranslation();
+ $submit_job = tmgmt_job_load($submit_job->tjid);
+ $this->assertTrue($submit_job->isActive());
+ $messages = $submit_job->getMessages();
+ $last_message = end($messages);
+ $this->assertEqual('Test submit.', $last_message->message);
+
+ // Translate a job.
+ $translate_job = $this->createJob();
+ $translate_job->settings = array('action' => 'translate');
+ $translate_job->requestTranslation();
+ $translate_job = tmgmt_job_load($translate_job->tjid);
+ foreach ($translate_job->getItems() as $job_item) {
+ $this->assertTrue($job_item->isNeedsReview());
+ }
+
+ $messages = $translate_job->getMessages();
+ // array_values() results in numeric keys, which is necessary for list.
+ list($debug, $translated, $needs_review) = array_values($messages);
+ $this->assertEqual('Test translator called.', $debug->message);
+ $this->assertEqual('debug', $debug->type);
+ $this->assertEqual('Test translation created.', $translated->message);
+ $this->assertEqual('status', $translated->type);
+
+ // The third message is specific to a job item and has different state
+ // constants.
+ $this->assertEqual('The translation of !source to @language is finished and can now be reviewed.', $needs_review->message);
+ $this->assertEqual('status', $needs_review->type);
+
+ $i = 1;
+ foreach ($translate_job->getItems() as $item) {
+ // Check the translated text.
+ if ($i != 3) {
+ $expected_text = 'de_Text for job item with type ' . $item->item_type . ' and id ' . $item->item_id . '.';
+ }
+ else {
+ // The third item has an explicitly stored data value.
+ $expected_text = 'de_Stored data';
+ }
+ $item_data = $item->getData();
+ $this->assertEqual($expected_text, $item_data['dummy']['deep_nesting']['#translation']['#text']);
+ $i++;
+ }
+
+ foreach ($translate_job->getItems() as $job_item) {
+ $job_item->acceptTranslation();
+ }
+
+ // @todo Accepting does not result in messages on the job anymore.
+ // Update once there are job item messages.
+ /*
+ $messages = $translate_job->getMessages();
+ $last_message = end($messages);
+ $this->assertEqual('Job accepted', $last_message->message);
+ $this->assertEqual('status', $last_message->type);*/
+
+ // Check if the translations have been "saved".
+ foreach ($translate_job->getItems() as $item) {
+ $this->assertTrue(variable_get('tmgmt_test_saved_translation_' . $item->item_type . '_' . $item->item_id, FALSE));
+ }
+
+ // A rejected job.
+ $reject_job = $this->createJob();
+ $reject_job->settings = array('action' => 'reject');
+ $reject_job->requestTranslation();
+ // Still rejected.
+ $this->assertTrue($reject_job->isRejected());
+
+ $messages = $reject_job->getMessages();
+ $last_message = end($messages);
+ $this->assertEqual('This is not supported.', $last_message->message);
+ $this->assertEqual('error', $last_message->type);
+
+ // A failing job.
+ $failing_job = $this->createJob();
+ $failing_job->settings = array('action' => 'fail');
+ $failing_job->requestTranslation();
+ // Still new.
+ $this->assertTrue($failing_job->isUnprocessed());
+
+ $messages = $failing_job->getMessages();
+ $last_message = end($messages);
+ $this->assertEqual('Service not reachable.', $last_message->message);
+ $this->assertEqual('error', $last_message->type);
+ }
+
+ /**
+ * Tests remote languages mappings support in the tmgmt core.
+ */
+ function testRemoteLanguagesMappings() {
+ $this->loginAsAdmin();
+ $this->setEnvironment('de');
+ $controller = $this->default_translator->getController();
+
+ $mappings = $controller->getRemoteLanguagesMappings($this->default_translator);
+ $this->assertEqual($mappings, array(
+ 'en' => 'en-us',
+ 'de' => 'de-ch',
+ ));
+
+ $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-us');
+ $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-ch');
+ $this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'en-us'), 'en');
+ $this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'de-ch'), 'de');
+
+ $this->default_translator->settings['remote_languages_mappings']['de'] = 'de-de';
+ $this->default_translator->settings['remote_languages_mappings']['en'] = 'en-uk';
+ $this->default_translator->save();
+
+ $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-uk');
+ $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-de');
+
+ // Test the fallback.
+ $info = &drupal_static('_tmgmt_plugin_info');
+ $info['translator']['test_translator']['map remote languages'] = FALSE;
+
+ $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en');
+ $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de');
+ }
+
+ /**
+ * Tests escaping and unescaping text.
+ */
+ function testEscaping() {
+ $controller = $this->default_translator->getController();
+
+ $tests = array(
+ array(
+ 'item' => array('#text' => 'no escaping'),
+ 'expected' => 'no escaping',
+ ),
+ array(
+ 'item' => array(
+ '#text' => 'single placeholder',
+ '#escape' => array(
+ 7 => array('string' => 'placeholder'),
+ ),
+ ),
+ 'expected' => 'single [[[placeholder]]]',
+ ),
+ array(
+ 'item' => array(
+ '#text' => 'two placeholder, the second placeholder',
+ '#escape' => array(
+ 4 => array('string' => 'placeholder'),
+ 28 => array('string' => 'placeholder'),
+ ),
+ ),
+ 'expected' => 'two [[[placeholder]]], the second [[[placeholder]]]',
+ ),
+ array(
+ 'item' => array(
+ '#text' => 'something, something else',
+ '#escape' => array(
+ 0 => array('string' => 'something'),
+ 21 => array('string' => 'else'),
+ ),
+ ),
+ 'expected' => '[[[something]]], something [[[else]]]',
+ ),
+ array(
+ 'item' => array(
+ '#text' => 'something, something else',
+ '#escape' => array(
+ 21 => array('string' => 'else'),
+ 11 => array('string' => 'something'),
+ ),
+ ),
+ 'expected' => 'something, [[[something]]] [[[else]]]',
+ ),
+ );
+
+ foreach ($tests as $test) {
+ $escaped = $controller->escapeText($test['item']);
+ // Assert that the string was escaped as expected.
+ $this->assertEqual($escaped, $test['expected']);
+
+ // Assert that the string is the same as the original when unescaped.
+ $this->assertEqual($controller->unescapeText($escaped), $test['item']['#text']);
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.upgrade.alpha1.test b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.upgrade.alpha1.test
new file mode 100644
index 00000000..8aba604c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt.upgrade.alpha1.test
@@ -0,0 +1,159 @@
+ t('Upgrade tests Alpha1'),
+ 'description' => t('Tests the upgrade path from 7.x-1.0-alpha1'),
+ 'group' => t('Translation Management'),
+ );
+ }
+
+ function setUp() {
+ // Enable all dependencies.
+ parent::setUp(array('entity', 'views', 'translation', 'locale'));
+
+ // Create the tmgmt tables and fill them.
+ module_load_include('inc', 'tmgmt', 'tests/tmgmt_alpha1_dump.sql');
+
+ // @todo: Figure out why this is necessary.
+ $enabled_modules = db_query("SELECT name FROM {system} where status = 1 and type = 'module'")->fetchCol();
+ foreach ($enabled_modules as $enabled_module) {
+ module_load_install($enabled_module);
+ // Set the schema version to the number of the last update provided
+ // by the module.
+ $versions = drupal_get_schema_versions($enabled_module);
+ $version = $versions ? max($versions) : SCHEMA_INSTALLED;
+ db_update('system')
+ ->condition('name', $enabled_module)
+ ->fields(array('schema_version' => $version))
+ ->execute();
+ }
+
+ // Set schema version to 0 and then install the tmgmt modules, to simulate
+ // an enabling.
+ db_update('system')
+ ->condition('name', array('tmgmt', 'tmgmt_ui', 'tmgmt_field', 'tmgmt_node', 'tmgmt_test', 'tmgmt_node_ui'))
+ ->fields(array(
+ 'schema_version' => 0,
+ ))
+ ->execute();
+ module_enable(array('tmgmt', 'tmgmt_ui', 'tmgmt_field', 'tmgmt_node', 'tmgmt_test', 'tmgmt_node_ui'));
+
+ // Log in as a user that can run update.php
+ $admin = $this->drupalCreateUser(array('administer software updates'));
+ $this->drupalLogin($admin);
+
+ $this->performUpgrade();
+ }
+
+ /**
+ * Verifies that the data has been migrated properly
+ */
+ function testUpgradePath() {
+ // Log in as a user with enough permissions.
+ $translator = $this->drupalCreateUser(array('administer tmgmt'));
+ $this->drupalLogin($translator);
+ // Go to a job and check the review form.
+ $this->drupalGet('admin/tmgmt/jobs/1');
+ // Make sure the #status values have been set accordingly.
+ $this->assertRaw(t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', array('@accepted' => 0, '@reviewed' => 0, '@translated' => 2, '@pending' => 0)));
+ // Extract the word count field and make sure it's correct.
+ $word_count = $this->xpath('//td[contains(@class, :class)]', array(':class' => 'views-field-word-count-1'));
+ $this->assertEqual(6, trim((string)reset($word_count)));
+
+ $this->clickLink(t('review'));
+ // Needs review icon.
+ $this->assertRaw('tmgmt-ui-icon-yellow tmgmt-ui-state-translated');
+ // Translated values.
+ $this->assertRaw('de_Test content');
+ $this->assertRaw('de_This is the body.');
+ // Reject button.
+ $this->assertRaw('✗');
+
+ // Check that accepted count has been updated correctly.
+ $this->drupalGet('admin/tmgmt/jobs/2');
+ // Make sure the #status values have been set accordingly.
+ $this->assertRaw(t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', array('@accepted' => 2, '@reviewed' => 0, '@translated' => 0, '@pending' => 0)));
+
+
+ }
+
+
+ /**
+ * Perform the upgrade.
+ *
+ * Copied and adapted from UpgradePathTestCase::performUpgrade().
+ *
+ * @param $register_errors
+ * Register the errors during the upgrade process as failures.
+ * @return
+ * TRUE if the upgrade succeeded, FALSE otherwise.
+ */
+ protected function performUpgrade($register_errors = TRUE) {
+ $update_url = $GLOBALS['base_url'] . '/update.php';
+
+ // Load the first update screen.
+ $this->drupalGet($update_url, array('external' => TRUE));
+ if (!$this->assertResponse(200)) {
+ return FALSE;
+ }
+
+ // Continue.
+ $this->drupalPost(NULL, array(), t('Continue'));
+ if (!$this->assertResponse(200)) {
+ return FALSE;
+ }
+
+ // The test should pass if there are no pending updates.
+ $content = $this->drupalGetContent();
+ if (strpos($content, t('No pending updates.')) !== FALSE) {
+ $this->pass(t('No pending updates and therefore no upgrade process to test.'));
+ $this->pendingUpdates = FALSE;
+ return TRUE;
+ }
+
+ // Go!
+ $this->drupalPost(NULL, array(), t('Apply pending updates'));
+ if (!$this->assertResponse(200)) {
+ return FALSE;
+ }
+
+ // Check for errors during the update process.
+ foreach ($this->xpath('//li[@class=:class]', array(':class' => 'failure')) as $element) {
+ $message = strip_tags($element->asXML());
+ $this->upgradeErrors[] = $message;
+ if ($register_errors) {
+ $this->fail($message);
+ }
+ }
+
+ if (!empty($this->upgradeErrors)) {
+ // Upgrade failed, the installation might be in an inconsistent state,
+ // don't process.
+ return FALSE;
+ }
+
+ // Check if there still are pending updates.
+ $this->drupalGet($update_url, array('external' => TRUE));
+ $this->drupalPost(NULL, array(), t('Continue'));
+ if (!$this->assertText(t('No pending updates.'), t('No pending updates at the end of the update process.'))) {
+ return FALSE;
+ }
+
+ // Clear caches.
+ $this->checkPermissions(array(), TRUE);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_alpha1_dump.sql.inc b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_alpha1_dump.sql.inc
new file mode 100644
index 00000000..2a0719ba
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_alpha1_dump.sql.inc
@@ -0,0 +1,463 @@
+ array(
+ 'cid' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ ),
+ 'expire' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'created' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'serialized' => array(
+ 'type' => 'int',
+ 'size' => 'small',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'indexes' => array(
+ 'expire' => array(
+ 'expire',
+ ),
+ ),
+ 'primary key' => array(
+ 'cid',
+ ),
+ 'module' => 'tmgmt',
+ 'name' => 'cache_tmgmt',
+));
+
+db_create_table('tmgmt_job', array(
+ 'fields' => array(
+ 'tjid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'source_language' => array(
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ),
+ 'target_language' => array(
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ),
+ 'state' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ),
+ 'created' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'changed' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'translator' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ ),
+ 'settings' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'reference' => array(
+ 'type' => 'varchar',
+ 'length' => 256,
+ ),
+ 'uid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'label' => array(
+ 'type' => 'varchar',
+ 'length' => 256,
+ ),
+ ),
+ 'primary key' => array(
+ 'tjid',
+ ),
+ 'indexes' => array(
+ 'state' => array(
+ 'state',
+ ),
+ 'reference' => array(
+ 'reference',
+ ),
+ ),
+ 'module' => 'tmgmt',
+ 'name' => 'tmgmt_job',
+));
+db_insert('tmgmt_job')->fields(array(
+ 'tjid',
+ 'source_language',
+ 'target_language',
+ 'state',
+ 'created',
+ 'changed',
+ 'translator',
+ 'settings',
+ 'reference',
+ 'uid',
+ 'label',
+))
+->values(array(
+ 'tjid' => '1',
+ 'source_language' => 'en',
+ 'target_language' => 'de',
+ 'state' => '1',
+ 'created' => '1342074121',
+ 'changed' => '1342074125',
+ 'translator' => 'test_translator',
+ 'settings' => 'a:1:{s:6:"action";s:9:"translate";}',
+ 'reference' => NULL,
+ 'uid' => '1',
+ 'label' => '',
+))
+->values(array(
+ 'tjid' => '2',
+ 'source_language' => 'en',
+ 'target_language' => 'es',
+ 'state' => '5',
+ 'created' => '1342074121',
+ 'changed' => '1342074127',
+ 'translator' => 'test_translator',
+ 'settings' => 'a:1:{s:6:"action";s:9:"translate";}',
+ 'reference' => NULL,
+ 'uid' => '1',
+ 'label' => '',
+))
+->execute();
+
+db_create_table('tmgmt_job_item', array(
+ 'fields' => array(
+ 'tjiid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'tjid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'unsigned' => TRUE,
+ ),
+ 'plugin' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'item_type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ ),
+ 'item_id' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'state' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'translation' => array(
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'changed' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array(
+ 'tjiid',
+ ),
+ 'indexes' => array(
+ 'job_id' => array(
+ 'tjid',
+ ),
+ ),
+ 'foreign keys' => array(
+ 'job' => array(
+ 'table' => 'tmgmt_job',
+ 'columns' => array(
+ 'tjid',
+ ),
+ ),
+ ),
+ 'module' => 'tmgmt',
+ 'name' => 'tmgmt_job_item',
+));
+db_insert('tmgmt_job_item')->fields(array(
+ 'tjiid',
+ 'tjid',
+ 'plugin',
+ 'item_type',
+ 'item_id',
+ 'state',
+ 'data',
+ 'translation',
+ 'changed',
+))
+->values(array(
+ 'tjiid' => '1',
+ 'tjid' => '1',
+ 'plugin' => 'node',
+ 'item_type' => 'node',
+ 'item_id' => '1',
+ 'state' => '2',
+ 'data' => 'a:3:{s:6:"#label";s:7:"Article";s:10:"node_title";a:2:{s:6:"#label";s:5:"Title";s:5:"#text";s:12:"Test content";}s:4:"body";a:2:{s:6:"#label";s:4:"Body";i:0;a:2:{s:6:"#label";s:8:"Delta #0";s:5:"value";a:3:{s:6:"#label";s:4:"Body";s:5:"#text";s:17:"This is the body.";s:10:"#translate";b:1;}}}}',
+ 'translation' => 'a:2:{s:10:"node_title";a:2:{s:6:"#label";s:5:"Title";s:5:"#text";s:15:"de_Test content";}s:4:"body";a:1:{i:0;a:1:{s:5:"value";a:3:{s:6:"#label";s:4:"Body";s:5:"#text";s:20:"de_This is the body.";s:10:"#translate";b:1;}}}}',
+ 'changed' => '1342074125',
+))
+->values(array(
+ 'tjiid' => '2',
+ 'tjid' => '2',
+ 'plugin' => 'node',
+ 'item_type' => 'node',
+ 'item_id' => '1',
+ 'state' => '3',
+ 'data' => 'a:3:{s:6:"#label";s:7:"Article";s:10:"node_title";a:2:{s:6:"#label";s:5:"Title";s:5:"#text";s:12:"Test content";}s:4:"body";a:2:{s:6:"#label";s:4:"Body";i:0;a:2:{s:6:"#label";s:8:"Delta #0";s:5:"value";a:3:{s:6:"#label";s:4:"Body";s:5:"#text";s:17:"This is the body.";s:10:"#translate";b:1;}}}}',
+ 'translation' => 'a:2:{s:10:"node_title";a:2:{s:6:"#label";s:5:"Title";s:5:"#text";s:15:"es_Test content";}s:4:"body";a:1:{i:0;a:1:{s:5:"value";a:3:{s:6:"#label";s:4:"Body";s:5:"#text";s:20:"es_This is the body.";s:10:"#translate";b:1;}}}}',
+ 'changed' => '1342074127',
+))
+->execute();
+
+db_create_table('tmgmt_message', array(
+ 'fields' => array(
+ 'mid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'tjid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'tjiid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ ),
+ 'message' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ ),
+ 'variables' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'created' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array(
+ 'mid',
+ ),
+ 'indexes' => array(
+ 'tjid' => array(
+ 'tjid',
+ ),
+ 'tjiid' => array(
+ 'tjiid',
+ ),
+ ),
+ 'module' => 'tmgmt',
+ 'name' => 'tmgmt_message',
+));
+db_insert('tmgmt_message')->fields(array(
+ 'mid',
+ 'tjid',
+ 'tjiid',
+ 'message',
+ 'variables',
+ 'created',
+ 'type',
+))
+->values(array(
+ 'mid' => '1',
+ 'tjid' => '1',
+ 'tjiid' => NULL,
+ 'message' => 'Test translator called.',
+ 'variables' => 'a:0:{}',
+ 'created' => '1342074125',
+ 'type' => 'debug',
+))
+->values(array(
+ 'mid' => '2',
+ 'tjid' => '1',
+ 'tjiid' => NULL,
+ 'message' => 'Test translation created.',
+ 'variables' => 'a:0:{}',
+ 'created' => '1342074125',
+ 'type' => 'status',
+))
+->values(array(
+ 'mid' => '3',
+ 'tjid' => '1',
+ 'tjiid' => '1',
+ 'message' => 'The translation for !source is finished and can now be reviewed.',
+ 'variables' => 'a:1:{s:7:"!source";s:34:"Test content";}',
+ 'created' => '1342074125',
+ 'type' => 'status',
+))
+->values(array(
+ 'mid' => '4',
+ 'tjid' => '2',
+ 'tjiid' => NULL,
+ 'message' => 'Test translator called.',
+ 'variables' => 'a:0:{}',
+ 'created' => '1342074127',
+ 'type' => 'debug',
+))
+->values(array(
+ 'mid' => '5',
+ 'tjid' => '2',
+ 'tjiid' => NULL,
+ 'message' => 'Test translation created.',
+ 'variables' => 'a:0:{}',
+ 'created' => '1342074127',
+ 'type' => 'status',
+))
+->values(array(
+ 'mid' => '6',
+ 'tjid' => '2',
+ 'tjiid' => '2',
+ 'message' => 'The translation for !source is finished and can now be reviewed.',
+ 'variables' => 'a:1:{s:7:"!source";s:34:"Test content";}',
+ 'created' => '1342074127',
+ 'type' => 'status',
+))
+->execute();
+
+db_create_table('tmgmt_translator', array(
+ 'fields' => array(
+ 'tid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'label' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'description' => array(
+ 'type' => 'text',
+ 'size' => 'medium',
+ ),
+ 'plugin' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'settings' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'weight' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 1,
+ 'size' => 'tiny',
+ ),
+ 'module' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'primary key' => array(
+ 'tid',
+ ),
+ 'unique keys' => array(
+ 'name' => array(
+ 'name',
+ ),
+ ),
+ 'module' => 'tmgmt',
+ 'name' => 'tmgmt_translator',
+));
+db_insert('tmgmt_translator')->fields(array(
+ 'tid',
+ 'name',
+ 'label',
+ 'description',
+ 'plugin',
+ 'settings',
+ 'weight',
+ 'status',
+ 'module',
+))
+->values(array(
+ 'tid' => '1',
+ 'name' => 'test_translator',
+ 'label' => 'Test translator (auto created)',
+ 'description' => 'Simple translator for testing purposes.',
+ 'plugin' => 'test_translator',
+ 'settings' => 'a:2:{s:11:"auto_accept";b:0;s:15:"expose_settings";b:1;}',
+ 'weight' => '0',
+ 'status' => '1',
+ 'module' => NULL,
+))
+->execute();
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.info b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.info
new file mode 100644
index 00000000..2b0e684f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.info
@@ -0,0 +1,16 @@
+name = Translation Management Test plugins
+package = Translation Management
+core = 7.x
+hidden = TRUE
+dependencies[] = tmgmt
+files[] = tmgmt_test.plugin.source.inc
+files[] = tmgmt_test.plugin.html_source.inc
+files[] = tmgmt_test.plugin.translator.inc
+files[] = tmgmt_test.ui.translator.inc
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.module b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.module
new file mode 100644
index 00000000..cfaa5cda
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.module
@@ -0,0 +1,93 @@
+ array(
+ 'label' => t('Test translator'),
+ 'description' => t('Simple translator for testing purposes.'),
+ 'plugin controller class' => 'TMGMTTestTranslatorPluginController',
+ 'ui controller class' => 'TMGMTTestTranslatorUIController',
+ 'default settings' => array(
+ 'expose_settings' => TRUE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_tmgmt_source_plugin_info().
+ */
+function tmgmt_test_tmgmt_source_plugin_info() {
+ return array(
+ 'test_source' => array(
+ 'label' => t('Test source'),
+ 'description' => t('Simple source for testing purposes.'),
+ 'plugin controller class' => 'TMGMTTestSourcePluginController',
+ ),
+ 'test_html_source' => array(
+ 'label' => t('Test html source'),
+ 'description' => t('HTML source for testing purposes.'),
+ 'plugin controller class' => 'TMGMTTestHTMLSourcePluginController',
+ ),
+ );
+}
+
+/**
+ * Implements hook_tmgmt_source_suggestions().
+ */
+function tmgmt_test_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
+ $suggestions = array();
+ foreach ($items as $item) {
+ if ($item->plugin == 'test_source') {
+ $suggestions[] = array(
+ 'job_item' => tmgmt_job_item_create('test_source', $item->item_type . '_suggestion', $item->item_id),
+ 'reason' => t('Test suggestion for @type source @id', array('@type' => $item->item_type,'@id' => $item->item_id)),
+ 'from_item' => $item->tjiid,
+ );
+ }
+ }
+ return $suggestions;
+}
+
+/**
+ * Implements hook_tmgmt_fle_text_processor_plugin_info().
+ */
+function tmgmt_test_tmgmt_file_text_processor_plugin_info() {
+ return array(
+ 'test' => array(
+ 'label' => t('Test'),
+ 'plugin controller class' => 'TMGMTTestTextProcessor',
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function tmgmt_test_menu() {
+ $items['tmgmt-add-to-cart/%tmgmt_job_item'] = array(
+ 'title' => 'Add to cart',
+ 'description' => 'Provides the possibility to add testing job items into the cart.',
+ 'page callback' => 'tmgmt_test_add_to_cart',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Callback to add given job item into the cart.
+ */
+function tmgmt_test_add_to_cart(TMGMTJobITem $job_item) {
+ tmgmt_ui_cart_get()->addExistingJobItems(array($job_item));
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.html_source.inc b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.html_source.inc
new file mode 100644
index 00000000..eed0b1bc
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.html_source.inc
@@ -0,0 +1,23 @@
+ array(
+ 'deep_nesting' => array(
+ '#text' => file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'),
+ '#label' => 'Label for job item with type ' . $job_item->item_type . ' and id ' . $job_item->item_id . '.',
+ ),
+ ),
+ );
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.source.inc b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.source.inc
new file mode 100644
index 00000000..94166202
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.source.inc
@@ -0,0 +1,111 @@
+item_type == 'test_not_accessible') {
+ $path = 'admin';
+ }
+ return array('path' => $path, 'options' => array());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel(TMGMTJobItem $job_item) {
+ $label = $this->pluginType . ':' . $job_item->item_type . ':' . $job_item->item_id;
+
+ // We need to test if job and job item labels get properly truncated,
+ // therefore in case the job item type is "test_with_long_label" we append
+ // further text to the existing label.
+ if ($job_item->item_type == 'test_with_long_label') {
+ $label .= 'Some very long and boring label that definitely exceeds hundred and twenty eight characters which is the maximum character count for the job item label.';
+ }
+
+ return $label;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getData(TMGMTJobItem $job_item) {
+ // Allow tests to set custom source data.
+ $source = variable_get('tmgmt_test_source_data', array(
+ 'dummy' => array(
+ 'deep_nesting' => array(
+ '#text' => 'Text for job item with type @type and id @id.',
+ '#label' => 'Label for job item with type @type and id @id.',
+ ),
+ ),
+ ));
+
+ $variables = array(
+ '@type' => $job_item->item_type,
+ '@id' => $job_item->item_id,
+ );
+
+ $this->replacePlaceholders($source, $variables);
+
+ return $source;
+ }
+
+ /**
+ * Will replace placeholders in the #text offsets.
+ *
+ * @param array $data
+ * Data structures where to replace placeholders.
+ * @param $variables
+ * Key value pairs.
+ */
+ protected function replacePlaceholders(&$data, $variables) {
+ foreach (element_children($data) as $key) {
+ if (isset($data[$key]['#text'])) {
+ $data[$key]['#text'] = format_string($data[$key]['#text'], $variables);
+ }
+ else {
+ $this->replacePlaceholders($data[$key], $variables);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveTranslation(TMGMTJobItem $job_item) {
+ // Set a variable that can be checked later for a given job item.
+ variable_set('tmgmt_test_saved_translation_' . $job_item->item_type . '_' . $job_item->item_id, TRUE);
+ $job_item->accepted();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExistingLangCodes(TMGMTJobItem $job_item) {
+ return array_keys(language_list());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSourceLangCode(TMGMTJobItem $job_item) {
+ $source_languages = variable_get('tmgmt_test_source_languages', array());
+ if (isset($source_languages[$job_item->tjiid])) {
+ return $source_languages[$job_item->tjiid];
+ }
+
+ return 'en';
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.translator.inc b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.translator.inc
new file mode 100644
index 00000000..3621ee0a
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.plugin.translator.inc
@@ -0,0 +1,108 @@
+ 'en-us',
+ 'de' => 'de-ch',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCheckoutSettings(TMGMTJob $job) {
+ return $job->getTranslator()->getSetting('expose_settings');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function requestTranslation(TMGMTJob $job) {
+ // Add a debug message.
+ $job->addMessage('Test translator called.', array(), 'debug');
+
+ // Do something different based on the action, if defined.
+ $action = isset($job->settings['action']) ? $job->settings['action'] : '';
+ switch ($action) {
+ case 'submit':
+ $job->submitted('Test submit.');
+ break;
+
+ case 'reject':
+ $job->rejected('This is not supported.');
+ break;
+
+ case 'fail':
+ // Target not reachable.
+ $job->addMessage('Service not reachable.', array(), 'error');
+ break;
+
+ case 'translate':
+ default:
+ // The dummy translation prefixes strings with the target language.
+ $data = array_filter(tmgmt_flatten_data($job->getData()), '_tmgmt_filter_data');
+ $tdata = array();
+ foreach ($data as $key => $value) {
+ $tdata[$key]['#text'] = $job->target_language . '_' . $value['#text'];
+ }
+ $job->submitted('Test translation created.');
+ $job->addTranslatedData(tmgmt_unflatten_data($tdata));
+ break;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function canTranslate(TMGMTTranslator $translator, TMGMTJob $job) {
+ if (isset($job->settings['action']) && $job->settings['action'] == 'not_translatable') {
+ return FALSE;
+ }
+ return parent::canTranslate($translator, $job);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
+ $languages = drupal_map_assoc(array('en', 'de', 'es', 'it', 'zh-hans', 'gsw-berne'));
+ unset($languages[$source_language]);
+ return $languages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rejectDataItem(TMGMTJobItem $job_item, array $key, array $values = NULL) {
+ $key = '[' . implode('][', $key) . ']';
+ $job_item->addMessage('Rejected data item @key for job item @item in job @job.', array('@key' => $key, '@item' => $job_item->tjiid, '@job' => $job_item->tjid));
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rejectForm($form, &$form_state) {
+ return $form;
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.ui.translator.inc b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.ui.translator.inc
new file mode 100644
index 00000000..3c544133
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tests/tmgmt_test.ui.translator.inc
@@ -0,0 +1,66 @@
+ 'checkbox',
+ '#title' => t('Display settings'),
+ '#default_value' => TRUE,
+ );
+
+ $form['action'] = array(
+ '#type' => 'select',
+ '#title' => t('Default action'),
+ '#options' => array(
+ 'translate' => t('Translate'),
+ 'submit' => t('Submit'),
+ 'reject' => t('Reject'),
+ 'fail' => t('Fail'),
+ 'not_translatable' => t('Not translatable'),
+ ),
+ );
+ return parent::pluginSettingsForm($form, $form_state, $translator, $busy);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
+ if ($job->getTranslator()->getSetting('expose_settings')) {
+ $form['action'] = array(
+ '#type' => 'select',
+ '#title' => t('Action'),
+ '#options' => array(
+ 'translate' => t('Translate'),
+ 'submit' => t('Submit'),
+ 'reject' => t('Reject'),
+ 'fail' => t('Fail'),
+ 'not_translatable' => t('Not translatable'),
+ ),
+ '#default_value' => $job->getTranslator()->getSetting('action'),
+ );
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reviewDataItemElement($form, &$form_state, $data_item_key, $parent_key, array $data_item, TMGMTJobItem $item) {
+ $form['below'] = array(
+ '#markup' => t('Testing output of review data item element @key from the testing translator.', array('@key' => $data_item_key))
+ );
+
+ return $form;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tmgmt.api.php b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.api.php
new file mode 100644
index 00000000..e9e87df3
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.api.php
@@ -0,0 +1,272 @@
+ array(
+ 'label' => t('Test source'),
+ 'description' => t('Simple source for testing purposes.'),
+ 'controller class' => 'TMGMTTestSourcePluginController',
+ ),
+ );
+}
+
+/**
+ * Alter source plugins information.
+ *
+ * @param $info
+ * The defined source plugin information.
+ *
+ * @see hook_tmgmt_source_plugin_info()
+ */
+function hook_tmgmt_source_plugin_info_alter(&$info) {
+ $info['test_source']['description'] = t('Updated description');
+}
+
+/**
+ * Return a list of suggested sources for job items.
+ *
+ * @param array $items
+ * An array with TMGMTJobItem objects which must be checked for suggested
+ * translations.
+ * - TMGMTJobItem A JobItem to check for suggestions.
+ * - ...
+ * @param TMGMTJob $job
+ * The current translation job to check for additional translation items.
+ *
+ * @return array
+ * An array with all additional translation suggestions.
+ * - job_item: A TMGMTJobItem instance.
+ * - referenced: A string which indicates where this suggestion comes from.
+ * - from_job: The main TMGMTJob-ID which suggests this translation.
+ */
+function hook_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
+ return array(
+ array(
+ 'job_item' => tmgmt_job_item_create('entity', 'node', 0),
+ 'reason' => t('Referenced @type of field @label', array('@type' => 'entity', '@label' => 'label')),
+ 'from_item' => $items[1]->tjiid,
+ )
+ );
+}
+
+/**
+ * @} End of "addtogroup tmgmt_source".
+ */
+
+/**
+ * @addtogroup tmgmt_translator
+ * @{
+ */
+
+/**
+ * Provide information about translator plugins.
+ *
+ * @see TMGMTTestTranslatorPluginController
+ */
+function hook_tmgmt_translator_plugin_info() {
+ return array(
+ 'test_translator' => array(
+ 'label' => t('Test translator'),
+ 'description' => t('Simple translator for testing purposes.'),
+ 'plugin controller class' => 'TMGMTTestTranslatorPluginController',
+ 'ui controller class' => 'TMGMTTestTranslatorUIController',
+ 'default settings' => array(
+ 'expose_settings' => TRUE,
+ ),
+ // By default, a translator is automatically created with the default
+ // settings. Set auto create to FALSE to prevent this.
+ 'auto create' => TRUE,
+ // If the translator should provide remote languages mappings feature.
+ // It defaults to TRUE.
+ 'map remote languages' => FALSE,
+ // Flag defining if job settings are handled by plugin itself.
+ // Defaults to FALSE.
+ 'job settings custom handling' => FALSE,
+ ),
+ );
+}
+
+/**
+ * Alter information about translator plugins.
+ */
+function hook_tmgmt_translator_plugin_info_alter(&$info) {
+ $info['test_source']['description'] = t('Updated description');
+}
+
+/**
+ * @} End of "addtogroup tmgmt_translator".
+ */
+
+/**
+ * @defgroup tmgmt_job Translation Jobs
+ *
+ * A single task to translate something into a given language using a @link
+ * translator translator @endlink.
+ *
+ * Attached to these jobs are job items, which specify which @link source
+ * sources @endlink are to be translated.
+ *
+ * To create a new translation job, first create a job and then assign items to
+ * each. Each item needs to specify the source plugin that should be used
+ * and the type and id, which the source plugin then uses to identify it later
+ * on.
+ *
+ * @code
+ * $job = tmgmt_job_create('en', $target_language);
+ *
+ * for ($i = 1; $i < 3; $i++) {
+ * $job->addItem('test_source', 'test', $i);
+ * }
+ * @endcode
+ *
+ * Once a job has been created, it can be assigned to a translator plugin, which
+ * is the service that is going to do the translation.
+ *
+ * @code
+ * $job->translator = 'test_translator';
+ * // Translator specific settings.
+ * $job->settings = array(
+ * 'priority' => 5,
+ * );
+ * $job->save();
+ *
+ * // Get the translator plugin and request a translation.
+ * if ($job->isTranslatable()) {
+ * $job->requestTranslation();
+ * }
+ * @endcode
+ *
+ * The translation plugin will then request the text from the source plugin.
+ * Depending on the plugin, the text might be sent to an external service
+ * or assign it to a local user or team of users. At some point, a translation
+ * will be returned and saved in the job items.
+ *
+ * The translation can now be reviewed, accepted and the source plugins be told
+ * to save the translation.
+ *
+ * @code
+ * $job->accepted('Optional message');
+ * @endcode
+ */
+
+/**
+ * @defgroup tmgmt_translator Translators
+ *
+ * A translator plugin integrates a translation service.
+ *
+ * To define a translator, hook_tmgmt_translator_plugin_info() needs to be
+ * implemented and a controller class (specified in the info) created.
+ *
+ * A translator plugin is then responsible for sending out a translation job and
+ * storing the translated texts back into the job and marking it as needs review
+ * once it's finished.
+ *
+ * TBD.
+ */
+
+/**
+ * @defgroup tmgmt_source Translation source
+ *
+ * A source plugin represents translatable elements on a site.
+ *
+ * For example nodes, but also plain strings, menu items, other entities and so
+ * on.
+ *
+ * To define a source, hook_tmgmt_source_plugin_info() needs to be implemented
+ * and a controller class (specified in the info) created.
+ *
+ * A source has three separate tasks.
+ *
+ * - Allows to create a new @link job translation job @endlink and assign job
+ * items to itself.
+ * - Extract the translatable text into a nested array when
+ * requested to do in their implementation of
+ * TMGMTSourcePluginControllerInterface::getData().
+ * - Save the accepted translations returned by the translation plugin in their
+ * sources in their implementation of
+ * TMGMTSourcePluginControllerInterface::saveTranslation().
+ */
+
+/**
+ * @defgroup tmgmt_remote_languages_mapping Remote languages mapping
+ *
+ * Logic to deal with different language codes at client and server that stand
+ * for the same language.
+ *
+ * Each tmgmt plugin is expected to support this feature. However for those
+ * plugins where such feature has no use there is a plugin setting
+ * "map remote languages" which can be set to FALSE.
+ *
+ * @section mappings_info Mappings info
+ *
+ * There are several methods defined by
+ * TMGMTTranslatorPluginControllerInterface and implemented in
+ * TMGMTDefaultTranslatorPluginController that deal with mappings info.
+ *
+ * - getRemoteLanguagesMappings() - provides pairs of local_code => remote_code.
+ * - mapToRemoteLanguage() & mapToLocalLanguage() - helpers to map local/remote.
+ * Note that methods with same names and functionality are provided by the
+ * TMGMTTranslator entity. These are convenience methods.
+ *
+ * The above methods should not need reimplementation unless special logic is
+ * needed. However following methods provide only the fallback behaviour and
+ * therefore it is recommended that each plugin provides its specific
+ * implementation.
+ *
+ * - getDefaultRemoteLanguagesMappings() - we might know some mapping pairs
+ * prior to configuring a plugin, this is the place where we can define these
+ * mappings. The default implementation returns an empty array.
+ * - getSupportedRemoteLanguages() - gets array of language codes in lang_code =>
+ * lang_code format. It says with what languages the remote system can deal
+ * with. These codes are in the remote format.
+ *
+ * @section mapping_remote_to_local Mapping remote to local
+ *
+ * Mapping remote to local language codes is done when determining the
+ * language capabilities of the remote system. All following logic should then
+ * solely work with local language codes. There are two methods defined by
+ * the TMGMTTranslatorPluginControllerInterface interface. To do the mapping
+ * a plugin must implement getSupportedTargetLanguages().
+ *
+ * - getSupportedTargetLanguages() - should return local language codes. So
+ * within this method the mapping needs to be executed.
+ * - getSupportedLanguagePairs() - gets language pairs for which translations
+ * can be done. The language codes must be in local form. The default
+ * implementation uses getSupportedTargetLanguages() so mapping occur. However
+ * this approach is not effective and therefore each plugin should provide
+ * its specific implementation with regard to performance.
+ *
+ * @section mapping_local_to_remote Mapping local to remote
+ *
+ * Mapping of local to remote language codes is done upon translation job
+ * request in the TMGMTTranslatorPluginControllerInterface::requestTranslation()
+ * plugin implementation.
+ */
+
+/**
+ * @defgroup tmgmt_ui_cart Translation cart
+ *
+ * The translation cart can collect multiple source items of different types
+ * which are meant for translation into a list. The list then provides
+ * functionality to request translation of the items into multiple target
+ * languages.
+ *
+ * Each source can easily plug into the cart system utilising the
+ * tmgmt_ui_add_cart_form() on either the source overview page as well as the
+ * translate tab.
+ */
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tmgmt.info b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.info
new file mode 100644
index 00000000..2544a8bc
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.info
@@ -0,0 +1,58 @@
+name = Translation Management Core
+description = Core functionality for the Translation Management Suite.
+package = Translation Management
+core = 7.x
+
+dependencies[] = entity
+dependencies[] = locale
+dependencies[] = views
+simplytest_dependencies[] = tmgmt_demo
+
+files[] = includes/tmgmt.exception.inc
+files[] = controller/tmgmt.controller.job.inc
+files[] = controller/tmgmt.controller.job_item.inc
+files[] = controller/tmgmt.controller.remote.inc
+files[] = controller/tmgmt.controller.translator.inc
+files[] = entity/tmgmt.entity.job.inc
+files[] = entity/tmgmt.entity.job_item.inc
+files[] = entity/tmgmt.entity.message.inc
+files[] = entity/tmgmt.entity.remote.inc
+files[] = entity/tmgmt.entity.translator.inc
+files[] = plugin/tmgmt.plugin.base.inc
+files[] = plugin/tmgmt.plugin.interface.base.inc
+files[] = plugin/tmgmt.plugin.interface.reject.inc
+files[] = plugin/tmgmt.plugin.interface.source.inc
+files[] = plugin/tmgmt.plugin.interface.translator.inc
+files[] = plugin/tmgmt.plugin.source.inc
+files[] = plugin/tmgmt.plugin.translator.inc
+files[] = plugin/tmgmt.ui.interface.source.inc
+files[] = plugin/tmgmt.ui.interface.translator.inc
+files[] = plugin/tmgmt.ui.source.inc
+files[] = plugin/tmgmt.ui.translator.inc
+files[] = includes/tmgmt.info.inc
+files[] = tests/tmgmt.base.test
+files[] = tests/tmgmt.base.entity.test
+files[] = tests/tmgmt.crud.test
+files[] = tests/tmgmt.plugin.test
+files[] = tests/tmgmt.helper.test
+files[] = tests/tmgmt.upgrade.alpha1.test
+
+; Views integration and handlers
+files[] = views/tmgmt.views.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_entity_label.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_job_item_type.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_translator.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_job_operations.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_progress.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_wordcount.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_message_message.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_job_item_operations.inc
+files[] = views/handlers/tmgmt_handler_field_tmgmt_job_item_count.inc
+files[] = views/plugins/tmgmt_views_job_access.inc
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tmgmt.install b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.install
new file mode 100644
index 00000000..5ebc5220
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.install
@@ -0,0 +1,877 @@
+ 'A translation job represents a translation order that can be assigned to a translator.',
+ 'fields' => array(
+ 'tjid' => array(
+ 'description' => 'The identifier of the translation job.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'source_language' => array(
+ 'description' => 'The source language of the data.',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ),
+ 'target_language' => array(
+ 'description' => 'The language the data should be translated to.',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ),
+ 'state' => array(
+ 'description' => 'The state of the translation job.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ),
+ 'created' => array(
+ 'description' => 'Created timestamp.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'changed' => array(
+ 'description' => 'Changed timestamp.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'translator' => array(
+ 'description' => 'Machine name of the translator.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ ),
+ 'settings' => array(
+ 'description' => 'Translator specific configuration and context for this job.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'reference' => array(
+ 'description' => 'Remote identifier of this translation job.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ),
+ 'uid' => array(
+ 'description' => 'uid of the job creator',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'label' => array(
+ 'description' => 'Optional user provided label of the job.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ),
+ ),
+ 'primary key' => array('tjid'),
+ 'indexes' => array(
+ 'state' => array('state'),
+ 'reference' => array('reference'),
+ ),
+ );
+
+ $schema['tmgmt_remote'] = array(
+ 'description' => 'TMGMT job remote mapping.',
+ 'fields' => array(
+ 'trid' => array(
+ 'description' => 'The primary key.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'tjid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => '{tmgmt_job}.tjid foreign key',
+ ),
+ 'tjiid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => '{tmgmt_job_item}.tjjid foreign key',
+ ),
+ 'data_item_key' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Translation job data item key.',
+ ),
+ 'remote_identifier_1' => array(
+ 'type' => 'varchar',
+ 'length' => 127,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ),
+ 'remote_identifier_2' => array(
+ 'type' => 'varchar',
+ 'length' => 127,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ),
+ 'remote_identifier_3' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ),
+ 'remote_url' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Remote job url.',
+ ),
+ 'word_count' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Word count info provided by the remote service.',
+ ),
+ 'amount' => array(
+ 'type' => 'int',
+ 'size' => 'big',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Amount charged for the remote translation job.',
+ ),
+ 'currency' => array(
+ 'type' => 'char',
+ 'length' => 3,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Amount charged currency.',
+ ),
+ 'remote_data' => array(
+ 'description' => 'Custom remote data.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('trid'),
+ 'indexes' => array(
+ 'tjid' => array('tjid'),
+ 'tjiid' => array('tjiid'),
+ 'remote_identifiers' => array('remote_identifier_1', 'remote_identifier_2'),
+ ),
+ );
+
+ $schema['tmgmt_job_item'] = array(
+ 'description' => 'A job item connects a source to a translation job.',
+ 'fields' => array(
+ 'tjiid' => array(
+ 'description' => 'The identifier of the translation job item.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'tjid' => array(
+ 'description' => 'The identifier of the translation job.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'unsigned' => TRUE,
+ ),
+ 'plugin' => array(
+ 'description' => 'Indicates the plugin which provides this item.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'item_type' => array(
+ 'description' => 'The type of the item, e.g. the entity type.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ ),
+ 'item_id' => array(
+ 'description' => 'The unique id (within the given item type) of the item.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'state' => array(
+ 'description' => 'The state of the translation job item.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'description' => 'Can be used by the source plugin to store the data if it can not be retrieved anymore later on.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'changed' => array(
+ 'description' => 'Changed timestamp.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'count_pending' => array(
+ 'description' => 'Counter for all pending data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'count_translated' => array(
+ 'description' => 'Counter for all translated data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'count_accepted' => array(
+ 'description' => 'Counter for all accepted data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'count_reviewed' => array(
+ 'description' => 'Counter for all reviewed data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'word_count' => array(
+ 'description' => 'Word count of all texts contained in this job item.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('tjiid'),
+ 'indexes' => array(
+ 'job_id' => array('tjid'),
+ ),
+ 'foreign keys' => array(
+ 'job' => array(
+ 'table' => 'tmgmt_job',
+ 'columns' => array('tjid'),
+ ),
+ ),
+ );
+
+ $schema['tmgmt_translator'] = array(
+ 'description' => 'A translator is a combination of a translator type and type specific configuration.',
+ 'fields' => array(
+ 'tid' => array(
+ 'description' => 'The identifier of the translator.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'description' => 'Machine name identifier of the translator.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'label' => array(
+ 'description' => 'Label of the translator.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'description' => array(
+ 'description' => 'Description of the translator.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ ),
+ 'plugin' => array(
+ 'description' => 'Name of the translator service plugin.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'settings' => array(
+ 'description' => 'Translator specific settings.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'weight' => array(
+ 'description' => 'The weight of the translator.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'The exportable status of the entity.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ // Set the default to ENTITY_CUSTOM without using the constant as it is
+ // not safe to use it at this point.
+ 'default' => 0x01,
+ 'size' => 'tiny',
+ ),
+ 'module' => array(
+ 'description' => 'The name of the providing module if the entity has been defined in code.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'primary key' => array('tid'),
+ 'unique keys' => array(
+ 'name' => array('name'),
+ ),
+ );
+
+ $schema['tmgmt_message'] = array(
+ 'description' => 'A log message can be used to store events that affect a job.',
+ 'fields' => array(
+ 'mid' => array(
+ 'description' => 'The identifier of the message.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'tjid' => array(
+ 'description' => 'The identifier of the translation job that the message belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'tjiid' => array(
+ 'description' => 'The identifier of the translation job item that the message belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ ),
+ 'uid' => array(
+ 'description' => 'The identifier of the user who performed the action.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'default' => 0,
+ ),
+ 'message' => array(
+ 'description' => 'The language into the data should be translated.',
+ 'type' => 'text',
+ 'size' => 'big',
+ ),
+ 'variables' => array(
+ 'description' => 'The variables of the message as expected by t().',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'created' => array(
+ 'description' => 'Created timestamp.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'type' => array(
+ 'description' => 'Type of the message (debug, notice, warning or error)',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array('mid'),
+ 'indexes' => array(
+ 'tjid' => array('tjid'),
+ 'tjiid' => array('tjiid'),
+ ),
+ );
+
+ // Clone the schema for our cache table from Drupal core.
+ $schema['cache_tmgmt'] = drupal_get_schema_unprocessed('system', 'cache');
+
+ // Clone the schema for the entity cache module if it is enabled.
+ if (module_exists('entitycache')) {
+ $schema['cache_entity_tmgmt_translator'] = drupal_get_schema_unprocessed('system', 'cache');
+ }
+
+ return $schema;
+}
+
+/**
+ * Merge the content of the 'translation' field into the 'data' field.
+ */
+function tmgmt_update_7000(&$sandbox) {
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['max'] = db_query('SELECT COUNT(*) FROM {tmgmt_job_item}')->fetchField();
+ }
+
+ $results = db_select('tmgmt_job_item', 'tji')
+ ->fields('tji', array('tjiid', 'data', 'translation'))
+ ->range($sandbox['progress'], 50)
+ ->orderBy('tjiid', 'ASC')
+ ->execute();
+
+ foreach ($results as $item) {
+ $data = unserialize($item->data);
+ if (!empty($item->translation)) {
+ foreach (tmgmt_flatten_data(unserialize($item->translation)) as $key => $translation) {
+ if (!empty($item->data)) {
+ $key = explode('][', $key);
+ drupal_array_set_nested_value($data, array_merge($key, array('#translation')), $translation);
+ }
+ }
+ db_update('tmgmt_job_item')
+ ->condition('tjiid', $item->tjiid)
+ ->fields(array('data' => serialize($data)))
+ ->execute();
+ }
+
+ $sandbox['progress']++;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+}
+
+/**
+ * Remove the 'translation' field from the job item entity.
+ */
+function tmgmt_update_7001() {
+ db_drop_field('tmgmt_job_item', 'translation');
+}
+
+/**
+ * Add counter columns to {tmgmt_job_item}.
+ */
+function tmgmt_update_7002() {
+ // Defining schema of additional fields.
+ $schema = array(
+ 'count_pending' => array(
+ 'description' => 'Counter for all pending data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'count_translated' => array(
+ 'description' => 'Counter for all translated data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'count_approved' => array(
+ 'description' => 'Counter for all approved data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ );
+ // Add aditional fields to db.
+ foreach ($schema as $field => $spec) {
+ db_add_field('tmgmt_job_item', $field, $spec);
+ }
+}
+
+/**
+ * Data parsing helper function for tmgmt_update_7003().
+ *
+ * Copied from TMGMTJobItemController::count().
+ *
+ * @param $item
+ * The current data item.
+ * @param $entity
+ * The job item the count should be calculated.
+ */
+ function _tmgmt_data_count_7003(&$item, $entity) {
+ if (!empty($item['#text'])) {
+ if (_tmgmt_filter_data($item)) {
+
+ // Set default states if no state is set.
+ if (!isset($item['#status'])) {
+ // Translation is present.
+ if (!empty($item['#translation'])) {
+ $item['#status'] = TMGMT_DATA_ITEM_STATE_TRANSLATED;
+ }
+ // No translation present.
+ else {
+ $item['#status'] = TMGMT_DATA_ITEM_STATE_PENDING;
+ }
+ }
+ switch ($item['#status']) {
+ case 1:
+ $entity->count_approved++;
+ break;
+ case 2:
+ $entity->count_translated++;
+ break;
+ default:
+ $entity->count_pending++;
+ break;
+ }
+ }
+ }
+ else {
+ foreach (element_children($item) as $key) {
+ _tmgmt_data_count_7003($item[$key], $entity);
+ }
+ }
+}
+
+/**
+ * Set counters for existing job items.
+ */
+function tmgmt_update_7003(&$sandbox) {
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['last_tjiid'] = 0;
+ $sandbox['max'] = db_query('SELECT COUNT(tjiid) FROM {tmgmt_job_item}')->fetchField();
+ }
+ $result = db_query('SELECT tjiid, data FROM {tmgmt_job_item} WHERE tjiid > :last_tjiid ORDER BY tjiid LIMIT 10', array( ':last_tjiid' => $sandbox['last_tjiid']));
+ foreach ($result as $row) {
+ // Unseralize data, count it and then save the counters.
+ if (!empty($row->data)) {
+ $fake_job_item = (object)array(
+ 'count_approved' => 0,
+ 'count_translated' => 0,
+ 'count_pending' => 0,
+ );
+ $data = unserialize($row->data);
+ _tmgmt_data_count_7003($data, $fake_job_item);
+ $fields = (array)$fake_job_item + array(
+ 'data' => serialize($data),
+ );
+ db_update('tmgmt_job_item')
+ ->condition('tjiid', $row->tjiid)
+ ->fields($fields)
+ ->execute();
+ }
+ $sandbox['progress']++;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+ if ($row) {
+ $sandbox['last_tjiid'] = $row->tjiid;
+ }
+}
+
+/**
+ * Replace the [#translation][#finished] attribute with [#status].
+ */
+function tmgmt_update_7004(&$sandbox) {
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['last_tjiid'] = 0;
+ $sandbox['max'] = db_query('SELECT COUNT(tjiid) FROM {tmgmt_job_item}')->fetchField();
+ }
+ $result = db_query('SELECT tjiid, data FROM {tmgmt_job_item} WHERE tjiid > :last_tjiid ORDER BY tjiid LIMIT 10', array(':last_tjiid' => $sandbox['last_tjiid']));
+ foreach ($result as $row) {
+ if (!empty($row->data)) {
+ $data = unserialize($row->data);
+ $flattened_data = array_filter(tmgmt_flatten_data($data), '_tmgmt_filter_data');
+ // Loop over data, find finished translations, remove the flag and set
+ // the status instead.
+ foreach ($flattened_data as $key => $values) {
+ if (!empty($values['#translation']['#finished'])) {
+ $finished = $values['#translation']['#finished'];
+ unset($values['#translation']['#finished']);
+ drupal_array_set_nested_value($data, array_merge($key, array('#translation')), $values['#translation']);
+ if ($finished && (empty($values['#status']) || $values['#status'] == TMGMT_DATA_ITEM_STATE_PENDING)) {
+ drupal_array_set_nested_value($data, array_merge($key, array('#status')), TMGMT_DATA_ITEM_STATE_TRANSLATED);
+ }
+ // Save the updated data structure.
+ db_update('tmgmt_job_item')
+ ->condition('tjiid', $row->tjiid)
+ ->fields(array('data' => serialize($data)))
+ ->execute();
+ }
+ }
+ }
+ $sandbox['progress']++;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+ if ($row) {
+ $sandbox['last_tjiid'] = $row->tjiid;
+ }
+}
+
+/**
+ * Add word count column to {tmgmt_job_item}.
+ */
+function tmgmt_update_7005() {
+ if (!db_field_exists('tmgmt_job_item', 'word_count')) {
+ $spec = array(
+ 'description' => 'Word count of all texts contained in this job item.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ );
+ db_add_field('tmgmt_job_item', 'word_count', $spec);
+ }
+}
+
+/**
+ * Data parsing helper function for tmgmt_update_7006().
+ *
+ * Copied from TMGMTJobItemController::count().
+ *
+ * @param $item
+ * The current data item.
+ * @param $entity
+ * The job item the count should be calculated.
+ */
+ function _tmgmt_data_count_7006($item, $entity) {
+ if (!empty($item['#text'])) {
+ if (_tmgmt_filter_data($item)) {
+ module_load_include('module', 'tmgmt');
+ // Count words of the data item.
+ $entity->word_count += tmgmt_word_count($item['#text']);
+ }
+ }
+ else {
+ foreach (element_children($item) as $key) {
+ _tmgmt_data_count_7006($item[$key], $entity);
+ }
+ }
+}
+
+/**
+ * Set word count for existing job items.
+ */
+function tmgmt_update_7006(&$sandbox) {
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['last_tjiid'] = 0;
+ $sandbox['max'] = db_query('SELECT COUNT(tjiid) FROM {tmgmt_job_item}')->fetchField();
+ }
+
+ $result = db_query('SELECT tjiid, data FROM {tmgmt_job_item} WHERE tjiid > :last_tjiid ORDER BY tjiid LIMIT 10', array(':last_tjiid' => $sandbox['last_tjiid']));
+ foreach ($result as $row) {
+ // Unseralize data, count it and then save the counters.
+ if (!empty($row->data)) {
+ $fake_job_item = (object)array(
+ 'word_count' => 0,
+ );
+ _tmgmt_data_count_7006(unserialize($row->data), $fake_job_item);
+ db_update('tmgmt_job_item')
+ ->condition('tjiid', $row->tjiid)
+ ->fields((array)$fake_job_item)
+ ->execute();
+ }
+ $sandbox['progress']++;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+ if ($row) {
+ $sandbox['last_tjiid'] = $row->tjiid;
+ }
+}
+
+/**
+ * Removing the tmgmt_auto_accept variable.
+ */
+function tmgmt_update_7007() {
+ variable_del('tmgmt_auto_accept');
+}
+
+/**
+ * Introduce the reviewed counter and rename approved.
+ */
+function tmgmt_update_7008() {
+ // Rename approved to reviewed to simplify the upgrade path.
+ db_change_field('tmgmt_job_item', 'count_approved', 'count_reviewed', array(
+ 'description' => 'Counter for all reviewed data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ));
+ // Add the accepted counter.
+ db_add_field('tmgmt_job_item', 'count_accepted', array(
+ 'description' => 'Counter for all accepted data items.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ));
+}
+
+/**
+ * Update data item status of accepted job items to accepted.
+ */
+function tmgmt_update_7009() {
+ // First set count_accepted to the value of count_reviewed.
+ db_update('tmgmt_job_item')
+ ->expression('count_accepted', 'count_reviewed + count_pending + count_translated')
+ ->condition('state', TMGMT_JOB_ITEM_STATE_ACCEPTED)
+ ->execute();
+ // Then set count_reviewed to 0.
+ db_update('tmgmt_job_item')
+ ->fields(array(
+ 'count_reviewed' => 0,
+ 'count_translated' => 0,
+ 'count_pending' => 0,
+ ))
+ ->condition('state', TMGMT_JOB_ITEM_STATE_ACCEPTED)
+ ->execute();
+}
+
+/**
+ * Create tmgmt_remote table to provide generic functionality for remote job
+ * mappings.
+ */
+function tmgmt_update_7010() {
+ db_create_table('tmgmt_remote', array(
+ 'description' => 'TMGMT job remote mapping.',
+ 'fields' => array(
+ 'trid' => array(
+ 'description' => 'The primary key.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ),
+ 'tjid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => '{tmgmt_job}.tjid foreign key',
+ ),
+ 'tjiid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => '{tmgmt_job_item}.tjjid foreign key',
+ ),
+ 'data_item_key' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Translation job data item key.',
+ ),
+ 'remote_identifier_1' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ),
+ 'remote_identifier_2' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ),
+ 'remote_identifier_3' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ),
+ 'remote_url' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Remote job url.',
+ ),
+ 'word_count' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Word count info provided by the remote service.',
+ ),
+ 'remote_data' => array(
+ 'description' => 'Custom remote data.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('trid'),
+ 'indexes' => array(
+ 'tjid' => array('tjid'),
+ 'tjiid' => array('tjiid'),
+ 'remote_identifiers' => array('remote_identifier_1', 'remote_identifier_2', 'remote_identifier_3'),
+ ),
+ ));
+}
+
+/**
+ * Add uid field for {tmgmt_message} table.
+ */
+function tmgmt_update_7011() {
+ db_add_field('tmgmt_message', 'uid', array(
+ 'description' => 'The identifier of the user who performed the action.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ));
+}
+
+/**
+ * Shortens remote_identifier_1/2 fields and recreates index for remote
+ * identifier fields to include only the first two.
+ */
+function tmgmt_update_7012() {
+ db_drop_index('tmgmt_remote', 'remote_identifiers');
+ db_change_field('tmgmt_remote', 'remote_identifier_1', 'remote_identifier_1', array(
+ 'type' => 'varchar',
+ 'length' => 127,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ));
+ db_change_field('tmgmt_remote', 'remote_identifier_2', 'remote_identifier_2', array(
+ 'type' => 'varchar',
+ 'length' => 127,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Custom remote identifier data.',
+ ));
+ db_add_index('tmgmt_remote', 'remote_identifiers', array('remote_identifier_1', 'remote_identifier_2'));
+}
+
+/**
+ * Shortens reference and label fields in {tmgmt_job} to length 255.
+ *
+ * Avoids issues with InnoDB and UTF-8 key limits.
+ */
+function tmgmt_update_7013() {
+ db_drop_index('tmgmt_job', 'reference');
+ db_change_field('tmgmt_job', 'reference', 'reference', array(
+ 'description' => 'Remote identifier of this translation job.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ));
+ db_change_field('tmgmt_job', 'label', 'label', array(
+ 'description' => 'Optional user provided label of the job.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ ));
+ db_add_index('tmgmt_job', 'reference', array('reference'));
+}
+
+/**
+ * Add amount and currency fields for {tmgmt_remote} table.
+ */
+function tmgmt_update_7014() {
+ db_add_field('tmgmt_remote', 'amount', array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Amount charged for the remote translation job.',
+ ));
+ db_add_field('tmgmt_remote', 'currency', array(
+ 'type' => 'char',
+ 'length' => 3,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Amount charged currency.',
+ ));
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tmgmt.module b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.module
new file mode 100644
index 00000000..9df8dc1e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.module
@@ -0,0 +1,1550 @@
+ t('Translation Management Job'),
+ 'module' => 'tmgmt',
+ 'controller class' => 'TMGMTJobController',
+ 'metadata controller class' => 'TMGMTJobMetadataController',
+ 'views controller class' => 'TMGMTJobViewsController',
+ 'entity class' => 'TMGMTJob',
+ 'base table' => 'tmgmt_job',
+ 'uri callback' => 'entity_class_uri',
+ 'label callback' => 'entity_class_label',
+ 'access callback' => 'tmgmt_job_access',
+ 'entity keys' => array(
+ 'id' => 'tjid',
+ ),
+ );
+ $info['tmgmt_job_item'] = array(
+ 'label' => t('Translation Management Job Item'),
+ 'module' => 'tmgmt',
+ 'controller class' => 'TMGMTJobItemController',
+ 'metadata controller class' => 'TMGMTJobItemMetadataController',
+ 'views controller class' => 'TMGMTJobItemViewsController',
+ 'entity class' => 'TMGMTJobItem',
+ 'base table' => 'tmgmt_job_item',
+ 'label callback' => 'entity_class_label',
+ 'uri callback' => 'entity_class_uri',
+ 'access callback' => 'tmgmt_job_item_access',
+ 'entity keys' => array(
+ 'id' => 'tjiid',
+ ),
+ );
+ $info['tmgmt_message'] = array(
+ 'label' => t('Translation Management Message'),
+ 'module' => 'tmgmt',
+ 'controller class' => 'EntityAPIController',
+ 'metadata controller class' => 'TMGMTMessageMetadataController',
+ 'views controller class' => 'TMGMTMessageViewsController',
+ 'entity class' => 'TMGMTMessage',
+ 'base table' => 'tmgmt_message',
+ 'label callback' => 'entity_class_label',
+ 'access callback' => 'tmgmt_message_access',
+ 'entity keys' => array(
+ 'id' => 'mid',
+ ),
+ );
+ $info['tmgmt_translator'] = array(
+ 'label' => t('Translation Management Translator'),
+ 'module' => 'tmgmt',
+ 'controller class' => 'TMGMTTranslatorController',
+ 'metadata controller class' => 'TMGMTTranslatorMetadataController',
+ 'views controller class' => 'EntityDefaultViewsController',
+ 'entity class' => 'TMGMTTranslator',
+ 'base table' => 'tmgmt_translator',
+ 'exportable' => TRUE,
+ 'access callback' => 'tmgmt_translator_access',
+ 'entity keys' => array(
+ 'id' => 'tid',
+ 'name' => 'name',
+ 'label' => 'label',
+ ),
+ );
+ // Make use of the entity cache module if it is enabled.
+ if (module_exists('entitycache')) {
+ $info['tmgmt_translator']['entity cache'] = TRUE;
+ $info['tmgmt_translator']['field cache'] = FALSE;
+ }
+
+ $info['tmgmt_remote'] = array(
+ 'label' => t('Remote job mapping'),
+ 'module' => 'tmgmt',
+ 'controller class' => 'TMGMTRemoteController',
+ 'entity class' => 'TMGMTRemote',
+ 'base table' => 'tmgmt_remote',
+ 'uri callback' => 'entity_class_uri',
+ 'label callback' => 'entity_class_label',
+ 'access callback' => 'tmgmt_remote_access',
+ 'entity keys' => array(
+ 'id' => 'trid',
+ ),
+ );
+ return $info;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function tmgmt_permission() {
+ $perms['administer tmgmt'] = array(
+ 'title' => t('Administer translation management'),
+ );
+ $perms['create translation jobs'] = array(
+ 'title' => t('Create translation jobs'),
+ );
+ $perms['submit translation jobs'] = array(
+ 'title' => t('Submit translation jobs'),
+ );
+ $perms['accept translation jobs'] = array(
+ 'title' => t('Accept and reject translation jobs'),
+ );
+ return $perms;
+}
+
+/**
+ * Implements hook_modules_installed().
+ */
+function tmgmt_modules_installed($modules) {
+ foreach (tmgmt_translator_plugin_info() as $key => $info) {
+ // Check if this translator plugin has been added by one of the recently
+ // installed modules and doesn't prevent auto creation.
+ if ((!isset($info['auto create']) || $info['auto create'] == TRUE) && in_array($info['module'], $modules)) {
+ tmgmt_translator_auto_create($key);
+ }
+ }
+}
+
+/**
+ * Implements hook_flush_caches().
+ */
+function tmgmt_flush_caches() {
+ return array('cache_tmgmt');
+}
+
+/**
+ * Implements hook_cron().
+ */
+function tmgmt_cron() {
+ $offset = variable_get('tmgmt_purge_finished', '_never');
+ if ($offset != '_never') {
+ // Delete all finished translation jobs that haven't been changed for a
+ // time span longer than the given offset.
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_job')
+ ->propertyCondition('state', TMGMT_JOB_STATE_FINISHED)
+ ->propertyCondition('changed', REQUEST_TIME - $offset, '<=')
+ ->execute();
+ if (!empty($result['tmgmt_job'])) {
+ $controller = entity_get_controller('tmgmt_job');
+ // Since the entity controller handles the deletion of the attached
+ // entities (messages, job items) we just need to invoke it directly.
+ $controller->delete(array_keys($result['tmgmt_job']));
+ }
+ }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function tmgmt_views_api() {
+ return array(
+ 'api' => 3.0,
+ 'path' => drupal_get_path('module', 'tmgmt') . '/views',
+ );
+}
+
+/**
+ * Returns an array of languages that are available for translation.
+ *
+ * @return array
+ * An array of languages in ISO format.
+ */
+function tmgmt_available_languages($exclude = array()) {
+ $languages = entity_metadata_language_list();
+ // Remove LANGUAGE_NONE and the language in $exclude from the list of
+ // available languages and then apply a filter that only leaves the supported
+ // target languages on the list.
+ unset($languages[LANGUAGE_NONE]);
+ foreach ($exclude as $item) {
+ unset($languages[$item]);
+ }
+ return $languages;
+}
+
+/**
+ * Returns the label of a language.
+ *
+ * @param $language
+ * A language in ISO format.
+ * @return string
+ * The label of the language or an empty string if the language or its label
+ * are not defined.
+ */
+function tmgmt_language_label($language) {
+ $languages = entity_metadata_language_list();
+ if (!empty($languages[$language])) {
+ return $languages[$language];
+ }
+ return '';
+}
+
+/**
+ * @addtogroup tmgmt_job
+ * @{
+ */
+/**
+ * Loads a translation job.
+ *
+ * @param int $tjid
+ * Translation job id.
+ *
+ * @return TMGMTJob
+ * Loaded translation job entity.
+ */
+function tmgmt_job_load($tjid) {
+ $jobs = tmgmt_job_load_multiple(array($tjid), array());
+ return $jobs ? reset($jobs) : FALSE;
+}
+
+/**
+ * Loads translation jobs.
+ */
+function tmgmt_job_load_multiple(array $tjids = array(), $conditions = array()) {
+ return entity_load('tmgmt_job', $tjids, $conditions);
+}
+
+/**
+ * Loads active job entities that have a job item with the identifiers.
+ *
+ * @param $plugin
+ * The source plugin.
+ * @param $item_type
+ * The source item type.
+ * @param $item_id
+ * The source item id.
+ * @param string $source_language
+ * The source language of the item.
+ *
+ * @return array
+ * An array of job entities.
+ */
+function tmgmt_job_item_load_latest($plugin, $item_type, $item_id, $source_language) {
+ $query = db_select('tmgmt_job_item', 'tji');
+ $query->innerJoin('tmgmt_job', 'tj', 'tj.tjid = tji.tjid');
+ $result = $query->condition('tj.source_language', $source_language)
+ // Only query for jobs that are currently active.
+ ->condition('tj.state', array(TMGMT_JOB_STATE_UNPROCESSED, TMGMT_JOB_STATE_ACTIVE))
+ // And only query for job items that are not yet finished.
+ ->condition('tji.state', TMGMT_JOB_ITEM_STATE_ACCEPTED, '<>')
+ ->condition('tji.plugin', $plugin)
+ ->condition('tji.item_type', $item_type)
+ ->condition('tji.item_id', $item_id)
+ ->fields('tji', array('tjiid'))
+ ->fields('tj', array('target_language'))
+ ->orderBy('tji.changed', 'DESC')
+ ->groupBy('tj.target_language')
+ ->groupBy('tji.tjiid')
+ ->groupBy('tji.changed')
+ ->execute();
+ if ($items = $result->fetchAllKeyed()) {
+ $return = array();
+ foreach (tmgmt_job_item_load_multiple(array_keys($items)) as $key => $item) {
+ $return[$items[$key]] = $item;
+ }
+ return $return;
+ }
+ return FALSE;
+}
+
+/**
+ * Loads all latest job entities that have a job item with the identifiers.
+ *
+ * @param $plugin
+ * The source plugin.
+ * @param $item_type
+ * The source item type.
+ * @param $item_id
+ * The source item id.
+ * @param string $source_language
+ * The source language of the item.
+ *
+ * @return array
+ * An array of job entities.
+ */
+function tmgmt_job_item_load_all_latest($plugin, $item_type, $item_id, $source_language) {
+ $query = db_select('tmgmt_job_item', 'tji');
+ $query->innerJoin('tmgmt_job', 'tj', 'tj.tjid = tji.tjid');
+ $result = $query->condition('tj.source_language', $source_language)
+ ->condition('tji.state', TMGMT_JOB_ITEM_STATE_ACCEPTED, '<>')
+ ->condition('tji.plugin', $plugin)
+ ->condition('tji.item_type', $item_type)
+ ->condition('tji.item_id', $item_id)
+ ->fields('tji', array('tjiid'))
+ ->fields('tj', array('target_language'))
+ ->orderBy('tji.changed', 'DESC')
+ ->groupBy('tj.target_language')
+ ->groupBy('tji.tjiid')
+ ->execute();
+ if ($items = $result->fetchAllKeyed()) {
+ $return = array();
+ foreach (tmgmt_job_item_load_multiple(array_keys($items)) as $key => $item) {
+ $return[$items[$key]] = $item;
+ }
+ return $return;
+ }
+ return FALSE;
+}
+
+/**
+ * Returns a job which matches the requested source- and target language by
+ * user. If no job exists, a new job object will be created.
+ *
+ * @param $source_language
+ * The source language from which should be translated.
+ * @param $target_language
+ * The target language into which should be translated.
+ * @param $account
+ * (Optional) A user object. Defaults to the currently logged in user.
+ *
+ * @return TMGMTJob
+ * The job entity.
+ */
+function tmgmt_job_match_item($source_language, $target_language, $account = NULL) {
+ $account = isset($account) ? $account : $GLOBALS['user'];
+ $query = new EntityFieldQuery();
+ $result = $query->entityCondition('entity_type', 'tmgmt_job')
+ ->propertyCondition('source_language', $source_language)
+ ->propertyCondition('target_language', $target_language)
+ ->propertyCondition('uid', $account->uid)
+ ->propertyCondition('state', TMGMT_JOB_STATE_UNPROCESSED)
+ ->execute();
+ if (!empty($result['tmgmt_job'])) {
+ $job = reset($result['tmgmt_job']);
+ return tmgmt_job_load($job->tjid);
+ }
+ return tmgmt_job_create($source_language, $target_language, $account->uid);
+}
+
+/**
+ * Checks whether a job is finished by querying the job item table for
+ * unfinished job items.
+ *
+ * @param $tjid
+ * The identifier of the job.
+ * @return bool
+ * TRUE if the job is finished, FALSE otherwise.
+ */
+function tmgmt_job_check_finished($tjid) {
+ $query = new EntityFieldQuery();
+ return !(boolean) $query->entityCondition('entity_type', 'tmgmt_job_item')
+ ->propertyCondition('tjid', $tjid)
+ ->propertyCondition('state', TMGMT_JOB_ITEM_STATE_ACCEPTED, '<>')
+ ->range(0, 1)
+ ->count()
+ ->execute();
+}
+
+/**
+ * Creates a translation job.
+ *
+ * @param $source_language
+ * The source language from which should be translated.
+ * @param $target_language
+ * The target language into which should be translated.
+ * @param $values
+ * (Optional) An array of additional entity values.
+ *
+ * @return TMGMTJob
+ * The job entity.
+ */
+function tmgmt_job_create($source_language, $target_language, $uid = NULL, array $values = array()) {
+ return entity_create('tmgmt_job', array_merge($values, array(
+ 'source_language' => $source_language,
+ 'target_language' => $target_language,
+ 'uid' => $uid,
+ )));
+}
+
+/**
+ * Access callback for the job entity.
+ *
+ *
+ * @param $op
+ * The operation being performed.
+ * @param $item
+ * (Optional) A TMGMTJob entity to check access for. If no entity is given, it
+ * will be determined whether access is allowed for all entities.
+ * @param $account
+ * (Optional) The user to check for. Leave it to NULL to check for the global
+ * user.
+ *
+ * @return boolean
+ * TRUE if access is allowed, FALSE otherwise.
+ */
+function tmgmt_job_access($op, $job = NULL, $account = NULL) {
+ if (user_access('administer tmgmt', $account)) {
+ // Administrators can do everything.
+ return TRUE;
+ }
+
+ switch ($op) {
+ case 'create':
+ return user_access('create translation jobs', $account);
+ break;
+ case 'view':
+ case 'update':
+ return user_access('create translation jobs', $account) || user_access('submit translation jobs', $account) || user_access('accept translation jobs', $account);
+ break;
+
+ case 'delete':
+ // Only administrators can delete jobs.
+ return FALSE;
+ break;
+
+ // Custom operations.
+ case 'submit':
+ return user_access('submit translation jobs');
+ break;
+
+ case 'abort':
+ case 'resubmit':
+ return user_access('submit translation jobs');
+ break;
+
+ case 'accept':
+ return user_access('accept translation jobs');
+ break;
+ }
+}
+
+/**
+ * Access callback for tmgmt remote entity.
+ */
+function tmgmt_remote_access($op, $tmgmt_remote = NULL, $account = NULL) {
+ return user_access('administer tmgmt', $account);
+}
+
+/**
+ * Loads an array with the word and status statistics of a job.
+ *
+ * @param $tjids
+ * An array of job ids.
+ *
+ * @return
+ * An array of objects with the keys word_count, count_pending,
+ * count_accepted, count_reviewed and count_translated.
+ */
+function tmgmt_job_statistics_load(array $tjids) {
+ $statistics = &drupal_static(__FUNCTION__, array());
+
+ // First try to get the values from the cache.
+ $return = array();
+ $tjids_to_load = array();
+ foreach ($tjids as $tjid) {
+ if (isset($statistics[$tjid])) {
+ // Info exists in cache, get it from there.
+ $return[$tjid] = $statistics[$tjid];
+ }
+ else {
+ // Info doesn't exist in cache, add job to the list that needs to be
+ // fetched.
+ $tjids_to_load[] = $tjid;
+ }
+ }
+
+ // If there are remaining jobs, build a query to fetch them.
+ if (!empty($tjids_to_load)) {
+ // Build the query to fetch the statistics.
+ $query = db_select('tmgmt_job_item', 'tji')
+ ->fields('tji', array('tjid'));
+ $query->addExpression('SUM(word_count)', 'word_count');
+ $query->addExpression('SUM(count_accepted)', 'count_accepted');
+ $query->addExpression('SUM(count_reviewed)', 'count_reviewed');
+ $query->addExpression('SUM(count_pending)', 'count_pending');
+ $query->addExpression('SUM(count_translated)', 'count_translated');
+ $result = $query->groupBy('tjid')
+ ->condition('tjid', $tjids_to_load)
+ ->execute();
+
+ foreach ($result as $row) {
+ $return[$row->tjid] = $statistics[$row->tjid] = $row;
+ }
+ }
+ return $return;
+}
+
+/**
+ * Returns a specific statistic of a job.
+ *
+ * @param $job
+ * The translation job entity.
+ * @param $key
+ * One of word_count, count_pending, count_accepted, count_reviewed and
+ * count_translated.
+ *
+ * @return
+ * The requested information as an integer.
+ */
+function tmgmt_job_statistic(TMGMTJob $job, $key) {
+ $statistics = tmgmt_job_statistics_load(array($job->tjid));
+ if (isset($statistics[$job->tjid]->$key)) {
+ return $statistics[$job->tjid]->$key;
+ }
+ return 0;
+}
+
+/**
+ * Access callback for the job item entity.
+ *
+ * @param $op
+ * The operation being performed.
+ * @param $item
+ * (Optional) A TMGMTJobItem entity to check access for. If no entity is
+ * given, it will be determined whether access is allowed for all entities.
+ * @param $account
+ * (Optional) The user to check for. Leave it to NULL to check for the global
+ * user.
+ *
+ * @return boolean
+ * TRUE if access is allowed, FALSE otherwise.
+ */
+function tmgmt_job_item_access($op, TMGMTJobItem $item = NULL, $account = NULL) {
+ // There are no item specific permissions yet.
+ return tmgmt_job_access($op, $item ? $item->getJob() : NULL, $account);
+}
+
+/**
+ * Access callback wrapper for reviewing a job item entity.
+ *
+ * @param TMGMTJobItem $item
+ * The job item to check access for.
+ * @param $account
+ * (Optional) The user to check for. Leave it to NULL to check for the global
+ * user.
+ *
+ * @return boolean
+ * TRUE if access is allowed, FALSE otherwise.
+ */
+function tmgmt_job_item_review_access(TMGMTJobItem $item, $account = NULL) {
+ if ($item->isNeedsReview() && $item->getSourceController() && $item->getTranslatorController()) {
+ return tmgmt_job_item_access('accept', $item, $account);
+ }
+ return FALSE;
+}
+
+/**
+ * Access callback for the job message entity.
+ *
+ * @param $op
+ * The operation being performed.
+ * @param $item
+ * (Optional) A TMGMTJobMessage entity to check access for. If no entity is
+ * given, it will be determined whether access is allowed for all entities.
+ * @param $account
+ * (Optional) The user to check for. Leave it to NULL to check for the global
+ * user.
+ *
+ * @return boolean
+ * TRUE if access is allowed, FALSE otherwise.
+ */
+function tmgmt_message_access($op, TMGMTMessage $message = NULL, $account = NULL) {
+ // All users that can see jobs can see messages as well.
+ if ($op == 'view') {
+ $job = NULL;
+ if ($message) {
+ $job = $message->getJob();
+ }
+ return tmgmt_job_access('view', $job, $account);
+ }
+ // Changing or creating messages is only possible for admins.
+ return user_access('administer tmgmt');
+}
+
+/**
+ * Static method to retrieve a labeled list of all available states.
+ *
+ * @return array
+ * A list of all available states.
+ */
+function tmgmt_job_states() {
+ return array(
+ TMGMT_JOB_STATE_UNPROCESSED => t('Unprocessed'),
+ TMGMT_JOB_STATE_ACTIVE => t('Active'),
+ TMGMT_JOB_STATE_REJECTED => t('Rejected'),
+ TMGMT_JOB_STATE_ABORTED => t('Aborted'),
+ TMGMT_JOB_STATE_FINISHED => t('Finished'),
+ );
+}
+
+/**
+ * Static method to retrieve a labeled list of all available states.
+ *
+ * @return array
+ * A list of all available states.
+ */
+function tmgmt_job_item_states() {
+ return array(
+ TMGMT_JOB_ITEM_STATE_ACTIVE => t('In progress'),
+ TMGMT_JOB_ITEM_STATE_REVIEW => t('Needs review'),
+ TMGMT_JOB_ITEM_STATE_ACCEPTED => t('Accepted'),
+ TMGMT_JOB_ITEM_STATE_ABORTED => t('Aborted'),
+ );
+}
+
+/**
+ * Loads a translation job item.
+ *
+ * @param $tjiid
+ * A job item id.
+ *
+ * @return TMGMTJobItem
+ * The loaded job item or FALSE if the query returned no results.
+ */
+function tmgmt_job_item_load($tjiid) {
+ $jobs = tmgmt_job_item_load_multiple(array($tjiid), array());
+ return $jobs ? reset($jobs) : FALSE;
+}
+
+/**
+ * Loads translation job items.
+ *
+ * @param $tjiids
+ * An array of job item ids.
+ * @param $conditions
+ * An array of additional conditions.
+ *
+ * @return TMGMTJobItem[]
+ * An array of job item entities or an empty array if the query returned no
+ * results.
+ */
+function tmgmt_job_item_load_multiple($tjiids = array(), $conditions = array()) {
+ return entity_load('tmgmt_job_item', $tjiids, $conditions);
+}
+
+/**
+ * Creates a translation job item.
+ *
+ * @param $plugin
+ * The plugin name.
+ * @param $item_type
+ * The source item type.
+ * @param $item_id
+ * The source item id.
+ * @param $values
+ * (Optional) An array of additional entity values to be set.
+ *
+ * @return TMGMTJobItem
+ * The created, not yet saved, job item entity.
+ */
+function tmgmt_job_item_create($plugin, $item_type, $item_id, array $values = array()) {
+ return entity_create('tmgmt_job_item', array_merge($values, array(
+ 'plugin' => $plugin,
+ 'item_type' => $item_type,
+ 'item_id' => $item_id,
+ )));
+}
+
+/**
+ * Loads a translation job message.
+ *
+ * @param $mid
+ * A job message id.
+ *
+ * @return TMGMTMessage
+ * A job message entity or FALSE if the query didn't yield any results.
+ */
+function tmgmt_message_load($mid) {
+ // Avoid collision with the message module because this looks like the module
+ // implements hook_ENTITY_TYPE_load() for message.
+ if (!is_array($mid)) {
+ $jobs = tmgmt_message_load_multiple(array($mid));
+ return $jobs ? reset($jobs) : FALSE;
+ }
+}
+
+/**
+ * Loads translation job messages.
+ */
+function tmgmt_message_load_multiple($mids = array(), $conditions = array()) {
+ return entity_load('tmgmt_message', $mids, $conditions);
+}
+
+/**
+ * Creates a translation job message.
+ *
+ * @param $message
+ * (Optional) The message to be saved.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ * @param $values
+ * (Optional) An array of additional entity values to be set.
+ *
+ * @return TMGMTJobItem
+ * The created, not yet saved, job item entity.
+ */
+function tmgmt_message_create($message = '', $variables = array(), $values = array()) {
+ return entity_create('tmgmt_message', array_merge($values, array(
+ 'message' => $message,
+ 'variables' => $variables,
+ )));
+}
+/**
+ * @} End of "addtogroup tmgmt_job".
+ */
+/**
+ * @addtogroup tmgmt_translator
+ * @{
+ */
+
+/**
+ * Access callback for the translator entity.
+ */
+function tmgmt_translator_access($op, TMGMTTranslator $translator = NULL, $account = NULL) {
+ if (isset($translator) && !$translator->getController()) {
+ return FALSE;
+ }
+ // Only administrators are allowed to manage translator entities.
+ return user_access('administer tmgmt', $account);
+}
+
+/**
+ * Checks whether a translator entity with the supplied name already exists.
+ *
+ * We can't use entity_load or any of its wrapper functions for that as our
+ * translator entity controller filters out broken translator entities (e.g. if
+ * the translator plugin of the translator entity doesn't exist (anymore).
+ *
+ * @param $name
+ * The machine-readable name of the translator entity that we are trying to
+ * save.
+ *
+ * @return boolean
+ * TRUE if a translator entity with the same machine-readable name already
+ * exists FALSE otherwise.
+ */
+function tmgmt_translator_exists($name) {
+ $query = new EntityFieldQuery();
+ return (boolean) $query->entityCondition('entity_type', 'tmgmt_translator')
+ ->propertyCondition('name', $name)
+ ->count()
+ ->range(0, 1)
+ ->execute();
+}
+
+/**
+ * Loads a translator based on the name.
+ *
+ * @param $name
+ * The machine-readable name of the translator entity to load.
+ *
+ * @return TMGMTTranslator
+ * A translator entity.
+ */
+function tmgmt_translator_load($name) {
+ $translators = entity_load_multiple_by_name('tmgmt_translator', array($name));
+ return $translators ? reset($translators) : FALSE;
+}
+
+/**
+ * Loads multiple translators based on their name.
+ *
+ * @param $names
+ * (Optional) An array of machine-readable names of the translator entities to
+ * load or FALSE to load all available translator entities.
+ *
+ * @return array
+ * An array of translators with the machine-readable name of the translators
+ * as array keys.
+ */
+function tmgmt_translator_load_multiple($names = array()) {
+ return entity_load_multiple_by_name('tmgmt_translator', $names);
+}
+
+/**
+ * Loads all translators that are available and, if a translation job is given,
+ * support translations for that job with its current configuration.
+ *
+ * @param TMGMTJob $job
+ * (Optional) A translation job.
+ *
+ * @return array
+ * An array of translators with the machine-readable name of the translators
+ * as array keys.
+ */
+function tmgmt_translator_load_available($job) {
+ $translators = tmgmt_translator_load_multiple(FALSE);
+ foreach ($translators as $name => $translator) {
+ if (!$translator->isAvailable() || (isset($job) && !$translator->canTranslate($job))) {
+ unset($translators[$name]);
+ }
+ }
+ return $translators;
+}
+
+/**
+ * Checks whether a translator with a certain name is busy and therefore can't
+ * be modified or deleted. A translator is considered 'busy' if there are jobs
+ * attached to it that are in an active state.
+ *
+ * @param $translator
+ * The machine-readable name of a translator.
+ *
+ * @return boolean
+ * TRUE if the translator is busy, FALSE otherwise.
+ */
+function tmgmt_translator_busy($translator) {
+ $query = new EntityFieldQuery();
+ return (boolean) $query->entityCondition('entity_type', 'tmgmt_job')
+ ->propertyCondition('state', TMGMT_JOB_STATE_ACTIVE)
+ ->propertyCondition('translator', $translator)
+ ->range(0, 1)
+ ->count()
+ ->execute();
+}
+
+/**
+ * Creates a translator entity.
+ *
+ * @param $plugin
+ * The plugin of the translator.
+ * @param $name
+ * The machine-readable name of the translator.
+ * @param $label
+ * The label of the translator.
+ * @param $description
+ * (Optional) The description of the translator. Defaults to an empty string.
+ * @param $settings
+ * (Optional) An array of settings for the translator.
+ * @param $values
+ * (Optional) Array of additional entity values.
+ *
+ * @return TMGMTTranslator
+ * The created, not yet saved, translator entity.
+ */
+function tmgmt_translator_create($plugin, $name, $label, $description = '', $settings = array(), $values = array()) {
+ return entity_create('tmgmt_translator', array_merge($values, array(
+ 'plugin' => $plugin,
+ 'name' => $name,
+ 'label' => $label,
+ 'description' => $description,
+ 'settings' => $settings,
+ )));
+}
+
+/**
+ * Auto creates a translator from a translator plugin definition.
+ *
+ * @param $plugin
+ * The machine-readable name of a translator plugin.
+ */
+function tmgmt_translator_auto_create($plugin) {
+ if ($info = tmgmt_translator_plugin_info($plugin)) {
+ if (!tmgmt_translator_exists($plugin)) {
+ $label = $info['label'] . ' (auto created)';
+ $translator = tmgmt_translator_create($plugin, $plugin, $label, $info['description']);
+ // Append some default settings from the translator plugin definition.
+ $translator->settings = $translator->getController()->defaultSettings();
+ $translator->save();
+ }
+ }
+}
+
+/**
+ * Determines all available service plugins.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a service plugin.
+ *
+ * @return array
+ * An array of translator plugin definitions.
+ */
+function tmgmt_translator_plugin_info($plugin = NULL) {
+ return _tmgmt_plugin_info('translator', $plugin);
+}
+
+/**
+ * Determines the controller class for a given service plugin.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a service plugin.
+ *
+ * @return array|TMGMTTranslatorPluginControllerInterface
+ * - If the translator exists the controller object for the given source plugin
+ * or an array containing all available translator plugin controller objects
+ * if no plugin name was given.
+ * - Array of existing Translators if a translator with given name does not
+ * exists.
+ */
+function tmgmt_translator_plugin_controller($plugin = NULL) {
+ return _tmgmt_plugin_controller('translator', $plugin);
+}
+
+/**
+ * Get the ui controller class for a given translator plugin.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a translator plugin.
+ *
+ * @return TMGMTTranslatorUIControllerInterface
+ * The ui controller object for the given translator plugin or an array
+ * containing all available translator plugin controller objects if no plugin
+ * name was given.
+ */
+function tmgmt_translator_ui_controller($plugin = NULL) {
+ return _tmgmt_plugin_controller('translator', $plugin, 'ui', 'TMGMTDefaultTranslatorUIController');
+}
+
+/**
+ * Returns an array of all available translator plugins with the labels as
+ * values and the machine-readable name as the key.
+ *
+ * @return array
+ * An array of the labels of all available plugins.
+ */
+function tmgmt_translator_plugin_labels() {
+ return _tmgmt_plugin_labels('translator');
+}
+
+/**
+ * Returns a list of all available translator labels.
+ *
+ * @return array
+ * An array containing all available translator labels.
+ */
+function tmgmt_translator_labels() {
+ $labels = array();
+ foreach (tmgmt_translator_load_multiple(FALSE) as $translator) {
+ $labels[$translator->name] = $translator->label();
+ }
+ return $labels;
+}
+
+/**
+ * Returns a list of flagged translator labels. If a translator is not available
+ * it will be suffixed with a short text explaining why it is not available.
+ * This can either be because the configuration of the passed job is not
+ * supported or because the translator service can't be reached.
+ *
+ * @param TMGMTJob $job
+ * (Optional) A translation job.
+ *
+ * @return array
+ * An array of flagged translator labels.
+ */
+function tmgmt_translator_labels_flagged($job = NULL) {
+ $labels = array();
+ foreach (tmgmt_translator_load_multiple(FALSE) as $translator) {
+ if (!$translator->isAvailable()) {
+ $labels[$translator->name] = t('@label (not available)', array('@label' => $translator->label()));
+ }
+ elseif (isset($job) && !$translator->canTranslate($job)) {
+ $labels[$translator->name] = t('@label (unsupported)', array('@label' => $translator->label()));
+ }
+ else {
+ $labels[$translator->name] = $translator->label();
+ }
+ }
+ return $labels;
+}
+
+/**
+ * Determines if the translator plugin supports remote language mappings.
+ *
+ * @param TMGMTTranslator $translator
+ * Translator entity.
+ *
+ * @return bool
+ * In case translator does not explicitly state that it does not provide the
+ * mapping feature it will return TRUE.
+ */
+function tmgmt_provide_remote_languages_mappings(TMGMTTranslator $translator) {
+ $info = tmgmt_translator_plugin_info($translator->plugin);
+
+ if (!isset($info['map remote languages'])) {
+ return TRUE;
+ }
+
+ return $info['map remote languages'];
+}
+
+/**
+ * Determines if job settings of the translator will be handled by its plugin.
+ *
+ * @param TMGMTTranslator $translator
+ * Translator entity.
+ *
+ * @return bool
+ * If job settings are to be handled by the plugin.
+ */
+function tmgmt_job_settings_custom_handling(TMGMTTranslator $translator) {
+ $info = tmgmt_translator_plugin_info($translator->plugin);
+
+ if (isset($info['job settings custom handling'])) {
+ return $info['job settings custom handling'];
+ }
+
+ return FALSE;
+}
+/**
+ * @} End of "addtogroup tmgmt_translator".
+ */
+/**
+ * @addtogroup tmgmt_source
+ * @{
+ */
+
+/**
+ * Determines all available source object plugins.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a source plugin.
+ *
+ * @return array
+ * An array of source plugin definitions.
+ */
+function tmgmt_source_plugin_info($plugin = NULL) {
+ return _tmgmt_plugin_info('source', $plugin);
+}
+
+/**
+ * Get the plugin controller class for a given source plugin.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a source plugin.
+ *
+ * @return TMGMTSourcePluginControllerInterface
+ * The controller object for the given source plugin or an array containing
+ * all available source plugin controller objects if no plugin name was given.
+ */
+function tmgmt_source_plugin_controller($plugin = NULL) {
+ return _tmgmt_plugin_controller('source', $plugin);
+}
+
+/**
+ * Get the ui controller class for a given source plugin.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a source plugin.
+ *
+ * @return TMGMTSourceUIControllerInterface
+ * The ui controller object for the given source plugin or an array containing
+ * all available source ui controller objects if no plugin name was given.
+ */
+function tmgmt_source_ui_controller($plugin = NULL) {
+ return _tmgmt_plugin_controller('source', $plugin, 'ui', 'TMGMTDefaultSourceUIController');
+}
+
+/**
+ * Get the views controller class for a given source plugin.
+ *
+ * @param $plugin
+ * (Optional) The machine-readable name of a source plugin.
+ *
+ * @return TMGMTSourceViewsControllerInterface
+ * The views controller object for the given source plugin or an array
+ * containing all available source views controller objects if no plugin name
+ * was given.
+ */
+function tmgmt_source_views_controller($plugin = NULL) {
+ return _tmgmt_plugin_controller('source', $plugin, 'views', 'TMGMTDefaultSourceViewsController');
+}
+
+/**
+ * Returns an array of all available source plugins with the labels as
+ * values and the machine-readable name as the key.
+ *
+ * @return array
+ * An array of the labels of all available plugins.
+ */
+function tmgmt_source_plugin_labels() {
+ return _tmgmt_plugin_labels('source');
+}
+
+/**
+ * Returns an array of translatable item types of a source plugin.
+ *
+ * @param $plugin
+ * The machine-readable name of a source plugin.
+ *
+ * @return array
+ * The array of translatable item types.
+ *
+ * @see TMGMTSourcePluginControllerInterface::getItemTypes()
+ */
+function tmgmt_source_translatable_item_types($plugin) {
+ $controller = tmgmt_source_plugin_controller($plugin);
+ return $controller->getItemTypes();
+}
+
+/**
+ * @param $plugin
+ * @param $item_type
+ * @return bool
+ */
+function tmgmt_source_is_translatable_item_type($plugin, $item_type) {
+ return array_key_exists($item_type, tmgmt_source_translatable_item_types($plugin));
+}
+/**
+ * @} End of "addtogroup tmgmt_source".
+ */
+
+/**
+ * Discovers all available source and/or translator plugins.
+ * @param $type
+ * The type of the plugin. Can be 'translator' or 'source'.
+ * @param $plugin
+ * (Optional) The machine-readable name of a source plugin.
+ *
+ * @return array
+ * An array of source and/or translator plugins.
+ */
+function _tmgmt_plugin_info($type, $plugin = NULL) {
+ $info = &drupal_static(__FUNCTION__);
+ if (!isset($info[$type])) {
+ $info[$type] = array();
+ foreach (module_implements('tmgmt_' . $type . '_plugin_info') as $module) {
+ foreach (module_invoke($module, 'tmgmt_' . $type . '_plugin_info') as $key => $item) {
+ $info[$type][$key] = $item;
+ $info[$type][$key]['module'] = $module;
+ $info[$type][$key]['plugin'] = $key;
+ }
+ }
+ drupal_alter('tmgmt_' . $type . '_plugin_info', $info[$type]);
+ }
+ if (isset($plugin) && isset($info[$type][$plugin])) {
+ return $info[$type][$plugin];
+ }
+ elseif (!isset($plugin)) {
+ return $info[$type];
+ }
+}
+
+/**
+ * Determines the controller class for a given plugin type.
+ *
+ * @param $type
+ * The type of the plugin. Can be 'translator' or 'source'.
+ * @param $plugin
+ * (Optional) The machine-readable name of a source plugin.
+ *
+ * @return TMGMTPluginBaseInterface
+ * The controller object for the given plugin or an array containing all
+ * available plugin controller objects if no plugin name was given.
+ */
+function _tmgmt_plugin_controller($type, $plugin = NULL, $controller = 'plugin', $default = NULL) {
+ $key = $controller . ' controller class';
+ $cache = &drupal_static(__FUNCTION__);
+ if (!isset($plugin) && !isset($cache[$type][$controller])) {
+ $cache[$type][$controller] = array();
+ foreach (_tmgmt_plugin_info($type) as $name => $info) {
+ if (!isset($cache[$type][$controller][$name])) {
+ $class = isset($default) && !isset($info[$key]) ? $default : $info[$key];
+ $cache[$type][$controller][$name] = new $class($type, $name);
+ }
+ }
+ }
+ elseif (isset($plugin) && !isset($cache[$type][$controller][$plugin])) {
+ $info = _tmgmt_plugin_info($type, $plugin);
+ if (empty($info[$key]) && empty($default)) {
+ $cache[$type][$controller][$plugin] = FALSE;
+ }
+ else {
+ $class = empty($info[$key]) ? $default : $info[$key];
+ $cache[$type][$controller][$plugin] = new $class($type, $plugin);
+ }
+ }
+ if (isset($plugin)) {
+ return $cache[$type][$controller][$plugin];
+ }
+ else {
+ return array_filter($cache[$type][$controller]);
+ }
+}
+
+/**
+ * Returns an array of labels of all available plugins of a given type with the
+ * machine-readable name as the key.
+ *
+ * @return array
+ * An array of the labels of all available plugins.
+ */
+function _tmgmt_plugin_labels($type) {
+ $list = array();
+ $plugin_info = 'tmgmt_' . $type . '_plugin_info';
+ foreach ($plugin_info() as $key => $info) {
+ $list[$key] = $info['label'];
+ }
+ return $list;
+}
+
+/**
+ * Converts a nested data array into a flattened structure with a combined key.
+ *
+ * This function can be used by translators to help with the data conversion.
+ *
+ * Nested keys will be joined together using a colon, so for example
+ * $data['key1']['key2']['key3'] will be converted into
+ * $flattened_data['key1][key2][key3'].
+ *
+ * @param $data
+ * The nested array structure that should be flattened.
+ * @param $prefix
+ * Internal use only, indicates the current key prefix when recursing into
+ * the data array.
+ *
+ * @return array
+ * The flattened data array.
+ *
+ * @see tmgmt_unflatten_data()
+ */
+function tmgmt_flatten_data($data, $prefix = NULL, $label = array()) {
+ $flattened_data = array();
+ if (isset($data['#label'])) {
+ $label[] = $data['#label'];
+ }
+ // Each element is either a text (has #text property defined) or has children,
+ // not both.
+ if (!empty($data['#text'])) {
+ $flattened_data[$prefix] = $data;
+ $flattened_data[$prefix]['#parent_label'] = $label;
+ }
+ else {
+ $prefix = isset($prefix) ? $prefix . TMGMT_ARRAY_DELIMITER : '';
+ foreach (element_children($data) as $key) {
+ $flattened_data += tmgmt_flatten_data($data[$key], $prefix . $key, $label);
+ }
+ }
+ return $flattened_data;
+}
+
+/**
+ * Converts string keys to array keys.
+ *
+ * There are three conventions for data keys in use. This function accepts each
+ * of it an ensures a array of keys.
+ *
+ * @param $key
+ * The key can be either be an array containing the keys of a nested array
+ * hierarchy path or a string with '][' or '|' as delimiter.
+ *
+ * @return
+ * Array of keys.
+ */
+function tmgmt_ensure_keys_array($key) {
+ if (empty($key)) {
+ return array();
+ }
+ if (!is_array($key)) {
+ if (strstr($key, '|')) {
+ $key = str_replace('|', TMGMT_ARRAY_DELIMITER, $key);
+ }
+ $key = explode(TMGMT_ARRAY_DELIMITER, $key);
+ }
+ return $key;
+}
+
+/**
+ * Converts keys array to string key.
+ *
+ * There are three conventions for data keys in use. This function accepts each
+ * of it an ensures a sting keys.
+ *
+ * @param $key
+ * The key can be either be an array containing the keys of a nested array
+ * hierarchy path or a string.
+ * @param
+ * Delimiter to be use in the keys string. Default is ']['.
+ *
+ * @return
+ * Keys string.
+ */
+function tmgmt_ensure_keys_string($key, $delimiter = TMGMT_ARRAY_DELIMITER) {
+ if (is_array($key)) {
+ $key = implode($delimiter, $key);
+ }
+ return $key;
+}
+
+/**
+ * Converts a flattened data structure into a nested array.
+ *
+ * This function can be used by translators to help with the data conversion.
+ *
+ * Nested keys will be created based on the colon, so for example
+ * $flattened_data['key1][key2][key3'] will be converted into
+ * $data['key1']['key2']['key3'].
+ *
+ * @param $data
+ * The flattened data array.
+ *
+ * @return array
+ * The nested data array.
+ *
+ * @see tmgmt_flatten_data()
+ */
+function tmgmt_unflatten_data($flattened_data) {
+ $data = array();
+ foreach ($flattened_data as $key => $flattened_data_entry) {
+ drupal_array_set_nested_value($data, explode(TMGMT_ARRAY_DELIMITER, $key), $flattened_data_entry);
+ }
+ return $data;
+}
+
+/**
+ * Array filter callback for filtering untranslatable source data elements.
+ */
+function _tmgmt_filter_data($value) {
+ return !(empty($value['#text']) || (isset($value['#translate']) && $value['#translate'] === FALSE));
+}
+
+/**
+ * Fetches an array of exportables from files.
+ *
+ * @param $module
+ * The module invoking this request. (Can be called by other modules.)
+ * @param $directory
+ * The subdirectory in the custom module.
+ * @param $extension
+ * The file extension.
+ * @param $name
+ * The name of the variable found in each file. Defaults to the same as
+ * $extension.
+ *
+ * @return array
+ * Array of $name objects.
+ */
+function _tmgmt_load_exports($module, $directory, $extension, $name = NULL) {
+ if (!$name) {
+ $name = $extension;
+ }
+ $return = array();
+ // Find all the files in the directory with the correct extension.
+ $files = file_scan_directory(drupal_get_path('module', $module) . "/$directory", "/\.{$extension}$/");
+ foreach ($files as $path => $file) {
+ require DRUPAL_ROOT . '/' . $path;
+ if (isset($name)) {
+ $return[$$name->name] = $$name;
+ }
+ }
+ return $return;
+}
+
+/**
+ * Returns a label for a data item.
+ *
+ * @param array $data_item
+ * The data item array.
+ * @param int $max_length
+ * (optional) Specify the max length that the resulting label string should
+ * be cut to.
+ *
+ * @return string
+ * A label for the data item.
+ */
+function tmgmt_data_item_label(array $data_item, $max_length = NULL) {
+ if (!empty($data_item['#parent_label'])) {
+ if ($max_length) {
+ // When having multiple label parts, we don't know how long each of them is,
+ // truncating each to the same length might result in a considerably shorter
+ // length than max length when there are short and long labels. Instead,
+ // start with the max length and repeat until the whole string is less than
+ // max_length. Remove 4 characters per part to avoid unecessary loops.
+ $current_max_length = $max_length - (count($data_item['#parent_label']) * 4);
+ do {
+ $current_max_length--;
+ $labels = array();
+ foreach ($data_item['#parent_label'] as $label_part) {
+ // If this not the last part, reserve 3 characters for the delimiter.
+ $labels[] = truncate_utf8($label_part, $current_max_length, FALSE, TRUE);
+ }
+ $label = implode(t(' > '), $labels);
+ } while (drupal_strlen($label) > $max_length);
+ return $label;
+ }
+ else {
+ return implode(t(' > '), $data_item['#parent_label']);
+ }
+ }
+ elseif (!empty($data_item['#label'])) {
+ return $max_length ? truncate_utf8($data_item['#label'], $max_length, FALSE, TRUE) : $data_item['#label'];
+ }
+ else {
+ // As a last resort, fall back to a shortened version of the text. Default
+ // to a limit of 50 characters.
+ return truncate_utf8($data_item['#text'], $max_length ? $max_length : 50, FALSE, TRUE);
+ }
+}
+
+/**
+ * Implements hook_views_plugins().
+ */
+function tmgmt_views_plugins() {
+ $plugins = array(
+ 'access' => array(
+ 'tmgmt_views_job_access' => array(
+ 'title' => t('Job view access'),
+ 'help' => t('Check if the user is allowed to view jobs'),
+ 'handler' => 'tmgmt_views_job_access',
+ 'path' => drupal_get_path('module', 'tmgmt') . '/views/plugins',
+ ),
+ ),
+ );
+ return $plugins;
+}
+
+/**
+ * Calculates number of words, which a text consists of.
+ * Is placed as a separately function to be coverable by unit tests.
+ * @see TMGMTWordCountUnitTestCase
+ *
+ * @param string $text
+ * @return int
+ * Returns count of words of text.
+ */
+function tmgmt_word_count($text) {
+ // Strip tags in case it is requested to not include them in the count.
+ if (variable_get('tmgmt_word_count_exclude_tags', TRUE)) {
+ $text = strip_tags($text);
+ }
+ // Replace each punctuation mark with space.
+ $text = str_replace(array('`', '~', '!', '@', '"', '#', '$', ';', '%', '^', ':', '?', '&', '*', '(', ')', '-', '_', '+', '=', '{', '}', '[', ']', '\\', '|', '/', '\'', '<', '>', ',', '.'), ' ', $text);
+ // Remove duplicate spaces.
+ $text = trim(preg_replace('/ {2,}/', ' ', $text));
+ // Turn into an array.
+ $array = ($text) ? explode(' ', $text) : array();
+ // How many are they?
+ $count = count($array);
+ // That is what we need.
+ return $count;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/tmgmt.rules.inc b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.rules.inc
new file mode 100644
index 00000000..eb107d33
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/tmgmt.rules.inc
@@ -0,0 +1,247 @@
+ t('Request Job translation'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'job' => array(
+ 'type' => 'tmgmt_job',
+ 'label' => t('Translation Job'),
+ 'description' => t('The translation job for which translations should be requested.'),
+ ),
+ ),
+ 'access callback' => 'tmgmt_rules_job_submit_access',
+ );
+ $info['tmgmt_rules_job_accept_translation'] = array(
+ 'label' => t('Accept Job translation'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'job' => array(
+ 'type' => 'tmgmt_job',
+ 'label' => t('Translation Job'),
+ 'description' => t('The translation job for which translations should be accepted.'),
+ ),
+ 'message' => array(
+ 'type' => 'text',
+ 'label' => t('An optional message'),
+ 'description' => t('Will be stored in the job message and displayed to the user.'),
+ 'optional' => TRUE,
+ ),
+ ),
+ 'access callback' => 'tmgmt_rules_job_accept_translation_access',
+ );
+ $info['tmgmt_rules_job_abort_translation'] = array(
+ 'label' => t('Abort translation job'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'job' => array(
+ 'type' => 'tmgmt_job',
+ 'label' => t('Translation Job'),
+ 'description' => t('The translation job that should be aborted.'),
+ ),
+ ),
+ 'access callback' => 'tmgmt_rules_job_submit_access',
+ );
+ $info['tmgmt_rules_job_delete'] = array(
+ 'label' => t('Delete Job'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'job' => array(
+ 'type' => 'tmgmt_job',
+ 'label' => t('Translation Job'),
+ 'description' => t('The translation job that should be deleted.'),
+ ),
+ ),
+ 'access callback' => 'tmgmt_rules_job_delete_access',
+ );
+ $info['tmgmt_rules_job_checkout'] = array(
+ 'label' => t('Checkout a job'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'job' => array(
+ 'type' => 'tmgmt_job',
+ 'label' => t('Translation Job'),
+ 'description' => t('The translation job that should be checked out.'),
+ ),
+ ),
+ 'access callback' => 'tmgmt_rules_job_submit_access',
+ );
+ $info['tmgmt_get_first_from_node_list'] = array(
+ 'label' => t('Get first item from a list of nodes'),
+ 'group' => t('Data'),
+ 'parameter' => array(
+ 'list' => array(
+ 'type' => 'list',
+ 'label' => t('List'),
+ 'restriction' => 'selector',
+ ),
+ ),
+ 'provides' => array(
+ 'first_node' => array(
+ 'type' => 'node',
+ 'label' => t('Node'),
+ ),
+ ),
+ );
+ $info['tmgmt_rules_create_job'] = array(
+ 'label' => t('Create a job for a given source language'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'source_language' => array(
+ 'type' => 'text',
+ 'label' => t('Source Language'),
+ 'description' => t('The language from which should be translated'),
+ 'options list' => 'entity_metadata_language_list',
+ ),
+ ),
+ 'provides' => array(
+ 'job' => array(
+ 'label' => t('Job'),
+ 'type' => 'tmgmt_job',
+ ),
+ ),
+ );
+ $info['tmgmt_rules_job_add_item'] = array(
+ 'label' => t('Add an item to a job'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'job' => array(
+ 'type' => 'tmgmt_job',
+ 'label' => t('Translation Job'),
+ 'description' => t('The translation job that should be canceled.'),
+ ),
+ 'plugin' => array(
+ 'type' => 'token',
+ 'label' => t('Source plugin'),
+ 'description' => t('The source plugin of this item'),
+ //'options list' => 'entity_metadata_language_list',
+ ),
+ 'item_type' => array(
+ 'type' => 'token',
+ 'label' => t('Item type'),
+ 'description' => t('The item type'),
+ //'options list' => 'entity_metadata_language_list',
+ ),
+ 'item_id' => array(
+ 'type' => 'text',
+ 'label' => t('Item ID'),
+ 'description' => t('ID of the referenced item'),
+ ),
+ ),
+ );
+ return $info;
+}
+
+/**
+ * Rules callback to request a translation of a job.
+ */
+function tmgmt_rules_job_request_translation(TMGMTJob $job) {
+ if ($job->isTranslatable()) {
+ $job->requestTranslation();
+ }
+}
+
+/**
+ * Rules callback to accept a translation of a job.
+ */
+function tmgmt_rules_job_accept_translation(TMGMTJob $job, $message) {
+ foreach ($job->getItems() as $item) {
+ if ($item->isNeedsReview()) {
+ $item->acceptTranslation();
+ }
+ }
+}
+
+/**
+ * Rules callback to cancel a translation job.
+ */
+function tmgmt_rules_job_abort_translation(TMGMTJob $job) {
+ $job->abortTranslation();
+}
+
+/**
+ * Rules callback to redirect to a translation job.
+ */
+function tmgmt_rules_job_checkout(TMGMTJob $job) {
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ // If necessary, do a redirect.
+ if ($redirects) {
+ tmgmt_ui_redirect_queue_set($redirects, current_path());
+ drupal_goto(tmgmt_ui_redirect_queue_dequeue());
+
+ // Count of the job messages is one less due to the final redirect.
+ drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
+ }
+}
+
+/**
+ * Rules callback to get the job for a specific language combination.
+ */
+function tmgmt_rules_create_job($source_language) {
+ return array(
+ 'job' => tmgmt_job_create($source_language, ''),
+ );
+}
+
+/**
+ * Rules callback to add an item to a job.
+ */
+function tmgmt_rules_job_add_item(TMGMTJob $job, $plugin, $item_type, $item_id) {
+ try {
+ $job->addItem($plugin, $item_type, $item_id);
+ }
+ catch (TMGMTException $e) {
+ watchdog_exception('tmgmt', $e);
+ drupal_set_message(t('Unable to add job item of type %type with id %id. Make sure the source content is not empty.',
+ array('%type' => $item_type, '%id' => $item_id)), 'error');
+ }
+}
+
+/**
+ * Rules action to extract the first node from a node list.
+ */
+function tmgmt_get_first_from_node_list($list) {
+ return array(
+ 'first_node' => reset($list),
+ );
+}
+
+/**
+ * Rules action to delete a translation job.
+ */
+function tmgmt_rules_job_delete(TMGMTJob $job) {
+ // Prevent users without job delete permission to be able to delete jobs.
+ if (tmgmt_job_access('delete')) {
+ $job->delete();
+ }
+}
+
+/**
+ * Checks access to rules job delete action.
+ */
+function tmgmt_rules_job_delete_access() {
+ return tmgmt_job_access('delete');
+}
+
+/**
+ * Checks access to rules job submit like actions.
+ */
+function tmgmt_rules_job_submit_access() {
+ return tmgmt_job_access('submit');
+}
+
+/**
+ * Checks access to rules accept translation action.
+ */
+function tmgmt_rules_job_accept_translation_access() {
+ return tmgmt_job_access('accept');
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/templates/tmgmt_file_html_template.tpl.php b/sites/all/modules/contrib/localisation/tmgmt/translators/file/templates/tmgmt_file_html_template.tpl.php
new file mode 100644
index 00000000..eafe5585
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/templates/tmgmt_file_html_template.tpl.php
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+ Job ID
+
+
+ $item): ?>
+
+ $field): ?>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.api.php b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.api.php
new file mode 100644
index 00000000..283790df
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.api.php
@@ -0,0 +1,58 @@
+ array(
+ 'label' => t('XLIFF'),
+ 'plugin controller class' => 'TMGMTFileFormatXLIFF',
+ ),
+ 'html' => array(
+ 'label' => t('HTML'),
+ 'plugin controller class' => 'TMGMTFileFormatHTML',
+ ),
+ );
+}
+
+/**
+ * Provide information about available text processors.
+ *
+ * @return array
+ * An array of available text processor definitions. The key is the text
+ * processor name.
+ */
+function hook_tmgmt_file_text_processor_plugin_info() {
+ return array(
+ 'mask_html_for_xliff' => array(
+ 'label' => t('Escape HTML'),
+ 'processor class' => 'TMGMTFileXLIFFMaskHTMLProcessor',
+ ),
+ );
+}
+
+/**
+ * Alter file format plugins provided by other modules.
+ *
+ * @see hook_tmgmt_file_format_plugin_info()
+ */
+function hook_tmgmt_file_format_plugin_info_alter($file_formats) {
+ // Switch the used HTML plugin controller class.
+ $file_formats['html']['plugin controller class'] = 'MyModuleCustomizedHTML';
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.drush.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.drush.inc
new file mode 100644
index 00000000..d1180c01
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.drush.inc
@@ -0,0 +1,80 @@
+ 'Import XLIFF translation files',
+ 'arguments' => array(
+ 'name' => 'Directory path that is search for *.xlf files or a file name',
+ ),
+ 'aliases' => array('tmti'),
+ );
+
+ return $items;
+}
+
+/**
+ * Import XLIFF files from a directory or single file.
+ */
+function drush_tmgmt_file_tmgmt_translate_import($name = NULL) {
+ if (!$name) {
+ return drush_set_error(dt('You need to provide a directory path or filename.'));
+ }
+
+ if (!file_exists($name)) {
+ // Drush changes the current working directory to the drupal root directory.
+ // Also check the current directory.
+ if (!file_exists(drush_cwd() . '/' . $name)) {
+ return drush_set_error(dt('@name does not exists or is not accessible.', array('@name' => $name)));
+ }
+ else {
+ // The path is relative to the current directory, update the variable.
+ $name = drush_cwd() . '/' . $name;
+ }
+ }
+
+ if (is_dir($name)) {
+ drush_log(dt('Scanning dir @dir.', array('@dir' => $name)), 'success');
+ $files = file_scan_directory($name, '/.*\.xlf$/');
+ if (empty($files)) {
+ drush_set_error(dt('No files found to import in @name.', array('@name' => $name)));
+ }
+ }
+ else {
+ // Create the structure expected by the loop below.
+ $files = array($name => (object)array('name' => basename($name)));
+ }
+
+ $controller = tmgmt_file_format_controller('xlf');
+ foreach ($files as $path => $info) {
+ $job = $controller->validateImport($path);
+ if (empty($job)) {
+ drush_log(dt('No translation job found for @filename.', array('@filename' => $info->name)), 'error');
+ continue;
+ }
+
+ if ($job->isFinished()) {
+ drush_log(dt('Skipping @filename for finished job @name (#@id).', array('@filename' => $info->name, '@name' => $job->label(), '@id' => $job->tjid)), 'warning');
+ continue;
+ }
+
+ try {
+ // Validation successful, start import.
+ $job->addTranslatedData($controller->import($path));
+ drush_log(dt('Successfully imported file @filename for translation job @name (#@id).', array('@filename' => $info->name, '@name' => $job->label(), '@id' => $job->tjid)), 'success');
+ }
+ catch (Exception $e) {
+ drush_log(dt('Failed importing file @filename: @error', array('@filename' => $info->name, '@error' => $e->getMessage())), 'error');
+ }
+ }
+}
+?>
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.format.html.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.format.html.inc
new file mode 100644
index 00000000..eac570f3
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.format.html.inc
@@ -0,0 +1,103 @@
+getItems($conditions) as $item) {
+ $data = array_filter(tmgmt_flatten_data($item->getData()), '_tmgmt_filter_data');
+ foreach ($data as $key => $value) {
+ $items[$item->tjiid][$this->encodeIdSafeBase64($item->tjiid . '][' . $key)] = $value;
+ }
+ }
+ return theme('tmgmt_file_html_template', array(
+ 'tjid' => $job->tjid,
+ 'source_language' => $job->getTranslator()->mapToRemoteLanguage($job->source_language),
+ 'target_language' => $job->getTranslator()->mapToRemoteLanguage($job->target_language),
+ 'items' => $items,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function import($imported_file, $is_file = TRUE) {
+ $dom = new DOMDocument();
+ $dom->loadHTMLFile($imported_file);
+ $xml = simplexml_import_dom($dom);
+
+ $data = array();
+ foreach ($xml->xpath("//div[@class='atom']") as $atom) {
+ // Assets are our strings (eq fields in nodes).
+ $key = $this->decodeIdSafeBase64((string) $atom['id']);
+ $data[$key]['#text'] = (string) $atom;
+ }
+ return tmgmt_unflatten_data($data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateImport($imported_file) {
+ $dom = new DOMDocument();
+ if (!$dom->loadHTMLFile($imported_file)) {
+ return FALSE;
+ }
+ $xml = simplexml_import_dom($dom);
+
+ // Collect meta information.
+ $meta_tags = $xml->xpath('//meta');
+ $meta = array();
+ foreach ($meta_tags as $meta_tag) {
+ $meta[(string) $meta_tag['name']] = (string) $meta_tag['content'];
+ }
+
+ // Check required meta tags.
+ foreach (array('JobID', 'languageSource', 'languageTarget') as $name) {
+ if (!isset($meta[$name])) {
+ return FALSE;
+ }
+ }
+
+ // Attempt to load the job.
+ if (!$job = tmgmt_job_load($meta['JobID'])) {
+ drupal_set_message(t('The imported file job id @file_tjid is not available.', array(
+ '@file_tjid' => $job->tjid,
+ )), 'error');
+ return FALSE;
+ }
+
+ // Check language.
+ if ($meta['languageSource'] != $job->getTranslator()->mapToRemoteLanguage($job->source_language) ||
+ $meta['languageTarget'] != $job->getTranslator()->mapToRemoteLanguage($job->target_language)) {
+ return FALSE;
+ }
+
+ // Validation successful.
+ return $job;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.format.interface.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.format.interface.inc
new file mode 100644
index 00000000..df218043
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.format.interface.inc
@@ -0,0 +1,48 @@
+ markup.
+ * - tags are marked with .
+ * - tags are marked with tags. The title and alt
+ * attributes should have been extracted into elements, however are not
+ * as Trados studio triggers a fatal error in case there are two
+ * elements at the same level.
+ *
+ * Not implemented:
+ * - Attributes of element are written only as attributes of element
+ * instead of using x-html: prefix. This results in conflict with own
+ * element's attributes such as "id". The reason why x-html prefix has not
+ * been used is that Trados studio triggered fatal error on xml validation.
+ * - Translatable attributes like title and alt.
+ * @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html#elem_img
+ * - Forms - this is big part
+ * @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html#HTMLForms
+ * -
elements
+ * @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html#Elem_preformatted
+ */
+class TMGMTFileformatXLIFF extends XMLWriter implements TMGMTFileFormatInterface {
+
+ /**
+ * Contains a reference to the currently being exported job.
+ *
+ * @var TMGMTJob
+ */
+ protected $job;
+
+ protected $importedXML;
+ protected $importedTransUnits;
+
+ /**
+ * Adds a job item to the xml export.
+ *
+ * @param $item
+ * The job item entity.
+ */
+ protected function addItem(TMGMTJobItem $item) {
+ $this->startElement('group');
+ $this->writeAttribute('id', $item->tjiid);
+
+ // Add a note for the source label.
+ $this->writeElement('note', $item->getSourceLabel());
+
+ // @todo: Write in nested groups instead of flattening it.
+ $data = array_filter(tmgmt_flatten_data($item->getData()), '_tmgmt_filter_data');
+ foreach ($data as $key => $element) {
+ $this->addTransUnit($item->tjiid . '][' . $key, $element, $this->job);
+ }
+ $this->endElement();
+ }
+
+ /**
+ * Adds a single translation unit for a data element.
+ *
+ * @param $key
+ * The unique identifier for this data element.
+ * @param $element
+ * Array with the properties #text and optionally #label.
+ * @param TMGMTJob $job
+ * Translation job.
+ */
+ protected function addTransUnit($key, $element, TMGMTJob $job) {
+
+ $key_array = tmgmt_ensure_keys_array($key);
+
+ $this->startElement('trans-unit');
+ $this->writeAttribute('id', $key);
+ $this->writeAttribute('resname', $key);
+
+ $this->startElement('source');
+ $this->writeAttribute('xml:lang', $this->job->getTranslator()->mapToRemoteLanguage($this->job->source_language));
+
+ if ($job->getSetting('xliff_cdata')) {
+ $this->writeCdata(trim($element['#text']));
+ }
+ elseif ($job->getSetting('xliff_processing')) {
+ $this->writeRaw($this->processForExport($element['#text'], $key_array));
+ }
+ else {
+ $this->text($element['#text']);
+ }
+
+ $this->endElement();
+ $this->startElement('target');
+ $this->writeAttribute('xml:lang', $this->job->getTranslator()->mapToRemoteLanguage($this->job->target_language));
+
+ if (!empty($element['#translation']['#text'])) {
+ if ($job->getSetting('xliff_processing')) {
+ $this->writeRaw($this->processForExport($element['#translation']['#text'], $key_array));
+ }
+ else {
+ $this->text($element['#translation']['#text']);
+ }
+ }
+
+ $this->endElement();
+ if (isset($element['#label'])) {
+ $this->writeElement('note', $element['#label']);
+ }
+ $this->endElement();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function export(TMGMTJob $job, $conditions = array()) {
+
+ $this->job = $job;
+
+ $this->openMemory();
+ $this->setIndent(true);
+ $this->setIndentString(' ');
+ $this->startDocument('1.0', 'UTF-8');
+
+ // Root element with schema definition.
+ $this->startElement('xliff');
+ $this->writeAttribute('version', '1.2');
+ $this->writeAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2');
+ $this->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
+ $this->writeAttribute('xsi:schemaLocation', 'urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-strict.xsd');
+
+ // File element.
+ $this->startElement('file');
+ $this->writeAttribute('original', 'xliff-core-1.2-strict.xsd');
+ $this->writeAttribute('source-language', $job->getTranslator()->mapToRemoteLanguage($job->source_language));
+ $this->writeAttribute('target-language', $job->getTranslator()->mapToRemoteLanguage($job->target_language));
+ $this->writeAttribute('datatype', 'plaintext');
+ // Date needs to be in ISO-8601 UTC
+ $this->writeAttribute('date', date('Y-m-d\Th:m:i\Z'));
+
+ $this->startElement('header');
+ $this->startElement('phase-group');
+ $this->startElement('phase');
+ $this->writeAttribute('tool-id', 'tmgmt');
+ $this->writeAttribute('phase-name', 'extraction');
+ $this->writeAttribute('process-name', 'extraction');
+ $this->writeAttribute('job-id', $job->tjid);
+
+ $this->endElement();
+ $this->endElement();
+ $this->startElement('tool');
+ $this->writeAttribute('tool-id', 'tmgmt');
+ $this->writeAttribute('tool-name', 'Drupal Translation Management Tools');
+ $this->endElement();
+ $this->endElement();
+
+ $this->startElement('body');
+
+ foreach ($job->getItems($conditions) as $item) {
+ $this->addItem($item);
+ }
+
+ // End the body, file and xliff tags.
+ $this->endElement();
+ $this->endElement();
+ $this->endElement();
+ $this->endDocument();
+ return $this->outputMemory();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function import($imported_file, $is_file = TRUE) {
+ if (!$this->getImportedXML($imported_file, $is_file)) {
+ return FALSE;
+ }
+ $phase = $this->importedXML->xpath("//xliff:phase[@phase-name='extraction']");
+ $phase = reset($phase);
+ $job = tmgmt_job_load((string) $phase['job-id']);
+ return tmgmt_unflatten_data($this->getImportedTargets($job));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateImport($imported_file) {
+ // Validates imported XLIFF file.
+ // Checks:
+ // - Job ID
+ // - Target ans source languages
+ // - Content integrity.
+
+ if (!($xml = $this->getImportedXML($imported_file))) {
+ drupal_set_message(t('The imported file is not a valid XML.'), 'error');
+ return FALSE;
+ }
+ // Check if our phase information is there.
+ $phase = $xml->xpath("//xliff:phase[@phase-name='extraction']");
+ if ($phase) {
+ $phase = reset($phase);
+ }
+ else {
+ drupal_set_message(t('The imported file is missing required XLIFF phase information.'), 'error');
+ return FALSE;
+ }
+
+ // Check if the job has a valid job reference.
+ if (!isset($phase['job-id'])) {
+ drupal_set_message(t('The imported file does not contain a job reference.'), 'error');
+ return FALSE;
+ }
+
+ // Attempt to load the job if none passed.
+ $job = tmgmt_job_load((int) $phase['job-id']);
+ if (empty($job)) {
+ drupal_set_message(t('The imported file job id @file_tjid is not available.', array(
+ '@file_tjid' => $phase['job-id'],
+ )), 'error');
+ return FALSE;
+ }
+
+ // @todo We use the $job to addMessage in case of failure. However the job
+ // context is not safe at this point.
+
+ // Compare source language.
+ if (!isset($xml->file['source-language']) || $job->getTranslator()->mapToRemoteLanguage($job->source_language) != $xml->file['source-language']) {
+ $job->addMessage('The imported file source language @file_language does not match the job source language @job_language.', array(
+ '@file_language' => empty($xml->file['source-language']) ? t('none') : $xml->file['source-language'],
+ '@job_language' => $job->source_language,
+ ), 'error');
+ return FALSE;
+ }
+
+ // Compare target language.
+ if (!isset($xml->file['target-language']) || $job->getTranslator()->mapToRemoteLanguage($job->target_language) != $xml->file['target-language']) {
+ $job->addMessage('The imported file target language @file_language does not match the job target language @job_language.', array(
+ '@file_language' => empty($xml->file['target-language']) ? t('none') : $xml->file['target-language'],
+ '@job_language' => $job->target_language,
+ ), 'error');
+ return FALSE;
+ }
+
+ $targets = $this->getImportedTargets($job);
+
+ if (empty($targets)) {
+ $job->addMessage('The imported file seems to be missing translation.', 'error');
+ return FALSE;
+ }
+
+ // In case we do not do xliff processing we cannot do the elements
+ // count validation.
+ if (!$job->getSetting('xliff_processing')) {
+ return $job;
+ }
+
+ $reader = new XMLReader();
+ $xliff_validation = $job->getSetting('xliff_validation');
+
+ foreach ($targets as $id => $target) {
+ $array_key = tmgmt_ensure_keys_array($id);
+ $job_item = tmgmt_job_item_load(array_shift($array_key));
+ $count = 0;
+ $reader->XML('' . $target['#text'] . '');
+ while ($reader->read()) {
+ if (in_array($reader->name, array('translation', '#text'))) {
+ continue;
+ }
+ $count++;
+ }
+
+ if (!isset($xliff_validation[$id]) || $xliff_validation[$id] != $count) {
+ $job_item->addMessage('Failed to validate semantic integrity of %key element. Please check also the HTML code of the element in the review process.',
+ array('%key' => tmgmt_ensure_keys_string($array_key)));
+ }
+ }
+
+ // Validation successful.
+ return $job;
+ }
+
+ /**
+ * Returns the simple XMLElement object.
+ *
+ * @param string $imported_file
+ * Path to a file or an XML string to import.
+ * @param bool $is_file
+ * (optional) Whether $imported_file is the path to a file or not.
+ *
+ * @return bool|\SimpleXMLElement
+ * The parsed SimpleXMLElement object. FALSE in case of failed parsing.
+ */
+ protected function getImportedXML($imported_file, $is_file = TRUE) {
+ if (empty($this->importedXML)) {
+ // It is not possible to load the file directly with simplexml as it gets
+ // url encoded due to the temporary://. This is a PHP bug, see
+ // https://bugs.php.net/bug.php?id=61469
+ if ($is_file) {
+ $imported_file = file_get_contents($imported_file);
+ }
+
+ if (!($this->importedXML = simplexml_load_string($imported_file))) {
+ return FALSE;
+ }
+ // Register the XLIFF namespace, required for xpath.
+ $this->importedXML->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
+ }
+
+ return $this->importedXML;
+ }
+
+ protected function getImportedTargets(TMGMTJob $job) {
+ if (empty($this->importedXML)) {
+ return FALSE;
+ }
+
+ if (empty($this->importedTransUnits)) {
+ $reader = new XMLReader();
+ foreach ($this->importedXML->xpath('//xliff:trans-unit') as $unit) {
+ if (!$job->getSetting('xliff_processing')) {
+ $this->importedTransUnits[(string) $unit['id']]['#text'] = (string) $unit->target;
+ continue;
+ }
+
+ $reader->XML($unit->target->asXML());
+ $reader->read();
+ $this->importedTransUnits[(string) $unit['id']]['#text'] =
+ $this->processForImport($reader->readInnerXML(), $job);
+ }
+ }
+
+ return $this->importedTransUnits;
+ }
+
+ /**
+ * Processes trans-unit/target to rebuild back the HTML.
+ *
+ * @param string $translation
+ * Job data array.
+ * @param TMGMTJob $job
+ * Translation job.
+ *
+ * @return string
+ */
+ protected function processForImport($translation, TMGMTJob $job) {
+ // In case we do not want to do xliff processing return the translation as
+ // is.
+ if (!$job->getSetting('xliff_processing')) {
+ return $translation;
+ }
+
+ $reader = new XMLReader();
+ $reader->XML('' . $translation . '');
+ $text = '';
+
+ while ($reader->read()) {
+ // If the current element is text append it to the result text.
+ if ($reader->name == '#text' || $reader->name == '#cdata-section') {
+ $text .= $reader->value;
+ }
+ elseif ($reader->name == 'x') {
+ if ($reader->getAttribute('ctype') == 'lb') {
+ $text .= ' ';
+ }
+ }
+ elseif ($reader->name == 'ph') {
+ if ($reader->getAttribute('ctype') == 'image') {
+ $text .= 'moveToNextAttribute()) {
+ // @todo - we have to use x-html: prefixes for attributes.
+ if ($reader->name != 'ctype' && $reader->name != 'id') {
+ $text .= " {$reader->name}=\"{$reader->value}\"";
+ }
+ }
+ $text .= ' />';
+ }
+ }
+ }
+ return $text;
+ }
+
+ /**
+ * Helper function to process the source text.
+ *
+ * @param string $source
+ * Job data array.
+ * @param array $key_array
+ * The source item data key.
+ *
+ * @return string
+ */
+ protected function processForExport($source, array $key_array) {
+ $tjiid = $key_array[0];
+ $key_string = tmgmt_ensure_keys_string($key_array);
+ // The reason why we use DOMDocument object here and not just XMLReader
+ // is the DOMDocument's ability to deal with broken HTML.
+ $dom = new DOMDocument();
+ // We need to append the head with encoding so that special characters
+ // are read correctly.
+ $dom->loadHTML("" . $source . '');
+
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDOMIterator($dom),
+ RecursiveIteratorIterator::SELF_FIRST);
+
+ $writer = new XMLWriter();
+ $writer->openMemory();
+ $writer->startDocument('1.0', 'UTF-8');
+ $writer->startElement('wrapper');
+
+ $tray = array();
+ $non_pair_tags = array('br', 'img');
+
+ if (!isset($this->job->settings['xliff_validation'])) {
+ $this->job->settings['xliff_validation'] = array();
+ }
+ $xliff_validation = $this->job->settings['xliff_validation'];
+
+ /** @var DOMElement $node */
+ foreach ($iterator as $node) {
+
+ if (in_array($node->nodeName, array('html', 'body', 'head', 'meta'))) {
+ continue;
+ }
+
+ if ($node->nodeType === XML_ELEMENT_NODE) {
+ // Increment the elements count and compose element id.
+ if (!isset($xliff_validation[$key_string])) {
+ $xliff_validation[$key_string] = 0;
+ }
+ $xliff_validation[$key_string]++;
+ $id = 'tjiid' . $tjiid . '-' . $xliff_validation[$key_string];
+
+ $is_pair_tag = !in_array($node->nodeName, $non_pair_tags);
+
+ if ($is_pair_tag) {
+ $this->writeBPT($writer, $node, $id);
+ }
+ elseif ($node->nodeName == 'img') {
+ $this->writeIMG($writer, $node, $id);
+ }
+ elseif ($node->nodeName == 'br') {
+ $this->writeBR($writer, $node, $id);
+ }
+
+ // Add to tray new element info.
+ $tray[$id] = array(
+ 'name' => $node->nodeName,
+ 'id' => $id,
+ 'value' => $node->nodeValue,
+ 'built_text' => '',
+ 'is_pair_tag' => $is_pair_tag,
+ );
+
+ }
+ // The current node is a text.
+ elseif ($node->nodeName == '#text') {
+ // Add the node value to the text output.
+ $writer->writeCdata($this->toEntities($node->nodeValue));
+ foreach ($tray as &$info) {
+ $info['built_text'] .= $node->nodeValue;
+ }
+ }
+
+ // Reverse so that pair tags are closed in the expected order.
+ $reversed_tray = array_reverse($tray);
+ foreach ($reversed_tray as $_info) {
+ // If the build_text equals to the node value and it is not a pair tag
+ // add the end pair tag markup.
+ if ($_info['value'] == $_info['built_text'] && $_info['is_pair_tag']) {
+ // Count also for the closing elements.
+ $xliff_validation[$key_string]++;
+ $this->writeEPT($writer, $_info['name'], $_info['id']);
+ // When the end pair tag has been written unset the element info
+ // from the tray.
+ unset($tray[$_info['id']]);
+ }
+ }
+ }
+
+ // Set the xliff_validation data and save the job.
+ $this->job->settings['xliff_validation'] = $xliff_validation;
+ $this->job->save();
+
+ $writer->endElement();
+ // Load the output with XMLReader so that we can easily get the inner xml.
+ $reader = new XMLReader();
+ $reader->XML($writer->outputMemory());
+ $reader->read();
+ return $reader->readInnerXML();
+ }
+
+ /**
+ * Writes br tag.
+ *
+ * @param XMLWriter $writer
+ * Writer that writes the output.
+ * @param DOMElement $node
+ * Current node.
+ * @param $id
+ * Current node id.
+ */
+ protected function writeBR(XMLWriter $writer, DOMElement $node, $id) {
+ $writer->startElement('x');
+ $writer->writeAttribute('id', $id);
+ $writer->writeAttribute('ctype', 'lb');
+ $writer->endElement();
+ }
+
+ /**
+ * Writes beginning pair tag.
+ *
+ * @param XMLWriter $writer
+ * Writer that writes the output.
+ * @param DOMElement $node
+ * Current node.
+ * @param $id
+ * Current node id.
+ */
+ protected function writeBPT(XMLWriter $writer, DOMElement $node, $id) {
+ $beginning_tag = '<' . $node->nodeName;
+ if ($node->hasAttributes()) {
+ $attributes = array();
+ /** @var DOMAttr $attribute */
+ foreach ($node->attributes as $attribute) {
+ $attributes[] = $attribute->name . '="' . $attribute->value . '"';
+ }
+
+ $beginning_tag .= ' '. implode(' ', $attributes);
+ }
+ $beginning_tag .= '>';
+ $writer->startElement('bpt');
+ $writer->writeAttribute('id', $id);
+ $writer->text($beginning_tag);
+ $writer->endElement();
+ }
+
+ /**
+ * Writes ending pair tag.
+ *
+ * @param XMLWriter $writer
+ * Writer that writes the output.
+ * @param string $name
+ * Ending tag name.
+ * @param $id
+ * Current node id.
+ */
+ protected function writeEPT(XMLWriter $writer, $name, $id) {
+ $writer->startElement('ept');
+ $writer->writeAttribute('id', $id);
+ $writer->text('' . $name . '>');
+ $writer->endElement();
+ }
+
+ /**
+ * Writes img tag.
+ *
+ * Note that alt and title attributes are not written as sub elements as
+ * Trados studio is not able to deal with two sub elements at one level.
+ *
+ * @param XMLWriter $writer
+ * Writer that writes the output.
+ * @param DOMElement $node
+ * Current node.
+ * @param $id
+ * Current node id.
+ */
+ protected function writeIMG(XMLWriter $writer, DOMElement $node, $id) {
+ $writer->startElement('ph');
+ $writer->writeAttribute('id', $id);
+ $writer->writeAttribute('ctype', 'image');
+ foreach ($node->attributes as $attribute) {
+ // @todo - uncomment when issue with Trados/sub elements fixed.
+ /*
+ if (in_array($attribute->name, array('title', 'alt'))) {
+ continue;
+ }
+ */
+ $writer->writeAttribute($attribute->name, $attribute->value);
+ }
+ /*
+ if ($alt_attribute = $node->getAttribute('alt')) {
+ $writer->startElement('sub');
+ $writer->writeAttribute('id', $id . '-img-alt');
+ $writer->writeAttribute('ctype', 'x-img-alt');
+ $writer->text($alt_attribute);
+ $writer->endElement();
+ $this->elementsCount++;
+ }
+ if ($title_attribute = $node->getAttribute('title')) {
+ $writer->startElement('sub');
+ $writer->writeAttribute('id', $id . '-img-title');
+ $writer->writeAttribute('ctype', 'x-img-title');
+ $writer->text($title_attribute);
+ $writer->endElement();
+ $this->elementsCount++;
+ }
+ */
+ $writer->endElement();
+ }
+
+ /**
+ * Convert critical characters to HTML entities.
+ *
+ * DOMDocument will convert HTML entities to its actual characters. This can
+ * lead into situation when not allowed characters will appear in the content.
+ *
+ * @param string $string
+ * String to escape.
+ *
+ * @return string
+ * Escaped string.
+ */
+ protected function toEntities($string) {
+ return str_replace(array('&', '>', '<'), array('&', '>', '<'), $string);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.info b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.info
new file mode 100644
index 00000000..49c2b842
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.info
@@ -0,0 +1,20 @@
+name = Export / Import File
+description = A translator which allows you to export source data into a file and import the translated in return.
+package = Translation Management
+core = 7.x
+dependencies[] = tmgmt
+configure = admin/config/regional/tmgmt_translator
+files[] = tmgmt_file.plugin.inc
+files[] = tmgmt_file.ui.inc
+files[] = tmgmt_file.format.interface.inc
+files[] = tmgmt_file.format.xliff.inc
+files[] = tmgmt_file.format.html.inc
+files[] = tmgmt_file.recursive_iterator.inc
+files[] = tmgmt_file.test
+
+; Information added by Drupal.org packaging script on 2016-09-21
+version = "7.x-1.0-rc2+1-dev"
+core = "7.x"
+project = "tmgmt"
+datestamp = "1474446494"
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.module b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.module
new file mode 100644
index 00000000..df6b26b0
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.module
@@ -0,0 +1,195 @@
+ array(
+ 'label' => t('File translator'),
+ 'description' => t('File translator that exports and imports files.'),
+ 'plugin controller class' => 'TMGMTFileTranslatorPluginController',
+ 'ui controller class' => 'TMGMTFileTranslatorUIController',
+ ),
+ );
+}
+
+/**
+ * Implements hook_theme().
+ */
+function tmgmt_file_theme() {
+ return array(
+ 'tmgmt_file_html_template' => array(
+ 'path' => drupal_get_path('module', 'tmgmt_file') . '/templates',
+ 'template' => 'tmgmt_file_html_template',
+ ),
+ );
+}
+
+/**
+ * Import form submit callback.
+ */
+function tmgmt_file_import_form_submit($form, &$form_state) {
+ // Ensure we have the file uploaded.
+ $job = $form_state['tmgmt_job'];
+ $supported_formats = array_keys(tmgmt_file_format_plugin_info());
+ if ($file = file_save_upload('file', array('file_validate_extensions' => array(implode(' ', $supported_formats))))) {
+ $extension = pathinfo($file->uri, PATHINFO_EXTENSION);
+ $controller = tmgmt_file_format_controller($extension);
+ if ($controller) {
+ // Validate the file on job.
+ $validated_job = $controller->validateImport($file->uri, $job);
+ if (!$validated_job) {
+ $job->addMessage('Failed to validate file, import aborted.', array(), 'error');
+ }
+ elseif ($validated_job->tjid != $job->tjid) {
+ $job->addMessage('The imported file job id @file_tjid does not match the job id @job_tjid.', array(
+ '@file_tjid' => $validated_job->tjid,
+ '@job_tjid' => $job->tjid,
+ ), 'error');
+ }
+ else {
+ try {
+ // Validation successful, start import.
+ $job->addTranslatedData($controller->import($file->uri));
+ $job->addMessage('Successfully imported file.');
+ } catch (Exception $e) {
+ $job->addMessage('File import failed with the following message: @message', array('@message' => $e->getMessage()), 'error');
+ }
+ }
+ }
+ }
+ foreach ($job->getMessagesSince() as $message) {
+ // Ignore debug messages.
+ if ($message->type == 'debug') {
+ continue;
+ }
+ if ($text = $message->getMessage()) {
+ drupal_set_message(filter_xss($text), $message->type);
+ }
+ }
+}
+
+/**
+ * Returns information about file format plugins.
+ *
+ * @param $plugin
+ * (Optional) Name of a plugin/extension.
+ *
+ * @return array
+ * If a plugin name is provided, information about that plugin, an array of
+ * plugin information otherwise. The information of each plugin consists of
+ * the label and plugin controller class, keyed by the plugin name which is
+ * also the extension for that file format.
+ */
+function tmgmt_file_format_plugin_info($plugin = NULL) {
+ return _tmgmt_plugin_info('file_format', $plugin);
+}
+
+/**
+ * Returns an array of file format plugin labels.
+ */
+function tmgmt_file_format_plugin_labels() {
+ return _tmgmt_plugin_labels('file_format');
+}
+
+/**
+ * Returns the file format plugin controller.
+ *
+ * @param $plugin
+ * (Optional) Name of a plugin/extension.
+ *
+ * @return TMGMTFileFormatInterface
+ * Either a specific file format plugin controller instance or an array of
+ * available controllers.
+ */
+function tmgmt_file_format_controller($plugin = NULL) {
+ return _tmgmt_plugin_controller('file_format', $plugin);
+}
+
+/**
+ * Implements hook_tmgmt_file_format_info().
+ */
+function tmgmt_file_tmgmt_file_format_plugin_info() {
+ return array(
+ 'xlf' => array(
+ 'label' => t('XLIFF'),
+ 'plugin controller class' => 'TMGMTFileFormatXLIFF',
+ ),
+ 'html' => array(
+ 'label' => t('HTML'),
+ 'plugin controller class' => 'TMGMTFileFormatHTML',
+ ),
+ );
+}
+
+/**
+ * Implements hook_tmgmt_job_delete().
+ */
+function tmgmt_file_tmgmt_job_delete(TMGMTJob $job) {
+ $translator = $job->getTranslator();
+
+ // Ignore jobs that don't have a file translator.
+ if (!$translator || $translator->plugin != 'file') {
+ return;
+ }
+ // Check if there are any files that need to be deleted.
+ // @todo There doesn't seem to be an API function for this...
+ $args = array(
+ ':module' => 'tmgmt_file',
+ ':type' => 'tmgmt_job',
+ ':id' => $job->tjid,
+ );
+ $result = db_query('SELECT fid FROM {file_usage} WHERE module = :module and type = :type and id = :id', $args);
+ $fids = $result->fetchCol();
+ if (!empty($fids)) {
+ foreach (file_load_multiple($fids) as $file) {
+ file_usage_delete($file, 'tmgmt_file', 'tmgmt_job', $job->tjid);
+ // It is very unlikely that these files are used anywhere else. Delete it.
+ file_delete($file);
+ }
+ }
+}
+
+/**
+ * Implements hook_file_download().
+ */
+function tmgmt_file_file_download($uri) {
+ // Get the file record based on the URI. If not in the database just return.
+ $files = file_load_multiple(array(), array('uri' => $uri));
+ if (count($files)) {
+ foreach ($files as $item) {
+ // Since some database servers sometimes use a case-insensitive comparison
+ // by default, double check that the filename is an exact match.
+ if ($item->uri === $uri) {
+ $file = $item;
+ break;
+ }
+ }
+ }
+ if (!isset($file)) {
+ return;
+ }
+
+ // Check if this file belongs to a job.
+ $usage_list = file_usage_list($file);
+ if (!isset($usage_list['tmgmt_file']['tmgmt_job'])) {
+ return;
+ }
+
+ foreach (tmgmt_job_load_multiple(array_keys($usage_list['tmgmt_file']['tmgmt_job'])) as $job) {
+ if (tmgmt_job_access('view', $job)) {
+ // Access is granted.
+ $headers = file_get_content_headers($file);
+ return $headers;
+ }
+ }
+
+ // Returning nothing means access denied unless another module specifically
+ // grants access.
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.plugin.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.plugin.inc
new file mode 100644
index 00000000..e0bfc51c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.plugin.inc
@@ -0,0 +1,60 @@
+tjid . '_' . $job->source_language . '_' . $job->target_language;
+
+ $export = tmgmt_file_format_controller($job->getSetting('export_format'));
+
+ $path = $job->getSetting('scheme') . '://tmgmt_file/' . $name . '.' . $job->getSetting('export_format');
+ $dirname = dirname($path);
+ if (file_prepare_directory($dirname, FILE_CREATE_DIRECTORY)) {
+ $file = file_save_data($export->export($job), $path);
+ file_usage_add($file, 'tmgmt_file', 'tmgmt_job', $job->tjid);
+ $job->submitted('Exported file can be downloaded here.', array('!link' => file_create_url($file->uri)));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCheckoutSettings(TMGMTJob $job) {
+ return $job->getTranslator()->getSetting('allow_override');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultSettings() {
+ return array(
+ 'export_format' => 'xlf',
+ 'allow_override' => TRUE,
+ 'scheme' => 'public',
+ // Making this setting TRUE by default is more appropriate, however we
+ // need to make it FALSE due to backwards compatibility.
+ 'xliff_processing' => FALSE,
+ 'xliff_cdata' => FALSE,
+ );
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.recursive_iterator.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.recursive_iterator.inc
new file mode 100644
index 00000000..36f2f83e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.recursive_iterator.inc
@@ -0,0 +1,99 @@
+position = 0;
+ $this->nodeList = $domNode->childNodes;
+ }
+
+ /**
+ * Returns the current DOMNode.
+ *
+ * @return DOMNode
+ * Current DOMNode object.
+ */
+ public function current() {
+ return $this->nodeList->item($this->position);
+ }
+
+ /**
+ * Returns an iterator for the current iterator entry.
+ *
+ * @return RecursiveDOMIterator
+ * Iterator with children elements.
+ */
+ public function getChildren() {
+ return new self($this->current());
+ }
+
+ /**
+ * Checks if current element has children.
+ *
+ * @return bool
+ * Has children.
+ */
+ public function hasChildren() {
+ return $this->current()->hasChildNodes();
+ }
+
+ /**
+ * Returns the current position.
+ *
+ * @return int
+ * Current position
+ */
+ public function key() {
+ return $this->position;
+ }
+
+ /**
+ * Moves the current position to the next element.
+ */
+ public function next() {
+ $this->position++;
+ }
+
+ /**
+ * Rewind the Iterator to the first element.
+ */
+ public function rewind() {
+ $this->position = 0;
+ }
+
+ /**
+ * Checks if current position is valid.
+ *
+ * @return bool
+ * Is valid.
+ */
+ public function valid() {
+ return $this->position < $this->nodeList->length;
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.test b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.test
new file mode 100644
index 00000000..28280b8c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.test
@@ -0,0 +1,536 @@
+ 'File Translator tests',
+ 'description' => 'Tests the file translator plugin integration.',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('tmgmt_file', 'tmgmt_ui'));
+ $this->loginAsAdmin();
+ $this->setEnvironment('de');
+ }
+
+ /**
+ * Test the content processing for XLIFF export and import.
+ */
+ function testXLIFFTextProcessing() {
+ $translator = $this->createTranslator();
+ $translator->plugin = 'file';
+ $translator->settings = array(
+ 'export_format' => 'xlf',
+ 'xliff_processing' => TRUE,
+ );
+ $translator->save();
+
+ // Get the source text.
+ $source_text = trim(file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'));
+
+ // Create the reader instance, it will be used through the tests.
+ $reader = new XMLReader();
+ $xliff_elements = array('bpt', 'ept', 'ph', 'x', '#text', '#cdata-section', 'content');
+
+ // ==== First test the whole cycle ==== //
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->addItem('test_html_source', 'test', '1');
+
+ // Requesting translation will mask the html.
+ $job->requestTranslation();
+ $content = $this->getTransUnitsContent($job);
+ // Test that the exported trans unit contains only xliff elements.
+ $reader->XML('' . $content[0]['source'] . '');
+ while ($reader->read()) {
+ if (!in_array($reader->name, $xliff_elements)) {
+ $this->fail(t('The source contains unexpected element %element', array('%element' => $reader->name)));
+ }
+ }
+ $reader->XML('' . $content[0]['target'] . '');
+ while ($reader->read()) {
+ if (!in_array($reader->name, $xliff_elements)) {
+ $this->fail(t('The target contains unexpected element %element', array('%element' => $reader->name)));
+ }
+ }
+
+ // Import the file, make sure all the html has been revealed and no xliff
+ // elements are present in the job translation.
+ $messages = $job->getMessages();
+ $message = reset($messages);
+ $translated_file = 'public://tmgmt_file/translated.xlf';
+ $this->createTranslationFile($message->variables['!link'], 'one paragraph', 'one translated paragraph', $translated_file);
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $translated_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+ // Reset caches and reload job.
+ entity_get_controller('tmgmt_job')->resetCache();
+ entity_get_controller('tmgmt_job_item')->resetCache();
+ $job = tmgmt_job_load($job->tjid);
+
+ // Do the comparison of the translation text and the source. It must be the
+ // same as there was no change done to the translation.
+ $item_data = $job->getData(array(1, 'dummy', 'deep_nesting'));
+ $this->assertEqual(trim($item_data[1]['#translation']['#text']), str_replace('one paragraph', 'one translated paragraph', $source_text));
+ $job_items = $job->getItems();
+ /** @var TMGMTJobItem $job_item */
+ $job_item = array_shift($job_items);
+ // Job item must be in review.
+ $this->assertTrue($job_item->isNeedsReview());
+
+ $this->assertIntegrityCheck($job, FALSE);
+
+ // ==== Test integrity check ==== //
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->addItem('test_html_source', 'test', '1');
+ $job->requestTranslation();
+
+ $messages = $job->getMessages();
+ $message = reset($messages);
+ // Get the xml content and remove the element representing . This will
+ // result in different element counts in the source and target and should
+ // trigger an error and not import the translation.
+ $translated_file = 'public://tmgmt_file/translated.xlf';
+ $this->createTranslationFile($message->variables['!link'], '', '', $translated_file);
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $translated_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+ entity_get_controller('tmgmt_job')->resetCache();
+ entity_get_controller('tmgmt_job_item')->resetCache();
+ $job = tmgmt_job_load($job->tjid);
+
+ $this->assertIntegrityCheck($job);
+
+ // Set the XLIFF processing to FALSE and test it results in the source
+ // text not being XLIFF processed.
+ $translator->settings['xliff_processing'] = FALSE;
+ $translator->save();
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->addItem('test_html_source', 'test', '1');
+ $job->requestTranslation();
+ $targets = $this->getTransUnitsContent($job);
+ $this->assertEqual(trim(html_entity_decode($targets['0']['source'])), $source_text);
+ }
+
+ /**
+ * Test the CDATA option for XLIFF export and import.
+ */
+ function testXLIFFCDATA() {
+ $translator = $this->createTranslator();
+ $translator->plugin = 'file';
+ $translator->settings = array(
+ 'export_format' => 'xlf',
+ 'xliff_cdata' => TRUE,
+ );
+ $translator->save();
+
+ // Get the source text.
+ $source_text = trim(file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'));
+
+ // Create a new job.
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->addItem('test_html_source', 'test', '1');
+ $job->requestTranslation();
+ $messages = $job->getMessages();
+ $message = reset($messages);
+
+ $download_url = $message->variables['!link'];
+ // Get XLIFF content.
+ $xliff = file_get_contents($download_url);
+
+ $dom = new \DOMDocument();
+ $dom->loadXML($xliff);
+ $this->assertTrue($dom->schemaValidate(drupal_get_path('module', 'tmgmt_file') . '/xliff-core-1.2-strict.xsd'));
+
+ // "Translate" items.
+ $xml = simplexml_import_dom($dom);
+ $translated_text = array();
+ foreach ($xml->file->body->children() as $group) {
+ foreach ($group->children() as $transunit) {
+ if ($transunit->getName() == 'trans-unit') {
+ // The target should be empty.
+ $this->assertEqual($transunit->target, '');
+
+ // Update translations using CDATA.
+ $node = dom_import_simplexml($transunit->target);
+ $owner = $node->ownerDocument;
+ $node->appendChild($owner->createCDATASection($xml->file['target-language'] . '_' . (string) $transunit->source));
+
+ // Store the text to allow assertions later on.
+ $translated_text[(string) $group['id']][(string) $transunit['id']] = (string) $transunit->target;
+ }
+ }
+ }
+
+ $translated_file = 'public://tmgmt_file/translated file.xlf';
+ $xml->asXML($translated_file);
+
+ // Import the file and check translation for the "dummy" item.
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $translated_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+ $this->clickLink(t('review'));
+ foreach ($translated_text[1] as $key => $value) {
+ $this->assertText(htmlspecialchars($value));
+ }
+ }
+
+ /**
+ * Gets trans-unit content from the XLIFF file that has been exported for the
+ * given job as last.
+ */
+ protected function getTransUnitsContent(TMGMTJob $job) {
+ $messages = $job->getMessages();
+ $message = reset($messages);
+ $download_url = $message->variables['!link'];
+ $xml_string = file_get_contents($download_url);
+ $xml = simplexml_load_string($xml_string);
+
+ // Register the xliff namespace, required for xpath.
+ $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
+
+ $reader = new XMLReader();
+ $data = array();
+ $i = 0;
+ foreach ($xml->xpath('//xliff:trans-unit') as $unit) {
+ $reader->XML($unit->source->asXML());
+ $reader->read();
+ $data[$i]['source'] = $reader->readInnerXML();
+ $reader->XML($unit->target->asXML());
+ $reader->read();
+ $data[$i]['target'] = $reader->readInnerXML();
+ $i++;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Tests export and import for the HTML format.
+ */
+ function testHTML() {
+ $translator = $this->createTranslator();
+ $translator->plugin = 'file';
+ $translator->settings = array(
+ 'export_format' => 'html',
+ );
+ $translator->save();
+
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->addItem('test_source', 'test', '1');
+ $job->addItem('test_source', 'test', '2');
+
+ $job->requestTranslation();
+ $messages = $job->getMessages();
+ $message = reset($messages);
+
+ $download_url = $message->variables['!link'];
+
+ // "Translate" items.
+ $xml = simplexml_load_file($download_url);
+ $translated_text = array();
+ foreach ($xml->body->children() as $group) {
+ for ($i = 0; $i < $group->count(); $i++) {
+ // This does not actually override the whole object, just the content.
+ $group->div[$i] = (string) $xml->head->meta[3]['content'] . '_' . (string) $group->div[$i];
+ // Store the text to allow assertions later on.
+ $translated_text[(string) $group['id']][(string) $group->div[$i]['id']] = (string) $group->div[$i];
+ }
+ }
+
+ $translated_file = 'public://tmgmt_file/translated.html';
+ $xml->asXML($translated_file);
+ $this->importFile($translated_file, $translated_text, $job);
+ }
+
+ /**
+ * Tests import and export for the XLIFF format.
+ */
+ function testXLIFF() {
+ $translator = $this->createTranslator();
+ $translator->plugin = 'file';
+ $translator->settings = array(
+ 'export_format' => 'xlf',
+ );
+ $translator->save();
+
+ // Set multiple data items for the source.
+ variable_set('tmgmt_test_source_data', array(
+ 'dummy' => array(
+ 'deep_nesting' => array(
+ '#text' => file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html') . ' @id.',
+ '#label' => 'Label of deep nested item @id',
+ ),
+ ),
+ 'another_item' => array(
+ '#text' => 'Text of another item @id.',
+ '#label' => 'Label of another item @id.',
+ ),
+ ));
+
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $first_item = $job->addItem('test_source', 'test', '1');
+ // Keep the first item data for later use.
+ $first_item_data = tmgmt_flatten_data($first_item->getData());
+ $job->addItem('test_source', 'test', '2');
+
+ $job->requestTranslation();
+ $messages = $job->getMessages();
+ $message = reset($messages);
+
+ $download_url = $message->variables['!link'];
+ $xliff = file_get_contents($download_url);
+ $dom = new DOMDocument();
+ $dom->loadXML($xliff);
+ $this->assertTrue($dom->schemaValidate(drupal_get_path('module', 'tmgmt_file') . '/xliff-core-1.2-strict.xsd'));
+
+ // "Translate" items.
+ $xml = simplexml_import_dom($dom);
+ $translated_text = array();
+ foreach ($xml->file->body->children() as $group) {
+ foreach ($group->children() as $transunit) {
+ if ($transunit->getName() == 'trans-unit') {
+ // The target should be empty.
+ $this->assertEqual($transunit->target, '');
+ $transunit->target = $xml->file['target-language'] . '_' . (string) $transunit->source;
+ // Store the text to allow assertions later on.
+ $translated_text[(string) $group['id']][(string) $transunit['id']] = (string) $transunit->target;
+ }
+ }
+ }
+
+ // Change the job id to a non-existing one and try to import it.
+ $wrong_xml = clone $xml;
+ $wrong_xml->file->header->{'phase-group'}->phase['job-id'] = 500;
+ $wrong_file = 'public://tmgmt_file/wrong_file.xlf';
+ $wrong_xml->asXML($wrong_file);
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $wrong_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+ $this->assertText(t('Failed to validate file, import aborted.'));
+
+ // Change the job id to a wrong one and try to import it.
+ $wrong_xml = clone $xml;
+ $second_job = $this->createJob();
+ $second_job->translator = $translator->name;
+ // We need to add the elements count value into settings, otherwise the
+ // validation will fail on integrity check.
+ $second_job->settings['xliff_validation'][1] = 0;
+ $second_job->settings['xliff_validation'][2] = 0;
+ $second_job->save();
+ $wrong_xml->file->header->{'phase-group'}->phase['job-id'] = $second_job->tjid;
+ $wrong_file = 'public://tmgmt_file/wrong_file.xlf';
+ $wrong_xml->asXML($wrong_file);
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $wrong_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+ $this->assertRaw(t('The imported file job id @file_tjid does not match the job id @job_tjid.', array(
+ '@file_tjid' => $second_job->tjid,
+ '@job_tjid' => $job->tjid,
+ )));
+
+ $translated_file = 'public://tmgmt_file/translated file.xlf';
+ $xml->asXML($translated_file);
+
+ // Import the file and accept translation for the "dummy" item.
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $translated_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+ $this->clickLink(t('review'));
+ $this->drupalPostAJAX(NULL, NULL, array('reviewed-dummy|deep_nesting' => '✓'));
+
+ // Update the translation for "another" item and import.
+ $xml->file->body->group[0]->{'trans-unit'}[1]->target = $xml->file->body->group[0]->{'trans-unit'}[1]->target . ' updated';
+ $xml->asXML($translated_file);
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $translated_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+
+ // At this point we must have the "dummy" item accepted and intact. The
+ // "another" item must have updated translation.
+ $this->clickLink(t('review'));
+ $this->assertFieldByName('dummy|deep_nesting[translation]', 'de_' . $first_item_data['dummy][deep_nesting']['#text']);
+ $this->assertFieldByName('another_item[translation]', 'de_' . $first_item_data['another_item']['#text'] . ' updated');
+
+ // Now finish the import/save as completed process doing another extra
+ // import. The extra import will test that a duplicate import of the same
+ // file does not break the process.
+ $this->importFile($translated_file, $translated_text, $job);
+
+ $this->assertNoText(t('Import translated file'));
+
+ // Create a job, assign to the file translator and delete before attaching
+ // a file.
+ $other_job = $this->createJob();
+ $other_job->translator = $translator->name;
+ $other_job->save();
+ $other_job->delete();
+ // Make sure the file of the other job still exists.
+ $response = drupal_http_request($download_url);
+ $this->assertEqual(200, $response->code);
+
+ // Delete the job and then make sure that the file has been deleted.
+ $job->delete();
+ $response = drupal_http_request($download_url);
+ $this->assertEqual(404, $response->code);
+ }
+
+
+ /**
+ * Tests storing files in the private file system.
+ */
+ function testPrivate() {
+ // Enable the private file system.
+ variable_set('file_private_path', variable_get('file_public_path') . '/private');
+
+ // Create a translator using the private file system.
+ // @todo: Test the configuration UI.
+ $translator = $this->createTranslator();
+ $translator->plugin = 'file';
+ $translator->settings = array(
+ 'export_format' => 'xlf',
+ 'scheme' => 'private',
+ );
+ $translator->save();
+
+ $job = $this->createJob();
+ $job->translator = $translator->name;
+ $job->addItem('test_source', 'test', '1');
+ $job->addItem('test_source', 'test', '2');
+
+ $job->requestTranslation();
+ $messages = $job->getMessages();
+ $message = reset($messages);
+
+ $download_url = $message->variables['!link'];
+ $this->drupalGet($download_url);
+ // Verify that the URL is served using the private file system and the
+ // access checks work.
+ $this->assertTrue(preg_match('|system/files|', $download_url));
+ $this->assertResponse(200);
+
+ $this->drupalLogout();
+ // Verify that access is now protected.
+ $this->drupalGet($download_url);
+ $this->assertResponse(403);
+ }
+
+ protected function importFile($translated_file, $translated_text, TMGMTJob $job) {
+ // To test the upload form functionality, navigate to the edit form.
+ $uri = $job->uri();
+ $edit = array(
+ 'files[file]' => $translated_file,
+ );
+ $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
+
+ // Make sure the translations have been imported correctly.
+ $this->assertNoText(t('In progress'));
+ // @todo: Enable this assertion once new releases for views and entity
+ // module are out.
+ //$this->assertText(t('Needs review'));
+
+ // Review both items.
+ $this->clickLink(t('review'));
+ foreach ($translated_text[1] as $key => $value) {
+ $this->assertText(check_plain($value));
+ }
+ foreach ($translated_text[2] as $key => $value) {
+ $this->assertNoText(check_plain($value));
+ }
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+ // Review both items.
+ $this->clickLink(t('review'));
+ foreach ($translated_text[1] as $key => $value) {
+ $this->assertNoText(check_plain($value));
+ }
+ foreach ($translated_text[2] as $key => $value) {
+ $this->assertText(check_plain($value));
+ }
+ $this->drupalPost(NULL, array(), t('Save as completed'));
+ // @todo: Enable this assertion once new releases for views and entity
+ // module are out.
+ //$this->assertText(t('Accepted'));
+ $this->assertText(t('Finished'));
+ $this->assertNoText(t('Needs review'));
+ }
+
+ /**
+ * Creates a translated XLIFF file based on the replacement definition.
+ *
+ * @param string $source_file
+ * Source file name.
+ * @param $search
+ * String to search in the source.
+ * @param $replace
+ * String to replace it with in the target.
+ * @param $translated_file
+ * Name of the file to write.
+ */
+ protected function createTranslationFile($source_file, $search, $replace, $translated_file) {
+ $xml_string = file_get_contents($source_file);
+ preg_match('/(.+)<\/source>/s', $xml_string, $matches);
+ $target = str_replace($search, $replace, $matches[1]);
+ if ($replace) {
+ $this->assertTrue(strpos($target, $replace) !== FALSE, 'String replaced in translation');
+ }
+ $translated_xml_string = str_replace('', '' . $target . '', $xml_string);
+ file_put_contents($translated_file, $translated_xml_string);
+ }
+
+ /**
+ * Asserts import integrity for a job.
+ *
+ * @param TMGMTJob $job
+ * The job to check.
+ * @param bool $expected
+ * (optional) If an integrity failed message is expected or not, defaults
+ * to FALSE.
+ */
+ protected function assertIntegrityCheck(TMGMTJob $job, $expected = TRUE) {
+ $integrity_check_failed = FALSE;
+ /** @var TMGMTMessage $message */
+ foreach ($job->getMessages() as $message) {
+ if ($message->getMessage() == t('Failed to validate semantic integrity of %key element. Please check also the HTML code of the element in the review process.', array('%key' => 'dummy][deep_nesting'))) {
+ $integrity_check_failed = TRUE;
+ break;
+ }
+ }
+ // Check if the message was found or not, based on the expected argument.
+ if ($expected) {
+ $this->assertTrue($integrity_check_failed, 'The validation of semantic integrity must fail.');
+ }
+ else {
+ $this->assertFalse($integrity_check_failed, 'The validation of semantic integrity must not fail.');
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.ui.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.ui.inc
new file mode 100644
index 00000000..94a6f580
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/tmgmt_file.ui.inc
@@ -0,0 +1,115 @@
+ 'radios',
+ '#title' => t('Export to'),
+ '#options' => tmgmt_file_format_plugin_labels(),
+ '#default_value' => $translator->getSetting('export_format'),
+ '#description' => t('Please select the format you want to export data.'),
+ );
+
+ $form['xliff_cdata'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('XLIFF CDATA'),
+ '#description' => t('Check to use CDATA for import/export.'),
+ '#default_value' => $translator->getSetting('xliff_cdata'),
+ );
+
+ $form['xliff_processing'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Extended XLIFF processing'),
+ '#description' => t('Check to further process content semantics and mask HTML tags instead just escaping it.'),
+ '#default_value' => $translator->getSetting('xliff_processing'),
+ );
+
+ $form['xliff_message'] = array(
+ '#type' => 'item',
+ '#markup' => t('By selecting CDATA option, XLIFF processing will be ignored.'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ $form['allow_override'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Allow to override the format per job'),
+ '#default_value' => $translator->getSetting('allow_override'),
+ );
+
+ // Any visible, writeable wrapper can potentially be used for the files
+ // directory, including a remote file system that integrates with a CDN.
+ foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $info) {
+ $options[$scheme] = check_plain($info['description']);
+ }
+
+ if (!empty($options)) {
+ $form['scheme'] = array(
+ '#type' => 'radios',
+ '#title' => t('Download method'),
+ '#default_value' => $translator->getSetting('scheme'),
+ '#options' => $options,
+ '#description' => t('Choose the location where exported files should be stored. The usage of a protected location (e.g. private://) is recommended to prevent unauthorized access.'),
+ );
+ }
+
+ return parent::pluginSettingsForm($form, $form_state, $translator);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
+ if ($job->getTranslator()->getSetting('allow_override')) {
+ $form['export_format'] = array(
+ '#type' => 'radios',
+ '#title' => t('Export to'),
+ '#options' => tmgmt_file_format_plugin_labels(),
+ '#default_value' => $job->getTranslator()->getSetting('export_format'),
+ '#description' => t('Please select the format you want to export data.'),
+ );
+ }
+ return parent::checkoutSettingsForm($form, $form_state, $job);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkoutInfo(TMGMTJob $job) {
+ // If the job is finished, it's not possible to import translations anymore.
+ if ($job->isFinished()) {
+ return parent::checkoutInfo($job);
+ }
+ $form = array(
+ '#type' => 'fieldset',
+ '#title' => t('Import translated file'),
+ );
+
+ $supported_formats = array_keys(tmgmt_file_format_plugin_info());
+ $form['file'] = array(
+ '#type' => 'file',
+ '#title' => t('File file'),
+ '#size' => 50,
+ '#description' => t('Supported formats: @formats.', array('@formats' => implode(', ', $supported_formats))),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Import'),
+ '#submit' => array('tmgmt_file_import_form_submit'),
+ );
+ return $this->checkoutInfoWrapper($job, $form);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/file/xliff-core-1.2-strict.xsd b/sites/all/modules/contrib/localisation/tmgmt/translators/file/xliff-core-1.2-strict.xsd
new file mode 100644
index 00000000..3ce2a8e8
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/file/xliff-core-1.2-strict.xsd
@@ -0,0 +1,2223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Values for the attribute 'context-type'.
+
+
+
+
+ Indicates a database content.
+
+
+
+
+ Indicates the content of an element within an XML document.
+
+
+
+
+ Indicates the name of an element within an XML document.
+
+
+
+
+ Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found.
+
+
+
+
+ Indicates a the number of parameters contained within the <source>.
+
+
+
+
+ Indicates notes pertaining to the parameters in the <source>.
+
+
+
+
+ Indicates the content of a record within a database.
+
+
+
+
+ Indicates the name of a record within a database.
+
+
+
+
+ Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file.
+
+
+
+
+
+
+ Values for the attribute 'count-type'.
+
+
+
+
+ Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts.
+
+
+
+
+ Indicates the count units are translation units existing already in the same document.
+
+
+
+
+ Indicates a total count.
+
+
+
+
+
+
+ Values for the attribute 'ctype' when used other elements than <ph> or <x>.
+
+
+
+
+ Indicates a run of bolded text.
+
+
+
+
+ Indicates a run of text in italics.
+
+
+
+
+ Indicates a run of underlined text.
+
+
+
+
+ Indicates a run of hyper-text.
+
+
+
+
+
+
+ Values for the attribute 'ctype' when used with <ph> or <x>.
+
+
+
+
+ Indicates a inline image.
+
+
+
+
+ Indicates a page break.
+
+
+
+
+ Indicates a line break.
+
+
+
+
+
+
+
+
+
+
+
+ Values for the attribute 'datatype'.
+
+
+
+
+ Indicates Active Server Page data.
+
+
+
+
+ Indicates C source file data.
+
+
+
+
+ Indicates Channel Definition Format (CDF) data.
+
+
+
+
+ Indicates ColdFusion data.
+
+
+
+
+ Indicates C++ source file data.
+
+
+
+
+ Indicates C-Sharp data.
+
+
+
+
+ Indicates strings from C, ASM, and driver files data.
+
+
+
+
+ Indicates comma-separated values data.
+
+
+
+
+ Indicates database data.
+
+
+
+
+ Indicates portions of document that follows data and contains metadata.
+
+
+
+
+ Indicates portions of document that precedes data and contains metadata.
+
+
+
+
+ Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import).
+
+
+
+
+ Indicates standard user input screen data.
+
+
+
+
+ Indicates HyperText Markup Language (HTML) data - document instance.
+
+
+
+
+ Indicates content within an HTML document’s <body> element.
+
+
+
+
+ Indicates Windows INI file data.
+
+
+
+
+ Indicates Interleaf data.
+
+
+
+
+ Indicates Java source file data (extension '.java').
+
+
+
+
+ Indicates Java property resource bundle data.
+
+
+
+
+ Indicates Java list resource bundle data.
+
+
+
+
+ Indicates JavaScript source file data.
+
+
+
+
+ Indicates JScript source file data.
+
+
+
+
+ Indicates information relating to formatting.
+
+
+
+
+ Indicates LISP source file data.
+
+
+
+
+ Indicates information relating to margin formats.
+
+
+
+
+ Indicates a file containing menu.
+
+
+
+
+ Indicates numerically identified string table.
+
+
+
+
+ Indicates Maker Interchange Format (MIF) data.
+
+
+
+
+ Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute.
+
+
+
+
+ Indicates GNU Machine Object data.
+
+
+
+
+ Indicates Message Librarian strings created by Novell's Message Librarian Tool.
+
+
+
+
+ Indicates information to be displayed at the bottom of each page of a document.
+
+
+
+
+ Indicates information to be displayed at the top of each page of a document.
+
+
+
+
+ Indicates a list of property values (e.g., settings within INI files or preferences dialog).
+
+
+
+
+ Indicates Pascal source file data.
+
+
+
+
+ Indicates Hypertext Preprocessor data.
+
+
+
+
+ Indicates plain text file (no formatting other than, possibly, wrapping).
+
+
+
+
+ Indicates GNU Portable Object file.
+
+
+
+
+ Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc.
+
+
+
+
+ Indicates Windows .NET binary resources.
+
+
+
+
+ Indicates Windows .NET Resources.
+
+
+
+
+ Indicates Rich Text Format (RTF) data.
+
+
+
+
+ Indicates Standard Generalized Markup Language (SGML) data - document instance.
+
+
+
+
+ Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD).
+
+
+
+
+ Indicates Scalable Vector Graphic (SVG) data.
+
+
+
+
+ Indicates VisualBasic Script source file.
+
+
+
+
+ Indicates warning message.
+
+
+
+
+ Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file).
+
+
+
+
+ Indicates Extensible HyperText Markup Language (XHTML) data - document instance.
+
+
+
+
+ Indicates Extensible Markup Language (XML) data - document instance.
+
+
+
+
+ Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD).
+
+
+
+
+ Indicates Extensible Stylesheet Language (XSL) data.
+
+
+
+
+ Indicates XUL elements.
+
+
+
+
+
+
+ Values for the attribute 'mtype'.
+
+
+
+
+ Indicates the marked text is an abbreviation.
+
+
+
+
+ ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept.
+
+
+
+
+ ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective').
+
+
+
+
+ ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging').
+
+
+
+
+ ISO-12620: A proper-name term, such as the name of an agency or other proper entity.
+
+
+
+
+ ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another.
+
+
+
+
+ ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language.
+
+
+
+
+ Indicates the marked text is a date and/or time.
+
+
+
+
+ ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign.
+
+
+
+
+ ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form.
+
+
+
+
+ ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula.
+
+
+
+
+ ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record.
+
+
+
+
+ ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy').
+
+
+
+
+ ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body.
+
+
+
+
+ ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages.
+
+
+
+
+ ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like.
+
+
+
+
+ ISO-12620 2.1.17: A unit to track object.
+
+
+
+
+ Indicates the marked text is a name.
+
+
+
+
+ ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others.
+
+
+
+
+ ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system.
+
+
+
+
+ Indicates the marked text is a phrase.
+
+
+
+
+ ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase.
+
+
+
+
+ Indicates the marked text should not be translated.
+
+
+
+
+ ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet.
+
+
+
+
+ Indicates that the marked text represents a segment.
+
+
+
+
+ ISO-12620 2.1.18.2: A fixed, lexicalized phrase.
+
+
+
+
+ ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs').
+
+
+
+
+ ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system.
+
+
+
+
+ ISO-12620 2.1.19: A fixed chunk of recurring text.
+
+
+
+
+ ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof.
+
+
+
+
+ ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry.
+
+
+
+
+ ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language.
+
+
+
+
+ Indicates the marked text is a term.
+
+
+
+
+ ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted.
+
+
+
+
+ ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system.
+
+
+
+
+ ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza').
+
+
+
+
+ ISO-12620 2.1.9: One of the alternate forms of a term.
+
+
+
+
+
+
+ Values for the attribute 'restype'.
+
+
+
+
+ Indicates a Windows RC AUTO3STATE control.
+
+
+
+
+ Indicates a Windows RC AUTOCHECKBOX control.
+
+
+
+
+ Indicates a Windows RC AUTORADIOBUTTON control.
+
+
+
+
+ Indicates a Windows RC BEDIT control.
+
+
+
+
+ Indicates a bitmap, for example a BITMAP resource in Windows.
+
+
+
+
+ Indicates a button object, for example a BUTTON control Windows.
+
+
+
+
+ Indicates a caption, such as the caption of a dialog box.
+
+
+
+
+ Indicates the cell in a table, for example the content of the <td> element in HTML.
+
+
+
+
+ Indicates check box object, for example a CHECKBOX control in Windows.
+
+
+
+
+ Indicates a menu item with an associated checkbox.
+
+
+
+
+ Indicates a list box, but with a check-box for each item.
+
+
+
+
+ Indicates a color selection dialog.
+
+
+
+
+ Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows.
+
+
+
+
+ Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234).
+
+
+
+
+ Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403).
+
+
+
+
+ Indicates a UI base class element that cannot be represented by any other element.
+
+
+
+
+ Indicates a context menu.
+
+
+
+
+ Indicates a Windows RC CTEXT control.
+
+
+
+
+ Indicates a cursor, for example a CURSOR resource in Windows.
+
+
+
+
+ Indicates a date/time picker.
+
+
+
+
+ Indicates a Windows RC DEFPUSHBUTTON control.
+
+
+
+
+ Indicates a dialog box.
+
+
+
+
+ Indicates a Windows RC DLGINIT resource block.
+
+
+
+
+ Indicates an edit box object, for example an EDIT control in Windows.
+
+
+
+
+ Indicates a filename.
+
+
+
+
+ Indicates a file dialog.
+
+
+
+
+ Indicates a footnote.
+
+
+
+
+ Indicates a font name.
+
+
+
+
+ Indicates a footer.
+
+
+
+
+ Indicates a frame object.
+
+
+
+
+ Indicates a XUL grid element.
+
+
+
+
+ Indicates a groupbox object, for example a GROUPBOX control in Windows.
+
+
+
+
+ Indicates a header item.
+
+
+
+
+ Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML.
+
+
+
+
+ Indicates a Windows RC HEDIT control.
+
+
+
+
+ Indicates a horizontal scrollbar.
+
+
+
+
+ Indicates an icon, for example an ICON resource in Windows.
+
+
+
+
+ Indicates a Windows RC IEDIT control.
+
+
+
+
+ Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF.
+
+
+
+
+ Indicates a label object.
+
+
+
+
+ Indicates a label that is also a HTML link (not necessarily a URL).
+
+
+
+
+ Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML).
+
+
+
+
+ Indicates a listbox object, for example an LISTBOX control in Windows.
+
+
+
+
+ Indicates an list item (an entry in a list).
+
+
+
+
+ Indicates a Windows RC LTEXT control.
+
+
+
+
+ Indicates a menu (a group of menu-items).
+
+
+
+
+ Indicates a toolbar containing one or more tope level menus.
+
+
+
+
+ Indicates a menu item (an entry in a menu).
+
+
+
+
+ Indicates a XUL menuseparator element.
+
+
+
+
+ Indicates a message, for example an entry in a MESSAGETABLE resource in Windows.
+
+
+
+
+ Indicates a calendar control.
+
+
+
+
+ Indicates an edit box beside a spin control.
+
+
+
+
+ Indicates a catch all for rectangular areas.
+
+
+
+
+ Indicates a standalone menu not necessarily associated with a menubar.
+
+
+
+
+ Indicates a pushbox object, for example a PUSHBOX control in Windows.
+
+
+
+
+ Indicates a Windows RC PUSHBUTTON control.
+
+
+
+
+ Indicates a radio button object.
+
+
+
+
+ Indicates a menuitem with associated radio button.
+
+
+
+
+ Indicates raw data resources for an application.
+
+
+
+
+ Indicates a row in a table.
+
+
+
+
+ Indicates a Windows RC RTEXT control.
+
+
+
+
+ Indicates a user navigable container used to show a portion of a document.
+
+
+
+
+ Indicates a generic divider object (e.g. menu group separator).
+
+
+
+
+ Windows accelerators, shortcuts in resource or property files.
+
+
+
+
+ Indicates a UI control to indicate process activity but not progress.
+
+
+
+
+ Indicates a splitter bar.
+
+
+
+
+ Indicates a Windows RC STATE3 control.
+
+
+
+
+ Indicates a window for providing feedback to the users, like 'read-only', etc.
+
+
+
+
+ Indicates a string, for example an entry in a STRINGTABLE resource in Windows.
+
+
+
+
+ Indicates a layers of controls with a tab to select layers.
+
+
+
+
+ Indicates a display and edits regular two-dimensional tables of cells.
+
+
+
+
+ Indicates a XUL textbox element.
+
+
+
+
+ Indicates a UI button that can be toggled to on or off state.
+
+
+
+
+ Indicates an array of controls, usually buttons.
+
+
+
+
+ Indicates a pop up tool tip text.
+
+
+
+
+ Indicates a bar with a pointer indicating a position within a certain range.
+
+
+
+
+ Indicates a control that displays a set of hierarchical data.
+
+
+
+
+ Indicates a URI (URN or URL).
+
+
+
+
+ Indicates a Windows RC USERBUTTON control.
+
+
+
+
+ Indicates a user-defined control like CONTROL control in Windows.
+
+
+
+
+ Indicates the text of a variable.
+
+
+
+
+ Indicates version information about a resource like VERSIONINFO in Windows.
+
+
+
+
+ Indicates a vertical scrollbar.
+
+
+
+
+ Indicates a graphical window.
+
+
+
+
+
+
+ Values for the attribute 'size-unit'.
+
+
+
+
+ Indicates a size in 8-bit bytes.
+
+
+
+
+ Indicates a size in Unicode characters.
+
+
+
+
+ Indicates a size in columns. Used for HTML text area.
+
+
+
+
+ Indicates a size in centimeters.
+
+
+
+
+ Indicates a size in dialog units, as defined in Windows resources.
+
+
+
+
+ Indicates a size in 'font-size' units (as defined in CSS).
+
+
+
+
+ Indicates a size in 'x-height' units (as defined in CSS).
+
+
+
+
+ Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster'
+
+
+
+
+ Indicates a size in inches.
+
+
+
+
+ Indicates a size in millimeters.
+
+
+
+
+ Indicates a size in percentage.
+
+
+
+
+ Indicates a size in pixels.
+
+
+
+
+ Indicates a size in point.
+
+
+
+
+ Indicates a size in rows. Used for HTML text area.
+
+
+
+
+
+
+ Values for the attribute 'state'.
+
+
+
+
+ Indicates the terminating state.
+
+
+
+
+ Indicates only non-textual information needs adaptation.
+
+
+
+
+ Indicates both text and non-textual information needs adaptation.
+
+
+
+
+ Indicates only non-textual information needs review.
+
+
+
+
+ Indicates both text and non-textual information needs review.
+
+
+
+
+ Indicates that only the text of the item needs to be reviewed.
+
+
+
+
+ Indicates that the item needs to be translated.
+
+
+
+
+ Indicates that the item is new. For example, translation units that were not in a previous version of the document.
+
+
+
+
+ Indicates that changes are reviewed and approved.
+
+
+
+
+ Indicates that the item has been translated.
+
+
+
+
+
+
+ Values for the attribute 'state-qualifier'.
+
+
+
+
+ Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously.
+
+
+
+
+ Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.).
+
+
+
+
+ Indicates a match based on matching IDs (in addition to matching text).
+
+
+
+
+ Indicates a translation derived from a glossary.
+
+
+
+
+ Indicates a translation derived from existing translation.
+
+
+
+
+ Indicates a translation derived from machine translation.
+
+
+
+
+ Indicates a translation derived from a translation repository.
+
+
+
+
+ Indicates a translation derived from a translation memory.
+
+
+
+
+ Indicates the translation is suggested by machine translation.
+
+
+
+
+ Indicates that the item has been rejected because of incorrect grammar.
+
+
+
+
+ Indicates that the item has been rejected because it is incorrect.
+
+
+
+
+ Indicates that the item has been rejected because it is too long or too short.
+
+
+
+
+ Indicates that the item has been rejected because of incorrect spelling.
+
+
+
+
+ Indicates the translation is suggested by translation memory.
+
+
+
+
+
+
+ Values for the attribute 'unit'.
+
+
+
+
+ Refers to words.
+
+
+
+
+ Refers to pages.
+
+
+
+
+ Refers to <trans-unit> elements.
+
+
+
+
+ Refers to <bin-unit> elements.
+
+
+
+
+ Refers to glyphs.
+
+
+
+
+ Refers to <trans-unit> and/or <bin-unit> elements.
+
+
+
+
+ Refers to the occurrences of instances defined by the count-type value.
+
+
+
+
+ Refers to characters.
+
+
+
+
+ Refers to lines.
+
+
+
+
+ Refers to sentences.
+
+
+
+
+ Refers to paragraphs.
+
+
+
+
+ Refers to segments.
+
+
+
+
+ Refers to placeables (inline elements).
+
+
+
+
+
+
+ Values for the attribute 'priority'.
+
+
+
+
+ Highest priority.
+
+
+
+
+ High priority.
+
+
+
+
+ High priority, but not as important as 2.
+
+
+
+
+ High priority, but not as important as 3.
+
+
+
+
+ Medium priority, but more important than 6.
+
+
+
+
+ Medium priority, but less important than 5.
+
+
+
+
+ Low priority, but more important than 8.
+
+
+
+
+ Low priority, but more important than 9.
+
+
+
+
+ Low priority.
+
+
+
+
+ Lowest priority.
+
+
+
+
+
+
+
+
+ This value indicates that all properties can be reformatted. This value must be used alone.
+
+
+
+
+ This value indicates that no properties should be reformatted. This value must be used alone.
+
+
+
+
+
+
+
+
+
+
+
+
+ This value indicates that all information in the coord attribute can be modified.
+
+
+
+
+ This value indicates that the x information in the coord attribute can be modified.
+
+
+
+
+ This value indicates that the y information in the coord attribute can be modified.
+
+
+
+
+ This value indicates that the cx information in the coord attribute can be modified.
+
+
+
+
+ This value indicates that the cy information in the coord attribute can be modified.
+
+
+
+
+ This value indicates that all the information in the font attribute can be modified.
+
+
+
+
+ This value indicates that the name information in the font attribute can be modified.
+
+
+
+
+ This value indicates that the size information in the font attribute can be modified.
+
+
+
+
+ This value indicates that the weight information in the font attribute can be modified.
+
+
+
+
+ This value indicates that the information in the css-style attribute can be modified.
+
+
+
+
+ This value indicates that the information in the style attribute can be modified.
+
+
+
+
+ This value indicates that the information in the exstyle attribute can be modified.
+
+
+
+
+
+
+
+
+
+
+
+
+ Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document.
+
+
+
+
+ Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed.
+
+
+
+
+ Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed.
+
+
+
+
+
+
+
+
+ Represents a translation proposal from a translation memory or other resource.
+
+
+
+
+ Represents a previous version of the target element.
+
+
+
+
+ Represents a rejected version of the target element.
+
+
+
+
+ Represents a translation to be used for reference purposes only, for example from a related product or a different language.
+
+
+
+
+ Represents a proposed translation that was used for the translation of the trans-unit, possibly modified.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Values for the attribute 'coord'.
+
+
+
+
+
+
+
+ Version values: 1.0 and 1.1 are allowed for backward compatibility.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/README.txt b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/README.txt
new file mode 100644
index 00000000..73a6ec93
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/README.txt
@@ -0,0 +1,59 @@
+TMGMT Local Translator
+----------------------
+
+A user interface to execute the actual translation of TMGMT source elements.
+Includes management capabilities for handling translations and users.
+
+Requirements
+------------
+
+Local Translator is a translation plugin for TMGMT and is included in TMGMT core.
+
+Basic Concepts
+--------------
+
+Acts as a translator plugin for TMGMT. Can be used to do manual local
+translation. In addition it offers some management capabilities to handle the
+assignment of jobs to users.
+
+The local translator adds two permissions:
+
+- Provide translation services
+
+ The user can assign jobs to himself (assuming he has the right skills) and
+ execute the translation. Adds a 'Translate' link to the User menu.
+
+- Administer translation tasks
+
+ Assign jobs to other users for translation. Adds a 'Manage Translate Tasks'
+ to the User Menu.
+
+
+Getting started
+---------------
+
+In TMGMT, a translation job can be sent to the local translator. In the checkout
+settings, the plugin offers the possibility to assign the job to a specific user.
+Only users with the required language skills are listed at this moment. If no
+person is selected, the job will be moved to the 'unassigned' task list for later
+treatment.
+
+Following the 'Translate' link in the User Menu, the user finds a listing
+of the tasks assigned to him as well as eligible tasks to assign to himself,
+depending on his skills. Assigned tasks will show a 'translate' link in the
+action column. Follow it to get a list of the task items to be translated.
+Choose to translate one item to get to the actual translation page.
+
+It lists two panes for each data item contained in the task item. One showing
+the text in the original language. The second one for writing in the translation.
+Each line sports a check button to its right. Once the translation is done, the
+data item can be checked off as complete. This check is purely informational
+and has no functional consequences.
+
+A translation task item can be saved at any time for later rework. It will show
+up as pending or translated depending on the state of the check mark for each
+item.
+
+Once the data items are all translated, the 'Save as completed' button will
+finalize the task and send it back to TMGMT core for further processing. Please
+note: Completing a task item is independent of the state of the checkboxes.
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.controller.task.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.controller.task.inc
new file mode 100644
index 00000000..c38010c3
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.controller.task.inc
@@ -0,0 +1,39 @@
+changed = REQUEST_TIME;
+ return parent::save($entity, $transaction);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($ids, $transaction = NULL) {
+ parent::delete($ids, $transaction);
+
+ $query = new EntityFieldQuery();
+ $result = $query
+ ->entityCondition('entity_type', 'tmgmt_local_task_item')
+ ->propertyCondition('tltid', $ids)
+ ->execute();
+ if (!empty($result['tmgmt_local_task_item'])) {
+ entity_delete_multiple('tmgmt_local_task_item', array_keys($result['tmgmt_local_task_item']));
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.controller.task_item.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.controller.task_item.inc
new file mode 100644
index 00000000..c9a995e1
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.controller.task_item.inc
@@ -0,0 +1,77 @@
+isCompleted()) {
+ $entity->count_untranslated = 0;
+ $entity->count_translated = count(tmgmt_flatten_data($entity->data));
+ $entity->count_completed = 0;
+ }
+ // Consider everything completed if the job is completed.
+ elseif ($entity->isClosed()) {
+ $entity->count_untranslated = 0;
+ $entity->count_translated = 0;
+ $entity->count_completed = count(tmgmt_flatten_data($entity->data));
+ }
+ // Count the data item states.
+ else {
+ // Start with assuming that all data is untranslated, then go through it
+ // and count translated data.
+ $entity->count_untranslated = count(array_filter(tmgmt_flatten_data($entity->getJobItem()->getData()), '_tmgmt_filter_data'));
+ $entity->count_translated = 0;
+ $entity->count_completed = 0;
+ $this->count($entity->data, $entity);
+ }
+ return parent::save($entity, $transaction);
+ }
+
+ /**
+ * Parse all data items recursively and sums up the counters for
+ * accepted, translated and pending items.
+ *
+ * @param $item
+ * The current data item.
+ * @param $entity
+ * The job item the count should be calculated.
+ */
+ protected function count(&$item, $entity) {
+ if (!empty($item['#text'])) {
+ if (_tmgmt_filter_data($item)) {
+
+ // Set default states if no state is set.
+ if (!isset($item['#status'])) {
+ $item['#status'] = TMGMT_DATA_ITEM_STATE_UNTRANSLATED;
+ }
+ switch ($item['#status']) {
+ case TMGMT_DATA_ITEM_STATE_TRANSLATED:
+ $entity->count_untranslated--;
+ $entity->count_translated++;
+ break;
+ }
+ }
+ }
+ else {
+ foreach (element_children($item) as $key) {
+ $this->count($item[$key], $entity);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task.inc
new file mode 100644
index 00000000..7e9f20f5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task.inc
@@ -0,0 +1,76 @@
+path));
+ $wildcard = isset($this->entityInfo['admin ui']['menu wildcard']) ? $this->entityInfo['admin ui']['menu wildcard'] : '%entity_object';
+ $items[$this->path . '/' . $wildcard] = array(
+ 'title callback' => 'entity_label',
+ 'title arguments' => array($this->entityType, $id_count),
+ 'page callback' => 'tmgmt_local_task_view',
+ 'page arguments' => array($id_count),
+ 'load arguments' => array($this->entityType),
+ 'access callback' => 'entity_access',
+ 'access arguments' => array('view', $this->entityType, $id_count),
+ 'file' => 'tmgmt_local.pages.inc',
+ 'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
+ );
+ $items[$this->path . '/' . $wildcard . '/delete'] = array(
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
+ 'load arguments' => array($this->entityType),
+ 'access callback' => 'entity_access',
+ 'access arguments' => array('delete', $this->entityType, $id_count),
+ 'type' => MENU_CALLBACK,
+ );
+ $items[$this->path . '/' . $wildcard . '/unassign'] = array(
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
+ 'load arguments' => array($this->entityType),
+ 'access callback' => 'entity_access',
+ 'access arguments' => array('unassign', $this->entityType, $id_count),
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function operationForm($form, &$form_state, $entity, $op) {
+ switch ($op) {
+ case 'delete':
+ $confirm_question = t('Are you sure you want to delete the translation task %label?', array('%label' => $entity->label()));
+ return confirm_form($form, $confirm_question, $this->path);
+ case 'unassign':
+ $confirm_question = t('Are you sure you want to unassign from the translation task %label?', array('%label' => $entity->label()));
+ return confirm_form($form, $confirm_question, $this->path);
+ }
+ drupal_not_found();
+ exit;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyOperation($op, $entity) {
+ switch ($op) {
+ case 'delete':
+ $entity->delete();
+ return t('Deleted the translation local task %label.', array('%label' => $entity->label()));
+ case 'unassign':
+ $entity->unassign();
+ $entity->save();
+ return t('Unassigned from translation local task %label.', array('%label' => $entity->label()));
+ }
+ return FALSE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task_item.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task_item.inc
new file mode 100644
index 00000000..22f3ffbd
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task_item.inc
@@ -0,0 +1,27 @@
+path));
+ $items[$this->path . '/%tmgmt_local_task/item/%tmgmt_local_task_item'] = array(
+ 'title callback' => 'entity_label',
+ 'title arguments' => array($this->entityType, $id_count + 2),
+ 'page callback' => 'tmgmt_local_task_item_view',
+ 'page arguments' => array($id_count + 2),
+ 'load arguments' => array($this->entityType),
+ 'access callback' => 'entity_access',
+ 'access arguments' => array('view', $this->entityType, $id_count + 2),
+ 'file' => 'tmgmt_local.pages.inc',
+ 'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
+ );
+ return $items;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/entity/tmgmt_local.entity.task.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/entity/tmgmt_local.entity.task.inc
new file mode 100644
index 00000000..3782edff
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/entity/tmgmt_local.entity.task.inc
@@ -0,0 +1,387 @@
+ 'translate/' . $this->tltid);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defaultLabel() {
+ if (empty($this->tuid)) {
+ if (empty($this->title)) {
+ return t('Task for @job', array('@job' => $this->getJob()->label()));
+ }
+ else {
+ return $this->title;
+ }
+ }
+ else {
+ if (empty($this->title)) {
+ return t('Task for @job assigned to @translator', array('@job' => $this->getJob()->label(), '@translator' => entity_label('user', user_load($this->tuid))));
+ }
+ else {
+ return t('@title assigned to @translator', array('@title' => $this->title, '@translator' => entity_label('user', user_load($this->tuid))));
+ }
+ }
+
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildContent($view_mode = 'full', $langcode = NULL) {
+ $content = entity_ui_get_form('tmgmt_local_task', $this);
+ return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+ }
+
+ /**
+ * Return the corresponding translation job.
+ *
+ * @return TMGMTJob
+ */
+ public function getJob() {
+ return tmgmt_job_load($this->tjid);
+ }
+
+ /**
+ * Assign translation task to passed user.
+ *
+ * @param object $user
+ * User object.
+ */
+ public function assign($user) {
+ $this->incrementLoopCount(TMGMT_LOCAL_TASK_STATUS_PENDING, $user->uid);
+ $this->tuid = $user->uid;
+ $this->status = TMGMT_LOCAL_TASK_STATUS_PENDING;
+ }
+
+ /**
+ * Unassign translation task.
+ */
+ public function unassign() {
+ // We also need to increment loop count when unassigning.
+ $this->incrementLoopCount(TMGMT_LOCAL_TASK_STATUS_UNASSIGNED, 0);
+ $this->tuid = 0;
+ $this->status = TMGMT_LOCAL_TASK_STATUS_UNASSIGNED;
+ }
+
+ /**
+ * Returns all job items attached to this task.
+ *
+ * @return array
+ * An array of translation job items.
+ */
+ public function getItems($conditions = array()) {
+ $query = new EntityFieldQuery();
+ $query->entityCondition('entity_type', 'tmgmt_local_task_item');
+ $query->propertyCondition('tltid', $this->tltid);
+ foreach ($conditions as $key => $condition) {
+ if (is_array($condition)) {
+ $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+ $query->propertyCondition($key, $condition['value'], $operator);
+ }
+ else {
+ $query->propertyCondition($key, $condition);
+ }
+ }
+ $results = $query->execute();
+ if (!empty($results['tmgmt_local_task_item'])) {
+ return entity_load('tmgmt_local_task_item', array_keys($results['tmgmt_local_task_item']));
+ }
+ return array();
+ }
+
+ /**
+ * Create a task item for this task and the given job item.
+ *
+ * @param TMGMTJobItem $job_item
+ * The job item.
+ */
+ public function addTaskItem(TMGMTJobItem $job_item) {
+ // Save the task to get an id.
+ if (empty($this->tltid)) {
+ $this->save();
+ }
+
+ $local_task = entity_create('tmgmt_local_task_item', array(
+ 'tltid' => $this->identifier(),
+ 'tjiid' => $job_item->identifier(),
+ ));
+ $local_task->save();
+ return $local_task;
+ }
+
+ /**
+ * Returns the status of the task. Can be one of the task status constants.
+ *
+ * @return int
+ * The status of the task or NULL if it hasn't been set yet.
+ */
+ public function getStatus() {
+ return $this->status;
+ }
+
+ /**
+ * Updates the status of the task.
+ *
+ * @param $status
+ * The new status of the task. Has to be one of the task status constants.
+ * @param $message
+ * (Optional) The log message to be saved along with the status change.
+ * @param $variables
+ * (Optional) An array of variables to replace in the message on display.
+ *
+ * @return int
+ * The updated status of the task if it could be set.
+ *
+ * @see TMGMTJob::addMessage()
+ */
+ public function setStatus($status) {
+ // Return TRUE if the status could be set. Return FALSE otherwise.
+ if (array_key_exists($status, tmgmt_local_task_statuses())) {
+ $this->incrementLoopCount($status, $this->tuid);
+ $this->status = $status;
+ $this->save();
+ }
+ return $this->status;
+ }
+
+ /**
+ * Checks whether the passed value matches the current status.
+ *
+ * @param $status
+ * The value to check the current status against.
+ *
+ * @return boolean
+ * TRUE if the passed status matches the current status, FALSE otherwise.
+ */
+ public function isStatus($status) {
+ return $this->getStatus() == $status;
+ }
+
+ /**
+ * Checks whether the user described by $account is the author of this task.
+ *
+ * @param $account
+ * (Optional) A user object. Defaults to the currently logged in user.
+ */
+ public function isAuthor($account = NULL) {
+ $account = isset($account) ? $account : $GLOBALS['user'];
+ return $this->uid == $account->uid;
+ }
+
+ /**
+ * Returns whether the status of this task is 'unassigned'.
+ *
+ * @return boolean
+ * TRUE if the status is 'unassigned', FALSE otherwise.
+ */
+ public function isUnassigned() {
+ return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_UNASSIGNED);
+ }
+
+ /**
+ * Returns whether the status of this task is 'pending'.
+ *
+ * @return boolean
+ * TRUE if the status is 'pending', FALSE otherwise.
+ */
+ public function isPending() {
+ return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_PENDING);
+ }
+
+ /**
+ * Returns whether the status of this task is 'completed'.
+ *
+ * @return boolean
+ * TRUE if the status is 'completed', FALSE otherwise.
+ */
+ public function isCompleted() {
+ return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_COMPLETED);
+ }
+
+ /**
+ * Returns whether the status of this task is 'rejected'.
+ *
+ * @return boolean
+ * TRUE if the status is 'rejected', FALSE otherwise.
+ */
+ public function isRejected() {
+ return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_REJECTED);
+ }
+
+ /**
+ * Returns whether the status of this task is 'closed'.
+ *
+ * @return boolean
+ * TRUE if the status is 'closed', FALSE otherwise.
+ */
+ public function isClosed() {
+ return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_CLOSED);
+ }
+
+ /**
+ * Count of all translated data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountTranslated() {
+ return tmgmt_local_task_statistic($this, 'count_translated');
+ }
+
+ /**
+ * Count of all untranslated data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountUntranslated() {
+ return tmgmt_local_task_statistic($this, 'count_untranslated');
+ }
+
+ /**
+ * Count of all completed data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountCompleted() {
+ return tmgmt_local_task_statistic($this, 'count_completed');
+ }
+
+ /**
+ * Sums up all word counts of this task job items.
+ *
+ * @return
+ * The sum of all accepted counts
+ */
+ public function getWordCount() {
+ return tmgmt_local_task_statistic($this, 'word_count');
+ }
+
+
+ /**
+ * Returns loop count of a task.
+ *
+ * @return int
+ * Task loop count.
+ */
+ public function getLoopCount() {
+ return $this->loop_count;
+ }
+
+ /**
+ * Increment loop_count property depending on current status, new status and
+ * new translator.
+ *
+ * @param int $newStatus
+ * New status of task.
+ * @param int $new_tuid
+ * New translator uid.
+ */
+ public function incrementLoopCount($newStatus, $new_tuid) {
+ if ($this->status == TMGMT_LOCAL_TASK_STATUS_PENDING
+ && $newStatus == TMGMT_LOCAL_TASK_STATUS_PENDING
+ && $this->tuid != $new_tuid) {
+ ++$this->loop_count;
+ }
+ else if ($this->status != TMGMT_LOCAL_TASK_STATUS_UNASSIGNED
+ && $newStatus == TMGMT_LOCAL_TASK_STATUS_UNASSIGNED) {
+ ++$this->loop_count;
+ }
+ else if ($this->status != TMGMT_LOCAL_TASK_STATUS_UNASSIGNED
+ && $this->status != TMGMT_LOCAL_TASK_STATUS_PENDING
+ && $newStatus == TMGMT_LOCAL_TASK_STATUS_PENDING) {
+ ++$this->loop_count;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/entity/tmgmt_local.entity.task_item.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/entity/tmgmt_local.entity.task_item.inc
new file mode 100644
index 00000000..384ddbe2
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/entity/tmgmt_local.entity.task_item.inc
@@ -0,0 +1,252 @@
+ 'translate/' . $this->tltid . '/item/' . $this->tltiid);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defaultLabel() {
+ if ($job_item = $this->getJobItem()) {
+ return $job_item->label();
+ }
+ return t('Missing job item');
+ }
+
+ /**
+ * Returns the translation task.
+ *
+ * @return TMGMTLocalTask
+ */
+ public function getTask() {
+ return entity_load_single('tmgmt_local_task', $this->tltid);
+ }
+
+ /**
+ * Returns the translation job item.
+ *
+ * @return TMGMTJobItem
+ */
+ public function getJobItem() {
+ return entity_load_single('tmgmt_job_item', $this->tjiid);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildContent($view_mode = 'full', $langcode = NULL) {
+ $content = drupal_get_form('tmgmt_local_translation_form', $this);
+ return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+ }
+
+ /**
+ * Returns TRUE if the local task is pending.
+ *
+ * @return bool
+ * TRUE if the local task item is untranslated.
+ */
+ public function isPending() {
+ return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_PENDING;
+ }
+
+ /**
+ * Returns TRUE if the local task is translated (fully translated).
+ *
+ * @return bool
+ * TRUE if the local task item is translated.
+ */
+ public function isCompleted() {
+ return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
+ }
+
+ /**
+ * Rreturns TRUE if the local task is closed (translated and accepted).
+ *
+ * @return bool
+ * TRUE if the local task item is translated and accepted.
+ */
+ public function isClosed() {
+ return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
+ }
+
+ /**
+ * Sets the task item status to completed.
+ */
+ public function completed() {
+ $this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
+ }
+
+ /**
+ * Sets the task item status to closed.
+ */
+ public function closed() {
+ $this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
+ }
+
+ /**
+ * Updates the values for a specific substructure in the data array.
+ *
+ * The values are either set or updated but never deleted.
+ *
+ * @param $key
+ * Key pointing to the item the values should be applied.
+ * The key can be either be an array containing the keys of a nested array
+ * hierarchy path or a string with '][' or '|' as delimiter.
+ * @param $values
+ * Nested array of values to set.
+ */
+ public function updateData($key, $values = array()) {
+ foreach ($values as $index => $value) {
+ // In order to preserve existing values, we can not aplly the values array
+ // at once. We need to apply each containing value on its own.
+ // If $value is an array we need to advance the hierarchy level.
+ if (is_array($value)) {
+ $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+ }
+ // Apply the value.
+ else {
+ drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+ }
+ }
+ }
+
+ /**
+ * Array of translations.
+ *
+ * The structure is similar to the form API in the way that it is a possibly
+ * nested array with the following properties whose presence indicate that the
+ * current element is a text that might need to be translated.
+ *
+ * - #text: The translated text of the corresponding entry in the job item.
+ * - #status: The status of the translation.
+ *
+ * The key can be an alphanumeric string.
+ *
+ * @param array $key
+ * If present, only the subarray identified by key is returned.
+ * @param string $index
+ * Optional index of an attribute below $key.
+ *
+ * @return array
+ * A structured data array.
+ */
+ public function getData(array $key = array(), $index = NULL) {
+ if (empty($key)) {
+ return $this->data;
+ }
+ if ($index) {
+ $key = array_merge($key, array($index));
+ }
+ return drupal_array_get_nested_value($this->data, $key);
+ }
+
+ /**
+ * Count of all translated data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountTranslated() {
+ return $this->count_translated;
+ }
+
+ /**
+ * Count of all untranslated data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountUntranslated() {
+ return $this->count_untranslated;
+ }
+
+ /**
+ * Count of all completed data items.
+ *
+ * @return
+ * Translated count
+ */
+ public function getCountCompleted() {
+ return $this->count_completed;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/includes/tmgmt_local.info.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/includes/tmgmt_local.info.inc
new file mode 100644
index 00000000..61da293c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/includes/tmgmt_local.info.inc
@@ -0,0 +1,91 @@
+type);
+ $properties = &$info[$this->type]['properties'];
+
+ // Make the created and changed property appear as date.
+ $properties['changed']['type'] = $properties['created']['type'] = 'date';
+
+ // Add the options list for the defined status constants.
+ $properties['status']['options list'] = 'tmgmt_local_task_statuses';
+
+ // Link the job property to the corresponding job entity.
+ $properties['job'] = array(
+ 'label' => t('Translation Job'),
+ 'type' => 'tmgmt_job',
+ 'description' => t('Corresponding job entity of the translation task.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer tmgmt',
+ 'required' => TRUE,
+ 'schema field' => 'tjid',
+ );
+
+ // Link the author property to the corresponding user entity.
+ $properties['author'] = array(
+ 'label' => t('Author'),
+ 'type' => 'user',
+ 'description' => t('The author of the translation task.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer tmgmt',
+ 'required' => TRUE,
+ 'schema field' => 'uid',
+ );
+
+ // Link the author property to the corresponding user entity.
+ $properties['translator'] = array(
+ 'label' => t('Translator'),
+ 'type' => 'user',
+ 'description' => t('The assigned translator for translation task.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer tmgmt',
+ 'required' => TRUE,
+ 'schema field' => 'tuid',
+ );
+
+ return $info;
+ }
+
+}
+
+
+/**
+ * Metadata controller for the local task entity.
+ */
+class TMGMTLocalTaskItemMetadataController extends EntityDefaultMetadataController {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function entityPropertyInfo() {
+ $info = parent::entityPropertyInfo();
+ $properties = &$info[$this->type]['properties'];
+
+ // Add the options list for the defined status constants.
+ $properties['status']['options list'] = 'tmgmt_local_task_item_statuses';
+
+ // Link the job property to the corresponding job entity.
+ $properties['tjiid']['type'] = 'tmgmt_job_item';
+
+ // Link the author property to the corresponding user entity.
+ $properties['tltid']['type'] = 'tmgmt_local_task';
+
+ return $info;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/includes/tmgmt_local.pages.inc b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/includes/tmgmt_local.pages.inc
new file mode 100644
index 00000000..4da12d4f
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/translators/tmgmt_local/includes/tmgmt_local.pages.inc
@@ -0,0 +1,515 @@
+entityType(), array($task), 'full', NULL, TRUE);
+}
+
+/**
+ * Entity API form the local task entity.
+ */
+function tmgmt_local_task_form($form, &$form_state, TMGMTLocalTask $task, $op = 'edit') {
+ $wrapper = entity_metadata_wrapper('tmgmt_local_task', $task);
+
+ // Set the title of the page to the label and the current status of the task.
+ drupal_set_title(t('@label (@status)', array('@label' => $task->label(), '@status' => $wrapper->status->label())));
+
+ // Check if the translator entity is completely new or not.
+ $old = empty($task->is_new) && $op != 'clone';
+
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $task->title,
+ '#access' => user_access('administer tmgmt') || user_access('administer translation tasks'),
+ );
+
+ $form['status'] = array(
+ '#type' => 'select',
+ '#title' => t('Status'),
+ '#options' => tmgmt_local_task_statuses(),
+ '#default_value' => $wrapper->status->value(),
+ '#access' => user_access('administer tmgmt') || user_access('administer translation tasks'),
+ );
+
+ $translators = tmgmt_local_translators($task->getJob()->source_language, array($task->getJob()->target_language));
+ $form['tuid'] = array(
+ '#title' => t('Assigned'),
+ '#type' => 'select',
+ '#options' => $translators,
+ '#empty_option' => t('- Select user -'),
+ '#default_value' => $task->tuid,
+ '#access' => user_access('administer tmgmt') || user_access('administer translation tasks'),
+ );
+
+ if ($view = views_get_view('tmgmt_local_task_items')) {
+ $form['items'] = array(
+ '#type' => 'item',
+ '#title' => $view->get_title(),
+ '#prefix' => '
',
+ );
+
+ // Display created time only for jobs that are not new anymore.
+ if (!$job->isUnprocessed()) {
+ $form['info']['created'] = array(
+ '#type' => 'item',
+ '#title' => t('Created'),
+ '#markup' => format_date($job->created),
+ '#prefix' => '
', implode(' ', $classes), $title, $icon);
+}
+
+/**
+ * Render one single data item as a table row.
+ */
+function theme_tmgmt_ui_translator_review_form_element($variables) {
+ $element = $variables['element'];
+ // Label of all element groups.
+ if (!isset($element['#top_label'])) {
+ $element['#top_label'] = array_shift($element['#parent_label']);
+ }
+ // Label of the current data item.
+ if (!isset($element['#leave_label'])) {
+ $element['#leave_label'] = array_pop($element['#parent_label']);
+ }
+ // Do not repeat labels inside the same hierarchy.
+ if ($element['#top_label'] == $element['#leave_label']) {
+ $element['#leave_label'] = '';
+ }
+ $result = '
';
+
+ return $output;
+}
+
+/**
+ * Attempts to checkout a number of jobs and prepare the necessary redirects.
+ *
+ * @param array $form_state
+ * Form state array, used to set the initial redirect.
+ * @param array $jobs
+ * Array of jobs to attempt checkout
+ *
+ * @ingroup tmgmt_job
+ *
+ * @see tmgmt_ui_job_checkout_multiple()
+ */
+function tmgmt_ui_job_checkout_and_redirect(array &$form_state, array $jobs) {
+ $redirects = tmgmt_ui_job_checkout_multiple($jobs);
+ // If necessary, do a redirect.
+ if ($redirects) {
+ if (isset($_GET['destination'])) {
+ // Remove existing destination, as that will prevent us from being
+ // redirect to the job checkout page. Set the destination as the final
+ // redirect instead.
+ tmgmt_ui_redirect_queue_set($redirects, $_GET['destination']);
+ unset($_GET['destination']);
+ }
+ else {
+ tmgmt_ui_redirect_queue_set($redirects, current_path());
+ }
+ $form_state['redirect'] = tmgmt_ui_redirect_queue_dequeue();
+
+ // Count of the job messages is one less due to the final redirect.
+ drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
+ }
+}
+
+/**
+ * Implements hook_help().
+ */
+function tmgmt_ui_help($path, $arg) {
+ $output = '';
+ if (strpos($path, 'admin/tmgmt/sources') !== FALSE) {
+ $output = '
' . t('The TMGMT cart is used to bundle text items from
+ different sources into one translation job. Use the "Add to cart" button to
+ add all selected items in any source list. From the cart page, you can
+ request a translation of all selected elements in the cart into any available
+ language. One translation job will be created for each language pair involved.')
+ . '
';
+ }
+
+ return $output;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.rules.inc b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.rules.inc
new file mode 100644
index 00000000..8433ccca
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.rules.inc
@@ -0,0 +1,42 @@
+ t('Add to cart'),
+ 'group' => t('Translation Management'),
+ 'parameter' => array(
+ 'plugin' => array(
+ 'type' => 'token',
+ 'label' => t('Source plugin'),
+ 'description' => t('The source plugin of this item'),
+ ),
+ 'item_type' => array(
+ 'type' => 'token',
+ 'label' => t('Item type'),
+ 'description' => t('The item type'),
+ ),
+ 'item_id' => array(
+ 'type' => 'text',
+ 'label' => t('Item ID'),
+ 'description' => t('ID of the referenced item'),
+ ),
+ ),
+ );
+
+ return $info;
+}
+
+/**
+ * Rules callback to add a job item into the cart.
+ */
+function tmgmt_ui_rules_source_add_item_to_cart($plugin, $item_type, $item_id) {
+ tmgmt_ui_cart_get()->addJobItem($plugin, $item_type, $item_id);
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.rules_defaults.inc b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.rules_defaults.inc
new file mode 100644
index 00000000..b330714c
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.rules_defaults.inc
@@ -0,0 +1,67 @@
+name] = $rule;
+ $data = '{ "rules_tmgmt_job_abort_translation" : {
+ "LABEL" : "Abort Translation",
+ "PLUGIN" : "rule",
+ "REQUIRES" : [ "tmgmt" ],
+ "USES VARIABLES" : { "job" : { "label" : "Job", "type" : "tmgmt_job" } },
+ "DO" : [ { "tmgmt_rules_job_abort_translation" : { "job" : [ "job" ] } } ]
+ }
+ }';
+ $rule = rules_import($data);
+ $configs[$rule->name] = $rule;
+ $data = '{ "rules_tmgmt_job_delete" : {
+ "LABEL" : "Delete Job",
+ "PLUGIN" : "rule",
+ "REQUIRES" : [ "tmgmt" ],
+ "USES VARIABLES" : { "job" : { "label" : "Job", "type" : "tmgmt_job" } },
+ "DO" : [ { "tmgmt_rules_job_delete" : { "job" : [ "job" ] } } ]
+ }
+ }';
+ $rule = rules_import($data);
+ $configs[$rule->name] = $rule;
+ $data = '{ "tmgmt_node_ui_tmgmt_nodes_add_items_to_cart" : {
+ "LABEL" : "Add to cart",
+ "PLUGIN" : "rule",
+ "REQUIRES" : [ "tmgmt", "rules", "tmgmt_ui" ],
+ "USES VARIABLES" : { "nodes" : { "label" : "Nodes", "type" : "list\u003Cnode\u003E" } },
+ "DO" : [
+ { "tmgmt_get_first_from_node_list" : {
+ "USING" : { "list" : [ "nodes" ] },
+ "PROVIDE" : { "first_node" : { "first_node" : "Node" } }
+ }
+ },
+ { "LOOP" : {
+ "USING" : { "list" : [ "nodes" ] },
+ "ITEM" : { "node" : "Node" },
+ "DO" : [
+ { "tmgmt_ui_rules_source_add_item_to_cart" : { "plugin" : "node", "item_type" : "node", "item_id" : "[node:nid]" } }
+ ]
+ }
+ }
+ ]
+ }
+ }';
+ $rule = rules_import($data);
+ $configs[$rule->name] = $rule;
+ return $configs;
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.test b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.test
new file mode 100644
index 00000000..c79b9ae5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui.test
@@ -0,0 +1,619 @@
+ 'UI tests',
+ 'description' => 'Verifies basic functionality of the user interface',
+ 'group' => 'Translation Management',
+ );
+ }
+
+ function setUp() {
+ parent::setup(array('tmgmt_ui'));
+ parent::createLanguagesLoginTranslator();
+ }
+
+ /**
+ * Test the page callbacks to create jobs and check them out.
+ */
+ function testCheckoutForm() {
+
+ // Add a first item to the job. This will auto-create the job.
+ $job = tmgmt_job_match_item('en', '');
+ $job->addItem('test_source', 'test', 1);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('test_source:test:1');
+
+ // Add two more job items.
+ $job->addItem('test_source', 'test', 2);
+ $job->addItem('test_source', 'test', 3);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('test_source:test:1');
+ $this->assertText('test_source:test:2');
+ $this->assertText('test_source:test:3');
+
+ // @todo: Test ajax functionality.
+
+ // Attempt to translate into greek.
+ $edit = array(
+ 'target_language' => 'el',
+ 'settings[action]' => 'translate',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ $this->assertText(t('@translator can not translate from @source to @target.', array('@translator' => 'Test translator (auto created)', '@source' => 'English', '@target' => 'Greek')));
+
+ // Job still needs to be in state new.
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isUnprocessed());
+
+ $edit = array(
+ 'target_language' => 'es',
+ 'settings[action]' => 'translate',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+
+ // Job needs to be in state active.
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isActive());
+ foreach ($job->getItems() as $job_item) {
+ /* @var $job_item TMGMTJobItem */
+ $this->assertTrue($job_item->isNeedsReview());
+ }
+ $this->assertText(t('Test translation created'));
+ $this->assertNoText(t('Test translator called'));
+
+ // Test redirection.
+ $this->assertText(t('Job overview'));
+
+ // Another job.
+ $previous_tjid = $job->tjid;
+ $job = tmgmt_job_match_item('en', '');
+ $job->addItem('test_source', 'test', 1);
+ $this->assertNotEqual($job->tjid, $previous_tjid);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('You can provide a label for this job in order to identify it easily later on.');
+ $this->assertText('test_source:test:1');
+
+ $edit = array(
+ 'target_language' => 'es',
+ 'settings[action]' => 'submit',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ $this->assertText(t('Test submit'));
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isActive());
+
+ // Another job.
+ $job = tmgmt_job_match_item('en', 'es');
+ $job->addItem('test_source', 'test', 1);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('You can provide a label for this job in order to identify it easily later on.');
+ $this->assertText('test_source:test:1');
+
+ $edit = array(
+ 'settings[action]' => 'reject',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ $this->assertText(t('This is not supported'));
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isRejected());
+
+ // Check displayed job messages.
+ $args = array('@view' => 'view-tmgmt-ui-job-messages');
+ $this->assertEqual(2, count($this->xpath('//div[contains(@class, @view)]//tbody/tr', $args)));
+
+ // Check that the author for each is the current user.
+ $message_authors = $this->xpath('////div[contains(@class, @view)]//td[contains(@class, @field)]/span', $args + array('@field' => 'views-field-name'));
+ $this->assertEqual(2, count($message_authors));
+ foreach ($message_authors as $message_author) {
+ $this->assertEqual((string)$message_author, $this->translator_user->name);
+ }
+
+ // Make sure that rejected jobs can be re-submitted.
+ $this->assertTrue($job->isSubmittable());
+ $edit = array(
+ 'settings[action]' => 'translate',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ $this->assertText(t('Test translation created'));
+
+ // Another job.
+ $job = tmgmt_job_match_item('en', 'es');
+ $job->addItem('test_source', 'test', 1);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('You can provide a label for this job in order to identify it easily later on.');
+ $this->assertText('test_source:test:1');
+
+ $edit = array(
+ 'settings[action]' => 'fail',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ $this->assertText(t('Service not reachable'));
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isUnprocessed());
+
+ // Verify that we are still on the form.
+ $this->assertText('You can provide a label for this job in order to identify it easily later on.');
+
+ // Another job.
+ $job = tmgmt_job_match_item('en', 'es');
+ $job->addItem('test_source', 'test', 1);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('You can provide a label for this job in order to identify it easily later on.');
+ $this->assertText('test_source:test:1');
+
+ $edit = array(
+ 'settings[action]' => 'not_translatable',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit to translator'));
+ // @todo Update to correct failure message.
+ $this->assertText(t('Fail'));
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isUnprocessed());
+
+ // Test default settings.
+ $this->default_translator->settings['action'] = 'reject';
+ $this->default_translator->save();
+ $job = tmgmt_job_match_item('en', 'es');
+ $job->addItem('test_source', 'test', 1);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ // Check checkout form.
+ $this->assertText('You can provide a label for this job in order to identify it easily later on.');
+ $this->assertText('test_source:test:1');
+
+ // The action should now default to reject.
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+ $this->assertText(t('This is not supported.'));
+ $job = entity_load_unchanged('tmgmt_job', $job->tjid);
+ $this->assertTrue($job->isRejected());
+ }
+
+ /**
+ * Tests the tmgmt_ui_job_checkout() function.
+ */
+ function testCheckoutFunction() {
+ $job = $this->createJob();
+
+ // Check out a job when only the test translator is available. That one has
+ // settings, so a checkout is necessary.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $uri = $job->uri();
+ $this->assertEqual($uri['path'], $redirects[0]);
+ $this->assertTrue($job->isUnprocessed());
+ $job->delete();
+
+ // Hide settings on the test translator.
+ $default_translator = tmgmt_translator_load('test_translator');
+ $default_translator->settings = array(
+ 'expose_settings' => FALSE,
+ );
+ $job = $this->createJob();
+
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->assertFalse($redirects);
+ $this->assertTrue($job->isActive());
+
+ // A job without target language needs to be checked out.
+ $job = $this->createJob('en', '');
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $uri = $job->uri();
+ $this->assertEqual($uri['path'], $redirects[0]);
+ $this->assertTrue($job->isUnprocessed());
+
+ // Create a second file translator. This should check
+ // out immediately.
+ $job = $this->createJob();
+
+ $second_translator = $this->createTranslator();
+ $second_translator->settings = array(
+ 'expose_settings' => FALSE,
+ );
+ $second_translator->save();
+
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $uri = $job->uri();
+ $this->assertEqual($uri['path'], $redirects[0]);
+ $this->assertTrue($job->isUnprocessed());
+ }
+
+ /**
+ * Tests of the job item review process.
+ */
+ public function testReview() {
+ $job = $this->createJob();
+ $job->translator = $this->default_translator->name;
+ $job->settings = array();
+ $job->save();
+ $item = $job->addItem('test_source', 'test', 1);
+
+ $data = tmgmt_flatten_data($item->getData());
+ $keys = array_keys($data);
+ $key = $keys[0];
+
+ $this->drupalGet('admin/tmgmt/items/' . $item->tjiid);
+ // Testing the result of the
+ // TMGMTTranslatorUIControllerInterface::reviewDataItemElement()
+ $this->assertText(t('Testing output of review data item element @key from the testing translator.', array('@key' => $key)));
+
+ // Test the review tool source textarea.
+ $this->assertFieldByName('dummy|deep_nesting[source]', $data[$key]['#text']);
+
+ // Save translation.
+ $this->drupalPost(NULL, array('dummy|deep_nesting[translation]' => $data[$key]['#text'] . 'translated'), t('Save'));
+ $this->drupalGet('admin/tmgmt/items/' . $item->tjiid);
+ // Check if translation has been saved.
+ $this->assertFieldByName('dummy|deep_nesting[translation]', $data[$key]['#text'] . 'translated');
+ }
+
+ /**
+ * Tests the UI of suggestions.
+ */
+ public function testSuggestions() {
+ // Prepare a job and a node for testing.
+ $job = $this->createJob();
+ $job->addItem('test_source', 'test', 1);
+ $job->addItem('test_source', 'test', 7);
+
+ // Go to checkout form.
+ $redirects = tmgmt_ui_job_checkout_multiple(array($job));
+ $this->drupalGet(reset($redirects));
+
+ $this->assertRaw('20');
+
+ // Load all suggestions.
+ $commands = $this->drupalPostAJAX(NULL, array(), array('op' => t('Load suggestions')));
+ $this->assertEqual(count($commands), 4, 'Found 4 commands in AJAX-Request.');
+
+ // Check each command for success.
+ foreach ($commands as $command) {
+ // No checks against the settings because we not use ajax to save.
+ if ($command['command'] == 'settings') {
+ }
+ // Other commands must be from type "insert".
+ else if ($command['command'] == 'insert') {
+ // This should be the tableselect javascript file for the header.
+ if (($command['method'] == 'prepend') && ($command['selector'] == 'head')) {
+ $this->assertTrue(substr_count($command['data'], 'misc/tableselect.js'), 'Javascript for Tableselect found.');
+ }
+ // Check for the main content, the tableselect with the suggestions.
+ else if (($command['method'] == NULL) && ($command['selector'] == NULL)) {
+ $this->assertTrue(substr_count($command['data'], '') == 5, 'Found five table header.');
+ $this->assertTrue(substr_count($command['data'], '') == 3, 'Found two suggestion and one table header.');
+ $this->assertTrue(substr_count($command['data'], '
11
') == 2, 'Found 10 words to translate per suggestion.');
+ $this->assertTrue(substr_count($command['data'], 'value="Add suggestions"'), 'Found add button.');
+ }
+ // Nothing to prepend...
+ else if (($command['method'] == 'prepend') && ($command['selector'] == NULL)) {
+ $this->assertTrue(empty($command['data']), 'No content will be prepended.');
+ }
+ else {
+ $this->fail('Unknown method/selector combination.');
+ debug($command);
+ }
+ }
+ else {
+ $this->fail('Unknown command.');
+ debug($command);
+ }
+ }
+
+ $this->assertText('test_source:test_suggestion:1');
+ $this->assertText('test_source:test_suggestion:7');
+ $this->assertText('Test suggestion for test source 1');
+ $this->assertText('Test suggestion for test source 7');
+
+ // Add the second suggestion.
+ $edit = array('suggestions_table[2]' => TRUE);
+ $this->drupalPost(NULL, $edit, t('Add suggestions'));
+
+ // Total word count should now include the added job.
+ $this->assertRaw('31');
+ // The suggestion for 7 was added, so there should now be a suggestion
+ // or the suggestion instead.
+ $this->assertNoText('Test suggestion for test source 7');
+ $this->assertText('test_source:test_suggestion_suggestion:7');
+
+ }
+
+ /**
+ * Test the process of aborting and resubmitting the job.
+ */
+ function testAbortJob() {
+ $job = $this->createJob();
+ $job->addItem('test_source', 'test', 1);
+ $job->addItem('test_source', 'test', 2);
+ $job->addItem('test_source', 'test', 3);
+
+ $edit = array(
+ 'target_language' => 'es',
+ 'settings[action]' => 'translate',
+ );
+ $this->drupalPost('admin/tmgmt/jobs/' . $job->tjid, $edit, t('Submit to translator'));
+
+ // Abort job.
+ $this->drupalPost('admin/tmgmt/jobs/' . $job->tjid, array(), t('Abort job'));
+ $this->drupalPost(NULL, array(), t('Confirm'));
+ // Reload job and check its state.
+ entity_get_controller('tmgmt_job')->resetCache();
+ /** @var TMGMTJob $job */
+ $job = tmgmt_job_load($job->tjid);
+ $this->assertTrue($job->isAborted());
+ foreach ($job->getItems() as $item) {
+ $this->assertTrue($item->isAborted());
+ }
+
+ // Resubmit the job.
+ $this->drupalPost('admin/tmgmt/jobs/' . $job->tjid, array(), t('Resubmit'));
+ $this->drupalPost(NULL, array(), t('Confirm'));
+ // Test for the log message.
+ $this->assertRaw(t('This job is a duplicate of the previously aborted job #@id',
+ array('@url' => url('admin/tmgmt/jobs/' . $job->tjid), '@id' => $job->tjid)));
+
+ // Load the resubmitted job and check for its status and values.
+ $url_parts = explode('/', $this->getUrl());
+ $resubmitted_job = tmgmt_job_load(array_pop($url_parts));
+
+ $this->assertTrue($resubmitted_job->isUnprocessed());
+ $this->assertEqual($job->translator, $resubmitted_job->translator);
+ $this->assertEqual($job->source_language, $resubmitted_job->source_language);
+ $this->assertEqual($job->target_language, $resubmitted_job->target_language);
+ $this->assertEqual($job->settings, $resubmitted_job->settings);
+
+ // Test if job items were duplicated correctly.
+ /** @var TMGMTJobItem $item */
+ foreach ($job->getItems() as $item) {
+ // We match job items based on "id #" string. This is not that straight
+ // forward, but it works as the test source text is generated as follows:
+ // Text for job item with type #type and id #id.
+ $_items = $resubmitted_job->getItems(array('data' => array('value' => '%id ' . $item->item_id . '%', 'operator' => 'LIKE')));
+ $_item = reset($_items);
+ /** @var TMGMTJobItem $_item */
+ $this->assertNotEqual($_item->tjid, $item->tjid);
+ $this->assertEqual($_item->plugin, $item->plugin);
+ $this->assertEqual($_item->item_id, $item->item_id);
+ $this->assertEqual($_item->item_type, $item->item_type);
+ // Make sure counts have been recalculated.
+ $this->assertTrue($_item->word_count > 0);
+ $this->assertTrue($_item->count_pending > 0);
+ $this->assertEqual($_item->count_translated, 0);
+ $this->assertEqual($_item->count_accepted, 0);
+ $this->assertEqual($_item->count_reviewed, 0);
+ }
+
+ // Navigate back to the aborted job and check for the log message.
+ $this->drupalGet('admin/tmgmt/jobs/' . $job->tjid);
+ $this->assertRaw(t('Job has been duplicated as a new job #@id.',
+ array('@url' => url('admin/tmgmt/jobs/' . $resubmitted_job->tjid), '@id' => $resubmitted_job->tjid)));
+
+ $this->drupalGet('admin/tmgmt/jobs');
+ $elements = $this->xpath('//table[contains(@class, @view)]//td[contains(., @text)]',
+ array('@view' => 'views-table', '@text' => t('N/A')));
+ $status = $elements[0];
+ $this->assertEqual(trim((string)$status), t('N/A'));
+
+ }
+
+ /**
+ * Test the cart functionality.
+ */
+ function testCart() {
+
+ $this->setEnvironment('fr');
+ $job_items = array();
+ // Create a few job items and add them to the cart.
+ for ($i = 1; $i < 6; $i++) {
+ $job_item = tmgmt_job_item_create('test_source', 'test', $i);
+ $job_item->save();
+ $job_items[$i] = $job_item;
+ }
+
+ $this->loginAsTranslator();
+ foreach ($job_items as $job_item) {
+ $this->drupalGet('tmgmt-add-to-cart/' . $job_item->tjiid);
+ }
+
+ // Check if the items are displayed in the cart.
+ $this->drupalGet('admin/tmgmt/cart');
+ foreach ($job_items as $job_item) {
+ $this->assertText($job_item->label());
+ }
+
+ // Test the remove items from cart functionality.
+ $this->drupalPost(NULL, array('items[1]' => TRUE, 'items[4]' => TRUE), t('Remove selected'));
+ $this->assertText($job_items[2]->label());
+ $this->assertText($job_items[3]->label());
+ $this->assertText($job_items[5]->label());
+ $this->assertNoText($job_items[1]->label());
+ $this->assertNoText($job_items[4]->label());
+ $this->assertText(t('Job items were removed from the cart.'));
+
+ // Test that removed job items from cart were deleted as well.
+ $existing_items = tmgmt_job_item_load_multiple(NULL);
+ $this->assertTrue(!isset($existing_items[$job_items[1]->tjiid]));
+ $this->assertTrue(!isset($existing_items[$job_items[4]->tjiid]));
+
+
+ $this->drupalPost(NULL, array(), t('Empty cart'));
+ $this->assertNoText($job_items[2]->label());
+ $this->assertNoText($job_items[3]->label());
+ $this->assertNoText($job_items[5]->label());
+ $this->assertText(t('All job items were removed from the cart.'));
+
+ // No remaining job items.
+ $existing_items = tmgmt_job_item_load_multiple(NULL);
+ $this->assertTrue(empty($existing_items));
+
+ $language_sequence = array('en', 'en', 'fr', 'fr', 'de', 'de');
+ for ($i = 1; $i < 7; $i++) {
+ $job_item = tmgmt_job_item_create('test_source', 'test', $i);
+ $job_item->save();
+ $job_items[$i] = $job_item;
+ $languages[$job_items[$i]->tjiid] = $language_sequence[$i - 1];
+ }
+ variable_set('tmgmt_test_source_languages', $languages);
+ foreach ($job_items as $job_item) {
+ $this->drupalGet('tmgmt-add-to-cart/' . $job_item->tjiid);
+ }
+
+ $this->drupalPost('admin/tmgmt/cart', array(
+ 'items[' . $job_items[1]->tjiid . ']' => TRUE,
+ 'items[' . $job_items[2]->tjiid . ']' => TRUE,
+ 'items[' . $job_items[3]->tjiid . ']' => TRUE,
+ 'items[' . $job_items[4]->tjiid . ']' => TRUE,
+ 'items[' . $job_items[5]->tjiid . ']' => TRUE,
+ 'target_language[]' => array('en', 'de'),
+ ), t('Request translation'));
+
+ $this->assertText(t('@count jobs need to be checked out.', array('@count' => 4)));
+
+ // We should have four jobs with following language combinations:
+ // [fr, fr] => [en]
+ // [de] => [en]
+ // [en, en] => [de]
+ // [fr, fr] => [de]
+
+ $jobs = entity_load('tmgmt_job', FALSE, array('source_language' => 'fr', 'target_language' => 'en'));
+ $job = reset($jobs);
+ $this->assertEqual(count($job->getItems()), 2);
+
+ $jobs = entity_load('tmgmt_job', FALSE, array('source_language' => 'de', 'target_language' => 'en'));
+ $job = reset($jobs);
+ $this->assertEqual(count($job->getItems()), 1);
+
+ $jobs = entity_load('tmgmt_job', FALSE, array('source_language' => 'en', 'target_language' => 'de'));
+ $job = reset($jobs);
+ $this->assertEqual(count($job->getItems()), 2);
+
+ $jobs = entity_load('tmgmt_job', FALSE, array('source_language' => 'fr', 'target_language' => 'de'));
+ $job = reset($jobs);
+ $this->assertEqual(count($job->getItems()), 2);
+
+ $this->drupalGet('admin/tmgmt/cart');
+ // Both fr and one de items must be gone.
+ $this->assertNoText($job_items[1]->label());
+ $this->assertNoText($job_items[2]->label());
+ $this->assertNoText($job_items[3]->label());
+ $this->assertNoText($job_items[4]->label());
+ $this->assertNoText($job_items[5]->label());
+ // One de item is in the cart as it was not selected for checkout.
+ $this->assertText($job_items[6]->label());
+ }
+
+ /**
+ * Test if the source is able to pull content in requested language.
+ */
+ function testCartEnforceSourceLanguage() {
+ $this->setEnvironment('sk');
+ $this->setEnvironment('cs');
+
+ module_enable(array('tmgmt_node'));
+
+ $content_type = $this->drupalCreateContentType();
+
+ $node_sk = $this->drupalCreateNode(array(
+ 'title' => $this->randomName(),
+ 'language' => 'sk',
+ 'body' => array('sk' => array(array($this->randomName()))),
+ 'type' => $content_type->type,
+ ));
+
+ $this->drupalCreateNode(array(
+ 'title' => $this->randomName(),
+ 'language' => 'en',
+ 'tnid' => $node_sk->nid,
+ 'body' => array('en' => array(array($this->randomName()))),
+ 'type' => $content_type->type,
+ ));
+
+ $node_cs = $this->drupalCreateNode(array(
+ 'title' => $this->randomName(),
+ 'language' => 'cs',
+ 'body' => array('cs' => array(array($this->randomName()))),
+ 'type' => $content_type->type,
+ ));
+
+ $this->loginAsTranslator();
+
+ $job_item_sk = tmgmt_job_item_create('node', 'node', $node_sk->nid);
+ $job_item_sk->save();
+ $this->drupalGet('tmgmt-add-to-cart/' . $job_item_sk->tjiid);
+ $job_items_data[$job_item_sk->item_id] = $job_item_sk->item_type;
+
+ $job_item_cs = tmgmt_job_item_create('node', 'node', $node_cs->nid);
+ $job_item_cs->save();
+ $this->drupalGet('tmgmt-add-to-cart/' . $job_item_cs->tjiid);
+ $job_items_data[$job_item_cs->item_id] = $job_item_cs->item_type;
+
+ $this->drupalPost('admin/tmgmt/cart', array(
+ 'enforced_source_language' => TRUE,
+ 'source_language' => 'en',
+ 'items[' . $job_item_cs->tjiid .']' => TRUE,
+ 'items[' . $job_item_sk->tjiid .']' => TRUE,
+ 'target_language[]' => array('es')
+ ), t('Request translation'));
+
+ $this->assertText(t('One job needs to be checked out.'));
+ $this->assertRaw(t('One item skipped. @language translation unavailable.', array('@language' => 'English')));
+
+ $args = explode('/', $this->getUrl());
+ $tjid = array_pop($args);
+
+ $this->drupalPost(NULL, array(), t('Submit to translator'));
+
+ // We cannot test for the item data as items without a job are not able to
+ // get the data in case the source language is overridden. Therefore only
+ // testing for item_id and item_type values.
+ foreach (tmgmt_job_load($tjid)->getItems() as $job_item) {
+ $this->assertEqual($job_items_data[$job_item->item_id], $job_item->item_type);
+ }
+
+ $this->drupalGet('admin/tmgmt/cart');
+ $this->assertText($node_cs->title);
+ $this->assertNoText($node_sk->title);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui_job.test b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui_job.test
new file mode 100644
index 00000000..14855448
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/tmgmt_ui_job.test
@@ -0,0 +1,78 @@
+ 'Sharemessage translation',
+ 'description' => 'Checks entity translation for an entity without a URL',
+ 'group' => 'Translation Management',
+ 'dependencies' => array('sharemessage', 'entity_translation'),
+ );
+ }
+
+ function setUp() {
+ // Sharemessage is an entity that doesn't provide entity URLs, necessary for
+ // testReviewForm().
+ $modules = array(
+ 'tmgmt_ui',
+ 'tmgmt_entity_ui',
+ 'tmgmt_file',
+ 'image',
+ 'block',
+ 'sharemessage',
+ );
+ parent::setUp($modules);
+ parent::createLanguagesLoginTranslator(array(
+ 'administer sharemessage entities',
+ 'view sharemessage entities',
+ 'administer entity translation',
+ 'translate any entity',
+ ));
+ }
+
+ /**
+ * Test whether the review form is accessible.
+ */
+ function testReviewForm() {
+ // First create a sharemessage.
+ $sharemessage = array(
+ 'label' => 'ShareMessage Test Label',
+ 'name' => 'sharemessage_test_label',
+ 'sharemessage_title[en][0][value]' => 'Test title',
+ 'sharemessage_long[en][0][value]' => 'Test description long',
+ 'block' => 1,
+ );
+ $this->drupalPost('admin/config/services/sharemessage/add', $sharemessage, t('Save share message'));
+ $this->assertText(t('Message @label saved.', array('@label' => $sharemessage['label'])));
+
+ // Enable translation for sharemessage entities.
+ $edit = array(
+ 'entity_translation_entity_types[sharemessage]' => TRUE,
+ );
+ $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
+
+ // Create a corresponding translation job via the UI (spanish translation).
+ $this->drupalPost('admin/config/services/sharemessage/manage/' . $sharemessage['name'] . '/translate', array('languages[es]' => TRUE), t('Request translation'));
+
+ // Submit the job to the file translator.
+ $this->drupalPost('admin/tmgmt/jobs/1', array('translator' => 'file'), t('Submit to translator'));
+
+ // Make sure the job status is on "In progress" after submission.
+ $this->drupalGet('admin/config/services/sharemessage/manage/' . $sharemessage['name'] . '/translate');
+ $this->assertText(t('In progress'));
+
+ // Check that the entity label is there even though there is no entity uri
+ // available.
+ $this->drupalGet('admin/tmgmt/items/1');
+ $this->assertText('ShareMessage');
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_item_messages.view.inc b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_item_messages.view.inc
new file mode 100644
index 00000000..5ccb497b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_item_messages.view.inc
@@ -0,0 +1,115 @@
+name = 'tmgmt_ui_job_item_messages';
+$view->description = 'Lists the messages that are related to a job item.';
+$view->tag = 'Translation Management';
+$view->base_table = 'tmgmt_message';
+$view->human_name = 'Translation Job Item Messages';
+$view->core = 7;
+$view->api_version = '3.0';
+$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+/* Display: Master */
+$handler = $view->new_display('default', 'Master', 'default');
+$handler->display->display_options['title'] = 'Messages';
+$handler->display->display_options['use_more_always'] = FALSE;
+$handler->display->display_options['access']['type'] = 'none';
+$handler->display->display_options['cache']['type'] = 'none';
+$handler->display->display_options['query']['type'] = 'views_query';
+$handler->display->display_options['query']['options']['query_comment'] = FALSE;
+$handler->display->display_options['exposed_form']['type'] = 'basic';
+$handler->display->display_options['pager']['type'] = 'full';
+$handler->display->display_options['pager']['options']['items_per_page'] = '10';
+$handler->display->display_options['style_plugin'] = 'table';
+$handler->display->display_options['style_options']['grouping'] = '';
+$handler->display->display_options['style_options']['columns'] = array(
+ 'created' => 'created',
+ 'message' => 'message',
+);
+$handler->display->display_options['style_options']['default'] = '-1';
+$handler->display->display_options['style_options']['info'] = array(
+ 'created' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'message' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+);
+/* No results behavior: Global: Text area */
+$handler->display->display_options['empty']['area']['id'] = 'area';
+$handler->display->display_options['empty']['area']['table'] = 'views';
+$handler->display->display_options['empty']['area']['field'] = 'area';
+$handler->display->display_options['empty']['area']['content'] = 'There are no messages attached to this translation job item.';
+$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+/* Relationship: Translation Management Message: Uid */
+$handler->display->display_options['relationships']['uid']['id'] = 'uid';
+$handler->display->display_options['relationships']['uid']['table'] = 'tmgmt_message';
+$handler->display->display_options['relationships']['uid']['field'] = 'uid';
+/* Field: Created */
+$handler->display->display_options['fields']['created']['id'] = 'created';
+$handler->display->display_options['fields']['created']['table'] = 'tmgmt_message';
+$handler->display->display_options['fields']['created']['field'] = 'created';
+$handler->display->display_options['fields']['created']['ui_name'] = 'Created';
+$handler->display->display_options['fields']['created']['date_format'] = 'short';
+/* Field: Message */
+$handler->display->display_options['fields']['message']['id'] = 'message';
+$handler->display->display_options['fields']['message']['table'] = 'tmgmt_message';
+$handler->display->display_options['fields']['message']['field'] = 'message';
+$handler->display->display_options['fields']['message']['ui_name'] = 'Message';
+/* Field: User: Name */
+$handler->display->display_options['fields']['name']['id'] = 'name';
+$handler->display->display_options['fields']['name']['table'] = 'users';
+$handler->display->display_options['fields']['name']['field'] = 'name';
+$handler->display->display_options['fields']['name']['relationship'] = 'uid';
+/* Sort criterion: Created */
+$handler->display->display_options['sorts']['created']['id'] = 'created';
+$handler->display->display_options['sorts']['created']['table'] = 'tmgmt_message';
+$handler->display->display_options['sorts']['created']['field'] = 'created';
+$handler->display->display_options['sorts']['created']['ui_name'] = 'Created';
+$handler->display->display_options['sorts']['created']['order'] = 'DESC';
+/* Contextual filter: Job Item */
+$handler->display->display_options['arguments']['tjiid']['id'] = 'tjiid';
+$handler->display->display_options['arguments']['tjiid']['table'] = 'tmgmt_message';
+$handler->display->display_options['arguments']['tjiid']['field'] = 'tjiid';
+$handler->display->display_options['arguments']['tjiid']['ui_name'] = 'Job Item';
+$handler->display->display_options['arguments']['tjiid']['default_argument_type'] = 'node';
+$handler->display->display_options['arguments']['tjiid']['summary']['number_of_records'] = '0';
+$handler->display->display_options['arguments']['tjiid']['summary']['format'] = 'default_summary';
+$handler->display->display_options['arguments']['tjiid']['summary_options']['items_per_page'] = '25';
+
+/* Display: Block */
+$handler = $view->new_display('block', 'Block', 'block');
+$handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+$translatables['tmgmt_ui_job_item_messages'] = array(
+ t('Master'),
+ t('Messages'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('There are no messages attached to this translation job item.'),
+ t('User'),
+ t('Created'),
+ t('Message'),
+ t('Name'),
+ t('All'),
+ t('Block'),
+);
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_items.view.inc b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_items.view.inc
new file mode 100644
index 00000000..99a8db30
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_items.view.inc
@@ -0,0 +1,167 @@
+name = 'tmgmt_ui_job_items';
+$view->description = 'Displays all job items that belong to a job.';
+$view->tag = 'Translation Management';
+$view->base_table = 'tmgmt_job_item';
+$view->human_name = 'Translation Job Items';
+$view->core = 7;
+$view->api_version = '3.0';
+$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+/* Display: Master */
+$handler = $view->new_display('default', 'Master', 'default');
+$handler->display->display_options['title'] = 'Job Items';
+$handler->display->display_options['use_more_always'] = FALSE;
+$handler->display->display_options['access']['type'] = 'none';
+$handler->display->display_options['cache']['type'] = 'none';
+$handler->display->display_options['query']['type'] = 'views_query';
+$handler->display->display_options['query']['options']['query_comment'] = FALSE;
+$handler->display->display_options['exposed_form']['type'] = 'basic';
+$handler->display->display_options['pager']['type'] = 'full';
+$handler->display->display_options['pager']['options']['items_per_page'] = '10';
+$handler->display->display_options['pager']['options']['offset'] = '0';
+$handler->display->display_options['pager']['options']['id'] = '0';
+$handler->display->display_options['style_plugin'] = 'table';
+$handler->display->display_options['style_options']['columns'] = array(
+ 'label' => 'label',
+ 'plugin' => 'plugin',
+ 'state' => 'state',
+ 'changed' => 'changed',
+);
+$handler->display->display_options['style_options']['default'] = '-1';
+$handler->display->display_options['style_options']['info'] = array(
+ 'label' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'plugin' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'state' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'changed' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+);
+/* No results behavior: Global: Text area */
+$handler->display->display_options['empty']['area']['id'] = 'area';
+$handler->display->display_options['empty']['area']['table'] = 'views';
+$handler->display->display_options['empty']['area']['field'] = 'area';
+$handler->display->display_options['empty']['area']['content'] = 'There are no items attached to this translation job.';
+$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+/* Field: Label */
+$handler->display->display_options['fields']['label']['id'] = 'label';
+$handler->display->display_options['fields']['label']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['label']['field'] = 'label';
+$handler->display->display_options['fields']['label']['ui_name'] = 'Label';
+/* Field: Translation Management Job Item: Type */
+$handler->display->display_options['fields']['type']['id'] = 'type';
+$handler->display->display_options['fields']['type']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['type']['field'] = 'type';
+/* Field: State */
+$handler->display->display_options['fields']['state']['id'] = 'state';
+$handler->display->display_options['fields']['state']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['state']['field'] = 'state';
+$handler->display->display_options['fields']['state']['ui_name'] = 'State';
+/* Field: Progress */
+$handler->display->display_options['fields']['progress']['id'] = 'progress';
+$handler->display->display_options['fields']['progress']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['progress']['field'] = 'progress';
+$handler->display->display_options['fields']['progress']['ui_name'] = 'Progress';
+/* Field: Translation Management Job Item: Word count */
+$handler->display->display_options['fields']['word_count_1']['id'] = 'word_count_1';
+$handler->display->display_options['fields']['word_count_1']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['word_count_1']['field'] = 'word_count';
+/* Field: Changed */
+$handler->display->display_options['fields']['changed']['id'] = 'changed';
+$handler->display->display_options['fields']['changed']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['changed']['field'] = 'changed';
+$handler->display->display_options['fields']['changed']['ui_name'] = 'Changed';
+$handler->display->display_options['fields']['changed']['date_format'] = 'short';
+/* Field: Operations */
+$handler->display->display_options['fields']['operations']['id'] = 'operations';
+$handler->display->display_options['fields']['operations']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['operations']['field'] = 'operations';
+$handler->display->display_options['fields']['operations']['ui_name'] = 'Operations';
+$handler->display->display_options['fields']['operations']['element_label_colon'] = FALSE;
+/* Contextual filter: Job Item */
+$handler->display->display_options['arguments']['tjid']['id'] = 'tjid';
+$handler->display->display_options['arguments']['tjid']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['arguments']['tjid']['field'] = 'tjid';
+$handler->display->display_options['arguments']['tjid']['ui_name'] = 'Job Item';
+$handler->display->display_options['arguments']['tjid']['default_argument_type'] = 'node';
+$handler->display->display_options['arguments']['tjid']['summary']['number_of_records'] = '0';
+$handler->display->display_options['arguments']['tjid']['summary']['format'] = 'default_summary';
+$handler->display->display_options['arguments']['tjid']['summary_options']['items_per_page'] = '25';
+
+/* Display: Submit list */
+$handler = $view->new_display('block', 'Submit list', 'submit');
+$handler->display->display_options['defaults']['fields'] = FALSE;
+/* Field: Label */
+$handler->display->display_options['fields']['label']['id'] = 'label';
+$handler->display->display_options['fields']['label']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['label']['field'] = 'label';
+$handler->display->display_options['fields']['label']['ui_name'] = 'Label';
+/* Field: Translation Management Job Item: Type */
+$handler->display->display_options['fields']['type']['id'] = 'type';
+$handler->display->display_options['fields']['type']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['type']['field'] = 'type';
+/* Field: Translation Management Job Item: Word count */
+$handler->display->display_options['fields']['word_count_1']['id'] = 'word_count_1';
+$handler->display->display_options['fields']['word_count_1']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['word_count_1']['field'] = 'word_count';
+/* Field: Operations */
+$handler->display->display_options['fields']['operations']['id'] = 'operations';
+$handler->display->display_options['fields']['operations']['table'] = 'tmgmt_job_item';
+$handler->display->display_options['fields']['operations']['field'] = 'operations';
+$handler->display->display_options['fields']['operations']['ui_name'] = 'Operations';
+$handler->display->display_options['fields']['operations']['element_label_colon'] = FALSE;
+
+/* Display: Block */
+$handler = $view->new_display('block', 'Block', 'block');
+$translatables['tmgmt_ui_job_items'] = array(
+ t('Master'),
+ t('Job Items'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('There are no items attached to this translation job.'),
+ t('Label'),
+ t('Type'),
+ t('State'),
+ t('Progress'),
+ t('Word count'),
+ t('.'),
+ t(','),
+ t('Changed'),
+ t('Operations'),
+ t('All'),
+ t('Submit list'),
+ t('Block'),
+);
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_messages.view.inc b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_messages.view.inc
new file mode 100644
index 00000000..c5a62dd5
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_messages.view.inc
@@ -0,0 +1,143 @@
+name = 'tmgmt_ui_job_messages';
+$view->description = 'Lists the messages that are related to a job.';
+$view->tag = 'Translation Management';
+$view->base_table = 'tmgmt_message';
+$view->human_name = 'Translation Job Messages';
+$view->core = 7;
+$view->api_version = '3.0';
+$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+/* Display: Master */
+$handler = $view->new_display('default', 'Master', 'default');
+$handler->display->display_options['title'] = 'Messages';
+$handler->display->display_options['use_more_always'] = FALSE;
+$handler->display->display_options['access']['type'] = 'none';
+$handler->display->display_options['cache']['type'] = 'none';
+$handler->display->display_options['query']['type'] = 'views_query';
+$handler->display->display_options['query']['options']['query_comment'] = FALSE;
+$handler->display->display_options['exposed_form']['type'] = 'basic';
+$handler->display->display_options['pager']['type'] = 'full';
+$handler->display->display_options['pager']['options']['items_per_page'] = '10';
+$handler->display->display_options['style_plugin'] = 'table';
+$handler->display->display_options['style_options']['grouping'] = '';
+$handler->display->display_options['style_options']['columns'] = array(
+ 'created' => 'created',
+ 'message' => 'message',
+);
+$handler->display->display_options['style_options']['default'] = '-1';
+$handler->display->display_options['style_options']['info'] = array(
+ 'created' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'message' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+);
+/* No results behavior: Global: Text area */
+$handler->display->display_options['empty']['area']['id'] = 'area';
+$handler->display->display_options['empty']['area']['table'] = 'views';
+$handler->display->display_options['empty']['area']['field'] = 'area';
+$handler->display->display_options['empty']['area']['content'] = 'There are no messages attached to this translation job.';
+$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+/* Relationship: Job Item */
+$handler->display->display_options['relationships']['tjiid']['id'] = 'tjiid';
+$handler->display->display_options['relationships']['tjiid']['table'] = 'tmgmt_message';
+$handler->display->display_options['relationships']['tjiid']['field'] = 'tjiid';
+$handler->display->display_options['relationships']['tjiid']['ui_name'] = 'Job Item';
+$handler->display->display_options['relationships']['tjiid']['label'] = 'Job Item';
+/* Relationship: Translation Management Message: Uid */
+$handler->display->display_options['relationships']['uid']['id'] = 'uid';
+$handler->display->display_options['relationships']['uid']['table'] = 'tmgmt_message';
+$handler->display->display_options['relationships']['uid']['field'] = 'uid';
+/* Field: Created */
+$handler->display->display_options['fields']['created']['id'] = 'created';
+$handler->display->display_options['fields']['created']['table'] = 'tmgmt_message';
+$handler->display->display_options['fields']['created']['field'] = 'created';
+$handler->display->display_options['fields']['created']['ui_name'] = 'Created';
+$handler->display->display_options['fields']['created']['date_format'] = 'short';
+/* Field: Message */
+$handler->display->display_options['fields']['message']['id'] = 'message';
+$handler->display->display_options['fields']['message']['table'] = 'tmgmt_message';
+$handler->display->display_options['fields']['message']['field'] = 'message';
+$handler->display->display_options['fields']['message']['ui_name'] = 'Message';
+/* Field: Job Item */
+$handler->display->display_options['fields']['rendered_entity']['id'] = 'rendered_entity';
+$handler->display->display_options['fields']['rendered_entity']['table'] = 'views_entity_tmgmt_job_item';
+$handler->display->display_options['fields']['rendered_entity']['field'] = 'rendered_entity';
+$handler->display->display_options['fields']['rendered_entity']['relationship'] = 'tjiid';
+$handler->display->display_options['fields']['rendered_entity']['ui_name'] = 'Job Item';
+$handler->display->display_options['fields']['rendered_entity']['label'] = 'Related item';
+$handler->display->display_options['fields']['rendered_entity']['empty'] = 'None';
+$handler->display->display_options['fields']['rendered_entity']['hide_alter_empty'] = FALSE;
+$handler->display->display_options['fields']['rendered_entity']['link_to_entity'] = 1;
+/* Field: User: Name */
+$handler->display->display_options['fields']['name']['id'] = 'name';
+$handler->display->display_options['fields']['name']['table'] = 'users';
+$handler->display->display_options['fields']['name']['field'] = 'name';
+$handler->display->display_options['fields']['name']['relationship'] = 'uid';
+/* Sort criterion: Created */
+$handler->display->display_options['sorts']['created']['id'] = 'created';
+$handler->display->display_options['sorts']['created']['table'] = 'tmgmt_message';
+$handler->display->display_options['sorts']['created']['field'] = 'created';
+$handler->display->display_options['sorts']['created']['ui_name'] = 'Created';
+$handler->display->display_options['sorts']['created']['order'] = 'DESC';
+/* Contextual filter: Job */
+$handler->display->display_options['arguments']['tjid']['id'] = 'tjid';
+$handler->display->display_options['arguments']['tjid']['table'] = 'tmgmt_message';
+$handler->display->display_options['arguments']['tjid']['field'] = 'tjid';
+$handler->display->display_options['arguments']['tjid']['ui_name'] = 'Job';
+$handler->display->display_options['arguments']['tjid']['default_argument_type'] = 'node';
+$handler->display->display_options['arguments']['tjid']['summary']['number_of_records'] = '0';
+$handler->display->display_options['arguments']['tjid']['summary']['format'] = 'default_summary';
+$handler->display->display_options['arguments']['tjid']['summary_options']['items_per_page'] = '25';
+/* Contextual filter: Job Item */
+$handler->display->display_options['arguments']['tjiid']['id'] = 'tjiid';
+$handler->display->display_options['arguments']['tjiid']['table'] = 'tmgmt_message';
+$handler->display->display_options['arguments']['tjiid']['field'] = 'tjiid';
+$handler->display->display_options['arguments']['tjiid']['ui_name'] = 'Job Item';
+$handler->display->display_options['arguments']['tjiid']['default_argument_type'] = 'node';
+$handler->display->display_options['arguments']['tjiid']['summary']['number_of_records'] = '0';
+$handler->display->display_options['arguments']['tjiid']['summary']['format'] = 'default_summary';
+$handler->display->display_options['arguments']['tjiid']['summary_options']['items_per_page'] = '25';
+
+/* Display: Block */
+$handler = $view->new_display('block', 'Block', 'block');
+$handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+$translatables['tmgmt_ui_job_messages'] = array(
+ t('Master'),
+ t('Messages'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('There are no messages attached to this translation job.'),
+ t('Job Item'),
+ t('User'),
+ t('Created'),
+ t('Message'),
+ t('Related item'),
+ t('None'),
+ t('Name'),
+ t('All'),
+ t('Block'),
+);
diff --git a/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_overview.view.inc b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_overview.view.inc
new file mode 100644
index 00000000..a4577b56
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/ui/views/tmgmt_ui_job_overview.view.inc
@@ -0,0 +1,321 @@
+name = 'tmgmt_ui_job_overview';
+$view->description = 'Gives a bulk operation overview of translation jobs in the system.';
+$view->tag = 'Translation Management';
+$view->base_table = 'tmgmt_job';
+$view->human_name = 'Translation Job Overview';
+$view->core = 7;
+$view->api_version = '3.0';
+$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+/* Display: Master */
+$handler = $view->new_display('default', 'Master', 'default');
+$handler->display->display_options['title'] = 'Job overview';
+$handler->display->display_options['use_more_always'] = FALSE;
+$handler->display->display_options['access']['type'] = 'tmgmt_views_job_access';
+$handler->display->display_options['cache']['type'] = 'none';
+$handler->display->display_options['query']['type'] = 'views_query';
+$handler->display->display_options['query']['options']['query_comment'] = FALSE;
+$handler->display->display_options['exposed_form']['type'] = 'basic';
+$handler->display->display_options['pager']['type'] = 'full';
+$handler->display->display_options['pager']['options']['items_per_page'] = '10';
+$handler->display->display_options['style_plugin'] = 'table';
+$handler->display->display_options['style_options']['columns'] = array(
+ 'views_bulk_operations' => 'views_bulk_operations',
+ 'label' => 'label',
+ 'source_language' => 'source_language',
+ 'target_language' => 'target_language',
+ 'label_1' => 'label_1',
+ 'state' => 'state',
+ 'created' => 'created',
+ 'changed' => 'changed',
+ 'operations' => 'operations',
+);
+$handler->display->display_options['style_options']['default'] = 'changed';
+$handler->display->display_options['style_options']['info'] = array(
+ 'views_bulk_operations' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'label' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'source_language' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'target_language' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'label_1' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'state' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'created' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'changed' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'operations' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+);
+/* No results behavior: Global: Text area */
+$handler->display->display_options['empty']['area']['id'] = 'area';
+$handler->display->display_options['empty']['area']['table'] = 'views';
+$handler->display->display_options['empty']['area']['field'] = 'area';
+$handler->display->display_options['empty']['area']['content'] = 'There are no translation jobs that match the specified filter criteria.';
+$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+/* Relationship: Translator */
+$handler->display->display_options['relationships']['translator']['id'] = 'translator';
+$handler->display->display_options['relationships']['translator']['table'] = 'tmgmt_job';
+$handler->display->display_options['relationships']['translator']['field'] = 'translator';
+$handler->display->display_options['relationships']['translator']['ui_name'] = 'Translator';
+$handler->display->display_options['relationships']['translator']['label'] = 'Translator';
+/* Field: Bulk operations */
+$handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations';
+$handler->display->display_options['fields']['views_bulk_operations']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations';
+$handler->display->display_options['fields']['views_bulk_operations']['ui_name'] = 'Bulk operations';
+$handler->display->display_options['fields']['views_bulk_operations']['label'] = '';
+$handler->display->display_options['fields']['views_bulk_operations']['element_label_colon'] = FALSE;
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '0';
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1;
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0;
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10';
+$handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array(
+ 'rules_component::rules_tmgmt_job_accept_translation' => array(
+ 'selected' => 1,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 1,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'rules_component::rules_tmgmt_job_abort_translation' => array(
+ 'selected' => 1,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 1,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::views_bulk_operations_delete_item' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 1,
+ 'label' => 'Delete Job',
+ ),
+ 'rules_component::rules_tmgmt_job_delete' => array(
+ 'selected' => 1,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::views_bulk_operations_script_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+ 'action::views_bulk_operations_modify_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ 'settings' => array(
+ 'show_all_tokens' => 1,
+ 'display_values' => array(
+ '_all_' => '_all_',
+ ),
+ ),
+ ),
+ 'action::views_bulk_operations_argument_selector_action' => array(
+ 'selected' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ 'settings' => array(
+ 'url' => '',
+ ),
+ ),
+ 'action::system_send_email_action' => array(
+ 'selected' => 0,
+ 'postpone_processing' => 0,
+ 'skip_confirmation' => 0,
+ 'override_label' => 0,
+ 'label' => '',
+ ),
+);
+/* Field: Label */
+$handler->display->display_options['fields']['label']['id'] = 'label';
+$handler->display->display_options['fields']['label']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['label']['field'] = 'label';
+$handler->display->display_options['fields']['label']['ui_name'] = 'Label';
+/* Field: From */
+$handler->display->display_options['fields']['source_language']['id'] = 'source_language';
+$handler->display->display_options['fields']['source_language']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['source_language']['field'] = 'source_language';
+$handler->display->display_options['fields']['source_language']['ui_name'] = 'From';
+$handler->display->display_options['fields']['source_language']['label'] = 'From';
+/* Field: To */
+$handler->display->display_options['fields']['target_language']['id'] = 'target_language';
+$handler->display->display_options['fields']['target_language']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['target_language']['field'] = 'target_language';
+$handler->display->display_options['fields']['target_language']['ui_name'] = 'To';
+$handler->display->display_options['fields']['target_language']['label'] = 'To';
+/* Field: State */
+$handler->display->display_options['fields']['state']['id'] = 'state';
+$handler->display->display_options['fields']['state']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['state']['field'] = 'state';
+$handler->display->display_options['fields']['state']['ui_name'] = 'State';
+/* Field: Translator */
+$handler->display->display_options['fields']['translator']['id'] = 'translator';
+$handler->display->display_options['fields']['translator']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['translator']['field'] = 'translator';
+$handler->display->display_options['fields']['translator']['ui_name'] = 'Translator';
+/* Field: Progress */
+$handler->display->display_options['fields']['progress']['id'] = 'progress';
+$handler->display->display_options['fields']['progress']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['progress']['field'] = 'progress';
+$handler->display->display_options['fields']['progress']['ui_name'] = 'Progress';
+/* Field: Translation Management Job: Word count */
+$handler->display->display_options['fields']['word_count']['id'] = 'word_count';
+$handler->display->display_options['fields']['word_count']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['word_count']['field'] = 'word_count';
+/* Field: Changed */
+$handler->display->display_options['fields']['changed']['id'] = 'changed';
+$handler->display->display_options['fields']['changed']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['changed']['field'] = 'changed';
+$handler->display->display_options['fields']['changed']['ui_name'] = 'Changed';
+$handler->display->display_options['fields']['changed']['date_format'] = 'short';
+/* Field: Operations */
+$handler->display->display_options['fields']['operations']['id'] = 'operations';
+$handler->display->display_options['fields']['operations']['table'] = 'tmgmt_job';
+$handler->display->display_options['fields']['operations']['field'] = 'operations';
+$handler->display->display_options['fields']['operations']['ui_name'] = 'Operations';
+/* Sort criterion: Changed */
+$handler->display->display_options['sorts']['changed']['id'] = 'changed';
+$handler->display->display_options['sorts']['changed']['table'] = 'tmgmt_job';
+$handler->display->display_options['sorts']['changed']['field'] = 'changed';
+$handler->display->display_options['sorts']['changed']['ui_name'] = 'Changed';
+$handler->display->display_options['sorts']['changed']['order'] = 'DESC';
+/* Filter criterion: State */
+$handler->display->display_options['filters']['state']['id'] = 'state';
+$handler->display->display_options['filters']['state']['table'] = 'tmgmt_job';
+$handler->display->display_options['filters']['state']['field'] = 'state';
+$handler->display->display_options['filters']['state']['ui_name'] = 'State';
+$handler->display->display_options['filters']['state']['exposed'] = TRUE;
+$handler->display->display_options['filters']['state']['expose']['operator_id'] = 'state_op';
+$handler->display->display_options['filters']['state']['expose']['label'] = 'State';
+$handler->display->display_options['filters']['state']['expose']['operator'] = 'state_op';
+$handler->display->display_options['filters']['state']['expose']['identifier'] = 'state';
+/* Filter criterion: From */
+$handler->display->display_options['filters']['source_language']['id'] = 'source_language';
+$handler->display->display_options['filters']['source_language']['table'] = 'tmgmt_job';
+$handler->display->display_options['filters']['source_language']['field'] = 'source_language';
+$handler->display->display_options['filters']['source_language']['ui_name'] = 'From';
+$handler->display->display_options['filters']['source_language']['exposed'] = TRUE;
+$handler->display->display_options['filters']['source_language']['expose']['operator_id'] = 'source_language_op';
+$handler->display->display_options['filters']['source_language']['expose']['label'] = 'From';
+$handler->display->display_options['filters']['source_language']['expose']['operator'] = 'source_language_op';
+$handler->display->display_options['filters']['source_language']['expose']['identifier'] = 'from';
+/* Filter criterion: To */
+$handler->display->display_options['filters']['target_language']['id'] = 'target_language';
+$handler->display->display_options['filters']['target_language']['table'] = 'tmgmt_job';
+$handler->display->display_options['filters']['target_language']['field'] = 'target_language';
+$handler->display->display_options['filters']['target_language']['ui_name'] = 'To';
+$handler->display->display_options['filters']['target_language']['exposed'] = TRUE;
+$handler->display->display_options['filters']['target_language']['expose']['operator_id'] = 'target_language_op';
+$handler->display->display_options['filters']['target_language']['expose']['label'] = 'To';
+$handler->display->display_options['filters']['target_language']['expose']['operator'] = 'target_language_op';
+$handler->display->display_options['filters']['target_language']['expose']['identifier'] = 'to';
+/* Filter criterion: Translator */
+$handler->display->display_options['filters']['translator']['id'] = 'translator';
+$handler->display->display_options['filters']['translator']['table'] = 'tmgmt_job';
+$handler->display->display_options['filters']['translator']['field'] = 'translator';
+$handler->display->display_options['filters']['translator']['ui_name'] = 'Translator';
+$handler->display->display_options['filters']['translator']['exposed'] = TRUE;
+$handler->display->display_options['filters']['translator']['expose']['operator_id'] = 'translator_op';
+$handler->display->display_options['filters']['translator']['expose']['label'] = 'Translator';
+$handler->display->display_options['filters']['translator']['expose']['operator'] = 'translator_op';
+$handler->display->display_options['filters']['translator']['expose']['identifier'] = 'translator';
+
+/* Display: Page */
+$handler = $view->new_display('page', 'Page', 'page');
+$handler->display->display_options['path'] = 'admin/tmgmt/overview';
+$handler->display->display_options['menu']['type'] = 'default tab';
+$handler->display->display_options['menu']['title'] = 'Jobs';
+$handler->display->display_options['menu']['weight'] = '-1';
+$handler->display->display_options['menu']['context'] = 0;
+$handler->display->display_options['tab_options']['type'] = 'normal';
+$handler->display->display_options['tab_options']['title'] = 'Translation';
+$handler->display->display_options['tab_options']['description'] = 'Translation overview';
+$handler->display->display_options['tab_options']['weight'] = -9;
+$handler->display->display_options['tab_options']['name'] = 'management';
+$translatables['tmgmt_ui_job_overview'] = array(
+ t('Master'),
+ t('Job overview'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('There are no translation jobs that match the specified filter criteria.'),
+ t('Translator'),
+ t('Label'),
+ t('From'),
+ t('To'),
+ t('State'),
+ t('Progress'),
+ t('Word count'),
+ t('Changed'),
+ t('Operations'),
+ t('Page'),
+);
+
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_entity_label.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_entity_label.inc
new file mode 100644
index 00000000..f827cafd
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_entity_label.inc
@@ -0,0 +1,18 @@
+get_value($values)) {
+ return $entity->label();
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_count.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_count.inc
new file mode 100644
index 00000000..f2d195dd
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_count.inc
@@ -0,0 +1,71 @@
+ '');
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+ $options = array('' => t('- All -'));
+ $options += tmgmt_job_item_states();
+ $form['state'] = array(
+ '#title' => t('Job item status'),
+ '#description' => t('Count only job items of a certain status.'),
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $this->options['state'],
+ );
+ }
+
+ function use_group_by() {
+ return FALSE;
+ }
+
+
+ function query() {
+ $this->ensure_my_table();
+
+ // Therefore construct the join.
+ $join = new views_join();
+ $join->definition['left_table'] = $this->table_alias;
+ $join->definition['left_field'] = $this->real_field;
+ $join->definition['table'] = 'tmgmt_job_item';
+ $join->definition['field'] = 'tjid';
+ $join->definition['type'] = 'LEFT';
+
+ if (!empty($this->options['state'])) {
+ $join->extra = array(array(
+ 'field' => 'state',
+ 'value' => $this->options['state']
+ ));
+ }
+ $join->construct();
+
+ // Add the join to the tmgmt_job_item table.
+ $this->table_alias = $this->query->add_table('tmgmt_job_item', $this->relationship, $join);
+
+ // And finally add the count of the job items field.
+ $params = array('function' => 'count');
+ $this->field_alias = $this->query->add_field($this->table_alias, 'tjiid', NULL, $params);
+
+ $this->add_additional_fields();
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_operations.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_operations.inc
new file mode 100644
index 00000000..580ca12b
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_operations.inc
@@ -0,0 +1,43 @@
+get_value($values);
+ $element = array();
+ $element['#theme'] = 'links';
+ $element['#attributes'] = array('class' => array('inline'));
+ $uri = $item->uri();
+ if ($item->getCountTranslated() > 0 && entity_access('accept', 'tmgmt_job_item', $item)) {
+ $element['#links']['review'] = array(
+ 'href' => $uri['path'],
+ 'query' => array('destination' => current_path()),
+ 'title' => t('review'),
+ );
+ }
+ // Do not display view on unprocessed jobs.
+ elseif (!$item->getJob()->isUnprocessed()) {
+ $element['#links']['view'] = array(
+ 'href' => $uri['path'],
+ 'query' => array('destination' => current_path()),
+ 'title' => t('view'),
+ );
+ }
+ if (user_access('administer tmgmt') && !$item->isAccepted()) {
+ $element['#links']['delete'] = array(
+ 'href' => 'admin/tmgmt/items/' . $item->tjiid . '/delete',
+ 'query' => array('destination' => current_path()),
+ 'title' => t('delete'),
+ );
+ }
+ return drupal_render($element);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_type.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_type.inc
new file mode 100644
index 00000000..5f6a86b4
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_item_type.inc
@@ -0,0 +1,16 @@
+get_value($values)) {
+ return $entity->getSourceType();
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_operations.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_operations.inc
new file mode 100644
index 00000000..2011d116
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_job_operations.inc
@@ -0,0 +1,48 @@
+get_value($values);
+ $element = array();
+ $element['#theme'] = 'links';
+ $element['#attributes'] = array('class' => array('inline'));
+ $uri = $job->uri();
+ if ($job->isSubmittable() && entity_access('submit', 'tmgmt_job', $job)) {
+ $element['#links']['submit'] = array(
+ 'href' => $uri['path'],
+ 'query' => array('destination' => current_path()),
+ 'title' => t('submit'),
+ );
+ }
+ else {
+ $element['#links']['manage'] = array(
+ 'href' => $uri['path'],
+ 'title' => t('manage'),
+ );
+ }
+ if ($job->isAbortable() && entity_access('submit', 'tmgmt_job', $job)) {
+ $element['#links']['cancel'] = array(
+ 'href' => 'admin/tmgmt/jobs/' . $job->tjid . '/abort',
+ 'query' => array('destination' => current_path()),
+ 'title' => t('abort'),
+ );
+ }
+ if ($job->isDeletable() && user_access('administer tmgmt')) {
+ $element['#links']['delete'] = array(
+ 'href' => 'admin/tmgmt/jobs/' . $job->tjid . '/delete',
+ 'query' => array('destination' => current_path()),
+ 'title' => t('delete'),
+ );
+ }
+ return drupal_render($element);
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_message_message.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_message_message.inc
new file mode 100644
index 00000000..fa9f5b9d
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_message_message.inc
@@ -0,0 +1,47 @@
+additional_fields['variables'] = 'variables';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+ $options['format'] = array('default' => 'formatted');
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+ $form['format'] = array(
+ '#type' => 'select',
+ '#title' => t('Format'),
+ '#description' => t("Choose whether the field should display the raw text or display formatted text with replaced variables with it's values."),
+ '#default_value' => $this->options['format'],
+ '#options' => array(
+ 'formatted' => t('Formatted'),
+ 'raw' => t('Raw'),
+ ),
+ );
+ }
+
+ function render($values) {
+ if ($message = $this->get_value($values)) {
+ if ($this->options['format'] == 'formatted') {
+ $message = $this->sanitize_value(t($message, unserialize($this->get_value($values, 'variables'))), 'xss');
+ }
+ else {
+ $message = $this->sanitize_value($message, 'xss');
+ }
+ return $message;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_progress.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_progress.inc
new file mode 100644
index 00000000..11855f38
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_progress.inc
@@ -0,0 +1,105 @@
+entity_type == 'tmgmt_job') {
+ $tjids = array();
+ foreach ($values as $value) {
+ // Do not load statistics for aborted jobs.
+ if ($value->tmgmt_job_state == TMGMT_JOB_STATE_ABORTED) {
+ continue;
+ }
+ $tjids[] = $value->tjid;
+ }
+ tmgmt_job_statistics_load($tjids);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function render($values) {
+ /** @var TMGMTJobItem|TMGMTJob $object */
+ $object = $this->get_value($values);
+ // If job has been aborted the status info is not applicable.
+ if ($object->isAborted()) {
+ return t('N/A');
+ }
+
+ $counts = array(
+ '@accepted' => $object->getCountAccepted(),
+ '@reviewed' => $object->getCountReviewed(),
+ '@translated' => $object->getCountTranslated(),
+ '@pending' => $object->getCountPending(),
+ );
+ $id = $object->internalIdentifier();
+
+ if (module_exists('google_chart_tools')) {
+ draw_chart($this->build_progressbar_settings($id, $counts));
+ return '';
+ }
+ $title = t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', $counts);
+ return sprintf('%s', $title, implode('/', $counts));
+ }
+
+ /**
+ * Creates a settings array for the google chart tools.
+ *
+ * The settings are preset with values to display a progress bar for either
+ * a job or job item.
+ *
+ * @param $id
+ * The id of the chart.
+ * @param $counts
+ * Array with the counts for accepted, translated and pending.
+ * @param $prefix
+ * Prefix to id.
+ * @return
+ * Settings array.
+ */
+ function build_progressbar_settings($id, $counts, $prefix = 'progress') {
+ $settings['chart'][$prefix . $id] = array(
+ 'header' => array(t('Accepted'), t('Reviewed'), t('Translated'), t('Pending')),
+ 'rows' => array(
+ array($counts['@accepted'], $counts['@reviewed'], $counts['@translated'], $counts['@pending']),
+ ),
+ 'columns' => array(''),
+ 'chartType' => 'PieChart',
+ 'containerId' => $prefix . $id,
+ 'options' => array(
+ 'backgroundColor' => 'transparent',
+ 'colors' => array('#00b600', '#60ff60', '#ffff00', '#6060ff'),
+ 'forceIFrame' => FALSE,
+ 'chartArea' => array(
+ 'left' => 0,
+ 'top' => 0,
+ 'width' => '50%',
+ 'height' => '100%',
+ ),
+ 'fontSize' => 9,
+ 'title' => t('Progress'),
+ 'titlePosition' => 'none',
+ 'width' => 60,
+ 'height' => 50,
+ 'isStacked' => TRUE,
+ 'legend' => array('position' => 'none'),
+ 'pieSliceText' => 'none',
+ )
+ );
+ return $settings;
+ }
+
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_translator.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_translator.inc
new file mode 100644
index 00000000..6fbc1d8e
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_translator.inc
@@ -0,0 +1,21 @@
+get_value($values)) {
+ $translators = tmgmt_translator_labels();
+ return isset($translators[$entity->translator]) ? check_plain($translators[$entity->translator]) : t('Missing translator');
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_wordcount.inc b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_wordcount.inc
new file mode 100644
index 00000000..3e42df19
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/handlers/tmgmt_handler_field_tmgmt_wordcount.inc
@@ -0,0 +1,34 @@
+entity_type == 'tmgmt_job') {
+ $tjids = array();
+ foreach ($values as $value) {
+ $tjids[] = $value->tjid;
+ }
+ tmgmt_job_statistics_load($tjids);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function render($values) {
+ $object = $this->get_value($values);
+ return $object->getWordCount();
+ }
+}
diff --git a/sites/all/modules/contrib/localisation/tmgmt/views/plugins/tmgmt_views_job_access.inc b/sites/all/modules/contrib/localisation/tmgmt/views/plugins/tmgmt_views_job_access.inc
new file mode 100644
index 00000000..7ba7f013
--- /dev/null
+++ b/sites/all/modules/contrib/localisation/tmgmt/views/plugins/tmgmt_views_job_access.inc
@@ -0,0 +1,33 @@
+views_data();
+ }
+ return $data;
+}
+
+/**
+ * Views controller class for the job item entity.
+ */
+class TMGMTJobItemViewsController extends EntityDefaultViewsController {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function views_data() {
+ $data = parent::views_data();
+ $data['tmgmt_job_item']['label'] = array(
+ 'title' => t('Label'),
+ 'help' => t('Displays a label of the job item.'),
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_entity_label',
+ ),
+ );
+ $data['tmgmt_job_item']['type'] = array(
+ 'title' => t('Type'),
+ 'help' => t('Displays a type of the job item.'),
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_job_item_type',
+ ),
+ );
+ $data['tmgmt_job_item']['progress'] = array(
+ 'title' => t('Progress'),
+ 'help' => t('Displays the progress of a job item.'),
+ 'real field' => 'tjiid',
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_progress',
+ ),
+ );
+ $data['tmgmt_job_item']['operations'] = array(
+ 'title' => t('Operations'),
+ 'help' => t('Displays a list of options which are available for a job item.'),
+ 'real field' => 'tjiid',
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_job_item_operations',
+ ),
+ );
+ return $data;
+ }
+
+}
+
+/**
+ * Views controller class for the job entity.
+ */
+class TMGMTJobViewsController extends EntityDefaultViewsController {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function views_data() {
+ $data = parent::views_data();
+ $data['tmgmt_job']['operations'] = array(
+ 'title' => t('Operations'),
+ 'help' => t('Displays a list of options which are available for a job.'),
+ 'real field' => 'tjid',
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_job_operations',
+ ),
+ );
+ $data['tmgmt_job']['progress'] = array(
+ 'title' => t('Progress'),
+ 'help' => t('Displays the progress of a job.'),
+ 'real field' => 'tjid',
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_progress',
+ ),
+ );
+ $data['tmgmt_job']['word_count'] = array(
+ 'title' => t('Word count'),
+ 'help' => t('Displays the word count of a job.'),
+ 'real field' => 'tjid',
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_wordcount',
+ ),
+ );
+ $data['tmgmt_job']['label']['field']['handler'] = 'tmgmt_handler_field_tmgmt_entity_label';
+ $data['tmgmt_job']['translator']['field']['handler'] = 'tmgmt_handler_field_tmgmt_translator';
+ $data['tmgmt_job']['job_item'] = array(
+ 'title' => t('Job items'),
+ 'help' => t('Get the job items of the job'),
+ 'relationship' => array(
+ 'base' => 'tmgmt_job_item',
+ 'base field' => 'tjid',
+ 'real field' => 'tjid',
+ 'label' => t('Job items'),
+ ),
+ );
+ $data['tmgmt_job']['item_count'] = array(
+ 'title' => t('Job item count'),
+ 'help' => t('Show the amount of job items per job (per job item status)'),
+ 'real field' => 'tjid',
+ 'field' => array(
+ 'handler' => 'tmgmt_handler_field_tmgmt_job_item_count',
+ ),
+ );
+ return $data;
+ }
+
+}
+/**
+ * Views controller class for the job message entity.
+ */
+class TMGMTMessageViewsController extends EntityDefaultViewsController {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function views_data() {
+ $data = parent::views_data();
+ $data['tmgmt_message']['message']['field']['handler'] = 'tmgmt_handler_field_tmgmt_message_message';
+ return $data;
+ }
+
+}
+
+interface TMGMTSourceViewsControllerInterface extends TMGMTPluginBaseInterface {
+
+ /**
+ * Defines the result for hook_views_data().
+ */
+ public function views_data();
+
+}
+
+/**
+ * Vies controller class for source plugins.
+ */
+class TMGMTDefaultSourceViewsController extends TMGMTPluginBase implements TMGMTSourceViewsControllerInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function views_data() {
+ // @todo Implement this in a generic fashion.
+ /* $key = $this->pluginInfo['something'];
+ $data[$key]['tmgmt_translatable_types'] = array(
+ 'title' => t('Translatable types'),
+ 'help' => t('Filter translatable elements based on their types.'),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_in_operator',
+ 'real field' => 'type',
+ 'options callback' => 'tmgmt_source_translatable_item_types',
+ 'options arguments' => array($this->pluginType),
+ ),
+ ); */
+ return array();
+ }
+
+}
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/LICENSE.txt b/sites/all/modules/contrib/taxonomy/tac_lite/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/README.txt b/sites/all/modules/contrib/taxonomy/tac_lite/README.txt
new file mode 100644
index 00000000..a2e23c73
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/README.txt
@@ -0,0 +1,124 @@
+VERSION: 7.x-1.x development build
+
+OVERVIEW
+--------
+
+Tac_lite stands for Taxonomy Access Control Lite. This module
+grants access so that some users may see content that is
+hidden from others. A simple scheme based on taxonomy, roles and
+users controls which content is hidden.
+
+Bear in mind that, like all modules which use Drupal's built-in
+node_access features, this module does not prevent users from
+viewing/editing nodes which Drupal's permission allow them to
+view/edit. To use, configure Drupal to not grant the permission, then
+configure tac_lite to grant it.
+
+As the name implies, this module shares some functionality with an
+earlier module called Taxonomy Access Control (TAC). If you are
+shopping around for an access control module to use, consider that one
+as you may find that it suits your needs. In my case, I wanted access
+control but without some of the complexity introduced by TAC. I also
+wanted more flexibility in granting access on a per user basis.
+
+Here are some key features of tac_lite:
+
+* Designed to be as simple as possible in installation and administration.
+
+* Uses Drupal's node_access hooks and taxonomy module to leave the
+ smallest possible footprint while doing it's job. For example, it
+ introduces no new database tables.
+
+* Grant permissions based on roles.
+
+* Grant permissions per user. (Give a specific user access beyond
+ what his/her roles allow).
+
+* Supports view, update and delete permissions.
+
+USE CASE
+--------
+
+Here's how I originally used this module. This description might make
+it easier to understand why one might prefer tac_lite over TAC.
+
+My website helps me manage my work projects. I use Drupal's project
+module to track issues. Some of my projects are for the public to see
+(i.e. Drupal modules) others are limited to my clients and partners.
+These restricted projects should be visible only to me, the client in
+question, and partner(s) working on that particular project.
+
+I've defined a vocabulary for my projects (same one used by
+project.module) and I've defined a client role and a partner role.
+Partners can contribute to the website, while clients can read content
+but post only issues.
+
+Using TAC (or as far as I know all other access control modules) I
+would have to create a new role for each project/role combination.
+That is, for the Acme project I'd have to create roles 'Acme Client'
+and 'Acme Partner' in order to assign permissions just the way I want
+them.
+
+Using tac_lite, I simply associate each user with the project(s) they
+are allowed to see. That is, I associate some clients and some
+partners with Acme. Their role (client or partner) controls what they
+can do, and the associations through tac_lite control what they can
+see.
+
+INSTALL
+-------
+
+Enable taxonomy module. It's required.
+
+Install this package the normal way.
+- put this file in a subdirectory of the modules directory.
+- enable using admin interface
+- no database tables to install.
+
+
+USAGE
+-----
+
+Log in as an administrator. (uid==1, or a user with
+administer_tac_lite permission)
+
+Create a vocabulary which you will use to categorize private nodes.
+You may want to create a vocabulary called "Privacy" with terms like
+"public", "private", and "administers only".
+
+Associate the vocabulary with node types, as you would normally do.
+
+Go to administer >> user management >> access control >> access
+control by taxonomy.
+
+Select the category you created in the earlier step ("Privacy").
+
+Create some content. Choose a node type you've associated with "Privacy".
+
+Note that you can view the content you just created. Other users cannot.
+
+Edit the account of another user. Go to the tac_lite access tab under edit.
+
+Select a term you selected when creating the node and submit changes.
+
+Now the user can also access the node you created.
+
+
+NOTES
+-----
+
+If behavior of this or any other access control module seems to be
+incorrect, try rebuilding the node access table. This may be done
+under administer >> content management >> post settings. There is a
+button there labelled "rebuild permissions"
+
+Another useful tool is a sub-module of the devel module, called
+devel_node_access which can give you some insight into the contents of
+your node_access table. Recommended for troubleshooting.
+
+
+AUTHOR
+------
+
+Dave Cohen
+http://www.dave-cohen.com
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.info b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.info
new file mode 100644
index 00000000..fcd963ad
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.info
@@ -0,0 +1,11 @@
+name = Taxonomy Access Control Lite
+description = Simple access control based on categories.
+dependencies[] = taxonomy
+core = 7.x
+package = Access control
+; Information added by Drupal.org packaging script on 2015-10-11
+version = "7.x-1.2+2-dev"
+core = "7.x"
+project = "tac_lite"
+datestamp = "1444524081"
+
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.install b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.install
new file mode 100644
index 00000000..0fee5b5b
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.install
@@ -0,0 +1,118 @@
+fetchField();
+ $num_updated = db_update('system')
+ ->fields(array(
+ 'weight' => $taxonomy_weight + 9,
+ ))
+ ->condition('name', 'tac_lite')
+ ->execute();
+
+ // Note that it is not necessary to rebuild the node access table here, as
+ // that will be done when module settings are saved.
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * Clean up tac_lite variables.
+ */
+function tac_lite_uninstall() {
+ for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) {
+ variable_del('tac_lite_config_scheme_' . $i);
+ variable_del('tac_lite_grants_scheme_' . $i);
+ }
+ variable_del('tac_lite_schemes');
+ variable_del('tac_lite_categories');
+}
+
+/**
+ * Ensure that tac_lite hooks are invoked after taxonomy module hooks.
+ */
+function tac_lite_update_1() {
+ $taxonomy_weight = db_query("SELECT weight FROM {system} WHERE name = 'taxonomy'")->fetchField();
+ $num_updated = db_update('system')
+ ->fields(array(
+ 'weight' => $taxonomy_weight + 9,
+ ))
+ ->condition('name', 'tac_lite')
+ ->execute();
+}
+
+/**
+ * Ensure that the node_access table is thoroughly cleaned up in Drupal 5 update.
+ */
+function tac_lite_update_2() {
+ node_access_rebuild(); // Would batch mode help here?
+ // Assume success and return with message.
+ return t('Rebuilt node access table for tac_lite module.');
+}
+
+/**
+ * Introducing schemes. Rename tac_lite_default_grants to tac_lite_grants_scheme_1.
+ */
+function tac_lite_update_3() {
+ $num_updated = db_update('variable')
+ ->fields(array(
+ 'name' => 'tac_lite_grants_scheme_1',
+ ))
+ ->condition('name', 'tac_lite_default_grants')
+ ->execute();
+}
+
+/**
+ * Start of updates to Drupal 6.x-1.2. Start using Drupal standard
+ * update numbers.
+ */
+
+/**
+ * Rename permission from "administer_tac_lite" to "administer
+ * tac_lite" for UI consistency.
+ */
+function tac_lite_update_6001() {
+ // TODO: Please review to make sure this is handling this update properly for this version of code. (only change was formatting and table name)
+ $result = db_query("SELECT * FROM {role_permission} WHERE perm LIKE '%administer_tac_lite%'");
+ foreach ($result as $permission) {
+ $perm = str_replace('administer_tac_lite', 'administer tac_lite', $permission->perm);
+ //db_query("UPDATE {permission} SET perm = '". db_escape_string($perm) ."' WHERE rid =". $permission->rid);
+ $num_updated = db_update('permission')
+ ->fields(array(
+ 'perm' => $perm,
+ ))
+ ->condition('rid', $permission->rid)
+ ->execute();
+ }
+}
+
+/**
+ * The tac_lite.module now supports an option to apply access by taxonomy to unpublished nodes as well as published content. The default behavior is that tac_lite has no effect on unpublished content. You should review each of your tac_lite schemes and, optionally, adjust this setting before rebuilding node access permissions.
+ */
+function tac_lite_update_7001() {
+ // See https://drupal.org/node/1918272 for details.
+ drupal_set_message(t('Please review each of your taxonomy access control schemes. If necessary, adjust the new option to affect access to unpublished content. Then rebuild content access permissions.', array(
+ '!url' => url('admin/config/people/tac_lite'),
+ )));
+ node_access_needs_rebuild(TRUE);
+}
+
+/**
+ * Rebuild node_access permissions, for sites upgrading from tac_lite 1.0 (or
+ * 1.1) to 1.2. This will fix a bug in which some nodes were erroneously added
+ * to the node_access table. You will be prompted to rebuild access permissions
+ * after the update process is complete. (See the status report page.)
+ */
+function tac_lite_update_7002() {
+ node_access_needs_rebuild(TRUE);
+}
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.module b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.module
new file mode 100644
index 00000000..69b645bf
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite.module
@@ -0,0 +1,735 @@
+' . t('') . '';
+ $output = '
' . t('Taxonomy Access Control Lite allows you to restrict access to site content. It uses a simple scheme based on Taxonomy, Users and Roles.') . '
';
+ $output .= '
' . t('This module leverages Drupal\'s node_access table allows this module to grant permission to view, update, and delete nodes. To control which users can create new nodes, use Drupal\'s role based permissions.') . '
';
+ $output .= '
' . t('It is important to understand that this module grants privileges, as opposed to revoking privileges. So, use Drupal\'s built-in permissions to hide content from certain roles, then use this module to show the content. This module cannot hide content that the user is allowed to see based on their existing privileges.') . '
';
+$output .= '
' . t('There are several steps required to set up Taxonomy Access Control Lite.') . '
';
+ $output .= '';
+ $output .= '
' . t('Define one or more vocabularies whose terms will control which users have access. For example, you could define a vocabulary called \'Privacy\' with terms \'Public\' and \'Private\'.') . '
';
+ $output .= '
' . t('Tell this module which vocabularies control privacy. (!link)', array('!link' => l(t('administer -> people -> access control by taxonomy'), 'admin/people/access/tac_lite'))) . '
';
+ $output .= '
' . t('Configure one or more schemes. simple site may need only one scheme which grants view permission. A more complex site might require additional schemes for update and delete. Each scheme associates roles and terms. Users will be granted priviliges based on their role and the terms with which nodes are tagged.') . '
' . t('Try disabling tac_lite.module, rebuilding permissions. With the module disabled, users should not have the privileges you are attempting to grant with this module.') . '
';
+ $output .= '
' . t('The devel_node_access.module (part of devel) helps to see exactly what Drupal\'s node_access table is doing.', array(
+ '!url' => 'http://drupal.org/project/devel',
+ )) . '
';
+ $output .= '
';
+ return $output;
+ break;
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function tac_lite_permission() {
+ return array(
+ 'administer tac_lite' => array(
+ 'title' => t('administer tac_lite'),
+ 'description' => t('TODO Add a description for \'administer tac_lite\''),
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function tac_lite_menu() {
+ global $user;
+ $items = array();
+
+ $items['admin/config/people/tac_lite'] = array(
+ 'title' => 'Access by Taxonomy',
+ 'description' => "taxonomy-based permissions by tac_lite",
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('tac_lite_admin_settings'),
+ 'weight' => 1, // after 'roles' tab
+ 'access arguments' => array('administer tac_lite'),
+ );
+
+ $items['admin/config/people/tac_lite/settings'] = array(
+ 'title' => 'Settings',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -1,
+ 'access arguments' => array('administer tac_lite'),
+ );
+
+ $schemes = variable_get('tac_lite_schemes', 1);
+ for ($i = 1; $i <= $schemes; $i++) {
+ $scheme = variable_get('tac_lite_config_scheme_'. $i, FALSE);
+ if ($scheme) {
+ $title = $scheme['name'];
+ } else {
+ $title = "Scheme $i";
+ }
+ $items['admin/config/people/tac_lite/scheme_' . $i] = array(
+ 'title' => $title,
+ 'page callback' => 'tac_lite_admin_settings_scheme',
+ 'page arguments' => array((string)$i),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => $i,
+ 'access arguments' => array('administer tac_lite'),
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Returns the settings form
+ */
+function tac_lite_admin_settings($form, &$form_state) {
+ $vocabularies = taxonomy_get_vocabularies();
+
+ if (!count($vocabularies)) {
+ $form['body'] = array(
+ '#type' => 'markup',
+ '#markup' => t('You must create a vocabulary before you can use tac_lite.',
+ array('!url' => url('admin/structure/taxonomy/add/vocabulary'))),
+ );
+ return $form;
+ }
+ else {
+ $options = array();
+ foreach ($vocabularies as $vid => $vocab) {
+ $options[$vid] = $vocab->name;
+ }
+
+ $form['tac_lite_categories'] = array(
+ '#type' => 'select',
+ '#title' => t('Vocabularies'),
+ '#default_value' => variable_get('tac_lite_categories', NULL),
+ '#options' => $options,
+ '#description' => t('Select one or more vocabularies to control privacy. Use caution with hierarchical (nested) taxonomies as visibility settings may cause problems on node edit forms. Do not select free tagging vocabularies, they are not supported.'),
+ '#multiple' => TRUE,
+ '#required' => TRUE,
+ );
+
+ $scheme_options = array();
+ // Currently only view, edit, delete permissions possible, so 7
+ // permutations will be more than enough.
+ for ($i = 1; $i < 8; $i++)
+ $scheme_options[$i] = $i;
+ $form['tac_lite_schemes'] = array(
+ '#type' => 'select',
+ '#title' => t('Number of Schemes'),
+ '#description' => t('Each scheme allows for a different set of permissions. For example, use scheme 1 for read-only permission; scheme 2 for read and update; scheme 3 for delete; etc. Additional schemes increase the size of your node_access table, so use no more than you need.'),
+ '#default_value' => variable_get('tac_lite_schemes', 1),
+ '#options' => $scheme_options,
+ '#required' => TRUE,
+ );
+
+ $form['tac_lite_rebuild'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Rebuild content permissions now'),
+ '#default_value' => FALSE, // default false because usually only needed after scheme has been changed.
+ '#description' => t('Do this once, after you have fully configured access by taxonomy.'),
+ '#weight' => 9,
+ );
+
+
+ $ret = system_settings_form($form);
+ // Special handling is required when this form is submitted.
+ $ret['#submit'][] = '_tac_lite_admin_settings_submit';
+ return $ret;
+ }
+}
+
+/**
+ * This form submit callback ensures that the form values are saved, and also
+ * the node access database table is rebuilt.
+ * 2008 : Modified by Paulo to be compliant with drupal 6
+ */
+function _tac_lite_admin_settings_submit($form, &$form_state) {
+ $rebuild = $form_state['values']['tac_lite_rebuild'];
+
+ // Rebuild the node_access table.
+ if ($rebuild) {
+ node_access_rebuild(TRUE);
+ }
+ else {
+ drupal_set_message(t('Do not forget to rebuild node access permissions after you have configured taxonomy-based access.', array(
+ '!url' => url('admin/reports/status/rebuild'),
+ )), 'warning');
+ }
+
+ // And rebuild menus, in case the number of schemes has changed.
+ menu_rebuild();
+
+ variable_del('tac_lite_rebuild'); // We don't need to store this as a system variable.
+}
+
+/**
+ * Menu callback to create a form for each scheme.
+ * @param $i
+ * The index of the scheme that we will be creating a form for. Passed in as a page argument from the menu.
+ */
+function tac_lite_admin_settings_scheme($i) {
+ return drupal_get_form('tac_lite_admin_scheme_form', $i);
+}
+
+/**
+ * helper function
+ */
+function _tac_lite_config($scheme) {
+ // different defaults for scheme 1
+ if ($scheme === 1) {
+ $config = variable_get('tac_lite_config_scheme_' . $scheme, array(
+ 'name' => t('read'),
+ 'perms' => array('grant_view'),
+ ));
+ }
+ else {
+ $config = variable_get('tac_lite_config_scheme_' . $scheme, array(
+ 'name' => NULL,
+ 'perms' => array(),
+ ));
+ }
+
+ // Merge defaults, for backward compatibility.
+ $config += array(
+ 'term_visibility' => (isset($config['perms']['grant_view']) && $config['perms']['grant_view']),
+ 'unpublished' => FALSE,
+ );
+
+ // For backward compatability, use naming convention for scheme 1
+ if ($scheme == 1) {
+ $config['realm'] = 'tac_lite';
+ }
+ else {
+ $config['realm'] = 'tac_lite_scheme_' . $scheme;
+ }
+
+ return $config;
+}
+
+/**
+ * Returns the form for role-based privileges.
+ */
+function tac_lite_admin_scheme_form($form, $form_state, $i) {
+ $vids = variable_get('tac_lite_categories', NULL);
+ $roles = user_roles();
+
+ $config = _tac_lite_config($i);
+ $form['#tac_lite_config'] = $config;
+ if (count($vids)) {
+ $form['tac_lite_config_scheme_' . $i] = array('#tree' => TRUE);
+ $form['tac_lite_config_scheme_' . $i]['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Scheme name'),
+ '#description' => t('A human-readable name for administrators to see. For example, \'read\' or \'read and write\'.'),
+ '#default_value' => $config['name'],
+ '#required' => TRUE,
+ );
+ // Currently, only view, update and delete are supported by node_access
+ $options = array(
+ 'grant_view' => 'view',
+ 'grant_update' => 'update',
+ 'grant_delete' => 'delete',
+ );
+ $form['tac_lite_config_scheme_' . $i]['perms'] = array(
+ '#type' => 'select',
+ '#title' => t('Permissions'),
+ '#multiple' => TRUE,
+ '#options' => $options,
+ '#default_value' => $config['perms'],
+ '#description' => t('Select which permissions are granted by this scheme. Note when granting update, it is best to enable visibility on all terms. Otherwise a user may unknowingly remove invisible terms while editing a node.'),
+ '#required' => FALSE,
+ );
+
+ $form['tac_lite_config_scheme_' . $i]['unpublished'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Apply to unpublished content'),
+ '#description' => t('If checked, permissions in this scheme will apply to unpublished content. If this scheme includes the view permission, then unpublished nodes will be visible to users whose roles would grant them access to the published node.'),
+ '#default_value' => $config['unpublished'],
+ );
+
+ $form['tac_lite_config_scheme_' . $i]['term_visibility'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Visibility'),
+ '#description' => t('If checked, this scheme determines whether a user can view terms. Note the view permission in the select field above refers to node visibility. This checkbox refers to term visibility, for example in a content edit form or tag cloud.'),
+ '#default_value' => $config['term_visibility'],
+ );
+
+ $form['helptext'] = array(
+ '#type' => 'markup',
+ '#markup' => t('To grant to an individual user, visit the access by taxonomy tab on the account edit page.'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $form['helptext2'] = array(
+ '#type' => 'markup',
+ '#markup' => t('To grant by role, select the terms below.'),
+ '#prefix' => '
',
+ '#markup' => t('First, select one or more vocabularies on the settings tab. Then, return to this page to complete configuration.', array('!url' => url('admin/config/people/tac_lite/settings'))));
+ return $form;
+ }
+}
+
+/**
+ * Submit function for admin settings form to rebuild the menu.
+ */
+function tac_lite_admin_scheme_form_submit($form, &$form_state) {
+ variable_set('menu_rebuild_needed', TRUE);
+
+ // Rebuild the node_access table.
+ if ($form_state['values']['tac_lite_rebuild']) {
+ node_access_rebuild(TRUE);
+ }
+ else {
+ drupal_set_message(t('Do not forget to rebuild node access permissions after you have configured taxonomy-based access.', array(
+ '!url' => url('admin/reports/status/rebuild'),
+ )), 'warning');
+ }
+ variable_del('tac_lite_rebuild'); // We don't need to store this as a system variable.
+}
+
+/**
+ * Implementation of hook_user_categories
+ *
+ * Creates the user edit category form for tac_lite's user-specific permissions under user/edit
+ */
+function tac_lite_user_categories() {
+ return array(
+ array(
+ 'name' => 'tac_lite',
+ 'title' => t('Access by taxonomy'),
+ 'weight' => 5,
+ 'access callback' => 'user_access',
+ 'access arguments' => array('administer users'),
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_form_alter().
+ *
+ * @param $form
+ * Nested array of form elements that comprise the form.
+ * @param $form_state
+ * A keyed array containing the current state of the form. The arguments that drupal_get_form() was originally called with are available in the array $form_state['build_info']['args'].
+ * @param $form_id
+ * String representing the name of the form itself. Typically this is the name of the function that generated the form.
+ *
+ */
+function tac_lite_form_alter(&$form, &$form_state, $form_id){
+ // Catch for the tac_lite category on the user edit form.
+ if ($form_id == 'user_profile_form') {
+ if ($form['#user_category'] == 'tac_lite') {
+ $vocabularies = taxonomy_get_vocabularies();
+ $vids = variable_get('tac_lite_categories', NULL);
+ if (count($vids)) {
+ for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) {
+ $config = _tac_lite_config($i);
+ if ($config['name']) {
+ $perms = $config['perms'];
+ if ($config['term_visibility']) {
+ $perms[] = t('term visibility');
+ }
+ $form['tac_lite'][$config['realm']] = array(
+ '#type' => 'fieldset',
+ '#title' => $config['name'],
+ '#description' => t('This scheme controls %perms.', array('%perms' => implode(' and ', $perms))),
+ '#tree' => TRUE,
+ );
+ // Create a form element for each vocabulary
+ foreach ($vids as $vid) {
+ $v = $vocabularies[$vid];
+ // TODO: Should we be looking in form_state also for the default value?
+ // (Might only be necessary if we are adding in custom validation?)
+ $default_values = array();
+ if (!empty($form['#user']->data[$config['realm']])) {
+ if (isset($form['#user']->data[$config['realm']][$vid])) {
+ $default_values = $form['#user']->data[$config['realm']][$vid];
+ }
+ }
+ $form['tac_lite'][$config['realm']][$vid] = _tac_lite_term_select($v, $default_values);
+ $form['tac_lite'][$config['realm']][$vid]['#description'] =
+ t('Grant permission to this user by selecting terms. Note that permissions are in addition to those granted based on user roles.');
+ }
+ }
+ }
+ $form['tac_lite'][0] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('You may grant this user access based on the schemes and terms below. These permissions are in addition to role based grants on scheme settings pages.',
+ array('!url' => url('admin/config/people/tac_lite/scheme_1'))) . "
\n",
+ '#weight' => -1,
+ );
+ }
+ else {
+ // TODO: Do we need to handle the situation where no vocabularies have been set up yet / none have been assigned to tac_lite?
+ }
+ return $form;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_user_presave().
+ *
+ * Move the tac_lite data into the data object
+ * @param $edit
+ * The array of form values submitted by the user.
+ * @param $account
+ * The user object on which the operation is performed.
+ * @param $category
+ * The active category of user information being edited.
+ */
+function tac_lite_user_presave(&$edit, $account, $category) {
+ // Only proceed if we are in the tac_lite category.
+ if ($category == 'tac_lite'){
+ // Go through each scheme and copy the form value into the data element
+ for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) {
+ $config = _tac_lite_config($i);
+ if ($config['name']) {
+ $edit['data'][$config['realm']] = $edit[$config['realm']];
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_node_access_records().
+ *
+ * We are given a node and we return records for the node_access table. In
+ * our case, we inpect the node's taxonomy and grant permissions based on the
+ * terms.
+ */
+function tac_lite_node_access_records($node) {
+ // Get the tids we care about that are assigned to this node
+ $tids = _tac_lite_get_terms($node);
+
+ if (!count($tids)) {
+ // no relevant terms found.
+
+ // in drupal 4-7 we had to write a row into the database. In drupal 5 and later, it should be safe to do nothing.
+ }
+ else {
+ // if we're here, the node has terms associated with it which restrict
+ // access to the node.
+ $grants = array();
+ for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) {
+ $config = _tac_lite_config($i);
+ // Only apply grants to published nodes, or unpublished nodes if requested in the scheme
+ if ($node->status || $config['unpublished']) {
+ foreach ($tids as $tid) {
+ $grant = array(
+ 'realm' => $config['realm'],
+ 'gid' => $tid, // use term id as grant id
+ 'grant_view' => 0,
+ 'grant_update' => 0,
+ 'grant_delete' => 0,
+ 'priority' => 0,
+ );
+ foreach ($config['perms'] as $perm) {
+ $grant[$perm] = TRUE;
+ }
+ $grants[] = $grant;
+ }
+ }
+ }
+ return $grants;
+ }
+}
+
+/**
+ * Gets terms from a node that belong to vocabularies selected for use by tac_lite
+ *
+ * @param $node
+ * A node object
+ * @return
+ * An array of term ids
+ */
+function _tac_lite_get_terms($node) {
+ $tids = array();
+
+ // Get the vids that tac_lite cares about.
+ $vids = variable_get('tac_lite_categories', NULL);
+ if ($vids) {
+ // Load all terms found in term reference fields.
+ // This logic should work for all nodes (published or not).
+ $terms_by_vid = tac_lite_node_get_terms($node);
+ if (!empty($terms_by_vid)) {
+ foreach ($vids as $vid) {
+ if (!empty($terms_by_vid[$vid])) {
+ foreach ($terms_by_vid[$vid] as $tid => $term) {
+ $tids[$tid] = $tid;
+ }
+ }
+ }
+ }
+
+ // The logic above should have all terms already, but just in case we use
+ // the "original" logic below. The taxonomy module stopped writing to the
+ // taxonomy_index for unpublished nodes, so this works only for published
+ // nodes.
+ $query = db_select('taxonomy_index', 'r');
+ $t_alias = $query->join('taxonomy_term_data', 't', 'r.tid = t.tid');
+ $v_alias = $query->join('taxonomy_vocabulary', 'v', 't.vid = v.vid');
+ $query->fields( $t_alias );
+ $query->condition("r.nid", $node->nid);
+ $query->condition("t.vid", $vids, 'IN');
+ $result = $query->execute();
+ foreach ($result as $term) {
+ if (empty($tids[$term->tid])) {
+ watchdog('tac_lite', 'Unexpected term id %tid associated with !node. Please report this to !url.', array(
+ '%tid' => $term->tid,
+ '!node' => l($node->title, 'node/' . $node->nid),
+ '!url' => 'https://drupal.org/node/1918272',
+ ), WATCHDOG_DEBUG);
+ }
+ $tids[$term->tid] = $term->tid;
+ }
+ }
+ elseif (user_access('administer tac_lite')) {
+ drupal_set_message(t('tac_lite.module enabled, but not configured. No tac_lite terms associated with %title.', array(
+ '!admin_url' => url('admin/config/people/tac_lite'),
+ '%title' => $node->title,
+ )));
+ }
+
+ return $tids;
+}
+
+/**
+ * In Drupal 6.x, there was taxonomy_node_get_terms(). Drupal 7.x should
+ * provide the same feature, but doesn't. Here is our workaround, based on
+ * https://drupal.org/comment/5573176#comment-5573176.
+ *
+ * We organize our data structure by vid and tid.
+ */
+function tac_lite_node_get_terms($node) {
+ $terms = &drupal_static(__FUNCTION__);
+
+ if (!isset($terms[$node->nid])) {
+ // Get tids from all taxonomy_term_reference fields.
+ $fields = field_info_fields();
+ foreach ($fields as $field_name => $field) {
+ // Our goal is to get all terms, regardless of language, associated with the node. Does the code below do that?
+ if ($field['type'] == 'taxonomy_term_reference' && field_info_instance('node', $field_name, $node->type)) {
+ if (($items = field_get_items('node', $node, $field_name)) && is_array($items)) {
+ foreach ($items as $item) {
+ // Sometimes $item contains only tid, sometimes entire term. Thanks Drupal for remaining mysterious!
+ // We need to term to determine the vocabulary id.
+ if (!empty($item['taxonomy_term'])) {
+ $term = $item['taxonomy_term'];
+ }
+ else {
+ $term = taxonomy_term_load($item['tid']);
+ }
+ if ($term) {
+ $terms[$node->nid][$term->vid][$term->tid] = $term;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return isset($terms[$node->nid]) ? $terms[$node->nid] : FALSE;
+}
+
+/**
+ * Helper function to build a taxonomy term select element for a form.
+ *
+ * @param $v
+ * A vocabulary object containing a vid and name.
+ * @param $default_values
+ * An array of values to use for the default_value argument for this form element.
+ */
+function _tac_lite_term_select($v, $default_values = array()) {
+ $tree = taxonomy_get_tree($v->vid);
+ $options = array(0 => '<' . t('none') . '>');
+ if ($tree) {
+ foreach ($tree as $term) {
+ $choice = new stdClass();
+ $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
+ $options[] = $choice;
+ }
+ }
+ $field_array = array(
+ '#type' => 'select',
+ '#title' => $v->name,
+ '#default_value' => $default_values,
+ '#options' => $options,
+ '#multiple' => TRUE,
+ '#description' => $v->description,
+ );
+ return $field_array;
+}
+
+/**
+ * Return the term ids of terms this user is allowed to access.
+ *
+ * Users are granted access to terms either because of who they are,
+ * or because of the roles they have.
+ */
+function _tac_lite_user_tids($account, $scheme) {
+ // grant id 0 is reserved for nodes which were not given a grant id when they were created. By adding 0 to the grant id, we let the user view those nodes.
+ $grants = array(0);
+ $config = _tac_lite_config($scheme);
+ $realm = $config['realm'];
+ if (isset($account->data[$realm]) && count($account->data[$realm])) {
+ // $account->$realm is array. Keys are vids, values are array of tids within that vocabulary, to which the user has access
+ foreach ($account->data[$realm] as $tids) {
+ if (count($tids)) {
+ $grants = array_merge($grants, $tids);
+ }
+ }
+ }
+
+ // add per-role grants in addition to per-user grants
+ $defaults = variable_get('tac_lite_grants_scheme_' . $scheme, array());
+ foreach ($account->roles as $rid => $role_name) {
+ if (isset($defaults[$rid]) && count($defaults[$rid])) {
+ foreach ($defaults[$rid] as $tids) {
+ if (count($tids)) {
+ $grants = array_merge($grants, $tids);
+ }
+ }
+ }
+ }
+
+ // Because of some flakyness in the form API and the form we insert under
+ // user settings, we may have a bogus entry with vid set
+ // to ''. Here we make sure not to return that.
+ unset($grants['']);
+
+ return $grants;
+}
+
+/**
+ * Implementation of hook_node_grants().
+ *
+ * Returns any grants which may give the user permission to perform the
+ * requested op.
+ */
+function tac_lite_node_grants($account, $op) {
+ $grants = array();
+ for ($i = 1; $i <= variable_get('tac_lite_schemes', 1); $i++) {
+ $config = _tac_lite_config($i);
+ if (in_array('grant_' . $op, $config['perms'])) {
+ $grants[$config['realm']] = _tac_lite_user_tids($account, $i);
+ }
+ }
+ if (count($grants)) {
+ return $grants;
+ }
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ *
+ * Acts on queries that list terms (generally these should be tagged with 'term_access')
+ * to remove any terms that this user should not be able to see.
+ */
+function tac_lite_query_term_access_alter(QueryAlterableInterface $query) {
+ global $user;
+
+ // If this user has administer rights, don't filter
+ if (user_access('administer tac_lite')) {
+ return;
+ }
+
+ // Get our vocabularies and schemes from variables. Return if we have none.
+ $vids = variable_get('tac_lite_categories', NULL);
+ $schemes = variable_get('tac_lite_schemes', 1);
+ if (!$vids || !count($vids) || !$schemes) {
+ return;
+ }
+
+ // the terms this user is allowed to see
+ $term_visibility = FALSE;
+ $tids = array();
+ for ($i = 1; $i <= $schemes; $i++) {
+ $config = _tac_lite_config($i);
+ if ($config['term_visibility']) {
+ $tids = array_merge($tids, _tac_lite_user_tids($user, $i));
+ $term_visibility = TRUE;
+ }
+ }
+
+ if ($term_visibility) {
+ // HELP: What is the proper way to find the alias of the primary table here?
+ $primary_table = '';
+ $t = $query->getTables();
+ foreach($t as $key => $info) {
+ if (!$info['join type']) {
+ $primary_table = $info['alias'];
+ }
+ }
+
+ // Prevent query from finding terms the current user does not have permission to see.
+ $query->leftJoin('taxonomy_term_data', 'tac_td', $primary_table . '.tid = tac_td.tid');
+ $or = db_or();
+ $or->condition($primary_table . '.tid', $tids, 'IN');
+ $or->condition('tac_td.vid', $vids, 'NOT IN');
+ $query->condition($or);
+ }
+}
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite_create.info b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite_create.info
new file mode 100644
index 00000000..aec4ac4a
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite_create.info
@@ -0,0 +1,12 @@
+name = Taxonomy Access Control Lite Create
+description = Hide taxonomy terms on node add/edit forms.
+dependencies[] = tac_lite
+core = 7.x
+package = Access control
+
+; Information added by Drupal.org packaging script on 2015-10-11
+version = "7.x-1.2+2-dev"
+core = "7.x"
+project = "tac_lite"
+datestamp = "1444524081"
+
diff --git a/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite_create.module b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite_create.module
new file mode 100644
index 00000000..e82a650a
--- /dev/null
+++ b/sites/all/modules/contrib/taxonomy/tac_lite/tac_lite_create.module
@@ -0,0 +1,128 @@
+ $value){
+ // First check for taxonomy_term_reference fields.
+ if ($value['type'] == 'taxonomy_term_reference') {
+ // Then check to see if they are associated with this node type (entity bundle).
+ if (isset($value['bundles']['node'])) {
+ if (in_array($form['#bundle'], $value['bundles']['node'])) {
+ // Add an entry to our term_fields in the form of field name => vocabulary machine name
+ $term_fields[$key] = $value['settings']['allowed_values'][0]['vocabulary'];
+ }
+ }
+ }
+ }
+
+ // Now that we have the names of the fields, go through each one in the form.
+ foreach($term_fields as $field_name => $vocab_name) {
+ // Get the language key so we can find the correct element in the field.
+ $field_language = $form[$field_name]['#language'];
+
+ // Avoid PHP errors
+ if (empty($form[$field_name]) || empty($form[$field_name][$field_language])) {
+ continue;
+ }
+
+ // Skip auto complete fields as they are not supported.
+ if ($form[$field_name][$field_language]['#type'] != 'select' && $form[$field_name][$field_language]['#type'] != 'radios'){
+ continue;
+ }
+
+ // Get the vocabulary info so we can get the vid.
+ $v = taxonomy_vocabulary_machine_name_load($vocab_name);
+
+ // We only want to act on this field if it is tied to a vocabulary we are set to control.
+ if(!in_array($v->vid, $vids)) {
+ continue;
+ }
+
+ // Go through each option for this field.
+ foreach ($form[$field_name][$field_language]['#options'] as $term_id => $term_name) {
+ // Skip the "" option (or anything like it).
+ if (!is_numeric($term_id)) {
+ continue;
+ }
+
+ // Is this option not in the list of terms this user can create with?
+ if (!in_array($term_id, $tids)) {
+ // The term is not the default value, and user is not allowed to create with it.
+ // HELP: What if it IS the default value? Do we not unset it?
+ if ($term_id != $form[$field_name][$field_language]['#default_value']) {
+ // Unset the option to it doesn't appear.
+ // TODO: It would be a nice feature to have tids that are already selected.
+ // go into a value element so we could merge then when saving the node.
+ unset($form[$field_name][$field_language]['#options'][$term_id]);
+ }
+ }
+ }
+
+ // If there are no options left, but this field is required then throw a 404.
+ if (count($form[$field_name][$field_language]['#options']) == 0 && !empty($form[$field_name]['#required'])) {
+ drupal_set_message(t('You have no permissions to add content to the required %name vocabulary. Please contact the site administrator if you believe you should have permission to add content.', array('%name' => $form[$field_name]['#title'])));
+ drupal_access_denied();
+ exit();
+ }
+
+ // Don't show if the only option left is .
+ if (empty($form[$field_name][$field_language]['#options']) ||
+ (count($form[$field_name][$field_language]['#options']) == 1 && isset($form[$field_name][$field_language]['#options']['_none']))) {
+ $form[$field_name]['#access'] = FALSE;
+ }
+ // HELP: Is size deprecated?
+ /*
+ if (isset($form[$field_name]['#size']) && $form[$field_name]['#size'] > count($form['taxonomy'][$field_language]['#options'])) {
+ $form[$field_name]['#size'] = count($form[$field_name][$field_language]['#options']);
+ }
+ */
+ }
+ }
+ elseif ($form_id == 'tac_lite_admin_scheme_form') {
+ $config = $form['#tac_lite_config'];
+ $scheme = $form_state['build_info']['args'][0];
+ if (!empty($form['tac_lite_config_scheme_' . $scheme])) {
+ $form['tac_lite_config_scheme_' . $scheme]['tac_lite_create'] = array(
+ '#type' => 'checkbox',
+ '#title' => 'Visibility on create and edit forms',
+ '#default_value' => isset($config['tac_lite_create']) ? $config['tac_lite_create'] : FALSE,
+ '#description' => t('Show terms when creating content. This does not control which users can create a given content type. This does control which terms appear on the node edit forms. Note that schemes granting update permission (above) imply visibility on forms as well.'),
+ );
+ }
+ }
+}
diff --git a/sites/all/modules/examples/LICENSE.txt b/sites/all/modules/examples/LICENSE.txt
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/sites/all/modules/examples/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/examples/README.txt b/sites/all/modules/examples/README.txt
new file mode 100644
index 00000000..5904fefe
--- /dev/null
+++ b/sites/all/modules/examples/README.txt
@@ -0,0 +1,62 @@
+Examples for Developers
+=======================
+
+http://drupal.org/project/examples
+
+What Is This?
+-------------
+
+This set of modules is intended to provide working examples of Drupal's
+features and APIs. The modules strive to be simple, well documented and
+modification friendly, in order to help developers quickly learn their inner
+workings.
+
+These examples are meant to teach you about code-level development for Drupal
+7. Some solutions might be better served using a contributed module, so that
+you don't end up having to re-invent the wheel in PHP.
+
+
+How To Use The Examples
+-----------------------
+
+There are three main ways to interact with the examples in this project:
+
+1. Enable the modules and use them within Drupal. Not all modules will have
+obvious things to see within Drupal. For instance, while the Page and Form API
+examples will show you forms, the Database API example will not show you much
+within Drupal itself.
+
+2. Read the code. Much effort has gone into making the example code readable,
+not only in terms of the code itself, but also the extensive inline comments
+and documentation blocks.
+
+3. Browse the code and documentation on the web. There are two main places to
+do this:
+
+* https://api.drupal.org/api/examples is the main API site for all of Drupal.
+It has all manner of cross-linked references between the example code and the
+APIs being demonstrated.
+
+* http://drupalcode.org/project/examples.git allows you to browse the git
+repository for the Examples project.
+
+
+How To Install The Modules
+--------------------------
+
+1. Install Examples for Developers (unpacking it to your Drupal
+/sites/all/modules directory if you're installing by hand, for example).
+
+2. Enable any Example modules in Admin menu > Site building > Modules.
+
+3. Rebuild access permissions if you are prompted to.
+
+4. Profit! The examples will appear in your Navigation menu (on the left
+sidebar by default; you'll need to reenable it if you removed it).
+
+Now you can read the code and its comments and see the result, experiment with
+it, and hopefully quickly grasp how things work.
+
+If you find a problem, incorrect comment, obsolete or improper code or such,
+please search for an issue about it at http://drupal.org/project/issues/examples
+If there isn't already an issue for it, please create a new one.
diff --git a/sites/all/modules/examples/action_example/action_example.info b/sites/all/modules/examples/action_example/action_example.info
new file mode 100644
index 00000000..8d895183
--- /dev/null
+++ b/sites/all/modules/examples/action_example/action_example.info
@@ -0,0 +1,19 @@
+name = Action example
+description = Demonstrates providing actions that can be associated to triggers.
+package = Example modules
+core = 7.x
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:trigger
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = action_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/action_example/action_example.module b/sites/all/modules/examples/action_example/action_example.module
new file mode 100644
index 00000000..993106e0
--- /dev/null
+++ b/sites/all/modules/examples/action_example/action_example.module
@@ -0,0 +1,375 @@
+ array(
+ 'label' => t('Action Example: A basic example action that does nothing'),
+ 'type' => 'system',
+ 'configurable' => FALSE,
+ 'triggers' => array('any'),
+ ),
+ 'action_example_unblock_user_action' => array(
+ 'label' => t('Action Example: Unblock a user'),
+ 'type' => 'user',
+ 'configurable' => FALSE,
+ 'triggers' => array('any'),
+ ),
+ 'action_example_node_sticky_action' => array(
+ 'type' => 'node',
+ 'label' => t('Action Example: Promote to frontpage and sticky on top any content created by :'),
+ 'configurable' => TRUE,
+ 'behavior' => array('changes_property'),
+ 'triggers' => array('node_presave', 'node_insert', 'node_update'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a menu entry which explains what the module does.
+ */
+function action_example_menu() {
+ $items['examples/action_example'] = array(
+ 'title' => 'Action Example',
+ 'description' => 'Provides a basic information page.',
+ 'page callback' => '_action_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+
+/**
+ * A simple page to explain to the developer what to do.
+ */
+function _action_example_page() {
+ return t("The Action Example provides three example actions which can be configured on the Actions configuration page and assigned to triggers on the Triggers configuration page.", array('@actions_url' => url('admin/config/system/actions'), '@triggers_url' => url('admin/structure/trigger/node')));
+}
+
+/**
+ * Action function for action_example_basic_action.
+ *
+ * This action is not expecting any type of entity object, and can be used with
+ * any trigger type or any event.
+ *
+ * @param object $entity
+ * An optional entity object.
+ * @param array $context
+ * Array with parameters for this action: depends on the trigger.
+ *
+ * @see action_example_action_info()
+ */
+function action_example_basic_action(&$entity, $context = array()) {
+ // In this case we are ignoring the entity and the context. This case of
+ // action is useful when your action does not depend on the context, and
+ // the function must do something regardless the scope of the trigger.
+ // Simply announces that the action was executed using a message.
+ drupal_set_message(t('action_example_basic_action fired'));
+ watchdog('action_example', 'action_example_basic_action fired.');
+}
+
+/**
+ * Action function for action_example_unblock_user_action.
+ *
+ * This action is expecting an entity object user, node or comment. If none of
+ * the above is provided (because it was not called from an user/node/comment
+ * trigger event), then the action will be taken on the current logged in user.
+ *
+ * Unblock an user. This action can be fired from different trigger types:
+ * - User trigger: this user will be unblocked.
+ * - Node/Comment trigger: the author of the node or comment will be unblocked.
+ * - Other: (including system or custom defined types), current user will be
+ * unblocked. (Yes, this seems like an incomprehensible use-case.)
+ *
+ * @param object $entity
+ * An optional user object (could be a user, or an author if context is
+ * node or comment)
+ * @param array $context
+ * Array with parameters for this action: depends on the trigger. The context
+ * is not used in this example.
+ */
+function action_example_unblock_user_action(&$entity, $context = array()) {
+
+ // First we check that entity is a user object. If this is the case, then this
+ // is a user-type trigger.
+ if (isset($entity->uid)) {
+ $uid = $entity->uid;
+ }
+ elseif (isset($context['uid'])) {
+ $uid = $context['uid'];
+ }
+ // If neither of those are valid, then block the current user.
+ else {
+ $uid = $GLOBALS['user']->uid;
+ }
+ $account = user_load($uid);
+ $account = user_save($account, array('status' => 1));
+ watchdog('action_example', 'Unblocked user %name.', array('%name' => $account->name));
+ drupal_set_message(t('Unblocked user %name', array('%name' => $account->name)));
+}
+
+/**
+ * Form function for action_example_node_sticky_action.
+ *
+ * Since we defined action_example_node_sticky_action as 'configurable' => TRUE,
+ * this action requires a configuration form to create/configure the action.
+ * In this circumstance, Drupal will attempt to call a function named by
+ * combining the action name (action_example_node_sticky_action) and _form, in
+ * this case yielding action_example_node_sticky_action_form.
+ *
+ * In Drupal, actions requiring creation and configuration are called 'advanced
+ * actions', because they must be customized to define their functionality.
+ *
+ * The 'action_example_node_sticky_action' allows creating rules to promote and
+ * set sticky content created by selected users on certain events. A form is
+ * used to configure which user is affected by this action, and this form
+ * includes the standard _validate and _submit hooks.
+ */
+
+
+/**
+ * Generates settings form for action_example_node_sticky_action().
+ *
+ * @param array $context
+ * An array of options of this action (in case it is being edited)
+ *
+ * @return array
+ * Settings form as Form API array.
+ *
+ * @see action_example_action_info()
+ */
+function action_example_node_sticky_action_form($context) {
+ /*
+ * We return a configuration form to set the requirements that will
+ * match this action before being executed. This is a regular Drupal form and
+ * may include any type of information you want, but all the fields of the
+ * form will be saved into the $context variable.
+ *
+ * In this case we are promoting all content types submitted by this user, but
+ * it is possible to extend these conditions providing more options in the
+ * settings form.
+ */
+ $form['author'] = array(
+ '#title' => t('Author name'),
+ '#type' => 'textfield',
+ '#description' => t('Any content created, presaved or updated by this user will be promoted to front page and set as sticky.'),
+ '#default_value' => isset($context['author']) ? $context['author'] : '',
+ );
+ // Verify user permissions and provide an easier way to fill this field.
+ if (user_access('access user profiles')) {
+ $form['author']['#autocomplete_path'] = 'user/autocomplete';
+ }
+ // No more options, return the form.
+ return $form;
+}
+
+/**
+ * Validates settings form for action_example_node_sticky_action().
+ *
+ * Verifies that user exists before continuing.
+ */
+function action_example_node_sticky_action_validate($form, $form_state) {
+ if (!$account = user_load_by_name($form_state['values']['author'])) {
+ form_set_error('author', t('Please, provide a valid username'));
+ }
+}
+
+/**
+ * Submit handler for action_example_node_sticky_action.
+ *
+ * Returns an associative array of values which will be available in the
+ * $context when an action is executed.
+ */
+function action_example_node_sticky_action_submit($form, $form_state) {
+ return array('author' => $form_state['values']['author']);
+}
+
+/**
+ * Action function for action_example_node_sticky_action.
+ *
+ * Promote and set sticky flag. This is the special action that has been
+ * customized using the configuration form, validated with the validation
+ * function, and submitted with the submit function.
+ *
+ * @param object $node
+ * A node object provided by the associated trigger.
+ * @param array $context
+ * Array with the following elements:
+ * - 'author': username of the author's content this function will promote and
+ * set as sticky.
+ */
+function action_example_node_sticky_action($node, $context) {
+ if (function_exists('dsm')) {
+ dsm($node, 'action_example_node_sticky_action is firing. Here is the $node');
+ dsm($context, 'action_example_node_sticky_action is firing. Here is the $context');
+ }
+ // Get the user configured for this special action.
+ $account = user_load_by_name($context['author']);
+ // Is the node created by this user? then promote and set as sticky.
+ if ($account->uid == $node->uid) {
+ $node->promote = NODE_PROMOTED;
+ $node->sticky = NODE_STICKY;
+ watchdog('action',
+ 'Set @type %title to sticky and promoted by special action for user %username.',
+ array(
+ '@type' => node_type_get_name($node),
+ '%title' => $node->title,
+ '%username' => $account->name,
+ )
+ );
+ drupal_set_message(
+ t('Set @type %title to sticky and promoted by special action for user %username.',
+ array(
+ '@type' => node_type_get_name($node),
+ '%title' => $node->title,
+ '%username' => $account->name,
+ )
+ )
+ );
+ }
+}
+/**
+ * @} End of "defgroup action_example".
+ */
diff --git a/sites/all/modules/examples/action_example/action_example.test b/sites/all/modules/examples/action_example/action_example.test
new file mode 100644
index 00000000..c6686ab9
--- /dev/null
+++ b/sites/all/modules/examples/action_example/action_example.test
@@ -0,0 +1,111 @@
+ 'Action example',
+ 'description' => 'Perform various tests on action_example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('trigger', 'action_example');
+ }
+
+ /**
+ * Test Action Example.
+ *
+ * 1. action_example_basic_action: Configure a action_example_basic_action to
+ * happen when user logs in.
+ * 2. action_example_unblock_user_action: When a user's profile is being
+ * viewed, unblock that user.
+ * 3. action_example_node_sticky_action: Create a user, configure that user
+ * to always be stickied using advanced configuration. Have the user
+ * create content; verify that it gets stickied.
+ */
+ public function testActionExample() {
+ // Create an administrative user.
+ $admin_user = $this->drupalCreateUser(
+ array(
+ 'administer actions',
+ 'access comments',
+ 'access content',
+ 'post comments',
+ 'skip comment approval',
+ 'create article content',
+ 'access user profiles',
+ 'administer users',
+ )
+ );
+ $this->drupalLogin($admin_user);
+
+ // 1. Assign basic action; then logout and login user and see if it puts
+ // the message on the screen.
+ $hash = drupal_hash_base64('action_example_basic_action');
+ $edit = array('aid' => $hash);
+ $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-login-assign-form');
+
+ $this->drupalLogout();
+ $this->drupalLogin($admin_user);
+ $this->assertText(t('action_example_basic_action fired'));
+
+ // 2. Unblock: When a user's profile is being viewed, unblock.
+ $normal_user = $this->drupalCreateUser();
+ // Create blocked user.
+ user_save($normal_user, array('status' => 0));
+ $normal_user = user_load($normal_user->uid, TRUE);
+ $this->assertFalse($normal_user->status, 'Normal user status has been set to blocked');
+
+ $hash = drupal_hash_base64('action_example_unblock_user_action');
+ $edit = array('aid' => $hash);
+ $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-view-assign-form');
+
+ $this->drupalGet("user/$normal_user->uid");
+ $normal_user = user_load($normal_user->uid, TRUE);
+ $this->assertTrue($normal_user->status, 'Normal user status has been set to unblocked');
+ $this->assertRaw(t('Unblocked user %name', array('%name' => $normal_user->name)));
+
+ // 3. Create a user whose posts are always to be stickied.
+ $sticky_user = $this->drupalCreateUser(
+ array(
+ 'access comments',
+ 'access content',
+ 'post comments',
+ 'skip comment approval',
+ 'create article content',
+ )
+ );
+
+ $action_label = $this->randomName();
+ $edit = array(
+ 'actions_label' => $action_label,
+ 'author' => $sticky_user->name,
+ );
+ $aid = $this->configureAdvancedAction('action_example_node_sticky_action', $edit);
+ $edit = array('aid' => drupal_hash_base64($aid));
+ $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-insert-assign-form');
+ // Now create a node and verify that it gets stickied.
+ $this->drupalLogout();
+ $this->drupalLogin($sticky_user);
+ $node = $this->drupalCreateNode();
+ $this->assertTrue($node->sticky, 'Node was set to sticky on creation');
+ }
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example.css b/sites/all/modules/examples/ajax_example/ajax_example.css
new file mode 100644
index 00000000..e1cdc694
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example.css
@@ -0,0 +1,17 @@
+/*
+ * @file
+ * CSS for ajax_example.
+ *
+ * See @link ajax_example_dependent_dropdown_degrades @endlink for
+ * details on what this file does. It is not used in any other example.
+ */
+
+/* Hides the next button when not degrading to non-javascript browser */
+html.js .next-button {
+ display: none;
+}
+
+/* Makes the next/choose button align to the right of the select control */
+.form-item-dropdown-first, .form-item-question-type-select {
+ display: inline-block;
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example.info b/sites/all/modules/examples/ajax_example/ajax_example.info
new file mode 100644
index 00000000..a42631b4
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example.info
@@ -0,0 +1,12 @@
+name = AJAX Example
+description = An example module showing how to use Drupal AJAX forms
+package = Example modules
+core = 7.x
+files[] = ajax_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/ajax_example/ajax_example.install b/sites/all/modules/examples/ajax_example/ajax_example.install
new file mode 100644
index 00000000..6d3d2130
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example.install
@@ -0,0 +1,56 @@
+ 'Stores example settings for nodes.',
+ 'fields' => array(
+ 'nid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {node}.nid to store settings.',
+ ),
+ 'example_1' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Node Form Example 1 checkbox',
+ ),
+ 'example_2' => array(
+ 'type' => 'varchar',
+ 'length' => 256,
+ 'not null' => FALSE,
+ 'default' => '',
+ 'description' => 'Node Form Example 2 textfield',
+ ),
+ ),
+ 'primary key' => array('nid'),
+ 'foreign keys' => array(
+ 'dnv_node' => array(
+ 'table' => 'node',
+ 'columns' => array('nid' => 'nid'),
+ ),
+ ),
+ );
+ return $schema;
+}
+
+/**
+ * Add the new ajax_example_node_form_alter table.
+ */
+function ajax_example_update_7100() {
+ if (!db_table_exists('ajax_example_node_form_alter')) {
+ $schema = ajax_example_schema();
+ db_create_table('ajax_example_node_form_alter', $schema['ajax_example_node_form_alter']);
+ return st('Created table ajax_example_node_form_alter');
+ }
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example.js b/sites/all/modules/examples/ajax_example/ajax_example.js
new file mode 100644
index 00000000..2d06038c
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example.js
@@ -0,0 +1,29 @@
+/*
+ * @file
+ * JavaScript for ajax_example.
+ *
+ * See @link ajax_example_dependent_dropdown_degrades @endlink for
+ * details on what this file does. It is not used in any other example.
+ */
+
+(function($) {
+
+ // Re-enable form elements that are disabled for non-ajax situations.
+ Drupal.behaviors.enableFormItemsForAjaxForms = {
+ attach: function() {
+ // If ajax is enabled.
+ if (Drupal.ajax) {
+ $('.enabled-for-ajax').removeAttr('disabled');
+ }
+
+ // Below is only for the demo case of showing with js turned off.
+ // It overrides the behavior of the CSS that would normally turn off
+ // the 'ok' button when JS is enabled. Here, for demonstration purposes,
+ // we have AJAX disabled but JS turned on, so use this to simulate.
+ if (!Drupal.ajax) {
+ $('html.js .next-button').show();
+ }
+ }
+ };
+
+})(jQuery);
diff --git a/sites/all/modules/examples/ajax_example/ajax_example.module b/sites/all/modules/examples/ajax_example/ajax_example.module
new file mode 100644
index 00000000..4bfa26c2
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example.module
@@ -0,0 +1,693 @@
+ 'AJAX Example',
+ 'page callback' => 'ajax_example_intro',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+
+ // Change the description of a form element.
+ $items['examples/ajax_example/simplest'] = array(
+ 'title' => 'Simplest AJAX Example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_simplest'),
+ 'access callback' => TRUE,
+ 'weight' => 0,
+ );
+ // Generate a changing number of checkboxes.
+ $items['examples/ajax_example/autocheckboxes'] = array(
+ 'title' => 'Generate checkboxes',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_autocheckboxes'),
+ 'access callback' => TRUE,
+ 'weight' => 1,
+ );
+ // Generate different textfields based on form state.
+ $items['examples/ajax_example/autotextfields'] = array(
+ 'title' => 'Generate textfields',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_autotextfields'),
+ 'access callback' => TRUE,
+ 'weight' => 2,
+ );
+
+ // Submit a form without a page reload.
+ $items['examples/ajax_example/submit_driven_ajax'] = array(
+ 'title' => 'Submit-driven AJAX',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_submit_driven_ajax'),
+ 'access callback' => TRUE,
+ 'weight' => 3,
+ );
+
+ // Repopulate a dropdown based on form state.
+ $items['examples/ajax_example/dependent_dropdown'] = array(
+ 'title' => 'Dependent dropdown',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dependent_dropdown'),
+ 'access callback' => TRUE,
+ 'weight' => 4,
+ );
+ // Repopulate a dropdown, but this time with graceful degredation.
+ // See ajax_example_graceful_degradation.inc.
+ $items['examples/ajax_example/dependent_dropdown_degrades'] = array(
+ 'title' => 'Dependent dropdown (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dependent_dropdown_degrades'),
+ 'access callback' => TRUE,
+ 'weight' => 5,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/dependent_dropdown_degrades_no_js'] = array(
+ 'title' => 'Dependent dropdown with javascript off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dependent_dropdown_degrades', TRUE),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 5,
+ );
+
+ // Populate a form section based on input in another element.
+ $items['examples/ajax_example/dynamic_sections'] = array(
+ 'title' => 'Dynamic Sections (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dynamic_sections'),
+ 'access callback' => TRUE,
+ 'weight' => 6,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/dynamic_sections_no_js'] = array(
+ 'title' => 'Dynamic Sections w/JS turned off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_dynamic_sections', TRUE),
+ 'access callback' => TRUE,
+ 'weight' => 6,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ );
+
+ // A classic multi-step wizard, but with no page reloads.
+ // See ajax_example_graceful_degradation.inc.
+ $items['examples/ajax_example/wizard'] = array(
+ 'title' => 'Wizard (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_wizard'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 7,
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/wizard_no_js'] = array(
+ 'title' => 'Wizard w/JS turned off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_wizard', TRUE),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 7,
+ );
+
+ // Add-more button that creates additional form elements.
+ // See ajax_example_graceful_degradation.inc.
+ $items['examples/ajax_example/add_more'] = array(
+ 'title' => 'Add-more button (with graceful degradation)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_add_more'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 8,
+ );
+ // The above example as it appears to users with no javascript.
+ $items['examples/ajax_example/add_more_no_js'] = array(
+ 'title' => 'Add-more button w/JS turned off',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_add_more', TRUE),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_graceful_degradation.inc',
+ 'weight' => 8,
+ );
+
+ // Use the AJAX framework outside the context of a form using the use-ajax
+ // class. See ajax_example_misc.inc.
+ $items['examples/ajax_example/ajax_link'] = array(
+ 'title' => 'Ajax Link ("use-ajax" class)',
+ 'page callback' => 'ajax_example_render_link',
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_misc.inc',
+ 'weight' => 9,
+ );
+ // Use the AJAX framework outside the context of a form using a renderable
+ // array of type link with the #ajax property. See ajax_example_misc.inc.
+ $items['examples/ajax_example/ajax_link_renderable'] = array(
+ 'title' => 'Ajax Link (Renderable Array)',
+ 'page callback' => 'ajax_example_render_link_ra',
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_misc.inc',
+ 'weight' => 9,
+ );
+ // A menu callback is required when using ajax outside of the Form API.
+ $items['ajax_link_callback'] = array(
+ 'page callback' => 'ajax_link_response',
+ 'access callback' => 'user_access',
+ 'access arguments' => array('access content'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'ajax_example_misc.inc',
+ );
+
+ // Use AJAX framework commands outside of the #ajax form property.
+ // See ajax_example_advanced.inc.
+ $items['examples/ajax_example/advanced_commands'] = array(
+ 'title' => 'AJAX framework commands',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_advanced_commands'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_advanced.inc',
+ 'weight' => 100,
+ );
+
+ // Autocomplete examples.
+ $items['examples/ajax_example/simple_autocomplete'] = array(
+ 'title' => 'Autocomplete (simple)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_simple_autocomplete'),
+ 'access arguments' => array('access user profiles'),
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'weight' => 10,
+ );
+ $items['examples/ajax_example/simple_user_autocomplete_callback'] = array(
+ 'page callback' => 'ajax_example_simple_user_autocomplete_callback',
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access user profiles'),
+ );
+ $items['examples/ajax_example/node_autocomplete'] = array(
+ 'title' => 'Autocomplete (node with nid)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_unique_autocomplete'),
+ 'access arguments' => array('access content'),
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'weight' => 11,
+ );
+ $items['examples/ajax_example/unique_node_autocomplete_callback'] = array(
+ 'page callback' => 'ajax_example_unique_node_autocomplete_callback',
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access content'),
+ );
+ $items['examples/ajax_example/node_by_author'] = array(
+ 'title' => 'Autocomplete (node limited by author)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_node_by_author_autocomplete'),
+ 'access callback' => TRUE,
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'weight' => 12,
+ );
+ $items['examples/ajax_example/node_by_author_autocomplete'] = array(
+ 'page callback' => 'ajax_example_node_by_author_node_autocomplete_callback',
+ 'file' => 'ajax_example_autocomplete.inc',
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access content'),
+ );
+ // This is the landing page for the progress bar example. It uses
+ // drupal_get_form() in order to build the form.
+ $items['examples/ajax_example/progressbar'] = array(
+ 'title' => 'Progress bar example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('ajax_example_progressbar_form'),
+ 'access arguments' => array('access content'),
+ 'file' => 'ajax_example_progressbar.inc',
+ );
+ // This is the callback route for the AJAX-based progress bar.
+ $items['examples/ajax_example/progressbar/progress/%'] = array(
+ 'title' => 'Progress bar progress',
+ 'page callback' => 'ajax_example_progressbar_progress',
+ 'page arguments' => array(4),
+ 'type' => MENU_CALLBACK,
+ 'access arguments' => array('access content'),
+ 'file' => 'ajax_example_progressbar.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * A basic introduction page for the ajax_example module.
+ */
+function ajax_example_intro() {
+ $markup = t('The AJAX example module provides many examples of AJAX including forms, links, and AJAX commands.');
+
+ $list[] = l(t('Simplest AJAX Example'), 'examples/ajax_example/simplest');
+ $list[] = l(t('Generate checkboxes'), 'examples/ajax_example/autocheckboxes');
+ $list[] = l(t('Generate textfields'), 'examples/ajax_example/autotextfields');
+ $list[] = l(t('Submit-driven AJAX'), 'examples/ajax_example/submit_driven_ajax');
+ $list[] = l(t('Dependent dropdown'), 'examples/ajax_example/dependent_dropdown');
+ $list[] = l(t('Dependent dropdown (with graceful degradation)'), 'examples/ajax_example/dependent_dropdown_degrades');
+ $list[] = l(t('Dynamic Sections w/JS turned off'), 'examples/ajax_example/dependent_dropdown_degrades_no_js');
+ $list[] = l(t('Wizard (with graceful degradation)'), 'examples/ajax_example/wizard');
+ $list[] = l(t('Wizard w/JS turned off'), 'examples/ajax_example/wizard_no_js');
+ $list[] = l(t('Add-more button (with graceful degradation)'), 'examples/ajax_example/add_more');
+ $list[] = l(t('Add-more button w/JS turned off'), 'examples/ajax_example/add_more_no_js');
+ $list[] = l(t('Ajax Link ("use-ajax" class)'), 'examples/ajax_example/ajax_link');
+ $list[] = l(t('Ajax Link (Renderable Array)'), 'examples/ajax_example/ajax_link_renderable');
+ $list[] = l(t('AJAX framework commands'), 'examples/ajax_example/advanced_commands');
+ $list[] = l(t('Autocomplete (simple)'), 'examples/ajax_example/simple_autocomplete');
+ $list[] = l(t('Autocomplete (node with nid)'), 'examples/ajax_example/node_autocomplete');
+ $list[] = l(t('Autocomplete (node limited by author)'), 'examples/ajax_example/node_by_author');
+
+ $variables['items'] = $list;
+ $variables['type'] = 'ul';
+ $markup .= theme('item_list', $variables);
+
+ return $markup;
+}
+
+/**
+ * Basic AJAX callback example.
+ *
+ * Simple form whose ajax-enabled 'changethis' member causes a text change
+ * in the description of the 'replace_textfield' member.
+ *
+ * See @link http://drupal.org/node/262422 Form API Tutorial @endlink
+ */
+function ajax_example_simplest($form, &$form_state) {
+ $form = array();
+ $form['changethis'] = array(
+ '#title' => t("Choose something and explain why"),
+ '#type' => 'select',
+ '#options' => array(
+ 'one' => 'one',
+ 'two' => 'two',
+ 'three' => 'three',
+ ),
+ '#ajax' => array(
+ // #ajax has two required keys: callback and wrapper.
+ // 'callback' is a function that will be called when this element changes.
+ 'callback' => 'ajax_example_simplest_callback',
+ // 'wrapper' is the HTML id of the page element that will be replaced.
+ 'wrapper' => 'replace_textfield_div',
+ // There are also several optional keys - see ajax_example_autocheckboxes
+ // below for details on 'method', 'effect' and 'speed' and
+ // ajax_example_dependent_dropdown for 'event'.
+ ),
+ );
+
+ // This entire form element will be replaced whenever 'changethis' is updated.
+ $form['replace_textfield'] = array(
+ '#type' => 'textfield',
+ '#title' => t("Why"),
+ // The prefix/suffix provide the div that we're replacing, named by
+ // #ajax['wrapper'] above.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // An AJAX request calls the form builder function for every change.
+ // We can change how we build the form based on $form_state.
+ if (!empty($form_state['values']['changethis'])) {
+ $form['replace_textfield']['#description'] = t("Say why you chose '@value'", array('@value' => $form_state['values']['changethis']));
+ }
+ return $form;
+}
+
+/**
+ * Callback for ajax_example_simplest.
+ *
+ * On an ajax submit, the form builder function is called again, then the $form
+ * and $form_state are passed to this callback function so it can select which
+ * portion of the form to send on to the client.
+ *
+ * @return array
+ * Renderable array (the textfield element)
+ */
+function ajax_example_simplest_callback($form, $form_state) {
+ // The form has already been submitted and updated. We can return the replaced
+ // item as it is.
+ return $form['replace_textfield'];
+}
+
+/**
+ * Form manipulation through AJAX.
+ *
+ * AJAX-enabled select element causes replacement of a set of checkboxes
+ * based on the selection.
+ */
+function ajax_example_autocheckboxes($form, &$form_state) {
+ // Since the form builder is called after every AJAX request, we rebuild
+ // the form based on $form_state.
+ $num_checkboxes = !empty($form_state['values']['howmany_select']) ? $form_state['values']['howmany_select'] : 1;
+
+ $form['howmany_select'] = array(
+ '#title' => t('How many checkboxes do you want?'),
+ '#type' => 'select',
+ '#options' => array(1 => 1, 2 => 2, 3 => 3, 4 => 4),
+ '#default_value' => $num_checkboxes,
+ '#ajax' => array(
+ 'callback' => 'ajax_example_autocheckboxes_callback',
+ 'wrapper' => 'checkboxes-div',
+ // 'method' defaults to replaceWith, but valid values also include
+ // append, prepend, before and after.
+ // 'method' => 'replaceWith',
+ // 'effect' defaults to none. Other valid values are 'fade' and 'slide'.
+ // See ajax_example_autotextfields for an example of 'fade'.
+ 'effect' => 'slide',
+ // 'speed' defaults to 'slow'. You can also use 'fast'
+ // or a number of milliseconds for the animation to last.
+ // 'speed' => 'slow',
+ // Don't show any throbber...
+ 'progress' => array('type' => 'none'),
+ ),
+ );
+
+ $form['checkboxes_fieldset'] = array(
+ '#title' => t("Generated Checkboxes"),
+ // The prefix/suffix provide the div that we're replacing, named by
+ // #ajax['wrapper'] above.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#type' => 'fieldset',
+ '#description' => t('This is where we get automatically generated checkboxes'),
+ );
+
+ for ($i = 1; $i <= $num_checkboxes; $i++) {
+ $form['checkboxes_fieldset']["checkbox$i"] = array(
+ '#type' => 'checkbox',
+ '#title' => "Checkbox $i",
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Callback for autocheckboxes.
+ *
+ * Callback element needs only select the portion of the form to be updated.
+ * Since #ajax['callback'] return can be HTML or a renderable array (or an
+ * array of commands), we can just return a piece of the form.
+ * See @link ajax_example_advanced.inc AJAX Advanced Commands for more details
+ * on AJAX framework commands.
+ *
+ * @return array
+ * Renderable array (the checkboxes fieldset)
+ */
+function ajax_example_autocheckboxes_callback($form, $form_state) {
+ return $form['checkboxes_fieldset'];
+}
+
+
+/**
+ * Show/hide textfields based on AJAX-enabled checkbox clicks.
+ */
+function ajax_example_autotextfields($form, &$form_state) {
+
+ $form['ask_first_name'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Ask me my first name'),
+ '#ajax' => array(
+ 'callback' => 'ajax_example_autotextfields_callback',
+ 'wrapper' => 'textfields',
+ 'effect' => 'fade',
+ ),
+ );
+ $form['ask_last_name'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Ask me my last name'),
+ '#ajax' => array(
+ 'callback' => 'ajax_example_autotextfields_callback',
+ 'wrapper' => 'textfields',
+ 'effect' => 'fade',
+ ),
+ );
+
+ $form['textfields'] = array(
+ '#title' => t("Generated text fields for first and last name"),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#type' => 'fieldset',
+ '#description' => t('This is where we put automatically generated textfields'),
+ );
+
+ // Since checkboxes return TRUE or FALSE, we have to check that
+ // $form_state has been filled as well as what it contains.
+ if (!empty($form_state['values']['ask_first_name']) && $form_state['values']['ask_first_name']) {
+ $form['textfields']['first_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First Name'),
+ );
+ }
+ if (!empty($form_state['values']['ask_last_name']) && $form_state['values']['ask_last_name']) {
+ $form['textfields']['last_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last Name'),
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Click Me'),
+ );
+
+ return $form;
+}
+
+/**
+ * Callback for autotextfields.
+ *
+ * Selects the piece of the form we want to use as replacement text and returns
+ * it as a form (renderable array).
+ *
+ * @return array
+ * Renderable array (the textfields element)
+ */
+function ajax_example_autotextfields_callback($form, $form_state) {
+ return $form['textfields'];
+}
+
+
+/**
+ * A very basic form which with an AJAX-enabled submit.
+ *
+ * On submit, the markup in the #markup element is updated.
+ */
+function ajax_example_submit_driven_ajax($form, &$form_state) {
+ $form['box'] = array(
+ '#type' => 'markup',
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#markup' => '
Initial markup for box
',
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_submit_driven_callback',
+ 'wrapper' => 'box',
+ ),
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Callback for submit_driven example.
+ *
+ * Select the 'box' element, change the markup in it, and return it as a
+ * renderable array.
+ *
+ * @return array
+ * Renderable array (the box element)
+ */
+function ajax_example_submit_driven_callback($form, $form_state) {
+ // In most cases, it is recommended that you put this logic in form generation
+ // rather than the callback. Submit driven forms are an exception, because
+ // you may not want to return the form at all.
+ $element = $form['box'];
+ $element['#markup'] = "Clicked submit ({$form_state['values']['op']}): " . date('c');
+ return $element;
+}
+
+
+/**
+ * AJAX-based dropdown example form.
+ *
+ * A form with a dropdown whose options are dependent on a
+ * choice made in a previous dropdown.
+ *
+ * On changing the first dropdown, the options in the second
+ * are updated.
+ */
+function ajax_example_dependent_dropdown($form, &$form_state) {
+ // Get the list of options to populate the first dropdown.
+ $options_first = _ajax_example_get_first_dropdown_options();
+ // If we have a value for the first dropdown from $form_state['values'] we use
+ // this both as the default value for the first dropdown and also as a
+ // parameter to pass to the function that retrieves the options for the
+ // second dropdown.
+ $selected = isset($form_state['values']['dropdown_first']) ? $form_state['values']['dropdown_first'] : key($options_first);
+
+ $form['dropdown_first'] = array(
+ '#type' => 'select',
+ '#title' => 'Instrument Type',
+ '#options' => $options_first,
+ '#default_value' => $selected,
+ // Bind an ajax callback to the change event (which is the default for the
+ // select form type) of the first dropdown. It will replace the second
+ // dropdown when rebuilt.
+ '#ajax' => array(
+ // When 'event' occurs, Drupal will perform an ajax request in the
+ // background. Usually the default value is sufficient (eg. change for
+ // select elements), but valid values include any jQuery event,
+ // most notably 'mousedown', 'blur', and 'submit'.
+ // 'event' => 'change',
+ 'callback' => 'ajax_example_dependent_dropdown_callback',
+ 'wrapper' => 'dropdown-second-replace',
+ ),
+ );
+
+ $form['dropdown_second'] = array(
+ '#type' => 'select',
+ '#title' => $options_first[$selected] . ' ' . t('Instruments'),
+ // The entire enclosing div created here gets replaced when dropdown_first
+ // is changed.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ // When the form is rebuilt during ajax processing, the $selected variable
+ // will now have the new value and so the options will change.
+ '#options' => _ajax_example_get_second_dropdown_options($selected),
+ '#default_value' => isset($form_state['values']['dropdown_second']) ? $form_state['values']['dropdown_second'] : '',
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+ return $form;
+}
+
+/**
+ * Selects just the second dropdown to be returned for re-rendering.
+ *
+ * Since the controlling logic for populating the form is in the form builder
+ * function, all we do here is select the element and return it to be updated.
+ *
+ * @return array
+ * Renderable array (the second dropdown)
+ */
+function ajax_example_dependent_dropdown_callback($form, $form_state) {
+ return $form['dropdown_second'];
+}
+
+/**
+ * Helper function to populate the first dropdown.
+ *
+ * This would normally be pulling data from the database.
+ *
+ * @return array
+ * Dropdown options.
+ */
+function _ajax_example_get_first_dropdown_options() {
+ // drupal_map_assoc() just makes an array('String' => 'String'...).
+ return drupal_map_assoc(
+ array(
+ t('String'),
+ t('Woodwind'),
+ t('Brass'),
+ t('Percussion'),
+ )
+ );
+}
+
+/**
+ * Helper function to populate the second dropdown.
+ *
+ * This would normally be pulling data from the database.
+ *
+ * @param string $key
+ * This will determine which set of options is returned.
+ *
+ * @return array
+ * Dropdown options
+ */
+function _ajax_example_get_second_dropdown_options($key = '') {
+ $options = array(
+ t('String') => drupal_map_assoc(
+ array(
+ t('Violin'),
+ t('Viola'),
+ t('Cello'),
+ t('Double Bass'),
+ )
+ ),
+ t('Woodwind') => drupal_map_assoc(
+ array(
+ t('Flute'),
+ t('Clarinet'),
+ t('Oboe'),
+ t('Bassoon'),
+ )
+ ),
+ t('Brass') => drupal_map_assoc(
+ array(
+ t('Trumpet'),
+ t('Trombone'),
+ t('French Horn'),
+ t('Euphonium'),
+ )
+ ),
+ t('Percussion') => drupal_map_assoc(
+ array(
+ t('Bass Drum'),
+ t('Timpani'),
+ t('Snare Drum'),
+ t('Tambourine'),
+ )
+ ),
+ );
+ if (isset($options[$key])) {
+ return $options[$key];
+ }
+ else {
+ return array();
+ }
+}
+/**
+ * @} End of "defgroup ajax_example".
+ */
diff --git a/sites/all/modules/examples/ajax_example/ajax_example.test b/sites/all/modules/examples/ajax_example/ajax_example.test
new file mode 100644
index 00000000..9e72f0f5
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example.test
@@ -0,0 +1,75 @@
+ 'Ajax example',
+ 'description' => 'Checks behavior of the Ajax Example',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable module.
+ */
+ public function setUp() {
+ parent::setUp('ajax_example');
+ }
+
+ /**
+ * Check the non-JS version of the "Dynamic Sections" example.
+ */
+ public function testDynamicSectionsNoJs() {
+ // The path to the example form.
+ $path = 'examples/ajax_example/dynamic_sections_no_js';
+ // Confirmation text for right and wrong answers.
+ $wrong = t('Wrong answer. Try again. (Hint: The right answer is "George Washington".)');
+ $right = t('You got the right answer: George Washington');
+ // For each question style, choose some parameters.
+ $params = array(
+ t('Multiple Choice') => array(
+ 'value' => t('Abraham Lincoln'),
+ 'answer' => t('Abraham Lincoln'),
+ 'response' => $wrong,
+ ),
+ t('True/False') => array(
+ 'value' => t('George Washington'),
+ 'answer' => t('George Washington'),
+ 'response' => $right,
+ ),
+ t('Fill-in-the-blanks') => array(
+ 'value' => NULL,
+ 'answer' => t('George Washington'),
+ 'response' => $right,
+ ),
+ );
+ foreach ($params as $style => $q_and_a) {
+ // Submit the initial form.
+ $edit = array('question_type_select' => $style);
+ $this->drupalPost($path, $edit, t('Choose'));
+ $this->assertResponse(200, format_string('Question style "@style" selected.', array('@style' => $style)));
+ // For convenience, make variables out of the entries in $QandA.
+ extract($q_and_a);
+ // Check for the expected input field.
+ $this->assertFieldByName('question', $value);
+ // Now, submit the dynamically generated form.
+ $edit = array('question' => $answer);
+ $this->drupalPost(NULL, $edit, t('Submit your answer'));
+ $this->assertRaw($response, 'Dynamic form has been submitted.');
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example_advanced.inc b/sites/all/modules/examples/ajax_example/ajax_example_advanced.inc
new file mode 100644
index 00000000..7c61d36c
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example_advanced.inc
@@ -0,0 +1,400 @@
+ 'markup',
+ '#markup' => t("
Demonstrates how AJAX commands can be used.
"),
+ );
+
+ // Shows the 'after' command with a callback generating commands.
+ $form['after_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("This shows the Ajax 'after' command. Click to put something below the div that says 'Something can be inserted after this'"),
+ );
+
+ $form['after_command_example_fieldset']['after_command_example'] = array(
+ '#value' => t("AJAX 'After': Click to put something after the div"),
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_after_callback',
+ ),
+ '#suffix' => "
Something can be inserted after this
+
'After' Command Status: Unknown
",
+ );
+
+ // Shows the 'alert' command.
+ $form['alert_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("Demonstrates the AJAX 'alert' command. Click the button."),
+ );
+ $form['alert_command_example_fieldset']['alert_command_example'] = array(
+ '#value' => t("AJAX 'Alert': Click to alert"),
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_alert_callback',
+ ),
+ );
+
+ // Shows the 'append' command.
+ $form['append_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("This shows the Ajax 'append' command. Click to put something below the div that says 'Something can be inserted after this'"),
+ );
+
+ $form['append_command_example_fieldset']['append_command_example'] = array(
+ '#value' => t("AJAX 'Append': Click to append something"),
+ '#type' => 'submit',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_append_callback',
+ ),
+ '#suffix' => "
",
+ );
+
+ // Shows the 'changed' command.
+ $form['changed_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("Demonstrates the AJAX 'changed' command. If region is 'changed', it is marked with CSS. This example also puts an asterisk by changed content."),
+ );
+
+ $form['changed_command_example_fieldset']['changed_command_example'] = array(
+ '#title' => t("AJAX changed: If checked, div is marked as changed."),
+ '#type' => 'checkbox',
+ '#default_value' => FALSE,
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_changed_callback',
+ ),
+ '#suffix' => "
",
+ );
+
+ // Shows the AJAX 'data' command. But there is no use of this information,
+ // as this would require a javascript client to use the data.
+ $form['data_command_example_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t("Demonstrates the AJAX 'data' command."),
+ );
+
+ $form['data_command_example_fieldset']['data_command_example'] = array(
+ '#title' => t("AJAX data: Set a key/value pair on a selector."),
+ '#type' => 'textfield',
+ '#default_value' => 'color=green',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_advanced_commands_data_callback',
+ ),
+ '#suffix' => "
This div should have key='time'/value='a time string' attached.
' . t("This example does a simplest possible autocomplete by username. You'll need a few users on your system for it to make sense.") . '
',
+ );
+
+ $form['user'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose a user (or a people, depending on your usage preference)'),
+ // The autocomplete path is provided in hook_menu in ajax_example.module.
+ '#autocomplete_path' => 'examples/ajax_example/simple_user_autocomplete_callback',
+ );
+
+ return $form;
+}
+
+/**
+ * This is just a copy of user_autocomplete().
+ *
+ * It works simply by searching usernames (and of course in Drupal usernames
+ * are unique, so can be used for identifying a record.)
+ *
+ * The returned $matches array has
+ * * key: string which will be displayed once the autocomplete is selected
+ * * value: the value which will is displayed in the autocomplete pulldown.
+ *
+ * In the simplest cases (see user_autocomplete()) these are the same, and
+ * nothing needs to be done. However, more more complicated autocompletes
+ * require more work. Here we demonstrate the difference by displaying the UID
+ * along with the username in the dropdown.
+ *
+ * In the end, though, we'll be doing something with the value that ends up in
+ * the textfield, so it needs to uniquely identify the record we want to access.
+ * This is demonstrated in ajax_example_unique_autocomplete().
+ *
+ * @param string $string
+ * The string that will be searched.
+ */
+function ajax_example_simple_user_autocomplete_callback($string = "") {
+ $matches = array();
+ if ($string) {
+ $result = db_select('users')
+ ->fields('users', array('name', 'uid'))
+ ->condition('name', db_like($string) . '%', 'LIKE')
+ ->range(0, 10)
+ ->execute();
+ foreach ($result as $user) {
+ // In the simplest case (see user_autocomplete), the key and the value are
+ // the same. Here we'll display the uid along with the username in the
+ // dropdown.
+ $matches[$user->name] = check_plain($user->name) . " (uid=$user->uid)";
+ }
+ }
+
+ drupal_json_output($matches);
+}
+
+/**
+ * An autocomplete form to look up nodes by title.
+ *
+ * An autocomplete form which looks up nodes by title in the node table,
+ * but must keep track of the nid, because titles are certainly not guaranteed
+ * to be unique.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * * @return array
+ * Form array.
+ */
+function ajax_example_unique_autocomplete($form, &$form_state) {
+
+ $form['info'] = array(
+ '#markup' => '
' . t("This example does a node autocomplete by title. The difference between this and a username autocomplete is that the node title may not be unique, so we have to use the nid for uniqueness, placing it in a parseable location in the textfield.") . '
',
+ );
+
+ $form['node'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose a node by title'),
+ // The autocomplete path is provided in hook_menu in ajax_example.module.
+ '#autocomplete_path' => 'examples/ajax_example/unique_node_autocomplete_callback',
+ );
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ );
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Node title validation handler.
+ *
+ * Validate handler to convert our string like "Some node title [3325]" into a
+ * nid.
+ *
+ * In case the user did not actually use the autocomplete or have a valid string
+ * there, we'll try to look up a result anyway giving it our best guess.
+ *
+ * Since the user chose a unique node, we must now use the same one in our
+ * submit handler, which means we need to look in the string for the nid.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ */
+function ajax_example_unique_autocomplete_validate($form, &$form_state) {
+ $title = $form_state['values']['node'];
+ $matches = array();
+
+ // This preg_match() looks for the last pattern like [33334] and if found
+ // extracts the numeric portion.
+ $result = preg_match('/\[([0-9]+)\]$/', $title, $matches);
+ if ($result > 0) {
+ // If $result is nonzero, we found a match and can use it as the index into
+ // $matches.
+ $nid = $matches[$result];
+ // Verify that it's a valid nid.
+ $node = node_load($nid);
+ if (empty($node)) {
+ form_error($form['node'], t('Sorry, no node with nid %nid can be found', array('%nid' => $nid)));
+ return;
+ }
+ }
+ // BUT: Not everybody will have javascript turned on, or they might hit ESC
+ // and not use the autocomplete values offered. In that case, we can attempt
+ // to come up with a useful value. This is not absolutely necessary, and we
+ // *could* just emit a form_error() as below.
+ else {
+ $nid = db_select('node')
+ ->fields('node', array('nid'))
+ ->condition('title', db_like($title) . '%', 'LIKE')
+ ->range(0, 1)
+ ->execute()
+ ->fetchField();
+ }
+
+ // Now, if we somehow found a nid, assign it to the node. If we failed, emit
+ // an error.
+ if (!empty($nid)) {
+ $form_state['values']['node'] = $nid;
+ }
+ else {
+ form_error($form['node'], t('Sorry, no node starting with %title can be found', array('%title' => $title)));
+ }
+}
+
+/**
+ * Submit handler for node lookup unique autocomplete example.
+ *
+ * Here the nid has already been placed in $form_state['values']['node'] by the
+ * validation handler.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ */
+function ajax_example_unique_autocomplete_submit($form, &$form_state) {
+ $node = node_load($form_state['values']['node']);
+ drupal_set_message(t('You found node %nid with title %title', array('%nid' => $node->nid, '%title' => $node->title)));
+}
+
+/**
+ * Autocomplete callback for nodes by title.
+ *
+ * Searches for a node by title, but then identifies it by nid, so the actual
+ * returned value can be used later by the form.
+ *
+ * The returned $matches array has
+ * - key: The title, with the identifying nid in brackets, like "Some node
+ * title [3325]"
+ * - value: the title which will is displayed in the autocomplete pulldown.
+ *
+ * Note that we must use a key style that can be parsed successfully and
+ * unambiguously. For example, if we might have node titles that could have
+ * [3325] in them, then we'd have to use a more restrictive token.
+ *
+ * @param string $string
+ * The string that will be searched.
+ */
+function ajax_example_unique_node_autocomplete_callback($string = "") {
+ $matches = array();
+ if ($string) {
+ $result = db_select('node')
+ ->fields('node', array('nid', 'title'))
+ ->condition('title', db_like($string) . '%', 'LIKE')
+ ->range(0, 10)
+ ->execute();
+ foreach ($result as $node) {
+ $matches[$node->title . " [$node->nid]"] = check_plain($node->title);
+ }
+ }
+
+ drupal_json_output($matches);
+}
+
+/**
+ * Search by title and author.
+ *
+ * In this example, we'll look up nodes by title, but we want only nodes that
+ * have been authored by a particular user. That means that we'll have to make
+ * an autocomplete function which takes a username as an argument, and use
+ * #ajax to change the #autocomplete_path based on the selected user.
+ *
+ * Although the implementation of the validate handler may look complex, it's
+ * just ambitious. The idea here is:
+ * 1. Autcomplete to get a valid username.
+ * 2. Use #ajax to update the node element with a #autocomplete_callback that
+ * gives the context for the username.
+ * 3. Do an autcomplete on the node field that is limited by the username.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_autocomplete($form, &$form_state) {
+
+ $form['intro'] = array(
+ '#markup' => '
' . t("This example uses a user autocomplete to dynamically change a node title autocomplete using #ajax.
+ This is a way to get past the fact that we have no other way to provide context to the autocomplete function.
+ It won't work very well unless you have a few users who have created some content that you can search for.") . '
',
+ );
+
+ $form['author'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose the username that authored nodes you are interested in'),
+ // Since we just need simple user lookup, we can use the simplest function
+ // of them all, user_autocomplete().
+ '#autocomplete_path' => 'user/autocomplete',
+ '#ajax' => array(
+ 'callback' => 'ajax_example_node_by_author_ajax_callback',
+ 'wrapper' => 'autocomplete-by-node-ajax-replace',
+ ),
+ );
+
+ // This form element with autocomplete will be replaced by #ajax whenever the
+ // author changes, allowing the search to be limited by user.
+ $form['node'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Choose a node by title'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#disabled' => TRUE,
+ );
+
+ // When the author changes in the author field, we'll change the
+ // autocomplete_path to match.
+ if (!empty($form_state['values']['author'])) {
+ $author = user_load_by_name($form_state['values']['author']);
+ if (!empty($author)) {
+ $autocomplete_path = 'examples/ajax_example/node_by_author_autocomplete/' . $author->uid;
+ $form['node']['#autocomplete_path'] = $autocomplete_path;
+ $form['node']['#title'] = t('Choose a node title authored by %author', array('%author' => $author->name));
+ $form['node']['#disabled'] = FALSE;
+ }
+ }
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ );
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * AJAX callback for author form element.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_ajax_callback($form, $form_state) {
+ return $form['node'];
+}
+
+/**
+ * Validate handler to convert our title string into a nid.
+ *
+ * In case the user did not actually use the autocomplete or have a valid string
+ * there, we'll try to look up a result anyway giving it our best guess.
+ *
+ * Since the user chose a unique node, we must now use the same one in our
+ * submit handler, which means we need to look in the string for the nid.
+ *
+ * This handler looks complex because it's ambitious (and tries to punt and
+ * find a node if they've entered a valid username and part of a title), but
+ * you *could* just do a form_error() if nothing were found, forcing people to
+ * use the autocomplete to look up the relevant items.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_autocomplete_validate($form, &$form_state) {
+ $title = $form_state['values']['node'];
+ $author = $form_state['values']['author'];
+ $matches = array();
+
+ // We must have a valid user.
+ $account = user_load_by_name($author);
+ if (empty($account)) {
+ form_error($form['author'], t('You must choose a valid author username'));
+ return;
+ }
+ // This preg_match() looks for the last pattern like [33334] and if found
+ // extracts the numeric portion.
+ $result = preg_match('/\[([0-9]+)\]$/', $title, $matches);
+ if ($result > 0) {
+ // If $result is nonzero, we found a match and can use it as the index into
+ // $matches.
+ $nid = $matches[$result];
+ // Verify that it's a valid nid.
+ $node = node_load($nid);
+ if (empty($node)) {
+ form_error($form['node'], t('Sorry, no node with nid %nid can be found', array('%nid' => $nid)));
+ return;
+ }
+ }
+ // BUT: Not everybody will have javascript turned on, or they might hit ESC
+ // and not use the autocomplete values offered. In that case, we can attempt
+ // to come up with a useful value. This is not absolutely necessary, and we
+ // *could* just emit a form_error() as below. Here we'll find the *first*
+ // matching title and assume that is adequate.
+ else {
+ $nid = db_select('node')
+ ->fields('node', array('nid'))
+ ->condition('uid', $account->uid)
+ ->condition('title', db_like($title) . '%', 'LIKE')
+ ->range(0, 1)
+ ->execute()
+ ->fetchField();
+ }
+
+ // Now, if we somehow found a nid, assign it to the node. If we failed, emit
+ // an error.
+ if (!empty($nid)) {
+ $form_state['values']['node'] = $nid;
+ }
+ else {
+ form_error($form['node'], t('Sorry, no node starting with %title can be found', array('%title' => $title)));
+ }
+}
+
+/**
+ * Submit handler for node lookup unique autocomplete example.
+ *
+ * Here the nid has already been placed in $form_state['values']['node'] by the
+ * validation handler.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form state.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_node_by_author_autocomplete_submit($form, &$form_state) {
+ $node = node_load($form_state['values']['node']);
+ $account = user_load($node->uid);
+ drupal_set_message(t('You found node %nid with title !title_link, authored by !user_link',
+ array(
+ '%nid' => $node->nid,
+ '!title_link' => l($node->title, 'node/' . $node->nid),
+ '!user_link' => theme('username', array('account' => $account)),
+ )
+ ));
+}
+
+/**
+ * Autocomplete callback for nodes by title but limited by author.
+ *
+ * Searches for a node by title given the passed-in author username.
+ *
+ * The returned $matches array has
+ * - key: The title, with the identifying nid in brackets, like "Some node
+ * title [3325]"
+ * - value: the title which will is displayed in the autocomplete pulldown.
+ *
+ * Note that we must use a key style that can be parsed successfully and
+ * unambiguously. For example, if we might have node titles that could have
+ * [3325] in them, then we'd have to use a more restrictive token.
+ *
+ * @param int $author_uid
+ * The author username to limit the search.
+ * @param string $string
+ * The string that will be searched.
+ */
+function ajax_example_node_by_author_node_autocomplete_callback($author_uid, $string = "") {
+ $matches = array();
+ if ($author_uid > 0 && trim($string)) {
+ $result = db_select('node')
+ ->fields('node', array('nid', 'title'))
+ ->condition('uid', $author_uid)
+ ->condition('title', db_like($string) . '%', 'LIKE')
+ ->range(0, 10)
+ ->execute();
+ foreach ($result as $node) {
+ $matches[$node->title . " [$node->nid]"] = check_plain($node->title);
+ }
+ }
+
+ drupal_json_output($matches);
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example_graceful_degradation.inc b/sites/all/modules/examples/ajax_example/ajax_example_graceful_degradation.inc
new file mode 100644
index 00000000..fe7cfb8d
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example_graceful_degradation.inc
@@ -0,0 +1,668 @@
+ 'fieldset',
+ );
+ $form['dropdown_first_fieldset']['dropdown_first'] = array(
+ '#type' => 'select',
+ '#title' => 'Instrument Type',
+ '#options' => $options_first,
+ '#attributes' => array('class' => array('enabled-for-ajax')),
+
+ // The '#ajax' property allows us to bind a callback to the server whenever
+ // this form element changes. See ajax_example_autocheckboxes and
+ // ajax_example_dependent_dropdown in ajax_example.module for more details.
+ '#ajax' => array(
+ 'callback' => 'ajax_example_dependent_dropdown_degrades_first_callback',
+ 'wrapper' => 'dropdown-second-replace',
+ ),
+ );
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded. This is for demonstration purposes only.
+ if ($no_js_use) {
+ unset($form['dropdown_first_fieldset']['dropdown_first']['#ajax']);
+ }
+
+ // Since we don't know if the user has js or not, we always need to output
+ // this element, then hide it with with css if javascript is enabled.
+ $form['dropdown_first_fieldset']['continue_to_second'] = array(
+ '#type' => 'submit',
+ '#value' => t('Choose'),
+ '#attributes' => array('class' => array('next-button')),
+ );
+
+ $form['dropdown_second_fieldset'] = array(
+ '#type' => 'fieldset',
+ );
+ $form['dropdown_second_fieldset']['dropdown_second'] = array(
+ '#type' => 'select',
+ '#title' => $options_first[$selected] . ' ' . t('Instruments'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#attributes' => array('class' => array('enabled-for-ajax')),
+ // When the form is rebuilt during processing (either AJAX or multistep),
+ // the $selected variable will now have the new value and so the options
+ // will change.
+ '#options' => _ajax_example_get_second_dropdown_options($selected),
+ );
+ $form['dropdown_second_fieldset']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('OK'),
+ // This class allows attached js file to override the disabled attribute,
+ // since it's not necessary in ajax-enabled form.
+ '#attributes' => array('class' => array('enabled-for-ajax')),
+ );
+
+ // Disable dropdown_second if a selection has not been made on dropdown_first.
+ if (empty($form_state['values']['dropdown_first'])) {
+ $form['dropdown_second_fieldset']['dropdown_second']['#disabled'] = TRUE;
+ $form['dropdown_second_fieldset']['dropdown_second']['#description'] = t('You must make your choice on the first dropdown before changing this second one.');
+ $form['dropdown_second_fieldset']['submit']['#disabled'] = TRUE;
+ }
+
+ return $form;
+}
+
+/**
+ * Submit function for ajax_example_dependent_dropdown_degrades().
+ */
+function ajax_example_dependent_dropdown_degrades_submit($form, &$form_state) {
+
+ // Now handle the case of the next, previous, and submit buttons.
+ // only submit will result in actual submission, all others rebuild.
+ switch ($form_state['triggering_element']['#value']) {
+ case t('OK'):
+ // Submit: We're done.
+ drupal_set_message(t('Your values have been submitted. dropdown_first=@first, dropdown_second=@second', array('@first' => $form_state['values']['dropdown_first'], '@second' => $form_state['values']['dropdown_second'])));
+ return;
+ }
+ // 'Choose' or anything else will cause rebuild of the form and present
+ // it again.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Selects just the second dropdown to be returned for re-rendering.
+ *
+ * @return array
+ * Renderable array (the second dropdown).
+ */
+function ajax_example_dependent_dropdown_degrades_first_callback($form, $form_state) {
+ return $form['dropdown_second_fieldset']['dropdown_second'];
+}
+
+
+/**
+ * Dynamically-enabled form with graceful no-JS degradation.
+ *
+ * Example of a form with portions dynamically enabled or disabled, but
+ * with graceful degradation in the case of no javascript.
+ *
+ * The idea here is that certain parts of the form don't need to be displayed
+ * unless a given option is selected, but then they should be displayed and
+ * configured.
+ *
+ * The third $no_js_use argument is strictly for demonstrating operation
+ * without javascript, without making the user/developer turn off javascript.
+ */
+function ajax_example_dynamic_sections($form, &$form_state, $no_js_use = FALSE) {
+
+ // Attach the CSS and JS we need to show this with and without javascript.
+ // Without javascript we need an extra "Choose" button, and this is
+ // hidden when we have javascript enabled.
+ $form['#attached']['css'] = array(
+ drupal_get_path('module', 'ajax_example') . '/ajax_example.css',
+ );
+ $form['#attached']['js'] = array(
+ drupal_get_path('module', 'ajax_example') . '/ajax_example.js',
+ );
+ $form['description'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('This example demonstrates a form which dynamically creates various sections based on the configuration in the form.
+ It deliberately allows graceful degradation to a non-javascript environment.
+ In a non-javascript environment, the "Choose" button next to the select control
+ is displayed; in a javascript environment it is hidden by the module CSS.
+
The basic idea here is that the form is built up based on
+ the selection in the question_type_select field, and it is built the same
+ whether we are in a javascript/AJAX environment or not.
+
+ Try the AJAX version and the simulated-non-AJAX version.
+ ', array('!ajax_link' => url('examples/ajax_example/dynamic_sections'), '!non_ajax_link' => url('examples/ajax_example/dynamic_sections_no_js'))) . '
',
+ );
+ $form['question_type_select'] = array(
+ '#type' => 'select',
+ '#title' => t('Question style'),
+ '#options' => drupal_map_assoc(
+ array(
+ t('Choose question style'),
+ t('Multiple Choice'),
+ t('True/False'),
+ t('Fill-in-the-blanks'),
+ )
+ ),
+ '#ajax' => array(
+ 'wrapper' => 'questions-fieldset-wrapper',
+ 'callback' => 'ajax_example_dynamic_sections_select_callback',
+ ),
+ );
+ // The CSS for this module hides this next button if JS is enabled.
+ $form['question_type_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Choose'),
+ '#attributes' => array('class' => array('next-button')),
+ // No need to validate when submitting this.
+ '#limit_validation_errors' => array(),
+ '#validate' => array(),
+ );
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ unset($form['question_type_select']['#ajax']);
+ }
+
+ // This fieldset just serves as a container for the part of the form
+ // that gets rebuilt.
+ $form['questions_fieldset'] = array(
+ '#type' => 'fieldset',
+ // These provide the wrapper referred to in #ajax['wrapper'] above.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ if (!empty($form_state['values']['question_type_select'])) {
+
+ $form['questions_fieldset']['question'] = array(
+ '#markup' => t('Who was the first president of the U.S.?'),
+ );
+ $question_type = $form_state['values']['question_type_select'];
+
+ switch ($question_type) {
+ case t('Multiple Choice'):
+ $form['questions_fieldset']['question'] = array(
+ '#type' => 'radios',
+ '#title' => t('Who was the first president of the United States'),
+ '#options' => drupal_map_assoc(
+ array(
+ t('George Bush'),
+ t('Adam McGuire'),
+ t('Abraham Lincoln'),
+ t('George Washington'),
+ )
+ ),
+ );
+ break;
+
+ case t('True/False'):
+ $form['questions_fieldset']['question'] = array(
+ '#type' => 'radios',
+ '#title' => t('Was George Washington the first president of the United States?'),
+ '#options' => array(t('George Washington') => t("True"), 0 => t("False")),
+ '#description' => t('Click "True" if you think George Washington was the first president of the United States.'),
+ );
+ break;
+
+ case t('Fill-in-the-blanks'):
+ $form['questions_fieldset']['question'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Who was the first president of the United States'),
+ '#description' => t('Please type the correct answer to the question.'),
+ );
+ break;
+ }
+
+ $form['questions_fieldset']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit your answer'),
+ );
+ }
+ return $form;
+}
+
+/**
+ * Validation function for ajax_example_dynamic_sections().
+ */
+function ajax_example_dynamic_sections_validate($form, &$form_state) {
+ $answer = $form_state['values']['question'];
+ if ($answer !== t('George Washington')) {
+ form_set_error('question', t('Wrong answer. Try again. (Hint: The right answer is "George Washington".)'));
+ }
+}
+
+/**
+ * Submit function for ajax_example_dynamic_sections().
+ */
+function ajax_example_dynamic_sections_submit($form, &$form_state) {
+ // This is only executed when a button is pressed, not when the AJAXified
+ // select is changed.
+ // Now handle the case of the next, previous, and submit buttons.
+ // Only submit will result in actual submission, all others rebuild.
+ switch ($form_state['triggering_element']['#value']) {
+ case t('Submit your answer'):
+ // Submit: We're done.
+ $form_state['rebuild'] = FALSE;
+ $answer = $form_state['values']['question'];
+
+ // Special handling for the checkbox.
+ if ($answer == 1 && $form['questions_fieldset']['question']['#type'] == 'checkbox') {
+ $answer = $form['questions_fieldset']['question']['#title'];
+ }
+ if ($answer === t('George Washington')) {
+ drupal_set_message(t('You got the right answer: @answer', array('@answer' => $answer)));
+ }
+ else {
+ drupal_set_message(t('Sorry, your answer (@answer) is wrong', array('@answer' => $answer)));
+ }
+ return;
+
+ // Any other form element will cause rebuild of the form and present
+ // it again.
+ case t('Choose'):
+ $form_state['values']['question_type_select'] = $form_state['input']['question_type_select'];
+ // Fall through.
+ default:
+ $form_state['rebuild'] = TRUE;
+ }
+}
+
+/**
+ * Callback for the select element.
+ *
+ * This just selects and returns the questions_fieldset.
+ */
+function ajax_example_dynamic_sections_select_callback($form, $form_state) {
+ return $form['questions_fieldset'];
+}
+
+/**
+ * Wizard form.
+ *
+ * This example is a classic wizard, where a different and sequential form
+ * is presented on each step of the form.
+ *
+ * In the AJAX version, the form is replaced for each wizard section. In the
+ * multistep version, it causes a new page load.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form.
+ * @param bool $no_js_use
+ * Used for this demonstration only. If true means that the form should be
+ * built using a simulated no-javascript approach (ajax.js will not be
+ * loaded.)
+ *
+ * @return array
+ * Form array.
+ */
+function ajax_example_wizard($form, &$form_state, $no_js_use = FALSE) {
+
+ // Provide a wrapper around the entire form, since we'll replace the whole
+ // thing with each submit.
+ $form['#prefix'] = '
';
+ $form['#suffix'] = '
';
+ // We want to deal with hierarchical form values.
+ $form['#tree'] = TRUE;
+ $form['description'] = array(
+ '#markup' => '
' . t('This example is a step-by-step wizard. The AJAX version does it without page reloads; the multistep version is the same code but simulates a non-javascript environment, showing it with page reloads.',
+ array('!ajax' => url('examples/ajax_example/wizard'), '!multistep' => url('examples/ajax_example/wizard_no_js')))
+ . '
',
+ );
+
+ // $form_state['storage'] has no specific drupal meaning, but it is
+ // traditional to keep variables for multistep forms there.
+ $step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step'];
+ $form_state['storage']['step'] = $step;
+
+ switch ($step) {
+ case 1:
+ $form['step1'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Step 1: Personal details'),
+ );
+ $form['step1']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your name'),
+ '#default_value' => empty($form_state['values']['step1']['name']) ? '' : $form_state['values']['step1']['name'],
+ '#required' => TRUE,
+ );
+ break;
+
+ case 2:
+ $form['step2'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Step 2: Street address info'),
+ );
+ $form['step2']['address'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your street address'),
+ '#default_value' => empty($form_state['values']['step2']['address']) ? '' : $form_state['values']['step2']['address'],
+ '#required' => TRUE,
+ );
+ break;
+
+ case 3:
+ $form['step3'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Step 3: City info'),
+ );
+ $form['step3']['city'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your city'),
+ '#default_value' => empty($form_state['values']['step3']['city']) ? '' : $form_state['values']['step3']['city'],
+ '#required' => TRUE,
+ );
+ break;
+ }
+ if ($step == 3) {
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t("Submit your information"),
+ );
+ }
+ if ($step < 3) {
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Next step'),
+ '#ajax' => array(
+ 'wrapper' => 'wizard-form-wrapper',
+ 'callback' => 'ajax_example_wizard_callback',
+ ),
+ );
+ }
+ if ($step > 1) {
+ $form['prev'] = array(
+ '#type' => 'submit',
+ '#value' => t("Previous step"),
+
+ // Since all info will be discarded, don't validate on 'prev'.
+ '#limit_validation_errors' => array(),
+ // #submit is required to use #limit_validation_errors
+ '#submit' => array('ajax_example_wizard_submit'),
+ '#ajax' => array(
+ 'wrapper' => 'wizard-form-wrapper',
+ 'callback' => 'ajax_example_wizard_callback',
+ ),
+ );
+ }
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ // For demonstration only! You don't need this.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ // For demonstration only.
+ unset($form['next']['#ajax']);
+ unset($form['prev']['#ajax']);
+ }
+
+ return $form;
+}
+
+/**
+ * Wizard callback function.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form.
+ *
+ * @return array
+ * Form array.
+ */
+function ajax_example_wizard_callback($form, $form_state) {
+ return $form;
+}
+
+/**
+ * Submit function for ajax_example_wizard.
+ *
+ * In AJAX this is only submitted when the final submit button is clicked,
+ * but in the non-javascript situation, it is submitted with every
+ * button click.
+ */
+function ajax_example_wizard_submit($form, &$form_state) {
+
+ // Save away the current information.
+ $current_step = 'step' . $form_state['storage']['step'];
+ if (!empty($form_state['values'][$current_step])) {
+ $form_state['storage']['values'][$current_step] = $form_state['values'][$current_step];
+ }
+
+ // Increment or decrement the step as needed. Recover values if they exist.
+ if ($form_state['triggering_element']['#value'] == t('Next step')) {
+ $form_state['storage']['step']++;
+ // If values have already been entered for this step, recover them from
+ // $form_state['storage'] to pre-populate them.
+ $step_name = 'step' . $form_state['storage']['step'];
+ if (!empty($form_state['storage']['values'][$step_name])) {
+ $form_state['values'][$step_name] = $form_state['storage']['values'][$step_name];
+ }
+ }
+ if ($form_state['triggering_element']['#value'] == t('Previous step')) {
+ $form_state['storage']['step']--;
+ // Recover our values from $form_state['storage'] to pre-populate them.
+ $step_name = 'step' . $form_state['storage']['step'];
+ $form_state['values'][$step_name] = $form_state['storage']['values'][$step_name];
+ }
+
+ // If they're done, submit.
+ if ($form_state['triggering_element']['#value'] == t('Submit your information')) {
+ $value_message = t('Your information has been submitted:') . ' ';
+ foreach ($form_state['storage']['values'] as $step => $values) {
+ $value_message .= "$step: ";
+ foreach ($values as $key => $value) {
+ $value_message .= "$key=$value, ";
+ }
+ }
+ drupal_set_message($value_message);
+ $form_state['rebuild'] = FALSE;
+ return;
+ }
+
+ // Otherwise, we still have work to do.
+ $form_state['rebuild'] = TRUE;
+}
+
+
+/**
+ * Form with 'add more' and 'remove' buttons.
+ *
+ * This example shows a button to "add more" - add another textfield, and
+ * the corresponding "remove" button.
+ *
+ * It works equivalently with javascript or not, and does the same basic steps
+ * either way.
+ *
+ * The basic idea is that we build the form based on the setting of
+ * $form_state['num_names']. The custom submit functions for the "add-one"
+ * and "remove-one" buttons increment and decrement $form_state['num_names']
+ * and then force a rebuild of the form.
+ *
+ * The $no_js_use argument is simply for demonstration: When set, it prevents
+ * '#ajax' from being set, thus making the example behave as if javascript
+ * were disabled in the browser.
+ */
+function ajax_example_add_more($form, &$form_state, $no_js_use = FALSE) {
+ $form['description'] = array(
+ '#markup' => '
' . t('This example shows an add-more and a remove-last button. The AJAX version does it without page reloads; the non-js version is the same code but simulates a non-javascript environment, showing it with page reloads.',
+ array('!ajax' => url('examples/ajax_example/add_more'), '!multistep' => url('examples/ajax_example/add_more_no_js')))
+ . '
',
+ );
+
+ // Because we have many fields with the same values, we have to set
+ // #tree to be able to access them.
+ $form['#tree'] = TRUE;
+ $form['names_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('People coming to the picnic'),
+ // Set up the wrapper so that AJAX will be able to replace the fieldset.
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // Build the fieldset with the proper number of names. We'll use
+ // $form_state['num_names'] to determine the number of textfields to build.
+ if (empty($form_state['num_names'])) {
+ $form_state['num_names'] = 1;
+ }
+ for ($i = 0; $i < $form_state['num_names']; $i++) {
+ $form['names_fieldset']['name'][$i] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ );
+ }
+ $form['names_fieldset']['add_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add one more'),
+ '#submit' => array('ajax_example_add_more_add_one'),
+ // See the examples in ajax_example.module for more details on the
+ // properties of #ajax.
+ '#ajax' => array(
+ 'callback' => 'ajax_example_add_more_callback',
+ 'wrapper' => 'names-fieldset-wrapper',
+ ),
+ );
+ if ($form_state['num_names'] > 1) {
+ $form['names_fieldset']['remove_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Remove one'),
+ '#submit' => array('ajax_example_add_more_remove_one'),
+ '#ajax' => array(
+ 'callback' => 'ajax_example_add_more_callback',
+ 'wrapper' => 'names-fieldset-wrapper',
+ ),
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ // For demonstration only! You don't need this.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ if (!empty($form['names_fieldset']['remove_name']['#ajax'])) {
+ unset($form['names_fieldset']['remove_name']['#ajax']);
+ }
+ unset($form['names_fieldset']['add_name']['#ajax']);
+ }
+
+ return $form;
+}
+
+/**
+ * Callback for both ajax-enabled buttons.
+ *
+ * Selects and returns the fieldset with the names in it.
+ */
+function ajax_example_add_more_callback($form, $form_state) {
+ return $form['names_fieldset'];
+}
+
+/**
+ * Submit handler for the "add-one-more" button.
+ *
+ * Increments the max counter and causes a rebuild.
+ */
+function ajax_example_add_more_add_one($form, &$form_state) {
+ $form_state['num_names']++;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for the "remove one" button.
+ *
+ * Decrements the max counter and causes a form rebuild.
+ */
+function ajax_example_add_more_remove_one($form, &$form_state) {
+ if ($form_state['num_names'] > 1) {
+ $form_state['num_names']--;
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Final submit handler.
+ *
+ * Reports what values were finally set.
+ */
+function ajax_example_add_more_submit($form, &$form_state) {
+ $output = t('These people are coming to the picnic: @names',
+ array(
+ '@names' => implode(', ', $form_state['values']['names_fieldset']['name']),
+ )
+ );
+ drupal_set_message($output);
+}
+/**
+ * @} End of "defgroup ajax_degradation_example".
+ */
diff --git a/sites/all/modules/examples/ajax_example/ajax_example_misc.inc b/sites/all/modules/examples/ajax_example/ajax_example_misc.inc
new file mode 100644
index 00000000..0b8084bb
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example_misc.inc
@@ -0,0 +1,116 @@
+use-ajax class applied to it, so if
+javascript is enabled, ajax.js will try to submit it via an AJAX call instead
+of a normal page load. The URL also contains the '/nojs/' magic string, which
+is stripped if javascript is enabled, allowing the server code to tell by the
+URL whether JS was enabled or not, letting it do different things based on that.");
+ $output = "
" . $explanation . "
";
+ // The use-ajax class is special, so that the link will call without causing
+ // a page reload. Note the /nojs portion of the path - if javascript is
+ // enabled, this part will be stripped from the path before it is called.
+ $link = l(t('Click here'), 'ajax_link_callback/nojs/', array('attributes' => array('class' => array('use-ajax'))));
+ $output .= "
$link
";
+ return $output;
+}
+
+/**
+ * AJAX-enabled link in a renderable array.
+ *
+ * Demonstrates a clickable AJAX-enabled link using a renderable array with the
+ * #ajax property.
+ *
+ * A link that is constructed as a renderable array can have the #ajax property,
+ * which ensures that the link submission is done without a page refresh. The
+ * href of the link is used as the ajax callback, but it degrades gracefully
+ * without JavaScript because if the 'nojs' portion of the href is not stripped
+ * out by js, the callback will return content as required for a full page
+ * reload.
+ *
+ * The necessary JavaScript file, ajax.js, will be included on the page
+ * automatically.
+ *
+ * @return array
+ * Form API array.
+ */
+function ajax_example_render_link_ra() {
+ $explanation = "
+The link below has been rendered as an element with the #ajax property, so if
+javascript is enabled, ajax.js will try to submit it via an AJAX call instead
+of a normal page load. The URL also contains the '/nojs/' magic string, which
+is stripped if javascript is enabled, allowing the server code to tell by the
+URL whether JS was enabled or not, letting it do different things based on that.";
+ $build['my_div'] = array(
+ '#markup' => $explanation . '',
+ );
+ $build['ajax_link'] = array(
+ '#type' => 'link',
+ '#title' => t('Click here'),
+ // Note the /nojs portion of the href - if javascript is enabled,
+ // this part will be stripped from the path before it is called.
+ '#href' => 'ajax_link_callback/nojs/',
+ '#id' => 'ajax_link',
+ '#ajax' => array(
+ 'wrapper' => 'myDiv',
+ 'method' => 'html',
+ ),
+ );
+ return $build;
+}
+
+/**
+ * Callback for link example.
+ *
+ * Takes different logic paths based on whether Javascript was enabled.
+ * If $type == 'ajax', it tells this function that ajax.js has rewritten
+ * the URL and thus we are doing an AJAX and can return an array of commands.
+ *
+ * @param string $type
+ * Either 'ajax' or 'nojs. Type is simply the normal URL argument to this URL.
+ *
+ * @return string|array
+ * If $type == 'ajax', returns an array of AJAX Commands.
+ * Otherwise, just returns the content, which will end up being a page.
+ *
+ * @ingroup ajax_example
+ */
+function ajax_link_response($type = 'ajax') {
+ if ($type == 'ajax') {
+ $output = t("This is some content delivered via AJAX");
+ $commands = array();
+ // See ajax_example_advanced.inc for more details on the available commands
+ // and how to use them.
+ $commands[] = ajax_command_append('#myDiv', $output);
+ $page = array('#type' => 'ajax', '#commands' => $commands);
+ ajax_deliver($page);
+ }
+ else {
+ $output = t("This is some content delivered via a page load.");
+ return $output;
+ }
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example_node_form_alter.inc b/sites/all/modules/examples/ajax_example/ajax_example_node_form_alter.inc
new file mode 100644
index 00000000..3dd073bd
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example_node_form_alter.inc
@@ -0,0 +1,149 @@
+ 'checkbox',
+ '#title' => t('AJAX Example 1'),
+ '#description' => t('Enable to show second field.'),
+ '#default_value' => $node->ajax_example['example_1'],
+ '#ajax' => array(
+ 'callback' => 'ajax_example_form_node_callback',
+ 'wrapper' => 'ajax-example-form-node',
+ 'effect' => 'fade',
+ ),
+ );
+ $form['container'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // If the state values exist and 'ajax_example_1' state value is 1 or
+ // if the state values don't exist and 'example1' variable is 1 then
+ // display the ajax_example_2 field.
+ if (!empty($form_state['values']['ajax_example_1']) && $form_state['values']['ajax_example_1'] == 1
+ || empty($form_state['values']) && $node->ajax_example['example_1']) {
+
+ $form['container']['ajax_example_2'] = array(
+ '#type' => 'textfield',
+ '#title' => t('AJAX Example 2'),
+ '#description' => t('AJAX Example 2'),
+ '#default_value' => empty($form_state['values']['ajax_example_2']) ? $node->ajax_example['example_2'] : $form_state['values']['ajax_example_2'],
+ );
+ }
+}
+
+/**
+ * Returns changed part of the form.
+ *
+ * @return array
+ * Form API array.
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_form_node_callback($form, $form_state) {
+ return $form['container'];
+}
+
+/**
+ * Implements hook_node_submit().
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_submit($node, $form, &$form_state) {
+ $values = $form_state['values'];
+ // Move the new data into the node object.
+ $node->ajax_example['example_1'] = $values['ajax_example_1'];
+ // Depending on the state of ajax_example_1; it may not exist.
+ $node->ajax_example['example_2'] = isset($values['ajax_example_2']) ? $values['ajax_example_2'] : '';
+}
+
+/**
+ * Implements hook_node_prepare().
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_prepare($node) {
+ if (empty($node->ajax_example)) {
+ // Set default values, since this only runs when adding a new node.
+ $node->ajax_example['example_1'] = 0;
+ $node->ajax_example['example_2'] = '';
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_load($nodes, $types) {
+ $result = db_query('SELECT * FROM {ajax_example_node_form_alter} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)))->fetchAllAssoc('nid');
+
+ foreach ($nodes as &$node) {
+ $node->ajax_example['example_1']
+ = isset($result[$node->nid]->example_1) ?
+ $result[$node->nid]->example_1 : 0;
+ $node->ajax_example['example_2']
+ = isset($result[$node->nid]->example_2) ?
+ $result[$node->nid]->example_2 : '';
+ }
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_insert($node) {
+ if (isset($node->ajax_example)) {
+ db_insert('ajax_example_node_form_alter')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'example_1' => $node->ajax_example['example_1'],
+ 'example_2' => $node->ajax_example['example_2'],
+ ))
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_node_update().
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_update($node) {
+ if (db_select('ajax_example_node_form_alter', 'a')->fields('a')->condition('nid', $node->nid, '=')->execute()->fetchAssoc()) {
+ db_update('ajax_example_node_form_alter')
+ ->fields(array(
+ 'example_1' => $node->ajax_example['example_1'],
+ 'example_2' => $node->ajax_example['example_2'],
+ ))
+ ->condition('nid', $node->nid)
+ ->execute();
+ }
+ else {
+ // Cleaner than doing it again.
+ ajax_example_node_insert($node);
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ * @see ajax_example_form_node_form_alter()
+ */
+function ajax_example_node_delete($node) {
+ db_delete('ajax_example_node_form_alter')
+ ->condition('nid', $node->nid)
+ ->execute();
+}
diff --git a/sites/all/modules/examples/ajax_example/ajax_example_progressbar.inc b/sites/all/modules/examples/ajax_example/ajax_example_progressbar.inc
new file mode 100644
index 00000000..b611150c
--- /dev/null
+++ b/sites/all/modules/examples/ajax_example/ajax_example_progressbar.inc
@@ -0,0 +1,116 @@
+ '',
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ '#ajax' => array(
+ // Here we set up our AJAX callback handler.
+ 'callback' => 'ajax_example_progressbar_callback',
+ // Tell FormAPI about our progress bar.
+ 'progress' => array(
+ 'type' => 'bar',
+ 'message' => t('Execute..'),
+ // Have the progress bar access this URL path.
+ 'url' => url('examples/ajax_example/progressbar/progress/' . $form_state['time']),
+ // The time interval for the progress bar to check for updates.
+ 'interval' => 1000,
+ ),
+ ),
+ );
+
+ return $form;
+}
+
+/**
+ * Get the progress bar execution status, as JSON.
+ *
+ * This is the menu handler for
+ * examples/ajax_example/progressbar/progress/$time.
+ *
+ * This function is our wholly arbitrary job that we're checking the status for.
+ * In this case, we're reading a system variable that is being updated by
+ * ajax_example_progressbar_callback().
+ *
+ * We set up the AJAX progress bar to check the status every second, so this
+ * will execute about once every second.
+ *
+ * The progress bar JavaScript accepts two values: message and percentage. We
+ * set those in an array and in the end convert it JSON for sending back to the
+ * client-side JavaScript.
+ *
+ * @param int $time
+ * Timestamp.
+ *
+ * @see ajax_example_progressbar_callback()
+ */
+function ajax_example_progressbar_progress($time) {
+ $progress = array(
+ 'message' => t('Starting execute...'),
+ 'percentage' => -1,
+ );
+
+ $completed_percentage = variable_get('example_progressbar_' . $time, 0);
+
+ if ($completed_percentage) {
+ $progress['message'] = t('Executing...');
+ $progress['percentage'] = $completed_percentage;
+ }
+
+ drupal_json_output($progress);
+}
+
+/**
+ * Our submit handler.
+ *
+ * This handler spends some time changing a variable and sleeping, and then
+ * finally returns a form element which marks the #progress-status DIV as
+ * completed.
+ *
+ * While this is occurring, ajax_example_progressbar_progress() will be called
+ * a number of times by the client-sid JavaScript, which will poll the variable
+ * being set here.
+ *
+ * @see ajax_example_progressbar_progress()
+ */
+function ajax_example_progressbar_callback($form, &$form_state) {
+ $variable_name = 'example_progressbar_' . $form_state['time'];
+ $commands = array();
+
+ variable_set($variable_name, 10);
+ sleep(2);
+ variable_set($variable_name, 40);
+ sleep(2);
+ variable_set($variable_name, 70);
+ sleep(2);
+ variable_set($variable_name, 90);
+ sleep(2);
+ variable_del($variable_name);
+
+ $commands[] = ajax_command_html('#progress-status', t('Executed.'));
+
+ return array(
+ '#type' => 'ajax',
+ '#commands' => $commands,
+ );
+}
diff --git a/sites/all/modules/examples/batch_example/batch_example.info b/sites/all/modules/examples/batch_example/batch_example.info
new file mode 100644
index 00000000..6529375d
--- /dev/null
+++ b/sites/all/modules/examples/batch_example/batch_example.info
@@ -0,0 +1,12 @@
+name = Batch example
+description = An example outlining how a module can define batch operations.
+package = Example modules
+core = 7.x
+files[] = batch_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/batch_example/batch_example.install b/sites/all/modules/examples/batch_example/batch_example.install
new file mode 100644
index 00000000..4ce14503
--- /dev/null
+++ b/sites/all/modules/examples/batch_example/batch_example.install
@@ -0,0 +1,85 @@
+fetchField();
+ // A place to store messages during the run.
+ $sandbox['messages'] = array();
+ // Last node read via the query.
+ $sandbox['current_node'] = -1;
+ }
+
+ // Process nodes by groups of 10 (arbitrary value).
+ // 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;
+
+ // Retrieve the next group of nids.
+ $result = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->orderBy('n.nid', 'ASC')
+ ->where('n.nid > :nid', array(':nid' => $sandbox['current_node']))
+ ->extend('PagerDefault')
+ ->limit($limit)
+ ->execute();
+ foreach ($result as $row) {
+ // Here we actually perform a dummy 'update' on the current node.
+ $node = db_query('SELECT nid FROM {node} WHERE nid = :nid', array(':nid' => $row->nid))->fetchField();
+
+ // Update our progress information.
+ $sandbox['progress']++;
+ $sandbox['current_node'] = $row->nid;
+ }
+
+ // 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']);
+
+ // Set up a per-run message; Make a copy of $sandbox so we can change it.
+ // This is simply a debugging stanza to illustrate how to capture status
+ // from each pass through hook_update_N().
+ $sandbox_status = $sandbox;
+ // Don't want them in the output.
+ unset($sandbox_status['messages']);
+ $sandbox['messages'][] = t('$sandbox=') . print_r($sandbox_status, TRUE);
+
+ if ($sandbox['#finished']) {
+ // hook_update_N() may optionally return a string which will be displayed
+ // to the user.
+ $final_message = '
' . implode('
', $sandbox['messages']) . "
";
+ return t('The batch_example demonstration update did what it was supposed to do: @message', array('@message' => $final_message));
+ }
+}
diff --git a/sites/all/modules/examples/batch_example/batch_example.module b/sites/all/modules/examples/batch_example/batch_example.module
new file mode 100644
index 00000000..fddf344b
--- /dev/null
+++ b/sites/all/modules/examples/batch_example/batch_example.module
@@ -0,0 +1,319 @@
+ 'Batch example',
+ 'description' => 'Example of Drupal batch processing',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('batch_example_simple_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Form builder function to allow choice of which batch to run.
+ */
+function batch_example_simple_form() {
+ $form['description'] = array(
+ '#type' => 'markup',
+ '#markup' => t('This example offers two different batches. The first does 1000 identical operations, each completed in on run; the second does 20 operations, but each takes more than one run to operate if there are more than 5 nodes.'),
+ );
+ $form['batch'] = array(
+ '#type' => 'select',
+ '#title' => 'Choose batch',
+ '#options' => array(
+ 'batch_1' => t('batch 1 - 1000 operations, each loading the same node'),
+ 'batch_2' => t('batch 2 - 20 operations. each one loads all nodes 5 at a time'),
+ ),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Go',
+ );
+
+ // If no nodes, prevent submission.
+ // Find out if we have a node to work with. Otherwise it won't work.
+ $nid = batch_example_lowest_nid();
+ if (empty($nid)) {
+ drupal_set_message(t("You don't currently have any nodes, and this example requires a node to work with. As a result, this form is disabled."));
+ $form['submit']['#disabled'] = TRUE;
+ }
+ return $form;
+}
+
+/**
+ * Submit handler.
+ *
+ * @param array $form
+ * Form API form.
+ * @param array $form_state
+ * Form API form.
+ */
+function batch_example_simple_form_submit($form, &$form_state) {
+ $function = 'batch_example_' . $form_state['values']['batch'];
+
+ // Reset counter for debug information.
+ $_SESSION['http_request_count'] = 0;
+
+ // Execute the function named batch_example_batch_1() or
+ // batch_example_batch_2().
+ $batch = $function();
+ batch_set($batch);
+}
+
+
+/**
+ * Batch 1 definition: Load the node with the lowest nid 1000 times.
+ *
+ * This creates an operations array defining what batch 1 should do, including
+ * what it should do when it's finished. In this case, each operation is the
+ * same and by chance even has the same $nid to operate on, but we could have
+ * a mix of different types of operations in the operations array.
+ */
+function batch_example_batch_1() {
+ $nid = batch_example_lowest_nid();
+ $num_operations = 1000;
+ drupal_set_message(t('Creating an array of @num operations', array('@num' => $num_operations)));
+
+ $operations = array();
+ // Set up an operations array with 1000 elements, each doing function
+ // batch_example_op_1.
+ // Each operation in the operations array means at least one new HTTP request,
+ // running Drupal from scratch to accomplish the operation. If the operation
+ // returns with $context['finished'] != TRUE, then it will be called again.
+ // In this example, $context['finished'] is always TRUE.
+ for ($i = 0; $i < $num_operations; $i++) {
+ // Each operation is an array consisting of
+ // - The function to call.
+ // - An array of arguments to that function.
+ $operations[] = array(
+ 'batch_example_op_1',
+ array(
+ $nid,
+ t('(Operation @operation)', array('@operation' => $i)),
+ ),
+ );
+ }
+ $batch = array(
+ 'operations' => $operations,
+ 'finished' => 'batch_example_finished',
+ );
+ return $batch;
+}
+
+/**
+ * Batch operation for batch 1: load a node.
+ *
+ * This is the function that is called on each operation in batch 1.
+ */
+function batch_example_op_1($nid, $operation_details, &$context) {
+ $node = node_load($nid, NULL, TRUE);
+
+ // Store some results for post-processing in the 'finished' callback.
+ // The contents of 'results' will be available as $results in the
+ // 'finished' function (in this example, batch_example_finished()).
+ $context['results'][] = $node->nid . ' : ' . check_plain($node->title);
+
+ // Optional message displayed under the progressbar.
+ $context['message'] = t('Loading node "@title"', array('@title' => $node->title)) . ' ' . $operation_details;
+
+ _batch_example_update_http_requests();
+}
+
+/**
+ * Batch 2 : Prepare a batch definition that will load all nodes 20 times.
+ */
+function batch_example_batch_2() {
+ $num_operations = 20;
+
+ // Give helpful information about how many nodes are being operated on.
+ $node_count = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
+ drupal_set_message(
+ t('There are @node_count nodes so each of the @num operations will require @count HTTP requests.',
+ array(
+ '@node_count' => $node_count,
+ '@num' => $num_operations,
+ '@count' => ceil($node_count / 5),
+ )
+ )
+ );
+
+ $operations = array();
+ // 20 operations, each one loads all nodes.
+ for ($i = 0; $i < $num_operations; $i++) {
+ $operations[] = array(
+ 'batch_example_op_2',
+ array(t('(Operation @operation)', array('@operation' => $i))),
+ );
+ }
+ $batch = array(
+ 'operations' => $operations,
+ 'finished' => 'batch_example_finished',
+ // Message displayed while processing the batch. Available placeholders are:
+ // @current, @remaining, @total, @percentage, @estimate and @elapsed.
+ // These placeholders are replaced with actual values in _batch_process(),
+ // using strtr() instead of t(). The values are determined based on the
+ // number of operations in the 'operations' array (above), NOT by the number
+ // of nodes that will be processed. In this example, there are 20
+ // operations, so @total will always be 20, even though there are multiple
+ // nodes per operation.
+ // Defaults to t('Completed @current of @total.').
+ 'title' => t('Processing batch 2'),
+ 'init_message' => t('Batch 2 is starting.'),
+ 'progress_message' => t('Processed @current out of @total.'),
+ 'error_message' => t('Batch 2 has encountered an error.'),
+ );
+ return $batch;
+}
+
+/**
+ * Batch operation for batch 2 : load all nodes, 5 by five.
+ *
+ * After each group of 5 control is returned to the batch API for later
+ * continuation.
+ */
+function batch_example_op_2($operation_details, &$context) {
+ // Use the $context['sandbox'] at your convenience to store the
+ // information needed to track progression between successive calls.
+ if (empty($context['sandbox'])) {
+ $context['sandbox'] = array();
+ $context['sandbox']['progress'] = 0;
+ $context['sandbox']['current_node'] = 0;
+
+ // Save node count for the termination message.
+ $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
+ }
+
+ // Process nodes by groups of 5 (arbitrary value).
+ // When a group of five 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.
+ // That way even though we're already processing at the operation level
+ // the operation itself is interruptible.
+ $limit = 5;
+
+ // Retrieve the next group of nids.
+ $result = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->orderBy('n.nid', 'ASC')
+ ->where('n.nid > :nid', array(':nid' => $context['sandbox']['current_node']))
+ ->extend('PagerDefault')
+ ->limit($limit)
+ ->execute();
+ foreach ($result as $row) {
+ // Here we actually perform our dummy 'processing' on the current node.
+ $node = node_load($row->nid, NULL, TRUE);
+
+ // Store some results for post-processing in the 'finished' callback.
+ // The contents of 'results' will be available as $results in the
+ // 'finished' function (in this example, batch_example_finished()).
+ $context['results'][] = $node->nid . ' : ' . check_plain($node->title) . ' ' . $operation_details;
+
+ // Update our progress information.
+ $context['sandbox']['progress']++;
+ $context['sandbox']['current_node'] = $node->nid;
+ $context['message'] = check_plain($node->title);
+ }
+
+ // Inform the batch engine that we are not finished,
+ // and provide an estimation of the completion level we reached.
+ if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+ $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']);
+ }
+ _batch_example_update_http_requests();
+}
+
+/**
+ * Batch 'finished' callback used by both batch 1 and batch 2.
+ */
+function batch_example_finished($success, $results, $operations) {
+ if ($success) {
+ // Here we could do something meaningful with the results.
+ // We just display the number of nodes we processed...
+ drupal_set_message(t('@count results processed in @requests HTTP requests.', array('@count' => count($results), '@requests' => _batch_example_get_http_requests())));
+ drupal_set_message(t('The final result was "%final"', array('%final' => end($results))));
+ }
+ else {
+ // An error occurred.
+ // $operations contains the operations that remained unprocessed.
+ $error_operation = reset($operations);
+ drupal_set_message(
+ t('An error occurred while processing @operation with arguments : @args',
+ array(
+ '@operation' => $error_operation[0],
+ '@args' => print_r($error_operation[0], TRUE),
+ )
+ ),
+ 'error'
+ );
+ }
+}
+
+/**
+ * Utility function - simply queries and loads the lowest nid.
+ *
+ * @return int|NULL
+ * A nid or NULL if there are no nodes.
+ */
+function batch_example_lowest_nid() {
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->orderBy('n.nid', 'ASC')
+ ->extend('PagerDefault')
+ ->limit(1);
+ $nid = $select->execute()->fetchField();
+ return $nid;
+}
+
+/**
+ * Utility function to increment HTTP requests in a session variable.
+ */
+function _batch_example_update_http_requests() {
+ $_SESSION['http_request_count']++;
+}
+
+/**
+ * Utility function to count the HTTP requests in a session variable.
+ *
+ * @return int
+ * Number of requests.
+ */
+function _batch_example_get_http_requests() {
+ return !empty($_SESSION['http_request_count']) ? $_SESSION['http_request_count'] : 0;
+}
+/**
+ * @} End of "defgroup batch_example".
+ */
diff --git a/sites/all/modules/examples/batch_example/batch_example.test b/sites/all/modules/examples/batch_example/batch_example.test
new file mode 100644
index 00000000..cc93f5aa
--- /dev/null
+++ b/sites/all/modules/examples/batch_example/batch_example.test
@@ -0,0 +1,60 @@
+ 'Batch example functionality',
+ 'description' => 'Verify the defined batches.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('batch_example');
+ // Create user.
+ $this->webUser = $this->drupalCreateUser();
+ }
+
+ /**
+ * Login user, create 30 nodes and test both batch examples.
+ */
+ public function testBatchExampleBasic() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Create 30 nodes.
+ for ($count = 0; $count < 30; $count++) {
+ $node = $this->drupalCreateNode();
+ }
+
+ // Launch Batch 1
+ $result = $this->drupalPost('examples/batch_example', array('batch' => 'batch_1'), t('Go'));
+ // Check that 1000 operations were performed.
+ $this->assertText('1000 results processed');
+
+ // Launch Batch 2
+ $result = $this->drupalPost('examples/batch_example', array('batch' => 'batch_2'), t('Go'));
+ // Check that 600 operations were performed.
+ $this->assertText('600 results processed');
+ }
+}
diff --git a/sites/all/modules/examples/block_example/block_example.info b/sites/all/modules/examples/block_example/block_example.info
new file mode 100644
index 00000000..30b483af
--- /dev/null
+++ b/sites/all/modules/examples/block_example/block_example.info
@@ -0,0 +1,19 @@
+name = Block Example
+description = An example outlining how a module can define blocks.
+package = Example modules
+core = 7.x
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:block
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = block_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/block_example/block_example.install b/sites/all/modules/examples/block_example/block_example.install
new file mode 100644
index 00000000..50541823
--- /dev/null
+++ b/sites/all/modules/examples/block_example/block_example.install
@@ -0,0 +1,14 @@
+ 'block_example_page',
+ 'access callback' => TRUE,
+ 'title' => 'Block Example',
+ );
+ return $items;
+}
+
+/**
+ * Simple page function to explain what the block example is about.
+ */
+function block_example_page() {
+ $page = array(
+ '#type' => 'markup',
+ '#markup' => t('The Block Example provides three sample blocks which demonstrate the various block APIs. To experiment with the blocks, enable and configure them on the block admin page.', array('@url' => url('admin/structure/block'))),
+ );
+ return $page;
+}
+/**
+ * Implements hook_block_info().
+ *
+ * This hook declares what blocks are provided by the module.
+ */
+function block_example_block_info() {
+ // This hook returns an array, each component of which is an array of block
+ // information. The array keys are the 'delta' values used in other block
+ // hooks.
+ //
+ // The required block information is a block description, which is shown
+ // to the site administrator in the list of possible blocks. You can also
+ // provide initial settings for block weight, status, etc.
+ //
+ // Many options are defined in hook_block_info():
+ $blocks['example_configurable_text'] = array(
+ // info: The name of the block.
+ 'info' => t('Example: configurable text string'),
+ // Block caching options (per role, per user, etc.)
+ // DRUPAL_CACHE_PER_ROLE is the default.
+ 'cache' => DRUPAL_CACHE_PER_ROLE,
+ );
+
+ // This sample shows how to provide default settings. In this case we'll
+ // enable the block in the first sidebar and make it visible only on
+ // 'node/*' pages. See the hook_block_info() documentation for these.
+ $blocks['example_empty'] = array(
+ 'info' => t('Example: empty block'),
+ 'status' => TRUE,
+ 'region' => 'sidebar_first',
+ 'visibility' => BLOCK_VISIBILITY_LISTED,
+ 'pages' => 'node/*',
+ );
+
+ $blocks['example_uppercase'] = array(
+ // info: The name of the block.
+ 'info' => t('Example: uppercase this please'),
+ 'status' => TRUE,
+ 'region' => 'sidebar_first',
+ );
+
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_configure().
+ *
+ * This hook declares configuration options for blocks provided by this module.
+ */
+function block_example_block_configure($delta = '') {
+ $form = array();
+ // The $delta parameter tells us which block is being configured.
+ // In this example, we'll allow the administrator to customize
+ // the text of the 'configurable text string' block defined in this module.
+ if ($delta == 'example_configurable_text') {
+ // All we need to provide is the specific configuration options for our
+ // block. Drupal will take care of the standard block configuration options
+ // (block title, page visibility, etc.) and the save button.
+ $form['block_example_string'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Block contents'),
+ '#size' => 60,
+ '#description' => t('This text will appear in the example block.'),
+ '#default_value' => variable_get('block_example_string', t('Some example content.')),
+ );
+ }
+ return $form;
+}
+
+/**
+ * Implements hook_block_save().
+ *
+ * This hook declares how the configured options for a block
+ * provided by this module are saved.
+ */
+function block_example_block_save($delta = '', $edit = array()) {
+ // We need to save settings from the configuration form.
+ // We need to check $delta to make sure we are saving the right block.
+ if ($delta == 'example_configurable_text') {
+ // Have Drupal save the string to the database.
+ variable_set('block_example_string', $edit['block_example_string']);
+ }
+}
+
+/**
+ * Implements hook_block_view().
+ *
+ * This hook generates the contents of the blocks themselves.
+ */
+function block_example_block_view($delta = '') {
+ // The $delta parameter tells us which block is being requested.
+ switch ($delta) {
+ case 'example_configurable_text':
+ // The subject is displayed at the top of the block. Note that it
+ // should be passed through t() for translation. The title configured
+ // for the block using Drupal UI supercedes this one.
+ $block['subject'] = t('Title of first block (example_configurable_text)');
+ // The content of the block is typically generated by calling a custom
+ // function.
+ $block['content'] = block_example_contents($delta);
+ break;
+
+ case 'example_empty':
+ $block['subject'] = t('Title of second block (example_empty)');
+ $block['content'] = block_example_contents($delta);
+ break;
+
+ case 'example_uppercase':
+ $block['subject'] = t("uppercase this please");
+ $block['content'] = t("This block's title will be changed to uppercase. Any other block with 'uppercase' in the subject or title will also be altered. If you change this block's title through the UI to omit the word 'uppercase', it will still be altered to uppercase as the subject key has not been changed.");
+ break;
+ }
+ return $block;
+}
+
+/**
+ * A module-defined block content function.
+ */
+function block_example_contents($which_block) {
+ switch ($which_block) {
+ case 'example_configurable_text':
+ // Modules would typically perform some database queries to fetch the
+ // content for their blocks. Here, we'll just use the variable set in the
+ // block configuration or, if none has set, a default value.
+ // Block content can be returned in two formats: renderable arrays
+ // (as here) are preferred though a simple string will work as well.
+ // Block content created through the UI defaults to a string.
+ $result = array(
+ '#markup' => variable_get('block_example_string',
+ t('A default value. This block was created at %time',
+ array('%time' => date('c'))
+ )
+ ),
+ );
+ return $result;
+
+ case 'example_empty':
+ // It is possible that a block not have any content, since it is
+ // probably dynamically constructed. In this case, Drupal will not display
+ // the block at all. This block will not be displayed.
+ return;
+ }
+}
+
+/*
+ * The following hooks can be used to alter blocks
+ * provided by your own or other modules.
+ */
+
+/**
+ * Implements hook_block_list_alter().
+ *
+ * This hook allows you to add, remove or modify blocks in the block list. The
+ * block list contains the block definitions. This example requires
+ * search module and the search block enabled
+ * to see how this hook implementation works.
+ *
+ * You may also be interested in hook_block_info_alter(), which allows changes
+ * to the behavior of blocks.
+ */
+function block_example_block_list_alter(&$blocks) {
+ // We are going to make the search block sticky on bottom of regions. For
+ // this example, we will modify the block list and append the search block at
+ // the end of the list, so even if the administrator configures the block to
+ // be on the top of the region, it will demote to bottom again.
+ foreach ($blocks as $bid => $block) {
+ if (($block->module == 'search') && ($block->delta == 'form')) {
+ // Remove the block from the list and append to the end.
+ unset($blocks[$bid]);
+ $blocks[$bid] = $block;
+ break;
+ }
+ }
+}
+
+/**
+ * Implements hook_block_view_alter().
+ *
+ * This hook allows you to modify the output of any block in the system.
+ *
+ * In addition, instead of hook_block_view_alter(), which is called for all
+ * blocks, you can also use hook_block_view_MODULE_DELTA_alter() to alter a
+ * specific block. To change only our block using
+ * hook_block_view_MODULE_DELTA_alter, we would use the function:
+ * block_example_block_view_block_example_example_configurable_text_alter()
+ *
+ * We are going to uppercase the subject (the title of the block as shown to the
+ * user) of any block if the string "uppercase" appears in the block title or
+ * subject. Default block titles are set programmatically in the subject key;
+ * titles created through the UI are saved in the title key. This module creates
+ * an example block to demonstrate this effect (default title set
+ * programmatically as subject). You can also demonstrate the effect of this
+ * hook by creating a new block whose title has the string 'uppercase' in it
+ * (set as title through the UI).
+ */
+function block_example_block_view_alter(&$data, $block) {
+ // We'll search for the string 'uppercase'.
+ if ((!empty($block->title) && stristr($block->title, 'uppercase')) || (!empty($data['subject']) && stristr($data['subject'], 'uppercase'))) {
+ // This will uppercase the default title.
+ $data['subject'] = isset($data['subject']) ? drupal_strtoupper($data['subject']) : '';
+ // This will uppercase a title set in the UI.
+ $block->title = isset($block->title) ? drupal_strtoupper($block->title) : '';
+ }
+}
+/**
+ * @} End of "defgroup block_example".
+ */
diff --git a/sites/all/modules/examples/block_example/block_example.test b/sites/all/modules/examples/block_example/block_example.test
new file mode 100644
index 00000000..6698a11f
--- /dev/null
+++ b/sites/all/modules/examples/block_example/block_example.test
@@ -0,0 +1,114 @@
+ 'Block example functionality',
+ 'description' => 'Test the configuration options and block created by Block Example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('block_example', 'search');
+ // Create user. Search content permission granted for the search block to
+ // be shown.
+ $this->webUser = $this->drupalCreateUser(
+ array(
+ 'administer blocks',
+ 'search content',
+ 'access contextual links',
+ )
+ );
+ }
+
+ /**
+ * Functional test for our block example.
+ *
+ * Login user, create an example node, and test block functionality through
+ * the admin and user interfaces.
+ */
+ public function testBlockExampleBasic() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Find the blocks in the settings page.
+ $this->drupalGet('admin/structure/block');
+ $this->assertRaw(t('Example: configurable text string'), 'Block configurable-string found.');
+ $this->assertRaw(t('Example: empty block'), 'Block empty-block found.');
+
+ // Verify the default settings for block are processed.
+ $this->assertFieldByName('blocks[block_example_example_empty][region]', 'sidebar_first', 'Empty block is enabled in first sidebar successfully verified.');
+ $this->assertFieldByName('blocks[block_example_example_configurable_text][region]', -1, 'Configurable text block is disabled in first sidebar successfully verified.');
+
+ // Verify that blocks are not shown.
+ $this->drupalGet('/');
+ $this->assertNoRaw(t('Title of first block (example_configurable_text)'), 'Block configurable test not found.');
+ $this->assertNoRaw(t('Title of second block (example_empty)'), 'Block empty not found.');
+
+ // Enable the Configurable text block and verify.
+ $this->drupalPost('admin/structure/block', array('blocks[block_example_example_configurable_text][region]' => 'sidebar_first'), t('Save blocks'));
+ $this->assertFieldByName('blocks[block_example_example_configurable_text][region]', 'sidebar_first', 'Configurable text block is enabled in first sidebar successfully verified.');
+
+ // Verify that blocks are there. Empty block will not be shown, because it
+ // is empty.
+ $this->drupalGet('/');
+ $this->assertRaw(t('Title of first block (example_configurable_text)'), 'Block configurable text found.');
+
+ // Change content of configurable text block.
+ $string = $this->randomName();
+ $this->drupalPost('admin/structure/block/manage/block_example/example_configurable_text/configure', array('block_example_string' => $string), t('Save block'));
+
+ // Verify that new content is shown.
+ $this->drupalGet('/');
+ $this->assertRaw($string, 'Content of configurable text block successfully verified.');
+
+ // Make sure our example uppercased block is shown as altered by the
+ // hook_block_view_alter().
+ $this->assertRaw(t('UPPERCASE THIS PLEASE'));
+
+ // Create a new block and make sure it gets uppercased.
+ $post = array(
+ 'title' => t('configurable block to be uppercased'),
+ 'info' => t('configurable block to be uppercased'),
+ 'body[value]' => t('body of new block'),
+ 'regions[bartik]' => 'sidebar_first',
+ );
+ $this->drupalPost('admin/structure/block/add', $post, t('Save block'));
+ $this->drupalGet('/');
+ $this->assertRaw(('CONFIGURABLE BLOCK TO BE UPPERCASED'));
+
+ // Verify that search block is at the bottom of the region.
+ // Enable the search block on top of sidebar_first.
+ $block_options = array(
+ 'blocks[search_form][region]' => 'sidebar_first',
+ 'blocks[search_form][weight]' => -9,
+ );
+ $this->drupalPost('admin/structure/block', $block_options, t('Save blocks'));
+
+ // The first 'configure block' link should be from our configurable block,
+ // the second from the Navigation menu, and the fifth (#4) from
+ // search block if it was successfully pushed to the bottom.
+ $this->drupalGet('/');
+ $this->clickLink('Configure block', 4);
+ $this->assertText(t("'@search' block", array('@search' => t('Search form'))), 'hook_block_info_alter successfully verified.');
+ }
+}
diff --git a/sites/all/modules/examples/cache_example/cache_example.info b/sites/all/modules/examples/cache_example/cache_example.info
new file mode 100644
index 00000000..6c98f757
--- /dev/null
+++ b/sites/all/modules/examples/cache_example/cache_example.info
@@ -0,0 +1,13 @@
+name = Cache Example
+description = An example outlining how to use Cache API.
+package = Example modules
+core = 7.x
+
+files[] = cache_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/cache_example/cache_example.module b/sites/all/modules/examples/cache_example/cache_example.module
new file mode 100644
index 00000000..8e2d8732
--- /dev/null
+++ b/sites/all/modules/examples/cache_example/cache_example.module
@@ -0,0 +1,246 @@
+ 'Cache example',
+ 'description' => 'Example of Drupal Cache API',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('cache_example_page_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Main page for cache_example.
+ *
+ * Displays a page/form which outlines how Drupal's cache works.
+ */
+function cache_example_page_form($form, &$form_state) {
+ // Log execution time.
+ $start_time = microtime(TRUE);
+
+ // Try to load the files count from cache. This function will accept two
+ // arguments:
+ // - cache object name (cid)
+ // - cache bin, the (optional) cache bin (most often a database table) where
+ // the object is to be saved.
+ //
+ // cache_get() returns the cached object or FALSE if object does not exist.
+ if ($cache = cache_get('cache_example_files_count')) {
+ /*
+ * Get cached data. Complex data types will be unserialized automatically.
+ */
+ $files_count = $cache->data;
+ }
+ else {
+ // If there was no cached data available we have to search filesystem.
+ // Recursively get all files from Drupal's folder.
+ $files_count = count(file_scan_directory('.', '/.*/'));
+
+ // Since we have recalculated, we now need to store the new data into cache.
+ // Complex data types will be automatically serialized before being saved
+ // into cache.
+ // Here we use the default setting and create an unexpiring cache item.
+ // See below for an example that creates an expiring cache item.
+ cache_set('cache_example_files_count', $files_count);
+ }
+
+ $end_time = microtime(TRUE);
+ $duration = $end_time - $start_time;
+
+ // Format intro message.
+ $intro_message = '
' . t('This example will search the entire drupal folder and display a count of the files in it.') . ' ';
+ $intro_message .= t('This can take a while, since there are a lot of files to be searched.') . ' ';
+ $intro_message .= t('We will search filesystem just once and save output to the cache. We will use cached data for later requests.') . '
';
+ $intro_message .= '
' . t('Reload this page to see cache in action.', array('@url' => request_uri())) . ' ';
+ $intro_message .= t('You can use the button below to remove cached data.') . '
';
+
+ $form['file_search'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('File search caching'),
+ );
+ $form['file_search']['introduction'] = array(
+ '#markup' => $intro_message,
+ );
+
+ $color = empty($cache) ? 'red' : 'green';
+ $retrieval = empty($cache) ? t('calculated by traversing the filesystem') : t('retrieved from cache');
+
+ $form['file_search']['statistics'] = array(
+ '#type' => 'item',
+ '#markup' => t('%count files exist in this Drupal installation; @retrieval in @time ms. (Source: @source)',
+ array(
+ '%count' => $files_count,
+ '@retrieval' => $retrieval,
+ '@time' => number_format($duration * 1000, 2),
+ '@color' => $color,
+ '@source' => empty($cache) ? t('actual file search') : t('cached'),
+ )
+ ),
+ );
+ $form['file_search']['remove_file_count'] = array(
+ '#type' => 'submit',
+ '#submit' => array('cache_example_form_expire_files'),
+ '#value' => t('Explicitly remove cached file count'),
+ );
+
+ $form['expiration_demo'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Cache expiration settings'),
+ );
+ $form['expiration_demo']['explanation'] = array(
+ '#markup' => t('A cache item can be set as CACHE_PERMANENT, meaning that it will only be removed when explicitly cleared, or it can have an expiration time (a Unix timestamp).'),
+ );
+ $expiring_item = cache_get('cache_example_expiring_item');
+ $item_status = $expiring_item ?
+ t('Cache item exists and is set to expire at %time', array('%time' => $expiring_item->data)) :
+ t('Cache item does not exist');
+ $form['expiration_demo']['current_status'] = array(
+ '#type' => 'item',
+ '#title' => t('Current status of cache item "cache_example_expiring_item"'),
+ '#markup' => $item_status,
+ );
+ $form['expiration_demo']['expiration'] = array(
+ '#type' => 'select',
+ '#title' => t('Time before cache expiration'),
+ '#options' => array(
+ 'never_remove' => t('CACHE_PERMANENT'),
+ -10 => t('Immediate expiration'),
+ 10 => t('10 seconds from form submission'),
+ 60 => t('1 minute from form submission'),
+ 300 => t('5 minutes from form submission'),
+ ),
+ '#default_value' => -10,
+ '#description' => t('Any cache item can be set to only expire when explicitly cleared, or to expire at a given time.'),
+ );
+ $form['expiration_demo']['create_cache_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create a cache item with this expiration'),
+ '#submit' => array('cache_example_form_create_expiring_item'),
+ );
+
+ $form['cache_clearing'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Expire and remove options'),
+ '#description' => t("We have APIs to expire cached items and also to just remove them. Unfortunately, they're all the same API, cache_clear_all"),
+ );
+ $form['cache_clearing']['cache_clear_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Type of cache clearing to do'),
+ '#options' => array(
+ 'expire' => t('Remove items from the "cache" bin that have expired'),
+ 'remove_all' => t('Remove all items from the "cache" bin regardless of expiration (super-wildcard)'),
+ 'remove_wildcard' => t('Remove all items from the "cache" bin that match the pattern "cache_example"'),
+ ),
+ '#default_value' => 'expire',
+ );
+ // Submit button to clear cached data.
+ $form['cache_clearing']['clear_expired'] = array(
+ '#type' => 'submit',
+ '#value' => t('Clear or expire cache'),
+ '#submit' => array('cache_example_form_cache_clearing'),
+ '#access' => user_access('administer site configuration'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler that explicitly clears cache_example_files_count from cache.
+ */
+function cache_example_form_expire_files($form, &$form_state) {
+ // Clear cached data. This function will delete cached object from cache bin.
+ //
+ // The first argument is cache id to be deleted. Since we've provided it
+ // explicitly, it will be removed whether or not it has an associated
+ // expiration time. The second argument (required here) is the cache bin.
+ // Using cache_clear_all() explicitly in this way
+ // forces removal of the cached item.
+ cache_clear_all('cache_example_files_count', 'cache');
+
+ // Display message to the user.
+ drupal_set_message(t('Cached data key "cache_example_files_count" was cleared.'), 'status');
+}
+
+/**
+ * Submit handler to create a new cache item with specified expiration.
+ */
+function cache_example_form_create_expiring_item($form, &$form_state) {
+ $interval = $form_state['values']['expiration'];
+ if ($interval == 'never_remove') {
+ $expiration = CACHE_PERMANENT;
+ $expiration_friendly = t('Never expires');
+ }
+ else {
+ $expiration = time() + $interval;
+ $expiration_friendly = format_date($expiration);
+ }
+ // Set the expiration to the actual Unix timestamp of the end of the required
+ // interval.
+ cache_set('cache_example_expiring_item', $expiration_friendly, 'cache', $expiration);
+ drupal_set_message(t('cache_example_expiring_item was set to expire at %time', array('%time' => $expiration_friendly)));
+}
+
+/**
+ * Submit handler to demonstrate the various uses of cache_clear_all().
+ */
+function cache_example_form_cache_clearing($form, &$form_state) {
+ switch ($form_state['values']['cache_clear_type']) {
+ case 'expire':
+ // Here we'll remove all cache keys in the 'cache' bin that have expired.
+ cache_clear_all(NULL, 'cache');
+ drupal_set_message(t('cache_clear_all(NULL, "cache") was called, removing any expired cache items.'));
+ break;
+
+ case 'remove_all':
+ // This removes all keys in a bin using a super-wildcard. This
+ // has nothing to do with expiration. It's just brute-force removal.
+ cache_clear_all('*', 'cache', TRUE);
+ drupal_set_message(t('ALL entries in the "cache" bin were removed with cache_clear_all("*", "cache", TRUE).'));
+ break;
+
+ case 'remove_wildcard':
+ // We can also explicitly remove all cache items whose cid begins with
+ // 'cache_example' by using a wildcard. This again is brute-force
+ // removal, not expiration.
+ cache_clear_all('cache_example', 'cache', TRUE);
+ drupal_set_message(t('Cache entries whose cid began with "cache_example" in the "cache" bin were removed with cache_clear_all("cache_example", "cache", TRUE).'));
+ break;
+ }
+}
+
+/**
+ * @} End of "defgroup cache_example".
+ */
diff --git a/sites/all/modules/examples/cache_example/cache_example.test b/sites/all/modules/examples/cache_example/cache_example.test
new file mode 100644
index 00000000..a445efaa
--- /dev/null
+++ b/sites/all/modules/examples/cache_example/cache_example.test
@@ -0,0 +1,81 @@
+ 'Cache example functionality',
+ 'description' => 'Test the Cache Example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable module.
+ */
+ public function setUp() {
+ parent::setUp('cache_example');
+ }
+
+ /**
+ * Functional tests for cache_example.
+ *
+ * Load cache example page and test if displaying uncached version. Reload
+ * once again and test if displaying cached version. Find reload link and
+ * click on it. Clear cache at the end and test if displaying uncached
+ * version again.
+ */
+ public function testCacheExampleBasic() {
+ // We need administrative privileges to clear the cache.
+ $admin_user = $this->drupalCreateUser(array('administer site configuration'));
+ $this->drupalLogin($admin_user);
+
+ // Get uncached output of cache example page and assert some things to be
+ // sure.
+ $this->drupalGet('examples/cache_example');
+ $this->assertText('Source: actual file search');
+ // Reload the page; the number should be cached.
+ $this->drupalGet('examples/cache_example');
+ $this->assertText('Source: cached');
+
+ // Now push the button to remove the count.
+ $this->drupalPost('examples/cache_example', array(), t('Explicitly remove cached file count'));
+ $this->assertText('Source: actual file search');
+
+ // Create a cached item. First make sure it doesn't already exist.
+ $this->assertText('Cache item does not exist');
+ $this->drupalPost('examples/cache_example', array('expiration' => -10), t('Create a cache item with this expiration'));
+ // We should now have an already-expired item.
+ $this->assertText('Cache item exists and is set to expire');
+ // Now do the expiration operation.
+ $this->drupalPost('examples/cache_example', array('cache_clear_type' => 'expire'), t('Clear or expire cache'));
+ // And verify that it was removed.
+ $this->assertText('Cache item does not exist');
+
+ // Create a cached item. This time we'll make it not expire.
+ $this->drupalPost('examples/cache_example', array('expiration' => 'never_remove'), t('Create a cache item with this expiration'));
+ // We should now have an never-remove item.
+ $this->assertText('Cache item exists and is set to expire at Never expires');
+ // Now do the expiration operation.
+ $this->drupalPost('examples/cache_example', array('cache_clear_type' => 'expire'), t('Clear or expire cache'));
+ // And verify that it was not removed.
+ $this->assertText('Cache item exists and is set to expire at Never expires');
+ // Now do full removal.
+ $this->drupalPost('examples/cache_example', array('cache_clear_type' => 'remove_wildcard'), t('Clear or expire cache'));
+ // And verify that it was removed.
+ $this->assertText('Cache item does not exist');
+ }
+
+}
diff --git a/sites/all/modules/examples/contextual_links_example/contextual-links-example-object.tpl.php b/sites/all/modules/examples/contextual_links_example/contextual-links-example-object.tpl.php
new file mode 100644
index 00000000..4a073d46
--- /dev/null
+++ b/sites/all/modules/examples/contextual_links_example/contextual-links-example-object.tpl.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/sites/all/modules/examples/contextual_links_example/contextual_links_example.info b/sites/all/modules/examples/contextual_links_example/contextual_links_example.info
new file mode 100644
index 00000000..935e0ee9
--- /dev/null
+++ b/sites/all/modules/examples/contextual_links_example/contextual_links_example.info
@@ -0,0 +1,12 @@
+name = Contextual links example
+description = Demonstrates how to use contextual links for enhancing the user experience.
+package = Example modules
+core = 7.x
+files[] = contextual_links_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/contextual_links_example/contextual_links_example.module b/sites/all/modules/examples/contextual_links_example/contextual_links_example.module
new file mode 100644
index 00000000..e80c79a7
--- /dev/null
+++ b/sites/all/modules/examples/contextual_links_example/contextual_links_example.module
@@ -0,0 +1,388 @@
+ path. If the path
+ // you are adding corresponds to a commonly performed action on the node, you
+ // can choose to expose it as a contextual link. Since the Node module
+ // already has code to display all contextual links underneath the node/
+ // path (such as "Edit" and "Delete") when a node is being rendered outside
+ // of its own page (for example, when a teaser of the node is being displayed
+ // on the front page of the site), you only need to inform Drupal's menu
+ // system that your path is a contextual link also, and it will automatically
+ // appear with the others. In the example below, we add a contextual link
+ // named "Example action" to the list.
+ $items['node/%node/example-action'] = array(
+ 'title' => 'Example action',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('contextual_links_example_node_action_form', 1),
+ 'access callback' => TRUE,
+ // To be displayed as a contextual link, a menu item should be defined as
+ // one of the node's local tasks.
+ 'type' => MENU_LOCAL_TASK,
+ // To make the local task display as a contextual link, specify the
+ // optional 'context' argument. The most common method is to set both
+ // MENU_CONTEXT_PAGE and MENU_CONTEXT_INLINE (shown below), which causes
+ // the link to display as both a tab on the node page and as an entry in
+ // the contextual links dropdown. This is recommended for most cases
+ // because not all users who have permission to visit the "Example action"
+ // page will necessarily have access to contextual links, and they still
+ // need a way to get to the page via the user interface.
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ // If we give the item a large weight, we can make it display as the last
+ // tab on the page, as well as the last item inside the contextual links
+ // dropdown.
+ 'weight' => 80,
+ );
+
+ // Second example (attaching contextual links to a block):
+ //
+ // If your module provides content that is displayed in a block, you can
+ // attach contextual links to the block that allow actions to be performed on
+ // it. This is useful for administrative pages that affect the content
+ // wherever it is displayed or used on the site. For configuration options
+ // that only affect the appearance of the content in the block itself, it is
+ // better to implement hook_block_configure() rather than creating a separate
+ // administrative page (this allows your options to appear when an
+ // administrator clicks the existing "Configure block" contextual link
+ // already provided by the Block module).
+ //
+ // In the code below, we assume that your module has a type of object
+ // ("contextual links example object") that will be displayed in a block. The
+ // code below defines menu items for this object using a standard pattern,
+ // with "View" and "Edit object" as the object's local tasks, and makes the
+ // "Edit object" item display as a contextual link in addition to a tab. Once
+ // the contextual links are defined here, additional steps are required to
+ // actually display the content in a block and attach the contextual links to
+ // the block itself. This occurs in contextual_links_example_block_info() and
+ // contextual_links_example_block_view().
+ $items['examples/contextual-links/%contextual_links_example_object'] = array(
+ 'title' => 'Contextual links example object',
+ 'page callback' => 'contextual_links_example_object_page',
+ 'page arguments' => array(2),
+ 'access callback' => TRUE,
+ );
+ $items['examples/contextual-links/%contextual_links_example_object/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['examples/contextual-links/%contextual_links_example_object/edit'] = array(
+ 'title' => 'Edit object',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('contextual_links_example_object_edit_form', 2),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ // As in our first example, this is the line of code that makes "Edit
+ // "object" display as a contextual link in addition to as a tab.
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+
+ // Third example (attaching contextual links directly to your module's
+ // content):
+ //
+ // Sometimes your module may want to display its content in an arbitrary
+ // location and attach contextual links there. For example, you might
+ // display your content in a listing on its own page and then attach the
+ // contextual links directly to each piece of content in the listing. Here,
+ // we will reuse the menu items and contextual links that were defined for
+ // our example object above, and display them in a listing in
+ // contextual_links_overview_page().
+ $items['examples/contextual-links'] = array(
+ 'title' => 'Contextual Links Example',
+ 'page callback' => 'contextual_links_overview_page',
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Menu loader callback for the object defined by this module.
+ *
+ * @param int $id
+ * The ID of the object to load.
+ *
+ * @return object|FALSE
+ * A fully loaded object, or FALSE if the object does not exist.
+ */
+function contextual_links_example_object_load($id) {
+ // In a real use case, this function might load an object from the database.
+ // For the sake of this example, we just define a stub object with a basic
+ // title and content for any numeric ID that is passed in.
+ if (is_numeric($id)) {
+ $object = new stdClass();
+ $object->id = $id;
+ $object->title = t('Title for example object @id', array('@id' => $id));
+ $object->content = t('This is the content of example object @id.', array('@id' => $id));
+ return $object;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implements hook_block_info().
+ */
+function contextual_links_example_block_info() {
+ // Define the block that will display our module's content.
+ $blocks['example']['info'] = t('Contextual links example block');
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function contextual_links_example_block_view($delta = '') {
+ if ($delta == 'example') {
+ // Display our module's content inside a block. In a real use case, we
+ // might define a new block for each object that exists. For the sake of
+ // this example, though, we only define one block and hardcode it to always
+ // display object #1.
+ $id = 1;
+ $object = contextual_links_example_object_load($id);
+ $block['subject'] = t('Contextual links example block for object @id', array('@id' => $id));
+ $block['content'] = array(
+ // In order to attach contextual links, the block's content must be a
+ // renderable array. (Normally this would involve themed output using
+ // #theme, but for simplicity we just use HTML markup directly here.)
+ '#type' => 'markup',
+ '#markup' => filter_xss($object->content),
+ // Contextual links are attached to the block array using the special
+ // #contextual_links property. The #contextual_links property contains an
+ // array, keyed by the name of each module that is attaching contextual
+ // links to it.
+ '#contextual_links' => array(
+ 'contextual_links_example' => array(
+ // Each element is itself an array, containing two elements which are
+ // combined together to form the base path whose contextual links
+ // should be attached. The two elements are split such that the first
+ // is the static part of the path and the second is the dynamic part.
+ // (This split is for performance reasons.) For example, the code
+ // below tells Drupal to load the menu item corresponding to the path
+ // "examples/contextual-links/$id" and attach all this item's
+ // contextual links (which were defined in hook_menu()) to the object
+ // when it is rendered. If the contextual links you are attaching
+ // don't have any dynamic elements in their path, you can pass an
+ // empty array as the second element.
+ 'examples/contextual-links',
+ array($id),
+ ),
+ ),
+ );
+ // Since we are attaching our contextual links to a block, and the Block
+ // module takes care of rendering the block in such a way that contextual
+ // links are supported, we do not need to do anything else here. When the
+ // appropriate conditions are met, the contextual links we have defined
+ // will automatically appear attached to the block, next to the "Configure
+ // block" link that the Block module itself provides.
+ return $block;
+ }
+}
+
+/**
+ * Menu callback; displays a listing of objects defined by this module.
+ *
+ * @see contextual_links_example_theme()
+ * @see contextual-links-example-object.tpl.php
+ * @see contextual_links_example_block_view()
+ */
+function contextual_links_overview_page() {
+ $build = array();
+
+ // For simplicity, we will hardcode this example page to list five of our
+ // module's objects.
+ for ($id = 1; $id <= 5; $id++) {
+ $object = contextual_links_example_object_load($id);
+ $build[$id] = array(
+ // To support attaching contextual links to an object that we are
+ // displaying on our own, the object must be themed in a particular way.
+ // See contextual_links_example_theme() and
+ // contextual-links-example-object.tpl.php for more discussion.
+ '#theme' => 'contextual_links_example_object',
+ '#object' => $object,
+ // Contextual links are attached to the block using the special
+ // #contextual_links property. See contextual_links_example_block_view()
+ // for discussion of the syntax used here.
+ '#contextual_links' => array(
+ 'contextual_links_example' => array(
+ 'examples/contextual-links',
+ array($id),
+ ),
+ ),
+ );
+ }
+
+ return $build;
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * @see template_preprocess_contextual_links_example_object()
+ */
+function contextual_links_example_theme() {
+ // The core Contextual Links module imposes two restrictions on how an object
+ // must be themed in order for it to display the object's contextual links in
+ // the user interface:
+ // - The object must use a template file rather than a theme function. See
+ // contextual-links-example-object.tpl.php for more information on how the
+ // template file should be structured.
+ // - The first variable passed to the template must be a renderable array. In
+ // this case, we accomplish that via the most common method, by passing a
+ // single renderable element.
+ return array(
+ 'contextual_links_example_object' => array(
+ 'template' => 'contextual-links-example-object',
+ 'render element' => 'element',
+ ),
+ );
+}
+
+/**
+ * Process variables for contextual-links-example-object.tpl.php.
+ *
+ * @see contextual_links_overview_page()
+ */
+function template_preprocess_contextual_links_example_object(&$variables) {
+ // Here we take the object that is being themed and define some useful
+ // variables that we will print in the template file.
+ $variables['title'] = filter_xss($variables['element']['#object']->title);
+ $variables['content'] = filter_xss($variables['element']['#object']->content);
+}
+
+/**
+ * Menu callback; displays an object defined by this module on its own page.
+ *
+ * @see contextual_links_overview_page()
+ */
+function contextual_links_example_object_page($object) {
+ // Here we render the object but without the #contextual_links property,
+ // since we don't want contextual links to appear when the object is already
+ // being displayed on its own page.
+ $build = array(
+ '#theme' => 'contextual_links_example_object',
+ '#object' => $object,
+ );
+
+ return $build;
+}
+
+/**
+ * Form callback; display the form for editing our module's content.
+ *
+ * @ingroup forms
+ * @see contextual_links_example_object_edit_form_submit()
+ */
+function contextual_links_example_object_edit_form($form, &$form_state, $object) {
+ $form['text'] = array(
+ '#markup' => t('This is the page that would allow you to edit object @id.', array('@id' => $object->id)),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $form['object_id'] = array(
+ '#type' => 'value',
+ '#value' => $object->id,
+ );
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for contextual_links_example_object_edit_form().
+ */
+function contextual_links_example_object_edit_form_submit($form, &$form_state) {
+ drupal_set_message(t('Object @id was edited.', array('@id' => $form_state['values']['object_id'])));
+}
+
+/**
+ * Form callback; display the form for performing an example action on a node.
+ *
+ * @ingroup forms
+ * @see contextual_links_example_node_action_form_submit()
+ */
+function contextual_links_example_node_action_form($form, &$form_state, $node) {
+ $form['text'] = array(
+ '#markup' => t('This is the page that would allow you to perform an example action on node @nid.', array('@nid' => $node->nid)),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $form['nid'] = array(
+ '#type' => 'value',
+ '#value' => $node->nid,
+ );
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for contextual_links_example_node_action_form().
+ */
+function contextual_links_example_node_action_form_submit($form, &$form_state) {
+ drupal_set_message(t('The example action was performed on node @nid.', array('@nid' => $form_state['values']['nid'])));
+}
+/**
+ * @} End of "defgroup contextual_links_example".
+ */
diff --git a/sites/all/modules/examples/contextual_links_example/contextual_links_example.test b/sites/all/modules/examples/contextual_links_example/contextual_links_example.test
new file mode 100644
index 00000000..a7abc0ed
--- /dev/null
+++ b/sites/all/modules/examples/contextual_links_example/contextual_links_example.test
@@ -0,0 +1,62 @@
+ 'Contextual links example functionality',
+ 'description' => 'Tests the behavior of the contextual links provided by the Contextual links example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('contextual', 'contextual_links_example');
+ $this->webUser = $this->drupalCreateUser(array('access contextual links', 'administer blocks'));
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Test the various contextual links that this module defines and displays.
+ */
+ public function testContextualLinksExample() {
+ // Create a node and promote it to the front page. Then view the front page
+ // and verify that the "Example action" contextual link works.
+ $node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
+ $this->drupalGet('');
+ $this->clickLink(t('Example action'));
+ $this->assertUrl('node/' . $node->nid . '/example-action', array('query' => array('destination' => 'node')));
+
+ // Visit our example overview page and click the third contextual link.
+ // This should take us to a page for editing the third object we defined.
+ $this->drupalGet('examples/contextual-links');
+ $this->clickLink('Edit object', 2);
+ $this->assertUrl('examples/contextual-links/3/edit', array('query' => array('destination' => 'examples/contextual-links')));
+
+ // Enable our module's block, go back to the front page, and click the
+ // "Edit object" contextual link that we expect to be there.
+ $edit['blocks[contextual_links_example_example][region]'] = 'sidebar_first';
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ $this->drupalGet('');
+ $this->clickLink('Edit object');
+ $this->assertUrl('examples/contextual-links/1/edit', array('query' => array('destination' => 'node')));
+ }
+}
diff --git a/sites/all/modules/examples/cron_example/cron_example.info b/sites/all/modules/examples/cron_example/cron_example.info
new file mode 100644
index 00000000..eb5d4048
--- /dev/null
+++ b/sites/all/modules/examples/cron_example/cron_example.info
@@ -0,0 +1,12 @@
+name = Cron example
+description = Demonstrates hook_cron() and related features
+package = Example modules
+core = 7.x
+files[] = cron_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/cron_example/cron_example.module b/sites/all/modules/examples/cron_example/cron_example.module
new file mode 100644
index 00000000..6e9c2bd4
--- /dev/null
+++ b/sites/all/modules/examples/cron_example/cron_example.module
@@ -0,0 +1,266 @@
+ 'Cron Example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('cron_example_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * The form to provide a link to cron.php.
+ */
+function cron_example_form($form, &$form_state) {
+ $form['status'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Cron status information'),
+ );
+ $form['status']['intro'] = array(
+ '#markup' => '
' . t('The cron example demonstrates hook_cron() and hook_cron_queue_info() processing. If you have administrative privileges you can run cron from this page and see the results.') . '
' . t('There are currently %queue_1 items in queue 1 and %queue_2 items in queue 2',
+ array(
+ '%queue_1' => $queue_1->numberOfItems(),
+ '%queue_2' => $queue_2->numberOfItems(),
+ )) . '
',
+ );
+ $form['cron_queue_setup']['num_items'] = array(
+ '#type' => 'select',
+ '#title' => t('Number of items to add to queue'),
+ '#options' => drupal_map_assoc(array(1, 5, 10, 100, 1000)),
+ '#default_value' => 5,
+ );
+ $form['cron_queue_setup']['queue'] = array(
+ '#type' => 'radios',
+ '#title' => t('Queue to add items to'),
+ '#options' => array(
+ 'cron_example_queue_1' => t('Queue 1'),
+ 'cron_example_queue_2' => t('Queue 2'),
+ ),
+ '#default_value' => 'cron_example_queue_1',
+ );
+ $form['cron_queue_setup']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add jobs to queue'),
+ '#submit' => array('cron_example_add_jobs_to_queue'),
+ );
+
+ $form['configuration'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Configuration of cron_example_cron()'),
+ );
+ $form['configuration']['cron_example_interval'] = array(
+ '#type' => 'select',
+ '#title' => t('Cron interval'),
+ '#description' => t('Time after which cron_example_cron will respond to a processing request.'),
+ '#default_value' => variable_get('cron_example_interval', 60 * 60),
+ '#options' => array(
+ 60 => t('1 minute'),
+ 300 => t('5 minutes'),
+ 3600 => t('1 hour'),
+ 60 * 60 * 24 => t('1 day'),
+ ),
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Allow user to directly execute cron, optionally forcing it.
+ */
+function cron_example_form_cron_run_submit($form, &$form_state) {
+ if (!empty($form_state['values']['cron_reset'])) {
+ variable_set('cron_example_next_execution', 0);
+ }
+
+ // We don't usually use globals in this way. This is used here only to
+ // make it easy to tell if cron was run by this form.
+ $GLOBALS['cron_example_show_status_message'] = TRUE;
+ if (drupal_cron_run()) {
+ drupal_set_message(t('Cron ran successfully.'));
+ }
+ else {
+ drupal_set_message(t('Cron run failed.'), 'error');
+ }
+}
+
+/**
+ * Submit function used to add the items to the queue.
+ */
+function cron_example_add_jobs_to_queue($form, &$form_state) {
+ $queue = $form_state['values']['queue'];
+ $num_items = $form_state['values']['num_items'];
+
+ $queue = DrupalQueue::get($queue);
+ for ($i = 1; $i <= $num_items; $i++) {
+ $item = new stdClass();
+ $item->created = time();
+ $item->sequence = $i;
+ $queue->createItem($item);
+ }
+}
+/**
+ * Implements hook_cron().
+ *
+ * hook_cron() is the traditional (pre-Drupal 7) hook for doing "background"
+ * processing. It gets called every time the Drupal cron runs and must decide
+ * what it will do.
+ *
+ * In this example, it does a watchdog() call after the time named in
+ * the variable 'cron_example_next_execution' has arrived, and then it
+ * resets that variable to a time in the future.
+ */
+function cron_example_cron() {
+ // Default to an hourly interval. Of course, cron has to be running at least
+ // hourly for this to work.
+ $interval = variable_get('cron_example_interval', 60 * 60);
+ // We usually don't want to act every time cron runs (which could be every
+ // minute) so keep a time for the next run in a variable.
+ if (time() >= variable_get('cron_example_next_execution', 0)) {
+ // This is a silly example of a cron job.
+ // It just makes it obvious that the job has run without
+ // making any changes to your database.
+ watchdog('cron_example', 'cron_example ran');
+ if (!empty($GLOBALS['cron_example_show_status_message'])) {
+ drupal_set_message(t('cron_example executed at %time', array('%time' => date_iso8601(time(0)))));
+ }
+ variable_set('cron_example_next_execution', time() + $interval);
+ }
+}
+
+
+/**
+ * Implements hook_cron_queue_info().
+ *
+ * hook_cron_queue_info() and family are new since Drupal 7, and allow any
+ * process to add work to the queue to be acted on when cron runs. Queues are
+ * described and worker callbacks are provided, and then only the worker
+ * callback needs to be implemented.
+ *
+ * All the details of queue use are done by the cron_queue implementation, so
+ * one doesn't need to know much about DrupalQueue().
+ *
+ * @see queue_example.module
+ */
+function cron_example_cron_queue_info() {
+ $queues['cron_example_queue_1'] = array(
+ 'worker callback' => 'cron_example_queue_1_worker',
+ // One second max runtime per cron run.
+ 'time' => 1,
+ );
+ $queues['cron_example_queue_2'] = array(
+ 'worker callback' => 'cron_example_queue_2_worker',
+ 'time' => 10,
+ );
+ return $queues;
+}
+
+/**
+ * Simple worker for our queues.
+ *
+ * @param object $item
+ * Any object to be worked on.
+ */
+function cron_example_queue_1_worker($item) {
+ cron_example_queue_report_work(1, $item);
+}
+
+/**
+ * Simple worker for our queues.
+ *
+ * @param object $item
+ * Any object to be worked on.
+ */
+function cron_example_queue_2_worker($item) {
+ cron_example_queue_report_work(2, $item);
+}
+
+/**
+ * Simple reporter for the workers.
+ *
+ * @param int $worker
+ * Worker number.
+ * @param object $item
+ * The $item which was stored in the cron queue.
+ */
+function cron_example_queue_report_work($worker, $item) {
+ if (!empty($GLOBALS['cron_example_show_status_message'])) {
+ drupal_set_message(
+ t('Queue @worker worker processed item with sequence @sequence created at @time',
+ array(
+ '@worker' => $worker,
+ '@sequence' => $item->sequence,
+ '@time' => date_iso8601($item->created),
+ )
+ )
+ );
+ }
+ watchdog('cron_example', 'Queue @worker worker processed item with sequence @sequence created at @time',
+ array(
+ '@worker' => $worker,
+ '@sequence' => $item->sequence,
+ '@time' => date_iso8601($item->created),
+ )
+ );
+}
+
+/**
+ * @} End of "defgroup cron_example".
+ */
diff --git a/sites/all/modules/examples/cron_example/cron_example.test b/sites/all/modules/examples/cron_example/cron_example.test
new file mode 100644
index 00000000..8dc8104e
--- /dev/null
+++ b/sites/all/modules/examples/cron_example/cron_example.test
@@ -0,0 +1,84 @@
+ 'Cron example functionality',
+ 'description' => 'Test the functionality of the Cron Example.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('cron_example');
+ // Create user. Search content permission granted for the search block to
+ // be shown.
+ $this->webUser = $this->drupalCreateUser(array('administer site configuration'));
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Test running cron through the user interface.
+ */
+ public function testCronExampleBasic() {
+ // Pretend that cron has never been run (even though simpletest seems to
+ // run it once...)
+ variable_set('cron_example_next_execution', 0);
+ $this->drupalGet('examples/cron_example');
+
+ // Initial run should cause cron_example_cron() to fire.
+ $post = array();
+ $this->drupalPost('examples/cron_example', $post, t('Run cron now'));
+ $this->assertText(t('cron_example executed at'));
+
+ // Forcing should also cause cron_example_cron() to fire.
+ $post['cron_reset'] = TRUE;
+ $this->drupalPost(NULL, $post, t('Run cron now'));
+ $this->assertText(t('cron_example executed at'));
+
+ // But if followed immediately and not forced, it should not fire.
+ $post['cron_reset'] = FALSE;
+ $this->drupalPost(NULL, $post, t('Run cron now'));
+ $this->assertNoText(t('cron_example executed at'));
+
+ $this->assertText(t('There are currently 0 items in queue 1 and 0 items in queue 2'));
+ $post = array(
+ 'num_items' => 5,
+ 'queue' => 'cron_example_queue_1',
+ );
+ $this->drupalPost(NULL, $post, t('Add jobs to queue'));
+ $this->assertText('There are currently 5 items in queue 1 and 0 items in queue 2');
+ $post = array(
+ 'num_items' => 100,
+ 'queue' => 'cron_example_queue_2',
+ );
+ $this->drupalPost(NULL, $post, t('Add jobs to queue'));
+ $this->assertText('There are currently 5 items in queue 1 and 100 items in queue 2');
+
+ $post = array();
+ $this->drupalPost('examples/cron_example', $post, t('Run cron now'));
+ $this->assertPattern('/Queue 1 worker processed item with sequence 5 /');
+ $this->assertPattern('/Queue 2 worker processed item with sequence 100 /');
+ }
+}
+
+/**
+ * @} End of "addtogroup cron_example".
+ */
diff --git a/sites/all/modules/examples/dbtng_example/dbtng_example.info b/sites/all/modules/examples/dbtng_example/dbtng_example.info
new file mode 100644
index 00000000..7accda96
--- /dev/null
+++ b/sites/all/modules/examples/dbtng_example/dbtng_example.info
@@ -0,0 +1,12 @@
+name = DBTNG example
+description = An example module showing how use the database API: DBTNG.
+package = Example modules
+core = 7.x
+files[] = dbtng_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/dbtng_example/dbtng_example.install b/sites/all/modules/examples/dbtng_example/dbtng_example.install
new file mode 100644
index 00000000..46f4a6d7
--- /dev/null
+++ b/sites/all/modules/examples/dbtng_example/dbtng_example.install
@@ -0,0 +1,102 @@
+ 'John',
+ 'surname' => 'Doe',
+ 'age' => 0,
+ );
+ db_insert('dbtng_example')
+ ->fields($fields)
+ ->execute();
+
+ // Add another entry.
+ $fields = array(
+ 'name' => 'John',
+ 'surname' => 'Roe',
+ 'age' => 100,
+ 'uid' => 1,
+ );
+ db_insert('dbtng_example')
+ ->fields($fields)
+ ->execute();
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * Defines the database tables used by this module.
+ * Remember that the easiest way to create the code for hook_schema is with
+ * the @link http://drupal.org/project/schema schema module @endlink
+ *
+ * @see hook_schema()
+ * @ingroup dbtng_example
+ */
+function dbtng_example_schema() {
+
+ $schema['dbtng_example'] = array(
+ 'description' => 'Stores example person entries for demonstration purposes.',
+ 'fields' => array(
+ 'pid' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique person ID.',
+ ),
+ 'uid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "Creator user's {users}.uid",
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Name of the person.',
+ ),
+ 'surname' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Surname of the person.',
+ ),
+ 'age' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ 'description' => 'The age of the person in years.',
+ ),
+ ),
+ 'primary key' => array('pid'),
+ 'indexes' => array(
+ 'name' => array('name'),
+ 'surname' => array('surname'),
+ 'age' => array('age'),
+ ),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/examples/dbtng_example/dbtng_example.module b/sites/all/modules/examples/dbtng_example/dbtng_example.module
new file mode 100644
index 00000000..f468dc48
--- /dev/null
+++ b/sites/all/modules/examples/dbtng_example/dbtng_example.module
@@ -0,0 +1,579 @@
+fields(array('name' => 'John', 'surname' => 'Doe'))
+ * ->execute();
+ * @endcode
+ *
+ * db_update() example:
+ * @code
+ * // UPDATE {dbtng_example} SET name = 'Jane' WHERE name = 'John'
+ * db_update('dbtng_example')
+ * ->fields(array('name' => 'Jane'))
+ * ->condition('name', 'John')
+ * ->execute();
+ * @endcode
+ *
+ * db_delete() example:
+ * @code
+ * // DELETE FROM {dbtng_example} WHERE name = 'Jane'
+ * db_delete('dbtng_example')
+ * ->condition('name', 'Jane')
+ * ->execute();
+ * @endcode
+ *
+ * See @link database Database Abstraction Layer @endlink
+ * @see db_insert()
+ * @see db_update()
+ * @see db_delete()
+ * @see drupal_write_record()
+ */
+
+/**
+ * Save an entry in the database.
+ *
+ * The underlying DBTNG function is db_insert().
+ *
+ * In Drupal 6, this would have been:
+ * @code
+ * db_query(
+ * "INSERT INTO {dbtng_example} (name, surname, age)
+ * VALUES ('%s', '%s', '%d')",
+ * $entry['name'],
+ * $entry['surname'],
+ * $entry['age']
+ * );
+ * @endcode
+ *
+ * Exception handling is shown in this example. It could be simplified
+ * without the try/catch blocks, but since an insert will throw an exception
+ * and terminate your application if the exception is not handled, it is best
+ * to employ try/catch.
+ *
+ * @param array $entry
+ * An array containing all the fields of the database record.
+ *
+ * @see db_insert()
+ */
+function dbtng_example_entry_insert($entry) {
+ $return_value = NULL;
+ try {
+ $return_value = db_insert('dbtng_example')
+ ->fields($entry)
+ ->execute();
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('db_insert failed. Message = %message, query= %query',
+ array('%message' => $e->getMessage(), '%query' => $e->query_string)), 'error');
+ }
+ return $return_value;
+}
+
+/**
+ * Update an entry in the database.
+ *
+ * The former, deprecated techniques used db_query() or drupal_write_record():
+ * @code
+ * drupal_write_record('dbtng_example', $entry, $entry['pid']);
+ * @endcode
+ *
+ * @code
+ * db_query(
+ * "UPDATE {dbtng_example}
+ * SET name = '%s', surname = '%s', age = '%d'
+ * WHERE pid = %d",
+ * $entry['pid']
+ * );
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing all the fields of the item to be updated.
+ *
+ * @see db_update()
+ */
+function dbtng_example_entry_update($entry) {
+ try {
+ // db_update()...->execute() returns the number of rows updated.
+ $count = db_update('dbtng_example')
+ ->fields($entry)
+ ->condition('pid', $entry['pid'])
+ ->execute();
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('db_update failed. Message = %message, query= %query',
+ array('%message' => $e->getMessage(), '%query' => $e->query_string)), 'error');
+ }
+ return $count;
+}
+
+/**
+ * Delete an entry from the database.
+ *
+ * The usage of db_query is deprecated except for static queries.
+ * Formerly, a deletion might have been accomplished like this:
+ * @code
+ * db_query("DELETE FROM {dbtng_example} WHERE pid = %d", $entry['pid]);
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing at least the person identifier 'pid' element of the
+ * entry to delete.
+ *
+ * @see db_delete()
+ */
+function dbtng_example_entry_delete($entry) {
+ db_delete('dbtng_example')
+ ->condition('pid', $entry['pid'])
+ ->execute();
+
+}
+
+
+/**
+ * Read from the database using a filter array.
+ *
+ * In Drupal 6, the standard function to perform reads was db_query(), and
+ * for static queries, it still is.
+ *
+ * db_query() used an SQL query with placeholders and arguments as parameters.
+ *
+ * @code
+ * // Old way
+ * $query = "SELECT * FROM {dbtng_example} n WHERE n.uid = %d AND name = '%s'";
+ * $result = db_query($query, $uid, $name);
+ * @endcode
+ *
+ * Drupal 7 DBTNG provides an abstracted interface that will work with a wide
+ * variety of database engines.
+ *
+ * db_query() is deprecated except when doing a static query. The following is
+ * perfectly acceptable in Drupal 7. See
+ * @link http://drupal.org/node/310072 the handbook page on static queries @endlink
+ *
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * db_query(
+ * "SELECT * FROM {dbtng_example} WHERE uid = :uid and name = :name",
+ * array(':uid' => 0, ':name' => 'John')
+ * )->execute();
+ * @endcode
+ *
+ * But for more dynamic queries, Drupal provides the db_select() API method, so
+ * there are several ways to perform the same SQL query. See the
+ * @link http://drupal.org/node/310075 handbook page on dynamic queries. @endlink
+ *
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->condition('uid', 0)
+ * ->condition('name', 'John')
+ * ->execute();
+ * @endcode
+ *
+ * Here is db_select with named placeholders:
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * $arguments = array(':name' => 'John', ':uid' => 0);
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->where('uid = :uid AND name = :name', $arguments)
+ * ->execute();
+ * @endcode
+ *
+ * Conditions are stacked and evaluated as AND and OR depending on the type of
+ * query. For more information, read the conditional queries handbook page at:
+ * http://drupal.org/node/310086
+ *
+ * The condition argument is an 'equal' evaluation by default, but this can be
+ * altered:
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE age > 18
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->condition('age', 18, '>')
+ * ->execute();
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing all the fields used to search the entries in the table.
+ *
+ * @return object
+ * An object containing the loaded entries if found.
+ *
+ * @see db_select()
+ * @see db_query()
+ * @see http://drupal.org/node/310072
+ * @see http://drupal.org/node/310075
+ */
+function dbtng_example_entry_load($entry = array()) {
+ // Read all fields from the dbtng_example table.
+ $select = db_select('dbtng_example', 'example');
+ $select->fields('example');
+
+ // Add each field and value as a condition to this query.
+ foreach ($entry as $field => $value) {
+ $select->condition($field, $value);
+ }
+ // Return the result in object format.
+ return $select->execute()->fetchAll();
+}
+
+/**
+ * Render a filtered list of entries in the database.
+ *
+ * DBTNG also helps processing queries that return several rows, providing the
+ * found objects in the same query execution call.
+ *
+ * This function queries the database using a JOIN between users table and the
+ * example entries, to provide the username that created the entry, and creates
+ * a table with the results, processing each row.
+ *
+ * SELECT
+ * e.pid as pid, e.name as name, e.surname as surname, e.age as age
+ * u.name as username
+ * FROM
+ * {dbtng_example} e
+ * JOIN
+ * users u ON e.uid = u.uid
+ * WHERE
+ * e.name = 'John' AND e.age > 18
+ *
+ * @see db_select()
+ * @see http://drupal.org/node/310075
+ */
+function dbtng_example_advanced_list() {
+ $output = '';
+
+ $select = db_select('dbtng_example', 'e');
+ // Join the users table, so we can get the entry creator's username.
+ $select->join('users', 'u', 'e.uid = u.uid');
+ // Select these specific fields for the output.
+ $select->addField('e', 'pid');
+ $select->addField('u', 'name', 'username');
+ $select->addField('e', 'name');
+ $select->addField('e', 'surname');
+ $select->addField('e', 'age');
+ // Filter only persons named "John".
+ $select->condition('e.name', 'John');
+ // Filter only persons older than 18 years.
+ $select->condition('e.age', 18, '>');
+ // Make sure we only get items 0-49, for scalability reasons.
+ $select->range(0, 50);
+
+ // Now, loop all these entries and show them in a table. Note that there is no
+ // db_fetch_* object or array function being called here. Also note that the
+ // following line could have been written as
+ // $entries = $select->execute()->fetchAll() which would return each selected
+ // record as an object instead of an array.
+ $entries = $select->execute()->fetchAll(PDO::FETCH_ASSOC);
+ if (!empty($entries)) {
+ $rows = array();
+ foreach ($entries as $entry) {
+ // Sanitize the data before handing it off to the theme layer.
+ $rows[] = array_map('check_plain', $entry);
+ }
+ // Make a table for them.
+ $header = array(t('Id'), t('Created by'), t('Name'), t('Surname'), t('Age'));
+ $output .= theme('table', array('header' => $header, 'rows' => $rows));
+ }
+ else {
+ drupal_set_message(t('No entries meet the filter criteria (Name = "John" and Age > 18).'));
+ }
+ return $output;
+}
+
+/**
+ * Implements hook_help().
+ *
+ * Show some help on each form provided by this module.
+ */
+function dbtng_example_help($path) {
+ $output = '';
+ switch ($path) {
+ case 'examples/dbtng':
+ $output = t('Generate a list of all entries in the database. There is no filter in the query.');
+ break;
+
+ case 'examples/dbtng/advanced':
+ $output = t('A more complex list of entries in the database.') . ' ';
+ $output .= t('Only the entries with name = "John" and age older than 18 years are shown, the username of the person who created the entry is also shown.');
+ break;
+
+ case 'examples/dbtng/update':
+ $output = t('Demonstrates a database update operation.');
+ break;
+
+ case 'examples/dbtng/add':
+ $output = t('Add an entry to the dbtng_example table.');
+ break;
+ }
+ return $output;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Set up calls to drupal_get_form() for all our example cases.
+ */
+function dbtng_example_menu() {
+ $items = array();
+
+ $items['examples/dbtng'] = array(
+ 'title' => 'DBTNG Example',
+ 'page callback' => 'dbtng_example_list',
+ 'access callback' => TRUE,
+ );
+ $items['examples/dbtng/list'] = array(
+ 'title' => 'List',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['examples/dbtng/add'] = array(
+ 'title' => 'Add entry',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('dbtng_example_form_add'),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -9,
+ );
+ $items['examples/dbtng/update'] = array(
+ 'title' => 'Update entry',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('dbtng_example_form_update'),
+ 'type' => MENU_LOCAL_TASK,
+ 'access callback' => TRUE,
+ 'weight' => -5,
+ );
+ $items['examples/dbtng/advanced'] = array(
+ 'title' => 'Advanced list',
+ 'page callback' => 'dbtng_example_advanced_list',
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ );
+
+ return $items;
+}
+
+/**
+ * Render a list of entries in the database.
+ */
+function dbtng_example_list() {
+ $output = '';
+
+ // Get all entries in the dbtng_example table.
+ if ($entries = dbtng_example_entry_load()) {
+ $rows = array();
+ foreach ($entries as $entry) {
+ // Sanitize the data before handing it off to the theme layer.
+ $rows[] = array_map('check_plain', (array) $entry);
+ }
+ // Make a table for them.
+ $header = array(t('Id'), t('uid'), t('Name'), t('Surname'), t('Age'));
+ $output .= theme('table', array('header' => $header, 'rows' => $rows));
+ }
+ else {
+ drupal_set_message(t('No entries have been added yet.'));
+ }
+ return $output;
+}
+
+/**
+ * Prepare a simple form to add an entry, with all the interesting fields.
+ */
+function dbtng_example_form_add($form, &$form_state) {
+ $form = array();
+
+ $form['add'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Add a person entry'),
+ );
+ $form['add']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#size' => 15,
+ );
+ $form['add']['surname'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Surname'),
+ '#size' => 15,
+ );
+ $form['add']['age'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Age'),
+ '#size' => 5,
+ '#description' => t("Values greater than 127 will cause an exception. Try it - it's a great example why exception handling is needed with DTBNG."),
+ );
+ $form['add']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for 'add entry' form.
+ */
+function dbtng_example_form_add_submit($form, &$form_state) {
+ global $user;
+
+ // Save the submitted entry.
+ $entry = array(
+ 'name' => $form_state['values']['name'],
+ 'surname' => $form_state['values']['surname'],
+ 'age' => $form_state['values']['age'],
+ 'uid' => $user->uid,
+ );
+ $return = dbtng_example_entry_insert($entry);
+ if ($return) {
+ drupal_set_message(t("Created entry @entry", array('@entry' => print_r($entry, TRUE))));
+ }
+}
+
+/**
+ * Sample UI to update a record.
+ */
+function dbtng_example_form_update($form, &$form_state) {
+ $form = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ $entries = dbtng_example_entry_load();
+ $keyed_entries = array();
+ if (empty($entries)) {
+ $form['no_values'] = array(
+ '#value' => t("No entries exist in the table dbtng_example table."),
+ );
+ return $form;
+ }
+
+ foreach ($entries as $entry) {
+ $options[$entry->pid] = t("@pid: @name @surname (@age)",
+ array(
+ '@pid' => $entry->pid,
+ '@name' => $entry->name,
+ '@surname' => $entry->surname,
+ '@age' => $entry->age,
+ )
+ );
+ $keyed_entries[$entry->pid] = $entry;
+ }
+ $default_entry = !empty($form_state['values']['pid']) ? $keyed_entries[$form_state['values']['pid']] : $entries[0];
+
+ $form_state['entries'] = $keyed_entries;
+
+ $form['pid'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => t('Choose entry to update'),
+ '#default_value' => $default_entry->pid,
+ '#ajax' => array(
+ 'wrapper' => 'updateform',
+ 'callback' => 'dbtng_example_form_update_callback',
+ ),
+ );
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Updated first name'),
+ '#size' => 15,
+ '#default_value' => $default_entry->name,
+ );
+
+ $form['surname'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Updated last name'),
+ '#size' => 15,
+ '#default_value' => $default_entry->surname,
+ );
+ $form['age'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Updated age'),
+ '#size' => 4,
+ '#default_value' => $default_entry->age,
+ '#description' => t("Values greater than 127 will cause an exception"),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Update'),
+ );
+ return $form;
+}
+
+/**
+ * AJAX callback handler for the pid select.
+ *
+ * When the pid changes, populates the defaults from the database in the form.
+ */
+function dbtng_example_form_update_callback($form, $form_state) {
+ $entry = $form_state['entries'][$form_state['values']['pid']];
+ // Setting the #value of items is the only way I was able to figure out
+ // to get replaced defaults on these items. #default_value will not do it
+ // and shouldn't.
+ foreach (array('name', 'surname', 'age') as $item) {
+ $form[$item]['#value'] = $entry->$item;
+ }
+ return $form;
+}
+
+/**
+ * Submit handler for 'update entry' form.
+ */
+function dbtng_example_form_update_submit($form, &$form_state) {
+ global $user;
+
+ // Save the submitted entry.
+ $entry = array(
+ 'pid' => $form_state['values']['pid'],
+ 'name' => $form_state['values']['name'],
+ 'surname' => $form_state['values']['surname'],
+ 'age' => $form_state['values']['age'],
+ 'uid' => $user->uid,
+ );
+ $count = dbtng_example_entry_update($entry);
+ drupal_set_message(t("Updated entry @entry (@count row updated)",
+ array('@count' => $count, '@entry' => print_r($entry, TRUE))));
+}
+/**
+ * @} End of "defgroup dbtng_example".
+ */
diff --git a/sites/all/modules/examples/dbtng_example/dbtng_example.test b/sites/all/modules/examples/dbtng_example/dbtng_example.test
new file mode 100644
index 00000000..6302bcee
--- /dev/null
+++ b/sites/all/modules/examples/dbtng_example/dbtng_example.test
@@ -0,0 +1,191 @@
+ 'DBTNG example unit and UI tests',
+ 'description' => 'Various unit tests on the dbtng example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('dbtng_example');
+ }
+
+ /**
+ * Test default module installation, two entries in the database table.
+ */
+ public function testInstall() {
+ $result = dbtng_example_entry_load();
+ $this->assertEqual(
+ count($result),
+ 2,
+ 'Found two entries in the table after installing the module.'
+ );
+ }
+
+
+ /**
+ * Test the UI.
+ */
+ public function testUI() {
+ // Test the basic list.
+ $this->drupalGet('examples/dbtng');
+ $this->assertPattern("/John[td\/<>\w]+Doe/", "Text 'John Doe' found in table");
+
+ // Test the add tab.
+ // Add the new entry.
+ $this->drupalPost('examples/dbtng/add',
+ array(
+ 'name' => 'Some',
+ 'surname' => 'Anonymous',
+ 'age' => 33,
+ ),
+ t('Add')
+ );
+ // Now find the new entry.
+ $this->drupalGet('examples/dbtng');
+ $this->assertPattern("/Some[td\/<>\w]+Anonymous/", "Text 'Some Anonymous' found in table");
+
+ // Try the update tab.
+ // Find out the pid of our "anonymous" guy.
+ $result = dbtng_example_entry_load(array('surname' => 'Anonymous'));
+ $this->drupalGet("examples/dbtng");
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry in the table with surname = "Anonymous".'
+ );
+ $entry = $result[0];
+ unset($entry->uid);
+ $entry->name = 'NewFirstName';
+ $this->drupalPost('examples/dbtng/update', (array) $entry, t('Update'));
+ // Now find the new entry.
+ $this->drupalGet('examples/dbtng');
+ $this->assertPattern("/NewFirstName[td\/<>\w]+Anonymous/", "Text 'NewFirstName Anonymous' found in table");
+
+ // Try the advanced tab.
+ $this->drupalGet('examples/dbtng/advanced');
+ $rows = $this->xpath("//*[@id='block-system-main']/div/table[1]/tbody/tr");
+ $this->assertEqual(count($rows), 1, "One row found in advanced view");
+ $this->assertFieldByXPath("//*[@id='block-system-main']/div/table[1]/tbody/tr/td[4]", "Roe", "Name 'Roe' Exists in advanced list");
+ }
+
+ /**
+ * Test several combinations, adding entries, updating and deleting.
+ */
+ public function testAPIExamples() {
+ // Create a new entry.
+ $entry = array(
+ 'name' => 'James',
+ 'surname' => 'Doe',
+ 'age' => 23,
+ );
+ dbtng_example_entry_insert($entry);
+
+ // Save another entry.
+ $entry = array(
+ 'name' => 'Jane',
+ 'surname' => 'NotDoe',
+ 'age' => 19,
+ );
+ dbtng_example_entry_insert($entry);
+
+ // Verify that 4 records are found in the database.
+ $result = dbtng_example_entry_load();
+ $this->assertEqual(
+ count($result),
+ 4,
+ 'Found a total of four entries in the table after creating two additional entries.'
+ );
+
+ // Verify 2 of these records have 'Doe' as surname.
+ $result = dbtng_example_entry_load(array('surname' => 'Doe'));
+ $this->assertEqual(
+ count($result),
+ 2,
+ 'Found two entries in the table with surname = "Doe".'
+ );
+
+ // Now find our not-Doe entry.
+ $result = dbtng_example_entry_load(array('surname' => 'NotDoe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry in the table with surname "NotDoe');
+ // Our NotDoe will be changed to "NowDoe".
+ $entry = $result[0];
+ $entry->surname = "NowDoe";
+ dbtng_example_entry_update((array) $entry);
+
+ $result = dbtng_example_entry_load(array('surname' => 'NowDoe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ "Found renamed 'NowDoe' surname");
+
+ // Read only John Doe entry.
+ $result = dbtng_example_entry_load(array('name' => 'John', 'surname' => 'Doe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry for John Doe.'
+ );
+ // Get the entry.
+ $entry = (array) end($result);
+ // Change age to 45
+ $entry['age'] = 45;
+ // Update entry in database.
+ dbtng_example_entry_update((array) $entry);
+
+ // Find entries with age = 45
+ // Read only John Doe entry.
+ $result = dbtng_example_entry_load(array('surname' => 'NowDoe'));
+ $this->assertEqual(
+ count($result),
+ 1,
+ 'Found one entry with surname = Nowdoe.'
+ );
+
+ // Verify it is Jane NowDoe.
+ $entry = (array) end($result);
+ $this->assertEqual(
+ $entry['name'],
+ 'Jane',
+ 'The name Jane is found in the entry'
+ );
+ $this->assertEqual(
+ $entry['surname'],
+ 'NowDoe',
+ 'The surname NowDoe is found in the entry'
+ );
+
+ // Delete the entry.
+ dbtng_example_entry_delete($entry);
+
+ // Verify that now there are only 3 records.
+ $result = dbtng_example_entry_load();
+ $this->assertEqual(
+ count($result),
+ 3,
+ 'Found only three records, a record was deleted.'
+ );
+ }
+}
diff --git a/sites/all/modules/examples/email_example/email_example.info b/sites/all/modules/examples/email_example/email_example.info
new file mode 100644
index 00000000..ef8bc013
--- /dev/null
+++ b/sites/all/modules/examples/email_example/email_example.info
@@ -0,0 +1,12 @@
+name = E-mail Example
+description = Demonstrate Drupal's e-mail APIs.
+package = Example modules
+core = 7.x
+files[] = email_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/email_example/email_example.module b/sites/all/modules/examples/email_example/email_example.module
new file mode 100644
index 00000000..6b84bc81
--- /dev/null
+++ b/sites/all/modules/examples/email_example/email_example.module
@@ -0,0 +1,212 @@
+ $message['language']->language,
+ );
+
+ switch ($key) {
+ // Send a simple message from the contact form.
+ case 'contact_message':
+ $message['subject'] = t('E-mail sent from @site-name', array('@site-name' => variable_get('site_name', 'Drupal')), $options);
+ // Note that the message body is an array, not a string.
+ $message['body'][] = t('@name sent you the following message:', array('@name' => $user->name), $options);
+ // Because this is just user-entered text, we do not need to translate it.
+ // Since user-entered text may have unintentional HTML entities in it like
+ // '<' or '>', we need to make sure these entities are properly escaped,
+ // as the body will later be transformed from HTML to text, meaning
+ // that a normal use of '<' will result in truncation of the message.
+ $message['body'][] = check_plain($params['message']);
+ break;
+ }
+}
+
+/**
+ * Sends an e-mail.
+ *
+ * @param array $form_values
+ * An array of values from the contact form fields that were submitted.
+ * There are just two relevant items: $form_values['email'] and
+ * $form_values['message'].
+ */
+function email_example_mail_send($form_values) {
+ // All system mails need to specify the module and template key (mirrored from
+ // hook_mail()) that the message they want to send comes from.
+ $module = 'email_example';
+ $key = 'contact_message';
+
+ // Specify 'to' and 'from' addresses.
+ $to = $form_values['email'];
+ $from = variable_get('site_mail', 'admin@example.com');
+
+ // "params" loads in additional context for email content completion in
+ // hook_mail(). In this case, we want to pass in the values the user entered
+ // into the form, which include the message body in $form_values['message'].
+ $params = $form_values;
+
+ // The language of the e-mail. This will one of three values:
+ // - user_preferred_language(): Used for sending mail to a particular website
+ // user, so that the mail appears in their preferred language.
+ // - global $language: Used when sending a mail back to the user currently
+ // viewing the site. This will send it in the language they're currently
+ // using.
+ // - language_default(): Used when sending mail to a pre-existing, 'neutral'
+ // address, such as the system e-mail address, or when you're unsure of the
+ // language preferences of the intended recipient.
+ //
+ // Since in our case, we are sending a message to a random e-mail address that
+ // is not necessarily tied to a user account, we will use the site's default
+ // language.
+ $language = language_default();
+
+ // Whether or not to automatically send the mail when drupal_mail() is
+ // called. This defaults to TRUE, and is normally what you want unless you
+ // need to do additional processing before drupal_mail_send() is called.
+ $send = TRUE;
+ // Send the mail, and check for success. Note that this does not guarantee
+ // message delivery; only that there were no PHP-related issues encountered
+ // while sending.
+ $result = drupal_mail($module, $key, $to, $language, $params, $from, $send);
+ if ($result['result'] == TRUE) {
+ drupal_set_message(t('Your message has been sent.'));
+ }
+ else {
+ drupal_set_message(t('There was a problem sending your message and it was not sent.'), 'error');
+ }
+
+}
+
+/**
+ * Implements hook_mail_alter().
+ *
+ * This function is not required to send an email using Drupal's mail system.
+ *
+ * Hook_mail_alter() provides an interface to alter any aspect of email sent by
+ * Drupal. You can use this hook to add a common site footer to all outgoing
+ * email, add extra header fields, and/or modify the email in anyway. HTML-izing
+ * the outgoing email is one possibility.
+ */
+function email_example_mail_alter(&$message) {
+ // For the purpose of this example, modify all the outgoing messages and
+ // attach a site signature. The signature will be translated to the language
+ // in which message was built.
+ $options = array(
+ 'langcode' => $message['language']->language,
+ );
+
+ $signature = t("\n--\nMail altered by email_example module.", array(), $options);
+ if (is_array($message['body'])) {
+ $message['body'][] = $signature;
+ }
+ else {
+ // Some modules use the body as a string, erroneously.
+ $message['body'] .= $signature;
+ }
+}
+
+/**
+ * Supporting functions.
+ */
+
+/**
+ * Implements hook_menu().
+ *
+ * Set up a page with an e-mail contact form on it.
+ */
+function email_example_menu() {
+ $items['example/email_example'] = array(
+ 'title' => 'E-mail Example: contact form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('email_example_form'),
+ 'access arguments' => array('access content'),
+ );
+
+ return $items;
+}
+
+/**
+ * The contact form.
+ */
+function email_example_form() {
+ $form['intro'] = array(
+ '#markup' => t('Use this form to send a message to an e-mail address. No spamming!'),
+ );
+ $form['email'] = array(
+ '#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#required' => TRUE,
+ );
+ $form['message'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Message'),
+ '#required' => TRUE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Form validation logic for the contact form.
+ */
+function email_example_form_validate($form, &$form_state) {
+ if (!valid_email_address($form_state['values']['email'])) {
+ form_set_error('email', t('That e-mail address is not valid.'));
+ }
+}
+
+/**
+ * Form submission logic for the contact form.
+ */
+function email_example_form_submit($form, &$form_state) {
+ email_example_mail_send($form_state['values']);
+}
+/**
+ * @} End of "defgroup email_example".
+ */
diff --git a/sites/all/modules/examples/email_example/email_example.test b/sites/all/modules/examples/email_example/email_example.test
new file mode 100644
index 00000000..f95a2cda
--- /dev/null
+++ b/sites/all/modules/examples/email_example/email_example.test
@@ -0,0 +1,103 @@
+ 'Email example',
+ 'description' => 'Verify the email submission using the contact form.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the email_example module.
+ parent::setUp('email_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testContactForm() {
+ // Create and login user.
+ $account = $this->drupalCreateUser();
+ $this->drupalLogin($account);
+
+ // Set default language for t() translations.
+ $t_options = array(
+ 'langcode' => language_default()->language,
+ );
+
+ // First try to send to an invalid email address.
+ $email_options = array(
+ 'email' => $this->randomName(),
+ 'message' => $this->randomName(128),
+ );
+ $result = $this->drupalPost('example/email_example', $email_options, t('Submit'));
+
+ // Verify that email address is invalid and email was not sent.
+ $this->assertText(t('That e-mail address is not valid.'), 'Options were validated and form submitted.');
+ $this->assertTrue(!count($this->drupalGetMails()), 'No email was sent.');
+
+ // Now try with a valid email address.
+ $email_options['email'] = $this->randomName() . '@' . $this->randomName() . '.drupal';
+ $result = $this->drupalPost('example/email_example', $email_options, t('Submit'));
+
+ // Verify that email address is valid and email was sent.
+ $this->assertTrue(count($this->drupalGetMails()), 'An email has been sent.');
+
+ // Validate sent email.
+ $email = $this->drupalGetMails();
+ // Grab the first entry.
+ $email = $email[0];
+
+ // Verify email recipient.
+ $this->assertEqual(
+ $email['to'],
+ $email_options['email'],
+ 'Email recipient successfully verified.'
+ );
+
+ // Verify email subject.
+ $this->assertEqual(
+ $email['subject'],
+ t('E-mail sent from @site-name', array('@site-name' => variable_get('site_name', 'Drupal')), $t_options),
+ 'Email subject successfully verified.'
+ );
+
+ // Verify email body.
+ $this->assertTrue(
+ strstr(
+ $email['body'],
+ t('@name sent you the following message:', array('@name' => $account->name), $t_options)
+ ),
+ 'Email body successfully verified.'
+ );
+
+ // Verify that signature is attached.
+ $this->assertTrue(
+ strstr(
+ $email['body'],
+ t("--\nMail altered by email_example module.", array(), $t_options)
+ ),
+ 'Email signature successfully verified.'
+ );
+ }
+}
diff --git a/sites/all/modules/examples/entity_example/entity_example.info b/sites/all/modules/examples/entity_example/entity_example.info
new file mode 100644
index 00000000..4dfe5654
--- /dev/null
+++ b/sites/all/modules/examples/entity_example/entity_example.info
@@ -0,0 +1,20 @@
+name = Entity Example
+description = A simple entity example showing the main steps required to set up your own entity.
+core = 7.x
+package = Example modules
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:field
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = entity_example.test
+configure = admin/structure/entity_example_basic/manage
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/entity_example/entity_example.install b/sites/all/modules/examples/entity_example/entity_example.install
new file mode 100644
index 00000000..9735dce6
--- /dev/null
+++ b/sites/all/modules/examples/entity_example/entity_example.install
@@ -0,0 +1,71 @@
+ 'The base table for our basic entity.',
+ 'fields' => array(
+ 'basic_id' => array(
+ 'description' => 'Primary key of the basic entity.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ // If we allow multiple bundles, then the schema must handle that;
+ // We'll put it in the 'bundle_type' field to avoid confusion with the
+ // entity type.
+ 'bundle_type' => array(
+ 'description' => 'The bundle type',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => TRUE,
+ ),
+ // Additional properties are just things that are common to all
+ // entities and don't require field storage.
+ 'item_description' => array(
+ 'description' => 'A description of the item',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp of the entity creation time.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('basic_id'),
+ );
+
+ return $schema;
+}
+
+
+/**
+ * Implements hook_uninstall().
+ *
+ * At uninstall time we'll notify field.module that the entity was deleted
+ * so that attached fields can be cleaned up.
+ *
+ * @ingroup entity_example
+ */
+function entity_example_uninstall() {
+ field_attach_delete_bundle('entity_example_basic', 'first_example_bundle');
+}
diff --git a/sites/all/modules/examples/entity_example/entity_example.module b/sites/all/modules/examples/entity_example/entity_example.module
new file mode 100644
index 00000000..de39adb0
--- /dev/null
+++ b/sites/all/modules/examples/entity_example/entity_example.module
@@ -0,0 +1,635 @@
+ t('Example Basic Entity'),
+
+ // The controller for our Entity, extending the Drupal core controller.
+ 'controller class' => 'EntityExampleBasicController',
+
+ // The table for this entity defined in hook_schema()
+ 'base table' => 'entity_example_basic',
+
+ // Returns the uri elements of an entity.
+ 'uri callback' => 'entity_example_basic_uri',
+
+ // IF fieldable == FALSE, we can't attach fields.
+ 'fieldable' => TRUE,
+
+ // entity_keys tells the controller what database fields are used for key
+ // functions. It is not required if we don't have bundles or revisions.
+ // Here we do not support a revision, so that entity key is omitted.
+ 'entity keys' => array(
+ // The 'id' (basic_id here) is the unique id.
+ 'id' => 'basic_id' ,
+ // Bundle will be determined by the 'bundle_type' field.
+ 'bundle' => 'bundle_type',
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'bundle_type',
+ ),
+
+ // FALSE disables caching. Caching functionality is handled by Drupal core.
+ 'static cache' => TRUE,
+
+ // Bundles are alternative groups of fields or configuration
+ // associated with a base entity type.
+ 'bundles' => array(
+ 'first_example_bundle' => array(
+ 'label' => 'First example bundle',
+ // 'admin' key is used by the Field UI to provide field and
+ // display UI pages.
+ 'admin' => array(
+ 'path' => 'admin/structure/entity_example_basic/manage',
+ 'access arguments' => array('administer entity_example_basic entities'),
+ ),
+ ),
+ ),
+ // View modes allow entities to be displayed differently based on context.
+ // As a demonstration we'll support "Tweaky", but we could have and support
+ // multiple display modes.
+ 'view modes' => array(
+ 'tweaky' => array(
+ 'label' => t('Tweaky'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ );
+
+ return $info;
+}
+
+/**
+ * Fetch a basic object.
+ *
+ * This function ends up being a shim between the menu system and
+ * entity_example_basic_load_multiple().
+ *
+ * This function gets its name from the menu system's wildcard
+ * naming conventions. For example, /path/%wildcard would end
+ * up calling wildcard_load(%wildcard value). In our case defining
+ * the path: examples/entity_example/basic/%entity_example_basic in
+ * hook_menu() tells Drupal to call entity_example_basic_load().
+ *
+ * @param int $basic_id
+ * Integer specifying the basic entity id.
+ * @param bool $reset
+ * A boolean indicating that the internal cache should be reset.
+ *
+ * @return object
+ * A fully-loaded $basic object or FALSE if it cannot be loaded.
+ *
+ * @see entity_example_basic_load_multiple()
+ * @see entity_example_menu()
+ */
+function entity_example_basic_load($basic_id = NULL, $reset = FALSE) {
+ $basic_ids = (isset($basic_id) ? array($basic_id) : array());
+ $basic = entity_example_basic_load_multiple($basic_ids, array(), $reset);
+ return $basic ? reset($basic) : FALSE;
+}
+
+/**
+ * Loads multiple basic entities.
+ *
+ * We only need to pass this request along to entity_load(), which
+ * will in turn call the load() method of our entity controller class.
+ */
+function entity_example_basic_load_multiple($basic_ids = FALSE, $conditions = array(), $reset = FALSE) {
+ return entity_load('entity_example_basic', $basic_ids, $conditions, $reset);
+}
+
+/**
+ * Implements the uri callback.
+ */
+function entity_example_basic_uri($basic) {
+ return array(
+ 'path' => 'examples/entity_example/basic/' . $basic->basic_id,
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function entity_example_menu() {
+ $items['examples/entity_example'] = array(
+ 'title' => 'Entity Example',
+ 'page callback' => 'entity_example_info_page',
+ 'access arguments' => array('view any entity_example_basic entity'),
+ );
+
+ // This provides a place for Field API to hang its own
+ // interface and has to be the same as what was defined
+ // in basic_entity_info() above.
+ $items['admin/structure/entity_example_basic/manage'] = array(
+ 'title' => 'Administer entity_example_basic entity type',
+ 'page callback' => 'entity_example_basic_list_entities',
+ 'access arguments' => array('administer entity_example_basic entities'),
+ );
+
+ // Add example entities.
+ $items['admin/structure/entity_example_basic/manage/add'] = array(
+ 'title' => 'Add an Entity Example Basic Entity',
+ 'page callback' => 'entity_example_basic_add',
+ 'access arguments' => array('create entity_example_basic entities'),
+ 'type' => MENU_LOCAL_ACTION,
+ );
+
+ // List of all entity_example_basic entities.
+ $items['admin/structure/entity_example_basic/manage/list'] = array(
+ 'title' => 'List',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ );
+
+ // The page to view our entities - needs to follow what
+ // is defined in basic_uri and will use load_basic to retrieve
+ // the necessary entity info.
+ $items['examples/entity_example/basic/%entity_example_basic'] = array(
+ 'title callback' => 'entity_example_basic_title',
+ 'title arguments' => array(3),
+ 'page callback' => 'entity_example_basic_view',
+ 'page arguments' => array(3),
+ 'access arguments' => array('view any entity_example_basic entity'),
+ );
+
+ // 'View' tab for an individual entity page.
+ $items['examples/entity_example/basic/%entity_example_basic/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ // 'Edit' tab for an individual entity page.
+ $items['examples/entity_example/basic/%entity_example_basic/edit'] = array(
+ 'title' => 'Edit',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('entity_example_basic_form', 3),
+ 'access arguments' => array('edit any entity_example_basic entity'),
+ 'type' => MENU_LOCAL_TASK,
+ );
+
+ // Add example entities.
+ $items['examples/entity_example/basic/add'] = array(
+ 'title' => 'Add an Entity Example Basic Entity',
+ 'page callback' => 'entity_example_basic_add',
+ 'access arguments' => array('create entity_example_basic entities'),
+ );
+
+ return $items;
+}
+
+/**
+ * Basic information for the page.
+ */
+function entity_example_info_page() {
+ $content['preface'] = array(
+ '#type' => 'item',
+ '#markup' => t('The entity example provides a simple example entity.'),
+ );
+ if (user_access('administer entity_example_basic entities')) {
+ $content['preface']['#markup'] = t('You can administer these and add fields and change the view !link.',
+ array('!link' => l(t('here'), 'admin/structure/entity_example_basic/manage'))
+ );
+ }
+ $content['table'] = entity_example_basic_list_entities();
+
+ return $content;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function entity_example_permission() {
+ $permissions = array(
+ 'administer entity_example_basic entities' => array(
+ 'title' => t('Administer entity_example_basic entities'),
+ ),
+ 'view any entity_example_basic entity' => array(
+ 'title' => t('View any Entity Example Basic entity'),
+ ),
+ 'edit any entity_example_basic entity' => array(
+ 'title' => t('Edit any Entity Example Basic entity'),
+ ),
+ 'create entity_example_basic entities' => array(
+ 'title' => t('Create Entity Example Basic Entities'),
+ ),
+ );
+ return $permissions;
+}
+
+/**
+ * Returns a render array with all entity_example_basic entities.
+ *
+ * In this basic example we know that there won't be many entities,
+ * so we'll just load them all for display. See pager_example.module
+ * to implement a pager. Most implementations would probably do this
+ * with the contrib Entity API module, or a view using views module,
+ * but we avoid using non-core features in the Examples project.
+ *
+ * @see pager_example.module
+ */
+function entity_example_basic_list_entities() {
+ $content = array();
+ // Load all of our entities.
+ $entities = entity_example_basic_load_multiple();
+ if (!empty($entities)) {
+ foreach ($entities as $entity) {
+ // Create tabular rows for our entities.
+ $rows[] = array(
+ 'data' => array(
+ 'id' => $entity->basic_id,
+ 'item_description' => l($entity->item_description, 'examples/entity_example/basic/' . $entity->basic_id),
+ 'bundle' => $entity->bundle_type,
+ ),
+ );
+ }
+ // Put our entities into a themed table. See theme_table() for details.
+ $content['entity_table'] = array(
+ '#theme' => 'table',
+ '#rows' => $rows,
+ '#header' => array(t('ID'), t('Item Description'), t('Bundle')),
+ );
+ }
+ else {
+ // There were no entities. Tell the user.
+ $content[] = array(
+ '#type' => 'item',
+ '#markup' => t('No entity_example_basic entities currently exist.'),
+ );
+ }
+ return $content;
+}
+
+/**
+ * Callback for a page title when this entity is displayed.
+ */
+function entity_example_basic_title($entity) {
+ return t('Entity Example Basic (item_description=@item_description)', array('@item_description' => $entity->item_description));
+}
+
+/**
+ * Menu callback to display an entity.
+ *
+ * As we load the entity for display, we're responsible for invoking a number
+ * of hooks in their proper order.
+ *
+ * @see hook_entity_prepare_view()
+ * @see hook_entity_view()
+ * @see hook_entity_view_alter()
+ */
+function entity_example_basic_view($entity, $view_mode = 'tweaky') {
+ // Our entity type, for convenience.
+ $entity_type = 'entity_example_basic';
+ // Start setting up the content.
+ $entity->content = array(
+ '#view_mode' => $view_mode,
+ );
+ // Build fields content - this is where the Field API really comes in to play.
+ // The task has very little code here because it all gets taken care of by
+ // field module.
+ // field_attach_prepare_view() lets the fields load any data they need
+ // before viewing.
+ field_attach_prepare_view($entity_type, array($entity->basic_id => $entity),
+ $view_mode);
+ // We call entity_prepare_view() so it can invoke hook_entity_prepare_view()
+ // for us.
+ entity_prepare_view($entity_type, array($entity->basic_id => $entity));
+ // Now field_attach_view() generates the content for the fields.
+ $entity->content += field_attach_view($entity_type, $entity, $view_mode);
+
+ // OK, Field API done, now we can set up some of our own data.
+ $entity->content['created'] = array(
+ '#type' => 'item',
+ '#title' => t('Created date'),
+ '#markup' => format_date($entity->created),
+ );
+ $entity->content['item_description'] = array(
+ '#type' => 'item',
+ '#title' => t('Item Description'),
+ '#markup' => $entity->item_description,
+ );
+
+ // Now to invoke some hooks. We need the language code for
+ // hook_entity_view(), so let's get that.
+ global $language;
+ $langcode = $language->language;
+ // And now invoke hook_entity_view().
+ module_invoke_all('entity_view', $entity, $entity_type, $view_mode,
+ $langcode);
+ // Now invoke hook_entity_view_alter().
+ drupal_alter(array('entity_example_basic_view', 'entity_view'),
+ $entity->content, $entity_type);
+
+ // And finally return the content.
+ return $entity->content;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ *
+ * This exposes the "extra fields" (usually properties that can be configured
+ * as if they were fields) of the entity as pseudo-fields
+ * so that they get handled by the Entity and Field core functionality.
+ * Node titles get treated in a similar manner.
+ */
+function entity_example_field_extra_fields() {
+ $form_elements['item_description'] = array(
+ 'label' => t('Item Description'),
+ 'description' => t('Item Description (an extra form field)'),
+ 'weight' => -5,
+ );
+ $display_elements['created'] = array(
+ 'label' => t('Creation date'),
+ 'description' => t('Creation date (an extra display field)'),
+ 'weight' => 0,
+ );
+ $display_elements['item_description'] = array(
+ 'label' => t('Item Description'),
+ 'description' => t('Just like title, but trying to point out that it is a separate property'),
+ 'weight' => 0,
+ );
+
+ // Since we have only one bundle type, we'll just provide the extra_fields
+ // for it here.
+ $extra_fields['entity_example_basic']['first_example_bundle']['form'] = $form_elements;
+ $extra_fields['entity_example_basic']['first_example_bundle']['display'] = $display_elements;
+
+ return $extra_fields;
+}
+
+/**
+ * Provides a wrapper on the edit form to add a new entity.
+ */
+function entity_example_basic_add() {
+ // Create a basic entity structure to be used and passed to the validation
+ // and submission functions.
+ $entity = entity_get_controller('entity_example_basic')->create();
+ return drupal_get_form('entity_example_basic_form', $entity);
+}
+
+/**
+ * Form function to create an entity_example_basic entity.
+ *
+ * The pattern is:
+ * - Set up the form for the data that is specific to your
+ * entity: the columns of your base table.
+ * - Call on the Field API to pull in the form elements
+ * for fields attached to the entity.
+ */
+function entity_example_basic_form($form, &$form_state, $entity) {
+ $form['item_description'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Item Description'),
+ '#required' => TRUE,
+ '#default_value' => $entity->item_description,
+ );
+
+ $form['basic_entity'] = array(
+ '#type' => 'value',
+ '#value' => $entity,
+ );
+
+ field_attach_form('entity_example_basic', $entity, $form, $form_state);
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#weight' => 100,
+ );
+ $form['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete'),
+ '#submit' => array('entity_example_basic_edit_delete'),
+ '#weight' => 200,
+ );
+
+ return $form;
+}
+
+
+/**
+ * Validation handler for entity_example_basic_add_form form.
+ *
+ * We pass things straight through to the Field API to handle validation
+ * of the attached fields.
+ */
+function entity_example_basic_form_validate($form, &$form_state) {
+ field_attach_form_validate('entity_example_basic', $form_state['values']['basic_entity'], $form, $form_state);
+}
+
+
+/**
+ * Form submit handler: Submits basic_add_form information.
+ */
+function entity_example_basic_form_submit($form, &$form_state) {
+ $entity = $form_state['values']['basic_entity'];
+ $entity->item_description = $form_state['values']['item_description'];
+ field_attach_submit('entity_example_basic', $entity, $form, $form_state);
+ $entity = entity_example_basic_save($entity);
+ $form_state['redirect'] = 'examples/entity_example/basic/' . $entity->basic_id;
+}
+
+/**
+ * Form deletion handler.
+ *
+ * @todo: 'Are you sure?' message.
+ */
+function entity_example_basic_edit_delete($form, &$form_state) {
+ $entity = $form_state['values']['basic_entity'];
+ entity_example_basic_delete($entity);
+ drupal_set_message(t('The entity %item_description (ID %id) has been deleted',
+ array('%item_description' => $entity->item_description, '%id' => $entity->basic_id))
+ );
+ $form_state['redirect'] = 'examples/entity_example';
+}
+
+/**
+ * We save the entity by calling the controller.
+ */
+function entity_example_basic_save(&$entity) {
+ return entity_get_controller('entity_example_basic')->save($entity);
+}
+
+/**
+ * Use the controller to delete the entity.
+ */
+function entity_example_basic_delete($entity) {
+ entity_get_controller('entity_example_basic')->delete($entity);
+}
+
+/**
+ * EntityExampleBasicControllerInterface definition.
+ *
+ * We create an interface here because anyone could come along and
+ * use hook_entity_info_alter() to change our controller class.
+ * We want to let them know what methods our class needs in order
+ * to function with the rest of the module, so here's a handy list.
+ *
+ * @see hook_entity_info_alter()
+ */
+interface EntityExampleBasicControllerInterface
+ extends DrupalEntityControllerInterface {
+
+ /**
+ * Create an entity.
+ */
+ public function create();
+
+ /**
+ * Save an entity.
+ *
+ * @param object $entity
+ * The entity to save.
+ */
+ public function save($entity);
+
+ /**
+ * Delete an entity.
+ *
+ * @param object $entity
+ * The entity to delete.
+ */
+ public function delete($entity);
+
+}
+
+/**
+ * EntityExampleBasicController extends DrupalDefaultEntityController.
+ *
+ * Our subclass of DrupalDefaultEntityController lets us add a few
+ * important create, update, and delete methods.
+ */
+class EntityExampleBasicController
+ extends DrupalDefaultEntityController
+ implements EntityExampleBasicControllerInterface {
+
+ /**
+ * Create and return a new entity_example_basic entity.
+ */
+ public function create() {
+ $entity = new stdClass();
+ $entity->type = 'entity_example_basic';
+ $entity->basic_id = 0;
+ $entity->bundle_type = 'first_example_bundle';
+ $entity->item_description = '';
+ return $entity;
+ }
+
+ /**
+ * Saves the custom fields using drupal_write_record().
+ */
+ public function save($entity) {
+ // If our entity has no basic_id, then we need to give it a
+ // time of creation.
+ if (empty($entity->basic_id)) {
+ $entity->created = time();
+ }
+ // Invoke hook_entity_presave().
+ module_invoke_all('entity_presave', $entity, 'entity_example_basic');
+ // The 'primary_keys' argument determines whether this will be an insert
+ // or an update. So if the entity already has an ID, we'll specify
+ // basic_id as the key.
+ $primary_keys = $entity->basic_id ? 'basic_id' : array();
+ // Write out the entity record.
+ drupal_write_record('entity_example_basic', $entity, $primary_keys);
+ // We're going to invoke either hook_entity_update() or
+ // hook_entity_insert(), depending on whether or not this is a
+ // new entity. We'll just store the name of hook_entity_insert()
+ // and change it if we need to.
+ $invocation = 'entity_insert';
+ // Now we need to either insert or update the fields which are
+ // attached to this entity. We use the same primary_keys logic
+ // to determine whether to update or insert, and which hook we
+ // need to invoke.
+ if (empty($primary_keys)) {
+ field_attach_insert('entity_example_basic', $entity);
+ }
+ else {
+ field_attach_update('entity_example_basic', $entity);
+ $invocation = 'entity_update';
+ }
+ // Invoke either hook_entity_update() or hook_entity_insert().
+ module_invoke_all($invocation, $entity, 'entity_example_basic');
+ return $entity;
+ }
+
+ /**
+ * Delete a single entity.
+ *
+ * Really a convenience function for deleteMultiple().
+ */
+ public function delete($entity) {
+ $this->deleteMultiple(array($entity));
+ }
+
+ /**
+ * Delete one or more entity_example_basic entities.
+ *
+ * Deletion is unfortunately not supported in the base
+ * DrupalDefaultEntityController class.
+ *
+ * @param array $entities
+ * An array of entity IDs or a single numeric ID.
+ */
+ public function deleteMultiple($entities) {
+ $basic_ids = array();
+ if (!empty($entities)) {
+ $transaction = db_transaction();
+ try {
+ foreach ($entities as $entity) {
+ // Invoke hook_entity_delete().
+ module_invoke_all('entity_delete', $entity, 'entity_example_basic');
+ field_attach_delete('entity_example_basic', $entity);
+ $basic_ids[] = $entity->basic_id;
+ }
+ db_delete('entity_example_basic')
+ ->condition('basic_id', $basic_ids, 'IN')
+ ->execute();
+ }
+ catch (Exception $e) {
+ $transaction->rollback();
+ watchdog_exception('entity_example', $e);
+ throw $e;
+ }
+ }
+ }
+}
+
+/**
+ * @} End of "defgroup entity_example".
+ */
diff --git a/sites/all/modules/examples/entity_example/entity_example.test b/sites/all/modules/examples/entity_example/entity_example.test
new file mode 100644
index 00000000..89ad1678
--- /dev/null
+++ b/sites/all/modules/examples/entity_example/entity_example.test
@@ -0,0 +1,162 @@
+ 'Entity example',
+ 'description' => 'Basic entity example tests',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('entity_example');
+
+ // Create and login user with access.
+ $permissions = array(
+ 'access content',
+ 'view any entity_example_basic entity',
+ 'edit any entity_example_basic entity',
+ 'create entity_example_basic entities',
+ 'administer entity_example_basic entities',
+ 'administer site configuration',
+ 'administer fields',
+ );
+ $account = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($account);
+
+ // Attach a field.
+ $field = array(
+ 'field_name' => 'entity_example_test_text' ,
+ 'type' => 'text',
+ );
+ field_create_field($field);
+ $instance = array(
+ 'label' => 'Subject',
+ 'field_name' => 'entity_example_test_text',
+ 'entity_type' => 'entity_example_basic',
+ 'bundle' => 'first_example_bundle',
+ );
+ field_create_instance($instance);
+ }
+
+ /**
+ * Test Entity Example features.
+ *
+ * - CRUD
+ * - Table display
+ * - User access
+ * - Field management
+ * - Display management
+ */
+ public function testEntityExampleBasic() {
+ // Create 10 entities.
+ for ($i = 1; $i <= 10; $i++) {
+ $edit[$i]['item_description'] = $this->randomName();
+ $edit[$i]['entity_example_test_text[und][0][value]'] = $this->randomName(32);
+
+ $this->drupalPost('examples/entity_example/basic/add', $edit[$i], 'Save');
+ $this->assertText('item_description=' . $edit[$i]['item_description']);
+
+ $this->drupalGet('examples/entity_example/basic/' . $i);
+ $this->assertText('item_description=' . $edit[$i]['item_description']);
+ $this->assertText($edit[$i]['entity_example_test_text[und][0][value]']);
+ }
+
+ // Delete entity 5.
+ $this->drupalPost('examples/entity_example/basic/5/edit', $edit[5], 'Delete');
+ $this->drupalGet('examples/entity_example/basic/5');
+ $this->assertResponse(404, 'Deleted entity 5 no longer exists');
+ unset($edit[5]);
+
+ // Update entity 2 and verify the update.
+ $edit[2] = array(
+ 'item_description' => 'updated entity 2 ',
+ 'entity_example_test_text[und][0][value]' => 'updated entity 2 test text',
+ );
+ $this->drupalPost('examples/entity_example/basic/2/edit', $edit[2], 'Save');
+ $this->assertText('item_description=' . $edit[2]['item_description']);
+ $this->assertText('updated entity 2 test text');
+
+ // View the entity list page and verify that the items which still exist
+ // are there, and that the deleted #5 no longer is there.
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ foreach ($edit as $id => $item) {
+ $this->assertRaw('examples/entity_example/basic/' . $id . '">' . $item['item_description'] . '');
+ }
+ $this->assertNoRaw('examples/entity_example/basic/5">');
+
+ // Add a field through the field UI and verify that it behaves correctly.
+ $field_edit = array(
+ 'fields[_add_new_field][label]' => 'New junk field',
+ 'fields[_add_new_field][field_name]' => 'new_junk_field',
+ 'fields[_add_new_field][type]' => 'text',
+ 'fields[_add_new_field][widget_type]' => 'text_textfield',
+ );
+ $this->drupalPost('admin/structure/entity_example_basic/manage/fields', $field_edit, t('Save'));
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+ $this->drupalPost(NULL, array(), t('Save settings'));
+ $this->assertResponse(200);
+
+ // Now verify that we can edit and view this entity with fields.
+ $edit[10]['field_new_junk_field[und][0][value]'] = $this->randomName();
+ $this->drupalPost('examples/entity_example/basic/10/edit', $edit[10], 'Save');
+ $this->assertResponse(200);
+ $this->assertText('item_description=' . $edit[10]['item_description']);
+ $this->assertText($edit[10]['field_new_junk_field[und][0][value]'], 'Custom field updated successfully');
+
+ // Create and login user without view access.
+ $account = $this->drupalCreateUser(array('access content'));
+ $this->drupalLogin($account);
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ $this->assertResponse(403);
+ $this->drupalGet('examples/entity_example/basic/2');
+ $this->assertResponse(403, 'User does not have permission to view entity');
+
+ // Create and login user with view access but no edit access.
+ $account = $this->drupalCreateUser(array('access content', 'view any entity_example_basic entity'));
+ $this->drupalLogin($account);
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ $this->assertResponse(403, 'Denied access to admin manage page');
+ $this->drupalGet('examples/entity_example/basic/2');
+ $this->assertResponse(200, 'User has permission to view entity');
+ $this->drupalGet('examples/entity_example/basic/2/edit');
+ $this->assertResponse(403, 'User is denied edit privileges');
+
+ // Create and login user with view and edit but no manage privs.
+ $account = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'view any entity_example_basic entity',
+ 'edit any entity_example_basic entity',
+ )
+ );
+ $this->drupalLogin($account);
+ $this->drupalGet('admin/structure/entity_example_basic/manage');
+ $this->assertResponse(403, 'Denied access to admin manage page');
+ $this->drupalGet('examples/entity_example/basic/2');
+ $this->assertResponse(200, 'User has permission to view entity');
+ $this->drupalGet('examples/entity_example/basic/2/edit');
+ $this->assertResponse(200, 'User has edit privileges');
+ }
+}
diff --git a/sites/all/modules/examples/examples.index.php b/sites/all/modules/examples/examples.index.php
new file mode 100644
index 00000000..4a905400
--- /dev/null
+++ b/sites/all/modules/examples/examples.index.php
@@ -0,0 +1,40 @@
+ array('type' => 'varchar', 'length' => 7, 'not null' => FALSE),
+ );
+ $indexes = array(
+ 'rgb' => array('rgb'),
+ );
+ return array(
+ 'columns' => $columns,
+ 'indexes' => $indexes,
+ );
+}
diff --git a/sites/all/modules/examples/field_example/field_example.js b/sites/all/modules/examples/field_example/field_example.js
new file mode 100644
index 00000000..3ea7a26a
--- /dev/null
+++ b/sites/all/modules/examples/field_example/field_example.js
@@ -0,0 +1,25 @@
+/**
+ * @file
+ * Javascript for Field Example.
+ */
+
+/**
+ * Provides a farbtastic colorpicker for the fancier widget.
+ */
+(function ($) {
+ Drupal.behaviors.field_example_colorpicker = {
+ attach: function(context) {
+ $(".edit-field-example-colorpicker").live("focus", function(event) {
+ var edit_field = this;
+ var picker = $(this).closest('div').parent().find(".field-example-colorpicker");
+
+ // Hide all color pickers except this one.
+ $(".field-example-colorpicker").hide();
+ $(picker).show();
+ $.farbtastic(picker, function(color) {
+ edit_field.value = color;
+ }).setColor(edit_field.value);
+ });
+ }
+ }
+})(jQuery);
diff --git a/sites/all/modules/examples/field_example/field_example.module b/sites/all/modules/examples/field_example/field_example.module
new file mode 100644
index 00000000..241d06f0
--- /dev/null
+++ b/sites/all/modules/examples/field_example/field_example.module
@@ -0,0 +1,389 @@
+ array(
+ 'label' => t('Example Color RGB'),
+ 'description' => t('Demonstrates a field composed of an RGB color.'),
+ 'default_widget' => 'field_example_3text',
+ 'default_formatter' => 'field_example_simple_text',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_validate().
+ *
+ * This hook gives us a chance to validate content that's in our
+ * field. We're really only interested in the $items parameter, since
+ * it holds arrays representing content in the field we've defined.
+ * We want to verify that the items only contain RGB hex values like
+ * this: #RRGGBB. If the item validates, we do nothing. If it doesn't
+ * validate, we add our own error notification to the $errors parameter.
+ *
+ * @see field_example_field_widget_error()
+ */
+function field_example_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
+ foreach ($items as $delta => $item) {
+ if (!empty($item['rgb'])) {
+ if (!preg_match('@^#[0-9a-f]{6}$@', $item['rgb'])) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'field_example_invalid',
+ 'message' => t('Color must be in the HTML format #abcdef.'),
+ );
+ }
+ }
+ }
+}
+
+
+/**
+ * Implements hook_field_is_empty().
+ *
+ * hook_field_is_empty() is where Drupal asks us if this field is empty.
+ * Return TRUE if it does not contain data, FALSE if it does. This lets
+ * the form API flag an error when required fields are empty.
+ */
+function field_example_field_is_empty($item, $field) {
+ return empty($item['rgb']);
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ *
+ * We need to tell Drupal that we have two different types of formatters
+ * for this field. One will change the text color, and the other will
+ * change the background color.
+ *
+ * @see field_example_field_formatter_view()
+ */
+function field_example_field_formatter_info() {
+ return array(
+ // This formatter just displays the hex value in the color indicated.
+ 'field_example_simple_text' => array(
+ 'label' => t('Simple text-based formatter'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ // This formatter changes the background color of the content region.
+ 'field_example_color_background' => array(
+ 'label' => t('Change the background of the output text'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * Two formatters are implemented.
+ * - field_example_simple_text just outputs markup indicating the color that
+ * was entered and uses an inline style to set the text color to that value.
+ * - field_example_color_background does the same but also changes the
+ * background color of div.region-content.
+ *
+ * @see field_example_field_formatter_info()
+ */
+function field_example_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+
+ switch ($display['type']) {
+ // This formatter simply outputs the field as text and with a color.
+ case 'field_example_simple_text':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ // We create a render array to produce the desired markup,
+ // "
The color code ... #hexcolor
".
+ // See theme_html_tag().
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#attributes' => array(
+ 'style' => 'color: ' . $item['rgb'],
+ ),
+ '#value' => t('The color code in this field is @code', array('@code' => $item['rgb'])),
+ );
+ }
+ break;
+
+ // This formatter adds css to the page changing the '.region-content' area's
+ // background color. If there are many fields, the last one will win.
+ case 'field_example_color_background':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => t('The content area color has been changed to @code', array('@code' => $item['rgb'])),
+ '#attached' => array(
+ 'css' => array(
+ array(
+ 'data' => 'div.region-content { background-color:' . $item['rgb'] . ';}',
+ 'type' => 'inline',
+ ),
+ ),
+ ),
+ );
+ }
+ break;
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * Three widgets are provided.
+ * - A simple text-only widget where the user enters the '#ffffff'.
+ * - A 3-textfield widget that gathers the red, green, and blue values
+ * separately.
+ * - A farbtastic colorpicker widget that chooses the value graphically.
+ *
+ * These widget types will eventually show up in hook_field_widget_form,
+ * where we will have to flesh them out.
+ *
+ * @see field_example_field_widget_form()
+ */
+function field_example_field_widget_info() {
+ return array(
+ 'field_example_text' => array(
+ 'label' => t('RGB value as #ffffff'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ 'field_example_3text' => array(
+ 'label' => t('RGB text field'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ 'field_example_colorpicker' => array(
+ 'label' => t('Color Picker'),
+ 'field types' => array('field_example_rgb'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * hook_widget_form() is where Drupal tells us to create form elements for
+ * our field's widget.
+ *
+ * We provide one of three different forms, depending on the widget type of
+ * the Form API item provided.
+ *
+ * The 'field_example_colorpicker' and 'field_example_text' are essentially
+ * the same, but field_example_colorpicker adds a javascript colorpicker
+ * helper.
+ *
+ * field_example_3text displays three text fields, one each for red, green,
+ * and blue. However, the field type defines a single text column,
+ * rgb, which needs an HTML color spec. Define an element validate
+ * handler that converts our r, g, and b fields into a simulated single
+ * 'rgb' form element.
+ */
+function field_example_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ $value = isset($items[$delta]['rgb']) ? $items[$delta]['rgb'] : '';
+
+ $widget = $element;
+ $widget['#delta'] = $delta;
+
+ switch ($instance['widget']['type']) {
+
+ case 'field_example_colorpicker':
+ $widget += array(
+ '#suffix' => '',
+ '#attributes' => array('class' => array('edit-field-example-colorpicker')),
+ '#attached' => array(
+ // Add Farbtastic color picker.
+ 'library' => array(
+ array('system', 'farbtastic'),
+ ),
+ // Add javascript to trigger the colorpicker.
+ 'js' => array(drupal_get_path('module', 'field_example') . '/field_example.js'),
+ ),
+ );
+ // DELIBERATE fall-through: From here on the field_example_text and
+ // field_example_colorpicker are exactly the same.
+ case 'field_example_text':
+ $widget += array(
+ '#type' => 'textfield',
+ '#default_value' => $value,
+ // Allow a slightly larger size that the field length to allow for some
+ // configurations where all characters won't fit in input field.
+ '#size' => 7,
+ '#maxlength' => 7,
+ );
+ break;
+
+ case 'field_example_3text':
+ // Convert rgb value into r, g, and b for #default_value.
+ if (!empty($value)) {
+ preg_match_all('@..@', substr($value, 1), $match);
+ }
+ else {
+ $match = array(array());
+ }
+
+ // Make this a fieldset with the three text fields.
+ $widget += array(
+ '#type' => 'fieldset',
+ '#element_validate' => array('field_example_3text_validate'),
+
+ // #delta is set so that the validation function will be able
+ // to access external value information which otherwise would be
+ // unavailable.
+ '#delta' => $delta,
+
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'field_example') . '/field_example.css'),
+ ),
+ );
+
+ // Create a textfield for saturation values for Red, Green, and Blue.
+ foreach (array('r' => t('Red'), 'g' => t('Green'), 'b' => t('Blue')) as $key => $title) {
+ $widget[$key] = array(
+ '#type' => 'textfield',
+ '#title' => $title,
+ '#size' => 2,
+ '#default_value' => array_shift($match[0]),
+ '#attributes' => array('class' => array('rgb-entry')),
+ '#description' => t('The 2-digit hexadecimal representation of @color saturation, like "a1" or "ff"', array('@color' => $title)),
+ );
+ // Since Form API doesn't allow a fieldset to be required, we
+ // have to require each field element individually.
+ if ($instance['required'] == 1) {
+ $widget[$key]['#required'] = 1;
+ }
+ }
+ break;
+
+ }
+
+ $element['rgb'] = $widget;
+ return $element;
+}
+
+
+/**
+ * Validate the individual fields and then convert to RGB string.
+ */
+function field_example_3text_validate($element, &$form_state) {
+ // @todo: Isn't there a better way to find out which element?
+ $delta = $element['#delta'];
+ $field = $form_state['field'][$element['#field_name']][$element['#language']]['field'];
+ $field_name = $field['field_name'];
+ if (isset($form_state['values'][$field_name][$element['#language']][$delta]['rgb'])) {
+ $values = $form_state['values'][$field_name][$element['#language']][$delta]['rgb'];
+ foreach (array('r', 'g', 'b') as $colorfield) {
+ $colorfield_value = hexdec($values[$colorfield]);
+ // If they left any empty, we'll set the value empty and quit.
+ if (strlen($values[$colorfield]) == 0) {
+ form_set_value($element, '', $form_state);
+ return;
+ }
+ // If they gave us anything that's not hex, reject it.
+ if ((strlen($values[$colorfield]) != 2) || $colorfield_value < 0 || $colorfield_value > 255) {
+ form_error($element[$colorfield], t("Saturation value must be a 2-digit hexadecimal value between 00 and ff."));
+ }
+ }
+
+ $value = sprintf('#%02s%02s%02s', $values['r'], $values['g'], $values['b']);
+ form_set_value($element, $value, $form_state);
+ }
+}
+
+/**
+ * Implements hook_field_widget_error().
+ *
+ * hook_field_widget_error() lets us figure out what to do with errors
+ * we might have generated in hook_field_validate(). Generally, we'll just
+ * call form_error().
+ *
+ * @see field_example_field_validate()
+ * @see form_error()
+ */
+function field_example_field_widget_error($element, $error, $form, &$form_state) {
+ switch ($error['error']) {
+ case 'field_example_invalid':
+ form_error($element, $error['message']);
+ break;
+ }
+}
+
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a simple user interface that tells the developer where to go.
+ */
+function field_example_menu() {
+ $items['examples/field_example'] = array(
+ 'title' => 'Field Example',
+ 'page callback' => '_field_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * A simple page to explain to the developer what to do.
+ */
+function _field_example_page() {
+ return t("The Field Example provides a field composed of an HTML RGB value, like #ff00ff. To use it, add the field to a content type.");
+}
+/**
+ * @} End of "defgroup field_example".
+ */
diff --git a/sites/all/modules/examples/field_example/field_example.test b/sites/all/modules/examples/field_example/field_example.test
new file mode 100644
index 00000000..6baf973d
--- /dev/null
+++ b/sites/all/modules/examples/field_example/field_example.test
@@ -0,0 +1,184 @@
+ 'Field Example',
+ 'description' => 'Create a content type with example_field_rgb fields, create a node, check for correct values.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the email_example module.
+ parent::setUp(array('field_ui', 'field_example'));
+ }
+
+ /**
+ * Test basic functionality of the example field.
+ *
+ * - Creates a content type.
+ * - Adds a single-valued field_example_rgb to it.
+ * - Adds a multivalued field_example_rgb to it.
+ * - Creates a node of the new type.
+ * - Populates the single-valued field.
+ * - Populates the multivalued field with two items.
+ * - Tests the result.
+ */
+ public function testExampleFieldBasic() {
+ $content_type_machine = strtolower($this->randomName(10));
+ $title = $this->randomName(20);
+
+ // Create and login user.
+ $account = $this->drupalCreateUser(array('administer content types', 'administer fields'));
+ $this->drupalLogin($account);
+
+ $this->drupalGet('admin/structure/types');
+
+ // Create the content type.
+ $this->clickLink(t('Add content type'));
+
+ $edit = array(
+ 'name' => $content_type_machine,
+ '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_machine)));
+
+ $single_text_field = strtolower($this->randomName(10));
+ $single_colorpicker_field = strtolower($this->randomName(10));
+ $single_3text_field = strtolower($this->randomName(10));
+ $multivalue_3text_field = strtolower($this->randomName(10));
+
+ // Description of fields to be created;
+ $fields[$single_text_field] = array(
+ 'widget' => 'field_example_text',
+ 'cardinality' => '1',
+ );
+ $fields[$single_colorpicker_field] = array(
+ 'widget' => 'field_example_colorpicker',
+ 'cardinality' => 1,
+ );
+ $fields[$single_3text_field] = array(
+ 'widget' => 'field_example_3text',
+ 'cardinality' => 1,
+ );
+ $fields[$multivalue_3text_field] = array(
+ 'widget' => 'field_example_3text',
+ 'cardinality' => -1,
+ );
+
+ foreach ($fields as $fieldname => $details) {
+ $this->createField($fieldname, $details['widget'], $details['cardinality']);
+ }
+
+ // Somehow clicking "save" isn't enough, and we have to do a
+ // node_types_rebuild().
+ node_types_rebuild();
+ 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.');
+
+ $permission = 'create ' . $content_type_machine . ' content';
+ // Reset the permissions cache.
+ $this->checkPermissions(array($permission), TRUE);
+
+ // Now that we have a new content type, create a user that has privileges
+ // on the content type.
+ $account = $this->drupalCreateUser(array($permission));
+ $this->drupalLogin($account);
+
+ $this->drupalGet('node/add/' . $content_type_machine);
+
+ // Add a node.
+ $edit = array(
+ 'title' => $title,
+ 'field_' . $single_text_field . '[und][0][rgb]' => '#000001',
+ 'field_' . $single_colorpicker_field . '[und][0][rgb]' => '#000002',
+
+ 'field_' . $single_3text_field . '[und][0][rgb][r]' => '00',
+ 'field_' . $single_3text_field . '[und][0][rgb][g]' => '00',
+ 'field_' . $single_3text_field . '[und][0][rgb][b]' => '03',
+
+ 'field_' . $multivalue_3text_field . '[und][0][rgb][r]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][0][rgb][g]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][0][rgb][b]' => '04',
+
+ );
+ // We want to add a 2nd item to the multivalue field, so hit "add another".
+ $this->drupalPost(NULL, $edit, t('Add another item'));
+
+ $edit = array(
+ 'field_' . $multivalue_3text_field . '[und][1][rgb][r]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][1][rgb][g]' => '00',
+ 'field_' . $multivalue_3text_field . '[und][1][rgb][b]' => '05',
+ );
+ // Now we can fill in the second item in the multivalue field and save.
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertText(t('@content_type_machine @title has been created', array('@content_type_machine' => $content_type_machine, '@title' => $title)));
+
+ $output_strings = $this->xpath("//div[contains(@class,'field-type-field-example-rgb')]/div/div/p/text()");
+
+ $this->assertEqual((string) $output_strings[0], "The color code in this field is #000001");
+ $this->assertEqual((string) $output_strings[1], "The color code in this field is #000002");
+ $this->assertEqual((string) $output_strings[2], "The color code in this field is #000003");
+ $this->assertEqual((string) $output_strings[3], "The color code in this field is #000004");
+ $this->assertEqual((string) $output_strings[4], "The color code in this field is #000005");
+ }
+
+ /**
+ * Utility function to create fields on a content type.
+ *
+ * @param string $field_name
+ * Name of the field, like field_something
+ * @param string $widget_type
+ * Widget type, like field_example_3text
+ * @param int $cardinality
+ * Cardinality
+ */
+ protected function createField($field_name, $widget_type, $cardinality) {
+ // Add a singleton field_example_text field.
+ $edit = array(
+ 'fields[_add_new_field][label]' => $field_name,
+ 'fields[_add_new_field][field_name]' => $field_name,
+ 'fields[_add_new_field][type]' => 'field_example_rgb',
+ 'fields[_add_new_field][widget_type]' => $widget_type,
+
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // There are no settings for this, so just press the button.
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+
+ $edit = array('field[cardinality]' => (string) $cardinality);
+
+ // Using all the default settings, so press the button.
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+ debug(
+ t('Saved settings for field %field_name with widget %widget_type and cardinality %cardinality',
+ array(
+ '%field_name' => $field_name,
+ '%widget_type' => $widget_type,
+ '%cardinality' => $cardinality,
+ )
+ )
+ );
+ $this->assertText(t('Saved @name configuration.', array('@name' => $field_name)));
+ }
+}
diff --git a/sites/all/modules/examples/field_permission_example/field_permission_example.css b/sites/all/modules/examples/field_permission_example/field_permission_example.css
new file mode 100644
index 00000000..59cda31e
--- /dev/null
+++ b/sites/all/modules/examples/field_permission_example/field_permission_example.css
@@ -0,0 +1,22 @@
+/**
+ * @file
+ * CSS for Field Example.
+ */
+.stickynote {
+background:#fefabc;
+padding:0.8em;
+font-family:cursive;
+font-size:1.1em;
+color: #000;
+width:15em;
+
+-moz-transform: rotate(2deg);
+-webkit-transform: rotate(2deg);
+-o-transform: rotate(2deg);
+-ms-transform: rotate(2deg);
+transform: rotate(2deg);
+
+box-shadow: 0px 4px 6px #333;
+-moz-box-shadow: 0px 4px 6px #333;
+-webkit-box-shadow: 0px 4px 6px #333;
+}
diff --git a/sites/all/modules/examples/field_permission_example/field_permission_example.info b/sites/all/modules/examples/field_permission_example/field_permission_example.info
new file mode 100644
index 00000000..c91a14d7
--- /dev/null
+++ b/sites/all/modules/examples/field_permission_example/field_permission_example.info
@@ -0,0 +1,12 @@
+name = Field Permission Example
+description = A Field API Example: Fieldnote with Permissions
+package = Example modules
+core = 7.x
+files[] = tests/field_permission_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/field_permission_example/field_permission_example.install b/sites/all/modules/examples/field_permission_example/field_permission_example.install
new file mode 100644
index 00000000..55388d20
--- /dev/null
+++ b/sites/all/modules/examples/field_permission_example/field_permission_example.install
@@ -0,0 +1,32 @@
+ array('type' => 'text', 'size' => 'normal', 'not null' => FALSE),
+ );
+ return array(
+ 'columns' => $columns,
+ );
+}
diff --git a/sites/all/modules/examples/field_permission_example/field_permission_example.module b/sites/all/modules/examples/field_permission_example/field_permission_example.module
new file mode 100644
index 00000000..a1a96139
--- /dev/null
+++ b/sites/all/modules/examples/field_permission_example/field_permission_example.module
@@ -0,0 +1,329 @@
+ t('View own fieldnote'));
+ $perms['edit own fieldnote'] = array('title' => t('Edit own fieldnote'));
+ $perms['view any fieldnote'] = array('title' => t('View any fieldnote'));
+ $perms['edit any fieldnote'] = array('title' => t('Edit any fieldnote'));
+
+ return $perms;
+}
+
+/**
+ * Implements hook_field_access().
+ *
+ * We want to make sure that fields aren't being seen or edited
+ * by those who shouldn't.
+ *
+ * We have to build a permission string similar to those in
+ * hook_permission() in order to ask Drupal whether the user
+ * has that permission. Permission strings will end up being
+ * like 'view any fieldnote' or 'edit own fieldnote'.
+ *
+ * The tricky thing here is that a field can be attached to any type
+ * of entity, so it's not always trivial to figure out whether
+ * $account 'owns' the entity. We'll support access restrictions for
+ * user and node entity types, and be permissive with others,
+ * since that's easy to demonstrate.
+ *
+ * @see field_permission_example_permissions()
+ */
+function field_permission_example_field_access($op, $field, $entity_type, $entity, $account) {
+ // This hook will be invoked for every field type, so we have to
+ // check that it's the one we're interested in.
+ if ($field['type'] == 'field_permission_example_fieldnote') {
+ // First we'll check if the user has the 'superuser'
+ // permissions that node provides. This way administrators
+ // will be able to administer the content types.
+ if (user_access('bypass node access', $account)) {
+ drupal_set_message(t('User can bypass node access.'));
+ return TRUE;
+ }
+ if (user_access('administer content types', $account)) {
+ drupal_set_message(t('User can administer content types.'));
+ return TRUE;
+ }
+ // Now check for our own permissions.
+ // $context will end up being either 'any' or 'own.'
+ $context = 'any';
+ switch ($entity_type) {
+ case 'user':
+ case 'node':
+ // While administering the field itself, $entity will be
+ // NULL, so we have to check it.
+ if ($entity) {
+ if ($entity->uid == $account->uid) {
+ $context = 'own';
+ }
+ }
+ }
+ // Assemble a permission string, such as
+ // 'view any fieldnote'
+ $permission = $op . ' ' . $context . ' fieldnote';
+ // Finally, ask Drupal if this account has that permission.
+ $access = user_access($permission, $account);
+ $status = 'FALSE';
+ if ($access) {
+ $status = 'TRUE';
+ }
+ drupal_set_message($permission . ': ' . $status);
+ return $access;
+ }
+ // We have no opinion on field types other than our own.
+ return TRUE;
+}
+
+/**
+ * Implements hook_field_info().
+ *
+ * Provides the description of the field.
+ */
+function field_permission_example_field_info() {
+ return array(
+ // We name our field as the associative name of the array.
+ 'field_permission_example_fieldnote' => array(
+ 'label' => t('Fieldnote'),
+ 'description' => t('Place a note-taking field on entities, with granular permissions.'),
+ 'default_widget' => 'field_permission_example_widget',
+ 'default_formatter' => 'field_permission_example_formatter',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_is_empty().
+ *
+ * hook_field_is_empty() is where Drupal asks us if this field is empty.
+ * Return TRUE if it does not contain data, FALSE if it does. This lets
+ * the form API flag an error when required fields are empty.
+ */
+function field_permission_example_field_is_empty($item, $field) {
+ return empty($item['notes']);
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ *
+ * We need to tell Drupal about our excellent field formatter.
+ *
+ * It's some text in a div, styled to look like a sticky note.
+ *
+ * @see field_permission_example_field_formatter_view()
+ */
+function field_permission_example_field_formatter_info() {
+ return array(
+ // This formatter simply displays the text in a text field.
+ 'field_permission_example_formatter' => array(
+ 'label' => t('Simple text-based formatter'),
+ 'field types' => array('field_permission_example_fieldnote'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * Here we output the field for general consumption.
+ *
+ * The field will have a sticky note appearance, thanks to some
+ * simple CSS.
+ *
+ * Note that all of the permissions and access logic happens
+ * in hook_field_access(), and none of it is here.
+ */
+function field_permission_example_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+
+ switch ($display['type']) {
+ case 'field_permission_example_formatter':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ // We wrap the fieldnote content up in a div tag.
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ '#value' => check_plain($item['notes']),
+ // Let's give the note a nice sticky-note CSS appearance.
+ '#attributes' => array(
+ 'class' => 'stickynote',
+ ),
+ // ..And this is the CSS for the stickynote.
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'field_permission_example') .
+ '/field_permission_example.css'),
+ ),
+ );
+ }
+ break;
+ }
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * We're implementing just one widget: A basic textarea.
+ *
+ * @see field_permission_example_field_widget_form()
+ */
+function field_permission_example_field_widget_info() {
+ return array(
+ 'field_permission_example_widget' => array(
+ 'label' => t('Field note textarea'),
+ 'field types' => array('field_permission_example_fieldnote'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * Drupal wants us to create a form for our field. We'll use
+ * something very basic like a default textarea.
+ *
+ * @see field_permission_example_field_widget_info()
+ */
+function field_permission_example_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ // Grab the existing value for the field.
+ $value = isset($items[$delta]['notes']) ? $items[$delta]['notes'] : '';
+ // Grab a reference to the form element.
+ $widget = $element;
+ // Set up the delta for our return element.
+ $widget['#delta'] = $delta;
+
+ // Figure out which widget we need to generate.
+ // In our case, there's only one type.
+ switch ($instance['widget']['type']) {
+ case 'field_permission_example_widget':
+ $widget += array(
+ '#type' => 'textarea',
+ '#default_value' => $value,
+ );
+ break;
+ }
+
+ $element['notes'] = $widget;
+ return $element;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a simple user interface that gives the developer some clues.
+ */
+function field_permission_example_menu() {
+ $items['examples/field_permission_example'] = array(
+ 'title' => 'Field Permission Example',
+ 'page callback' => '_field_permission_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * A simple page to explain to the developer what to do.
+ *
+ * @see field_permission_example.module
+ */
+function _field_permission_example_page() {
+ $page = t("
The Field Permission Example module shows how you can restrict view and edit permissions within your field implementation. It adds a new field type called Fieldnote. Fieldnotes appear as simple text boxes on the create/edit form, and as sticky notes when viewed. By 'sticky note' we mean 'Post-It Note' but that's a trademarked term.
To see this field in action, add it to a content type or user profile. Go to the permissions page (");
+ $page .= l(t('admin/people/permissions'), 'admin/people/permissions');
+ $page .= t(") and look at the 'Field Permission Example' section. This allows you to change which roles can see and edit Fieldnote fields.
Creating different users with different capabilities will let you see these behaviors in action. Fieldnote helpfully displays a message telling you which permissions it is trying to resolve for the current field/user combination.
Definitely look through the code to see various implementation details.
");
+ return $page;
+}
+/**
+ * @} End of "defgroup field_permission_example".
+ */
diff --git a/sites/all/modules/examples/field_permission_example/tests/field_permission_example.test b/sites/all/modules/examples/field_permission_example/tests/field_permission_example.test
new file mode 100644
index 00000000..7e6c9b71
--- /dev/null
+++ b/sites/all/modules/examples/field_permission_example/tests/field_permission_example.test
@@ -0,0 +1,572 @@
+ 'Generic Field Test',
+ 'description' => 'Someone neglected to override GenericFieldTest::getInfo().',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Supply the field types we wish to test.
+ *
+ * Return an array of field types to instantiate and test.
+ *
+ * @return array
+ * The field types we wish to use.
+ */
+ protected function getFieldTypes() {
+ return array('these_are_not', 'valid_field_types', 'please_override');
+ }
+
+ /**
+ * The module to enable.
+ *
+ * @return string
+ * Module machine name.
+ */
+ protected function getModule() {
+ return 'this-is-not-a-module-name-please-override';
+ }
+
+ /**
+ * Simpletest's setUp().
+ *
+ * We want to be able to subclass this class, so we jump
+ * through a few hoops in order to get the modules from args
+ * and add our own.
+ */
+ public function setUp() {
+ $this->instanceNames = array();
+ $modules = func_get_args();
+ if (isset($modules[0]) && is_array($modules[0])) {
+ $modules = $modules[0];
+ }
+ $modules[] = 'node';
+ $modules[] = 'field_ui';
+ parent::setUp($modules);
+ }
+
+ /**
+ * Verify that all required fields are specified in hook_field_info().
+ *
+ * The full list is label, description, settings, instance_settings,
+ * default_widget, default_formatter, no_ui.
+ *
+ * Some are optional, and we won't check for those.
+ *
+ * In a sane world, this would be a unit test, rather than a
+ * web test, but module_implements is unavailable to us
+ * in unit tests.
+ *
+ * @see hook_field_info()
+ */
+ public function runTestGenericFieldInfo() {
+ $field_types = $this->getFieldTypes();
+ $module = $this->getModule();
+ $info_keys = array(
+ 'label',
+ 'description',
+ 'default_widget',
+ 'default_formatter',
+ );
+ // We don't want to use field_info_field_types()
+ // because there is a hook_field_info_alter().
+ // We're testing the module here, not the rest of
+ // the system. So invoke hook_field_info() ourselves.
+ $modules = module_implements('field_info');
+ $this->assertTrue(in_array($module, $modules),
+ 'Module ' . $module . ' implements hook_field_info()');
+
+ foreach ($field_types as $field_type) {
+ $field_info = module_invoke($module, 'field_info');
+ $this->assertTrue(isset($field_info[$field_type]),
+ 'Module ' . $module . ' defines field type ' . $field_type);
+ $field_info = $field_info[$field_type];
+ foreach ($info_keys as $key) {
+ $this->assertTrue(
+ isset($field_info[$key]),
+ $field_type . "'s " . $key . ' is set.'
+ );
+ }
+ }
+ }
+
+ /**
+ * Add all testable fields as instances to a content type.
+ *
+ * As a side-effect: Store the names of the instances created
+ * in $this->$instance_names.
+ *
+ * @param object $node_type
+ * A content type object. If none is provided, one will be generated.
+ *
+ * @return object
+ * The content type object that has the fields attached.
+ */
+ public function codeTestGenericAddAllFields($node_type = NULL) {
+ $this->instanceNames = array();
+ if (!$node_type) {
+ $node_type = $this->drupalCreateContentType();
+ }
+ foreach ($this->getFieldTypes() as $field_type) {
+ $instance_name = drupal_strtolower($this->randomName(32));
+ $field = array(
+ 'field_name' => $instance_name,
+ 'type' => $field_type,
+ );
+ $field = field_create_field($field);
+ $instance = array(
+ 'field_name' => $instance_name,
+ 'entity_type' => 'node',
+ 'bundle' => $node_type->name,
+ 'label' => drupal_strtolower($this->randomName(20)),
+ );
+ // Finally create the instance.
+ $instance = field_create_instance($instance);
+ // Reset the caches...
+ _field_info_collate_fields(TRUE);
+ // Grab this instance.
+ $verify_instance = field_info_instance('node', $instance_name, $node_type->name);
+ $this->assertTrue($verify_instance, 'Instance object exists.');
+ $this->assertTrue(
+ $verify_instance != NULL,
+ 'field_info_instance() says ' . $instance_name . ' (' . $node_type->name . ') was created.'
+ );
+ $this->instanceNames[] = $instance_name;
+ }
+ return $node_type;
+ }
+
+ /**
+ * Remove all fields in $this->field_names.
+ *
+ * @param mixed $node_type
+ * A content type object. If none is specified,
+ * the test fails.
+ */
+ public function codeTestGenericRemoveAllFields($node_type = NULL) {
+ if (!$node_type) {
+ $this->fail('No node type.');
+ }
+ if (count($this->instanceNames) < 1) {
+ $this->fail('There are no instances to remove.');
+ return;
+ }
+ foreach ($this->instanceNames as $instance_name) {
+ $instance = field_info_instance('node', $instance_name, $node_type->name);
+ $this->assertTrue($instance, "Instance exists, now we'll delete it.");
+ field_delete_field($instance_name);
+ $instance = field_info_instance('node', $instance_name, $node_type->name);
+ $this->assertFalse($instance, 'Instance was deleted.');
+ }
+ $this->instanceNames = array();
+ }
+
+ /**
+ * Add and delete all field types through Form API.
+ *
+ * @access public
+ */
+ public function formTestGenericFieldNodeAddDeleteForm() {
+ // Create and login user.
+ $account = $this->drupalCreateUser(array(
+ 'administer content types',
+ 'administer fields',
+ ));
+ $this->drupalLogin($account);
+
+ // Add a content type.
+ $node_type = $this->drupalCreateContentType();
+
+ // Add all our testable fields.
+ $field_names = $this->formAddAllFields($node_type);
+
+ // Now let's delete all the fields.
+ foreach ($field_names as $field_name) {
+ // This is the path for the 'delete' link on field admin page.
+ $this->drupalGet('admin/structure/types/manage/' .
+ $node_type->name . '/fields/field_' . $field_name . '/delete');
+ // Click the 'delete' button.
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->assertText(t('The field @field has been deleted from the @type content type.',
+ array('@field' => $field_name, '@type' => $node_type->name)));
+ }
+ }
+
+ /**
+ * Add all fields using Form API.
+ *
+ * @param mixed $node_type
+ * A content type object. If none is specified,
+ * the test fails.
+ */
+ protected function formAddAllFields($node_type = NULL) {
+ if (!$node_type) {
+ $this->fail('No content type specified.');
+ }
+ // Get all our field types.
+ $field_types = $this->getFieldTypes();
+ // Keep a list of no_ui fields so we can tell the user.
+ $unsafe_field_types = array();
+ $field_names = array();
+
+ $manage_path = 'admin/structure/types/manage/' . $node_type->name . '/fields';
+ foreach ($field_types as $field_type) {
+ // Get the field info.
+ $field_info = field_info_field_types($field_type);
+ // Exclude no_ui field types.
+ if (isset($field_info['no_ui']) && $field_info['no_ui']) {
+ $unsafe_field_types[] = $field_type;
+ }
+ else {
+ // Generate a name for our field.
+ // 26 is max length for field name.
+ $field_name = drupal_strtolower($this->randomName(26));
+ $field_names[$field_type] = $field_name;
+ // Create the field through Form API.
+ $this->formCreateField($manage_path, $field_type, $field_name,
+ $field_info['default_widget'], 1);
+ }
+ }
+
+ // Tell the user which fields we couldn't test.
+ if (!empty($unsafe_field_types)) {
+ debug(
+ 'Unable to attach these no_ui fields: ' .
+ implode(', ', $unsafe_field_types)
+ );
+ }
+
+ // Somehow clicking "save" isn't enough, and we have to
+ // rebuild a few caches.
+ node_types_rebuild();
+ menu_rebuild();
+ return $field_names;
+ }
+
+ /**
+ * Create a field using the content type management form.
+ *
+ * @param mixed $manage_path
+ * Path to our content type management form.
+ * @param mixed $field_type
+ * The type of field we're adding.
+ * @param mixed $field_name
+ * The name of the field instance we want.
+ * @param mixed $widget_type
+ * Which widget would we like?
+ * @param mixed $cardinality
+ * Cardinality for this field instance.
+ */
+ protected function formCreateField($manage_path, $field_type, $field_name, $widget_type, $cardinality) {
+ // $manage_path is the field edit form for our content type.
+ $this->drupalGet($manage_path);
+ $edit = array(
+ 'fields[_add_new_field][label]' => $field_name,
+ 'fields[_add_new_field][field_name]' => $field_name,
+ 'fields[_add_new_field][type]' => $field_type,
+ 'fields[_add_new_field][widget_type]' => $widget_type,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // Assume there are no settings for this,
+ // so just press the button.
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+
+ $edit = array('field[cardinality]' => (string) $cardinality);
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+
+ debug(
+ t('Saved settings for field !field_name with widget !widget_type and cardinality !cardinality',
+ array(
+ '!field_name' => $field_name,
+ '!widget_type' => $widget_type,
+ '!cardinality' => $cardinality,
+ )
+ )
+ );
+
+ $this->assertText(t('Saved @name configuration.', array('@name' => $field_name)));
+ }
+
+ /**
+ * Create a node with some field content.
+ *
+ * @return object
+ * Node object for the created node.
+ */
+ public function createFieldContentForUser(
+ $account = NULL,
+ $content = 'testable_content',
+ $node_type = NULL,
+ $instance_name = '',
+ $column = NULL
+ ) {
+ if (!$column) {
+ $this->fail('No column name given.');
+ return NULL;
+ }
+ if (!$account) {
+ $account = $this->drupalCreateUser(array(
+ 'bypass node access',
+ 'administer content types',
+ ));
+ }
+ $this->drupalLogin($account);
+
+ if (!$node_type) {
+ $node_type = $this->codeTestGenericAddAllFields();
+ }
+
+ if (!$instance_name) {
+ $instance_name = reset($this->instanceNames);
+ }
+ $field = array();
+ $field[LANGUAGE_NONE][0][$column] = $content;
+
+ $settings = array(
+ 'type' => $node_type->name,
+ $instance_name => $field,
+ );
+ $node = $this->drupalCreateNode($settings);
+
+ $this->assertTrue($node, 'Node of type ' . $node->type . ' allegedly created.');
+
+ $node = node_load($node->nid);
+ debug('Loaded node id: ' . $node->nid);
+ $this->assertTrue($node->$instance_name, 'Field actually created.');
+ $field = $node->$instance_name;
+ $this->assertTrue($field[LANGUAGE_NONE][0][$column] == $content,
+ 'Content was stored properly on the field.');
+ return $node;
+ }
+
+}
+
+class FieldTestPermissionsExample extends GenericFieldTest {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp(array('field_permission_example'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field Permission Example',
+ 'description' => 'Various tests on the functionality of the Fieldnote field.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getFieldTypes() {
+ return array('field_permission_example_fieldnote');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getModule() {
+ return 'field_permission_example';
+ }
+
+ /**
+ * Override createFieldContentForUser().
+ *
+ * We override so we can make sure $column is set to 'notes'.
+ */
+ public function createFieldContentForUser(
+ $account = NULL,
+ $content = 'fieldnote_testable_content',
+ $node_type = NULL,
+ $instance_name = '',
+ $column = 'notes'
+ ) {
+ return parent::createFieldContentForUser($account, $content, $node_type, $instance_name, $column);
+ }
+
+
+ /**
+ * Test of hook_field_info() and other implementation requirements.
+ *
+ * @see GenericFieldTest::runTestGenericFieldInfo()
+ */
+ public function testFieldnoteInfo() {
+ $this->runTestGenericFieldInfo();
+ }
+
+ /**
+ * Add and remove the field through Form API.
+ */
+ public function testAddRemoveFieldnoteForm() {
+ $this->formTestGenericFieldNodeAddDeleteForm();
+ }
+
+ /**
+ * Add and remove the field through code.
+ */
+ public function testAddRemoveFieldnoteCode() {
+ $node_type = $this->codeTestGenericAddAllFields();
+ $this->codeTestGenericRemoveAllFields($node_type);
+ }
+
+ /**
+ * Test view permissions.
+ */
+ public function testFieldnoteViewPerms() {
+ // We create two sets of content so we can get a few
+ // test cases out of the way.
+ $view_own_content = $this->randomName(23);
+ $view_any_content = $this->randomName(23);
+ $view_own_node = $this->createFieldContentForUser(NULL, $view_own_content);
+ // Get the type of the node so we can create another one.
+ $node_type = node_type_load($view_own_node->type);
+ $view_any_node = $this->createFieldContentForUser(NULL, $view_any_content, $node_type);
+
+ // There should be a node now, with some lovely content, but it's the wrong
+ // user for the view-own test.
+ $view_own_account = $this->drupalCreateUser(array(
+ 'view own fieldnote',
+ ));
+ debug("Created user with 'view own fieldnote' permission.");
+
+ // Now change the user id for the test node.
+ $view_own_node = node_load($view_own_node->nid);
+ $view_own_node->uid = $view_own_account->uid;
+ node_save($view_own_node);
+ $view_own_node = node_load($view_own_node->nid);
+ $this->assertTrue($view_own_node->uid == $view_own_account->uid, 'New user assigned to node.');
+
+ // Now we want to look at the page with the field and
+ // check that we can see it.
+ $this->drupalLogin($view_own_account);
+
+ $this->drupalGet('node/' . $view_own_node->nid);
+ // Check that the field content is present.
+ $output_strings = $this->xpath("//div[contains(@class,'stickynote')]/text()");
+ $this->assertEqual((string) reset($output_strings), $view_own_content);
+ debug("'view own fieldnote' can view own field.");
+
+ // This account shouldn't be able to see the field on the
+ // 'view any' node.
+ $this->drupalGet('node/' . $view_any_node->nid);
+ // Check that the field content is not present.
+ $output_strings = $this->xpath("//div[contains(@class,'stickynote')]/text()");
+ $this->assertNotEqual((string) reset($output_strings), $view_any_content);
+ debug("'view own fieldnote' cannot view other field.");
+
+ // Now, to test for 'view any fieldnote' we create another user
+ // with that permission, and try to look at the same node.
+ $view_any_account = $this->drupalCreateUser(array(
+ 'view any fieldnote',
+ ));
+ debug("Created user with 'view any fieldnote' permission.");
+ $this->drupalLogin($view_any_account);
+ // This account should be able to see the field on the
+ // 'view any' node.
+ $this->drupalGet('node/' . $view_any_node->nid);
+ // Check that the field content is present.
+ $output_strings = $this->xpath("//div[contains(@class,'stickynote')]/text()");
+ $this->assertEqual((string) reset($output_strings), $view_any_content);
+ debug("'view any fieldnote' can view other field.");
+ }
+
+ /**
+ * Test edit permissions.
+ *
+ * Note that this is mostly identical to testFieldnoteViewPerms() and could
+ * probably be refactored.
+ */
+ public function testFieldnoteEditPerms() {
+ // We create two sets of content so we can get a few
+ // test cases out of the way.
+ $edit_own_content = $this->randomName(23);
+ $edit_any_content = $this->randomName(23);
+ $edit_own_node = $this->createFieldContentForUser(NULL, $edit_own_content);
+ // Get the type of the node so we can create another one.
+ $node_type = node_type_load($edit_own_node->type);
+ $edit_any_node = $this->createFieldContentForUser(NULL, $edit_any_content, $node_type);
+
+ $edit_own_account = $this->drupalCreateUser(array(
+ 'edit own ' . $node_type->name . ' content',
+ 'edit own fieldnote',
+ ));
+ debug("Created user with 'edit own fieldnote' permission.");
+
+ // Now change the user id for the test node.
+ $edit_own_node = node_load($edit_own_node->nid);
+ $edit_own_node->uid = $edit_own_account->uid;
+ node_save($edit_own_node);
+ $edit_own_node = node_load($edit_own_node->nid);
+ $this->assertTrue($edit_own_node->uid == $edit_own_account->uid, 'New edit test user assigned to node.');
+
+ // Now we want to look at the page with the field and
+ // check that we can see it.
+ $this->drupalLogin($edit_own_account);
+
+ $this->drupalGet('node/' . $edit_own_node->nid . '/edit');
+ $this->assertText($edit_own_content, "'edit own fieldnote' can edit own fieldnote.");
+
+ // This account shouldn't be able to edit the field on the
+ // 'edit any' node.
+ $this->drupalGet('node/' . $edit_any_node->nid . '/edit');
+ $this->assertNoText($edit_any_content, "'edit own fieldnote' can not edit any fieldnote.");
+
+ // Now, to test for 'edit any fieldnote' we create another user
+ // with that permission, and try to edit at the same node.
+ // We have to add the ability to edit any node content, as well
+ // or Drupal will deny us access to the page.
+ $edit_any_account = $this->drupalCreateUser(array(
+ 'edit any ' . $node_type->name . ' content',
+ 'edit any fieldnote',
+ ));
+ debug("Created user with 'edit any fieldnote' permission.");
+ $this->drupalLogin($edit_any_account);
+ // This account should be able to see the field on the
+ // 'edit any' node.
+ $this->drupalGet('node/' . $edit_any_node->nid . '/edit');
+ $this->assertText($edit_any_content, "'edit any fieldnote' can edit any fieldnote.");
+ }
+
+}
diff --git a/sites/all/modules/examples/file_example/file_example.info b/sites/all/modules/examples/file_example/file_example.info
new file mode 100644
index 00000000..e36d70a2
--- /dev/null
+++ b/sites/all/modules/examples/file_example/file_example.info
@@ -0,0 +1,13 @@
+name = File example
+description = Examples of using the Drupal File API and Stream Wrappers.
+package = Example modules
+core = 7.x
+files[] = file_example_session_streams.inc
+files[] = file_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/file_example/file_example.module b/sites/all/modules/examples/file_example/file_example.module
new file mode 100644
index 00000000..c08bd746
--- /dev/null
+++ b/sites/all/modules/examples/file_example/file_example.module
@@ -0,0 +1,570 @@
+ 'File Example',
+ 'page callback' => 'file_example_intro',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+ $items['examples/file_example/fileapi'] = array(
+ 'title' => 'Use File API to read/write a file',
+ 'page callback' => 'drupal_get_form',
+ 'access arguments' => array('use file example'),
+ 'page arguments' => array('file_example_readwrite'),
+ );
+ $items['examples/file_example/access_session'] = array(
+ 'page callback' => 'file_example_session_contents',
+ 'access arguments' => array('use file example'),
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+}
+
+
+/**
+ * Implements hook_permission().
+ */
+function file_example_permission() {
+ return array(
+ 'use file example' => array(
+ 'title' => t('Use the examples in the File Example module'),
+ ),
+ );
+}
+
+/**
+ * A simple introduction to the workings of this module.
+ */
+function file_example_intro() {
+ $markup = t('The file example module provides a form and code to demonstrate the Drupal 7 file api. Experiment with the form, and then look at the submit handlers in the code to understand the file api.');
+ return array('#markup' => $markup);
+}
+/**
+ * Form builder function.
+ *
+ * A simple form that allows creation of a file, managed or unmanaged. It
+ * also allows reading/deleting a file and creation of a directory.
+ */
+function file_example_readwrite($form, &$form_state) {
+ if (empty($_SESSION['file_example_default_file'])) {
+ $_SESSION['file_example_default_file'] = 'session://drupal.txt';
+ }
+ $default_file = $_SESSION['file_example_default_file'];
+ if (empty($_SESSION['file_example_default_directory'])) {
+ $_SESSION['file_example_default_directory'] = 'session://directory1';
+ }
+ $default_directory = $_SESSION['file_example_default_directory'];
+
+ $form['write_file'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Write to a file'),
+ );
+ $form['write_file']['write_contents'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter something you would like to write to a file') . ' ' . date('m'),
+ '#default_value' => t('Put some text here or just use this text'),
+ );
+
+ $form['write_file']['destination'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $default_file,
+ '#title' => t('Optional: Enter the streamwrapper saying where it should be written'),
+ '#description' => t('This may be public://some_dir/test_file.txt or private://another_dir/some_file.txt, for example. If you include a directory, it must already exist. The default is "public://". Since this example supports session://, you can also use something like session://somefile.txt.'),
+ );
+
+ $form['write_file']['managed_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Write managed file'),
+ '#submit' => array('file_example_managed_write_submit'),
+ );
+ $form['write_file']['unmanaged_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Write unmanaged file'),
+ '#submit' => array('file_example_unmanaged_write_submit'),
+ );
+ $form['write_file']['unmanaged_php'] = array(
+ '#type' => 'submit',
+ '#value' => t('Unmanaged using PHP'),
+ '#submit' => array('file_example_unmanaged_php_submit'),
+ );
+
+ $form['fileops'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Read from a file'),
+ );
+ $form['fileops']['fileops_file'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $default_file,
+ '#title' => t('Enter the URI of a file'),
+ '#description' => t('This must be a stream-type description like public://some_file.txt or http://drupal.org or private://another_file.txt or (for this example) session://yet_another_file.txt.'),
+ );
+ $form['fileops']['read_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Read the file and store it locally'),
+ '#submit' => array('file_example_read_submit'),
+ );
+ $form['fileops']['delete_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete file'),
+ '#submit' => array('file_example_delete_submit'),
+ );
+ $form['fileops']['check_submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Check to see if file exists'),
+ '#submit' => array('file_example_file_check_exists_submit'),
+ );
+
+ $form['directory'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Create or prepare a directory'),
+ );
+
+ $form['directory']['directory_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Directory to create/prepare/delete'),
+ '#default_value' => $default_directory,
+ '#description' => t('This is a directory as in public://some/directory or private://another/dir.'),
+ );
+ $form['directory']['create_directory'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create directory'),
+ '#submit' => array('file_example_create_directory_submit'),
+ );
+ $form['directory']['delete_directory'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete directory'),
+ '#submit' => array('file_example_delete_directory_submit'),
+ );
+ $form['directory']['check_directory'] = array(
+ '#type' => 'submit',
+ '#value' => t('Check to see if directory exists'),
+ '#submit' => array('file_example_check_directory_submit'),
+ );
+
+ $form['debug'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Debugging'),
+ );
+ $form['debug']['show_raw_session'] = array(
+ '#type' => 'submit',
+ '#value' => t('Show raw $_SESSION contents'),
+ '#submit' => array('file_example_show_session_contents_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler to write a managed file.
+ *
+ * The key functions used here are:
+ * - file_save_data(), which takes a buffer and saves it to a named file and
+ * also creates a tracking record in the database and returns a file object.
+ * In this function we use FILE_EXISTS_RENAME (the default) as the argument,
+ * which means that if there's an existing file, create a new non-colliding
+ * filename and use it.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ */
+function file_example_managed_write_submit($form, &$form_state) {
+ $data = $form_state['values']['write_contents'];
+ $uri = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL;
+
+ // Managed operations work with a file object.
+ $file_object = file_save_data($data, $uri, FILE_EXISTS_RENAME);
+ if (!empty($file_object)) {
+ $url = file_create_url($file_object->uri);
+ $_SESSION['file_example_default_file'] = $file_object->uri;
+ drupal_set_message(
+ t('Saved managed file: %file to destination %destination (accessible via !url, actual uri=@uri)',
+ array(
+ '%file' => print_r($file_object, TRUE),
+ '%destination' => $uri, '@uri' => $file_object->uri,
+ '!url' => l(t('this URL'), $url),
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('Failed to save the managed file'), 'error');
+ }
+}
+
+/**
+ * Submit handler to write an unmanaged file.
+ *
+ * The key functions used here are:
+ * - file_unmanaged_save_data(), which takes a buffer and saves it to a named
+ * file, but does not create any kind of tracking record in the database.
+ * This example uses FILE_EXISTS_REPLACE for the third argument, meaning
+ * that if there's an existing file at this location, it should be replaced.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ */
+function file_example_unmanaged_write_submit($form, &$form_state) {
+ $data = $form_state['values']['write_contents'];
+ $destination = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL;
+
+ // With the unmanaged file we just get a filename back.
+ $filename = file_unmanaged_save_data($data, $destination, FILE_EXISTS_REPLACE);
+ if ($filename) {
+ $url = file_create_url($filename);
+ $_SESSION['file_example_default_file'] = $filename;
+ drupal_set_message(
+ t('Saved file as %filename (accessible via !url, uri=@uri)',
+ array(
+ '%filename' => $filename,
+ '@uri' => $filename,
+ '!url' => l(t('this URL'), $url),
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('Failed to save the file'), 'error');
+ }
+}
+
+/**
+ * Submit handler to write an unmanaged file using plain PHP functions.
+ *
+ * The key functions used here are:
+ * - file_unmanaged_save_data(), which takes a buffer and saves it to a named
+ * file, but does not create any kind of tracking record in the database.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ * - drupal_tempnam() generates a temporary filename for use.
+ */
+function file_example_unmanaged_php_submit($form, &$form_state) {
+ $data = $form_state['values']['write_contents'];
+ $destination = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL;
+
+ if (empty($destination)) {
+ // If no destination has been provided, use a generated name.
+ $destination = drupal_tempnam('public://', 'file');
+ }
+
+ // With all traditional PHP functions we can use the stream wrapper notation
+ // for a file as well.
+ $fp = fopen($destination, 'w');
+
+ // To demonstrate the fact that everything is based on streams, we'll do
+ // multiple 5-character writes to put this to the file. We could easily
+ // (and far more conveniently) write it in a single statement with
+ // fwrite($fp, $data).
+ $length = strlen($data);
+ $write_size = 5;
+ for ($i = 0; $i < $length; $i += $write_size) {
+ $result = fwrite($fp, substr($data, $i, $write_size));
+ if ($result === FALSE) {
+ drupal_set_message(t('Failed writing to the file %file', array('%file' => $destination)), 'error');
+ fclose($fp);
+ return;
+ }
+ }
+ $url = file_create_url($destination);
+ $_SESSION['file_example_default_file'] = $destination;
+ drupal_set_message(
+ t('Saved file as %filename (accessible via !url, uri=@uri)',
+ array(
+ '%filename' => $destination,
+ '@uri' => $destination,
+ '!url' => l(t('this URL'), $url),
+ )
+ )
+ );
+}
+
+/**
+ * Submit handler for reading a stream wrapper.
+ *
+ * Drupal now has full support for PHP's stream wrappers, which means that
+ * instead of the traditional use of all the file functions
+ * ($fp = fopen("/tmp/some_file.txt");) far more sophisticated and generalized
+ * (and extensible) things can be opened as if they were files. Drupal itself
+ * provides the public:// and private:// schemes for handling public and
+ * private files. PHP provides file:// (the default) and http://, so that a
+ * URL can be read or written (as in a POST) as if it were a file. In addition,
+ * new schemes can be provided for custom applications, as will be demonstrated
+ * below.
+ *
+ * Here we take the stream wrapper provided in the form. We grab the
+ * contents with file_get_contents(). Notice that's it's as simple as that:
+ * file_get_contents("http://example.com") or
+ * file_get_contents("public://somefile.txt") just works. Although it's
+ * not necessary, we use file_unmanaged_save_data() to save this file locally
+ * and then find a local URL for it by using file_create_url().
+ */
+function file_example_read_submit($form, &$form_state) {
+ $uri = $form_state['values']['fileops_file'];
+
+ if (!is_file($uri)) {
+ drupal_set_message(t('The file %uri does not exist', array('%uri' => $uri)), 'error');
+ return;
+ }
+
+ // Make a working filename to save this by stripping off the (possible)
+ // file portion of the streamwrapper. If it's an evil file extension,
+ // file_munge_filename() will neuter it.
+ $filename = file_munge_filename(preg_replace('@^.*/@', '', $uri), '', TRUE);
+ $buffer = file_get_contents($uri);
+
+ if ($buffer) {
+ $sourcename = file_unmanaged_save_data($buffer, 'public://' . $filename);
+ if ($sourcename) {
+ $url = file_create_url($sourcename);
+ $_SESSION['file_example_default_file'] = $sourcename;
+ drupal_set_message(
+ t('The file was read and copied to %filename which is accessible at !url',
+ array(
+ '%filename' => $sourcename,
+ '!url' => l($url, $url),
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('Failed to save the file'));
+ }
+ }
+ else {
+ // We failed to get the contents of the requested file.
+ drupal_set_message(t('Failed to retrieve the file %file', array('%file' => $uri)));
+ }
+}
+
+/**
+ * Submit handler to delete a file.
+ */
+function file_example_delete_submit($form, &$form_state) {
+
+ $uri = $form_state['values']['fileops_file'];
+
+ // Since we don't know if the file is managed or not, look in the database
+ // to see. Normally, code would be working with either managed or unmanaged
+ // files, so this is not a typical situation.
+ $file_object = file_example_get_managed_file($uri);
+
+ // If a managed file, use file_delete().
+ if (!empty($file_object)) {
+ $result = file_delete($file_object);
+ if ($result !== TRUE) {
+ drupal_set_message(t('Failed deleting managed file %uri. Result was %result',
+ array(
+ '%uri' => $uri,
+ '%result' => print_r($result, TRUE),
+ )
+ ), 'error');
+ }
+ else {
+ drupal_set_message(t('Successfully deleted managed file %uri', array('%uri' => $uri)));
+ $_SESSION['file_example_default_file'] = $uri;
+ }
+ }
+ // Else use file_unmanaged_delete().
+ else {
+ $result = file_unmanaged_delete($uri);
+ if ($result !== TRUE) {
+ drupal_set_message(t('Failed deleting unmanaged file %uri', array('%uri' => $uri, 'error')));
+ }
+ else {
+ drupal_set_message(t('Successfully deleted unmanaged file %uri', array('%uri' => $uri)));
+ $_SESSION['file_example_default_file'] = $uri;
+ }
+ }
+}
+
+/**
+ * Submit handler to check existence of a file.
+ */
+function file_example_file_check_exists_submit($form, &$form_state) {
+ $uri = $form_state['values']['fileops_file'];
+ if (is_file($uri)) {
+ drupal_set_message(t('The file %uri exists.', array('%uri' => $uri)));
+ }
+ else {
+ drupal_set_message(t('The file %uri does not exist.', array('%uri' => $uri)));
+ }
+
+}
+/**
+ * Submit handler for directory creation.
+ *
+ * Here we create a directory and set proper permissions on it using
+ * file_prepare_directory().
+ */
+function file_example_create_directory_submit($form, &$form_state) {
+ $directory = $form_state['values']['directory_name'];
+
+ // The options passed to file_prepare_directory are a bitmask, so we can
+ // specify either FILE_MODIFY_PERMISSIONS (set permissions on the directory),
+ // FILE_CREATE_DIRECTORY, or both together:
+ // FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY.
+ // FILE_MODIFY_PERMISSIONS will set the permissions of the directory by
+ // by default to 0755, or to the value of the variable 'file_chmod_directory'.
+ if (!file_prepare_directory($directory, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY)) {
+ drupal_set_message(t('Failed to create %directory.', array('%directory' => $directory)), 'error');
+ }
+ else {
+ drupal_set_message(t('Directory %directory is ready for use.', array('%directory' => $directory)));
+ $_SESSION['file_example_default_directory'] = $directory;
+ }
+}
+
+/**
+ * Submit handler for directory deletion.
+ *
+ * @see file_unmanaged_delete_recursive()
+ */
+function file_example_delete_directory_submit($form, &$form_state) {
+ $directory = $form_state['values']['directory_name'];
+
+ $result = file_unmanaged_delete_recursive($directory);
+ if (!$result) {
+ drupal_set_message(t('Failed to delete %directory.', array('%directory' => $directory)), 'error');
+ }
+ else {
+ drupal_set_message(t('Recursively deleted directory %directory.', array('%directory' => $directory)));
+ $_SESSION['file_example_default_directory'] = $directory;
+ }
+}
+
+/**
+ * Submit handler to test directory existence.
+ *
+ * This actually just checks to see if the directory is writable
+ *
+ * @param array $form
+ * FormAPI form.
+ * @param array $form_state
+ * FormAPI form state.
+ */
+function file_example_check_directory_submit($form, &$form_state) {
+ $directory = $form_state['values']['directory_name'];
+ $result = is_dir($directory);
+ if (!$result) {
+ drupal_set_message(t('Directory %directory does not exist.', array('%directory' => $directory)));
+ }
+ else {
+ drupal_set_message(t('Directory %directory exists.', array('%directory' => $directory)));
+ }
+}
+
+/**
+ * Utility submit function to show the contents of $_SESSION.
+ */
+function file_example_show_session_contents_submit($form, &$form_state) {
+ // If the devel module is installed, use it's nicer message format.
+ if (module_exists('devel')) {
+ dsm($_SESSION['file_example'], t('Entire $_SESSION["file_example"]'));
+ }
+ else {
+ drupal_set_message('
' . print_r($_SESSION['file_example'], TRUE) . '
');
+ }
+}
+
+/**
+ * Utility function to check for and return a managed file.
+ *
+ * In this demonstration code we don't necessarily know if a file is managed
+ * or not, so often need to check to do the correct behavior. Normal code
+ * would not have to do this, as it would be working with either managed or
+ * unmanaged files.
+ *
+ * @param string $uri
+ * The URI of the file, like public://test.txt.
+ */
+function file_example_get_managed_file($uri) {
+ $fid = db_query('SELECT fid FROM {file_managed} WHERE uri = :uri', array(':uri' => $uri))->fetchField();
+ if (!empty($fid)) {
+ $file_object = file_load($fid);
+ return $file_object;
+ }
+ return FALSE;
+}
+
+/**
+ * Implements hook_stream_wrappers().
+ *
+ * hook_stream_wrappers() is Drupal's way of exposing the class that PHP will
+ * use to provide a new stream wrapper class. In this case, we'll expose the
+ * 'session' scheme, so a file reference like "session://example/example.txt"
+ * is readable and writable as a location in the $_SESSION variable.
+ *
+ * @see FileExampleSessionStreamWrapper
+ */
+function file_example_stream_wrappers() {
+ $wrappers = array(
+ 'session' => array(
+ 'name' => t('Example: $_SESSION variable storage'),
+ 'class' => 'FileExampleSessionStreamWrapper',
+ 'description' => t('Store files in the $_SESSION variable as an example.'),
+ ),
+ );
+ return $wrappers;
+}
+
+/**
+ * Show the contents of a session file.
+ *
+ * This page callback function is called by the Menu API for the path
+ * examples/file_example/access_session. Any extra path elements
+ * beyond this are considered to be the session path. E.g.:
+ * examples/file_example/access_session/foo/bar.txt would be the
+ * equivalent of session://foo/bar.txt, which will map into
+ * $_SESSION as keys: $_SESSION['foo']['bar.txt']
+ *
+ * Menu API will pass in additional path elements as function arguments. You
+ * can obtain these with func_get_args().
+ *
+ * @return string
+ * A message containing the contents of the session file.
+ *
+ * @see file_get_contents()
+ */
+function file_example_session_contents() {
+ $path_components = func_get_args();
+ $session_path = 'session://' . implode('/', $path_components);
+ $content = file_get_contents($session_path);
+ if ($content !== FALSE) {
+ return t('Contents of @path :',
+ array('@path' => check_plain($session_path))) . ' ' .
+ print_r($content, TRUE);
+ }
+ return t('Unable to load contents of: @path',
+ array('@path' => check_plain($session_path)));
+}
+
+/**
+ * @} End of "defgroup file_example".
+ */
diff --git a/sites/all/modules/examples/file_example/file_example.test b/sites/all/modules/examples/file_example/file_example.test
new file mode 100644
index 00000000..41e73c61
--- /dev/null
+++ b/sites/all/modules/examples/file_example/file_example.test
@@ -0,0 +1,149 @@
+ 'File Example Functionality',
+ 'description' => 'Test File Example features and sample streamwrapper.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp(array('file_example'));
+ $this->priviledgedUser = $this->drupalCreateUser(array('use file example'));
+ $this->drupalLogin($this->priviledgedUser);
+ }
+
+ /**
+ * Test the basic File Example UI.
+ *
+ * - Create a directory to work with
+ * - Foreach scheme create and read files using each of the three methods.
+ */
+ public function testFileExampleBasic() {
+
+ $expected_text = array(
+ t('Write managed file') => t('Saved managed file'),
+ t('Write unmanaged file') => t('Saved file as'),
+ t('Unmanaged using PHP') => t('Saved file as'),
+ );
+ // For each of the three buttons == three write types.
+ $buttons = array(
+ t('Write managed file'),
+ t('Write unmanaged file'),
+ t('Unmanaged using PHP'),
+ );
+ foreach ($buttons as $button) {
+ // For each scheme supported by Drupal + the session:// wrapper.
+ $schemes = array('public', 'private', 'temporary', 'session');
+ foreach ($schemes as $scheme) {
+ // Create a directory for use.
+ $dirname = $scheme . '://' . $this->randomName(10);
+
+ // Directory does not yet exist; assert that.
+ $edit = array(
+ 'directory_name' => $dirname,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if directory exists'));
+ $this->assertRaw(t('Directory %dirname does not exist', array('%dirname' => $dirname)), 'Verify that directory does not exist.');
+
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Create directory'));
+ $this->assertRaw(t('Directory %dirname is ready for use', array('%dirname' => $dirname)));
+
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if directory exists'));
+ $this->assertRaw(t('Directory %dirname exists', array('%dirname' => $dirname)), 'Verify that directory now does exist.');
+
+ // Create a file in the directory we created.
+ $content = $this->randomName(30);
+ $filename = $dirname . '/' . $this->randomName(30) . '.txt';
+
+ // Assert that the file we're about to create does not yet exist.
+ $edit = array(
+ 'fileops_file' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if file exists'));
+ $this->assertRaw(t('The file %filename does not exist', array('%filename' => $filename)), 'Verify that file does not yet exist.');
+
+ debug(
+ t('Processing button=%button, scheme=%scheme, dir=%dirname, file=%filename',
+ array(
+ '%button' => $button,
+ '%scheme' => $scheme,
+ '%filename' => $filename,
+ '%dirname' => $dirname,
+ )
+ )
+ );
+ $edit = array(
+ 'write_contents' => $content,
+ 'destination' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, $button);
+ $this->assertText($expected_text[$button]);
+
+ // Capture the name of the output file, as it might have changed due
+ // to file renaming.
+ $element = $this->xpath('//span[@id="uri"]');
+ $output_filename = (string) $element[0];
+ debug($output_filename, 'Name of output file');
+
+ // Click the link provided that is an easy way to get the data for
+ // checking and make sure that the data we put in is what we get out.
+ if (!in_array($scheme, array('private', 'temporary'))) {
+ $this->clickLink(t('this URL'));
+ $this->assertText($content);
+ }
+
+ // Verify that the file exists.
+ $edit = array(
+ 'fileops_file' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if file exists'));
+ $this->assertRaw(t('The file %filename exists', array('%filename' => $filename)), 'Verify that file now exists.');
+
+ // Now read the file that got written above and verify that we can use
+ // the writing tools.
+ $edit = array(
+ 'fileops_file' => $output_filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Read the file and store it locally'));
+
+ $this->assertText(t('The file was read and copied'));
+
+ $edit = array(
+ 'fileops_file' => $filename,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Delete file'));
+ $this->assertText(t('Successfully deleted'));
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if file exists'));
+ $this->assertRaw(t('The file %filename does not exist', array('%filename' => $filename)), 'Verify file has been deleted.');
+
+ $edit = array(
+ 'directory_name' => $dirname,
+ );
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Delete directory'));
+ $this->drupalPost('examples/file_example/fileapi', $edit, t('Check to see if directory exists'));
+ $this->assertRaw(t('Directory %dirname does not exist', array('%dirname' => $dirname)), 'Verify that directory does not exist after deletion.');
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/examples/file_example/file_example_session_streams.inc b/sites/all/modules/examples/file_example/file_example_session_streams.inc
new file mode 100644
index 00000000..dc4850c3
--- /dev/null
+++ b/sites/all/modules/examples/file_example/file_example_session_streams.inc
@@ -0,0 +1,698 @@
+uri = $uri;
+ }
+
+ /**
+ * Implements getUri().
+ */
+ public function getUri() {
+ return $this->uri;
+ }
+
+ /**
+ * Implements getTarget().
+ *
+ * The "target" is the portion of the URI to the right of the scheme.
+ * So in session://example/test.txt, the target is 'example/test.txt'.
+ */
+ public function getTarget($uri = NULL) {
+ if (!isset($uri)) {
+ $uri = $this->uri;
+ }
+
+ list($scheme, $target) = explode('://', $uri, 2);
+
+ // Remove erroneous leading or trailing, forward-slashes and backslashes.
+ // In the session:// scheme, there is never a leading slash on the target.
+ return trim($target, '\/');
+ }
+
+ /**
+ * Implements getMimeType().
+ */
+ public static function getMimeType($uri, $mapping = NULL) {
+ if (!isset($mapping)) {
+ // The default file map, defined in file.mimetypes.inc is quite big.
+ // We only load it when necessary.
+ include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc';
+ $mapping = file_mimetype_mapping();
+ }
+
+ $extension = '';
+ $file_parts = explode('.', basename($uri));
+
+ // Remove the first part: a full filename should not match an extension.
+ array_shift($file_parts);
+
+ // Iterate over the file parts, trying to find a match.
+ // For my.awesome.image.jpeg, we try:
+ // - jpeg
+ // - image.jpeg, and
+ // - awesome.image.jpeg
+ while ($additional_part = array_pop($file_parts)) {
+ $extension = drupal_strtolower($additional_part . ($extension ? '.' . $extension : ''));
+ if (isset($mapping['extensions'][$extension])) {
+ return $mapping['mimetypes'][$mapping['extensions'][$extension]];
+ }
+ }
+
+ return 'application/octet-stream';
+ }
+
+ /**
+ * Implements getDirectoryPath().
+ *
+ * In this case there is no directory string, so return an empty string.
+ */
+ public function getDirectoryPath() {
+ return '';
+ }
+
+ /**
+ * Overrides getExternalUrl().
+ *
+ * We have set up a helper function and menu entry to provide access to this
+ * key via HTTP; normally it would be accessible some other way.
+ */
+ public function getExternalUrl() {
+ $path = $this->getLocalPath();
+ $url = url('examples/file_example/access_session/' . $path, array('absolute' => TRUE));
+ return $url;
+ }
+
+ /**
+ * We have no concept of chmod, so just return TRUE.
+ */
+ public function chmod($mode) {
+ return TRUE;
+ }
+
+ /**
+ * Implements realpath().
+ */
+ public function realpath() {
+ return 'session://' . $this->getLocalPath();
+ }
+
+ /**
+ * Returns the local path.
+ *
+ * Here we aren't doing anything but stashing the "file" in a key in the
+ * $_SESSION variable, so there's not much to do but to create a "path"
+ * which is really just a key in the $_SESSION variable. So something
+ * like 'session://one/two/three.txt' becomes
+ * $_SESSION['file_example']['one']['two']['three.txt'] and the actual path
+ * is "one/two/three.txt".
+ *
+ * @param string $uri
+ * Optional URI, supplied when doing a move or rename.
+ */
+ protected function getLocalPath($uri = NULL) {
+ if (!isset($uri)) {
+ $uri = $this->uri;
+ }
+
+ $path = str_replace('session://', '', $uri);
+ $path = trim($path, '/');
+ return $path;
+ }
+
+ /**
+ * Opens a stream, as for fopen(), file_get_contents(), file_put_contents().
+ *
+ * @param string $uri
+ * A string containing the URI to the file to open.
+ * @param string $mode
+ * The file mode ("r", "wb" etc.).
+ * @param int $options
+ * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
+ * @param string &$opened_path
+ * A string containing the path actually opened.
+ *
+ * @return bool
+ * Returns TRUE if file was opened successfully. (Always returns TRUE).
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-open.php
+ */
+ public function stream_open($uri, $mode, $options, &$opened_path) {
+ $this->uri = $uri;
+ // We make $session_content a reference to the appropriate key in the
+ // $_SESSION variable. So if the local path were
+ // /example/test.txt it $session_content would now be a
+ // reference to $_SESSION['file_example']['example']['test.txt'].
+ $this->sessionContent = &$this->uri_to_session_key($uri);
+
+ // Reset the stream pointer since this is an open.
+ $this->streamPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Return a reference to the correct $_SESSION key.
+ *
+ * @param string $uri
+ * The uri: session://something
+ * @param bool $create
+ * If TRUE, create the key
+ *
+ * @return array|bool
+ * A reference to the array at the end of the key-path, or
+ * FALSE if the path doesn't map to a key-path (and $create is FALSE).
+ */
+ protected function &uri_to_session_key($uri, $create = TRUE) {
+ // Since our uri_to_session_key() method returns a reference, we
+ // have to set up a failure flag variable.
+ $fail = FALSE;
+ $path = $this->getLocalPath($uri);
+ $path_components = explode('/', $path);
+ // Set up a reference to the root session:// 'directory.'
+ $var = &$_SESSION['file_example'];
+ // Handle case of just session://.
+ if (count($path_components) < 1) {
+ return $var;
+ }
+ // Walk through the path components and create keys in $_SESSION,
+ // unless we're told not to create them.
+ foreach ($path_components as $component) {
+ if ($create || isset($var[$component])) {
+ $var = &$var[$component];
+ }
+ else {
+ // This path doesn't exist as keys, either because the
+ // key doesn't exist, or because we're told not to create it.
+ return $fail;
+ }
+ }
+ return $var;
+ }
+
+ /**
+ * Support for flock().
+ *
+ * The $_SESSION variable has no locking capability, so return TRUE.
+ *
+ * @param int $operation
+ * One of the following:
+ * - LOCK_SH to acquire a shared lock (reader).
+ * - LOCK_EX to acquire an exclusive lock (writer).
+ * - LOCK_UN to release a lock (shared or exclusive).
+ * - LOCK_NB if you don't want flock() to block while locking (not
+ * supported on Windows).
+ *
+ * @return bool
+ * Always returns TRUE at the present time. (no support)
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-lock.php
+ */
+ public function stream_lock($operation) {
+ return TRUE;
+ }
+
+ /**
+ * Support for fread(), file_get_contents() etc.
+ *
+ * @param int $count
+ * Maximum number of bytes to be read.
+ *
+ * @return string
+ * The string that was read, or FALSE in case of an error.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-read.php
+ */
+ public function stream_read($count) {
+ if (is_string($this->sessionContent)) {
+ $remaining_chars = drupal_strlen($this->sessionContent) - $this->streamPointer;
+ $number_to_read = min($count, $remaining_chars);
+ if ($remaining_chars > 0) {
+ $buffer = drupal_substr($this->sessionContent, $this->streamPointer, $number_to_read);
+ $this->streamPointer += $number_to_read;
+ return $buffer;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for fwrite(), file_put_contents() etc.
+ *
+ * @param string $data
+ * The string to be written.
+ *
+ * @return int
+ * The number of bytes written (integer).
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-write.php
+ */
+ public function stream_write($data) {
+ // Sanitize the data in a simple way since we're putting it into the
+ // session variable.
+ $data = check_plain($data);
+ $this->sessionContent = substr_replace($this->sessionContent, $data, $this->streamPointer);
+ $this->streamPointer += drupal_strlen($data);
+ return drupal_strlen($data);
+ }
+
+ /**
+ * Support for feof().
+ *
+ * @return bool
+ * TRUE if end-of-file has been reached.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+ */
+ public function stream_eof() {
+ return FALSE;
+ }
+
+ /**
+ * Support for fseek().
+ *
+ * @param int $offset
+ * The byte offset to got to.
+ * @param int $whence
+ * SEEK_SET, SEEK_CUR, or SEEK_END.
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+ */
+ public function stream_seek($offset, $whence) {
+ if (drupal_strlen($this->sessionContent) >= $offset) {
+ $this->streamPointer = $offset;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for fflush().
+ *
+ * @return bool
+ * TRUE if data was successfully stored (or there was no data to store).
+ * This always returns TRUE, as this example provides and needs no
+ * flush support.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-flush.php
+ */
+ public function stream_flush() {
+ return TRUE;
+ }
+
+ /**
+ * Support for ftell().
+ *
+ * @return int
+ * The current offset in bytes from the beginning of file.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+ */
+ public function stream_tell() {
+ return $this->streamPointer;
+ }
+
+ /**
+ * Support for fstat().
+ *
+ * @return array
+ * An array with file status, or FALSE in case of an error - see fstat()
+ * for a description of this array.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+ */
+ public function stream_stat() {
+ return array(
+ 'size' => drupal_strlen($this->sessionContent),
+ );
+ }
+
+ /**
+ * Support for fclose().
+ *
+ * @return bool
+ * TRUE if stream was successfully closed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-close.php
+ */
+ public function stream_close() {
+ $this->streamPointer = 0;
+ // Unassign the reference.
+ unset($this->sessionContent);
+ return TRUE;
+ }
+
+ /**
+ * Support for unlink().
+ *
+ * @param string $uri
+ * A string containing the uri to the resource to delete.
+ *
+ * @return bool
+ * TRUE if resource was successfully deleted.
+ *
+ * @see http://php.net/manual/en/streamwrapper.unlink.php
+ */
+ public function unlink($uri) {
+ $path = $this->getLocalPath($uri);
+ $path_components = preg_split('/\//', $path);
+ $unset = '$_SESSION[\'file_example\']';
+ foreach ($path_components as $component) {
+ $unset .= '[\'' . $component . '\']';
+ }
+ // TODO: Is there a better way to delete from an array?
+ // drupal_array_get_nested_value() doesn't work because it only returns
+ // a reference; unsetting a reference only unsets the reference.
+ eval("unset($unset);");
+ return TRUE;
+ }
+
+ /**
+ * Support for rename().
+ *
+ * @param string $from_uri
+ * The uri to the file to rename.
+ * @param string $to_uri
+ * The new uri for file.
+ *
+ * @return bool
+ * TRUE if file was successfully renamed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.rename.php
+ */
+ public function rename($from_uri, $to_uri) {
+ $from_key = &$this->uri_to_session_key($from_uri);
+ $to_key = &$this->uri_to_session_key($to_uri);
+ if (is_dir($to_key) || is_file($to_key)) {
+ return FALSE;
+ }
+ $to_key = $from_key;
+ unset($from_key);
+ return TRUE;
+ }
+
+ /**
+ * Gets the name of the directory from a given path.
+ *
+ * @param string $uri
+ * A URI.
+ *
+ * @return string
+ * A string containing the directory name.
+ *
+ * @see drupal_dirname()
+ */
+ public function dirname($uri = NULL) {
+ list($scheme, $target) = explode('://', $uri, 2);
+ $target = $this->getTarget($uri);
+ if (strpos($target, '/')) {
+ $dirname = preg_replace('@/[^/]*$@', '', $target);
+ }
+ else {
+ $dirname = '';
+ }
+ return $scheme . '://' . $dirname;
+ }
+
+ /**
+ * Support for mkdir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to create.
+ * @param int $mode
+ * Permission flags - see mkdir().
+ * @param int $options
+ * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
+ *
+ * @return bool
+ * TRUE if directory was successfully created.
+ *
+ * @see http://php.net/manual/en/streamwrapper.mkdir.php
+ */
+ public function mkdir($uri, $mode, $options) {
+ // If this already exists, then we can't mkdir.
+ if (is_dir($uri) || is_file($uri)) {
+ return FALSE;
+ }
+
+ // Create the key in $_SESSION;
+ $this->uri_to_session_key($uri, TRUE);
+
+ // Place a magic file inside it to differentiate this from an empty file.
+ $marker_uri = $uri . '/.isadir.txt';
+ $this->uri_to_session_key($marker_uri, TRUE);
+ return TRUE;
+ }
+
+ /**
+ * Support for rmdir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to delete.
+ * @param int $options
+ * A bit mask of STREAM_REPORT_ERRORS.
+ *
+ * @return bool
+ * TRUE if directory was successfully removed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.rmdir.php
+ */
+ public function rmdir($uri, $options) {
+ $path = $this->getLocalPath($uri);
+ $path_components = preg_split('/\//', $path);
+ $unset = '$_SESSION[\'file_example\']';
+ foreach ($path_components as $component) {
+ $unset .= '[\'' . $component . '\']';
+ }
+ // TODO: I really don't like this eval.
+ debug($unset, 'array element to be unset');
+ eval("unset($unset);");
+
+ return TRUE;
+ }
+
+ /**
+ * Support for stat().
+ *
+ * This important function goes back to the Unix way of doing things.
+ * In this example almost the entire stat array is irrelevant, but the
+ * mode is very important. It tells PHP whether we have a file or a
+ * directory and what the permissions are. All that is packed up in a
+ * bitmask. This is not normal PHP fodder.
+ *
+ * @param string $uri
+ * A string containing the URI to get information about.
+ * @param int $flags
+ * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
+ *
+ * @return array|bool
+ * An array with file status, or FALSE in case of an error - see fstat()
+ * for a description of this array.
+ *
+ * @see http://php.net/manual/en/streamwrapper.url-stat.php
+ */
+ public function url_stat($uri, $flags) {
+ // Get a reference to the $_SESSION key for this URI.
+ $key = $this->uri_to_session_key($uri, FALSE);
+ // Default to fail.
+ $return = FALSE;
+ $mode = 0;
+
+ // We will call an array a directory and the root is always an array.
+ if (is_array($key) && array_key_exists('.isadir.txt', $key)) {
+ // S_IFDIR means it's a directory.
+ $mode = 0040000;
+ }
+ elseif ($key !== FALSE) {
+ // S_IFREG, means it's a file.
+ $mode = 0100000;
+ }
+
+ if ($mode) {
+ $size = 0;
+ if ($mode == 0100000) {
+ $size = drupal_strlen($key);
+ }
+
+ // There are no protections on this, so all writable.
+ $mode |= 0777;
+ $return = array(
+ 'dev' => 0,
+ 'ino' => 0,
+ 'mode' => $mode,
+ 'nlink' => 0,
+ 'uid' => 0,
+ 'gid' => 0,
+ 'rdev' => 0,
+ 'size' => $size,
+ 'atime' => 0,
+ 'mtime' => 0,
+ 'ctime' => 0,
+ 'blksize' => 0,
+ 'blocks' => 0,
+ );
+ }
+ return $return;
+ }
+
+ /**
+ * Support for opendir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to open.
+ * @param int $options
+ * Whether or not to enforce safe_mode (0x04).
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
+ */
+ public function dir_opendir($uri, $options) {
+ $var = &$this->uri_to_session_key($uri, FALSE);
+ if ($var === FALSE || !array_key_exists('.isadir.txt', $var)) {
+ return FALSE;
+ }
+
+ // We grab the list of key names, flip it so that .isadir.txt can easily
+ // be removed, then flip it back so we can easily walk it as a list.
+ $this->directoryKeys = array_flip(array_keys($var));
+ unset($this->directoryKeys['.isadir.txt']);
+ $this->directoryKeys = array_keys($this->directoryKeys);
+ $this->directoryPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Support for readdir().
+ *
+ * @return string|bool
+ * The next filename, or FALSE if there are no more files in the directory.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-readdir.php
+ */
+ public function dir_readdir() {
+ if ($this->directoryPointer < count($this->directoryKeys)) {
+ $next = $this->directoryKeys[$this->directoryPointer];
+ $this->directoryPointer++;
+ return $next;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for rewinddir().
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
+ */
+ public function dir_rewinddir() {
+ $this->directoryPointer = 0;
+ }
+
+ /**
+ * Support for closedir().
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-closedir.php
+ */
+ public function dir_closedir() {
+ $this->directoryPointer = 0;
+ unset($this->directoryKeys);
+ return TRUE;
+ }
+}
diff --git a/sites/all/modules/examples/filter_example/filter_example.info b/sites/all/modules/examples/filter_example/filter_example.info
new file mode 100644
index 00000000..3f6522ca
--- /dev/null
+++ b/sites/all/modules/examples/filter_example/filter_example.info
@@ -0,0 +1,12 @@
+name = Filter example
+description = An example module showing how to define a custom filter.
+package = Example modules
+core = 7.x
+files[] = filter_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/filter_example/filter_example.module b/sites/all/modules/examples/filter_example/filter_example.module
new file mode 100644
index 00000000..f5e9dffe
--- /dev/null
+++ b/sites/all/modules/examples/filter_example/filter_example.module
@@ -0,0 +1,203 @@
+, and replace it by the current time.
+ *
+ * Foo filter
+ *
+ * Drupal has several content formats (they are not filters), and in our example
+ * the foo replacement can be configured for each one of them, allowing an html
+ * or php replacement, so the module includes a settings callback, with options
+ * to configure that replacements. Also, a Tips callback will help showing the
+ * current replacement for the content type being edited.
+ *
+ * Time filter.
+ *
+ * This filter is a little trickier to implement than the previous one.
+ * Since the input involves special HTML characters (< and >) we have to
+ * run the filter before HTML is escaped/stripped by other filters. But
+ * we want to use HTML in our result as well, and so if we run this filter
+ * first our replacement string could be escaped or stripped. The solution
+ * is to use the "prepare" operation to escape the special characters, and
+ * to later replace our escaped version in the "process" step.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function filter_example_menu() {
+ $items['examples/filter_example'] = array(
+ 'title' => 'Filter Example',
+ 'page callback' => '_filter_example_information',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_help().
+ */
+function filter_example_help($path, $arg) {
+ switch ($path) {
+ case 'admin/help#filter_example':
+ return _filter_example_information();
+ }
+}
+
+/**
+ * Simply returns a little bit of information about the example.
+ */
+function _filter_example_information() {
+ return t("
This example provides two filters.
Foo Filter replaces
+ 'foo' with a configurable replacement.
Time Tag replaces the string
+ '<time />' with the current time.
To use these filters, go to !link and
+ configure an input format, or create a new one.
",
+ array('!link' => l(t('admin/config/content/formats'), 'admin/config/content/formats'))
+ );
+}
+
+/**
+ * Implements hook_filter_info().
+ *
+ * Here we define the different filters provided by the module. For this
+ * example, time_filter is a very static and simple replacement, but it requires
+ * some preparation of the string because of the special html tags < and >. The
+ * foo_filter is more complex, including its own settings and inline tips.
+ */
+function filter_example_filter_info() {
+ $filters['filter_foo'] = array(
+ 'title' => t('Foo Filter (example)'),
+ 'description' => t('Every instance of "foo" in the input text will be replaced with a preconfigured replacement.'),
+ 'process callback' => '_filter_example_filter_foo_process',
+ 'default settings' => array(
+ 'filter_example_foo' => 'bar',
+ ),
+ 'settings callback' => '_filter_example_filter_foo_settings',
+ 'tips callback' => '_filter_example_filter_foo_tips',
+ );
+ $filters['filter_time'] = array(
+ 'title' => t('Time Tag (example)'),
+ 'description' => t("Every instance of the special <time /> tag will be replaced with the current date and time in the user's specified time zone."),
+ 'prepare callback' => '_filter_example_filter_time_prepare',
+ 'process callback' => '_filter_example_filter_time_process',
+ 'tips callback' => '_filter_example_filter_time_tips',
+ );
+ return $filters;
+}
+
+/*
+ * Foo filter
+ *
+ * Drupal has several text formats (they are not filters), and in our example
+ * the foo replacement can be configured for each one of them, so the module
+ * includes a settings callback, with options to configure those replacements.
+ * Also, a Tips callback will help showing the current replacement
+ * for the content type being edited.
+ */
+
+/**
+ * Settings callback for foo filter.
+ *
+ * Make use of $format to have different replacements for every input format.
+ * Since we allow the administrator to define the string that gets substituted
+ * when "foo" is encountered, we need to provide an interface for this kind of
+ * customization. The object format is also an argument of the callback.
+ *
+ * The settings defined in this form are stored in database by the filter
+ * module, and they will be available in the $filter argument.
+ */
+function _filter_example_filter_foo_settings($form, $form_state, $filter, $format, $defaults) {
+ $settings['filter_example_foo'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Substitution string'),
+ '#default_value' => isset($filter->settings['filter_example_foo']) ? $filter->settings['filter_example_foo'] : $defaults['filter_example_foo'],
+ '#description' => t('The string to substitute for "foo" everywhere in the text.'),
+ );
+ return $settings;
+}
+
+/**
+ * Foo filter process callback.
+ *
+ * The actual filtering is performed here. The supplied text should be returned,
+ * once any necessary substitutions have taken place. The example just replaces
+ * foo with our custom defined string in the settings page.
+ */
+function _filter_example_filter_foo_process($text, $filter, $format) {
+ $replacement = isset($filter->settings['filter_example_foo']) ? $filter->settings['filter_example_foo'] : 'bar';
+ return str_replace('foo', $replacement, $text);
+}
+
+
+/**
+ * Filter tips callback for foo filter.
+ *
+ * The tips callback allows filters to provide help text to users during the
+ * content editing process. Short tips are provided on the content editing
+ * screen, while long tips are provided on a separate linked page. Short tips
+ * are optional, but long tips are highly recommended.
+ */
+function _filter_example_filter_foo_tips($filter, $format, $long = FALSE) {
+ $replacement = isset($filter->settings['filter_example_foo']) ? $filter->settings['filter_example_foo'] : 'bar';
+ if (!$long) {
+ // This string will be shown in the content add/edit form.
+ return t('foo replaced with %replacement.', array('%replacement' => $replacement));
+ }
+ else {
+ return t('Every instance of "foo" in the input text will be replaced with a configurable value. You can configure this value and put whatever you want there. The replacement value is "%replacement".', array('%replacement' => $replacement));
+ }
+}
+
+/**
+ * Time filter prepare callback.
+ *
+ * We'll use [filter-example-time] as a replacement for the time tag.
+ * Note that in a more complicated filter a closing tag may also be
+ * required. For more information, see "Temporary placeholders and
+ * delimiters" at http://drupal.org/node/209715.
+ */
+function _filter_example_filter_time_prepare($text, $filter) {
+ return preg_replace('!!', '[filter-example-time]', $text);
+}
+
+/**
+ * Time filter process callback.
+ *
+ * Now, in the "process" step, we'll search for our escaped time tags and
+ * do the real filtering: replace the xml tag with the date.
+ */
+function _filter_example_filter_time_process($text, $filter) {
+ return str_replace('[filter-example-time]', '' . format_date(time()) . '', $text);
+}
+
+
+/**
+ * Filter tips callback for time filter.
+ *
+ * The tips callback allows filters to provide help text to users during the
+ * content editing process. Short tips are provided on the content editing
+ * screen, while long tips are provided on a separate linked page. Short tips
+ * are optional, but long tips are highly recommended.
+ */
+function _filter_example_filter_time_tips($filter, $format, $long = FALSE) {
+ return t('<time /> is replaced with the current time.');
+}
+
+/**
+ * @} End of "defgroup filter_example".
+ */
diff --git a/sites/all/modules/examples/filter_example/filter_example.test b/sites/all/modules/examples/filter_example/filter_example.test
new file mode 100644
index 00000000..56c412a8
--- /dev/null
+++ b/sites/all/modules/examples/filter_example/filter_example.test
@@ -0,0 +1,109 @@
+ 'Filter example functionality',
+ 'description' => 'Verify that content is processed by example filter.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('filter_example');
+
+ // Load the used input formats.
+ $this->filteredHtml = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Filtered HTML'))->fetchObject();
+ $this->fullHtml = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchObject();
+
+ // Create user.
+ $this->webUser = $this->drupalCreateUser(array(
+ 'administer filters',
+ filter_permission_name($this->filteredHtml),
+ filter_permission_name($this->fullHtml),
+ 'bypass node access',
+ ));
+ }
+
+ /**
+ * Functional test of the foo filter.
+ *
+ * Login user, create an example node, and test blog functionality through
+ * the admin and user interfaces.
+ */
+ public function testFilterExampleBasic() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Enable both filters in format id 1 (default format).
+ $edit = array(
+ 'filters[filter_time][status]' => TRUE,
+ 'filters[filter_foo][status]' => TRUE,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $this->filteredHtml->format, $edit, t('Save configuration'));
+
+ // Create a content type to test the filters (with default format).
+ $content_type = $this->drupalCreateContentType();
+
+ // Create a test node.
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ "body[$langcode][0][value]" => 'What foo is it? it is ',
+ );
+ $result = $this->drupalPost('node/add/' . $content_type->type, $edit, t('Save'));
+ $this->assertResponse(200);
+ $time = format_date(time());
+ $this->assertRaw('What bar is it? it is ' . $time . '');
+
+ // Enable foo filter in other format id 2
+ $edit = array(
+ 'filters[filter_foo][status]' => TRUE,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $this->fullHtml->format, $edit, t('Save configuration'));
+
+ // Change foo filter replacement with a random string in format id 2
+ $replacement = $this->randomName();
+ $options = array(
+ 'filters[filter_foo][settings][filter_example_foo]' => $replacement,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $this->fullHtml->format, $options, t('Save configuration'));
+
+ // Create a test node with content format 2
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ "body[$langcode][0][value]" => 'What foo is it? it is ',
+ "body[$langcode][0][format]" => $this->fullHtml->format,
+ );
+ $result = $this->drupalPost('node/add/' . $content_type->type, $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Only foo filter is enabled.
+ $this->assertRaw("What " . $replacement . " is it", 'Foo filter successfully verified.');
+ }
+}
diff --git a/sites/all/modules/examples/form_example/form_example.info b/sites/all/modules/examples/form_example/form_example.info
new file mode 100644
index 00000000..6c7b4327
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example.info
@@ -0,0 +1,12 @@
+name = Form example
+description = Examples of using the Drupal Form API.
+package = Example modules
+core = 7.x
+files[] = form_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/form_example/form_example.module b/sites/all/modules/examples/form_example/form_example.module
new file mode 100644
index 00000000..b3df1dfc
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example.module
@@ -0,0 +1,234 @@
+ 'Form Example',
+ 'page callback' => 'form_example_intro',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+ $items['examples/form_example/tutorial'] = array(
+ 'title' => 'Form Tutorial',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_1'),
+ 'access callback' => TRUE,
+ 'description' => 'A set of ten tutorials',
+ 'file' => 'form_example_tutorial.inc',
+ 'type' => MENU_NORMAL_ITEM,
+ );
+ $items['examples/form_example/tutorial/1'] = array(
+ 'title' => '#1',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_1'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 1: Simplest form',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/2'] = array(
+ 'title' => '#2',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_2'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 2: Form with a submit button',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/3'] = array(
+ 'title' => '#3',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_3'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 3: Fieldsets',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/4'] = array(
+ 'title' => '#4',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_4'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 4: Required fields',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/5'] = array(
+ 'title' => '#5',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_5'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 5: More element attributes',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/6'] = array(
+ 'title' => '#6',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_6'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 6: Form with a validate handler',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/7'] = array(
+ 'title' => '#7',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_7'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 7: Form with a submit handler',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/8'] = array(
+ 'title' => '#8',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_8'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 8: Basic multistep form',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/tutorial/9'] = array(
+ 'title' => '#9',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_9'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 9: Form with dynamically added new fields',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ 'weight' => 9,
+ );
+ $items['examples/form_example/tutorial/10'] = array(
+ 'title' => '#10',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_10'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 10: Form with file upload',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ 'weight' => 10,
+ );
+ $items['examples/form_example/tutorial/11'] = array(
+ 'title' => '#11',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_11'),
+ 'access callback' => TRUE,
+ 'description' => 'Tutorial 11: generating a confirmation form',
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'form_example_tutorial.inc',
+ 'weight' => 11,
+ );
+ $items['examples/form_example/tutorial/11/confirm/%'] = array(
+ 'title' => 'Name Confirmation',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_tutorial_11_confirm_name', 5),
+ 'access callback' => TRUE,
+ 'description' => 'Confirmation form for tutorial 11. Generated using the confirm_form function',
+ 'file' => 'form_example_tutorial.inc',
+ );
+ $items['examples/form_example/states'] = array(
+ 'title' => '#states example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_states_form'),
+ 'access callback' => TRUE,
+ 'description' => 'How to use the #states attribute in FAPI',
+ 'file' => 'form_example_states.inc',
+ );
+ $items['examples/form_example/wizard'] = array(
+ 'title' => 'Extensible wizard example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_wizard'),
+ 'access callback' => TRUE,
+ 'description' => 'A general approach to a wizard multistep form.',
+ 'file' => 'form_example_wizard.inc',
+ );
+ $items['examples/form_example/element_example'] = array(
+ 'title' => 'Element example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_example_element_demo_form'),
+ 'access callback' => TRUE,
+ 'file' => 'form_example_elements.inc',
+ 'weight' => 100,
+ );
+
+ return $items;
+}
+
+/**
+ * Page callback for our general info page.
+ */
+function form_example_intro() {
+ $markup = t('The form example module provides a tutorial, extensible multistep example, an element example, and a #states example');
+ return array('#markup' => $markup);
+}
+
+/**
+ * Implements hook_help().
+ */
+function form_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/form_example/tutorial':
+ // TODO: Update the URL.
+ $help = t('This form example tutorial for Drupal 7 is the code from the Handbook 10-step tutorial');
+ break;
+
+ case 'examples/form_example/element_example':
+ $help = t('The Element Example shows how modules can provide their own Form API element types. Four different element types are demonstrated.');
+ break;
+ }
+ if (!empty($help)) {
+ return '
' . $help . '
';
+ }
+}
+
+/**
+ * Implements hook_element_info().
+ *
+ * To keep the various pieces of the example together in external files,
+ * this just returns _form_example_elements().
+ */
+function form_example_element_info() {
+ require_once 'form_example_elements.inc';
+ return _form_example_element_info();
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * The only theme implementation is by the element example. To keep the various
+ * parts of the example together, this actually returns
+ * _form_example_element_theme().
+ */
+function form_example_theme($existing, $type, $theme, $path) {
+ require_once 'form_example_elements.inc';
+ return _form_example_element_theme($existing, $type, $theme, $path);
+}
+/**
+ * @} End of "defgroup form_example".
+ */
diff --git a/sites/all/modules/examples/form_example/form_example.test b/sites/all/modules/examples/form_example/form_example.test
new file mode 100644
index 00000000..7b2206c1
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example.test
@@ -0,0 +1,288 @@
+ 'Form Example',
+ 'description' => 'Various tests on the form_example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules.
+ */
+ public function setUp() {
+ parent::setUp('form_example');
+ }
+
+ /**
+ * Test each tutorial.
+ */
+ public function testTutorials() {
+ // Tutorial #1
+ $this->drupalGet('examples/form_example/tutorial');
+ $this->assertText(t('#9'));
+
+ // #2
+ $this->drupalPost('examples/form_example/tutorial/2', array('name' => t('name')), t('Submit'));
+
+ // #4
+ $this->drupalPost('examples/form_example/tutorial/4',
+ array('first' => t('firstname'), 'last' => t('lastname')), t('Submit'));
+ $this->drupalPost('examples/form_example/tutorial/4', array(), t('Submit'));
+ $this->assertText(t('First name field is required'));
+ $this->assertText(t('Last name field is required'));
+
+ // #5
+ $this->drupalPost('examples/form_example/tutorial/5',
+ array('first' => t('firstname'), 'last' => t('lastname')), t('Submit'));
+ $this->assertText(t('Please enter your first name'));
+ $this->drupalPost('examples/form_example/tutorial/4', array(), t('Submit'));
+ $this->assertText(t('First name field is required'));
+ $this->assertText(t('Last name field is required'));
+
+ // #6
+ $this->drupalPost(
+ 'examples/form_example/tutorial/6',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1955,
+ ),
+ t('Submit'));
+ $this->assertNoText(t('Enter a year between 1900 and 2000'));
+ $this->drupalPost(
+ 'examples/form_example/tutorial/6',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1855,
+ ),
+ t('Submit')
+ );
+
+ $this->assertText(t('Enter a year between 1900 and 2000'));
+
+ // #7
+ $this->drupalPost(
+ 'examples/form_example/tutorial/7',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1955,
+ ),
+ t('Submit')
+ );
+ $this->assertText(t('The form has been submitted. name="firstname lastname", year of birth=1955'));
+ $this->drupalPost(
+ 'examples/form_example/tutorial/7',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1855,
+ ),
+ t('Submit')
+ );
+
+ $this->assertText(t('Enter a year between 1900 and 2000'));
+
+ // Test tutorial #8.
+ $this->drupalPost(
+ 'examples/form_example/tutorial/8',
+ array(
+ 'first' => t('firstname'),
+ 'last' => t('lastname'),
+ 'year_of_birth' => 1955,
+ ),
+ t('Next >>')
+ );
+
+ $this->drupalPost(NULL, array('color' => t('green')), t('<< Back'));
+ $this->drupalPost(NULL, array(), t('Next >>'));
+ $this->drupalPost(NULL, array('color' => t('red')), t('Submit'));
+ $this->assertText(t('The form has been submitted. name="firstname lastname", year of birth=1955'));
+ $this->assertText(t('And the favorite color is red'));
+
+ // #9
+ $url = 'examples/form_example/tutorial/9';
+ for ($i = 1; $i <= 4; $i++) {
+ if ($i > 1) {
+ // Later steps of multistep form take NULL.
+ $url = NULL;
+ }
+ $this->drupalPost(
+ $url,
+ array(
+ "name[$i][first]" => "firstname $i",
+ "name[$i][last]" => "lastname $i",
+ "name[$i][year_of_birth]" => 1950 + $i,
+ ),
+ t('Add another name')
+ );
+ $this->assertText(t('Name #@num', array('@num' => $i + 1)));
+ }
+
+ // Now remove the last name added (#5).
+ $this->drupalPost(NULL, array(), t('Remove latest name'));
+ $this->assertNoText("Name #5");
+
+ $this->drupalPost(NULL, array(), t('Submit'));
+
+ $this->assertText('Form 9 has been submitted');
+ for ($i = 1; $i <= 4; $i++) {
+ $this->assertText(t('@num: firstname @num lastname @num (@year)', array('@num' => $i, '@year' => 1950 + $i)));
+ }
+
+ // #10
+ $url = 'examples/form_example/tutorial/10';
+
+ $this->drupalPost($url, array(), t('Submit'));
+ $this->assertText(t('No file was uploaded.'));
+
+ // Get sample images.
+ $images = $this->drupalGetTestFiles('image');
+ foreach ($images as $image) {
+ $this->drupalPost($url, array('files[file]' => drupal_realpath($image->uri)), t('Submit'));
+ $this->assertText(t('The form has been submitted and the image has been saved, filename: @filename.', array('@filename' => $image->filename)));
+ }
+
+ // #11: Confirmation form.
+ // Try to submit without a name.
+ $url = 'examples/form_example/tutorial/11';
+ $this->drupalPost($url, array(), t('Submit'));
+ $this->assertText('Name field is required.');
+
+ // Verify that we can enter a name and get the confirmation form.
+ $this->drupalPost(
+ $url,
+ array('name' => t('name 1')), t('Submit')
+ );
+ $this->assertText(t('Is this really your name?'));
+ $this->assertFieldById('edit-name', 'name 1');
+
+ // Check the 'yes' button.
+ $confirmation_text = t("Confirmation form submission recieved. According to your submission your name is '@name'", array('@name' => 'name 1'));
+ $url = 'examples/form_example/tutorial/11/confirm/name%201';
+ $this->drupalPost($url, array(), t('This is my name'));
+ $this->assertText($confirmation_text);
+
+ // Check the 'no' button.
+ $this->drupalGet($url);
+ $this->clickLink(t('Nope, not my name'));
+ $this->assertNoText($confirmation_text);
+ }
+
+ /**
+ * Test Wizard tutorial.
+ *
+ * @TODO improve this using drupal_form_submit
+ */
+ public function testWizard() {
+ // Check if the wizard is there.
+ $this->drupalGet('examples/form_example/wizard');
+ $this->assertText(t('Extensible wizard example'));
+
+ $first_name = $this->randomName(8);
+ $last_name = $this->randomName(8);
+ $city = $this->randomName(8);
+ $aunts_name = $this->randomName(8);
+
+ // Submit the first step of the wizard.
+ $options = array(
+ 'first_name' => $first_name,
+ 'last_name' => $last_name,
+ );
+ $this->drupalPost('examples/form_example/wizard', $options, t('Next'));
+
+ // A label city is created, and two buttons appear, Previous and Next.
+ $this->assertText(t('Hint: Do not enter "San Francisco", and do not leave this out.'));
+
+ // Go back to the beginning and verify that the value is there.
+ $this->drupalPost(NULL, array(), t('Previous'));
+ $this->assertFieldByName('first_name', $first_name);
+ $this->assertFieldByName('last_name', $last_name);
+
+ // Go next. We should keep our values.
+ $this->drupalPost(NULL, array(), t('Next'));
+ $this->assertText(t('Hint: Do not enter "San Francisco", and do not leave this out.'));
+
+ // Try "San Francisco".
+ $this->drupalPost(NULL, array('city' => 'San Francisco'), t('Next'));
+ $this->assertText(t('You were warned not to enter "San Francisco"'));
+
+ // Try the real city.
+ $this->drupalPost(NULL, array('city' => $city), t('Next'));
+
+ // Enter the Aunt's name, but then the previous button.
+ $this->drupalPost(NULL, array('aunts_name' => $aunts_name), t('Previous'));
+ $this->assertFieldByName('city', $city);
+
+ // Go to first step and re-check all fields.
+ $this->drupalPost(NULL, array(), t('Previous'));
+ $this->assertFieldByName('first_name', $first_name);
+ $this->assertFieldByName('last_name', $last_name);
+
+ // Re-check second step.
+ $this->drupalPost(NULL, array(), t('Next'));
+ $this->assertText(t('Hint: Do not enter "San Francisco", and do not leave this out.'));
+ $this->assertFieldByName('city', $city);
+
+ // Re-check third step.
+ $this->drupalPost(NULL, array(), t('Next'));
+ $this->assertFieldByName('aunts_name', $aunts_name);
+
+ // Press finish and check for correct values.
+ $this->drupalPost(NULL, array(), t('Finish'));
+
+ $this->assertRaw(t('[first_name] => @first_name', array('@first_name' => $first_name)));
+ $this->assertRaw(t('[last_name] => @last_name', array('@last_name' => $last_name)));
+ $this->assertRaw(t('[city] => @city', array('@city' => $city)));
+ $this->assertRaw(t('[aunts_name] => @aunts_name', array('@aunts_name' => $aunts_name)));
+ }
+
+
+ /**
+ * Test the element_example form for correct behavior.
+ */
+ public function testElementExample() {
+ // Make one basic POST with a set of values and check for correct responses.
+ $edit = array(
+ 'a_form_example_textfield' => $this->randomName(),
+ 'a_form_example_checkbox' => TRUE,
+ 'a_form_example_element_discrete[areacode]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_discrete[prefix]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_discrete[extension]' => sprintf('%04d', rand(0, 9999)),
+ 'a_form_example_element_combined[areacode]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_combined[prefix]' => sprintf('%03d', rand(0, 999)),
+ 'a_form_example_element_combined[extension]' => sprintf('%04d', rand(0, 9999)),
+ );
+ $this->drupalPost('examples/form_example/element_example', $edit, t('Submit'));
+ $this->assertText(t('a_form_example_textfield has value @value', array('@value' => $edit['a_form_example_textfield'])));
+ $this->assertText(t('a_form_example_checkbox has value 1'));
+ $this->assertPattern(t('/areacode.*!areacode/', array('!areacode' => $edit['a_form_example_element_discrete[areacode]'])));
+ $this->assertPattern(t('/prefix.*!prefix/', array('!prefix' => $edit['a_form_example_element_discrete[prefix]'])));
+ $this->assertPattern(t('/extension.*!extension/', array('!extension' => $edit['a_form_example_element_discrete[extension]'])));
+
+ $this->assertText(t('a_form_example_element_combined has value @value', array('@value' => $edit['a_form_example_element_combined[areacode]'] . $edit['a_form_example_element_combined[prefix]'] . $edit['a_form_example_element_combined[extension]'])));
+
+ // Now flip the checkbox and check for correct behavior.
+ $edit['a_form_example_checkbox'] = FALSE;
+ $this->drupalPost('examples/form_example/element_example', $edit, t('Submit'));
+ $this->assertText(t('a_form_example_checkbox has value 0'));
+ }
+}
diff --git a/sites/all/modules/examples/form_example/form_example_elements.inc b/sites/all/modules/examples/form_example/form_example_elements.inc
new file mode 100644
index 00000000..1348a80a
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example_elements.inc
@@ -0,0 +1,531 @@
+ TRUE,
+
+ // Use theme('textfield') to format this element on output.
+ '#theme' => array('textfield'),
+
+ // Do not provide autocomplete.
+ '#autocomplete_path' => FALSE,
+
+ // Allow theme('form_element') to control the markup surrounding this
+ // value on output.
+ '#theme_wrappers' => array('form_element'),
+ );
+
+ // form_example_checkbox is mostly a copy of the system-defined checkbox
+ // element.
+ $types['form_example_checkbox'] = array(
+ // This is an HTML .
+ '#input' => TRUE,
+
+ // @todo: Explain #return_value.
+ '#return_value' => TRUE,
+
+ // Our #process array will use the standard process functions used for a
+ // regular checkbox.
+ '#process' => array('form_process_checkbox', 'ajax_process_form'),
+
+ // Use theme('form_example_checkbox') to render this element on output.
+ '#theme' => 'form_example_checkbox',
+
+ // Use theme('form_element') to provide HTML wrappers for this element.
+ '#theme_wrappers' => array('form_element'),
+
+ // Place the title after the element (to the right of the checkbox).
+ // This attribute affects the behavior of theme_form_element().
+ '#title_display' => 'after',
+
+ // We use the default function name for the value callback, so it does not
+ // have to be listed explicitly. The pattern for the default function name
+ // is form_type_TYPENAME_value().
+ // '#value_callback' => 'form_type_form_example_checkbox_value',
+ );
+
+ // This discrete phonenumber element keeps its values as the separate elements
+ // area code, prefix, extension.
+ $types['form_example_phonenumber_discrete'] = array(
+ // #input == TRUE means that the form value here will be used to determine
+ // what #value will be.
+ '#input' => TRUE,
+
+ // #process is an array of callback functions executed when this element is
+ // processed. Here it provides the child form elements which define
+ // areacode, prefix, and extension.
+ '#process' => array('form_example_phonenumber_discrete_process'),
+
+ // Validation handlers for this element. These are in addition to any
+ // validation handlers that might.
+ '#element_validate' => array('form_example_phonenumber_discrete_validate'),
+ '#autocomplete_path' => FALSE,
+ '#theme_wrappers' => array('form_example_inline_form_element'),
+ );
+
+ // Define form_example_phonenumber_combined, which combines the phone
+ // number into a single validated text string.
+ $types['form_example_phonenumber_combined'] = array(
+ '#input' => TRUE ,
+ '#process' => array('form_example_phonenumber_combined_process'),
+ '#element_validate' => array('form_example_phonenumber_combined_validate'),
+ '#autocomplete_path' => FALSE,
+ '#value_callback' => 'form_example_phonenumber_combined_value',
+ '#default_value' => array(
+ 'areacode' => '',
+ 'prefix' => '',
+ 'extension' => '',
+ ),
+ '#theme_wrappers' => array('form_example_inline_form_element'),
+ );
+ return $types;
+}
+
+
+/**
+ * Value callback for form_example_phonenumber_combined.
+ *
+ * Builds the current combined value of the phone number only when the form
+ * builder is not processing the input.
+ *
+ * @param array $element
+ * Form element.
+ * @param array $input
+ * Input.
+ * @param array $form_state
+ * Form state.
+ *
+ * @return array
+ * The modified element.
+ */
+function form_example_phonenumber_combined_value(&$element, $input = FALSE, $form_state = NULL) {
+ if (!$form_state['process_input']) {
+ $matches = array();
+ $match = preg_match('/^(\d{3})(\d{3})(\d{4})$/', $element['#default_value'], $matches);
+ if ($match) {
+ // Get rid of the "all match" element.
+ array_shift($matches);
+ list($element['areacode'], $element['prefix'], $element['extension']) = $matches;
+ }
+ }
+ return $element;
+}
+
+/**
+ * Value callback for form_example_checkbox element type.
+ *
+ * Copied from form_type_checkbox_value().
+ *
+ * @param array $element
+ * The form element whose value is being populated.
+ * @param mixed $input
+ * The incoming input to populate the form element. If this is FALSE, meaning
+ * there is no input, the element's default value should be returned.
+ *
+ * @return int
+ * The value represented by the form element.
+ */
+function form_type_form_example_checkbox_value($element, $input = FALSE) {
+ if ($input === FALSE) {
+ return isset($element['#default_value']) ? $element['#default_value'] : 0;
+ }
+ else {
+ return isset($input) ? $element['#return_value'] : 0;
+ }
+}
+
+/**
+ * Process callback for the discrete version of phonenumber.
+ */
+function form_example_phonenumber_discrete_process($element, &$form_state, $complete_form) {
+ // #tree = TRUE means that the values in $form_state['values'] will be stored
+ // hierarchically. In this case, the parts of the element will appear in
+ // $form_state['values'] as
+ // $form_state['values']['']['areacode'],
+ // $form_state['values']['']['prefix'],
+ // etc. This technique is preferred when an element has member form
+ // elements.
+ $element['#tree'] = TRUE;
+
+ // Normal FAPI field definitions, except that #value is defined.
+ $element['areacode'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#value' => $element['#value']['areacode'],
+ '#required' => TRUE,
+ '#prefix' => '(',
+ '#suffix' => ')',
+ );
+ $element['prefix'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#required' => TRUE,
+ '#value' => $element['#value']['prefix'],
+ );
+ $element['extension'] = array(
+ '#type' => 'textfield',
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#value' => $element['#value']['extension'],
+ );
+
+ return $element;
+}
+
+/**
+ * Validation handler for the discrete version of the phone number.
+ *
+ * Uses regular expressions to check that:
+ * - the area code is a three digit number.
+ * - the prefix is numeric 3-digit number.
+ * - the extension is a numeric 4-digit number.
+ *
+ * Any problems are shown on the form element using form_error().
+ */
+function form_example_phonenumber_discrete_validate($element, &$form_state) {
+ if (isset($element['#value']['areacode'])) {
+ if (0 == preg_match('/^\d{3}$/', $element['#value']['areacode'])) {
+ form_error($element['areacode'], t('The area code is invalid.'));
+ }
+ }
+ if (isset($element['#value']['prefix'])) {
+ if (0 == preg_match('/^\d{3}$/', $element['#value']['prefix'])) {
+ form_error($element['prefix'], t('The prefix is invalid.'));
+ }
+ }
+ if (isset($element['#value']['extension'])) {
+ if (0 == preg_match('/^\d{4}$/', $element['#value']['extension'])) {
+ form_error($element['extension'], t('The extension is invalid.'));
+ }
+ }
+ return $element;
+}
+
+/**
+ * Process callback for the combined version of the phonenumber element.
+ */
+function form_example_phonenumber_combined_process($element, &$form_state, $complete_form) {
+ // #tree = TRUE means that the values in $form_state['values'] will be stored
+ // hierarchically. In this case, the parts of the element will appear in
+ // $form_state['values'] as
+ // $form_state['values']['']['areacode'],
+ // $form_state['values']['']['prefix'],
+ // etc. This technique is preferred when an element has member form
+ // elements.
+ $element['#tree'] = TRUE;
+
+ // Normal FAPI field definitions, except that #value is defined.
+ $element['areacode'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#required' => TRUE,
+ '#prefix' => '(',
+ '#suffix' => ')',
+ );
+ $element['prefix'] = array(
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#maxlength' => 3,
+ '#required' => TRUE,
+ );
+ $element['extension'] = array(
+ '#type' => 'textfield',
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#required' => TRUE,
+ );
+
+ $matches = array();
+ $match = preg_match('/^(\d{3})(\d{3})(\d{4})$/', $element['#default_value'], $matches);
+ if ($match) {
+ // Get rid of the "all match" element.
+ array_shift($matches);
+ list($element['areacode']['#default_value'], $element['prefix']['#default_value'], $element['extension']['#default_value']) = $matches;
+ }
+
+ return $element;
+}
+
+/**
+ * Phone number validation function for the combined phonenumber.
+ *
+ * Uses regular expressions to check that:
+ * - the area code is a three digit number
+ * - the prefix is numeric 3-digit number
+ * - the extension is a numeric 4-digit number
+ *
+ * Any problems are shown on the form element using form_error().
+ *
+ * The combined value is then updated in the element.
+ */
+function form_example_phonenumber_combined_validate($element, &$form_state) {
+ $lengths = array(
+ 'areacode' => 3,
+ 'prefix' => 3,
+ 'extension' => 4,
+ );
+ foreach ($lengths as $member => $length) {
+ $regex = '/^\d{' . $length . '}$/';
+ if (!empty($element['#value'][$member]) && 0 == preg_match($regex, $element['#value'][$member])) {
+ form_error($element[$member], t('@member is invalid', array('@member' => $member)));
+ }
+ }
+
+ // Consolidate into the three parts into one combined value.
+ $value = $element['areacode']['#value'] . $element['prefix']['#value'] . $element['extension']['#value'];
+ form_set_value($element, $value, $form_state);
+ return $element;
+}
+
+/**
+ * Called by form_example_theme() to provide hook_theme().
+ *
+ * This is kept in this file so it can be with the theme functions it presents.
+ * Otherwise it would get lonely.
+ */
+function _form_example_element_theme() {
+ return array(
+ 'form_example_inline_form_element' => array(
+ 'render element' => 'element',
+ 'file' => 'form_example_elements.inc',
+ ),
+ 'form_example_checkbox' => array(
+ 'render element' => 'element',
+ 'file' => 'form_example_elements.inc',
+ ),
+ );
+}
+
+/**
+ * Themes a custom checkbox.
+ *
+ * This doesn't actually do anything, but is here to show that theming can
+ * be done here.
+ */
+function theme_form_example_checkbox($variables) {
+ $element = $variables['element'];
+ return theme('checkbox', $element);
+}
+
+/**
+ * Formats child form elements as inline elements.
+ */
+function theme_form_example_inline_form_element($variables) {
+ $element = $variables['element'];
+
+ // Add element #id for #type 'item'.
+ if (isset($element['#markup']) && !empty($element['#id'])) {
+ $attributes['id'] = $element['#id'];
+ }
+ // Add element's #type and #name as class to aid with JS/CSS selectors.
+ $attributes['class'] = array('form-item');
+ if (!empty($element['#type'])) {
+ $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-');
+ }
+ if (!empty($element['#name'])) {
+ $attributes['class'][] = 'form-item-' . strtr($element['#name'],
+ array(
+ ' ' => '-',
+ '_' => '-',
+ '[' => '-',
+ ']' => '',
+ )
+ );
+ }
+ // Add a class for disabled elements to facilitate cross-browser styling.
+ if (!empty($element['#attributes']['disabled'])) {
+ $attributes['class'][] = 'form-disabled';
+ }
+ $output = '
' . "\n";
+
+ // If #title is not set, we don't display any label or required marker.
+ if (!isset($element['#title'])) {
+ $element['#title_display'] = 'none';
+ }
+ $prefix = isset($element['#field_prefix']) ? '' . $element['#field_prefix'] . ' ' : '';
+ $suffix = isset($element['#field_suffix']) ? ' ' . $element['#field_suffix'] . '' : '';
+
+ switch ($element['#title_display']) {
+ case 'before':
+ $output .= ' ' . theme('form_element_label', $variables);
+ $output .= ' ' . '
' . $prefix . $element['#children'] . $suffix . "
\n";
+ break;
+
+ case 'invisible':
+ case 'after':
+ $output .= ' ' . $prefix . $element['#children'] . $suffix;
+ $output .= ' ' . theme('form_element_label', $variables) . "\n";
+ break;
+
+ case 'none':
+ case 'attribute':
+ // Output no label and no required marker, only the children.
+ $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
+ break;
+ }
+
+ if (!empty($element['#description'])) {
+ $output .= '
' . $element['#description'] . "
\n";
+ }
+
+ $output .= "
\n";
+
+ return $output;
+}
+
+/**
+ * Form content for examples/form_example/element_example.
+ *
+ * Simple form to demonstrate how to use the various new FAPI elements
+ * we've defined.
+ */
+function form_example_element_demo_form($form, &$form_state) {
+ $form['a_form_example_textfield'] = array(
+ '#type' => 'form_example_textfield',
+ '#title' => t('Form Example textfield'),
+ '#default_value' => variable_get('form_example_textfield', ''),
+ '#description' => t('form_example_textfield is a new type, but it is actually uses the system-provided functions of textfield'),
+ );
+
+ $form['a_form_example_checkbox'] = array(
+ '#type' => 'form_example_checkbox',
+ '#title' => t('Form Example checkbox'),
+ '#default_value' => variable_get('form_example_checkbox', FALSE),
+ '#description' => t('Nothing more than a regular checkbox but with a theme provided by this module.'),
+ );
+
+ $form['a_form_example_element_discrete'] = array(
+ '#type' => 'form_example_phonenumber_discrete',
+ '#title' => t('Discrete phone number'),
+ '#default_value' => variable_get(
+ 'form_example_element_discrete',
+ array(
+ 'areacode' => '999',
+ 'prefix' => '999',
+ 'extension' => '9999',
+ )
+ ),
+ '#description' => t('A phone number : areacode (XXX), prefix (XXX) and extension (XXXX). This one uses a "discrete" element type, one which stores the three parts of the telephone number separately.'),
+ );
+
+ $form['a_form_example_element_combined'] = array(
+ '#type' => 'form_example_phonenumber_combined',
+ '#title' => t('Combined phone number'),
+ '#default_value' => variable_get('form_example_element_combined', '0000000000'),
+ '#description' => t('form_example_element_combined one uses a "combined" element type, one with a single 10-digit value which is broken apart when needed.'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for form_example_element_demo_form().
+ */
+function form_example_element_demo_form_submit($form, &$form_state) {
+ // Exclude unnecessary elements.
+ unset($form_state['values']['submit'], $form_state['values']['form_id'], $form_state['values']['op'], $form_state['values']['form_token'], $form_state['values']['form_build_id']);
+
+ foreach ($form_state['values'] as $key => $value) {
+ variable_set($key, $value);
+ drupal_set_message(
+ t('%name has value %value',
+ array(
+ '%name' => $key,
+ '%value' => print_r($value, TRUE),
+ )
+ )
+ );
+ }
+}
diff --git a/sites/all/modules/examples/form_example/form_example_states.inc b/sites/all/modules/examples/form_example/form_example_states.inc
new file mode 100644
index 00000000..28aed715
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example_states.inc
@@ -0,0 +1,296 @@
+ array(
+ * 'visible' => array(
+ * ':input[name="student_type"]' => array('value' => 'high_school'),
+ * ),
+ * ),
+ * @endcode
+ * Meaning that the element is to be made visible when the condition is met.
+ * The condition is a combination of a jQuery selector (which selects the
+ * element we want to test) and a condition for that element. In this case,
+ * the condition is whether the return value of the 'student_type' element is
+ * 'high_school'. If it is, this element will be visible.
+ *
+ * So the syntax is:
+ * @code
+ * '#states' => array(
+ * 'action_to_take_on_this_form_element' => array(
+ * 'jquery_selector_for_another_element' => array(
+ * 'condition_type' => value,
+ * ),
+ * ),
+ * ),
+ * @endcode
+ *
+ * If you need an action to take place only when two different conditions are
+ * true, then you add both of those conditions to the action. See the
+ * 'country_writein' element below for an example.
+ *
+ * Note that the easiest way to select a textfield, checkbox, or select is with
+ * the
+ * @link http://api.jquery.com/input-selector/ ':input' jquery shortcut @endlink,
+ * which selects any any of those.
+ *
+ * There are examples below of changing or hiding an element when a checkbox
+ * is checked, when a textarea is filled, when a select has a given value.
+ *
+ * See drupal_process_states() for full documentation.
+ *
+ * @see forms_api_reference.html
+ */
+function form_example_states_form($form, &$form_state) {
+ $form['student_type'] = array(
+ '#type' => 'radios',
+ '#options' => array(
+ 'high_school' => t('High School'),
+ 'undergraduate' => t('Undergraduate'),
+ 'graduate' => t('Graduate'),
+ ),
+ '#title' => t('What type of student are you?'),
+ );
+ $form['high_school'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('High School Information'),
+ // This #states rule says that the "high school" fieldset should only
+ // be shown if the "student_type" form element is set to "High School".
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'high_school'),
+ ),
+ ),
+ );
+
+ // High school information.
+ $form['high_school']['tests_taken'] = array(
+ '#type' => 'checkboxes',
+ '#options' => drupal_map_assoc(array(t('SAT'), t('ACT'))),
+ '#title' => t('What standardized tests did you take?'),
+ // This #states rule says that this checkboxes array will be visible only
+ // when $form['student_type'] is set to t('High School').
+ // It uses the jQuery selector :input[name=student_type] to choose the
+ // element which triggers the behavior, and then defines the "High School"
+ // value as the one that triggers visibility.
+ '#states' => array(
+ // Action to take.
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'high_school'),
+ ),
+ ),
+ );
+
+ $form['high_school']['sat_score'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your SAT score:'),
+ '#size' => 4,
+
+ // This #states rule limits visibility to when the $form['tests_taken']
+ // 'SAT' checkbox is checked."
+ '#states' => array(
+ // Action to take.
+ 'visible' => array(
+ ':input[name="tests_taken[SAT]"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+ $form['high_school']['act_score'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your ACT score:'),
+ '#size' => 4,
+
+ // Set this element visible if the ACT checkbox above is checked.
+ '#states' => array(
+ // Action to take.
+ 'visible' => array(
+ ':input[name="tests_taken[ACT]"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ // Undergrad information.
+ $form['undergraduate'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Undergraduate Information'),
+ // This #states rule says that the "undergraduate" fieldset should only
+ // be shown if the "student_type" form element is set to "Undergraduate".
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'undergraduate'),
+ ),
+ ),
+ );
+
+ $form['undergraduate']['how_many_years'] = array(
+ '#type' => 'select',
+ '#title' => t('How many years have you completed?'),
+ // The options here are integers, but since all the action here happens
+ // using the DOM on the client, we will have to use strings to work with
+ // them.
+ '#options' => array(
+ 1 => t('One'),
+ 2 => t('Two'),
+ 3 => t('Three'),
+ 4 => t('Four'),
+ 5 => t('Lots'),
+ ),
+ );
+
+ $form['undergraduate']['comment'] = array(
+ '#type' => 'item',
+ '#description' => t("Wow, that's a long time."),
+ '#states' => array(
+ 'visible' => array(
+ // Note that '5' must be used here instead of the integer 5.
+ // The information is coming from the DOM as a string.
+ ':input[name="how_many_years"]' => array('value' => '5'),
+ ),
+ ),
+ );
+ $form['undergraduate']['school_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your college or university:'),
+ );
+ $form['undergraduate']['school_country'] = array(
+ '#type' => 'select',
+ '#options' => drupal_map_assoc(array(t('UK'), t('Other'))),
+ '#title' => t('In what country is your college or university located?'),
+ );
+ $form['undergraduate']['country_writein'] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#title' => t('Please enter the name of the country where your college or university is located.'),
+
+ // Only show this field if school_country is set to 'Other'.
+ '#states' => array(
+ // Action to take: Make visible.
+ 'visible' => array(
+ ':input[name="school_country"]' => array('value' => t('Other')),
+ ),
+ ),
+ );
+
+ $form['undergraduate']['thanks'] = array(
+ '#type' => 'item',
+ '#description' => t('Thanks for providing both your school and your country.'),
+ '#states' => array(
+ // Here visibility requires that two separate conditions be true.
+ 'visible' => array(
+ ':input[name="school_country"]' => array('value' => t('Other')),
+ ':input[name="country_writein"]' => array('filled' => TRUE),
+ ),
+ ),
+ );
+ $form['undergraduate']['go_away'] = array(
+ '#type' => 'submit',
+ '#value' => t('Done with form'),
+ '#states' => array(
+ // Here visibility requires that two separate conditions be true.
+ 'visible' => array(
+ ':input[name="school_country"]' => array('value' => t('Other')),
+ ':input[name="country_writein"]' => array('filled' => TRUE),
+ ),
+ ),
+ );
+
+ // Graduate student information.
+ $form['graduate'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Graduate School Information'),
+ // This #states rule says that the "graduate" fieldset should only
+ // be shown if the "student_type" form element is set to "Graduate".
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array('value' => 'graduate'),
+ ),
+ ),
+ );
+ $form['graduate']['more_info'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Please describe your graduate studies'),
+ );
+
+ $form['graduate']['info_provide'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Check here if you have provided information above'),
+ '#disabled' => TRUE,
+ '#states' => array(
+ // Mark this checkbox checked if the "more_info" textarea has something
+ // in it, if it's 'filled'.
+ 'checked' => array(
+ ':input[name="more_info"]' => array('filled' => TRUE),
+ ),
+ ),
+ );
+
+ $form['average'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter your average'),
+ // To trigger a state when the same controlling element can have more than
+ // one possible value, put all values in a higher-level array.
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="student_type"]' => array(
+ array('value' => 'high_school'),
+ array('value' => 'undergraduate'),
+ ),
+ ),
+ ),
+ );
+
+ $form['expand_more_info'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Check here if you want to add more information.'),
+ );
+ $form['more_info'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Additional Information'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+
+ // Expand the expand_more_info fieldset if the box is checked.
+ '#states' => array(
+ 'expanded' => array(
+ ':input[name="expand_more_info"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+ $form['more_info']['feedback'] = array(
+ '#type' => 'textarea',
+ '#title' => t('What do you have to say?'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit your information'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for form_example_states_form().
+ */
+function form_example_states_form_submit($form, &$form_state) {
+ drupal_set_message(t('Submitting values: @values', array('@values' => var_export($form_state['values'], TRUE))));
+}
diff --git a/sites/all/modules/examples/form_example/form_example_tutorial.inc b/sites/all/modules/examples/form_example/form_example_tutorial.inc
new file mode 100644
index 00000000..4042129e
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example_tutorial.inc
@@ -0,0 +1,934 @@
+Drupal handbook.');
+}
+
+/**
+ * Tutorial Example 1.
+ *
+ * This first form function is from the
+ * @link http://drupal.org/node/717722 Form Tutorial handbook page @endlink
+ *
+ * It just creates a very basic form with a textfield.
+ *
+ * This function is called the "form constructor function". It builds the form.
+ * It takes a two arguments, $form and $form_state, but if drupal_get_form()
+ * sends additional arguments, they will be provided after $form_state.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_1($form, &$form_state) {
+
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with nothing but a textfield'),
+ );
+ // This is the first form element. It's a textfield with a label, "Name"
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ );
+ return $form;
+}
+
+/**
+ * This is Example 2, a basic form with a submit button.
+ *
+ * @see http://drupal.org/node/717726
+ * @ingroup form_example
+ */
+function form_example_tutorial_2($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A simple form with a submit button'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ );
+
+ // Adds a simple submit button that refreshes the form and clears its
+ // contents. This is the default behavior for forms.
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 3: A basic form with fieldsets.
+ *
+ * We establish a fieldset element and then place two text fields within
+ * it, one for a first name and one for a last name. This helps us group
+ * related content.
+ *
+ * Study the code below and you'll notice that we renamed the array of the first
+ * and last name fields by placing them under the $form['name']
+ * array. This tells Form API these fields belong to the $form['name'] fieldset.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_3($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with a fieldset'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ );
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 4: Basic form with required fields.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_4($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with required fields'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ // Make the fieldset collapsible.
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ // Make these fields required.
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 5: Basic form with additional element attributes.
+ *
+ * This demonstrates additional attributes of text form fields.
+ *
+ * See the
+ * @link http://api.drupal.org/api/file/developer/topics/forms_api.html complete form reference @endlink
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_5($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with additional attributes'),
+ '#description' => t('This one adds #default_value and #description'),
+ );
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ '#default_value' => "First name",
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Example 6: A basic form with a validate handler.
+ *
+ * From http://drupal.org/node/717736
+ * @see form_example_tutorial_6_validate()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_6($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with a validation handler'),
+ );
+
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ '#default_value' => "First name",
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+
+ // New form field added to permit entry of year of birth.
+ // The data entered into this field will be validated with
+ // the default validation function.
+ $form['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => "Year of birth",
+ '#description' => 'Format is "YYYY"',
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Validation handler for Tutorial 6.
+ *
+ * Now we add a handler/function to validate the data entered into the
+ * "year of birth" field to make sure it's between the values of 1900
+ * and 2000. If not, it displays an error. The value report is
+ * $form_state['values'] (see http://drupal.org/node/144132#form-state).
+ *
+ * Notice the name of the function. It is simply the name of the form
+ * followed by '_validate'. This is always the name of the default validation
+ * function. An alternate list of validation functions could have been provided
+ * in $form['#validate'].
+ *
+ * @see form_example_tutorial_6()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_6_validate($form, &$form_state) {
+ $year_of_birth = $form_state['values']['year_of_birth'];
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
+ }
+}
+
+/**
+ * Example 7: With a submit handler.
+ *
+ * From the handbook page:
+ * http://drupal.org/node/717740
+ *
+ * @see form_example_tutorial_7_validate()
+ * @see form_example_tutorial_7_submit()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_7($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with a submit handler'),
+ );
+ $form['name'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ $form['name']['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#required' => TRUE,
+ '#default_value' => "First name",
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ );
+ $form['name']['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#required' => TRUE,
+ );
+ $form['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => "Year of birth",
+ '#description' => 'Format is "YYYY"',
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+
+/**
+ * Validation function for form_example_tutorial_7().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_7_validate($form, &$form_state) {
+ $year_of_birth = $form_state['values']['year_of_birth'];
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
+ }
+}
+
+/**
+ * Submit function for form_example_tutorial_7().
+ *
+ * Adds a submit handler/function to our form to send a successful
+ * completion message to the screen.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_7_submit($form, &$form_state) {
+ drupal_set_message(t('The form has been submitted. name="@first @last", year of birth=@year_of_birth',
+ array(
+ '@first' => $form_state['values']['first'],
+ '@last' => $form_state['values']['last'],
+ '@year_of_birth' => $form_state['values']['year_of_birth'],
+ )
+ ));
+}
+
+/**
+ * Example 8: A simple multistep form with a Next and a Back button.
+ *
+ * Handbook page: http://drupal.org/node/717750.
+ *
+ * For more extensive multistep forms, see
+ * @link form_example_wizard.inc form_example_wizard.inc @endlink
+ *
+ *
+ * Adds logic to our form builder to give it two pages.
+ * The @link ajax_example_wizard AJAX Example's Wizard Example @endlink
+ * gives an AJAX version of this same idea.
+ *
+ * @see form_example_tutorial_8_page_two()
+ * @see form_example_tutorial_8_page_two_back()
+ * @see form_example_tutorial_8_page_two_submit()
+ * @see form_example_tutorial_8_next_submit()
+ * @see form_example_tutorial.inc
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8($form, &$form_state) {
+
+ // Display page 2 if $form_state['page_num'] == 2
+ if (!empty($form_state['page_num']) && $form_state['page_num'] == 2) {
+ return form_example_tutorial_8_page_two($form, $form_state);
+ }
+
+ // Otherwise we build page 1.
+ $form_state['page_num'] = 1;
+
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A basic multistep form (page 1)'),
+ );
+
+ $form['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#description' => "Please enter your first name.",
+ '#size' => 20,
+ '#maxlength' => 20,
+ '#required' => TRUE,
+ '#default_value' => !empty($form_state['values']['first']) ? $form_state['values']['first'] : '',
+ );
+ $form['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last name'),
+ '#default_value' => !empty($form_state['values']['last']) ? $form_state['values']['last'] : '',
+ );
+ $form['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => "Year of birth",
+ '#description' => 'Format is "YYYY"',
+ '#default_value' => !empty($form_state['values']['year_of_birth']) ? $form_state['values']['year_of_birth'] : '',
+ );
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => 'Next >>',
+ '#submit' => array('form_example_tutorial_8_next_submit'),
+ '#validate' => array('form_example_tutorial_8_next_validate'),
+ );
+ return $form;
+}
+
+/**
+ * Returns the form for the second page of form_example_tutorial_8().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_page_two($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A basic multistep form (page 2)'),
+ );
+
+ $form['color'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Favorite color'),
+ '#required' => TRUE,
+ '#default_value' => !empty($form_state['values']['color']) ? $form_state['values']['color'] : '',
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ '#submit' => array('form_example_tutorial_8_page_two_submit'),
+ );
+ $form['back'] = array(
+ '#type' => 'submit',
+ '#value' => t('<< Back'),
+ '#submit' => array('form_example_tutorial_8_page_two_back'),
+ // We won't bother validating the required 'color' field, since they
+ // have to come back to this page to submit anyway.
+ '#limit_validation_errors' => array(),
+ );
+ return $form;
+}
+
+
+/**
+ * Validate handler for the next button on first page.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_next_validate($form, &$form_state) {
+ $year_of_birth = $form_state['values']['year_of_birth'];
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
+ }
+}
+
+/**
+ * Submit handler for form_example_tutorial_8() next button.
+ *
+ * Capture the values from page one and store them away so they can be used
+ * at final submit time.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_next_submit($form, &$form_state) {
+
+ // Values are saved for each page.
+ // to carry forward to subsequent pages in the form.
+ // and we tell FAPI to rebuild the form.
+ $form_state['page_values'][1] = $form_state['values'];
+
+ if (!empty($form_state['page_values'][2])) {
+ $form_state['values'] = $form_state['page_values'][2];
+ }
+
+ // When form rebuilds, it will look at this to figure which page to build.
+ $form_state['page_num'] = 2;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Back button handler submit handler.
+ *
+ * Since #limit_validation_errors = array() is set, values from page 2
+ * will be discarded. We load the page 1 values instead.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_page_two_back($form, &$form_state) {
+ $form_state['values'] = $form_state['page_values'][1];
+ $form_state['page_num'] = 1;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * The page 2 submit handler.
+ *
+ * This is the final submit handler. Gather all the data together and output
+ * it in a drupal_set_message().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_8_page_two_submit($form, &$form_state) {
+ // Normally, some code would go here to alter the database with the data
+ // collected from the form. Instead sets a message with drupal_set_message()
+ // to validate that the code worked.
+ $page_one_values = $form_state['page_values'][1];
+ drupal_set_message(t('The form has been submitted. name="@first @last", year of birth=@year_of_birth',
+ array(
+ '@first' => $page_one_values['first'],
+ '@last' => $page_one_values['last'],
+ '@year_of_birth' => $page_one_values['year_of_birth'],
+ )
+ ));
+
+ if (!empty($page_one_values['first2'])) {
+ drupal_set_message(t('Second name: name="@first @last", year of birth=@year_of_birth',
+ array(
+ '@first' => $page_one_values['first2'],
+ '@last' => $page_one_values['last2'],
+ '@year_of_birth' => $page_one_values['year_of_birth2'],
+ )
+ ));
+ }
+ drupal_set_message(t('And the favorite color is @color', array('@color' => $form_state['values']['color'])));
+
+ // If we wanted to redirect on submission, set $form_state['redirect']. For
+ // simple redirects, the value can be a string of the path to redirect to. For
+ // example, to redirect to /node, one would specify the following:
+ //
+ // $form_state['redirect'] = 'node';
+ //
+ // For more complex redirects, this value can be set to an array of options to
+ // pass to drupal_goto(). For example, to redirect to /foo?bar=1#baz, one
+ // would specify the following:
+ //
+ // @code
+ // $form_state['redirect'] = array(
+ // 'foo',
+ // array(
+ // 'query' => array('bar' => 1),
+ // 'fragment' => 'baz',
+ // ),
+ // );
+ // @endcode
+ //
+ // The first element in the array is the path to redirect to, and the second
+ // element in the array is the array of options. For more information on the
+ // available options, see http://api.drupal.org/url.
+}
+
+/**
+ * Example 9: A form with a dynamically added new fields.
+ *
+ * This example adds default values so that when the form is rebuilt,
+ * the form will by default have the previously-entered values.
+ *
+ * From handbook page http://drupal.org/node/717746.
+ *
+ * @see form_example_tutorial_9_add_name()
+ * @see form_example_tutorial_9_remove_name()
+ * @see form_example_tutorial_9_submit()
+ * @see form_example_tutorial_9_validate()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9($form, &$form_state) {
+
+ // We will have many fields with the same name, so we need to be able to
+ // access the form hierarchically.
+ $form['#tree'] = TRUE;
+
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A form with dynamically added new fields'),
+ );
+
+ if (empty($form_state['num_names'])) {
+ $form_state['num_names'] = 1;
+ }
+
+ // Build the number of name fieldsets indicated by $form_state['num_names']
+ for ($i = 1; $i <= $form_state['num_names']; $i++) {
+ $form['name'][$i] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Name #@num', array('@num' => $i)),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+
+ $form['name'][$i]['first'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First name'),
+ '#description' => t("Enter first name."),
+ '#size' => 20,
+ '#maxlength' => 20,
+ '#required' => TRUE,
+ );
+ $form['name'][$i]['last'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter Last name'),
+ '#required' => TRUE,
+ );
+ $form['name'][$i]['year_of_birth'] = array(
+ '#type' => 'textfield',
+ '#title' => t("Year of birth"),
+ '#description' => t('Format is "YYYY"'),
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+
+ // Adds "Add another name" button.
+ $form['add_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add another name'),
+ '#submit' => array('form_example_tutorial_9_add_name'),
+ );
+
+ // If we have more than one name, this button allows removal of the
+ // last name.
+ if ($form_state['num_names'] > 1) {
+ $form['remove_name'] = array(
+ '#type' => 'submit',
+ '#value' => t('Remove latest name'),
+ '#submit' => array('form_example_tutorial_9_remove_name'),
+ // Since we are removing a name, don't validate until later.
+ '#limit_validation_errors' => array(),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Submit handler for "Add another name" button on form_example_tutorial_9().
+ *
+ * $form_state['num_names'] tells the form builder function how many name
+ * fieldsets to build, so here we increment it.
+ *
+ * All elements of $form_state are persisted, so there's no need to use a
+ * particular key, like the old $form_state['storage']. We can just use
+ * $form_state['num_names'].
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_add_name($form, &$form_state) {
+ // Everything in $form_state is persistent, so we'll just use
+ // $form_state['add_name']
+ $form_state['num_names']++;
+
+ // Setting $form_state['rebuild'] = TRUE causes the form to be rebuilt again.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for "Remove name" button on form_example_tutorial_9().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_remove_name($form, &$form_state) {
+ if ($form_state['num_names'] > 1) {
+ $form_state['num_names']--;
+ }
+
+ // Setting $form_state['rebuild'] = TRUE causes the form to be rebuilt again.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Validate function for form_example_tutorial_9().
+ *
+ * Adds logic to validate the form to check the validity of the new fields,
+ * if they exist.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_validate($form, &$form_state) {
+
+ for ($i = 1; $i <= $form_state['num_names']; $i++) {
+ $year_of_birth = $form_state['values']['name'][$i]['year_of_birth'];
+
+ if ($year_of_birth && ($year_of_birth < 1900 || $year_of_birth > 2000)) {
+ form_set_error("name][$i][year_of_birth", t('Enter a year between 1900 and 2000.'));
+ }
+ }
+}
+
+/**
+ * Submit function for form_example_tutorial_9().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_9_submit($form, &$form_state) {
+ $output = t("Form 9 has been submitted.");
+ for ($i = 1; $i <= $form_state['num_names']; $i++) {
+ $output .= t("@num: @first @last (@date)...",
+ array(
+ '@num' => $i,
+ '@first' => $form_state['values']['name'][$i]['first'],
+ '@last' => $form_state['values']['name'][$i]['last'],
+ '@date' => $form_state['values']['name'][$i]['year_of_birth'],
+ )
+ ) . ' ';
+ }
+ drupal_set_message($output);
+}
+
+/**
+ * Example 10: A form with a file upload field.
+ *
+ * This example allows the user to upload a file to Drupal which is stored
+ * physically and with a reference in the database.
+ *
+ * @see form_example_tutorial_10_submit()
+ * @see form_example_tutorial_10_validate()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_10($form_state) {
+ // If you are familiar with how browsers handle files, you know that
+ // enctype="multipart/form-data" is required. Drupal takes care of that, so
+ // you don't need to include it yourself.
+ $form['file'] = array(
+ '#type' => 'file',
+ '#title' => t('Image'),
+ '#description' => t('Upload a file, allowed extensions: jpg, jpeg, png, gif'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for form_example_tutorial_10().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_10_validate($form, &$form_state) {
+ $file = file_save_upload('file', array(
+ // Validates file is really an image.
+ 'file_validate_is_image' => array(),
+ // Validate extensions.
+ 'file_validate_extensions' => array('png gif jpg jpeg'),
+ ));
+ // If the file passed validation:
+ if ($file) {
+ // Move the file into the Drupal file system.
+ if ($file = file_move($file, 'public://')) {
+ // Save the file for use in the submit handler.
+ $form_state['storage']['file'] = $file;
+ }
+ else {
+ form_set_error('file', t("Failed to write the uploaded file to the site's file folder."));
+ }
+ }
+ else {
+ form_set_error('file', t('No file was uploaded.'));
+ }
+}
+
+/**
+ * Submit handler for form_example_tutorial_10().
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_10_submit($form, &$form_state) {
+ $file = $form_state['storage']['file'];
+ // We are done with the file, remove it from storage.
+ unset($form_state['storage']['file']);
+ // Make the storage of the file permanent.
+ $file->status = FILE_STATUS_PERMANENT;
+ // Save file status.
+ file_save($file);
+ // Set a response to the user.
+ drupal_set_message(t('The form has been submitted and the image has been saved, filename: @filename.', array('@filename' => $file->filename)));
+}
+
+/**
+ * Example 11: adding a confirmation form.
+ *
+ * This example generates a simple form that, when submitted, directs
+ * the user to a confirmation form generated using the confirm_form function.
+ * It asks the user to verify that the name they input was correct
+ *
+ * @see form_example_tutorial_11_submit()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11($form, &$form_state) {
+ // This form is identical to the one in example 2 except for one thing: We are
+ // adding an #action tag to direct the form submission to a confirmation page.
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('A set of two forms that demonstrate the confirm_form function. This form has an explicit action to direct the form to a confirmation page'),
+ );
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#required' => TRUE,
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ );
+ return $form;
+}
+
+/**
+ * Submit function for form_example_tutorial_11().
+ *
+ * Adds a submit handler/function to our form to redirect
+ * the user to a confirmation page.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11_submit($form, &$form_state) {
+ // Simple submit function that changes the redirect of the form based on the
+ // value of the name field.
+ $name = $form_state['values']['name'];
+ $form_state['redirect'] = 'examples/form_example/tutorial/11/confirm/' . urlencode($name);
+}
+
+/**
+ * Example 11: A form generated with confirm_form().
+ *
+ * This function generates the confirmation form using the confirm_form()
+ * function. If confirmed, it sets a drupal message to demonstrate it's success.
+ *
+ * @param string $name
+ * The urlencoded name entered by the user.
+ *
+ * @see form_example_tutorial_11_confirm_name_submit()
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11_confirm_name($form, $form_state, $name) {
+ // confirm_form() returns a complete form array for confirming an action.
+ // It has 7 arguments: $form, $question, $path, $description, $yes, $no, and
+ // $name.
+ // - $form: Additional elements to add to the form that will be available in
+ // the submit handler.
+ // - $question: What is the user confirming? This will be the title of the
+ // page.
+ // - $path: Where should the page go if the user hits cancel?
+ // - $description = NULL: Additional text to display.
+ // - $yes = NULL: Anchor text for the confirmation button. Defaults to
+ // t('Confirm').
+ // - $no = NULL: Anchor text for the cancel link. Defaults to t('Cancel').
+ // - $name = 'confirm': The internal name used to refer to the confirmation
+ // item.
+
+
+
+ // First we make a textfield for our user's name. confirm_form() allows us to
+ // Add form elements to the confirmation form, so we'll take advangage of
+ // that.
+ $user_name_text_field = array(
+ 'name' => array(
+ '#type' => 'textfield',
+ // We don't want the user to be able to edit their name here.
+ '#disabled' => TRUE,
+ '#title' => t('Your name:'),
+ '#value' => urldecode($name),
+ ),
+ );
+
+ // The question to ask the user.
+ $confirmation_question = t('Is this really your name?');
+
+ // If the user clicks 'no,' they're sent to this path.
+ $cancel_path = 'examples/form_example/tutorial/11';
+
+ // Some helpful descriptive text.
+ $description = t('Please verify whether or not you have input your name correctly. If you verify you will be sent back to the form and a message will be set. Otherwise you will be sent to the same page but with no message.');
+
+ // These are the text for our yes and no buttons.
+ $yes_button = t('This is my name');
+ $no_button = t('Nope, not my name');
+
+ // The name Form API will use to refer to our confirmation form.
+ $confirm_name = 'confirm_example';
+
+ // Finally, call confirm_form() with our information, and then return the form
+ // array it gives us.
+ return confirm_form(
+ $user_name_text_field,
+ $confirmation_question,
+ $cancel_path,
+ $description,
+ $yes_button,
+ $no_button,
+ $confirm_name
+ );
+}
+
+/**
+ * Submit function for form_example_tutorial_11_confirm_form().
+ *
+ * Adds a submit handler/function to the confirmation form
+ * if this point is reached the submission has been confirmed
+ * so we will set a message to demonstrate the success.
+ *
+ * @ingroup form_example
+ */
+function form_example_tutorial_11_confirm_name_submit($form, &$form_state) {
+ drupal_set_message(t("Confirmation form submission recieved. According to your submission your name is '@name'", array("@name" => $form_state['values']['name'])));
+ $form_state['redirect'] = 'examples/form_example/tutorial/11';
+}
diff --git a/sites/all/modules/examples/form_example/form_example_wizard.inc b/sites/all/modules/examples/form_example/form_example_wizard.inc
new file mode 100644
index 00000000..b5ada158
--- /dev/null
+++ b/sites/all/modules/examples/form_example/form_example_wizard.inc
@@ -0,0 +1,325 @@
+ array(
+ 'form' => 'form_example_wizard_personal_info',
+ ),
+ 2 => array(
+ 'form' => 'form_example_wizard_location_info',
+ ),
+ 3 => array(
+ 'form' => 'form_example_wizard_other_info',
+ ),
+ );
+}
+
+/**
+ * The primary formbuilder function for the wizard form.
+ *
+ * This is the form that you should call with drupal_get_form() from your code,
+ * and it will include the rest of the step forms defined. You are not required
+ * to change this function, as this will handle all the step actions for you.
+ *
+ * This form has two defined submit handlers to process the different steps:
+ * - Previous: handles the way to get back one step in the wizard.
+ * - Next: handles each step form submission,
+ *
+ * The third handler, the finish button handler, is the default form_submit
+ * handler used to process the information.
+ *
+ * You are not required to change the next or previous handlers, but you must
+ * change the form_example_wizard_submit handler to perform the operations you
+ * need on the collected information.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard($form, &$form_state) {
+
+ // Initialize a description of the steps for the wizard.
+ if (empty($form_state['step'])) {
+ $form_state['step'] = 1;
+
+ // This array contains the function to be called at each step to get the
+ // relevant form elements. It will also store state information for each
+ // step.
+ $form_state['step_information'] = _form_example_steps();
+ }
+ $step = &$form_state['step'];
+ drupal_set_title(t('Extensible Wizard: Step @step', array('@step' => $step)));
+
+ // Call the function named in $form_state['step_information'] to get the
+ // form elements to display for this step.
+ $form = $form_state['step_information'][$step]['form']($form, $form_state);
+
+ // 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 ($step > 1) {
+ $form['prev'] = array(
+ '#type' => 'submit',
+ '#value' => t('Previous'),
+ '#name' => 'prev',
+ '#submit' => array('form_example_wizard_previous_submit'),
+ '#limit_validation_errors' => array(),
+ );
+ }
+
+ // Show the Next button only if there are more steps defined.
+ if ($step < count($form_state['step_information'])) {
+ // The Next button should be included on every step.
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Next'),
+ '#name' => 'next',
+ '#submit' => array('form_example_wizard_next_submit'),
+ );
+ }
+ else {
+ // Just in case there are no more steps, we use the default submit handler
+ // of the form wizard. Call this button Finish, Submit, or whatever you
+ // want to show. When this button is clicked, the
+ // form_example_wizard_submit handler will be called.
+ $form['finish'] = array(
+ '#type' => 'submit',
+ '#value' => t('Finish'),
+ );
+ }
+
+ // Include each validation function defined for the different steps.
+ if (function_exists($form_state['step_information'][$step]['form'] . '_validate')) {
+ $form['next']['#validate'] = array($form_state['step_information'][$step]['form'] . '_validate');
+ }
+
+ return $form;
+}
+
+/**
+ * Submit handler for the "previous" button.
+ *
+ * This function:
+ * - Stores away $form_state['values']
+ * - Decrements the step counter
+ * - Replaces $form_state['values'] with the values from the previous state.
+ * - Forces form rebuild.
+ *
+ * You are not required to change this function.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_previous_submit($form, &$form_state) {
+ $current_step = &$form_state['step'];
+ $form_state['step_information'][$current_step]['stored_values'] = $form_state['input'];
+ if ($current_step > 1) {
+ $current_step--;
+ $form_state['values'] = $form_state['step_information'][$current_step]['stored_values'];
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for the 'next' button.
+ *
+ * This function:
+ * - Saves away $form_state['values']
+ * - Increments the step count.
+ * - Replace $form_state['values'] from the last time we were at this page
+ * or with array() if we haven't been here before.
+ * - Force form rebuild.
+ *
+ * You are not required to change this function.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_next_submit($form, &$form_state) {
+ $current_step = &$form_state['step'];
+ $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];
+
+ if ($current_step < count($form_state['step_information'])) {
+ $current_step++;
+ if (!empty($form_state['step_information'][$current_step]['stored_values'])) {
+ $form_state['values'] = $form_state['step_information'][$current_step]['stored_values'];
+ }
+ else {
+ $form_state['values'] = array();
+ }
+ // Force rebuild with next step.
+ $form_state['rebuild'] = TRUE;
+ return;
+ }
+}
+
+/**
+ * The previous code was a 'skeleton' of a multistep wizard form. You are not
+ * required to change a line on the previous code (apart from defining your own
+ * steps in the _form_example_steps() function.
+ *
+ * All the code included from here is the content of the wizard, the steps of
+ * the form.
+ *
+ * First, let's show the defined steps for the wizard example.
+ * @ingroup form_example
+ */
+
+/**
+ * Returns form elements for the 'personal info' page of the wizard.
+ *
+ * This is the first step of the wizard, asking for two textfields: first name
+ * and last name.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_personal_info($form, &$form_state) {
+ $form = array();
+ $form['first_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('First Name'),
+ '#default_value' => !empty($form_state['values']['first_name']) ? $form_state['values']['first_name'] : '',
+ );
+ $form['last_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Last Name'),
+ '#default_value' => !empty($form_state['values']['last_name']) ? $form_state['values']['last_name'] : '',
+ );
+ return $form;
+}
+
+/**
+ * Returns form elements for the 'location info' page of the wizard.
+ *
+ * This is the second step of the wizard. This step asks for a textfield value:
+ * a City. This step also includes a validation declared later.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_location_info($form, &$form_state) {
+ $form = array();
+ $form['city'] = array(
+ '#type' => 'textfield',
+ '#title' => t('City'),
+ '#description' => t('Hint: Do not enter "San Francisco", and do not leave this out.'),
+ '#required' => TRUE,
+ '#default_value' => !empty($form_state['values']['city']) ? $form_state['values']['city'] : '',
+
+ );
+ return $form;
+}
+
+/**
+ * Custom validation form for the 'location info' page of the wizard.
+ *
+ * This is the validation function for the second step of the wizard.
+ * The city cannot be empty or be "San Francisco".
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_location_info_validate($form, &$form_state) {
+ if ($form_state['values']['city'] == 'San Francisco') {
+ form_set_error('city', t('You were warned not to enter "San Francisco"'));
+ }
+}
+
+/**
+ * Returns form elements for the 'other info' page of the wizard.
+ *
+ * This is the third and last step of the example wizard.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_other_info($form, &$form_state) {
+ $form = array();
+ $form['aunts_name'] = array(
+ '#type' => 'textfield',
+ '#title' => t("Your first cousin's aunt's Social Security number"),
+ '#default_value' => !empty($form_state['values']['aunts_name']) ? $form_state['values']['aunts_name'] : '',
+ );
+ return $form;
+}
+
+/**
+ * Wizard form submit handler.
+ *
+ * This function:
+ * - Saves away $form_state['values']
+ * - Process all the form values.
+ *
+ * And now comes the magic of the wizard, the function that should handle all
+ * the inputs from the user on each different step.
+ *
+ * This demonstration handler just do a drupal_set_message() with the
+ * information collected on each different step of the wizard.
+ *
+ * @ingroup form_example
+ */
+function form_example_wizard_submit($form, &$form_state) {
+ $current_step = &$form_state['step'];
+ $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];
+
+ // In this case we've completed the final page of the wizard, so process the
+ // submitted information.
+ drupal_set_message(t('This information was collected by this wizard:'));
+ foreach ($form_state['step_information'] as $index => $value) {
+ // Remove FAPI fields included in the values (form_token, form_id and
+ // form_build_id. This is not required, you may access the values using
+ // $value['stored_values'] but I'm removing them to make a more clear
+ // representation of the collected information as the complete array will
+ // be passed through drupal_set_message().
+ unset($value['stored_values']['form_id']);
+ unset($value['stored_values']['form_build_id']);
+ unset($value['stored_values']['form_token']);
+
+ // Now show all the values.
+ drupal_set_message(t('Step @num collected the following values:
@result
',
+ array(
+ '@num' => $index,
+ '@result' => print_r($value['stored_values'], TRUE),
+ )
+ ));
+ }
+}
diff --git a/sites/all/modules/examples/image_example/image_example.info b/sites/all/modules/examples/image_example/image_example.info
new file mode 100644
index 00000000..02d6346f
--- /dev/null
+++ b/sites/all/modules/examples/image_example/image_example.info
@@ -0,0 +1,19 @@
+name = Image Example
+description = Example implementation of image.module hooks.
+package = Example modules
+core = 7.x
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:image
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = image_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/image_example/image_example.install b/sites/all/modules/examples/image_example/image_example.install
new file mode 100644
index 00000000..89e5f6fa
--- /dev/null
+++ b/sites/all/modules/examples/image_example/image_example.install
@@ -0,0 +1,54 @@
+ 'Image Example',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_example_style_form'),
+ 'access arguments' => array('access content'),
+ 'file' => 'image_example.pages.inc',
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_help().
+ */
+function image_example_help($path) {
+ switch ($path) {
+ case 'image_example/styles':
+ $output = '
' . t('Use this form to upload an image and choose an Image Style to use when displaying the image. This demonstrates basic use of the Drupal 7 Image styles & effects system.') . '
';
+ $output .= '
' . t('Image styles can be added/edited using the !link.', array('!link' => l(t('Image styles UI'), 'admin/config/media/image-styles'))) . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_image_default_styles().
+ *
+ * hook_image_default_styles() declares to Drupal any image styles that are
+ * provided by the module. An image style is a collection of image effects that
+ * are performed in a specified order, manipulating the image and generating a
+ * new derivative image.
+ *
+ * This hook can be used to declare image styles that your module depends on or
+ * allow you to define image styles in code and gain the benefits of using
+ * a version control system.
+ */
+function image_example_image_default_styles() {
+ // This hook returns an array, each component of which describes an image
+ // style. The array keys are the machine-readable image style names and
+ // to avoid namespace conflicts should begin with the name of the
+ // implementing module. e.g.) 'mymodule_stylename'. Styles names should
+ // use only alpha-numeric characters, underscores (_), and hyphens (-).
+ $styles = array();
+ $styles['image_example_style'] = array();
+
+ // Each style array consists of an 'effects' array that is made up of
+ // sub-arrays which define the individual image effects that are combined
+ // together to create the image style.
+ $styles['image_example_style']['effects'] = array(
+ array(
+ // Name of the image effect. See image_image_effect_info() in
+ // modules/image/image.effects.inc for a list of image effects available
+ // in Drupal 7 core.
+ 'name' => 'image_scale',
+ // Arguments to pass to the effect callback function.
+ // The arguments that an effect accepts are documented with each
+ // individual image_EFFECT_NAME_effect function. See image_scale_effect()
+ // for an example.
+ 'data' => array(
+ 'width' => 100,
+ 'height' => 100,
+ 'upscale' => 1,
+ ),
+ // The order in which image effects should be applied when using this
+ // style.
+ 'weight' => 0,
+ ),
+ // Add a second effect to this image style. Effects are executed in order
+ // and are cumulative. When applying an image style to an image the result
+ // will be the combination of all effects associated with that style.
+ array(
+ 'name' => 'image_example_colorize',
+ 'data' => array(
+ 'color' => '#FFFF66',
+ ),
+ 'weight' => 1,
+ ),
+ );
+
+ return $styles;
+}
+
+/**
+ * Implements hook_image_style_save().
+ *
+ * Allows modules to respond to updates to an image style's
+ * settings.
+ */
+function image_example_image_style_save($style) {
+ // The $style parameter is an image style array with one notable exception.
+ // When a user has chosen to replace a deleted style with another style the
+ // $style['name'] property contains the name of the replacement style and
+ // $style['old_name'] contains the name of the style being deleted.
+ //
+ // Here we update a variable that contains the name of the image style that
+ // the block provided by this module uses when formatting images to use the
+ // new user chosen style name.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('image_example_style_name', '')) {
+ variable_set('image_example_style_name', $style['name']);
+ }
+}
+
+/**
+ * Implements hook_image_style_delete().
+ *
+ * This hook allows modules to respond to image styles being deleted.
+ *
+ * @see image_example_style_save()
+ */
+function image_example_image_style_delete($style) {
+ // See information about $style paramater in documentation for
+ // image_example_style_save().
+ //
+ // Update the modules variable that contains the name of the image style
+ // being deleted to the name of the replacement style.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('image_example_style_name', '')) {
+ variable_set('image_example_style_name', $style['name']);
+ }
+}
+
+/**
+ * Implements hook_image_style_flush().
+ *
+ * This hook allows modules to respond when a style is being flushed. Styles
+ * are flushed any time a style is updated, an effect associated with the style
+ * is updated, a new effect is added to the style, or an existing effect is
+ * removed.
+ *
+ * Flushing removes all images generated using this style from the host. Once a
+ * style has been flushed derivative images will need to be regenerated. New
+ * images will be generated automatically as needed but it is worth noting that
+ * on a busy site with lots of images this could have an impact on performance.
+ *
+ * Note: This function does not currently have any effect as the example module
+ * does not use any caches. It is demonstrated here for completeness sake only.
+ */
+function image_example_style_flush($style) {
+ // Empty any caches populated by our module that could contain stale data
+ // after the style has been flushed. Stale data occurs because the module may
+ // have cached content with a reference to the derivative image which is
+ // being deleted.
+ cache_clear_all('*', 'image_example', TRUE);
+}
+
+/**
+ * Implements hook_image_styles_alter().
+ *
+ * Allows your module to modify, add, or remove image styles provided
+ * by other modules. The best use of this hook is to modify default styles that
+ * have not been overriden by the user. Altering styles that have been
+ * overriden by the user could have an adverse affect on the user experience.
+ * If you add an effect to a style through this hook and the user attempts to
+ * remove the effect it will immediatly be re-applied.
+ */
+function image_example_image_styles_alter(&$styles) {
+ // The $styles paramater is an array of image style arrays keyed by style
+ // name. You can check to see if a style has been overriden by checking the
+ // $styles['stylename']['storage'] property.
+ // Verify that the effect has not been overriden.
+ if ($styles['thumbnail']['storage'] == IMAGE_STORAGE_DEFAULT) {
+ // Add an additional colorize effect to the system provided thumbnail
+ // effect.
+ $styles['thumbnail']['effects'][] = array(
+ 'label' => t('Colorize #FFFF66'),
+ 'name' => 'image_example_colorize',
+ 'effect callback' => 'image_example_colorize_effect',
+ 'data' => array(
+ 'color' => '#FFFF66',
+ ),
+ 'weight' => 1,
+ );
+ }
+}
+
+/**
+ * Implements hook_image_effect_info().
+ *
+ * This hook allows your module to define additional image manipulation effects
+ * that can be used with image styles.
+ */
+function image_example_image_effect_info() {
+ $effects = array();
+
+ // The array is keyed on the machine-readable effect name.
+ $effects['image_example_colorize'] = array(
+ // Human readable name of the effect.
+ 'label' => t('Colorize'),
+ // (optional) Brief description of the effect that will be shown when
+ // adding or configuring this image effect.
+ 'help' => t('The colorize effect will first remove all color from the source image and then tint the image using the color specified.'),
+ // Name of function called to perform this effect.
+ 'effect callback' => 'image_example_colorize_effect',
+ // (optional) Name of function that provides a $form array with options for
+ // configuring the effect. Note that you only need to return the fields
+ // specific to your module. Submit buttons will be added automatically, and
+ // configuration options will be serailized and added to the 'data' element
+ // of the effect. The function will recieve the $effect['data'] array as
+ // its only parameter.
+ 'form callback' => 'image_example_colorize_form',
+ // (optional) Name of a theme function that will output a summary of this
+ // effects configuation. Used when displaying list of effects associated
+ // with an image style. In this example the function
+ // theme_image_example_colorize_summary will be called via the theme()
+ // function. Your module must also implement hook_theme() in order for this
+ // function to work correctly. See image_example_theme() and
+ // theme_image_example_colorize_summary().
+ 'summary theme' => 'image_example_colorize_summary',
+ );
+
+ return $effects;
+}
+
+/**
+ * Form Builder; Configuration settings for colorize effect.
+ *
+ * Create a $form array with the fields necessary for configuring the
+ * image_example_colorize effect.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the colorize options. Therefore it does not not need to
+ * include metadata about the effect, nor a submit button.
+ *
+ * @param array $data
+ * The current configuration for this colorize effect.
+ */
+function image_example_colorize_form($data) {
+ $form = array();
+ // You do not need to worry about handling saving/updating/deleting of the
+ // data collected. The image module will automatically serialize and store
+ // all data associated with an effect.
+ $form['color'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Color'),
+ '#description' => t('The color to use when colorizing the image. Use web-style hex colors. e.g.) #FF6633.'),
+ '#default_value' => isset($data['color']) ? $data['color'] : '',
+ '#size' => 7,
+ '#max_length' => 7,
+ '#required' => TRUE,
+ );
+ return $form;
+}
+
+/**
+ * Image effect callback; Colorize an image resource.
+ *
+ * @param object $image
+ * An image object returned by image_load().
+ * @param array $data
+ * An array of attributes to use when performing the colorize effect with the
+ * following items:
+ * - "color": The web-style hex color to use when colorizing the image.
+ *
+ * @return bool
+ * TRUE on success. FALSE on failure to colorize image.
+ */
+function image_example_colorize_effect(&$image, $data) {
+ // Image manipulation should be done to the $image->resource, which will be
+ // automatically saved as a new image once all effects have been applied.
+ // If your effect makes changes to the $image->resource that relate to any
+ // information stored in the $image->info array (width, height, etc.) you
+ // should update that information as well. See modules/system/image.gd.inc
+ // for examples of functions that perform image manipulations.
+ //
+ // Not all GD installations are created equal. It is a good idea to check for
+ // the existence of image manipulation functions before using them.
+ // PHP installations using non-bundled GD do not have imagefilter(). More
+ // information about image manipulation functions is available in the PHP
+ // manual. http://www.php.net/manual/en/book.image.php
+ if (!function_exists('imagefilter')) {
+ watchdog('image', 'The image %image could not be colorized because the imagefilter() function is not available in this PHP installation.', array('%file' => $image->source));
+ return FALSE;
+ }
+
+ // Verify that Drupal is using the PHP GD library for image manipulations
+ // since this effect depends on functions in the GD library.
+ if ($image->toolkit != 'gd') {
+ watchdog('image', 'Image colorize failed on %path. Using non GD toolkit.', array('%path' => $image->source), WATCHDOG_ERROR);
+ return FALSE;
+ }
+
+ // Convert short #FFF syntax to full #FFFFFF syntax.
+ if (strlen($data['color']) == 4) {
+ $c = $data['color'];
+ $data['color'] = $c[0] . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
+ }
+
+ // Convert #FFFFFF syntax to hexadecimal colors.
+ $data['color'] = hexdec(str_replace('#', '0x', $data['color']));
+
+ // Convert the hexadecimal color value to a color index value.
+ $rgb = array();
+ for ($i = 16; $i >= 0; $i -= 8) {
+ $rgb[] = (($data['color'] >> $i) & 0xFF);
+ }
+
+ // First desaturate the image, and then apply the new color.
+ imagefilter($image->resource, IMG_FILTER_GRAYSCALE);
+ imagefilter($image->resource, IMG_FILTER_COLORIZE, $rgb[0], $rgb[1], $rgb[2]);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function image_example_theme() {
+ return array(
+ 'image_example_colorize_summary' => array(
+ 'variables' => array('data' => NULL),
+ ),
+ 'image_example_image' => array(
+ 'variables' => array('image' => NULL, 'style' => NULL),
+ 'file' => 'image_example.pages.inc',
+ ),
+ );
+}
+
+/**
+ * Formats a summary of an image colorize effect.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - data: The current configuration for this colorize effect.
+ */
+function theme_image_example_colorize_summary($variables) {
+ $data = $variables['data'];
+ return t('as color #@color.', array('@color' => $data['color']));
+}
+/**
+ * @} End of "defgroup image_example".
+ */
diff --git a/sites/all/modules/examples/image_example/image_example.pages.inc b/sites/all/modules/examples/image_example/image_example.pages.inc
new file mode 100644
index 00000000..d984defc
--- /dev/null
+++ b/sites/all/modules/examples/image_example/image_example.pages.inc
@@ -0,0 +1,166 @@
+ theme('image_example_image', array('image' => $image, 'style' => $style)),
+ );
+ }
+
+ // Use the #managed_file FAPI element to upload an image file.
+ $form['image_example_image_fid'] = array(
+ '#title' => t('Image'),
+ '#type' => 'managed_file',
+ '#description' => t('The uploaded image will be displayed on this page using the image style chosen below.'),
+ '#default_value' => variable_get('image_example_image_fid', ''),
+ '#upload_location' => 'public://image_example_images/',
+ );
+
+ // Provide a select field for choosing an image style to use when displaying
+ // the image.
+ $form['image_example_style_name'] = array(
+ '#title' => t('Image style'),
+ '#type' => 'select',
+ '#description' => t('Choose an image style to use when displaying this image.'),
+ // The image_style_options() function returns an array of all available
+ // image styles both the key and the value of the array are the image
+ // style's name. The function takes on paramater, a boolean flag
+ // signifying whether or not the array should include a option.
+ '#options' => image_style_options(TRUE),
+ '#default_value' => variable_get('image_example_style_name', ''),
+ );
+
+ // Submit Button.
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+
+ return $form;
+}
+
+/**
+ * Verifies that the user supplied an image with the form..
+ *
+ * @ingroup image_example
+ */
+function image_example_style_form_validate($form, &$form_state) {
+ if (!isset($form_state['values']['image_example_image_fid']) || !is_numeric($form_state['values']['image_example_image_fid'])) {
+ form_set_error('image_example_image_fid', t('Please select an image to upload.'));
+ }
+}
+
+/**
+ * Form Builder; Display a form for uploading an image.
+ *
+ * @ingroup image_example
+ */
+function image_example_style_form_submit($form, &$form_state) {
+ // When using the #managed_file form element the file is automatically
+ // uploaded an saved to the {file} table. The value of the corresponding
+ // form element is set to the {file}.fid of the new file.
+ //
+ // If fid is not 0 we have a valid file.
+ if ($form_state['values']['image_example_image_fid'] != 0) {
+ // The new file's status is set to 0 or temporary and in order to ensure
+ // that the file is not removed after 6 hours we need to change it's status
+ // to 1. Save the ID of the uploaded image for later use.
+ $file = file_load($form_state['values']['image_example_image_fid']);
+ $file->status = FILE_STATUS_PERMANENT;
+ file_save($file);
+
+ // When a module is managing a file, it must manage the usage count.
+ // Here we increment the usage count with file_usage_add().
+ file_usage_add($file, 'image_example', 'sample_image', 1);
+
+ // Save the fid of the file so that the module can reference it later.
+ variable_set('image_example_image_fid', $file->fid);
+ drupal_set_message(t('The image @image_name was uploaded and saved with an ID of @fid and will be displayed using the style @style.',
+ array(
+ '@image_name' => $file->filename,
+ '@fid' => $file->fid,
+ '@style' => $form_state['values']['image_example_style_name'],
+ )
+ ));
+ }
+ // If the file was removed we need to remove the module's reference to the
+ // removed file's fid, and remove the file.
+ elseif ($form_state['values']['image_example_image_fid'] == 0) {
+ // Retrieve the old file's id.
+ $fid = variable_get('image_example_image_fid', FALSE);
+ $file = $fid ? file_load($fid) : FALSE;
+ if ($file) {
+ // When a module is managing a file, it must manage the usage count.
+ // Here we decrement the usage count with file_usage_delete().
+ file_usage_delete($file, 'image_example', 'sample_image', 1);
+
+ // The file_delete() function takes a file object and checks to see if
+ // the file is being used by any other modules. If it is the delete
+ // operation is cancelled, otherwise the file is deleted.
+ file_delete($file);
+ }
+
+ // Either way the module needs to update it's reference since even if the
+ // file is in use by another module and not deleted we no longer want to
+ // use it.
+ variable_set('image_example_image_fid', FALSE);
+ drupal_set_message(t('The image @image_name was removed.', array('@image_name' => $file->filename)));
+ }
+
+ // Save the name of the image style chosen by the user.
+ variable_set('image_example_style_name', $form_state['values']['image_example_style_name']);
+}
+
+/**
+ * Theme function displays an image rendered using the specified style.
+ *
+ * @ingroup image_example
+ */
+function theme_image_example_image($variables) {
+ $image = $variables['image'];
+ $style = $variables['style'];
+
+ // theme_image_style() is the primary method for displaying images using
+ // one of the defined styles. The $variables array passed to the theme
+ // contains the following two important values:
+ // - 'style_name': the name of the image style to use when displaying the
+ // image.
+ // - 'path': the $file->uri of the image to display.
+ //
+ // When given a style and an image path the function will first determine
+ // if a derivative image already exists, in which case the existing image
+ // will be displayed. If the derivative image does not already exist the
+ // function returns an tag with a specially crafted callback URL
+ // as the src attribute for the tag. When accessed, the callback URL will
+ // generate the derivative image and serve it to the browser.
+ $output = theme('image_style',
+ array(
+ 'style_name' => $style,
+ 'path' => $image->uri,
+ 'getsize' => FALSE,
+ )
+ );
+ $output .= '
' . t('This image is being displayed using the image style %style_name.', array('%style_name' => $style)) . '
';
+ return $output;
+}
diff --git a/sites/all/modules/examples/image_example/image_example.test b/sites/all/modules/examples/image_example/image_example.test
new file mode 100644
index 00000000..92561515
--- /dev/null
+++ b/sites/all/modules/examples/image_example/image_example.test
@@ -0,0 +1,111 @@
+ 'Image example functionality',
+ 'description' => 'Test functionality of the Image Example module.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('image_example');
+ // Create user with permission to administer image styles.
+ $this->webUser = $this->drupalCreateUser(array('administer image styles', 'administer blocks'));
+ }
+
+ /**
+ * Test implementations of image API hooks.
+ */
+ public function testImageExample() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+
+ // Verify that the default style added by
+ // image_example_image_default_styles() is in the list of image styles.
+ $image_styles = image_styles();
+ $this->assertTrue(isset($image_styles['image_example_style']), 'The default style image_example_style is in the list of image styles.');
+
+ // Verify that the effect added to the default 'thumbnail' style by
+ // image_example_image_styles_alter() is present.
+ $this->assertTrue((isset($image_styles['thumbnail']['effects'][1]['name']) && $image_styles['thumbnail']['effects'][1]['name'] == 'image_example_colorize'), 'Effect added to the thumbnail style via hook_image_styles_alter() is present.');
+
+ // Create a new image style and add the effect provided by
+ // image_example_effect_info().
+ $new_style = array('name' => drupal_strtolower($this->randomName()));
+ $new_style = image_style_save($new_style);
+ $this->assertTrue(isset($new_style['isid']), format_string('Image style @style_name created.', array('@style_name' => $new_style['name'])));
+
+ $edit = array(
+ 'new' => 'image_example_colorize',
+ );
+ $this->drupalPost('admin/config/media/image-styles/edit/' . $new_style['name'], $edit, t('Add'));
+
+ // Verify the 'color' field provided by image_example_colorize_form()
+ // appears on the effect configuration page. And that we can fill it out.
+ $this->assertField('data[color]', 'Color field provided by image_example_effect_colorize_form is present on effect configuration page.');
+ $edit = array(
+ 'data[color]' => '#000000',
+ );
+ $this->drupalPost(NULL, $edit, t('Add effect'));
+ $this->assertText(t('The image effect was successfully applied.'), format_string('Colorize effect added to @style_name.', array('@style_name' => $new_style['name'])));
+
+ // Set the variable 'image_example_style_name' to the name of our new style
+ // then rename the style and ensure the variable name is changed.
+ // @todo Enable this block once http://drupal.org/node/713872 is fixed.
+ if (defined('bug_713872_fixed')) {
+ $style = image_style_load($new_style['name']);
+ variable_set('image_example_style_name', $style['name']);
+ $style['name'] = drupal_strtolower($this->randomName());
+ $style = image_style_save($style);
+ $variable = variable_get('image_example_style_name', '');
+ $this->assertTrue(($variable == $style['name']), 'Variable image_example_style_name successfully updated when renaming image style.');
+ }
+ }
+
+ /**
+ * Tests for image block provided by module.
+ */
+ public function testImageExamplePage() {
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+ $this->drupalCreateNode(array('promote' => 1));
+
+ // Upload an image to the image page.
+ $images = $this->drupalGetTestFiles('image');
+ $edit = array(
+ 'files[image_example_image_fid]' => drupal_realpath($images[0]->uri),
+ 'image_example_style_name' => 'image_example_style',
+ );
+ $this->drupalPost('image_example/styles', $edit, t('Save'));
+ $this->assertText(t('The image @image_name was uploaded', array('@image_name' => $images[0]->filename)), 'Image uploaded to image block.');
+
+ // Verify the image is displayed.
+ $this->drupalGet('image_example/styles');
+ $fid = variable_get('image_example_image_fid', FALSE);
+ $image = isset($fid) ? file_load($fid) : NULL;
+ $this->assertRaw(file_uri_target($image->uri), 'Image is displayed');
+ }
+}
diff --git a/sites/all/modules/examples/js_example/accordion.tpl.php b/sites/all/modules/examples/js_example/accordion.tpl.php
new file mode 100644
index 00000000..8ac70832
--- /dev/null
+++ b/sites/all/modules/examples/js_example/accordion.tpl.php
@@ -0,0 +1,59 @@
+
+
+ Mauris mauris ante, blandit et, ultrices a, suscipit eget, quam. Integer
+ ut neque. Vivamus nisi metus, molestie vel, gravida in, condimentum sit
+ amet, nunc. Nam a nibh. Donec suscipit eros. Nam mi. Proin viverra leo ut
+ odio. Curabitur malesuada. Vestibulum a velit eu ante scelerisque vulputate.
+
+ Sed non urna. Donec et ante. Phasellus eu ligula. Vestibulum sit amet
+ purus. Vivamus hendrerit, dolor at aliquet laoreet, mauris turpis porttitor
+ velit, faucibus interdum tellus libero ac justo. Vivamus non quam. In
+ suscipit faucibus urna.
+
+ Nam enim risus, molestie et, porta ac, aliquam ac, risus. Quisque lobortis.
+ Phasellus pellentesque purus in massa. Aenean in pede. Phasellus ac libero
+ ac tellus pellentesque semper. Sed ac felis. Sed commodo, magna quis
+ lacinia ornare, quam ante aliquam nisi, eu iaculis leo purus venenatis dui.
+
+ Cras dictum. Pellentesque habitant morbi tristique senectus et netus
+ et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in
+ faucibus orci luctus et ultrices posuere cubilia Curae; Aenean lacinia
+ mauris vel est.
+
+
+ Suspendisse eu nisl. Nullam ut libero. Integer dignissim consequat lectus.
+ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
+ inceptos himenaeos.
+
+
+
+
+
diff --git a/sites/all/modules/examples/js_example/css/jsweights.css b/sites/all/modules/examples/js_example/css/jsweights.css
new file mode 100644
index 00000000..e2d58c0f
--- /dev/null
+++ b/sites/all/modules/examples/js_example/css/jsweights.css
@@ -0,0 +1,5 @@
+
+div#js-weights div {
+ font-size: 20px;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/sites/all/modules/examples/js_example/js/black.js b/sites/all/modules/examples/js_example/js/black.js
new file mode 100644
index 00000000..c1daf7d2
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js/black.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsBlack = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.black;
+ var newDiv = $('').css('color', 'black').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/js_example/js/blue.js b/sites/all/modules/examples/js_example/js/blue.js
new file mode 100644
index 00000000..e69c188a
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js/blue.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsBlue = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.blue;
+ var newDiv = $('').css('color', 'blue').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/js_example/js/brown.js b/sites/all/modules/examples/js_example/js/brown.js
new file mode 100644
index 00000000..3e3fbc31
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js/brown.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsBrown = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.brown;
+ var newDiv = $('').css('color', 'brown').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/js_example/js/green.js b/sites/all/modules/examples/js_example/js/green.js
new file mode 100644
index 00000000..f6b1f323
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js/green.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsGreen = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.green;
+ var newDiv = $('').css('color', 'green').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/js_example/js/purple.js b/sites/all/modules/examples/js_example/js/purple.js
new file mode 100644
index 00000000..20c48f58
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js/purple.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsPurple = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.purple;
+ var newDiv = $('').css('color', 'purple').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/js_example/js/red.js b/sites/all/modules/examples/js_example/js/red.js
new file mode 100644
index 00000000..b26870b1
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js/red.js
@@ -0,0 +1,9 @@
+(function ($) {
+ Drupal.behaviors.jsWeightsRed = {
+ attach: function (context, settings) {
+ var weight = settings.jsWeights.red;
+ var newDiv = $('').css('color', 'red').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/js_example/js_example.info b/sites/all/modules/examples/js_example/js_example.info
new file mode 100644
index 00000000..8838444e
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js_example.info
@@ -0,0 +1,12 @@
+name = JS Example
+description = An example module showing how to use some of the new JavaScript features in Drupal 7
+package = Example modules
+core = 7.x
+files[] = js_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/js_example/js_example.module b/sites/all/modules/examples/js_example/js_example.module
new file mode 100644
index 00000000..6f23a1e8
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js_example.module
@@ -0,0 +1,122 @@
+ array(
+ 'template' => 'accordion',
+ 'variables' => array('title' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function js_example_menu() {
+ $items = array();
+ $items['js_example/weights'] = array(
+ 'title' => 'JS Example: see weighting in action',
+ 'page callback' => 'js_example_js_weights',
+ 'access callback' => TRUE,
+ );
+ $items['js_example/accordion'] = array(
+ 'title' => 'JS Example: jQuery UI accordion',
+ 'page callback' => 'js_example_accordion',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Weights demonstration.
+ *
+ * Here we demonstrate attaching a number of scripts to the render array.
+ * These scripts generate content according to 'weight' and color.
+ *
+ * On the Drupal side, we do three main things:
+ * - Create a container DIV, with an ID all the scripts can recognize.
+ * - Attach some scripts which generate color-coded content. We use the
+ * 'weight' attribute to set the order in which the scripts are included.
+ * - Add the color->weight array to the settings variable in each *color*.js
+ * file. This is where Drupal passes data out to JavaScript.
+ *
+ * Each of the color scripts (red.js, blue.js, etc) uses jQuery to find our
+ * DIV, and then add some content to it. The order in which the color scripts
+ * execute will end up being the order of the content.
+ *
+ * The 'weight' form atttribute determines the order in which a script is
+ * output to the page. To see this in action:
+ * - Uncheck the 'Aggregate Javascript files' setting at:
+ * admin/config/development/performance.
+ * - Load the page: js_example/weights. Examine the page source.
+ * You will see that the color js scripts have been added in the
+ * element in weight order.
+ *
+ * To test further, change a weight in the $weights array below, then save
+ * this file and reload js_example/weights. Examine the new source to see the
+ * reordering.
+ *
+ * @return array
+ * A renderable array.
+ */
+function js_example_js_weights() {
+ // Add some css to show which line is output by which script.
+ drupal_add_css(drupal_get_path('module', 'js_example') . '/css/jsweights.css');
+ // Create an array of items with random-ish weight values.
+ $weights = array(
+ 'red' => 100,
+ 'blue' => 23,
+ 'green' => 3,
+ 'brown' => 45,
+ 'black' => 5,
+ 'purple' => 60,
+ );
+ // Attach the weights array to our JavaScript settings. This allows the
+ // color scripts to discover their weight values, by accessing
+ // settings.jsWeights.*color*. The color scripts only use this information for
+ // display to the user.
+ drupal_add_js(array('jsWeights' => $weights), array('type' => 'setting'));
+ // Add our individual scripts. We add them in an arbitrary order, but the
+ // 'weight' attribute will cause Drupal to render (and thus load and execute)
+ // them in the weighted order.
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/red.js', array('weight' => $weights['red']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/blue.js', array('weight' => $weights['blue']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/green.js', array('weight' => $weights['green']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/brown.js', array('weight' => $weights['brown']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/black.js', array('weight' => $weights['black']));
+ drupal_add_js(drupal_get_path('module', 'js_example') . '/js/purple.js', array('weight' => $weights['purple']));
+ // Main container DIV. We give it a unique ID so that the JavaScript can
+ // find it using jQuery.
+ $output = '';
+ return $output;
+}
+
+/**
+ * Demonstrate accordion effect.
+ */
+function js_example_accordion() {
+ $title = t('Click sections to expand or collapse:');
+ $build['myelement'] = array(
+ '#theme' => 'my_accordion',
+ '#title' => $title,
+ );
+ $build['myelement']['#attached']['library'][] = array('system', 'ui.accordion');
+ $build['myelement']['#attached']['js'][] = array('data' => '(function($){$(function() { $("#accordion").accordion(); })})(jQuery);', 'type' => 'inline');
+ $output = drupal_render($build);
+ return $output;
+}
diff --git a/sites/all/modules/examples/js_example/js_example.test b/sites/all/modules/examples/js_example/js_example.test
new file mode 100644
index 00000000..d4697315
--- /dev/null
+++ b/sites/all/modules/examples/js_example/js_example.test
@@ -0,0 +1,46 @@
+ 'JavaScript Example',
+ 'description' => 'Functional tests for the JavaScript Example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('js_example');
+ }
+
+ /**
+ * Tests the menu paths defined in js_example module.
+ */
+ public function testJsExampleMenus() {
+ $paths = array(
+ 'js_example/weights',
+ 'js_example/accordion',
+ );
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertResponse(200, '200 response for path: ' . $path);
+ }
+ }
+}
diff --git a/sites/all/modules/examples/menu_example/menu_example.info b/sites/all/modules/examples/menu_example/menu_example.info
new file mode 100644
index 00000000..0c9d3459
--- /dev/null
+++ b/sites/all/modules/examples/menu_example/menu_example.info
@@ -0,0 +1,12 @@
+name = Menu example
+description = An example of advanced uses of the menu APIs.
+package = Example modules
+core = 7.x
+files[] = menu_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/menu_example/menu_example.module b/sites/all/modules/examples/menu_example/menu_example.module
new file mode 100644
index 00000000..6b60b4e2
--- /dev/null
+++ b/sites/all/modules/examples/menu_example/menu_example.module
@@ -0,0 +1,546 @@
+ MENU_NORMAL_ITEM,
+ //
+ // The menu title. Do NOT use t() which is called by default. You can
+ // override the use of t() by defining a 'title callback'. This is explained
+ // in the 'menu_example/title_callbacks' example below.
+ 'title' => 'Menu Example',
+
+ // Description (hover flyover for menu link). Does NOT use t(), which is
+ // called automatically.
+ 'description' => 'Simplest possible menu type, and the parent menu entry for others',
+
+ // Function to be called when this path is accessed.
+ 'page callback' => '_menu_example_basic_instructions',
+
+ // Arguments to the page callback. Here's we'll use them just to provide
+ // content for our page.
+ 'page arguments' => array(t('This page is displayed by the simplest (and base) menu example. Note that the title of the page is the same as the link title. You can also visit a similar page with no menu link. Also, note that there is a hook_menu_alter() example that has changed the path of one of the menu items.', array('!link' => url('examples/menu_example/path_only')))),
+
+ // If the page is meant to be accessible to all users, you can set 'access
+ // callback' to TRUE. This bypasses all access checks. For an explanation on
+ // how to use the permissions system to restrict access for certain users,
+ // see the example 'examples/menu_example/permissioned/controlled' below.
+ 'access callback' => TRUE,
+
+ // If the page callback is located in another file, specify it here and
+ // that file will be automatically loaded when needed.
+ // 'file' => 'menu_example.module',
+ //
+ // We can choose which menu gets the link. The default is 'navigation'.
+ // 'menu_name' => 'navigation',
+ //
+ // Show the menu link as expanded.
+ 'expanded' => TRUE,
+ );
+
+ // Show a menu link in a menu other than the default "Navigation" menu.
+ // The menu must already exist.
+ $items['examples/menu_example_alternate_menu'] = array(
+ 'title' => 'Menu Example: Menu in alternate menu',
+
+ // Machine name of the menu in which the link should appear.
+ 'menu_name' => 'main-menu',
+
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This will be in the Main menu instead of the default Navigation menu')),
+ 'access callback' => TRUE,
+ );
+
+ // A menu entry with simple permissions using user_access().
+ //
+ // First, provide a courtesy menu item that mentions the existence of the
+ // permissioned item.
+ $items['examples/menu_example/permissioned'] = array(
+ 'title' => 'Permissioned Example',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('A menu item that requires the "access protected menu example" permission is at examples/menu_example/permissioned/controlled', array('!link' => url('examples/menu_example/permissioned/controlled')))),
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+
+ // Now provide the actual permissioned menu item.
+ $items['examples/menu_example/permissioned/controlled'] = array(
+
+ // The title - do NOT use t() as t() is called automatically.
+ 'title' => 'Permissioned Menu Item',
+ 'description' => 'This menu entry will not appear and the page will not be accessible without the "access protected menu example" permission.',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This menu entry will not show and the page will not be accessible without the "access protected menu example" permission.')),
+
+ // For a permissioned menu entry, we provide an access callback which
+ // determines whether the current user should have access. The default is
+ // user_access(), which we'll use in this case. Since it's the default,
+ // we don't even have to enter it.
+ // 'access callback' => 'user_access',
+ //
+ // The 'access arguments' are passed to the 'access callback' to help it
+ // do its job. In the case of user_access(), we need to pass a permission
+ // as the first argument.
+ 'access arguments' => array('access protected menu example'),
+
+ // The optional weight element tells how to order the submenu items.
+ // Higher weights are "heavier", dropping to the bottom of the menu.
+ 'weight' => 10,
+ );
+
+ /*
+ * We will define our own "access callback" function. We'll use
+ * menu_example_custom_access() rather than the default user_access().
+ *
+ * The function takes a "role" of the user as an argument.
+ */
+ $items['examples/menu_example/custom_access'] = array(
+ 'title' => 'Custom Access Example',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('A menu item that requires the user to posess a role of "authenticated user" is at examples/menu_example/custom_access/page', array('!link' => url('examples/menu_example/custom_access/page')))),
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ 'weight' => -5,
+ );
+
+ $items['examples/menu_example/custom_access/page'] = array(
+ 'title' => 'Custom Access Menu Item',
+ 'description' => 'This menu entry will not show and the page will not be accessible without the user being an "authenticated user".',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This menu entry will not be visible and access will result in a 403 error unless the user has the "authenticated user" role. This is accomplished with a custom access callback.')),
+ 'access callback' => 'menu_example_custom_access',
+ 'access arguments' => array('authenticated user'),
+ );
+
+ // A menu router entry with no menu link. This could be used any time we
+ // don't want the user to see a link in the menu. Otherwise, it's the same
+ // as the "simplest" entry above. MENU_CALLBACK is used for all menu items
+ // which don't need a visible menu link, including services and other pages
+ // that may be linked to but are not intended to be accessed directly.
+ //
+ // First, provide a courtesy link in the menu so people can find this.
+ $items['examples/menu_example/path_only'] = array(
+ 'title' => 'MENU_CALLBACK example',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('A menu entry with no menu link (MENU_CALLBACK) is at !link', array('!link' => url('examples/menu_example/path_only/callback')))),
+ 'access callback' => TRUE,
+ 'weight' => 20,
+ );
+ $items['examples/menu_example/path_only/callback'] = array(
+
+ // A type of MENU_CALLBACK means leave the path completely out of the menu
+ // links.
+ 'type' => MENU_CALLBACK,
+
+ // The title is still used for the page title, even though it's not used
+ // for the menu link text, since there's no menu link.
+ 'title' => 'Callback Only',
+
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('The menu entry for this page is of type MENU_CALLBACK, so it provides only a path but not a link in the menu links, but it is the same in every other way to the simplest example.')),
+ 'access callback' => TRUE,
+ );
+
+ // A menu entry with tabs.
+ // For tabs we need at least 3 things:
+ // 1) A parent MENU_NORMAL_ITEM menu item (examples/menu_example/tabs in this
+ // example.)
+ // 2) A primary tab (the one that is active when we land on the base menu).
+ // This tab is of type MENU_DEFAULT_LOCAL_TASK.
+ // 3) Some other menu entries for the other tabs, of type MENU_LOCAL_TASK.
+ $items['examples/menu_example/tabs'] = array(
+ // 'type' => MENU_NORMAL_ITEM, // Not necessary since this is the default.
+ 'title' => 'Tabs',
+ 'description' => 'Shows how to create primary and secondary tabs',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This is the "tabs" menu entry.')),
+ 'access callback' => TRUE,
+ 'weight' => 30,
+ );
+
+ // For the default local task, we need very little configuration, as the
+ // callback and other conditions are handled by the parent callback.
+ $items['examples/menu_example/tabs/default'] = array(
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'title' => 'Default primary tab',
+ 'weight' => 1,
+ );
+ // Now add the rest of the tab entries.
+ foreach (array(t('second') => 2, t('third') => 3, t('fourth') => 4) as $tabname => $weight) {
+ $items["examples/menu_example/tabs/$tabname"] = array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => $tabname,
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This is the tab "@tabname" in the "basic tabs" example', array('@tabname' => $tabname))),
+ 'access callback' => TRUE,
+
+ // The weight property overrides the default alphabetic ordering of menu
+ // entries, allowing us to get our tabs in the order we want.
+ 'weight' => $weight,
+ );
+ }
+
+ // Finally, we'll add secondary tabs to the default tab of the tabs entry.
+ //
+ // The default local task needs very little information.
+ $items['examples/menu_example/tabs/default/first'] = array(
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'title' => 'Default secondary tab',
+ // The additional page callback and related items are handled by the
+ // parent menu item.
+ );
+ foreach (array(t('second'), t('third')) as $tabname) {
+ $items["examples/menu_example/tabs/default/$tabname"] = array(
+ 'type' => MENU_LOCAL_TASK,
+ 'title' => $tabname,
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This is the secondary tab "@tabname" in the "basic tabs" example "default" tab', array('@tabname' => $tabname))),
+ 'access callback' => TRUE,
+ );
+ }
+
+ // All the portions of the URL after the base menu are passed to the page
+ // callback as separate arguments, and can be captured by the page callback
+ // in its argument list. Our _menu_example_menu_page() function captures
+ // arguments in its function signature and can output them.
+ $items['examples/menu_example/use_url_arguments'] = array(
+ 'title' => 'Extra Arguments',
+ 'description' => 'The page callback can use the arguments provided after the path used as key',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This page demonstrates using arguments in the path (portions of the path after "menu_example/url_arguments". For example, access it with !link1 or !link2).', array('!link1' => url('examples/menu_example/use_url_arguments/one/two'), '!link2' => url('examples/menu_example/use_url_arguments/firstarg/secondarg')))),
+ 'access callback' => TRUE,
+ 'weight' => 40,
+ );
+
+ // The menu title can be dynamically created by using the 'title callback'
+ // which by default is t(). Here we provide a title callback which adjusts
+ // the menu title based on the current user's username.
+ $items['examples/menu_example/title_callbacks'] = array(
+ 'title callback' => '_menu_example_simple_title_callback',
+ 'title arguments' => array(t('Dynamic title: username=')),
+ 'description' => 'The title of this menu item is dynamically generated',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('The menu title is dynamically changed by the title callback')),
+ 'access callback' => TRUE,
+ 'weight' => 50,
+ );
+
+ // Sometimes we need to capture a specific argument within the menu path,
+ // as with the menu entry
+ // 'example/menu_example/placeholder_argument/3333/display', where we need to
+ // capture the "3333". In that case, we use a placeholder in the path provided
+ // in the menu entry. The (odd) way this is done is by using
+ // array(numeric_position_value) as the value for 'page arguments'. The
+ // numeric_position_value is the zero-based index of the portion of the URL
+ // which should be passed to the 'page callback'.
+ //
+ // First we provide a courtesy link with information on how to access
+ // an item with a placeholder.
+ $items['examples/menu_example/placeholder_argument'] = array(
+ 'title' => 'Placeholder Arguments',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('Demonstrate placeholders by visiting examples/menu_example/placeholder_argument/3343/display', array('!link' => url('examples/menu_example/placeholder_argument/3343/display')))),
+ 'access callback' => TRUE,
+ 'weight' => 60,
+ );
+
+ // Now the actual entry.
+ $items['examples/menu_example/placeholder_argument/%/display'] = array(
+ 'title' => 'Placeholder Arguments',
+ 'page callback' => '_menu_example_menu_page',
+
+ // Pass the value of '%', which is zero-based argument 3, to the
+ // 'page callback'. So if the URL is
+ // 'examples/menu_example/placeholder_argument/333/display' then the value
+ // 333 will be passed into the 'page callback'.
+ 'page arguments' => array(3),
+ 'access callback' => TRUE,
+ );
+
+ // Drupal provides magic placeholder processing as well, so if the placeholder
+ // is '%menu_example_arg_optional', the function
+ // menu_example_arg_optional_load($arg) will be called to translate the path
+ // argument to a more substantial object. $arg will be the value of the
+ // placeholder. Then the return value of menu_example_id_load($arg) will be
+ // passed to the 'page callback'.
+ // In addition, if (in this case) menu_example_arg_optional_to_arg() exists,
+ // then a menu link can be created using the results of that function as a
+ // default for %menu_example_arg_optional.
+ $items['examples/menu_example/default_arg/%menu_example_arg_optional'] = array(
+ 'title' => 'Processed Placeholder Arguments',
+ 'page callback' => '_menu_example_menu_page',
+ // Argument 3 (4rd arg) is the one we want.
+ 'page arguments' => array(3),
+ 'access callback' => TRUE,
+ 'weight' => 70,
+ );
+
+ $items['examples/menu_example/menu_original_path'] = array(
+ 'title' => 'Menu path that will be altered by hook_menu_alter()',
+ 'page callback' => '_menu_example_menu_page',
+ 'page arguments' => array(t('This menu item was created strictly to allow the hook_menu_alter() function to have something to operate on. hook_menu defined the path as examples/menu_example/menu_original_path. The hook_menu_alter() changes it to examples/menu_example/menu_altered_path. You can try navigating to both paths and see what happens!')),
+ 'access callback' => TRUE,
+ 'weight' => 80,
+ );
+ return $items;
+}
+
+/**
+ * Page callback for the simplest introduction menu entry.
+ *
+ * @param string $content
+ * Some content passed in.
+ */
+function _menu_example_basic_instructions($content = NULL) {
+ $base_content = t(
+ 'This is the base page of the Menu Example. There are a number of examples
+ here, from the most basic (like this one) to extravagant mappings of loaded
+ placeholder arguments. Enjoy!');
+ return '
' . $base_content . '
' . $content . '
';
+}
+
+/**
+ * Page callback for use with most of the menu entries.
+ *
+ * The arguments it receives determine what it outputs.
+ *
+ * @param string $content
+ * The base content to output.
+ * @param string $arg1
+ * First additional argument from the path used to access the menu
+ * @param string $arg2
+ * Second additional argument.
+ */
+function _menu_example_menu_page($content = NULL, $arg1 = NULL, $arg2 = NULL) {
+ $output = '
';
+ }
+ return $output;
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * Provides a demonstration access string.
+ */
+function menu_example_permission() {
+ return array(
+ 'access protected menu example' => array(
+ 'title' => t('Access the protected menu example'),
+ ),
+ );
+
+}
+
+/**
+ * Determine whether the current user has the role specified.
+ *
+ * @param string $role_name
+ * The role required for access
+ *
+ * @return bool
+ * True if the acting user has the role specified.
+ */
+function menu_example_custom_access($role_name) {
+ $access_granted = in_array($role_name, $GLOBALS['user']->roles);
+ return $access_granted;
+}
+
+/**
+ * Utility function to provide mappings from integers to some strings.
+ *
+ * This would normally be some database lookup to get an object or array from
+ * a key.
+ *
+ * @param int $id
+ * The integer key.
+ *
+ * @return string
+ * The string to which the integer key mapped, or NULL if it did not map.
+ */
+function _menu_example_mappings($id) {
+ $mapped_value = NULL;
+ static $mappings = array(
+ 1 => 'one',
+ 2 => 'two',
+ 3 => 'three',
+ 99 => 'jackpot! default',
+ );
+ if (isset($mappings[$id])) {
+ $mapped_value = $mappings[$id];
+ }
+ return $mapped_value;
+}
+
+/**
+ * The special _load function to load menu_example.
+ *
+ * Given an integer $id, load the string that should be associated with it.
+ * Normally this load function would return an array or object with more
+ * information.
+ *
+ * @param int $id
+ * The integer to load.
+ *
+ * @return string
+ * A string loaded from the integer.
+ */
+function menu_example_id_load($id) {
+ // Just map a magic value here. Normally this would load some more complex
+ // object from the database or other context.
+ $mapped_value = _menu_example_mappings($id);
+ if (!empty($mapped_value)) {
+ return t('Loaded value was %loaded', array('%loaded' => $mapped_value));
+ }
+ else {
+ return t('Sorry, the id %id was not found to be loaded', array('%id' => $id));
+ }
+}
+
+/**
+ * Implements hook_menu_alter().
+ *
+ * Changes the path 'examples/menu_example/menu_original_path' to
+ * 'examples/menu_example/menu_altered_path'.
+ * Changes the title callback of the 'user/UID' menu item.
+ *
+ * Change the path 'examples/menu_example/menu_original_path' to
+ * 'examples/menu_example/menu_altered_path'. This change will prevent the
+ * page from appearing at the original path (since the item is being unset).
+ * You will need to go to examples/menu_example/menu_altered_path manually to
+ * see the page.
+ *
+ * Remember that hook_menu_alter() only runs at menu_rebuild() time, not every
+ * time the page is built, so this typically happens only at cache clear time.
+ *
+ * The $items argument is the complete list of menu router items ready to be
+ * written to the menu_router table.
+ */
+function menu_example_menu_alter(&$items) {
+ if (!empty($items['examples/menu_example/menu_original_path'])) {
+ $items['examples/menu_example/menu_altered_path'] = $items['examples/menu_example/menu_original_path'];
+ $items['examples/menu_example/menu_altered_path']['title'] = 'Menu item altered by hook_menu_alter()';
+ unset($items['examples/menu_example/menu_original_path']);
+ }
+
+ // Here we will change the title callback to our own function, changing the
+ // 'user' link from the traditional to always being "username's account".
+ if (!empty($items['user/%user'])) {
+ $items['user/%user']['title callback'] = 'menu_example_user_page_title';
+ }
+}
+
+/**
+ * Title callback to rewrite the '/user' menu link.
+ *
+ * @param string $base_string
+ * string to be prepended to current user's name.
+ */
+function _menu_example_simple_title_callback($base_string) {
+ global $user;
+ $username = !empty($user->name) ? $user->name : t('anonymous');
+ return $base_string . ' ' . $username;
+}
+
+/**
+ * Title callback to rename the title dynamically, based on user_page_title().
+ *
+ * @param object $account
+ * User account related to the visited page.
+ */
+function menu_example_user_page_title($account) {
+ return is_object($account) ? t("@name's account", array('@name' => format_username($account))) : '';
+}
+
+/**
+ * Implements hook_menu_link_alter().
+ *
+ * This code will get the chance to alter a menu link when it is being saved
+ * in the menu interface at admin/build/menu. Whatever we do here overrides
+ * anything the user/administrator might have been trying to do.
+ */
+function menu_example_menu_link_alter(&$item, $menu) {
+ // Force the link title to remain 'Clear Cache' no matter what the admin
+ // does with the web interface.
+ if ($item['link_path'] == 'devel/cache/clear') {
+ $item['link_title'] = 'Clear Cache';
+ };
+}
+
+/**
+ * Loads an item based on its $id.
+ *
+ * In this case we're just creating a more extensive string. In a real example
+ * we would load or create some type of object.
+ *
+ * @param int $id
+ * Id of the item.
+ */
+function menu_example_arg_optional_load($id) {
+ $mapped_value = _menu_example_mappings($id);
+ if (!empty($mapped_value)) {
+ return t('Loaded value was %loaded', array('%loaded' => $mapped_value));
+ }
+ else {
+ return t('Sorry, the id %id was not found to be loaded', array('%id' => $id));
+ }
+}
+
+/**
+ * Utility function to provide default argument for wildcard.
+ *
+ * A to_arg() function is used to provide a default for the arg in the
+ * wildcard. The purpose is to provide a menu link that will function if no
+ * argument is given. For example, in the case of the menu item
+ * 'examples/menu_example/default_arg/%menu_example_arg_optional' the third argument
+ * is required, and the menu system cannot make a menu link using this path
+ * since it contains a placeholder. However, when the to_arg() function is
+ * provided, the menu system will create a menu link pointing to the path
+ * which would be created with the to_arg() function filling in the
+ * %menu_example_arg_optional.
+ *
+ * @param string $arg
+ * The arg (URL fragment) to be tested.
+ */
+function menu_example_arg_optional_to_arg($arg) {
+ // If our argument is not provided, give a default of 99.
+ return (empty($arg) || $arg == '%') ? 99 : $arg;
+}
+/**
+ * @} End of "defgroup menu_example".
+ */
diff --git a/sites/all/modules/examples/menu_example/menu_example.test b/sites/all/modules/examples/menu_example/menu_example.test
new file mode 100644
index 00000000..fd511abe
--- /dev/null
+++ b/sites/all/modules/examples/menu_example/menu_example.test
@@ -0,0 +1,116 @@
+ 'Menu example functionality',
+ 'description' => 'Checks behavior of Menu Example.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('menu_example');
+ }
+
+ /**
+ * Test the various menus.
+ */
+ public function testMenuExample() {
+ $this->drupalGet('');
+ $this->assertText(t('Menu Example: Menu in alternate menu'));
+ $this->clickLink(t('Menu Example'));
+ $this->assertText(t('This is the base page of the Menu Example'));
+
+ $this->drupalGet('examples/menu_example_alternate_menu');
+ $this->assertResponse(200);
+
+ $this->clickLink(t('Custom Access Example'));
+ $this->assertText(t('Custom Access Example'));
+
+ $this->clickLink(t('examples/menu_example/custom_access/page'));
+ $this->assertResponse(403);
+
+ $this->drupalGet('examples/menu_example/permissioned');
+ $this->assertText(t('Permissioned Example'));
+
+ $this->clickLink('examples/menu_example/permissioned/controlled');
+ $this->assertResponse(403);
+
+ $this->drupalGet('examples/menu_example');
+
+ $this->clickLink(t('MENU_CALLBACK example'));
+
+ $this->drupalGet('examples/menu_example/path_only/callback');
+ $this->assertText(t('The menu entry for this page is of type MENU_CALLBACK'));
+
+ $this->clickLink(t('Tabs'));
+ $this->assertText(t('This is the "tabs" menu entry'));
+
+ $this->drupalGet('examples/menu_example/tabs/second');
+ $this->assertText(t('This is the tab "second" in the "basic tabs" example'));
+
+ $this->clickLink(t('third'));
+ $this->assertText(t('This is the tab "third" in the "basic tabs" example'));
+
+ $this->clickLink(t('Extra Arguments'));
+
+ $this->drupalGet('examples/menu_example/use_url_arguments/one/two');
+ $this->assertText(t('Argument 1=one'));
+
+ $this->clickLink(t('Placeholder Arguments'));
+
+ $this->clickLink(t('examples/menu_example/placeholder_argument/3343/display'));
+ $this->assertRaw('
3343
');
+
+ $this->clickLink(t('Processed Placeholder Arguments'));
+ $this->assertText(t('Loaded value was jackpot! default'));
+
+ // Create a user with permissions to access protected menu entry.
+ $web_user = $this->drupalCreateUser(array('access protected menu example'));
+
+ // Use custom overridden drupalLogin function to verify the user is logged
+ // in.
+ $this->drupalLogin($web_user);
+
+ // Check that our title callback changing /user dynamically is working.
+ // Using ' because of the format_username function.
+ $this->assertRaw(t("@name's account", array('@name' => format_username($web_user))), format_string('Title successfully changed to account name: %name.', array('%name' => $web_user->name)));
+
+ // Now start testing other menu entries.
+ $this->drupalGet('examples/menu_example');
+
+ $this->clickLink(t('Custom Access Example'));
+ $this->assertText(t('Custom Access Example'));
+
+ $this->drupalGet('examples/menu_example/custom_access/page');
+ $this->assertResponse(200);
+
+ $this->drupalGet('examples/menu_example/permissioned');
+ $this->assertText('Permissioned Example');
+ $this->clickLink('examples/menu_example/permissioned/controlled');
+ $this->assertText('This menu entry will not show');
+
+ $this->drupalGet('examples/menu_example/menu_altered_path');
+ $this->assertText('This menu item was created strictly to allow the hook_menu_alter()');
+
+ }
+
+}
diff --git a/sites/all/modules/examples/node_access_example/node_access_example.info b/sites/all/modules/examples/node_access_example/node_access_example.info
new file mode 100644
index 00000000..0af021d6
--- /dev/null
+++ b/sites/all/modules/examples/node_access_example/node_access_example.info
@@ -0,0 +1,12 @@
+name = Node access example
+description = Demonstrates how a module can use Drupal's node access system
+package = Example modules
+core = 7.x
+files[] = node_access_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/node_access_example/node_access_example.install b/sites/all/modules/examples/node_access_example/node_access_example.install
new file mode 100644
index 00000000..2a25dbb8
--- /dev/null
+++ b/sites/all/modules/examples/node_access_example/node_access_example.install
@@ -0,0 +1,31 @@
+ 'Example table for node_access_example module',
+ 'fields' => array(
+ 'nid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'private' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('nid'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/examples/node_access_example/node_access_example.module b/sites/all/modules/examples/node_access_example/node_access_example.module
new file mode 100644
index 00000000..4de28878
--- /dev/null
+++ b/sites/all/modules/examples/node_access_example/node_access_example.module
@@ -0,0 +1,482 @@
+ 'Node Access Example',
+ 'page callback' => 'node_access_example_private_node_listing',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Our hook_menu() page callback function.
+ *
+ * Information for the user about what nodes are marked private on the system
+ * and which of those the user has access to.
+ *
+ * The queries showing what is accessible to the current user demonstrate the
+ * use of the 'node_access' tag to make sure that we don't show inappropriate
+ * information to unprivileged users.
+ *
+ * @return string
+ * Page content.
+ *
+ * @see page_example
+ */
+function node_access_example_private_node_listing() {
+ $content = '
' . t('This example shows how a module can use the Drupal node access system to allow access to specific nodes. You will need to look at the code and then experiment with it by creating nodes, marking them private, and accessing them as various users.') . '
';
+
+ // Find out how many nodes are marked private.
+ $query = db_select('node', 'n');
+ $query->addExpression('COUNT(n.nid)', 'private_count');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $num_private = $query
+ ->condition('nae.private', 1)->execute()->fetchField();
+
+ // Find out how many nodes owned by this user are marked private.
+ $query = db_select('node', 'n');
+ $query->addExpression('COUNT(n.nid)', 'private_count');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $num_personal = $query
+ ->condition('n.uid', $GLOBALS['user']->uid)
+ ->condition('nae.private', 1)
+ ->execute()->fetchfield();
+
+ $content .= '
' . t('There are currently @num private nodes in the system @num_personal are yours.', array('@num' => $num_private, '@num_personal' => $num_personal)) . '
';
+
+ // Use a 'node_access' tag with a query to find out how many this user has
+ // access to. This will be the standard way to make lists while respecting
+ // node access restrictions.
+ $query = db_select('node', 'n');
+ $query->addExpression('COUNT(n.nid)', 'private_count');
+ $query->addTag('node_access');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $num_private_accessible = $query->condition('nae.private', 1)->execute()->fetchField();
+ $content .= '
' . t('You have access to @num private nodes.', array('@num' => $num_private_accessible)) . '
';
+
+ // Use the key 'node_access' tag to get the key data from the nodes this
+ // has access to.
+ $query = db_select('node', 'n', array('fetch' => PDO::FETCH_ASSOC));
+ $query->addTag('node_access');
+ $query->join('node_access_example', 'nae', 'nae.nid = n.nid');
+ $query->join('users', 'u', 'u.uid = n.uid');
+ $result = $query->fields('n', array('nid', 'title', 'uid'))
+ ->fields('u', array('name'))
+ ->condition('nae.private', 1)->execute();
+
+ $rows = array();
+ foreach ($result as $node) {
+ $node['nid'] = l($node['nid'], 'node/' . $node['nid']);
+ $rows[] = array('data' => $node, 'class' => array('accessible'));
+ }
+ $content .= '
';
+
+ return array('#markup' => $content);
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * We create two permissions, which we can use as a base for our grant/deny
+ * decision:
+ *
+ * - 'access any private content' allows global access to content marked
+ * private by other users.
+ * - 'edit any private content' allows global edit
+ * privileges, basically overriding the node access system.
+ *
+ * Note that the 'edit any * content' and 'delete any * content' permissions
+ * will allow edit or delete permissions to the holder, regardless of what
+ * this module does.
+ *
+ * @see hook_permissions()
+ */
+function node_access_example_permission() {
+ return array(
+ 'access any private content' => array(
+ 'title' => t('Access any private content'),
+ 'description' => t('May view posts of other users even though they are marked private.'),
+ ),
+ 'edit any private content' => array(
+ 'title' => t('Edit any private content'),
+ 'description' => t('May edit posts of other users even though they are marked private.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_node_access().
+ *
+ * Allows view and edit access to private nodes, when the account requesting
+ * access has the username 'foobar'.
+ *
+ * hook_node_access() was introduced in Drupal 7. We use it here to demonstrate
+ * allowing certain privileges to an arbitrary user.
+ *
+ * @see hook_node_access()
+ */
+function node_access_example_node_access($node, $op, $account) {
+ // If $node is a string, the node has not yet been created. We don't care
+ // about that case.
+ if (is_string($node)) {
+ return NODE_ACCESS_IGNORE;
+ }
+ if (($op == 'view' || $op == 'update') && (!empty($account->name) && $account->name == 'foobar') && !empty($node->private)) {
+ drupal_set_message(t('Access to node @nid allowed because requester name (@name) is specifically allowed', array('@name' => $node->name, '@uid' => $account->uid)));
+ return NODE_ACCESS_ALLOW;
+ }
+ return NODE_ACCESS_IGNORE;
+}
+
+/**
+ * Here we define a constant for our node access grant ID, for the
+ * node_access_example_view and node_access_example_edit realms. This ID could
+ * be any integer, but here we choose 23, because it is this author's favorite
+ * number.
+ */
+define('NODE_ACCESS_EXAMPLE_GRANT_ALL', 23);
+
+/**
+ * Implements hook_node_grants().
+ *
+ * Tell the node access system what grant IDs the user belongs to for each
+ * realm, based on the operation being performed.
+ *
+ * When the user tries to perform an operation on the node, Drupal calls
+ * hook_node_grants() to determine grant ID and realm for the user. Drupal
+ * looks up the grant ID and realm for the node, and compares them to the
+ * grant ID and realm provided here. If grant ID and realm match for both
+ * user and node, then the operation is allowed.
+ *
+ * Grant ID and realm are both determined per node, by your module in
+ * hook_node_access_records().
+ *
+ * In our example, we've created three access realms: One for authorship, and
+ * two that track with the permission system.
+ *
+ * We always add node_access_example_author to the list of grants, with a grant
+ * ID equal to their user ID. We do this because in our model, authorship
+ * always gives you permission to edit or delete your nodes, even if they're
+ * marked private.
+ *
+ * Then we compare the user's permissions to the operation to determine whether
+ * the user falls into the other two realms: node_access_example_view, and/or
+ * node_access_example_edit. If the user has the 'access any private content'
+ * permission we defined in hook_permission(), they're declared as belonging to
+ * the node_access_example_realm. Similarly, if they have the 'edit any private
+ * content' permission, we add the node_access_example_edit realm to the list
+ * of grants they have.
+ *
+ * @see node_access_example_permission()
+ * @see node_access_example_node_access_records()
+ */
+function node_access_example_node_grants($account, $op) {
+ $grants = array();
+ // First grant a grant to the author for own content.
+ // Do not grant to anonymous user else all anonymous users would be author.
+ if ($account->uid) {
+ $grants['node_access_example_author'] = array($account->uid);
+ }
+
+ // Then, if "access any private content" is allowed to the account,
+ // grant view, update, or delete as necessary.
+ if ($op == 'view' && user_access('access any private content', $account)) {
+ $grants['node_access_example_view'] = array(NODE_ACCESS_EXAMPLE_GRANT_ALL);
+ }
+
+ if (($op == 'update' || $op == 'delete') && user_access('edit any private content', $account)) {
+ $grants['node_access_example_edit'] = array(NODE_ACCESS_EXAMPLE_GRANT_ALL);
+ }
+
+ return $grants;
+}
+
+/**
+ * Implements hook_node_access_records().
+ *
+ * All node access modules must implement this hook. If the module is
+ * interested in the privacy of the node passed in, return a list
+ * of node access values for each grant ID we offer.
+ *
+ * In this example, for each node which is marked 'private,' we define
+ * three realms:
+ *
+ * The first and second are realms are 'node_access_example_view' and
+ * 'node_access_example_edit,' which have a single grant ID, 1. The
+ * user is either a member of these realms or not, depending upon the
+ * operation and the access permission set.
+ *
+ * The third is node_access_example_author. It gives the node
+ * author special privileges. node_access_example_author has one grant ID for
+ * every UID, and each user is automatically a member of the group where
+ * GID == UID. This has the effect of giving each user their own grant ID
+ * for nodes they authored, within this realm.
+ *
+ * Drupal calls this hook when a node is saved, or when access permissions
+ * change in order to rebuild the node access database table(s).
+ *
+ * The array you return will define the realm and the grant ID for the
+ * given node. This is stored in the {node_access} table for subsequent
+ * comparison against the user's realm and grant IDs, which you'll
+ * supply in hook_node_grants().
+ *
+ * Realm names and grant IDs are arbitrary. Official drupal naming
+ * conventions do not cover access realms, but since all realms are
+ * stored in the same database table, it's probably a good idea to
+ * use descriptive names which follow the module name, such as
+ * 'mymodule_realmname'.
+ *
+ * @see node_access_example_node_grants()
+ */
+function node_access_example_node_access_records($node) {
+ // We only care about the node if it's been marked private. If not, it is
+ // treated just like any other node and we completely ignore it.
+ if (!empty($node->private)) {
+ $grants = array();
+ $grants[] = array(
+ 'realm' => 'node_access_example_view',
+ 'gid' => NODE_ACCESS_EXAMPLE_GRANT_ALL,
+ 'grant_view' => 1,
+ 'grant_update' => 0,
+ 'grant_delete' => 0,
+ 'priority' => 0,
+ );
+ $grants[] = array(
+ 'realm' => 'node_access_example_edit',
+ 'gid' => NODE_ACCESS_EXAMPLE_GRANT_ALL,
+ 'grant_view' => 1,
+ 'grant_update' => 1,
+ 'grant_delete' => 1,
+ 'priority' => 0,
+ );
+
+ // For the node_access_example_author realm, the grant ID (gid) is
+ // equivalent to the node author's user ID (UID).
+ // We check the node UID so that we don't grant author privileges for
+ // anonymous nodes to anonymous users.
+ if ($node->uid) {
+ $grants[] = array(
+ 'realm' => 'node_access_example_author',
+ 'gid' => $node->uid,
+ 'grant_view' => 1,
+ 'grant_update' => 1,
+ 'grant_delete' => 1,
+ 'priority' => 0,
+ );
+ }
+ return $grants;
+ }
+ // Return nothing if the node has not been marked private.
+}
+
+/**
+ * Implements hook_form_alter().
+ *
+ * This module adds a simple checkbox to the node form labeled private. If the
+ * checkbox is checked, only the node author and users with
+ * 'access any private content' privileges may see it.
+ */
+function node_access_example_form_alter(&$form, $form_state) {
+ if (!empty($form['#node_edit_form'])) {
+ $form['node_access_example'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Node Access Example'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#weight' => 8,
+ );
+
+ $form['node_access_example']['private'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Private'),
+ '#description' => t('Check here if this content should be set private and only shown to privileged users.'),
+ '#default_value' => isset($form['#node']->private) ? $form['#node']->private : FALSE,
+ );
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * Gather and add the private setting for the nodes Drupal is loading.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_load($nodes, $types) {
+ $result = db_query('SELECT nid, private FROM {node_access_example} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)));
+ foreach ($result as $record) {
+ $nodes[$record->nid]->private = $record->private;
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ *
+ * Delete the node_access_example record when the node is deleted.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_delete($node) {
+ db_delete('node_access_example')->condition('nid', $node->nid)->execute();
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * Insert a new access record when a node is created.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_insert($node) {
+ if (isset($node->private)) {
+ db_insert('node_access_example')->fields(
+ array(
+ 'nid' => $node->nid,
+ 'private' => (int) $node->private,
+ )
+ )->execute();
+ }
+ drupal_set_message(t('New node @nid was created and private=@private', array('@nid' => $node->nid, '@private' => !empty($node->private) ? 1 : 0)));
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * If the record in the node_access_example table already exists, we must
+ * update it. If it doesn't exist, we create it.
+ * @see nodeapi_example.module
+ */
+function node_access_example_node_update($node) {
+ // Find out if there is already a node_access_example record.
+ $exists = db_query('SELECT nid FROM {node_access_example} WHERE nid = :nid',
+ array(':nid' => $node->nid))->fetchField();
+
+ // If there is already a record, update it with the new private value.
+ if ($exists) {
+ $num_updated = db_update('node_access_example')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'private' => !empty($node->private) ? 1 : 0,
+ ))
+ ->condition('nid', $node->nid)
+ ->execute();
+ drupal_set_message(
+ t("Updated node @nid to set private=@private (@num nodes actually updated)",
+ array(
+ '@private' => $node->private,
+ '@num' => $num_updated,
+ '@nid' => $node->nid,
+ )
+ )
+ );
+ }
+ // Otherwise, create a new record.
+ else {
+ node_access_example_node_insert($node);
+ drupal_set_message(t('Inserted new node_access nid=@nid, private=@private', array('@nid' => $node->nid, '@private' => $node->private)));
+ }
+
+}
+
+/**
+ * @} End of "defgroup node_access_example".
+ */
diff --git a/sites/all/modules/examples/node_access_example/node_access_example.test b/sites/all/modules/examples/node_access_example/node_access_example.test
new file mode 100644
index 00000000..61257b71
--- /dev/null
+++ b/sites/all/modules/examples/node_access_example/node_access_example.test
@@ -0,0 +1,338 @@
+ 'Node Access Example functionality',
+ 'description' => 'Checks behavior of Node Access Example.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('node_access_example', 'search');
+ node_access_rebuild();
+ }
+
+ /**
+ * Test the "private" node access.
+ *
+ * - Create 3 users with "access content" and "create article" permissions.
+ * - Each user creates one private and one not private article.
+ * - Run cron to update search index.
+ * - Test that each user can view the other user's non-private article.
+ * - Test that each user cannot view the other user's private article.
+ * - Test that each user finds only appropriate (non-private + own private)
+ * in search results.
+ * - Logout.
+ * - Test that anonymous user can't view, edit or delete private content which
+ * has author.
+ * - Test that anonymous user can't view, edit or delete private content with
+ * anonymous author.
+ * - Create another user with 'view any private content'.
+ * - Test that user 4 can view all content created above.
+ * - Test that user 4 can search for all content created above.
+ * - Test that user 4 cannot edit private content above.
+ * - Create another user with 'edit any private content'
+ * - Test that user 5 can edit private content.
+ * - Test that user 5 can delete private content.
+ * - Test listings of nodes with 'node_access' tag on database search.
+ */
+ public function testNodeAccessBasic() {
+ $num_simple_users = 3;
+ $simple_users = array();
+
+ // Nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private;.
+ $nodes_by_user = array();
+ // Titles keyed by nid.
+ $titles = array();
+ // Array of nids marked private.
+ $private_nodes = array();
+ for ($i = 0; $i < $num_simple_users; $i++) {
+ $simple_users[$i] = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'create article content',
+ 'search content',
+ )
+ );
+ }
+ foreach ($simple_users as $web_user) {
+ $this->drupalLogin($web_user);
+ foreach (array(0 => 'Public', 1 => 'Private') as $is_private => $type) {
+ $edit = array(
+ 'title' => t('@private_public Article created by @user', array('@private_public' => $type, '@user' => $web_user->name)),
+ );
+ if ($is_private) {
+ $edit['private'] = TRUE;
+ $edit['body[und][0][value]'] = 'private node';
+ }
+ else {
+ $edit['body[und][0][value]'] = 'public node';
+ }
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+ debug(t('Created article with private=@private', array('@private' => $is_private)));
+ $this->assertText(t('Article @title has been created', array('@title' => $edit['title'])));
+ $nid = db_query('SELECT nid FROM {node} WHERE title = :title', array(':title' => $edit['title']))->fetchField();
+ $this->assertText(t('New node @nid was created and private=@private', array('@nid' => $nid, '@private' => $is_private)));
+ $private_status = db_query('SELECT private FROM {node_access_example} where nid = :nid', array(':nid' => $nid))->fetchField();
+ $this->assertTrue($is_private == $private_status, 'Node was properly set to private or not private in node_access_example table.');
+ if ($is_private) {
+ $private_nodes[] = $nid;
+ }
+ $titles[$nid] = $edit['title'];
+ $nodes_by_user[$web_user->uid][$nid] = $is_private;
+ }
+ }
+ debug($nodes_by_user);
+ // Build the search index.
+ $this->cronRun();
+ foreach ($simple_users as $web_user) {
+ $this->drupalLogin($web_user);
+ // Check to see that we find the number of search results expected.
+ $this->checkSearchResults('Private node', 1);
+ // Check own nodes to see that all are readable.
+ foreach (array_keys($nodes_by_user) as $uid) {
+ // All of this user's nodes should be readable to same.
+ if ($uid == $web_user->uid) {
+ foreach ($nodes_by_user[$uid] as $nid => $is_private) {
+ $this->drupalGet('node/' . $nid);
+ $this->assertResponse(200);
+ $this->assertTitle($titles[$nid] . ' | Drupal', 'Correct title for node found');
+ }
+ }
+ else {
+ // Otherwise, for other users, private nodes should get a 403,
+ // but we should be able to read non-private nodes.
+ foreach ($nodes_by_user[$uid] as $nid => $is_private) {
+ $this->drupalGet('node/' . $nid);
+ $this->assertResponse(
+ $is_private ? 403 : 200,
+ format_string('Node @nid by user @uid should get a @response for this user (@web_user_uid)',
+ array(
+ '@nid' => $nid,
+ '@uid' => $uid,
+ '@response' => $is_private ? 403 : 200,
+ '@web_user_uid' => $web_user->uid,
+ )
+ )
+ );
+ if (!$is_private) {
+ $this->assertTitle($titles[$nid] . ' | Drupal', 'Correct title for node was found');
+ }
+ }
+ }
+ }
+
+ // Check to see that the correct nodes are shown on examples/node_access.
+ $this->drupalGet('examples/node_access');
+ $accessible = $this->xpath("//tr[contains(@class,'accessible')]");
+ $this->assertEqual(count($accessible), 1, 'One private item accessible');
+ foreach ($accessible as $row) {
+ $this->assertEqual($row->td[2], $web_user->uid, 'Accessible row owned by this user');
+ }
+ }
+
+ // Test cases for anonymous user.
+ $this->drupalLogout();
+
+ // Test that private nodes with authors are not accessible.
+ foreach ($private_nodes as $nid) {
+ if (($node = node_load($nid)) === FALSE) {
+ continue;
+ }
+ $this->checkNodeAccess($nid, FALSE, FALSE, FALSE);
+ }
+
+ // Test that private nodes that don't have author are not accessible.
+ foreach ($private_nodes as $nid) {
+ if (($node = node_load($nid)) === FALSE) {
+ continue;
+ }
+ $original_uid = $node->uid;
+
+ // Change node author to anonymous.
+ $node->uid = 0;
+ node_save($node);
+ $node = node_load($nid);
+ $this->assertEqual($node->uid, 0);
+
+ $this->checkNodeAccess($nid, FALSE, FALSE, FALSE);
+
+ // Change node to original author.
+ $node->uid = $original_uid;
+ node_save($node);
+ }
+
+ // Now test that a user with 'access any private content' can view content.
+ $access_user = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'create article content',
+ 'access any private content',
+ 'search content',
+ )
+ );
+ $this->drupalLogin($access_user);
+
+ // Check to see that we find the number of search results expected.
+ $this->checkSearchResults('Private node', 3);
+
+ foreach ($nodes_by_user as $uid => $private_status) {
+ foreach ($private_status as $nid => $is_private) {
+ $this->drupalGet('node/' . $nid);
+ $this->assertResponse(200);
+ }
+ }
+
+ // Check to see that the correct nodes are shown on examples/node_access.
+ // This user should be able to see all 3 of them.
+ $this->drupalGet('examples/node_access');
+ $accessible = $this->xpath("//tr[contains(@class,'accessible')]");
+ $this->assertEqual(count($accessible), 3);
+
+ // Test that a user named 'foobar' can edit any private node due to
+ // node_access_example_node_access(). Note that this user will not be
+ // able to search for private nodes, and will not have available nodes
+ // shown on examples/node_access, because node_access() is not called
+ // for node listings, only for actual access to a node.
+ $edit_user = $this->drupalCreateUser(
+ array(
+ 'access comments',
+ 'access content',
+ 'post comments',
+ 'skip comment approval',
+ 'search content',
+ )
+ );
+ // Update the name of the user to 'foobar'.
+ db_update('users')
+ ->fields(array(
+ 'name' => 'foobar',
+ ))
+ ->condition('uid', $edit_user->uid)
+ ->execute();
+
+ $edit_user->name = 'foobar';
+ $this->drupalLogin($edit_user);
+
+ // Try to edit each of the private nodes.
+ foreach ($private_nodes as $nid) {
+ $body = $this->randomName();
+ $edit = array('body[und][0][value]' => $body);
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+ $this->assertText(t('has been updated'), 'Node was updated by "foobar" user');
+ }
+
+ // Test that a privileged user can edit and delete private content.
+ // This test should go last, as the nodes get deleted.
+ $edit_user = $this->drupalCreateUser(
+ array(
+ 'access content',
+ 'access any private content',
+ 'edit any private content',
+ )
+ );
+ $this->drupalLogin($edit_user);
+ foreach ($private_nodes as $nid) {
+ $body = $this->randomName();
+ $edit = array('body[und][0][value]' => $body);
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+ $this->assertText(t('has been updated'));
+ $this->drupalPost('node/' . $nid . '/edit', array(), t('Delete'));
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->assertText(t('has been deleted'));
+ }
+ }
+
+ /**
+ * Helper function.
+ *
+ * On the search page, search for a string and assert the expected number
+ * of results.
+ *
+ * @param string $search_query
+ * String to search for
+ * @param int $expected_result_count
+ * Expected result count
+ */
+ protected function checkSearchResults($search_query, $expected_result_count) {
+ $this->drupalPost('search/node', array('keys' => $search_query), t('Search'));
+ $search_results = $this->xpath("//ol[contains(@class, 'search-results')]/li");
+ $this->assertEqual(count($search_results), $expected_result_count, 'Found the expected number of search results');
+ }
+
+ /**
+ * Helper function.
+ *
+ * Test if a node with the id $nid has expected access grants.
+ *
+ * @param int $nid
+ * Node that will be checked.
+ *
+ * @return bool
+ * Checker ran successfully
+ */
+ protected function checkNodeAccess($nid, $grant_view, $grant_update, $grant_delete) {
+ // Test if node can be viewed.
+ if (!$this->checkResponse($grant_view, 'node/' . $nid)) {
+ return FALSE;
+ }
+
+ // Test if private node can be edited.
+ if (!$this->checkResponse($grant_update, 'node/' . $nid . '/edit')) {
+ return FALSE;
+ }
+
+ // Test if private node can be deleted.
+ if (!$this->checkResponse($grant_delete, 'node/' . $nid . '/delete')) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+
+ /**
+ * Helper function.
+ *
+ * Test if there is access to an $url
+ *
+ * @param bool $grant
+ * Access to the $url
+ *
+ * @param string $url
+ * url to make the get call.
+ *
+ * @return bool
+ * Get response
+ */
+ protected function checkResponse($grant, $url) {
+ $this->drupalGet($url);
+ if ($grant) {
+ $response = $this->assertResponse(200);
+ }
+ else {
+ $response = $this->assertResponse(403);
+ }
+ return $response;
+ }
+
+}
diff --git a/sites/all/modules/examples/node_example/node_example.info b/sites/all/modules/examples/node_example/node_example.info
new file mode 100644
index 00000000..2b9b8509
--- /dev/null
+++ b/sites/all/modules/examples/node_example/node_example.info
@@ -0,0 +1,19 @@
+name = Node example
+description = Demonstrates a custom content type and uses the field api.
+package = Example modules
+core = 7.x
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:image
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = node_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/node_example/node_example.module b/sites/all/modules/examples/node_example/node_example.module
new file mode 100644
index 00000000..c684b4c8
--- /dev/null
+++ b/sites/all/modules/examples/node_example/node_example.module
@@ -0,0 +1,377 @@
+ 'node_example_page',
+ 'access arguments' => array('access content'),
+ 'title' => 'Node Example',
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_node_info().
+ *
+ * We use hook_node_info() to define our node content type.
+ */
+function node_example_node_info() {
+ // We define the node type as an associative array.
+ return array(
+ 'node_example' => array(
+ 'name' => t('Example Node Type'),
+ // 'base' tells Drupal the base string for hook functions.
+ // This is often the module name; if base is set to 'mymodule',
+ // Drupal would call mymodule_insert() or similar for node
+ // hooks. In our case, the base is 'node_example'.
+ 'base' => 'node_example',
+ 'description' => t('This is an example node type with a few fields.'),
+ 'title_label' => t('Example Title'),
+ // We'll set the 'locked' attribute to TRUE, so users won't be
+ // able to change the machine name of our content type.
+ 'locked' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_node_type_insert().
+ *
+ * Much like hook_node_insert() lets us know that a node is being
+ * inserted into the database, hook_node_type_insert() lets us know
+ * that a new content type has been inserted.
+ *
+ * Since Drupal will at some point insert our new content type,
+ * this gives us a chance to add the fields we want.
+ *
+ * It is called for all inserts to the content type database, so
+ * we have to make sure we're only modifying the type we're
+ * concerned with.
+ */
+function node_example_node_type_insert($content_type) {
+ if ($content_type->type == 'node_example') {
+ // First we add the body field. Node API helpfully gives us
+ // node_add_body_field().
+ // We'll set the body label now, although we could also set
+ // it along with our other instance properties later.
+ $body_instance = node_add_body_field($content_type, t('Example Description'));
+
+ // Add our example_node_list view mode to the body instance
+ // display by instructing the body to display as a summary.
+ $body_instance['display']['example_node_list'] = array(
+ 'label' => 'hidden',
+ 'type' => 'text_summary_or_trimmed',
+ );
+
+ // Save our changes to the body field instance.
+ field_update_instance($body_instance);
+
+ // Create all the fields we are adding to our content type.
+ foreach (_node_example_installed_fields() as $field) {
+ field_create_field($field);
+ }
+
+ // Create all the instances for our fields.
+ foreach (_node_example_installed_instances() as $instance) {
+ $instance['entity_type'] = 'node';
+ $instance['bundle'] = 'node_example';
+ field_create_instance($instance);
+ }
+ }
+}
+
+/**
+ * Implements hook_form().
+ *
+ * Drupal needs for us to provide a form that lets the user
+ * add content. This is the form that the user will see if
+ * they go to node/add/node-example.
+ *
+ * You can get fancy with this form, or you can just punt
+ * and return the default form that node_content will provide.
+ */
+function node_example_form($node, $form_state) {
+ return node_content_form($node, $form_state);
+}
+
+/**
+ * Callback that builds our content and returns it to the browser.
+ *
+ * This callback comes from hook_menu().
+ *
+ * @return array
+ * A renderable array showing a list of our nodes.
+ *
+ * @see node_load()
+ * @see node_view()
+ * @see node_example_field_formatter_view()
+ */
+function node_example_page() {
+ // We'll start building a renderable array that will be our page.
+ // For now we just declare the array.
+ $renderable_array = array();
+ // We query the database and find all of the nodes for the type we defined.
+ $sql = 'SELECT nid FROM {node} n WHERE n.type = :type AND n.status = :status';
+ $result = db_query($sql,
+ array(
+ ':type' => 'node_example',
+ ':status' => 1,
+ )
+ );
+ $renderable_array['explanation'] = array(
+ '#markup' => t("Node Example nodes you've created will be displayed here. Note that the color fields will be displayed differently in this list, than if you view the node normally. Click on the node title to see the difference. This is a result of using our 'example_node_list' node view type."),
+ );
+ // Loop through each of our node_example nodes and instruct node_view
+ // to use our "example_node_list" view.
+ // http://api.drupal.org/api/function/node_load/7
+ // http://api.drupal.org/api/function/node_view/7
+ foreach ($result as $row) {
+ $node = node_load($row->nid);
+ $renderable_array['node_list'][] = node_view($node, 'example_node_list');
+ }
+ return $renderable_array;
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ *
+ * We need to modify the default node entity info by adding a new view mode to
+ * be used in functions like node_view() or node_build_content().
+ */
+function node_example_entity_info_alter(&$entity_info) {
+ // Add our new view mode to the list of view modes...
+ $entity_info['node']['view modes']['example_node_list'] = array(
+ 'label' => t('Example Node List'),
+ 'custom settings' => TRUE,
+ );
+}
+
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function node_example_field_formatter_info() {
+ return array(
+ 'node_example_colors' => array(
+ 'label' => t('Node Example Color Handle'),
+ 'field types' => array('text'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * @todo: We need to provide a formatter for the colors that a user is allowed
+ * to enter during node creation.
+ */
+function node_example_field_formatter_view($object_type, $object, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+ switch ($display['type']) {
+ case 'node_example_colors':
+ foreach ($items as $delta => $item) {
+ $element[$delta]['#type'] = 'markup';
+ $color = $item['safe_value'];
+ $element[$delta]['#markup'] = theme('example_node_color', array('color' => $color));
+ }
+ break;
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * This lets us tell Drupal about our theme functions and their arguments.
+ */
+function node_example_theme($existing, $type, $theme, $path) {
+ return array(
+ 'example_node_color' => array(
+ 'variables' => array('color' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_help().
+ */
+function node_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/node_example':
+ return "
" . t("The Node Example module provides a custom node type.
+ You can create new Example Node nodes using the node add form.",
+ array('!nodeadd' => url('node/add/node-example'))) . "
";
+ }
+}
+
+/**
+ * A custom theme function.
+ *
+ * By using this function to format our node-specific information, themes
+ * can override this presentation if they wish. This is a simplifed theme
+ * function purely for illustrative purposes.
+ */
+function theme_example_node_color($variables) {
+ $output = '' . $variables['color'] . '';
+ return $output;
+}
+
+/**
+ * Define the fields for our content type.
+ *
+ * This big array is factored into this function for readability.
+ *
+ * @return array
+ * An associative array specifying the fields we wish to add to our
+ * new node type.
+ */
+function _node_example_installed_fields() {
+ return array(
+ 'node_example_color' => array(
+ 'field_name' => 'node_example_color',
+ 'cardinality' => 3,
+ 'type' => 'text',
+ 'settings' => array(
+ 'max_length' => 60,
+ ),
+ ),
+ 'node_example_quantity' => array(
+ 'field_name' => 'node_example_quantity',
+ 'cardinality' => 1,
+ 'type' => 'text',
+ ),
+ 'node_example_image' => array(
+ 'field_name' => 'node_example_image',
+ 'type' => 'image',
+ 'cardinality' => 1,
+ ),
+ );
+}
+
+/**
+ * Define the field instances for our content type.
+ *
+ * The instance lets Drupal know which widget to use to allow the user to enter
+ * data and how to react in different view modes. We are going to display a
+ * page that uses a custom "node_example_list" view mode. We will set a
+ * cardinality of three allowing our content type to give the user three color
+ * fields.
+ *
+ * This big array is factored into this function for readability.
+ *
+ * @return array
+ * An associative array specifying the instances we wish to add to our new
+ * node type.
+ */
+function _node_example_installed_instances() {
+ return array(
+ 'node_example_color' => array(
+ 'field_name' => 'node_example_color',
+ 'label' => t('The colors available for this object.'),
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ ),
+ 'display' => array(
+ 'example_node_list' => array(
+ 'label' => 'hidden',
+ 'type' => 'node_example_colors',
+ ),
+ ),
+ ),
+ 'node_example_quantity' => array(
+ 'field_name' => 'node_example_quantity',
+ 'label' => t('Quantity required'),
+ 'type' => 'text',
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ ),
+ 'display' => array(
+ 'example_node_list' => array(
+ 'label' => 'hidden',
+ 'type' => 'hidden',
+ ),
+ ),
+ ),
+ 'node_example_image' => array(
+ 'field_name' => 'node_example_image',
+ 'label' => t('Upload an image:'),
+ 'required' => FALSE,
+ 'widget' => array(
+ 'type' => 'image_image',
+ 'weight' => 2.10,
+ ),
+ 'display' => array(
+ 'example_node_list' => array(
+ 'label' => 'hidden',
+ 'type' => 'image_link_content__thumbnail',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * @} End of "defgroup node_example".
+ */
diff --git a/sites/all/modules/examples/node_example/node_example.test b/sites/all/modules/examples/node_example/node_example.test
new file mode 100644
index 00000000..3c2cd5e2
--- /dev/null
+++ b/sites/all/modules/examples/node_example/node_example.test
@@ -0,0 +1,117 @@
+ 'Node example',
+ 'description' => 'Verify the custom node type creation.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('node_example');
+ }
+
+ /**
+ * API-level content type test.
+ *
+ * This test will verify that when the module is installed, it:
+ * - Adds a new content type, node_example.
+ * - Attaches a body field.
+ * - Attaches three other fields.
+ * - Creates a view mode, example_node_list.
+ */
+ public function testInstallationApi() {
+ // At this point, the module should be installed.
+ // First check for our content type.
+ $node_type = node_type_get_type('node_example');
+ $this->assertTrue($node_type, 'Node Example Type was created.', 'API');
+
+ // How about the body field?
+ $body = field_info_instance('node', 'body', 'node_example');
+ $this->assertTrue($body, 'Node Example Type has a body field.', 'API');
+
+ // Now look for our attached fields.
+ // We made a handy function that tells us...
+ $attached_fields = _node_example_installed_instances();
+ foreach ($attached_fields as $field_name => $field_info) {
+ $field = field_info_instance('node', $field_name, 'node_example');
+ $this->assertTrue($field,
+ 'Field: ' . $field_name . ' was attached to node_example.', 'API');
+ }
+
+ // And that view mode...
+ // entity_get_info() invokes hook_entity_info_alter(), so it's
+ // a good place to verify that our code works.
+ $entities = entity_get_info('node');
+ $this->assertTrue(isset($entities['view modes']['example_node_list']),
+ 'Added example_node_list view mode.', 'API');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testNodeCreation() {
+ // Create and login user.
+ $account = $this->drupalCreateUser(array('access content', 'create node_example content'));
+ $this->drupalLogin($account);
+
+ // Create a new node. The image makes it more complicated, so skip it.
+ $edit = array(
+ 'title' => $this->randomName(),
+ 'node_example_color[und][0][value]' => 'red',
+ 'node_example_color[und][1][value]' => 'green',
+ 'node_example_color[und][2][value]' => 'blue',
+ 'node_example_quantity[und][0][value]' => 100,
+ );
+ $this->drupalPost('node/add/node-example', $edit, t('Save'));
+ $this->assertText("Example Node Type " . $edit['title'] . " has been created", "Found node creation message");
+ $this->assertPattern("/The colors available.*red.*green.*blue/", "Correct 'colors available' on node page");
+
+ // Look on the examples page to make sure it shows up there also.
+ $this->drupalGet('examples/node_example');
+ $this->assertText($edit['title'], "Found random title string");
+ $this->assertPattern("/red.*green.*blue/", "Correct 'colors available' on node example page");
+
+ }
+
+ /**
+ * Check the value of body label.
+ *
+ * Checks whether body label has a value of "Example Description"
+ */
+ public function testBodyLabel() {
+ // Create and login user.
+ $account = $this->drupalCreateUser(array('access content', 'create node_example content'));
+ $this->drupalLogin($account);
+
+ // Request a node add node-example page.
+ // Test whether the body label equals 'Example Description'.
+ // Use '$this->assertRaw' to make certain to test the body label and not
+ // some other text.
+ $this->drupalGet('node/add/node-example');
+ $this->assertResponse(200, 'node/add/node-example page found');
+ $this->assertRaw('', 'Body label equals \'Example Description\'');
+ }
+}
diff --git a/sites/all/modules/examples/nodeapi_example/nodeapi_example.info b/sites/all/modules/examples/nodeapi_example/nodeapi_example.info
new file mode 100644
index 00000000..c23299c0
--- /dev/null
+++ b/sites/all/modules/examples/nodeapi_example/nodeapi_example.info
@@ -0,0 +1,12 @@
+name = NodeAPI example
+description = Demonstrates using the hook_node_* APIs (formerly hook_nodeapi) to alter a node from a different module.
+package = Example modules
+core = 7.x
+files[] = nodeapi_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/nodeapi_example/nodeapi_example.install b/sites/all/modules/examples/nodeapi_example/nodeapi_example.install
new file mode 100644
index 00000000..1d2a7ca2
--- /dev/null
+++ b/sites/all/modules/examples/nodeapi_example/nodeapi_example.install
@@ -0,0 +1,80 @@
+ 'Stores information of extended content.',
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => 'Node ID that the rating is applied to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'vid' => array(
+ 'description' => 'Revision ID, as we are tracking rating with node revisions',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'rating' => array(
+ 'description' => 'The rating of the node.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('vid'),
+ 'indexes' => array(
+ 'nid' => array('nid'),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * We need to clean up our variables data when uninstalling our module.
+ *
+ * Our implementation of nodeapi_example_form_alter() automatically
+ * creates a nodeapi_example_node_type_ variable for each node type
+ * the user wants to rate.
+ *
+ * To delete our variables we call variable_del for our variables'
+ * namespace, 'nodeapi_example_node_type_'. Note that an average module would
+ * have known variables that it had created, and it could just delete those
+ * explicitly. For example, see render_example_uninstall(). It's important
+ * not to delete variables that might be owned by other modules, so normally
+ * we would just explicitly delete a set of known variables.
+ *
+ * hook_uninstall() will only be called when uninstalling a module, not when
+ * disabling a module. This allows our data to stay in the database if the user
+ * only disables our module without uninstalling it.
+ *
+ * @ingroup nodeapi_example
+ */
+function nodeapi_example_uninstall() {
+ // Simple DB query to get the names of our variables.
+ $results = db_select('variable', 'v')
+ ->fields('v', array('name'))
+ ->condition('name', 'nodeapi_example_node_type_%', 'LIKE')
+ ->execute();
+ // Loop through and delete each of our variables.
+ foreach ($results as $result) {
+ variable_del($result->name);
+ }
+}
diff --git a/sites/all/modules/examples/nodeapi_example/nodeapi_example.module b/sites/all/modules/examples/nodeapi_example/nodeapi_example.module
new file mode 100644
index 00000000..423b35e9
--- /dev/null
+++ b/sites/all/modules/examples/nodeapi_example/nodeapi_example.module
@@ -0,0 +1,282 @@
+ 'fieldset',
+ '#title' => t('Rating settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#group' => 'additional_settings',
+ '#weight' => -1,
+ );
+
+ $form['rating']['nodeapi_example_node_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('NodeAPI Example Rating'),
+ '#default_value' => variable_get('nodeapi_example_node_type_' . $form['#node_type']->type, FALSE),
+ '#options' => array(
+ FALSE => t('Disabled'),
+ TRUE => t('Enabled'),
+ ),
+ '#description' => t('Should this node have a rating attached to it?'),
+ );
+ }
+ // Here we check to see if the type and node field are set. If so, it could
+ // be a node edit form.
+ elseif (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] . '_node_form' == $form_id) {
+ // If the rating is enabled for this node type, we insert our control
+ // into the form.
+ $node = $form['#node'];
+ if (variable_get('nodeapi_example_node_type_' . $form['type']['#value'], FALSE)) {
+ $form['nodeapi_example_rating'] = array(
+ '#type' => 'select',
+ '#title' => t('Rating'),
+ '#default_value' => isset($node->nodeapi_example_rating) ? $node->nodeapi_example_rating : '',
+ '#options' => array(0 => t('Unrated'), 1, 2, 3, 4, 5),
+ '#required' => TRUE,
+ '#weight' => 0,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_node_validate().
+ *
+ * Check that the rating attribute is set in the form submission, since the
+ * field is required. If not, send error message.
+ */
+function nodeapi_example_node_validate($node, $form) {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ if (isset($node->nodeapi_example_rating) && !$node->nodeapi_example_rating) {
+ form_set_error('nodeapi_example_rating', t('You must rate this content.'));
+ }
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * Loads the rating information if available for any of the nodes in the
+ * argument list.
+ */
+function nodeapi_example_node_load($nodes, $types) {
+ // We can use $types to figure out if we need to process any of these nodes.
+ $our_types = array();
+ foreach ($types as $type) {
+ if (variable_get('nodeapi_example_node_type_' . $type, FALSE)) {
+ $our_types[] = $type;
+ }
+ }
+
+ // Now $our_types contains all the types from $types that we want
+ // to deal with. If it's empty, we can safely return.
+ if (!count($our_types)) {
+ return;
+ }
+
+ // Now we need to make a list of revisions based on $our_types
+ foreach ($nodes as $node) {
+ // We are using the revision id instead of node id.
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ $vids[] = $node->vid;
+ }
+ }
+ // Check if we should load rating for any of the nodes.
+ if (!isset($vids) || !count($vids)) {
+ return;
+ }
+
+ // When we read, we don't care about the node->nid; we look for the right
+ // revision ID (node->vid).
+ $result = db_select('nodeapi_example', 'e')
+ ->fields('e', array('nid', 'vid', 'rating'))
+ ->where('e.vid IN (:vids)', array(':vids' => $vids))
+ ->execute();
+
+ foreach ($result as $record) {
+ $nodes[$record->nid]->nodeapi_example_rating = $record->rating;
+ }
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * As a new node is being inserted into the database, we need to do our own
+ * database inserts.
+ */
+function nodeapi_example_node_insert($node) {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ // Notice that we are ignoring any revision information using $node->nid
+ db_insert('nodeapi_example')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'vid' => $node->vid,
+ 'rating' => $node->nodeapi_example_rating,
+ ))
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ *
+ * When a node is deleted, we need to remove all related records from our table,
+ * including all revisions. For the delete operations we use node->nid.
+ */
+function nodeapi_example_node_delete($node) {
+ // Notice that we're deleting even if the content type has no rating enabled.
+ db_delete('nodeapi_example')
+ ->condition('nid', $node->nid)
+ ->execute();
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * As an existing node is being updated in the database, we need to do our own
+ * database updates.
+ *
+ * This hook is called when an existing node has been changed. We can't simply
+ * update, since the node may not have a rating saved, thus no
+ * database field. So we first check the database for a rating. If there is one,
+ * we update it. Otherwise, we call nodeapi_example_node_insert() to create one.
+ */
+function nodeapi_example_node_update($node) {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ // Check first if this node has a saved rating.
+ $rating = db_select('nodeapi_example', 'e')
+ ->fields('e', array(
+ 'rating',
+ ))
+ ->where('e.vid = (:vid)', array(':vid' => $node->vid))
+ ->execute()->fetchField();
+
+ if ($rating) {
+ // Node has been rated before.
+ db_update('nodeapi_example')
+ ->fields(array('rating' => $node->nodeapi_example_rating))
+ ->condition('vid', $node->vid)
+ ->execute();
+ }
+ else {
+ // Node was not previously rated, so insert a new rating in database.
+ nodeapi_example_node_insert($node);
+ }
+ }
+}
+
+/**
+ * Implements hook_node_view().
+ *
+ * This is a typical implementation that simply runs the node text through
+ * the output filters.
+ *
+ * Finally, we need to take care of displaying our rating when the node is
+ * viewed. This operation is called after the node has already been prepared
+ * into HTML and filtered as necessary, so we know we are dealing with an
+ * HTML teaser and body. We will inject our additional information at the front
+ * of the node copy.
+ *
+ * Using node API 'hook_node_view' is more appropriate than using a filter here,
+ * because filters transform user-supplied content, whereas we are extending it
+ * with additional information.
+ */
+function nodeapi_example_node_view($node, $build_mode = 'full') {
+ if (variable_get('nodeapi_example_node_type_' . $node->type, FALSE)) {
+ // Make sure to set a rating, also for nodes saved previously and not yet
+ // rated.
+ $rating = isset($node->nodeapi_example_rating) ? $node->nodeapi_example_rating : 0;
+ $node->content['nodeapi_example'] = array(
+ '#markup' => theme('nodeapi_example_rating', array('rating' => $rating)),
+ '#weight' => -1,
+ );
+ }
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * This lets us tell Drupal about our theme functions and their arguments.
+ */
+function nodeapi_example_theme() {
+ return array(
+ 'nodeapi_example_rating' => array(
+ 'variables' => array('rating' => NULL),
+ ),
+ );
+}
+
+/**
+ * A custom theme function.
+ *
+ * By using this function to format our rating, themes can override this
+ * presentation if they wish; for example, they could provide a star graphic
+ * for the rating. We also wrap the default presentation in a CSS class that
+ * is prefixed by the module name. This way, style sheets can modify the output
+ * without requiring theme code.
+ */
+function theme_nodeapi_example_rating($variables) {
+ $options = array(
+ 0 => t('Unrated'),
+ 1 => t('Poor'),
+ 2 => t('Needs improvement'),
+ 3 => t('Acceptable'),
+ 4 => t('Good'),
+ 5 => t('Excellent'),
+ );
+ $output = '
';
+ return $output;
+}
+
+/**
+ * @} End of "defgroup nodeapi_example".
+ */
diff --git a/sites/all/modules/examples/nodeapi_example/nodeapi_example.test b/sites/all/modules/examples/nodeapi_example/nodeapi_example.test
new file mode 100644
index 00000000..5dd756db
--- /dev/null
+++ b/sites/all/modules/examples/nodeapi_example/nodeapi_example.test
@@ -0,0 +1,222 @@
+ 'Node API example functionality',
+ 'description' => 'Demonstrate Node API hooks that allow altering a node. These are the former hook_nodeapi.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enables modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('nodeapi_example');
+
+ // Create admin user. This module has no access control, so we can use a
+ // trusted user. Revision access and revert permissions are required too.
+ $this->webUser = $this->drupalCreateUser(array(
+ // Required to set revision checkbox.
+ 'administer nodes',
+ 'administer content types',
+ 'bypass node access',
+ 'view revisions',
+ 'revert revisions',
+ ));
+ // Login the admin user.
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Log user in, creates an example node, and uses the rating system.
+ */
+ public function testNodeExampleBasic() {
+
+ // Login the user.
+ $this->drupalLogin($this->webUser);
+
+ // Create custom content type.
+ $content_type = $this->drupalCreateContentType();
+ $type = $content_type->type;
+
+ // Go to edit the settings of this content type.
+ $this->drupalGet('admin/structure/types/manage/' . $type);
+ $this->assertResponse(200);
+
+ // Check if the new Rating options appear in the settings page.
+ $this->assertText(t('NodeAPI Example Rating'), 'Rating options found in content type.');
+ $this->assertFieldByName('nodeapi_example_node_type', 1, 'Rating is Disabled by default.');
+
+ // Disable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => 0,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Create an example node.
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ );
+ $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that the rating is not shown, as we have not yet enabled it.
+ $this->assertNoRaw('Rating: ', 'Extended rating information is not shown.');
+
+ // Save current current url (we are viewing the new node).
+ $node_url = $this->getUrl();
+
+ // Enable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => TRUE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Check previously create node. It should be not rated.
+ $this->drupalGet($node_url);
+ $this->assertResponse(200);
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Unrated'))), 'Content is not rated.');
+
+ // Rate the content, 4 is for "Good"
+ $rate = array(
+ 'nodeapi_example_rating' => 4,
+ );
+ $this->drupalPost($node_url . '/edit', $rate, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Good'))), 'Content is successfully rated.');
+
+ }
+
+ /**
+ * Test revisions of ratings.
+ *
+ * Logs user in, creates an example node, and tests rating functionality with
+ * a node using revisions.
+ */
+ public function testNodeExampleRevision() {
+
+ // Login the user.
+ $this->drupalLogin($this->webUser);
+
+ // Create custom content type.
+ $content_type = $this->drupalCreateContentType();
+ $type = $content_type->type;
+
+ // Go to edit the settings of this content type.
+ $this->drupalGet('admin/structure/types/manage/' . $type);
+ $this->assertResponse(200);
+
+ // Check if the new Rating options appear in the settings page.
+ $this->assertText(t('NodeAPI Example Rating'), 'Rating options found in content type.');
+ $this->assertFieldByName('nodeapi_example_node_type', 1, 'Rating is Disabled by default.');
+
+ // Disable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => 0,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Create an example node.
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ );
+ $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that the rating is not shown, as we have not yet enabled it.
+ $this->assertNoRaw('Rating: ', 'Extended rating information is not shown.');
+
+ // Save current current url (we are viewing the new node).
+ $node_url = $this->getUrl();
+
+ // Enable the rating for this content type: 0 for Disabled, 1 for Enabled.
+ $content_settings = array(
+ 'nodeapi_example_node_type' => TRUE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $type, $content_settings, t('Save content type'));
+ $this->assertResponse(200);
+ $this->assertRaw(' has been updated.', 'Settings modified successfully for content type.');
+
+ // Check previously create node. It should be not rated.
+ $this->drupalGet($node_url);
+ $this->assertResponse(200);
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Unrated'))), 'Content is not rated.');
+
+ // Rate the content, 4 is for "Good"
+ $rate = array(
+ 'nodeapi_example_rating' => 4,
+ );
+ $this->drupalPost($node_url . '/edit', $rate, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Good'))), 'Content is successfully rated.');
+
+ // Rate the content to poor using a new revision, 1 is for "Poor"
+ $rate = array(
+ 'nodeapi_example_rating' => 1,
+ 'revision' => 1,
+ );
+ $this->drupalPost($node_url . '/edit', $rate, t('Save'));
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Poor'))), 'Content is successfully rated.');
+
+ // Now switch back to previous revision of the node.
+ $this->drupalGet($node_url . '/revisions');
+ // There is only a revision, so it must work just clicking the first link..
+ $this->clickLink('revert');
+ $revert_form = $this->getUrl();
+ $this->drupalPost($revert_form, array(), t('Revert'));
+
+ // Go back to the node page.
+ $this->drupalGet($node_url);
+ $this->assertResponse(200);
+
+ // Check that content has been rated.
+ $this->assertRaw(t('Rating: %rating', array('%rating' => t('Good'))), 'Content rating matches reverted revision.');
+
+ }
+
+}
diff --git a/sites/all/modules/examples/page_example/page_example.info b/sites/all/modules/examples/page_example/page_example.info
new file mode 100644
index 00000000..9e80ff38
--- /dev/null
+++ b/sites/all/modules/examples/page_example/page_example.info
@@ -0,0 +1,12 @@
+name = Page example
+description = An example module showing how to define a page to be displayed to the user at a given URL.
+package = Example modules
+core = 7.x
+files[] = page_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/page_example/page_example.module b/sites/all/modules/examples/page_example/page_example.module
new file mode 100644
index 00000000..7545c8f8
--- /dev/null
+++ b/sites/all/modules/examples/page_example/page_example.module
@@ -0,0 +1,213 @@
+ array(
+ 'title' => t('Access simple page'),
+ 'description' => t('Allow users to access simple page'),
+ ),
+ 'access arguments page' => array(
+ 'title' => t('Access page with arguments'),
+ 'description' => t('Allow users to access page with arguments'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Because hook_menu() registers URL paths for items defined by the function, it
+ * is necessary for modules that create pages. Each item can also specify a
+ * callback function for a given URL. The menu items returned here provide this
+ * information to the menu system.
+ *
+ * We will define some menus, and their paths will be interpreted as follows:
+ *
+ * If the user accesses http://example.com/?q=examples/page_example/simple,
+ * the menu system will first look for a menu item with that path. In this case
+ * it will find a match, and execute page_example_simple().
+ *
+ * If the user accesses http://example.com/?q=examples/page_example/arguments,
+ * the menu system will find no explicit match, and will fall back to the
+ * closest match, 'examples/page_example', executing page_example_description().
+ *
+ * If the user accesses
+ * http://example.com/?q=examples/page_example/arguments/1/2, the menu
+ * system will first look for examples/page_example/arguments/1/2. Not finding
+ * a match, it will look for examples/page_example/arguments/1/%. Again not
+ * finding a match, it will look for examples/page_example/arguments/%/2.
+ * Yet again not finding a match, it will look for
+ * examples/page_example/arguments/%/%. This time it finds a match, and so will
+ * execute page_example_arguments(1, 2). Since the parameters are passed to
+ * the function after the match, the function can do additional checking or
+ * make use of them before executing the callback function.
+ *
+ * @see hook_menu()
+ * @see menu_example
+ */
+function page_example_menu() {
+
+ // This is the minimum information you can provide for a menu item. This menu
+ // item will be created in the default menu, usually Navigation.
+ $items['examples/page_example'] = array(
+ 'title' => 'Page Example',
+ 'page callback' => 'page_example_description',
+ 'access callback' => TRUE,
+ 'expanded' => TRUE,
+ );
+
+ $items['examples/page_example/simple'] = array(
+ 'title' => 'Simple - no arguments',
+ 'page callback' => 'page_example_simple',
+ 'access arguments' => array('access simple page'),
+ );
+
+ // By using the MENU_CALLBACK type, we can register the callback for this
+ // path without the item appearing in the menu; the admin cannot enable the
+ // item in the menu, either.
+ //
+ // Notice that 'page arguments' is an array of numbers. These will be
+ // replaced with the corresponding parts of the menu path. In this case a 0
+ // would be replaced by 'examples', a 1 by 'page_example', and a 2 by
+ // 'arguments.' 3 and 4 will be replaced by whatever the user provides.
+ // These will be passed as arguments to the page_example_arguments() function.
+ $items['examples/page_example/arguments/%/%'] = array(
+ 'page callback' => 'page_example_arguments',
+ 'page arguments' => array(3, 4),
+ 'access arguments' => array('access arguments page'),
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Constructs a descriptive page.
+ *
+ * Our menu maps this function to the path 'examples/page_example'.
+ */
+function page_example_description() {
+ return array(
+ '#markup' =>
+ t('
The page_example provides two pages, "simple" and "arguments".
The simple page just returns a renderable array for display.
The arguments page takes two arguments and displays them, as in @arguments_link
',
+ array(
+ '@simple_link' => url('examples/page_example/simple', array('absolute' => TRUE)),
+ '@arguments_link' => url('examples/page_example/arguments/23/56', array('absolute' => TRUE)),
+ )
+ ),
+ );
+}
+
+/**
+ * Constructs a simple page.
+ *
+ * The simple page callback, mapped to the path 'examples/page_example/simple'.
+ *
+ * Page callbacks return a renderable array with the content area of the page.
+ * The theme system will later render and surround the content in the
+ * appropriate blocks, navigation, and styling.
+ *
+ * If you do not want to use the theme system (for example for outputting an
+ * image or XML), you should print the content yourself and not return anything.
+ */
+function page_example_simple() {
+ return array('#markup' => '
' . t('Simple page: The quick brown fox jumps over the lazy dog.') . '
');
+}
+
+/**
+ * A more complex page callback that takes arguments.
+ *
+ * This callback is mapped to the path 'examples/page_example/arguments/%/%'.
+ *
+ * The % arguments are passed in from the page URL. In our hook_menu
+ * implementation we instructed the menu system to extract the last two
+ * parameters of the path and pass them to this function as arguments.
+ *
+ * This function also demonstrates a more complex render array in the returned
+ * values. Instead of just rendering the HTML with a theme('item_list'), the
+ * list is left unrendered, and a #theme attached to it so that it can be
+ * rendered as late as possible, giving more parts of the system a chance to
+ * change it if necessary.
+ *
+ * Consult @link http://drupal.org/node/930760 Render Arrays documentation
+ * @endlink for details.
+ */
+function page_example_arguments($first, $second) {
+ // Make sure you don't trust the URL to be safe! Always check for exploits.
+ if (!is_numeric($first) || !is_numeric($second)) {
+ // We will just show a standard "access denied" page in this case.
+ drupal_access_denied();
+ // We actually don't get here.
+ return;
+ }
+
+ $list[] = t("First number was @number.", array('@number' => $first));
+ $list[] = t("Second number was @number.", array('@number' => $second));
+ $list[] = t('The total was @number.', array('@number' => $first + $second));
+
+ $render_array['page_example_arguments'] = array(
+ // The theme function to apply to the #items.
+ '#theme' => 'item_list',
+ // The list itself.
+ '#items' => $list,
+ '#title' => t('Argument Information'),
+ );
+ return $render_array;
+}
+/**
+ * @} End of "defgroup page_example".
+ */
diff --git a/sites/all/modules/examples/page_example/page_example.test b/sites/all/modules/examples/page_example/page_example.test
new file mode 100644
index 00000000..e3a76c6f
--- /dev/null
+++ b/sites/all/modules/examples/page_example/page_example.test
@@ -0,0 +1,126 @@
+ 'Page example functionality',
+ 'description' => 'Creates page and render the content based on the arguments passed in the URL.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('page_example');
+ }
+
+ /**
+ * Generates a random string of ASCII numeric characters (values 48 to 57).
+ *
+ * @param int $length
+ * Length of random string to generate.
+ *
+ * @return string
+ * Randomly generated string.
+ */
+ protected static function randomNumber($length = 8) {
+ $str = '';
+ for ($i = 0; $i < $length; $i++) {
+ $str .= chr(mt_rand(48, 57));
+ }
+ return $str;
+ }
+
+ /**
+ * Verify that current user has no access to page.
+ *
+ * @param string $url
+ * URL to verify.
+ */
+ public function pageExampleVerifyNoAccess($url) {
+ // Test that page returns 403 Access Denied.
+ $this->drupalGet($url);
+ $this->assertResponse(403);
+ }
+
+ /**
+ * Functional test for various page types.
+ */
+ public function testPageExampleBasic() {
+
+ // Verify that anonymous user can't access the pages created by
+ // page_example module.
+ $this->pageExampleVerifyNoAccess('examples/page_example/simple');
+ $this->pageExampleVerifyNoAccess('examples/page_example/arguments/1/2');
+
+ // Create a regular user and login.
+ $this->webUser = $this->drupalCreateUser();
+ $this->drupalLogin($this->webUser);
+
+ // Verify that regular user can't access the pages created by
+ // page_example module.
+ $this->pageExampleVerifyNoAccess('examples/page_example/simple');
+ $this->pageExampleVerifyNoAccess('examples/page_example/arguments/1/2');
+
+ // Create a user with permissions to access 'simple' page and login.
+ $this->webUser = $this->drupalCreateUser(array('access simple page'));
+ $this->drupalLogin($this->webUser);
+
+ // Verify that user can access simple content.
+ $this->drupalGet('examples/page_example/simple');
+ $this->assertResponse(200, 'simple content successfully accessed.');
+ $this->assertText(t('The quick brown fox jumps over the lazy dog.'), 'Simple content successfully verified.');
+
+ // Check if user can't access arguments page.
+ $this->pageExampleVerifyNoAccess('examples/page_example/arguments/1/2');
+
+ // Create a user with permissions to access 'simple' page and login.
+ $this->webUser = $this->drupalCreateUser(array('access arguments page'));
+ $this->drupalLogin($this->webUser);
+
+ // Verify that user can access simple content.
+ $first = $this->randomNumber(3);
+ $second = $this->randomNumber(3);
+ $this->drupalGet('examples/page_example/arguments/' . $first . '/' . $second);
+ $this->assertResponse(200, 'arguments content successfully accessed.');
+ // Verify argument usage.
+ $this->assertRaw(t("First number was @number.", array('@number' => $first)), 'arguments first argument successfully verified.');
+ $this->assertRaw(t("Second number was @number.", array('@number' => $second)), 'arguments second argument successfully verified.');
+ $this->assertRaw(t('The total was @number.', array('@number' => $first + $second)), 'arguments content successfully verified.');
+
+ // Verify incomplete argument call to arguments content.
+ $this->drupalGet('examples/page_example/arguments/' . $first . '/');
+ $this->assertText("provides two pages");
+
+ // Verify invalid argument call to arguments content.
+ $this->drupalGet('examples/page_example/arguments/' . $first . '/' . $this->randomString());
+ $this->assertResponse(403, 'Invalid argument for arguments content successfully verified');
+
+ // Verify invalid argument call to arguments content.
+ $this->drupalGet('examples/page_example/arguments/' . $this->randomString() . '/' . $second);
+ $this->assertResponse(403, 'Invalid argument for arguments content successfully verified');
+
+ // Check if user can't access simple page.
+ $this->pageExampleVerifyNoAccess('examples/page_example/simple');
+ }
+}
diff --git a/sites/all/modules/examples/pager_example/pager_example.info b/sites/all/modules/examples/pager_example/pager_example.info
new file mode 100644
index 00000000..d952c88f
--- /dev/null
+++ b/sites/all/modules/examples/pager_example/pager_example.info
@@ -0,0 +1,12 @@
+name = Pager example
+description = Demonstrates a page with content in a pager
+package = Example modules
+core = 7.x
+files[] = pager_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/pager_example/pager_example.module b/sites/all/modules/examples/pager_example/pager_example.module
new file mode 100644
index 00000000..18a024ba
--- /dev/null
+++ b/sites/all/modules/examples/pager_example/pager_example.module
@@ -0,0 +1,98 @@
+' . t('The layout here is a themed as a table with a default limit of 10 rows per page. The limit can be changed in the code by changing the limit to some other value. This can be extended to add a filter form as well so the user can choose how many they would like to see on each screen.') . '';
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function pager_example_menu() {
+ $items['examples/pager_example'] = array(
+ 'title' => 'Pager example',
+ 'description' => 'Show a page with a long list across multiple pages',
+ 'page callback' => 'pager_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Build the pager query.
+ *
+ * Uses the date_formats table since it is installed with ~35 rows
+ * in it and we don't have to create fake data in order to show
+ * this example.
+ *
+ * @return array
+ * A render array completely set up with a pager.
+ */
+function pager_example_page() {
+ // We are going to output the results in a table with a nice header.
+ $header = array(
+ array('data' => t('DFID')),
+ array('data' => t('Format')),
+ array('data' => t('Type')),
+ );
+
+ // We are extending the PagerDefault class here.
+ // It has a default of 10 rows per page.
+ // The extend('PagerDefault') part here does all the magic.
+ $query = db_select('date_formats', 'd')->extend('PagerDefault');
+ $query->fields('d', array('dfid', 'format', 'type'));
+
+ // Change the number of rows with the limit() call.
+ $result = $query
+ ->limit(10)
+ ->orderBy('d.dfid')
+ ->execute();
+
+ $rows = array();
+ foreach ($result as $row) {
+ // Normally we would add some nice formatting to our rows
+ // but for our purpose we are simply going to add our row
+ // to the array.
+ $rows[] = array('data' => (array) $row);
+ }
+
+ // Create a render array ($build) which will be themed as a table with a
+ // pager.
+ $build['pager_table'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#empty' => t('There are no date formats found in the db'),
+ );
+
+ // Attach the pager theme.
+ $build['pager_pager'] = array('#theme' => 'pager');
+
+ return $build;
+}
+/**
+ * @} End of "defgroup pager_example".
+ */
diff --git a/sites/all/modules/examples/pager_example/pager_example.test b/sites/all/modules/examples/pager_example/pager_example.test
new file mode 100644
index 00000000..554b6f26
--- /dev/null
+++ b/sites/all/modules/examples/pager_example/pager_example.test
@@ -0,0 +1,57 @@
+ 'Pager Example',
+ 'description' => 'Verify the pager functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('pager_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testPagerPage() {
+ // No need to login for this test.
+ $this->drupalGet('examples/pager_example');
+ $this->assertText('next', 'Found next link');
+ $this->assertText('last', 'Found last link');
+
+ // On the first page we shouldn't see the first
+ // or previous links.
+ $this->assertNoText('first', 'No first link on the first page');
+ $this->assertNoText('previous', 'No previous link on the first page');
+
+ // Let's go to the second page.
+ $this->drupalGet('examples/pager_example', array('query' => array('page' => 1)));
+ $this->assertText('next', 'Found next link');
+ $this->assertText('last', 'Found last link');
+
+ // On the second page we should also see the first
+ // and previous links.
+ $this->assertText('first', 'Found first link');
+ $this->assertText('previous', 'Found previous link');
+ }
+}
diff --git a/sites/all/modules/examples/queue_example/queue_example.css b/sites/all/modules/examples/queue_example/queue_example.css
new file mode 100644
index 00000000..fd80c4b1
--- /dev/null
+++ b/sites/all/modules/examples/queue_example/queue_example.css
@@ -0,0 +1,3 @@
+.form-item-string-to-add, div.form-item-claim-time {
+ display: inline;
+}
diff --git a/sites/all/modules/examples/queue_example/queue_example.info b/sites/all/modules/examples/queue_example/queue_example.info
new file mode 100644
index 00000000..a0bbf33e
--- /dev/null
+++ b/sites/all/modules/examples/queue_example/queue_example.info
@@ -0,0 +1,12 @@
+name = Queue example
+description = Examples of using the Drupal Queue API.
+package = Example modules
+core = 7.x
+files[] = queue_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/queue_example/queue_example.module b/sites/all/modules/examples/queue_example/queue_example.module
new file mode 100644
index 00000000..28bd1cc1
--- /dev/null
+++ b/sites/all/modules/examples/queue_example/queue_example.module
@@ -0,0 +1,351 @@
+ 'Queue Example: Insert and remove',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('queue_example_add_remove_form'),
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+/**
+ * Form generator for managing the queue.
+ *
+ * Provides an interface to add items to the queue, to retrieve (claim)
+ * an item from the head of the queue, and to claim and delete. Also
+ * allows the user to run cron manually, so that claimed items can be
+ * released.
+ */
+function queue_example_add_remove_form($form, &$form_state) {
+ // Simple counter that makes it possible to put auto-incrementing default
+ // string into the string to insert.
+ if (empty($form_state['storage']['insert_counter'])) {
+ $form_state['storage']['insert_counter'] = 1;
+ }
+
+ $queue_name = !empty($form_state['values']['queue_name']) ? $form_state['values']['queue_name'] : 'queue_example_first_queue';
+ $items = queue_example_retrieve_queue($queue_name);
+
+ // Add CSS to make the form a bit denser.
+ $form['#attached']['css'] = array(drupal_get_path('module', 'queue_example') . '/queue_example.css');
+
+ $form['help'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('This page is an interface on the Drupal queue API. You can add new items to the queue, "claim" one (retrieve the next item and keep a lock on it), and delete one (remove it from the queue). Note that claims are not expired until cron runs, so there is a special button to run cron to perform any necessary expirations.') . '
',
+ );
+
+ $form['queue_name'] = array(
+ '#type' => 'select',
+ '#title' => t('Choose queue to examine'),
+ '#options' => drupal_map_assoc(array('queue_example_first_queue', 'queue_example_second_queue')),
+ '#default_value' => $queue_name,
+ );
+ $form['queue_show'] = array(
+ '#type' => 'submit',
+ '#value' => t('Show queue'),
+ '#submit' => array('queue_example_show_queue'),
+ );
+ $form['status_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Queue status for @name', array('@name' => $queue_name)),
+ '#collapsible' => TRUE,
+ );
+ $form['status_fieldset']['status'] = array(
+ '#type' => 'markup',
+ '#markup' => theme('queue_items', array('items' => $items)),
+ );
+ $form['insert_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Insert into @name', array('@name' => $queue_name)),
+ );
+ $form['insert_fieldset']['string_to_add'] = array(
+ '#type' => 'textfield',
+ '#size' => 10,
+ '#default_value' => t('item @counter', array('@counter' => $form_state['storage']['insert_counter'])),
+ );
+ $form['insert_fieldset']['add_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Insert into queue'),
+ '#submit' => array('queue_example_add_remove_form_insert'),
+ );
+ $form['claim_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Claim from queue'),
+ '#collapsible' => TRUE,
+ );
+
+ $form['claim_fieldset']['claim_time'] = array(
+ '#type' => 'radios',
+ '#title' => t('Claim time, in seconds'),
+ '#options' => array(
+ 0 => t('none'),
+ 5 => t('5 seconds'),
+ 60 => t('60 seconds'),
+ ),
+ '#description' => t('This time is only valid if cron runs during this time period. You can run cron manually below.'),
+ '#default_value' => !empty($form_state['values']['claim_time']) ? $form_state['values']['claim_time'] : 5,
+ );
+ $form['claim_fieldset']['claim_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Claim the next item from the queue'),
+ '#submit' => array('queue_example_add_remove_form_claim'),
+ );
+ $form['claim_fieldset']['claim_and_delete_item'] = array(
+ '#type' => 'submit',
+ '#value' => t('Claim the next item and delete it'),
+ '#submit' => array('queue_example_add_remove_form_delete'),
+ );
+ $form['claim_fieldset']['run_cron'] = array(
+ '#type' => 'submit',
+ '#value' => t('Run cron manually to expire claims'),
+ '#submit' => array('queue_example_add_remove_form_run_cron'),
+ );
+ $form['delete_queue'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete the queue and items in it'),
+ '#submit' => array('queue_example_add_remove_form_clear_queue'),
+ );
+ return $form;
+}
+
+/**
+ * Submit function for the insert-into-queue button.
+ */
+function queue_example_add_remove_form_insert($form, &$form_state) {
+ // Get a queue (of the default type) called 'queue_example_queue'.
+ // If the default queue class is SystemQueue this creates a queue that stores
+ // its items in the database.
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+
+ // Queue the string.
+ $queue->createItem($form_state['values']['string_to_add']);
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('Queued your string (@string_to_add). There are now @count items in the queue.', array('@count' => $count, '@string_to_add' => $form_state['values']['string_to_add'])));
+ // Setting 'rebuild' to TRUE allows us to keep information in $form_state.
+ $form_state['rebuild'] = TRUE;
+ // Unsetting the string_to_add allows us to set the incremented default value
+ // for the user so they don't have to type anything.
+ unset($form_state['input']['string_to_add']);
+ $form_state['storage']['insert_counter']++;
+}
+
+/**
+ * Submit function for the show-queue button.
+ */
+function queue_example_show_queue($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+
+ // Get the number of items.
+ $count = $queue->numberOfItems();
+
+ // Update the form item counter.
+ $form_state['storage']['insert_counter'] = $count + 1;
+
+ // Unset the string_to_add textbox.
+ unset($form_state['input']['string_to_add']);
+
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit function for the "claim" button.
+ *
+ * Claims (retrieves) an item from the queue and reports the results.
+ */
+function queue_example_add_remove_form_claim($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $item = $queue->claimItem($form_state['values']['claim_time']);
+ $count = $queue->numberOfItems();
+ if (!empty($item)) {
+ drupal_set_message(
+ t('Claimed item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.',
+ array(
+ '@count' => $count,
+ '@item_id' => $item->item_id,
+ '@string' => $item->data,
+ '@seconds' => $form_state['values']['claim_time'],
+ )
+ )
+ );
+ }
+ else {
+ drupal_set_message(t('There were no items in the queue available to claim. There are @count items in the queue.', array('@count' => $count)));
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit function for "Claim and delete" button.
+ */
+function queue_example_add_remove_form_delete($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $count = $queue->numberOfItems();
+ $item = $queue->claimItem(60);
+ if (!empty($item)) {
+ drupal_set_message(
+ t('Claimed and deleted item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.',
+ array(
+ '@count' => $count,
+ '@item_id' => $item->item_id,
+ '@string' => $item->data,
+ '@seconds' => $form_state['values']['claim_time'],
+ )
+ )
+ );
+ $queue->deleteItem($item);
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('There are now @count items in the queue.', array('@count' => $count)));
+ }
+ else {
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('There were no items in the queue available to claim/delete. There are currently @count items in the queue.', array('@count' => $count)));
+ }
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit function for "run cron" button.
+ *
+ * Runs cron (to release expired claims) and reports the results.
+ */
+function queue_example_add_remove_form_run_cron($form, &$form_state) {
+ drupal_cron_run();
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $count = $queue->numberOfItems();
+ drupal_set_message(t('Ran cron. If claimed items expired, they should be expired now. There are now @count items in the queue', array('@count' => $count)));
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for clearing/deleting the queue.
+ */
+function queue_example_add_remove_form_clear_queue($form, &$form_state) {
+ $queue = DrupalQueue::get($form_state['values']['queue_name']);
+ $queue->deleteQueue();
+ drupal_set_message(t('Deleted the @queue_name queue and all items in it', array('@queue_name' => $form_state['values']['queue_name'])));
+}
+
+/**
+ * Retrieves the queue from the database for display purposes only.
+ *
+ * It is not recommended to access the database directly, and this is only here
+ * so that the user interface can give a good idea of what's going on in the
+ * queue.
+ *
+ * @param array $queue_name
+ * The name of the queue from which to fetch items.
+ */
+function queue_example_retrieve_queue($queue_name) {
+ $items = array();
+ $result = db_query("SELECT item_id, data, expire, created FROM {queue} WHERE name = :name ORDER BY item_id",
+ array(':name' => $queue_name),
+ array('fetch' => PDO::FETCH_ASSOC));
+ foreach ($result as $item) {
+ $items[] = $item;
+ }
+ return $items;
+}
+
+/**
+ * Themes the queue display.
+ *
+ * Again, this is not part of the demonstration of the queue API, but is here
+ * just to make the user interface more understandable.
+ *
+ * @param array $variables
+ * Our variables.
+ */
+function theme_queue_items($variables) {
+ $items = $variables['items'];
+ $rows = array();
+ foreach ($items as &$item) {
+ if ($item['expire'] > 0) {
+ $item['expire'] = t("Claimed: expires %expire", array('%expire' => date('r', $item['expire'])));
+ }
+ else {
+ $item['expire'] = t('Unclaimed');
+ }
+ $item['created'] = date('r', $item['created']);
+ $item['content'] = check_plain(unserialize($item['data']));
+ unset($item['data']);
+ $rows[] = $item;
+ }
+ if (!empty($rows)) {
+ $header = array(
+ t('Item ID'),
+ t('Claimed/Expiration'),
+ t('Created'),
+ t('Content/Data'),
+ );
+ $output = theme('table', array('header' => $header, 'rows' => $rows));
+ return $output;
+ }
+ else {
+ return t('There are no items in the queue.');
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function queue_example_theme() {
+ return array(
+ 'queue_items' => array(
+ 'variables' => array('items' => NULL),
+ ),
+ );
+}
+/**
+ * @} End of "defgroup queue_example".
+ */
diff --git a/sites/all/modules/examples/queue_example/queue_example.test b/sites/all/modules/examples/queue_example/queue_example.test
new file mode 100644
index 00000000..9901095e
--- /dev/null
+++ b/sites/all/modules/examples/queue_example/queue_example.test
@@ -0,0 +1,75 @@
+ 'Queue Example functionality',
+ 'description' => 'Test Queue Example functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('queue_example');
+ }
+
+ /**
+ * Test the queue behavior through user interaction.
+ */
+ public function testQueueExampleBasic() {
+
+ // Load the queue with 5 items.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = array('queue_name' => 'queue_example_first_queue', 'string_to_add' => "boogie$i");
+ $this->drupalPost('queue_example/insert_remove', $edit, t('Insert into queue'));
+ $this->assertText(t('There are now @number items in the queue', array('@number' => $i)));
+ }
+ // Claim each of the 5 items with a claim time of 0 seconds.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item from the queue'));
+ $this->assertPattern(t('%Claimed item id=.*string=@string for 0 seconds.%', array('@string' => "boogie$i")));
+ }
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item from the queue'));
+ $this->assertText(t('There were no items in the queue available to claim'));
+
+ // Sleep a second so we can make sure that the timeouts actually time out.
+ // Local systems work fine with this but apparently the PIFR server is so
+ // fast that it needs a sleep before the cron run.
+ sleep(1);
+
+ // Run cron to release expired items.
+ $this->drupalPost(NULL, array(), t('Run cron manually to expire claims'));
+
+ $queue_items = queue_example_retrieve_queue('queue_example_first_queue');
+
+ // Claim and delete each of the 5 items which should now be available.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item and delete it'));
+ $this->assertPattern(t('%Claimed and deleted item id=.*string=@string for 0 seconds.%', array('@string' => "boogie$i")));
+ }
+ // Verify that nothing is left to claim.
+ $edit = array('queue_name' => 'queue_example_first_queue', 'claim_time' => 0);
+ $this->drupalPost(NULL, $edit, t('Claim the next item from the queue'));
+ $this->assertText(t('There were no items in the queue available to claim'));
+ }
+}
diff --git a/sites/all/modules/examples/rdf_example/rdf_example.info b/sites/all/modules/examples/rdf_example/rdf_example.info
new file mode 100644
index 00000000..7523c6cb
--- /dev/null
+++ b/sites/all/modules/examples/rdf_example/rdf_example.info
@@ -0,0 +1,12 @@
+name = RDF Example
+description = Demonstrates an RDF mapping using the RDF mapping API.
+package = Example modules
+core = 7.x
+files[] = rdf_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/rdf_example/rdf_example.install b/sites/all/modules/examples/rdf_example/rdf_example.install
new file mode 100644
index 00000000..3749d43a
--- /dev/null
+++ b/sites/all/modules/examples/rdf_example/rdf_example.install
@@ -0,0 +1,129 @@
+ 'recipe',
+ 'name' => $t('Recipe'),
+ 'base' => 'node_content',
+ 'description' => $t('The recipe node is defined to demonstrate RDF mapping.'),
+ );
+
+ // Set additional defaults and save the content type.
+ $content_type = node_type_set_defaults($rdf_example);
+ node_type_save($content_type);
+
+ // Create all the fields we are adding to our content type.
+ // http://api.drupal.org/api/function/field_create_field/7
+ foreach (_rdf_example_installed_fields() as $field) {
+ field_create_field($field);
+ }
+
+ // Create all the instances for our fields.
+ // http://api.drupal.org/api/function/field_create_instance/7
+ foreach (_rdf_example_installed_instances() as $instance) {
+ $instance['entity_type'] = 'node';
+ $instance['bundle'] = $rdf_example['type'];
+ field_create_instance($instance);
+ }
+}
+
+/**
+ * Return a structured array defining the fields created by this content type.
+ *
+ * @ingroup rdf_example
+ */
+function _rdf_example_installed_fields() {
+ $t = get_t();
+ return array(
+ 'recipe_photo' => array(
+ 'field_name' => 'recipe_photo',
+ 'cardinality' => 1,
+ 'type' => 'image',
+ ),
+ 'recipe_summary' => array(
+ 'field_name' => 'recipe_summary',
+ 'cardinality' => 1,
+ 'type' => 'text',
+ 'settings' => array(
+ 'max_length' => 500,
+ ),
+ ),
+ );
+}
+
+/**
+ * Return a structured array defining the instances for this content type.
+ *
+ * @ingroup rdf_example
+ */
+function _rdf_example_installed_instances() {
+ $t = get_t();
+ return array(
+ 'recipe_photo' => array(
+ 'field_name' => 'recipe_photo',
+ 'label' => $t('Photo of the prepared dish'),
+ ),
+ 'recipe_summary' => array(
+ 'field_name' => 'recipe_summary',
+ 'label' => $t('Short summary describing the dish'),
+ 'widget' => array(
+ 'type' => 'text_textarea',
+ ),
+ ),
+ );
+}
+
+
+/**
+ * Implements hook_uninstall().
+ *
+ * @ingroup rdf_example
+ */
+function rdf_example_uninstall() {
+ // Delete recipe content.
+ $sql = 'SELECT nid FROM {node} n WHERE n.type = :type';
+ $result = db_query($sql, array(':type' => 'recipe'));
+ $nids = array();
+ foreach ($result as $row) {
+ $nids[] = $row->nid;
+ }
+ node_delete_multiple($nids);
+
+ // Delete field instances for now.
+ // Check status of http://drupal.org/node/1015846
+ $instances = field_info_instances('node', 'recipe');
+ foreach ($instances as $instance_name => $instance) {
+ field_delete_instance($instance);
+ }
+
+ // Delete node type.
+ node_type_delete('recipe');
+
+ field_purge_batch(1000);
+}
diff --git a/sites/all/modules/examples/rdf_example/rdf_example.module b/sites/all/modules/examples/rdf_example/rdf_example.module
new file mode 100644
index 00000000..bdce13dd
--- /dev/null
+++ b/sites/all/modules/examples/rdf_example/rdf_example.module
@@ -0,0 +1,86 @@
+ 'node',
+ 'bundle' => 'recipe',
+ 'mapping' => array(
+ 'rdftype' => array('v:Recipe'),
+ // We don't use the default bundle mapping for title. Instead, we add
+ // the v:name property. We still want to use dc:title as well, though,
+ // so we include it in the array.
+ 'title' => array(
+ 'predicates' => array('dc:title', 'v:name'),
+ ),
+ 'recipe_summary' => array(
+ 'predicates' => array('v:summary'),
+ ),
+ // The photo URI isn't a string but instead points to a resource, so we
+ // indicate that the attribute type is rel. If type isn't specified, it
+ // defaults to property, which is used for string values.
+ 'recipe_photo' => array(
+ 'predicates' => array('v:photo'),
+ 'type' => 'rel',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_rdf_namespaces().
+ *
+ * This hook should be used to define any prefixes used by this module that are
+ * not already defined in core by rdf_rdf_namespaces.
+ *
+ * @see hook_rdf_namespaces()
+ */
+function rdf_example_rdf_namespaces() {
+ return array(
+ // Google's namespace for their custom vocabularies.
+ 'v' => 'http://rdf.data-vocabulary.org/#',
+ );
+}
+
+/**
+ * Implements hook_help().
+ */
+function rdf_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/rdf_example':
+ return "
" . t(
+ "The RDF Example module provides RDF mappings for a custom node type and
+ alters another node type's RDF mapping.
+ You can check your RDF using a parser by copying
+ and pasting your HTML source code into the box. For clearest results,
+ use Turtle as your output format.",
+ array('!parser' => url('http://www.w3.org/2007/08/pyRdfa/#distill_by_input'))
+ ) . "
";
+ }
+}
+/**
+ * @} End of "defgroup rdf_example".
+ */
diff --git a/sites/all/modules/examples/rdf_example/rdf_example.test b/sites/all/modules/examples/rdf_example/rdf_example.test
new file mode 100644
index 00000000..cde7cd94
--- /dev/null
+++ b/sites/all/modules/examples/rdf_example/rdf_example.test
@@ -0,0 +1,55 @@
+ 'RDFa markup',
+ 'description' => 'Test RDFa markup generation.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('rdf', 'field_test', 'rdf_example');
+ }
+
+ /**
+ * Test whether RDF mapping is define in markup.
+ *
+ * Create a recipe node and test whether the RDF mapping defined for this
+ * bundle is reflected in the markup.
+ */
+ public function testAttributesInMarkup() {
+ $node = $this->drupalCreateNode(array('type' => 'recipe'));
+ $this->drupalGet('node/' . $node->nid);
+ $iso_date = date('c', $node->changed);
+ $url = url('node/' . $node->nid);
+
+ // The title is mapped to dc:title and v:name and is exposed in a meta tag
+ // in the header.
+ $recipe_title = $this->xpath("//span[contains(@property, 'dc:title') and contains(@property, 'v:name') and @content='$node->title']");
+ $this->assertTrue(!empty($recipe_title), 'Title is exposed with dc:title and v:name in meta element.');
+
+ // Test that the type is applied and that the default mapping for date is
+ // used.
+ $recipe_meta = $this->xpath("//div[(@about='$url') and (@typeof='v:Recipe')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$iso_date']");
+ $this->assertTrue(!empty($recipe_meta), 'RDF type is present on post. Properties dc:date and dc:created are present on post date.');
+ }
+}
diff --git a/sites/all/modules/examples/render_example/render_example.css b/sites/all/modules/examples/render_example/render_example.css
new file mode 100644
index 00000000..6b2ae5f2
--- /dev/null
+++ b/sites/all/modules/examples/render_example/render_example.css
@@ -0,0 +1,20 @@
+.render-array {
+ border: 2px solid black;
+ margin-top: 10px;
+ padding-left: 5px;
+ padding-top: 5px;
+}
+
+.render-header {
+ font-size: large;
+ font-style: italic;
+}
+
+.unrendered-label {
+ font-style: italic;
+ margin-top: 10px;
+}
+
+.rendered {
+ background-color: lightblue;
+}
diff --git a/sites/all/modules/examples/render_example/render_example.info b/sites/all/modules/examples/render_example/render_example.info
new file mode 100644
index 00000000..52f9ad17
--- /dev/null
+++ b/sites/all/modules/examples/render_example/render_example.info
@@ -0,0 +1,14 @@
+name = Render example
+description = Demonstrates drupal_render's capabilities and altering render arrays
+package = Example modules
+core = 7.x
+dependencies[] = devel
+stylesheets[all][] = render_example.css
+files[] = render_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/render_example/render_example.install b/sites/all/modules/examples/render_example/render_example.install
new file mode 100644
index 00000000..d2aa2e87
--- /dev/null
+++ b/sites/all/modules/examples/render_example/render_example.install
@@ -0,0 +1,17 @@
+ 'Render Example',
+ 'page callback' => 'render_example_info',
+ 'access callback' => TRUE,
+ );
+ $items['examples/render_example/altering'] = array(
+ 'title' => 'Alter pages and blocks',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('render_example_demo_form'),
+ 'access arguments' => array('access devel information'),
+ );
+ $items['examples/render_example/arrays'] = array(
+ 'title' => 'Render array examples',
+ 'page callback' => 'render_example_arrays',
+ 'access callback' => TRUE,
+ );
+
+ return $items;
+}
+
+
+/**
+ * Simple basic information about the module; an entry point.
+ */
+function render_example_info() {
+ return t('The render example provides a
', array('!arrays' => url('examples/render_example/arrays'), '!alter' => url('examples/render_example/altering')));
+}
+
+
+/**
+ * Provides a number of render arrays and show what they do.
+ *
+ * Each array is keyed by a description; it's returned for rendering at page
+ * render time. It's easy to add new examples to this.
+ *
+ * The array items in $demos are intended to be raw, normal render arrays
+ * that can be experimented with to end up with different outcomes.
+ */
+function render_example_arrays() {
+
+ // Interval in seconds for cache update with #cache.
+ $interval = 60;
+
+ $demos = array(
+ // Demonstrate the simplest markup, a #markup element.
+ t('Super simple #markup') => array(
+ '#markup' => t('Some basic text in a #markup (shows basic markup and how it is rendered)'),
+ ),
+
+ // Shows how #prefix and #suffix can add markup into an array.
+ t('Using #prefix and #suffix') => array(
+ '#markup' => t('This one adds a prefix and suffix, which put a div around the item'),
+ '#prefix' => '
(prefix) ',
+ '#suffix' => ' (suffix)
',
+ ),
+
+ // When #theme is provided, it is the #theme function's job to figure out
+ // the meaning of the render array. The #theme function receives the entire
+ // element in $variables and must return it, where it will be the content
+ // of '#children'. When a #theme or other function is provided, custom
+ // properties can be invented and used as needed, as the #separator
+ // property provided here.
+ //
+ // If #theme is not provided, either explicitly or by the underlying
+ // element, then the children are rendered using their own properties and
+ // the results go into #children.
+ t('theme for an element') => array(
+ 'child' => array(
+ t('This is some text that should be put together'),
+ t('This is some more text that we need'),
+ ),
+ // An element we've created which will be used by our theming function.
+ '#separator' => ' | ',
+ '#theme' => 'render_example_aggregate',
+ ),
+
+ // #theme_wrappers provides an array of theme functions which theme the
+ // envelope or "wrapper" of a set of child elements. The theme function
+ // finds its element children (the sub-arrays) already rendered in
+ // '#children'.
+ t('theme_wrappers demonstration') => array(
+ 'child1' => array('#markup' => t('Markup for child1')),
+ 'child2' => array('#markup' => t('Markup for child2')),
+ '#theme_wrappers' => array('render_example_add_div', 'render_example_add_notes'),
+ ),
+
+ // Add '#pre_render' and '#post_render' handlers.
+ // - '#pre_render' functions get access to the array before it is rendered
+ // and can change it. This is similar to a theme function, but it is a
+ // specific fixed function and changes the array in place rather than
+ // rendering it..
+ // - '#post_render' functions get access to the rendered content, but also
+ // have the original array available.
+ t('pre_render and post_render') => array(
+ '#markup' => '
' . t('markup for pre_render and post_render example') . '
',
+ '#pre_render' => array('render_example_add_suffix'),
+ '#post_render' => array('render_example_add_prefix'),
+ ),
+
+ // Cache an element for $interval seconds using #cache.
+ // The assumption here is that this is an expensive item to render, perhaps
+ // large or otherwise expensive. Of course here it's just a piece of markup,
+ // so we don't get the value.
+ //
+ // #cache allows us to set
+ // - 'keys', an array of strings that will create the string cache key.
+ // - 'bin', the cache bin
+ // - 'expire', the expire timestamp. Note that this is actually limited
+ // to the granularity of a cron run.
+ // - 'granularity', a bitmask determining at what level the caching is done
+ // (user, role, page).
+ t('cache demonstration') => array(
+ // If your expensive function were to be executed here it would happen
+ // on every page load regardless of the cache. The actual markup is
+ // added via the #pre_render function, so that drupal_render() will only
+ // execute the expensive function if this array has not been cached.
+ '#markup' => '',
+ '#pre_render' => array('render_example_cache_pre_render'),
+ '#cache' => array(
+ 'keys' => array('render_example', 'cache', 'demonstration'),
+ 'bin' => 'cache',
+ 'expire' => time() + $interval,
+ 'granularity' => DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE,
+ ),
+ ),
+ );
+
+ // The rest of this function just places the above arrays in a context where
+ // they can be rendered (hopefully attractively and usefully) on the page.
+ $page_array = array();
+ foreach ($demos as $key => $item) {
+ $page_array[$key]['#theme_wrappers'] = array('render_array');
+ $page_array[$key]['#description'] = $key;
+
+ $page_array[$key]['unrendered'] = array(
+ '#prefix' => '
' . t('Unrendered array (as plain text and with a krumo version)') . ':
',
+ '#type' => 'markup',
+ '#markup' => htmlentities(drupal_var_export($item)),
+ );
+ $page_array[$key]['kpr'] = array(
+ // The kpr() function is from devel module and is here only allow us
+ // to output the array in a way that's easy to explore.
+ '#markup' => kpr($item, TRUE),
+ );
+ $page_array[$key]['hr'] = array('#markup' => '');
+ $page_array[$key]['rendered'] = array($item);
+ $page_array[$key]['rendered']['#prefix'] = '
Rendered version (light blue):
' . '
';
+ $page_array[$key]['rendered']['#suffix'] = '
';
+ }
+
+ return $page_array;
+}
+
+/**
+ * A '#pre_render' function.
+ *
+ * @param array $element
+ * The element which will be rendered.
+ *
+ * @return array
+ * The altered element. In this case we add the #markup.
+ */
+function render_example_cache_pre_render($element) {
+ $element['#markup'] = render_example_cache_expensive();
+
+ // The following line is due to the bug described in
+ // http://drupal.org/node/914792. A #markup element's #pre_render must set
+ // #children because it replaces the default #markup pre_render, which is
+ // drupal_pre_render_markup().
+ $element['#children'] = $element['#markup'];
+ return $element;
+}
+
+/**
+ * A potentially expensive function.
+ *
+ * @return string
+ * Some demo text.
+ */
+function render_example_cache_expensive() {
+ $interval = 60;
+ $time_message = t('The current time was %time when this was cached. Updated every %interval seconds', array('%time' => date('r'), '%interval' => $interval));
+ // Uncomment the following line to demonstrate that this function is not
+ // being run when the rendered array is cached.
+ // drupal_set_message($time_message);
+ return $time_message;
+}
+
+/**
+ * A '#pre_render' function.
+ *
+ * @param array $element
+ * The element which will be rendered.
+ *
+ * @return array
+ * The altered element. In this case we add a #prefix to it.
+ */
+function render_example_add_suffix($element) {
+ $element['#suffix'] = '
' . t('This #suffix was added by a #pre_render') . '
';
+
+ // The following line is due to the bug described in
+ // http://drupal.org/node/914792. A #markup element's #pre_render must set
+ // #children because it replaces the default #markup pre_render, which is
+ // drupal_pre_render_markup().
+ $element['#children'] = $element['#markup'];
+ return $element;
+}
+
+/**
+ * A '#post_render' function to add a little markup onto the end markup.
+ *
+ * @param string $markup
+ * The rendered element.
+ * @param array $element
+ * The element which was rendered (for reference)
+ *
+ * @return string
+ * Markup altered as necessary. In this case we add a little postscript to it.
+ */
+function render_example_add_prefix($markup, $element) {
+ $markup = '
This markup was added after rendering by a #post_render
' . $markup;
+ return $markup;
+}
+
+/**
+ * A #theme function.
+ *
+ * This #theme function has the responsibility of consolidating/rendering the
+ * children's markup and returning it, where it will be placed in the
+ * element's #children property.
+ */
+function theme_render_example_aggregate($variables) {
+ $output = '';
+ foreach (element_children($variables['element']['child']) as $item) {
+ $output .= $variables['element']['child'][$item] . $variables['element']['#separator'];
+ }
+ return $output;
+}
+
+/*************** Altering Section **************************
+ * The following section of the example builds and arranges the altering
+ * example.
+ */
+
+/**
+ * Builds the form that offers options of what items to show.
+ */
+function render_example_demo_form($form, &$form_state) {
+ $form['description'] = array(
+ '#type' => 'markup',
+ '#markup' => t('This example shows what render arrays look like in the building of a page. It will not work unless the user running it has the "access devel information" privilege. It shows both the actual arrays used to build a page or block and also the capabilities of altering the page late in its lifecycle.'),
+ );
+
+ $form['show_arrays'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Show render arrays'),
+ );
+
+ foreach (array('block', 'page') as $type) {
+ $form['show_arrays']['render_example_show_' . $type] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show @type render arrays', array('@type' => $type)),
+ '#default_value' => variable_get('render_example_show_' . $type, FALSE),
+ );
+ }
+
+ $form['page_fiddling'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Make changes on page via hook_page_alter()'),
+ );
+ $form['page_fiddling']['render_example_note_about_render_arrays'] = array(
+ '#title' => t('Add a note about render arrays to top of sidebar_first (if it exists)'),
+ '#description' => t('Creates a simple render array that displays the use of #pre_render, #post_render, #theme, and #theme_wrappers.'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_note_about_render_arrays', FALSE),
+ );
+ $form['page_fiddling']['render_example_move_navigation_menu'] = array(
+ '#title' => t('Move the navigation menu to the top of the content area'),
+ '#description' => t('Uses hook_page_alter() to move the navigation menu into another region.'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_move_navigation_menu', FALSE),
+ );
+ $form['page_fiddling']['render_example_reverse_sidebar'] = array(
+ '#title' => t('Reverse ordering of sidebar_first elements (if it exists) - will affect the above'),
+ '#description' => t('Uses hook_page_alter() to reverse the ordering of items in sidebar_first'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_reverse_sidebar', FALSE),
+ );
+ $form['page_fiddling']['render_example_prefix'] = array(
+ '#title' => t('Use #prefix and #suffix to wrap a div around every block'),
+ '#description' => t('Uses hook_page_alter to wrap all blocks with a div using #prefix and #suffix'),
+ '#type' => 'checkbox',
+ '#default_value' => variable_get('render_example_prefix'),
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Implements hook_page_alter().
+ *
+ * Alters the page in several different ways based on how the form has been
+ * configured.
+ */
+function render_example_page_alter(&$page) {
+
+ // Re-sort the sidebar in reverse order.
+ if (variable_get('render_example_reverse_sidebar', FALSE) && !empty($page['sidebar_first'])) {
+ $page['sidebar_first'] = array_reverse($page['sidebar_first']);
+ foreach (element_children($page['sidebar_first']) as $element) {
+ // Reverse the weights if they exist.
+ if (!empty($page['sidebar_first'][$element]['#weight'])) {
+ $page['sidebar_first'][$element]['#weight'] *= -1;
+ }
+ }
+ $page['sidebar_first']['#sorted'] = FALSE;
+ }
+
+ // Add a list of items to the top of sidebar_first.
+ // This shows how #theme and #theme_wrappers work.
+ if (variable_get('render_example_note_about_render_arrays', FALSE) && !empty($page['sidebar_first'])) {
+ $items = array(
+ t('Render arrays are everywhere in D7'),
+ t('Leave content unrendered as much as possible'),
+ t('This allows rearrangement and alteration very late in page cycle'),
+ );
+
+ $note = array(
+ '#title' => t('Render Array Example'),
+ '#items' => $items,
+
+ // The functions in #pre_render get to alter the actual data before it
+ // gets rendered by the various theme functions.
+ '#pre_render' => array('render_example_change_to_ol'),
+ // The functions in #post_render get both the element and the rendered
+ // data and can add to the rendered data.
+ '#post_render' => array('render_example_add_hr'),
+ // The #theme theme operation gets the first chance at rendering the
+ // element and its children.
+ '#theme' => 'item_list',
+ // Then the theme operations in #theme_wrappers can wrap more around
+ // what #theme left in #chilren.
+ '#theme_wrappers' => array('render_example_add_div', 'render_example_add_notes'),
+ '#weight' => -9999,
+ );
+ $page['sidebar_first']['render_array_note'] = $note;
+ $page['sidebar_first']['#sorted'] = FALSE;
+ }
+
+ // Move the navigation menu into the content area.
+ if (variable_get('render_example_move_navigation_menu', FALSE) && !empty($page['sidebar_first']['system_navigation']) && !empty($page['content'])) {
+ $page['content']['system_navigation'] = $page['sidebar_first']['system_navigation'];
+ $page['content']['system_navigation']['#weight'] = -99999;
+ unset($page['content']['#sorted']);
+ unset($page['sidebar_first']['system_navigation']);
+ }
+
+ // Show the render array used to build the page render array display.
+ if (variable_get('render_example_show_page', FALSE)) {
+ $form['render_example_page_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Page render array'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+ $form['render_example_page_fieldset']['markup'] = array(
+ // The kpr() function is from devel module and is here only allow us
+ // to output the array in a way that's easy to explore.
+ '#markup' => kpr($page, TRUE),
+ );
+ $page['content']['page_render_array'] = drupal_get_form('render_example_embedded_form', $form);
+ $page['content']['page_render_array']['#weight'] = -999999;
+ $page['content']['#sorted'] = FALSE;
+ }
+
+ // Add render array to the bottom of each block.
+ if (variable_get('render_example_show_block', FALSE)) {
+ foreach (element_children($page) as $region_name) {
+ foreach (element_children($page[$region_name]) as $block_name) {
+
+ // Push the block down a level so we can add another block after it.
+ $old_block = $page[$region_name][$block_name];
+ $page[$region_name][$block_name] = array(
+ $block_name => $old_block,
+ );
+ $form = array();
+ $form['render_example_block_fieldset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Block render array'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+
+ $form['render_example_block_fieldset']['markup'] = array(
+ '#type' => 'item',
+ '#title' => t('%blockname block render array', array('%blockname' => $block_name)),
+ // The kpr() function is from devel module and is here only allow us
+ // to output the array in a way that's easy to explore.
+ '#markup' => kpr($old_block, TRUE),
+ );
+
+ // Add the new block that contains the render array.
+ $page[$region_name][$block_name]['render_example_block_render_array'] = drupal_get_form('render_example_embedded_form', $form);
+ $page[$region_name][$block_name]['render_example_block_render_array']['#weight'] = 999;
+ }
+ }
+ }
+
+ // Add #prefix and #suffix to a block to wrap a div around it.
+ if (variable_get('render_example_prefix', FALSE)) {
+ foreach (element_children($page) as $region_name) {
+ foreach (element_children($page[$region_name]) as $block_name) {
+ $block = &$page[$region_name][$block_name];
+ $block['#prefix'] = '
Prefixed
';
+ $block['#suffix'] = 'Block suffix
';
+ }
+ }
+ }
+
+}
+
+/**
+ * Utility function to build a named form given a set of form elements.
+ *
+ * This is a standard form builder function that takes an additional array,
+ * which is itself a form.
+ *
+ * @param array $form
+ * Form API form array.
+ * @param array $form_state
+ * Form API form state array.
+ * @param array $form_items
+ * The form items to be included in this form.
+ */
+function render_example_embedded_form($form, &$form_state, $form_items) {
+ return $form_items;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function render_example_theme() {
+ $items = array(
+ 'render_example_add_div' => array(
+ 'render element' => 'element',
+ ),
+ 'render_example_add_notes' => array(
+ 'render element' => 'element',
+ ),
+ 'render_array' => array(
+ 'render element' => 'element',
+ ),
+ 'render_example_aggregate' => array(
+ 'render element' => 'element',
+ ),
+ );
+ return $items;
+}
+
+/**
+ * Wraps a div around the already-rendered #children.
+ */
+function theme_render_example_add_div($variables) {
+ $element = $variables['element'];
+ $output = '
';
+ return $output;
+}
+
+/**
+ * Wraps a div and add a little text after the rendered #children.
+ */
+function theme_render_example_add_notes($variables) {
+ $element = $variables['element'];
+ $output = '
';
+ $output .= $element['#children'];
+ $output .= '' . t('This is a note added by a #theme_wrapper') . '';
+ $output .= '
';
+ return $rendered;
+}
+
+/**
+ * Adds a #type to the element before it gets rendered.
+ *
+ * In this case, changes from the default 'ul' to 'ol'.
+ *
+ * @param array $element
+ * The element to be altered, in this case a list, ready for theme_item_list.
+ *
+ * @return array
+ * The altered list (with '#type')
+ */
+function render_example_change_to_ol($element) {
+ $element['#type'] = 'ol';
+ return $element;
+}
+
+/**
+ * Alter the rendered output after all other theming.
+ *
+ * This #post_render function gets to alter the rendered output after all
+ * theme functions have acted on it, and it receives the original data, so
+ * can make decisions based on that. In this example, no use is made of the
+ * passed-in $element.
+ *
+ * @param string $markup
+ * The already-rendered data
+ * @param array $element
+ * The data element that was rendered
+ *
+ * @return string
+ * The altered data.
+ */
+function render_example_add_hr($markup, $element) {
+ $output = $markup . '';
+ return $output;
+}
+/**
+ * @} End of "defgroup render_example".
+ */
diff --git a/sites/all/modules/examples/render_example/render_example.test b/sites/all/modules/examples/render_example/render_example.test
new file mode 100644
index 00000000..744763f1
--- /dev/null
+++ b/sites/all/modules/examples/render_example/render_example.test
@@ -0,0 +1,150 @@
+ 'Render example functionality',
+ 'description' => 'Test Render Example',
+ 'group' => 'Examples',
+ 'dependencies' => array('devel'),
+ );
+ }
+
+ /**
+ * Enable modules and create user with specific permissions.
+ */
+ public function setUp() {
+ parent::setUp('devel', 'render_example');
+ }
+
+
+ /**
+ * Assert that all of the xpaths in the array have results.
+ *
+ * @param array $xpath_array
+ * An array of xpaths, each of which must return something.
+ */
+ public function assertRenderResults($xpath_array) {
+ foreach ($xpath_array as $xpath) {
+ $result = $this->xpath($xpath);
+ $this->assertTrue(!empty($result), format_string('Found xpath %xpath', array('%xpath' => $xpath)));
+ }
+ }
+
+
+ /**
+ * Asserts that the string value of the result is the same as the passed text.
+ *
+ * @param array $xpath_array
+ * Array of keyed arrays of tests to be made. Each child array consists of
+ * $xpath => $expected_text
+ */
+ public function assertRenderedText($xpath_array) {
+ foreach ($xpath_array as $xpath => $text) {
+ $result = $this->xpath($xpath);
+ $this->assertTrue((string) $result[0][0] == $text, format_string('%ary selects text %text', array('%ary' => $xpath, '%text' => $text)));
+ }
+ }
+
+
+ /**
+ * Basic test of rendering through user interaction.
+ *
+ * Login user, create an example node, and test blog functionality through
+ * the admin and user interfaces.
+ */
+ public function testRenderExampleBasic() {
+
+ // Create a user that can access devel information and log in.
+ $web_user = $this->drupalCreateUser(array('access devel information', 'access content'));
+ $this->drupalLogin($web_user);
+
+ // Turn on the block render array display and make sure it shows up.
+ $edit = array(
+ 'render_example_show_block' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+
+ $xpath_array = array(
+ "//div[@id='sidebar-first']//fieldset[starts-with(@id, 'edit-render-example-block-fieldset')]",
+ '//*[@id="content"]//fieldset[contains(@id,"edit-render-example-block-fieldset")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Turn off block render array display and turn on the page render array
+ // display.
+ $edit = array(
+ 'render_example_show_page' => TRUE,
+ 'render_example_show_block' => FALSE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+
+ $xpath_array = array(
+ '//*[@id="content"]//fieldset[starts-with(@id,"edit-render-example-page-fieldset")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Add note about render arrays to the top of sidebar_first.
+ $edit = array(
+ 'render_example_note_about_render_arrays' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+ $xpath_array = array(
+ '//*[@id="sidebar-first"]//ol//li[starts-with(.,"Render arrays are everywhere")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Move the navigation menu to the top of the content area.
+ $edit = array(
+ 'render_example_move_navigation_menu' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+ $xpath_array = array(
+ '//*[@id="content"]//h2[starts-with(.,"Navigation")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Skip a test for reversing order of sidebar_first as I think it would
+ // be too fragile.
+ //
+ // Test the addition of #prefix and #suffix
+ $edit = array(
+ 'render_example_prefix' => TRUE,
+ );
+ $this->drupalPost('examples/render_example/altering', $edit, t('Save configuration'));
+ $xpath_array = array(
+ '//*[@id="sidebar-first"]//*[contains(@class, "block-prefix")]/span[contains(@class, "block-suffix")]',
+ );
+ $this->assertRenderResults($xpath_array);
+
+ // Test some rendering facets of the various render examples.
+ $this->drupalGet('examples/render_example/arrays');
+ $content = $this->xpath('//*[@class="render-array"][1]');
+
+ $xpath_array = array(
+ '//div[@class="rendered"][starts-with(.,"Some basic text in a #markup")]' => 'Some basic text in a #markup (shows basic markup and how it is rendered)',
+ '//div[@class="rendered"][starts-with(.,"This is some text that should be put to")]' => 'This is some text that should be put together | This is some more text that we need | ',
+ '//div[@class="rendered"][starts-with(.,"The current time was")]' => 'The current time was when this was cached. Updated every seconds',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"(prefix)This one")]' => '(prefix)This one adds a prefix and suffix, which put a div around the item(suffix)',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"markup for pre_")]' => 'markup for pre_render and post_render example',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"This markup was added")]' => 'This markup was added after rendering by a #post_render',
+ '//div[@class="rendered"]/div[text()][starts-with(.,"This #suffix")]' => 'This #suffix was added by a #pre_render',
+ );
+ $this->assertRenderedText($xpath_array);
+
+ }
+}
diff --git a/sites/all/modules/examples/simpletest_example/simpletest_example.info b/sites/all/modules/examples/simpletest_example/simpletest_example.info
new file mode 100644
index 00000000..09a647e1
--- /dev/null
+++ b/sites/all/modules/examples/simpletest_example/simpletest_example.info
@@ -0,0 +1,19 @@
+name = SimpleTest Example
+description = Provides simpletest_example page node type.
+package = Example modules
+core = 7.x
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:simpletest
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = simpletest_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/simpletest_example/simpletest_example.install b/sites/all/modules/examples/simpletest_example/simpletest_example.install
new file mode 100644
index 00000000..0f412270
--- /dev/null
+++ b/sites/all/modules/examples/simpletest_example/simpletest_example.install
@@ -0,0 +1,29 @@
+ array(
+ 'name' => t('SimpleTest Example Node Type'),
+ 'base' => 'simpletest_example',
+ 'description' => t('simpletest_example page node type.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * In this case we're adding an addition permission that does the same
+ * as the one the node module offers, just to demonstrate this error.
+ */
+function simpletest_example_permission() {
+ $perms = array();
+ $perms['extra special edit any simpletest_example'] = array('title' => t('Extra special edit any SimpleTest Example'), 'description' => t('Extra special edit any SimpleTest Example'));
+ return $perms;
+}
+
+/**
+ * Implements hook_node_access().
+ *
+ * Demonstrates a bug that we'll find in our test.
+ *
+ * If this is running on the testbot, we don't want the error to show so will
+ * work around it by testing to see if we're in the 'checkout' directory.
+ */
+function simpletest_example_node_access($node, $op, $account) {
+ // Don't get involved if this isn't a simpletest_example node, etc.
+ $type = is_string($node) ? $node : $node->type;
+ if ($type != 'simpletest_example' || ($op != 'update' && $op != 'delete')) {
+ return NODE_ACCESS_IGNORE;
+ }
+
+ // This code has a BUG that we'll find in testing.
+ //
+ // This is the incorrect version we'll use to demonstrate test failure.
+ // The correct version should have ($op == 'update' || $op == 'delete').
+ // The author had mistakenly always tested with User 1 so it always
+ // allowed access and the bug wasn't noticed!
+ if (($op == 'delete') && (user_access('extra special edit any simpletest_example', $account) && ($account->uid == $node->uid))) {
+ return NODE_ACCESS_ALLOW;
+ }
+
+ return NODE_ACCESS_DENY;
+}
+
+/**
+ * Implements hook_form().
+ *
+ * Form for the node type.
+ */
+function simpletest_example_form($node, $form_state) {
+ $type = node_type_get_type($node);
+ $form = array();
+ if ($type->has_title) {
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => check_plain($type->title_label),
+ '#required' => TRUE,
+ '#default_value' => $node->title,
+ '#maxlength' => 255,
+ '#weight' => -5,
+ );
+ }
+ return $form;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides an explanation.
+ */
+function simpletest_example_menu() {
+ $items['examples/simpletest_example'] = array(
+ 'title' => 'Simpletest Example',
+ 'description' => 'Explain the simpletest example and allow the error logic to be executed.',
+ 'page callback' => '_simpletest_example_explanation',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Returns an explanation of this module.
+ */
+function _simpletest_example_explanation() {
+
+ $explanation = t("This Simpletest Example is designed to give an introductory tutorial to writing
+ a simpletest test. Please see the associated tutorial.");
+ return $explanation;
+}
+
+/**
+ * A simple self-contained function used to demonstrate unit tests.
+ *
+ * @see SimpletestUnitTestExampleTestCase
+ */
+function simpletest_example_empty_mysql_date($date_string) {
+ if (empty($date_string) || $date_string == '0000-00-00' || $date_string == '0000-00-00 00:00:00') {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * @} End of "defgroup simpletest_example".
+ */
diff --git a/sites/all/modules/examples/simpletest_example/simpletest_example.test b/sites/all/modules/examples/simpletest_example/simpletest_example.test
new file mode 100644
index 00000000..403891e9
--- /dev/null
+++ b/sites/all/modules/examples/simpletest_example/simpletest_example.test
@@ -0,0 +1,265 @@
+ 'SimpleTest Example',
+ 'description' => 'Ensure that the simpletest_example content type provided functions properly.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Set up the test environment.
+ *
+ * This method is called once per test method, before the test is executed.
+ * It gives you a chance to control the setup of the test environment.
+ *
+ * If you need a different test environment, then you should create another
+ * test class which overloads DrupalWebTestCase::setUp() differently.
+ *
+ * @see DrupalWebTestCase::setUp()
+ */
+ public function setUp() {
+ // We call parent::setUp() with the list of modules we want to enable.
+ // This can be an array or just a list of arguments.
+ parent::setUp('simpletest_example');
+ // Create and log in our user. The user has the arbitrary privilege
+ // 'extra special edit any simpletest_example' which is provided by
+ // our module to grant access.
+ $this->privilegedUser = $this->drupalCreateUser(array('create simpletest_example content', 'extra special edit any simpletest_example'));
+ $this->drupalLogin($this->privilegedUser);
+ }
+
+ /**
+ * Create a simpletest_example node using the node form.
+ */
+ public function testSimpleTestExampleCreate() {
+ // Create node to edit.
+ $edit = array();
+ $edit['title'] = $this->randomName(8);
+ $edit["body[und][0][value]"] = $this->randomName(16);
+ $this->drupalPost('node/add/simpletest-example', $edit, t('Save'));
+ $this->assertText(t('SimpleTest Example Node Type @title has been created.', array('@title' => $edit['title'])));
+ }
+
+ /**
+ * Create a simpletest_example node and then see if our user can edit it.
+ */
+ public function testSimpleTestExampleEdit() {
+ $settings = array(
+ 'type' => 'simpletest_example',
+ 'title' => $this->randomName(32),
+ 'body' => array(LANGUAGE_NONE => array(array($this->randomName(64)))),
+ );
+ $node = $this->drupalCreateNode($settings);
+
+ // For debugging, we might output the node structure with $this->verbose()
+ // It would only be output if the testing settings had 'verbose' set.
+ $this->verbose('Node created: ' . var_export($node, TRUE));
+
+ // We'll run this test normally, but not on the testbot, as it would
+ // indicate that the examples module was failing tests.
+ if (!$this->runningOnTestbot()) {
+ // The debug() statement will output information into the test results.
+ // It can also be used in Drupal 7 anywhere in code and will come out
+ // as a drupal_set_message().
+ debug('We are not running on the PIFR testing server, so will go ahead and catch the failure.');
+ $this->drupalGet("node/{$node->nid}/edit");
+ // Make sure we don't get a 401 unauthorized response:
+ $this->assertResponse(200, 'User is allowed to edit the content.');
+
+ // Looking for title text in the page to determine whether we were
+ // successful opening edit form.
+ $this->assertText(t("@title", array('@title' => $settings['title'])), "Found title in edit form");
+ }
+ }
+
+ /**
+ * Detect if we're running on PIFR testbot.
+ *
+ * Skip intentional failure in that case. It happens that on the testbot the
+ * site under test is in a directory named 'checkout' or 'site_under_test'.
+ *
+ * @return bool
+ * TRUE if running on testbot.
+ */
+ public function runningOnTestbot() {
+ // @todo: Add this line back once the testbot variable is available.
+ // https://www.drupal.org/node/2565181
+ // return env('DRUPALCI');
+ return TRUE;
+ }
+}
+
+
+/**
+ * Although most core test cases are based on DrupalWebTestCase and are
+ * functional tests (exercising the web UI) we also have DrupalUnitTestCase,
+ * which executes much faster because a Drupal install does not have to be
+ * one. No environment is provided to a test case based on DrupalUnitTestCase;
+ * it must be entirely self-contained.
+ *
+ * @see DrupalUnitTestCase
+ *
+ * @ingroup simpletest_example
+ */
+class SimpleTestUnitTestExampleTestCase extends DrupalUnitTestCase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'SimpleTest Example unit tests',
+ 'description' => 'Test that simpletest_example_empty_mysql_date works properly.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Set up the test environment.
+ *
+ * Note that we use drupal_load() instead of passing our module dependency
+ * to parent::setUp(). That's because we're using DrupalUnitTestCase, and
+ * thus we don't want to install the module, only load it's code.
+ *
+ * Also, DrupalUnitTestCase can't actually install modules. This is by
+ * design.
+ */
+ public function setUp() {
+ drupal_load('module', 'simpletest_example');
+ parent::setUp();
+ }
+
+ /**
+ * Test simpletest_example_empty_mysql_date().
+ *
+ * Note that no environment is provided; we're just testing the correct
+ * behavior of a function when passed specific arguments.
+ */
+ public function testSimpleTestUnitTestExampleFunction() {
+ $result = simpletest_example_empty_mysql_date(NULL);
+ // Note that test assertion messages should never be translated, so
+ // this string is not wrapped in t().
+ $message = 'A NULL value should return TRUE.';
+ $this->assertTrue($result, $message);
+
+ $result = simpletest_example_empty_mysql_date('');
+ $message = 'An empty string should return TRUE.';
+ $this->assertTrue($result, $message);
+
+ $result = simpletest_example_empty_mysql_date('0000-00-00');
+ $message = 'An "empty" MySQL DATE should return TRUE.';
+ $this->assertTrue($result, $message);
+
+ $result = simpletest_example_empty_mysql_date(date('Y-m-d'));
+ $message = 'A valid date should return FALSE.';
+ $this->assertFalse($result, $message);
+ }
+}
+
+/**
+ * SimpleTestExampleMockModuleTestCase allows us to demonstrate how you can
+ * use a mock module to aid in functional testing in Drupal.
+ *
+ * If you have some functionality that's not intrinsic to the code under test,
+ * you can add a special mock module that only gets installed during test
+ * time. This allows you to implement APIs created by your module, or otherwise
+ * exercise the code in question.
+ *
+ * This test case class is very similar to SimpleTestExampleTestCase. The main
+ * difference is that we enable the simpletest_example_test module in the
+ * setUp() method. Then we can test for behaviors provided by that module.
+ *
+ * @see SimpleTestExampleTestCase
+ *
+ * @ingroup simpletest_example
+ */
+class SimpleTestExampleMockModuleTestCase extends DrupalWebTestCase {
+
+ /**
+ * Give display information to the SimpleTest system.
+ *
+ * getInfo() returns a keyed array of information for SimpleTest to show.
+ *
+ * It's a good idea to organize your tests consistently using the 'group'
+ * key.
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'SimpleTest Mock Module Example',
+ 'description' => "Ensure that we can modify SimpleTest Example's content types.",
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Set up the test environment.
+ *
+ * Note that we're enabling both the simpletest_example and
+ * simpletest_example_test modules.
+ */
+ public function setUp() {
+ // We call parent::setUp() with the list of modules we want to enable.
+ parent::setUp('simpletest_example', 'simpletest_example_test');
+ }
+
+ /**
+ * Test modifications made by our mock module.
+ *
+ * We create a simpletest_example node and then see if our submodule
+ * operated on it.
+ */
+ public function testSimpleTestExampleMockModule() {
+ // Create a user.
+ $test_user = $this->drupalCreateUser(array('access content'));
+ // Log them in.
+ $this->drupalLogin($test_user);
+ // Set up some content.
+ $settings = array(
+ 'type' => 'simpletest_example',
+ 'title' => $this->randomName(32),
+ 'body' => array(LANGUAGE_NONE => array(array($this->randomName(64)))),
+ );
+ // Create the content node.
+ $node = $this->drupalCreateNode($settings);
+ // View the node.
+ $this->drupalGet("node/{$node->nid}");
+ // Check that our module did it's thing.
+ $this->assertText(t('The test module did its thing.'), "Found evidence of test module.");
+ }
+
+}
diff --git a/sites/all/modules/examples/simpletest_example/tests/simpletest_example_test.info b/sites/all/modules/examples/simpletest_example/tests/simpletest_example_test.info
new file mode 100644
index 00000000..add42ec2
--- /dev/null
+++ b/sites/all/modules/examples/simpletest_example/tests/simpletest_example_test.info
@@ -0,0 +1,13 @@
+name = "SimpleTest Example Mock Module"
+description = "Mock module for the SimpleTest Example module."
+package = Example modules
+core = 7.x
+hidden = TRUE
+dependencies[] = simpletest_example
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/simpletest_example/tests/simpletest_example_test.module b/sites/all/modules/examples/simpletest_example/tests/simpletest_example_test.module
new file mode 100644
index 00000000..dbaa9864
--- /dev/null
+++ b/sites/all/modules/examples/simpletest_example/tests/simpletest_example_test.module
@@ -0,0 +1,31 @@
+type == 'simpletest_example') {
+ $node->content['simpletest_example_test_section'] = array(
+ '#markup' => t('The test module did its thing.'),
+ '#weight' => -99,
+ );
+ }
+}
diff --git a/sites/all/modules/examples/tabledrag_example/tabledrag_example.info b/sites/all/modules/examples/tabledrag_example/tabledrag_example.info
new file mode 100644
index 00000000..c1c5bd7c
--- /dev/null
+++ b/sites/all/modules/examples/tabledrag_example/tabledrag_example.info
@@ -0,0 +1,12 @@
+name = Tabledrag Example
+description = Demonstrates how to create tabledrag forms.
+package = Example modules
+core = 7.x
+files[] = tabledrag_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/tabledrag_example/tabledrag_example.install b/sites/all/modules/examples/tabledrag_example/tabledrag_example.install
new file mode 100644
index 00000000..cb10d43c
--- /dev/null
+++ b/sites/all/modules/examples/tabledrag_example/tabledrag_example.install
@@ -0,0 +1,151 @@
+ 'Stores some entries for our tabledrag fun.',
+ 'fields' => array(
+ 'id' => array(
+ 'description' => 'The primary identifier for each item',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'description' => 'A name for this item',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'description' => array(
+ 'description' => 'A description for this item',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'itemgroup' => array(
+ 'description' => 'The group this item belongs to',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'weight' => array(
+ 'description' => 'The sortable weight for this item',
+ 'type' => 'int',
+ 'length' => 11,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'pid' => array(
+ 'description' => 'The primary id of the parent for this item',
+ 'type' => 'int',
+ 'length' => 11,
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'depth' => array(
+ 'description' => 'The depth of this item within the tree',
+ 'type' => 'int',
+ 'size' => 'small',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('id'),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ *
+ * This datafills the example item info which will be used in the example.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_install() {
+ // Ensure translations don't break at install time.
+ $t = get_t();
+ // Insert some values into the database.
+ $rows = array(
+ array(
+ 'name' => $t('Item One'),
+ 'description' => $t('The first item'),
+ 'itemgroup' => $t('Group1'),
+ ),
+ array(
+ 'name' => $t('Item Two'),
+ 'description' => $t('The second item'),
+ 'itemgroup' => $t('Group1'),
+ ),
+ array(
+ 'name' => $t('Item Three'),
+ 'description' => $t('The third item'),
+ 'itemgroup' => $t('Group1'),
+ ),
+ array(
+ 'name' => $t('Item Four'),
+ 'description' => $t('The fourth item'),
+ 'itemgroup' => $t('Group2'),
+ ),
+ array(
+ 'name' => $t('Item Five'),
+ 'description' => $t('The fifth item'),
+ 'itemgroup' => $t('Group2'),
+ ),
+ array(
+ 'name' => $t('Item Six'),
+ 'description' => $t('The sixth item'),
+ 'itemgroup' => $t('Group2'),
+ ),
+ array(
+ 'name' => $t('Item Seven'),
+ 'description' => $t('The seventh item'),
+ 'itemgroup' => $t('Group3'),
+ ),
+ array(
+ 'name' => $t('A Root Node'),
+ 'description' => $t('This item cannot be nested under a parent item'),
+ 'itemgroup' => $t('Group3'),
+ ),
+ array(
+ 'name' => $t('A Leaf Item'),
+ 'description' => $t('This item cannot have child items'),
+ 'itemgroup' => $t('Group3'),
+ ),
+ );
+ if (db_table_exists('tabledrag_example')) {
+ foreach ($rows as $row) {
+ db_insert('tabledrag_example')->fields($row)->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * This removes the example data when the module is uninstalled.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_uninstall() {
+ db_drop_table('tabledrag_example');
+}
diff --git a/sites/all/modules/examples/tabledrag_example/tabledrag_example.module b/sites/all/modules/examples/tabledrag_example/tabledrag_example.module
new file mode 100644
index 00000000..e6a8b5b2
--- /dev/null
+++ b/sites/all/modules/examples/tabledrag_example/tabledrag_example.module
@@ -0,0 +1,84 @@
+' . t('The form here is a themed as a table that is sortable using tabledrag handles.') . '';
+ }
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * We'll let drupal_get_form() generate the form page for us, for both of
+ * these menu items.
+ *
+ * @see drupal_get_form()
+ */
+function tabledrag_example_menu() {
+ // Basic example with single-depth sorting.
+ $items['examples/tabledrag_example_simple'] = array(
+ 'title' => 'TableDrag example (simple)',
+ 'description' => 'Show a page with a sortable tabledrag form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('tabledrag_example_simple_form'),
+ 'access callback' => TRUE,
+ 'file' => 'tabledrag_example_simple_form.inc',
+ );
+ // Basic parent/child example.
+ $items['examples/tabledrag_example_parent'] = array(
+ 'title' => 'TableDrag example (parent/child)',
+ 'description' => 'Show a page with a sortable parent/child tabledrag form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('tabledrag_example_parent_form'),
+ 'access callback' => TRUE,
+ 'file' => 'tabledrag_example_parent_form.inc',
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * We need run our forms through custom theme functions in order to build the
+ * table structure which is required by tabledrag.js. Before we can use our
+ * custom theme functions, we need to implement hook_theme in order to register
+ * them with Drupal.
+ *
+ * We are defining our theme hooks with the same name as the form generation
+ * function so that Drupal automatically calls our theming function when the
+ * form is displayed.
+ */
+function tabledrag_example_theme() {
+ return array(
+ // Theme function for the 'simple' example.
+ 'tabledrag_example_simple_form' => array(
+ 'render element' => 'form',
+ 'file' => 'tabledrag_example_simple_form.inc',
+ ),
+ // Theme function for the 'parent/child' example.
+ 'tabledrag_example_parent_form' => array(
+ 'render element' => 'form',
+ 'file' => 'tabledrag_example_parent_form.inc',
+ ),
+ );
+}
+/**
+ * @} End of "defgroup tabledrag_example".
+ */
diff --git a/sites/all/modules/examples/tabledrag_example/tabledrag_example.test b/sites/all/modules/examples/tabledrag_example/tabledrag_example.test
new file mode 100644
index 00000000..a7ccfec2
--- /dev/null
+++ b/sites/all/modules/examples/tabledrag_example/tabledrag_example.test
@@ -0,0 +1,46 @@
+ 'Tabledrag Example',
+ 'description' => 'Functional tests for the Tabledrag Example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('tabledrag_example');
+ }
+
+ /**
+ * Tests the menu paths defined in tabledrag_example module.
+ */
+ public function testTabledragExampleMenus() {
+ $paths = array(
+ 'examples/tabledrag_example_simple',
+ 'examples/tabledrag_example_parent',
+ );
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertResponse(200, '200 response for path: ' . $path);
+ }
+ }
+}
diff --git a/sites/all/modules/examples/tabledrag_example/tabledrag_example_parent_form.inc b/sites/all/modules/examples/tabledrag_example/tabledrag_example_parent_form.inc
new file mode 100644
index 00000000..2b1e6ee4
--- /dev/null
+++ b/sites/all/modules/examples/tabledrag_example/tabledrag_example_parent_form.inc
@@ -0,0 +1,333 @@
+id] = array(
+
+ // We'll use a form element of type '#markup' to display the item name.
+ 'name' => array(
+ '#markup' => $item->name,
+ ),
+
+ // We'll use a form element of type '#textfield' to display the item
+ // description, to demonstrate that form elements can be included in the
+ // table. We limit the input to 255 characters, which is the limit we
+ // set on the database field.
+ 'description' => array(
+ '#type' => 'textfield',
+ '#default_value' => $item->description,
+ '#size' => 20,
+ '#maxlength' => 255,
+ ),
+
+ // For parent/child relationships, we also need to add form items to
+ // store the current item's unique id and parent item's unique id.
+ //
+ // We would normally use a hidden element for this, but for this example
+ // we'll use a disabled textfield element called 'id' so that we can
+ // display the current item's id in the table.
+ //
+ // Because tabledrag modifies the #value of this element, we use
+ // '#default_value' instead of '#value' when defining a hidden element.
+ // Also, because tabledrag modifies '#value', we cannot use a markup
+ // element, which does not support the '#value' property. (Markup
+ // elements use the '#markup' property instead.)
+ 'id' => array(
+ // '#type' => 'hidden',
+ // '#default_value' => $item->id,
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#default_value' => $item->id,
+ '#disabled' => TRUE,
+ ),
+
+ // The same information holds true for the parent id field as for the
+ // item id field, described above.
+ 'pid' => array(
+ // '#type' => 'hidden',
+ // '#default_value' => $item->pid,
+ '#type' => 'textfield',
+ '#size' => 3,
+ '#default_value' => $item->pid,
+ ),
+
+ // The 'weight' field will be manipulated as we move the items around in
+ // the table using the tabledrag activity. We use the 'weight' element
+ // defined in Drupal's Form API.
+ 'weight' => array(
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $item->weight,
+ '#delta' => 10,
+ '#title_display' => 'invisible',
+ ),
+
+ // We'll use a hidden form element to pass the current 'depth' of each
+ // item within our parent/child tree structure to the theme function.
+ // This will be used to calculate the initial amount of indentation to
+ // add before displaying any child item rows.
+ 'depth' => array(
+ '#type' => 'hidden',
+ '#value' => $item->depth,
+ ),
+ );
+ }
+
+ // Now we add our submit button, for submitting the form results.
+ //
+ // The 'actions' wrapper used here isn't strictly necessary for tabledrag,
+ // but is included as a Form API recommended practice.
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save Changes'));
+ return $form;
+}
+
+/**
+ * Theme callback for the tabledrag_example_parent_form form.
+ *
+ * The theme callback will format the $form data structure into a table and
+ * add our tabledrag functionality. (Note that drupal_add_tabledrag should be
+ * called from the theme layer, and not from a form declaration. This helps
+ * keep template files clean and readable, and prevents tabledrag.js from
+ * being added twice accidently.
+ *
+ * @ingroup tabledrag_example
+ */
+function theme_tabledrag_example_parent_form($variables) {
+ $form = $variables['form'];
+
+ // Initialize the variable which will store our table rows.
+ $rows = array();
+
+ // Iterate over each element in our $form['example_items'] array.
+ foreach (element_children($form['example_items']) as $id) {
+
+ // Before we add our 'weight' column to the row, we need to give the
+ // element a custom class so that it can be identified in the
+ // drupal_add_tabledrag call.
+ //
+ // This could also have been done during the form declaration by adding
+ // @code
+ // '#attributes' => array('class' => 'example-item-weight'),
+ // @endcode
+ // directly to the 'weight' element in tabledrag_example_simple_form().
+ $form['example_items'][$id]['weight']['#attributes']['class'] = array('example-item-weight');
+
+ // In the parent/child example, we must also set this same custom class on
+ // our id and parent_id columns (which could also have been done within
+ // the form declaration, as above).
+ $form['example_items'][$id]['id']['#attributes']['class'] = array('example-item-id');
+ $form['example_items'][$id]['pid']['#attributes']['class'] = array('example-item-pid');
+
+ // To support the tabledrag behaviour, we need to assign each row of the
+ // table a class attribute of 'draggable'. This will add the 'draggable'
+ // class to the
element for that row when the final table is
+ // rendered.
+ $class = array('draggable');
+
+ // We can add the 'tabledrag-root' class to a row in order to indicate
+ // that the row may not be nested under a parent row. In our sample data
+ // for this example, the description for the item with id '8' flags it as
+ // a 'root' item which should not be nested.
+ if ($id == '8') {
+ $class[] = 'tabledrag-root';
+ }
+
+ // We can add the 'tabledrag-leaf' class to a row in order to indicate
+ // that the row may not contain child rows. In our sample data for this
+ // example, the description for the item with id '9' flags it as a 'leaf'
+ // item which can not contain child items.
+ if ($id == '9') {
+ $class[] = 'tabledrag-leaf';
+ }
+
+ // If this is a child element, we need to add some indentation to the row,
+ // so that it appears nested under its parent. Our $depth parameter was
+ // calculated while building the tree in tabledrag_example_parent_get_data
+ $indent = theme('indentation', array('size' => $form['example_items'][$id]['depth']['#value']));
+ unset($form['example_items'][$id]['depth']);
+
+ // We are now ready to add each element of our $form data to the $rows
+ // array, so that they end up as individual table cells when rendered
+ // in the final table. We run each element through the drupal_render()
+ // function to generate the final html markup for that element.
+ $rows[] = array(
+ 'data' => array(
+ // Add our 'name' column, being sure to include our indentation.
+ $indent . drupal_render($form['example_items'][$id]['name']),
+ // Add our 'description' column.
+ drupal_render($form['example_items'][$id]['description']),
+ // Add our 'weight' column.
+ drupal_render($form['example_items'][$id]['weight']),
+ // Add our hidden 'id' column.
+ drupal_render($form['example_items'][$id]['id']),
+ // Add our hidden 'parent id' column.
+ drupal_render($form['example_items'][$id]['pid']),
+ ),
+ // To support the tabledrag behaviour, we need to assign each row of the
+ // table a class attribute of 'draggable'. This will add the 'draggable'
+ // class to the
element for that row when the final table is
+ // rendered.
+ 'class' => $class,
+ );
+ }
+
+ // We now define the table header values. Ensure that the 'header' count
+ // matches the final column count for your table.
+ //
+ // Normally, we would hide the headers on our hidden columns, but we are
+ // leaving them visible in this example.
+ // $header = array(t('Name'), t('Description'), '', '', '');
+ $header = array(t('Name'), t('Description'), t('Weight'), t('ID'), t('PID'));
+
+ // We also need to pass the drupal_add_tabledrag() function an id which will
+ // be used to identify the
element containing our tabledrag form.
+ // Because an element's 'id' should be unique on a page, make sure the value
+ // you select is NOT the same as the form ID used in your form declaration.
+ $table_id = 'example-items-table';
+
+ // We can render our tabledrag table for output.
+ $output = theme('table', array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => $table_id),
+ ));
+
+ // And then render any remaining form elements (such as our submit button).
+ $output .= drupal_render_children($form);
+
+ // We now call the drupal_add_tabledrag() function in order to add the
+ // tabledrag.js goodness onto our page.
+ //
+ // For our parent/child tree table, we need to pass it:
+ // - the $table_id of our
element (example-items-table),
+ // - the $action to be performed on our form items ('match'),
+ // - a string describing where $action should be applied ('parent'),
+ // - the $group value (pid column) class name ('example-item-pid'),
+ // - the $subgroup value (pid column) class name ('example-item-pid'),
+ // - the $source value (id column) class name ('example-item-id'),
+ // - an optional $hidden flag identifying if the columns should be hidden,
+ // - an optional $limit parameter to control the max parenting depth.
+ drupal_add_tabledrag($table_id, 'match', 'parent', 'example-item-pid', 'example-item-pid', 'example-item-id', FALSE);
+
+ // Because we also want to sort in addition to providing parenting, we call
+ // the drupal_add_tabledrag function again, instructing it to update the
+ // weight field as items at the same level are re-ordered.
+ drupal_add_tabledrag($table_id, 'order', 'sibling', 'example-item-weight', NULL, NULL, FALSE);
+
+ return $output;
+}
+
+/**
+ * Submit callback for the tabledrag_example_parent_form form.
+ *
+ * Updates the 'weight' column for each element in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_parent_form_submit($form, &$form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ foreach ($form_state['values']['example_items'] as $id => $item) {
+ db_query(
+ "UPDATE {tabledrag_example} SET weight = :weight, pid = :pid WHERE id = :id",
+ array(':weight' => $item['weight'], ':pid' => $item['pid'], ':id' => $id)
+ );
+ }
+}
+
+/**
+ * Retrives the tree structure from database, and sorts by parent/child/weight.
+ *
+ * The sorting should result in children items immediately following their
+ * parent items, with items at the same level of the hierarchy sorted by
+ * weight.
+ *
+ * The approach used here may be considered too database-intensive.
+ * Optimization of the approach is left as an exercise for the reader. :)
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_parent_get_data() {
+ // Get all 'root node' items (items with no parents), sorted by weight.
+ $rootnodes = db_query('SELECT id, name, description, weight, pid
+ FROM {tabledrag_example}
+ WHERE (pid = 0)
+ ORDER BY weight ASC');
+ // Initialize a variable to store our ordered tree structure.
+ $itemtree = array();
+ // Depth will be incremented in our _get_tree() function for the first
+ // parent item, so we start it at -1.
+ $depth = -1;
+ // Loop through the root nodes, and add their trees to the array.
+ foreach ($rootnodes as $parent) {
+ tabledrag_example_get_tree($parent, $itemtree, $depth);
+ }
+ return $itemtree;
+}
+
+/**
+ * Recursively adds to the $itemtree array, ordered by parent/child/weight.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_get_tree($parentitem, &$itemtree = array(), &$depth = 0) {
+ // Increase our $depth value by one.
+ $depth++;
+ // Set the current tree 'depth' for this item, used to calculate indentation.
+ $parentitem->depth = $depth;
+ // Add the parent item to the tree.
+ $itemtree[$parentitem->id] = $parentitem;
+ // Retrieve each of the children belonging to this parent.
+ $children = db_query('SELECT id, name, description, weight, pid
+ FROM {tabledrag_example}
+ WHERE (pid = :pid)
+ ORDER BY weight ASC',
+ array(':pid' => $parentitem->id));
+ foreach ($children as $child) {
+ // Make sure this child does not already exist in the tree, to avoid loops.
+ if (!in_array($child->id, array_keys($itemtree))) {
+ // Add this child's tree to the $itemtree array.
+ tabledrag_example_get_tree($child, $itemtree, $depth);
+ }
+ }
+ // Finished processing this tree branch. Decrease our $depth value by one
+ // to represent moving to the next branch.
+ $depth--;
+}
diff --git a/sites/all/modules/examples/tabledrag_example/tabledrag_example_simple_form.inc b/sites/all/modules/examples/tabledrag_example/tabledrag_example_simple_form.inc
new file mode 100644
index 00000000..582f6b16
--- /dev/null
+++ b/sites/all/modules/examples/tabledrag_example/tabledrag_example_simple_form.inc
@@ -0,0 +1,177 @@
+id] = array(
+
+ // We'll use a form element of type '#markup' to display the item name.
+ 'name' => array(
+ '#markup' => check_plain($item->name),
+ ),
+
+ // We'll use a form element of type '#textfield' to display the item
+ // description, which will allow the value to be changed via the form.
+ // We limit the input to 255 characters, which is the limit we set on
+ // the database field.
+ 'description' => array(
+ '#type' => 'textfield',
+ '#default_value' => check_plain($item->description),
+ '#size' => 20,
+ '#maxlength' => 255,
+ ),
+
+ // The 'weight' field will be manipulated as we move the items around in
+ // the table using the tabledrag activity. We use the 'weight' element
+ // defined in Drupal's Form API.
+ 'weight' => array(
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $item->weight,
+ '#delta' => 10,
+ '#title_display' => 'invisible',
+ ),
+ );
+ }
+
+ // Now we add our submit button, for submitting the form results.
+ //
+ // The 'actions' wrapper used here isn't strictly necessary for tabledrag,
+ // but is included as a Form API recommended practice.
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save Changes'));
+ return $form;
+}
+
+/**
+ * Theme callback for the tabledrag_example_simple_form form.
+ *
+ * The theme callback will format the $form data structure into a table and
+ * add our tabledrag functionality. (Note that drupal_add_tabledrag should be
+ * called from the theme layer, and not from a form declaration. This helps
+ * keep template files clean and readable, and prevents tabledrag.js from
+ * being added twice accidently.
+ *
+ * @return array
+ * The rendered tabledrag form
+ *
+ * @ingroup tabledrag_example
+ */
+function theme_tabledrag_example_simple_form($variables) {
+ $form = $variables['form'];
+
+ // Initialize the variable which will store our table rows.
+ $rows = array();
+
+ // Iterate over each element in our $form['example_items'] array.
+ foreach (element_children($form['example_items']) as $id) {
+
+ // Before we add our 'weight' column to the row, we need to give the
+ // element a custom class so that it can be identified in the
+ // drupal_add_tabledrag call.
+ //
+ // This could also have been done during the form declaration by adding
+ // '#attributes' => array('class' => 'example-item-weight'),
+ // directy to the 'weight' element in tabledrag_example_simple_form().
+ $form['example_items'][$id]['weight']['#attributes']['class'] = array('example-item-weight');
+
+ // We are now ready to add each element of our $form data to the $rows
+ // array, so that they end up as individual table cells when rendered
+ // in the final table. We run each element through the drupal_render()
+ // function to generate the final html markup for that element.
+ $rows[] = array(
+ 'data' => array(
+ // Add our 'name' column.
+ drupal_render($form['example_items'][$id]['name']),
+ // Add our 'description' column.
+ drupal_render($form['example_items'][$id]['description']),
+ // Add our 'weight' column.
+ drupal_render($form['example_items'][$id]['weight']),
+ ),
+ // To support the tabledrag behaviour, we need to assign each row of the
+ // table a class attribute of 'draggable'. This will add the 'draggable'
+ // class to the
element for that row when the final table is
+ // rendered.
+ 'class' => array('draggable'),
+ );
+ }
+
+ // We now define the table header values. Ensure that the 'header' count
+ // matches the final column count for your table.
+ $header = array(t('Name'), t('Description'), t('Weight'));
+
+ // We also need to pass the drupal_add_tabledrag() function an id which will
+ // be used to identify the
element containing our tabledrag form.
+ // Because an element's 'id' should be unique on a page, make sure the value
+ // you select is NOT the same as the form ID used in your form declaration.
+ $table_id = 'example-items-table';
+
+ // We can render our tabledrag table for output.
+ $output = theme('table', array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => $table_id),
+ ));
+
+ // And then render any remaining form elements (such as our submit button).
+ $output .= drupal_render_children($form);
+
+ // We now call the drupal_add_tabledrag() function in order to add the
+ // tabledrag.js goodness onto our page.
+ //
+ // For a basic sortable table, we need to pass it:
+ // - the $table_id of our
element,
+ // - the $action to be performed on our form items ('order'),
+ // - a string describing where $action should be applied ('siblings'),
+ // - and the class of the element containing our 'weight' element.
+ drupal_add_tabledrag($table_id, 'order', 'sibling', 'example-item-weight');
+
+ return $output;
+}
+
+/**
+ * Submit callback for the tabledrag_example_simple_form form.
+ *
+ * Updates the 'weight' column for each element in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_simple_form_submit($form, &$form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ foreach ($form_state['values']['example_items'] as $id => $item) {
+ db_query(
+ "UPDATE {tabledrag_example} SET weight = :weight WHERE id = :id",
+ array(':weight' => $item['weight'], ':id' => $id)
+ );
+ }
+}
diff --git a/sites/all/modules/examples/tablesort_example/tablesort_example.info b/sites/all/modules/examples/tablesort_example/tablesort_example.info
new file mode 100644
index 00000000..4c42c2cf
--- /dev/null
+++ b/sites/all/modules/examples/tablesort_example/tablesort_example.info
@@ -0,0 +1,12 @@
+name = Table Sort example
+description = Demonstrates how to create sortable output in a table.
+package = Example modules
+core = 7.x
+files[] = tablesort_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/tablesort_example/tablesort_example.install b/sites/all/modules/examples/tablesort_example/tablesort_example.install
new file mode 100644
index 00000000..11a89393
--- /dev/null
+++ b/sites/all/modules/examples/tablesort_example/tablesort_example.install
@@ -0,0 +1,78 @@
+ 1, 'alpha' => 'e', 'random' => '912cv21'),
+ array('numbers' => 2, 'alpha' => 'a', 'random' => '0kuykuh'),
+ array('numbers' => 3, 'alpha' => 'm', 'random' => 'fuye8734h'),
+ array('numbers' => 4, 'alpha' => 'w', 'random' => '80jsv772'),
+ array('numbers' => 5, 'alpha' => 'o', 'random' => 'd82sf-csj'),
+ array('numbers' => 6, 'alpha' => 's', 'random' => 'au832'),
+ array('numbers' => 7, 'alpha' => 'e', 'random' => 't982hkv'),
+ );
+
+ if (db_table_exists('tablesort_example')) {
+ foreach ($rows as $row) {
+ db_insert('tablesort_example')->fields($row)->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * It's good to clean up after ourselves
+ *
+ * @ingroup tablesort_example
+ */
+function tablesort_example_uninstall() {
+ db_drop_table('tablesort_example');
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * @ingroup tablesort_example
+ */
+function tablesort_example_schema() {
+ $schema['tablesort_example'] = array(
+ 'description' => 'Stores some values for sorting fun.',
+ 'fields' => array(
+ 'numbers' => array(
+ 'description' => 'This column simply holds numbers values',
+ 'type' => 'varchar',
+ 'length' => 2,
+ 'not null' => TRUE,
+ ),
+ 'alpha' => array(
+ 'description' => 'This column simply holds alpha values',
+ 'type' => 'varchar',
+ 'length' => 2,
+ 'not null' => TRUE,
+ ),
+ 'random' => array(
+ 'description' => 'This column simply holds random values',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array('numbers'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/examples/tablesort_example/tablesort_example.module b/sites/all/modules/examples/tablesort_example/tablesort_example.module
new file mode 100644
index 00000000..85f10c65
--- /dev/null
+++ b/sites/all/modules/examples/tablesort_example/tablesort_example.module
@@ -0,0 +1,93 @@
+' . t('The layout here is a themed as a table that is sortable by clicking the header name.') . '';
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function tablesort_example_menu() {
+ $items['examples/tablesort_example'] = array(
+ 'title' => 'TableSort example',
+ 'description' => 'Show a page with a sortable table',
+ 'page callback' => 'tablesort_example_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Build the table render array.
+ *
+ * @return array
+ * A render array set for theming by theme_table().
+ */
+function tablesort_example_page() {
+ // We are going to output the results in a table with a nice header.
+ $header = array(
+ // The header gives the table the information it needs in order to make
+ // the query calls for ordering. TableSort uses the field information
+ // to know what database column to sort by.
+ array('data' => t('Numbers'), 'field' => 't.numbers'),
+ array('data' => t('Letters'), 'field' => 't.alpha'),
+ array('data' => t('Mixture'), 'field' => 't.random'),
+ );
+
+ // Using the TableSort Extender is what tells the query object that we are
+ // sorting.
+ $query = db_select('tablesort_example', 't')
+ ->extend('TableSort');
+ $query->fields('t');
+
+ // Don't forget to tell the query object how to find the header information.
+ $result = $query
+ ->orderByHeader($header)
+ ->execute();
+
+ $rows = array();
+ foreach ($result as $row) {
+ // Normally we would add some nice formatting to our rows
+ // but for our purpose we are simply going to add our row
+ // to the array.
+ $rows[] = array('data' => (array) $row);
+ }
+
+ // Build the table for the nice output.
+ $build['tablesort_table'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ );
+
+ return $build;
+}
+
+/**
+ * @} End of "defgroup tablesort_example".
+ */
diff --git a/sites/all/modules/examples/tablesort_example/tablesort_example.test b/sites/all/modules/examples/tablesort_example/tablesort_example.test
new file mode 100644
index 00000000..5462c757
--- /dev/null
+++ b/sites/all/modules/examples/tablesort_example/tablesort_example.test
@@ -0,0 +1,66 @@
+ 'TableSort Example',
+ 'description' => 'Verify the tablesort functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('tablesort_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testTableSortPage() {
+ // No need to login for this test.
+ $this->drupalGet('examples/tablesort_example', array('query' => array('sort' => 'desc', 'order' => 'Numbers')));
+ $this->assertRaw('
+
7
e
t982hkv
', 'Ordered by Number descending');
+
+ $this->drupalGet('examples/tablesort_example', array('query' => array('sort' => 'asc', 'order' => 'Numbers')));
+ $this->assertRaw('
+
1
e
912cv21
', 'Ordered by Number ascending');
+
+ // Sort by Letters.
+ $this->drupalGet('examples/tablesort_example', array('query' => array('sort' => 'desc', 'order' => 'Letters')));
+ $this->assertRaw('
+
', 'Ordered by Mixture ascending');
+ }
+
+}
diff --git a/sites/all/modules/examples/theming_example/theming-example-text-form.tpl.php b/sites/all/modules/examples/theming_example/theming-example-text-form.tpl.php
new file mode 100644
index 00000000..e75ae89f
--- /dev/null
+++ b/sites/all/modules/examples/theming_example/theming-example-text-form.tpl.php
@@ -0,0 +1,29 @@
+
+ *
+ * The following snippet will print the contents of the $text_form_content
+ * array, hidden in the source of the page, for you to discover the individual
+ * element names.
+ *
+ * '; ?>
+ */
+?>
+
+
+
+
+
diff --git a/sites/all/modules/examples/theming_example/theming_example.css b/sites/all/modules/examples/theming_example/theming_example.css
new file mode 100644
index 00000000..a24c698d
--- /dev/null
+++ b/sites/all/modules/examples/theming_example/theming_example.css
@@ -0,0 +1,11 @@
+/*
+ * style the list
+ * for OL you can have
+ * decimal | lower-roman | upper-roman | lower-alpha | upper-alpha
+ * for UL you can have
+ * disc | circle | square or an image eg url(x.png)
+ * you can also have 'none'
+ */
+ol.theming-example-list {
+ list-style-type: upper-alpha;
+}
diff --git a/sites/all/modules/examples/theming_example/theming_example.info b/sites/all/modules/examples/theming_example/theming_example.info
new file mode 100644
index 00000000..4edfcf81
--- /dev/null
+++ b/sites/all/modules/examples/theming_example/theming_example.info
@@ -0,0 +1,12 @@
+name = Theming example
+description = An example module showing how to use theming.
+package = Example modules
+core = 7.x
+files[] = theming_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/theming_example/theming_example.module b/sites/all/modules/examples/theming_example/theming_example.module
new file mode 100644
index 00000000..b4bc5d28
--- /dev/null
+++ b/sites/all/modules/examples/theming_example/theming_example.module
@@ -0,0 +1,385 @@
+ 'Theming Example',
+ 'description' => 'Some theming examples.',
+ 'page callback' => 'theming_example_page',
+ 'access callback' => TRUE,
+ 'access arguments' => array('access content'),
+ );
+ $items['examples/theming_example/theming_example_list_page'] = array(
+ 'title' => 'Theming a list',
+ 'page callback' => 'theming_example_list_page',
+ 'access arguments' => array('access content'),
+ 'weight' => 1,
+ );
+ $items['examples/theming_example/theming_example_select_form'] = array(
+ 'title' => 'Theming a form (select form)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('theming_example_select_form'),
+ 'access arguments' => array('access content'),
+ 'weight' => 2,
+ );
+ $items['examples/theming_example/theming_example_text_form'] = array(
+ 'title' => 'Theming a form (text form)',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('theming_example_text_form'),
+ 'access arguments' => array('access content'),
+ 'weight' => 3,
+ );
+
+ return $items;
+
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * Defines the theming capabilities provided by this module.
+ */
+function theming_example_theme($existing, $type, $theme, $path) {
+ return array(
+ 'theming_example_content_array' => array(
+ // We use 'render element' when the item to be passed is a self-describing
+ // render array (it will have #theme_wrappers)
+ 'render element' => 'element',
+ ),
+ 'theming_example_list' => array(
+ // We use 'variables' when the item to be passed is an array whose
+ // structure must be described here.
+ 'variables' => array(
+ 'title' => NULL,
+ 'items' => NULL,
+ ),
+ ),
+ 'theming_example_select_form' => array(
+ 'render element' => 'form',
+ ),
+ 'theming_example_text_form' => array(
+ 'render element' => 'form',
+ // In this one the rendering will be done by a template file
+ // (theming-example-text-form.tpl.php) instead of being rendered by a
+ // function. Note the use of dashes to separate words in place of
+ // underscores. The template file's extension is also left out so that
+ // it may be determined automatically depending on the template engine
+ // the site is using.
+ 'template' => 'theming-example-text-form',
+ ),
+ );
+}
+/**
+ * Initial landing page explaining the use of the module.
+ *
+ * We create a render array and specify the theme to be used through the use
+ * of #theme_wrappers. With all output, we aim to leave the content as a
+ * render array just as long as possible, so that other modules (or the theme)
+ * can alter it.
+ *
+ * @see render_example.module
+ * @see form_example_elements.inc
+ */
+function theming_example_page() {
+ $content[]['#markup'] = t('Some examples of pages and forms that are run through theme functions.');
+ $content[]['#markup'] = l(t('Simple page with a list'), 'examples/theming_example/theming_example_list_page');
+ $content[]['#markup'] = l(t('Simple form 1'), 'examples/theming_example/theming_example_select_form');
+ $content[]['#markup'] = l(t('Simple form 2'), 'examples/theming_example/theming_example_text_form');
+ $content['#theme_wrappers'] = array('theming_example_content_array');
+ return $content;
+}
+
+/**
+ * The list page callback.
+ *
+ * An example page where the output is supplied as an array which is themed
+ * into a list and styled with css.
+ *
+ * In this case we'll use the core-provided theme_item_list as a #theme_wrapper.
+ * Any theme need only override theme_item_list to change the behavior.
+ */
+function theming_example_list_page() {
+ $items = array(
+ t('First item'),
+ t('Second item'),
+ t('Third item'),
+ t('Fourth item'),
+ );
+
+ // First we'll create a render array that simply uses theme_item_list.
+ $title = t("A list returned to be rendered using theme('item_list')");
+ $build['render_version'] = array(
+ // We use #theme here instead of #theme_wrappers because theme_item_list()
+ // is the classic type of theme function that does not just assume a
+ // render array, but instead has its own properties (#type, #title, #items).
+ '#theme' => 'item_list',
+ // '#type' => 'ul', // The default type is 'ul'
+ // We can easily make sure that a css or js file is present using #attached.
+ '#attached' => array('css' => array(drupal_get_path('module', 'theming_example') . '/theming_example.css')),
+ '#title' => $title,
+ '#items' => $items,
+ '#attributes' => array('class' => array('render-version-list')),
+ );
+
+ // Now we'll create a render array which uses our own list formatter,
+ // theme('theming_example_list').
+ $title = t("The same list rendered by theme('theming_example_list')");
+ $build['our_theme_function'] = array(
+ '#theme' => 'theming_example_list',
+ '#attached' => array('css' => array(drupal_get_path('module', 'theming_example') . '/theming_example.css')),
+ '#title' => $title,
+ '#items' => $items,
+ );
+ return $build;
+}
+
+
+/**
+ * A simple form that displays a select box and submit button.
+ *
+ * This form will be be themed by the 'theming_example_select_form' theme
+ * handler.
+ */
+function theming_example_select_form($form, &$form_state) {
+ $options = array(
+ 'newest_first' => t('Newest first'),
+ 'newest_last' => t('Newest last'),
+ 'edited_first' => t('Edited first'),
+ 'edited_last' => t('Edited last'),
+ 'by_name' => t('By name'),
+ );
+ $form['choice'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => t('Choose which ordering you want'),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Go'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler for the select form.
+ *
+ * @param array $form
+ * Form API form array.
+ * @param array $form_state
+ * Form API form state array.
+ */
+function theming_example_select_form_submit($form, &$form_state) {
+ drupal_set_message(t('You chose %input', array('%input' => $form_state['values']['choice'])));
+}
+
+/**
+ * A simple form that displays a textfield and submit button.
+ *
+ * This form will be rendered by theme('form') (theme_form() by default)
+ * because we do not provide a theme function for it here.
+ */
+function theming_example_text_form($form, &$form_state) {
+ $form['text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Please input something!'),
+ '#required' => TRUE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Go'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler for the text form.
+ *
+ * @param array $form
+ * Form API form array.
+ * @param array $form_state
+ * Form API form state array.
+ */
+function theming_example_text_form_submit($form, &$form_state) {
+ drupal_set_message(t('You entered %input', array('%input' => $form_state['values']['text'])));
+}
+
+
+/**
+ * Theme a simple content array.
+ *
+ * This theme function uses the newer recommended format where a single
+ * render array is provided to the theme function.
+ */
+function theme_theming_example_content_array($variables) {
+ $element = $variables['element'];
+ $output = '';
+ foreach (element_children($element) as $count) {
+ if (!$count) {
+ // The first paragraph is bolded.
+ $output .= '
' . $element[$count]['#children'] . '
';
+ }
+ else {
+ // Following paragraphs are just output as routine paragraphs.
+ $output .= '
' . $element[$count]['#children'] . '
';
+ }
+ }
+ return $output;
+}
+
+/**
+ * Theming a simple list.
+ *
+ * This is just a simple wrapper around theme('item_list') but it's worth
+ * showing how a custom theme function can be implemented.
+ *
+ * @see theme_item_list()
+ */
+function theme_theming_example_list($variables) {
+ $title = $variables['title'];
+ $items = $variables['items'];
+
+ // Add the title to the list theme and
+ // state the list type. This defaults to 'ul'.
+ // Add a css class so that you can modify the list styling.
+ // We'll just call theme('item_list') to render.
+ $variables = array(
+ 'items' => $items,
+ 'title' => $title,
+ 'type' => 'ol',
+ 'attributes' => array('class' => 'theming-example-list'),
+ );
+ $output = theme('item_list', $variables);
+ return $output;
+}
+
+/**
+ * Theming a simple form.
+ *
+ * Since our form is named theming_example_select_form(), the default
+ * #theme function applied to is will be 'theming_example_select_form'
+ * if it exists. The form could also have specified a different
+ * #theme.
+ *
+ * Here we collect the title, theme it manually and
+ * empty the form title. We also wrap the form in a div.
+ */
+function theme_theming_example_select_form($variables) {
+ $form = $variables['form'];
+ $title = $form['choice']['#title'];
+ $form['choice']['#title'] = '';
+ $output = '' . $title . '';
+ $form['choice']['#prefix'] = '
';
+ $form['submit']['#suffix'] = '
';
+ $output .= drupal_render_children($form);
+ return $output;
+}
+
+/**
+ * Implements template_preprocess().
+ *
+ * We prepare variables for use inside the theming-example-text-form.tpl.php
+ * template file.
+ *
+ * In this example, we create a couple new variables, 'text_form' and
+ * 'text_form_content', that clean up the form output. Drupal will turn the
+ * array keys in the $variables array into variables for use in the template.
+ *
+ * So $variables['text_form'] becomes available as $text_form in the template.
+ *
+ * @see theming-example-text-form.tpl.php
+ */
+function template_preprocess_theming_example_text_form(&$variables) {
+ $variables['text_form_content'] = array();
+ $text_form_hidden = array();
+
+ // Each form element is rendered and saved as a key in $text_form_content, to
+ // give the themer the power to print each element independently in the
+ // template file. Hidden form elements have no value in the theme, so they
+ // are grouped into a single element.
+ foreach (element_children($variables['form']) as $key) {
+ $type = $variables['form'][$key]['#type'];
+ if ($type == 'hidden' || $type == 'token') {
+ $text_form_hidden[] = drupal_render($variables['form'][$key]);
+ }
+ else {
+ $variables['text_form_content'][$key] = drupal_render($variables['form'][$key]);
+ }
+ }
+ $variables['text_form_content']['hidden'] = implode($text_form_hidden);
+
+ // The entire form is then saved in the $text_form variable, to make it easy
+ // for the themer to print the whole form.
+ $variables['text_form'] = implode($variables['text_form_content']);
+}
+/**
+ * @} End of "defgroup theming_example".
+ */
diff --git a/sites/all/modules/examples/theming_example/theming_example.test b/sites/all/modules/examples/theming_example/theming_example.test
new file mode 100644
index 00000000..acf7445b
--- /dev/null
+++ b/sites/all/modules/examples/theming_example/theming_example.test
@@ -0,0 +1,66 @@
+ 'Theming Example',
+ 'description' => 'Verify theming example functionality',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ // Enable the module.
+ parent::setUp('theming_example');
+ }
+
+ /**
+ * Verify the functionality of the example module.
+ */
+ public function testThemingPage() {
+ // No need to login for this test.
+ // Check that the main page has been themed (first line with ) and has
+ // content.
+ $this->drupalGet('examples/theming_example');
+ $this->assertRaw('Some examples of pages');
+ $this->assertRaw('examples/theming_example/theming_example_select_form">Simple form 1');
+
+ // Visit the list demonstration page and check that css gets loaded
+ // and do some spot checks on how the two lists were themed.
+ $this->drupalGet('examples/theming_example/theming_example_list_page');
+ $this->assertPattern('/@import.*theming_example.css/');
+ $first_ul = $this->xpath('//ul[contains(@class,"render-version-list")]');
+ $this->assertTrue($first_ul[0]->li[0] == 'First item');
+ $second_ul = $this->xpath('//ol[contains(@class,"theming-example-list")]');
+ $this->assertTrue($second_ul[0]->li[1] == 'Second item');
+
+ // Visit the select form page to do spot checks.
+ $this->drupalGet('examples/theming_example/theming_example_select_form');
+ // We did explicit theming to accomplish the below...
+ $this->assertRaw('Choose which ordering you want');
+ $this->assertRaw('
');
+ $this->assertNoPattern('/@import.*theming_example.css/');
+
+ // Visit the text form page and do spot checks.
+ $this->drupalGet('examples/theming_example/theming_example_text_form');
+ $this->assertText('Please input something!');
+ // If it were themed normally there would be a div wrapper in our pattern.
+ $this->assertPattern('%
\s*= 7.40)
+files[] = token_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/token_example/token_example.module b/sites/all/modules/examples/token_example/token_example.module
new file mode 100644
index 00000000..a7dff547
--- /dev/null
+++ b/sites/all/modules/examples/token_example/token_example.module
@@ -0,0 +1,228 @@
+ 'Token example',
+ 'description' => 'Test replacement tokens in real time.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('token_example_example_form'),
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ *
+ * @todo Remove this when the testbot can properly pick up dependencies
+ * for contrib modules.
+ */
+function token_example_entity_info_alter(&$info) {
+ if (isset($info['taxonomy_term'])) {
+ $info['taxonomy_term']['token type'] = 'term';
+ }
+ if (isset($info['taxonomy_vocabulary'])) {
+ $info['taxonomy_vocabulary']['token type'] = 'vocabulary';
+ }
+}
+
+/**
+ * Form builder; display lists of supported token entities and text to tokenize.
+ */
+function token_example_example_form($form, &$form_state) {
+ $entities = entity_get_info();
+ $token_types = array();
+
+ // Scan through the list of entities for supported token entities.
+ foreach ($entities as $entity => $info) {
+ $object_callback = "_token_example_get_{$entity}";
+ if (function_exists($object_callback) && $objects = $object_callback()) {
+ $form[$entity] = array(
+ '#type' => 'select',
+ '#title' => $info['label'],
+ '#options' => array(0 => t('Not selected')) + $objects,
+ '#default_value' => isset($form_state['storage'][$entity]) ? $form_state['storage'][$entity] : 0,
+ '#access' => !empty($objects),
+ );
+
+ // Build a list of supported token types based on the available entites.
+ if ($form[$entity]['#access']) {
+ $token_types[$entity] = !empty($info['token type']) ? $info['token type'] : $entity;
+ }
+ }
+ }
+
+ $form['text'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Enter your text here'),
+ '#default_value' => 'Hello [current-user:name]!',
+ );
+
+ // Display the results of tokenized text.
+ if (!empty($form_state['storage']['text'])) {
+ $form['text']['#default_value'] = $form_state['storage']['text'];
+
+ $data = array();
+ foreach ($entities as $entity => $info) {
+ if (!empty($form_state['storage'][$entity])) {
+ $objects = entity_load($entity, array($form_state['storage'][$entity]));
+ if ($objects) {
+ $data[$token_types[$entity]] = reset($objects);
+ }
+ }
+ }
+
+ // Display the tokenized text.
+ $form['text_tokenized'] = array(
+ '#type' => 'item',
+ '#title' => t('Result'),
+ '#markup' => token_replace($form_state['storage']['text'], $data),
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ if (module_exists('token')) {
+ $form['token_tree'] = array(
+ '#theme' => 'token_tree',
+ '#token_types' => $token_types,
+ );
+ }
+ else {
+ $form['token_tree'] = array(
+ '#markup' => '
' . t('Enable the Token module to view the available token browser.', array('@drupal-token' => 'http://drupal.org/project/token')) . '
',
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Submit callback; store the submitted values into storage.
+ */
+function token_example_example_form_submit($form, &$form_state) {
+ $form_state['storage'] = $form_state['values'];
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Builds a list of available content.
+ */
+function _token_example_get_node() {
+ if (!user_access('access content') && !user_access('bypass node access')) {
+ return array();
+ }
+
+ $node_query = db_select('node', 'n');
+ $node_query->fields('n', array('nid', 'title'));
+ $node_query->condition('n.status', NODE_PUBLISHED);
+ $node_query->orderBy('n.created', 'DESC');
+ $node_query->range(0, 10);
+ $node_query->addTag('node_access');
+ $nodes = $node_query->execute()->fetchAllKeyed();
+ $nodes = array_map('check_plain', $nodes);
+ return $nodes;
+}
+
+/**
+ * Builds a list of available comments.
+ */
+function _token_example_get_comment() {
+ if (!module_exists('comment') || (!user_access('access comments') && !user_access('administer comments'))) {
+ return array();
+ }
+
+ $comment_query = db_select('comment', 'c');
+ $comment_query->innerJoin('node', 'n', 'n.nid = c.nid');
+ $comment_query->fields('c', array('cid', 'subject'));
+ $comment_query->condition('n.status', NODE_PUBLISHED);
+ $comment_query->condition('c.status', COMMENT_PUBLISHED);
+ $comment_query->orderBy('c.created', 'DESC');
+ $comment_query->range(0, 10);
+ $comment_query->addTag('node_access');
+ $comments = $comment_query->execute()->fetchAllKeyed();
+ $comments = array_map('check_plain', $comments);
+ return $comments;
+}
+
+/**
+ * Builds a list of available user accounts.
+ */
+function _token_example_get_user() {
+ if (!user_access('access user profiles') &&
+ !user_access('administer users')) {
+ return array();
+ }
+
+ $account_query = db_select('users', 'u');
+ $account_query->fields('u', array('uid', 'name'));
+ $account_query->condition('u.uid', 0, '>');
+ $account_query->condition('u.status', 1);
+ $account_query->range(0, 10);
+ $accounts = $account_query->execute()->fetchAllKeyed();
+ $accounts = array_map('check_plain', $accounts);
+ return $accounts;
+}
+
+/**
+ * Builds a list of available taxonomy terms.
+ */
+function _token_example_get_taxonomy_term() {
+ $term_query = db_select('taxonomy_term_data', 'ttd');
+ $term_query->fields('ttd', array('tid', 'name'));
+ $term_query->range(0, 10);
+ $term_query->addTag('term_access');
+ $terms = $term_query->execute()->fetchAllKeyed();
+ $terms = array_map('check_plain', $terms);
+ return $terms;
+}
+
+/**
+ * Builds a list of available files.
+ */
+function _token_example_get_file() {
+ $file_query = db_select('file_managed', 'f');
+ $file_query->fields('f', array('fid', 'filename'));
+ $file_query->range(0, 10);
+ $files = $file_query->execute()->fetchAllKeyed();
+ $files = array_map('check_plain', $files);
+ return $files;
+}
+/**
+ * @} End of "defgroup token_example".
+ */
diff --git a/sites/all/modules/examples/token_example/token_example.test b/sites/all/modules/examples/token_example/token_example.test
new file mode 100644
index 00000000..64a9cf14
--- /dev/null
+++ b/sites/all/modules/examples/token_example/token_example.test
@@ -0,0 +1,76 @@
+ 'Token example functionality',
+ 'description' => 'Verify the token example interface.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('token_example');
+ $this->webUser = $this->drupalCreateUser();
+ $this->drupalLogin($this->webUser);
+ }
+
+ /**
+ * Test interface.
+ */
+ public function testInterface() {
+ $filtered_id = db_query("SELECT format FROM {filter_format} WHERE name = 'Filtered HTML'")->fetchField();
+ $default_format_id = filter_default_format($this->webUser);
+
+ $this->drupalGet('examples/token');
+ $this->assertNoFieldByName('node');
+ $this->assertNoFieldByName('user');
+
+ $edit = array(
+ 'text' => 'User [current-user:name] is trying the token example!',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit'));
+ $this->assertText('User ' . $this->webUser->name . ' is trying the token example!');
+
+ // Create a node and then make the 'Plain text' text format the default.
+ $node = $this->drupalCreateNode(array('title' => 'Example node', 'status' => NODE_PUBLISHED));
+ db_update('filter_format')
+ ->fields(array('weight' => -10))
+ ->condition('name', 'Plain text')
+ ->execute();
+
+ $this->drupalGet('examples/token');
+
+ $edit = array(
+ 'text' => 'Would you like to view the [node:type-name] [node:title] with text format [node:body-format] (ID [node:body-format:id])?',
+ 'node' => $node->nid,
+ );
+ $this->drupalPost(NULL, $edit, t('Submit'));
+ $this->assertText('Would you like to view the Basic page Example node with text format Filtered HTML (ID ' . $filtered_id . ')?');
+
+ $edit = array(
+ 'text' => 'Your default text format is [default-format:name] (ID [default-format:id]).',
+ );
+ $this->drupalPost(NULL, $edit, t('Submit'));
+ $this->assertText('Your default text format is Filtered HTML (ID ' . $default_format_id . ')');
+ }
+}
diff --git a/sites/all/modules/examples/token_example/token_example.tokens.inc b/sites/all/modules/examples/token_example/token_example.tokens.inc
new file mode 100644
index 00000000..0aefa653
--- /dev/null
+++ b/sites/all/modules/examples/token_example/token_example.tokens.inc
@@ -0,0 +1,142 @@
+ t('Text formats'),
+ 'description' => t('Tokens related to text formats.'),
+ 'needs-data' => 'format',
+ );
+ $info['types']['default-format'] = array(
+ 'name' => t('Default text format'),
+ 'description' => t("Tokens related to the currently logged in user's default text format."),
+ 'type' => 'format',
+ );
+
+ // Tokens for the text format token type.
+ $info['tokens']['format']['id'] = array(
+ 'name' => t('ID'),
+ 'description' => t("The unique ID of the text format."),
+ );
+ $info['tokens']['format']['name'] = array(
+ 'name' => t('Name'),
+ 'description' => t("The name of the text format."),
+ );
+
+ // Node tokens.
+ $info['tokens']['node']['body-format'] = array(
+ 'name' => t('Body text format'),
+ 'description' => t("The name of the text format used on the node's body field."),
+ 'type' => 'format',
+ );
+
+ // Comment tokens.
+ if (module_exists('comment')) {
+ $info['tokens']['comment']['body-format'] = array(
+ 'name' => t('Body text format'),
+ 'description' => t("The name of the text format used on the comment's body field."),
+ 'type' => 'format',
+ );
+ }
+
+ return $info;
+}
+
+/**
+ * Implements hook_tokens().
+ *
+ * @ingroup token_example
+ */
+function token_example_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $replacements = array();
+ $sanitize = !empty($options['sanitize']);
+
+ // Text format tokens.
+ if ($type == 'format' && !empty($data['format'])) {
+ $format = $data['format'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'id':
+ // Since {filter_format}.format is an integer and not user-entered
+ // text, it does not need to ever be sanitized.
+ $replacements[$original] = $format->format;
+ break;
+
+ case 'name':
+ // Since the format name is user-entered text, santize when requested.
+ $replacements[$original] = $sanitize ? filter_xss($format->name) : $format->name;
+ break;
+ }
+ }
+ }
+
+ // Default format tokens.
+ if ($type == 'default-format') {
+ $default_id = filter_default_format();
+ $default_format = filter_format_load($default_id);
+ $replacements += token_generate('format', $tokens, array('format' => $default_format), $options);
+ }
+
+ // Node tokens.
+ if ($type == 'node' && !empty($data['node'])) {
+ $node = $data['node'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'body-format':
+ if ($items = field_get_items('node', $node, 'body')) {
+ $format = filter_format_load($items[0]['format']);
+ $replacements[$original] = $sanitize ? filter_xss($format->name) : $format->name;
+ }
+ break;
+ }
+ }
+
+ // Chained token relationships.
+ if ($format_tokens = token_find_with_prefix($tokens, 'body-format')) {
+ if ($items = field_get_items('node', $node, 'body')) {
+ $body_format = filter_format_load($items[0]['format']);
+ $replacements += token_generate('format', $format_tokens, array('format' => $body_format), $options);
+ }
+ }
+ }
+
+ // Comment tokens.
+ if ($type == 'comment' && !empty($data['comment'])) {
+ $comment = $data['comment'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'body-format':
+ if ($items = field_get_items('comment', $comment, 'comment_body')) {
+ $format = filter_format_load($items[0]['format']);
+ $replacements[$original] = $sanitize ? filter_xss($format->name) : $format->name;
+ }
+ break;
+ }
+ }
+
+ // Chained token relationships.
+ if ($format_tokens = token_find_with_prefix($tokens, 'body-format')) {
+ if ($items = field_get_items('comment', $comment, 'comment_body')) {
+ $body_format = filter_format_load($items[0]['format']);
+ $replacements += token_generate('format', $format_tokens, array('format' => $body_format), $options);
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/examples/trigger_example/trigger_example.info b/sites/all/modules/examples/trigger_example/trigger_example.info
new file mode 100644
index 00000000..173f447b
--- /dev/null
+++ b/sites/all/modules/examples/trigger_example/trigger_example.info
@@ -0,0 +1,19 @@
+name = Trigger example
+description = An example showing how a module can provide triggers that can have actions associated with them.
+package = Example modules
+core = 7.x
+; Since someone might install our module through Composer, we want to be sure
+; that the Drupal Composer facade knows we're specifying a core module rather
+; than a project. We do this by namespacing the dependency name with drupal:.
+dependencies[] = drupal:trigger
+; Since the namespacing feature is new as of Drupal 7.40, we have to require at
+; least that version of core.
+dependencies[] = drupal:system (>= 7.40)
+files[] = trigger_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/trigger_example/trigger_example.module b/sites/all/modules/examples/trigger_example/trigger_example.module
new file mode 100644
index 00000000..13725470
--- /dev/null
+++ b/sites/all/modules/examples/trigger_example/trigger_example.module
@@ -0,0 +1,318 @@
+ array(
+ 'user_first_time_login' => array(
+ 'label' => t('After a user has logged in for the first time'),
+ ),
+ ),
+ 'trigger_example' => array(
+ 'triggersomething' => array(
+ 'label' => t('After the triggersomething button is clicked'),
+ ),
+ ),
+ );
+}
+
+/**
+ * Triggers are used most of the time to do something when an event happens.
+ * The most common type of event is a hook invocation,
+ * but that is not the only possibility.
+ */
+
+/**
+ * Trigger: triggersomething. Run actions associated with an arbitrary event.
+ *
+ * Here pressing a button is a trigger. We have defined a
+ * custom function as a trigger (trigger_example_triggersomething).
+ * It will ask for all actions attached to the 'triggersomething' event,
+ * prepare a basic 'context' for them
+ * and run all of them. This could have been implemented by a hook
+ * implementation, but in this demonstration, it will just be called in a
+ * form's submit.
+ *
+ * This function is executed during the submission of the example form defined
+ * in this module.
+ *
+ * @param array $options
+ * Array of arguments used to call the triggersomething function, if any.
+ */
+function trigger_example_triggersomething($options = array()) {
+ // Ask the trigger module for all actions enqueued for the 'triggersomething'
+ // trigger.
+ $aids = trigger_get_assigned_actions('triggersomething');
+ // Prepare a basic context, indicating group and "hook", and call all the
+ // actions with this context as arguments.
+ $context = array(
+ 'group' => 'trigger_example',
+ 'hook' => 'triggersomething',
+ );
+ actions_do(array_keys($aids), (object) $options, $context);
+}
+
+
+/**
+ * The next trigger is more complex, we are providing a trigger for a
+ * new event: "user first time login". We need to create this event
+ * first.
+ */
+
+/**
+ * Implements hook_user_login().
+ *
+ * User first login trigger: Run actions on user first login.
+ *
+ * The event "User first time login" does not exist, we should create it before
+ * it can be used. We use hook_user_login to be informed when a user logs in and
+ * try to find if the user has previously logged in before. If the user has not
+ * accessed previously, we make a call to our trigger function.
+ */
+function trigger_example_user_login(&$edit, $account, $category = NULL) {
+ // Verify user has never accessed the site: last access was creation date.
+ if ($account->access == 0) {
+ // Call the aproppriate trigger function.
+ _trigger_example_first_time_login('user_first_time_login', $edit, $account, $category);
+ }
+}
+
+/**
+ * Trigger function for "User first time login".
+ *
+ * This trigger is a user-type triggers, so is grouped with other user-type
+ * triggers. It needs to provide all the context that user-type triggers
+ * provide. For this example, we are going to copy the trigger.module
+ * implementation for the 'User has logged in' event.
+ *
+ * This function will run all the actions assigned to the
+ * 'user_first_time_login' trigger.
+ *
+ * For testing you can use an update query like this to reset a user to
+ * "never logged in":
+ * @code
+ * update users set access=created where name='test1';
+ * @endcode
+ *
+ * @param string $hook
+ * The trigger identification.
+ * @param array $edit
+ * Modifications for the account object (should be empty).
+ * @param object $account
+ * User object that has logged in.
+ * @param string $category
+ * Category of the profile.
+ */
+function _trigger_example_first_time_login($hook, &$edit, $account, $category = NULL) {
+ // Keep objects for reuse so that changes actions make to objects can persist.
+ static $objects;
+ // Get all assigned actions for the 'user_first_time_login' trigger.
+ $aids = trigger_get_assigned_actions($hook);
+ $context = array(
+ 'group' => 'user',
+ 'hook' => $hook,
+ 'form_values' => &$edit,
+ );
+ // Instead of making a call to actions_do for all triggers, doing this loop
+ // we provide the opportunity for actions to alter the account object, and
+ // the next action should have this altered account object as argument.
+ foreach ($aids as $aid => $info) {
+ $type = $info['type'];
+ if ($type != 'user') {
+ if (!isset($objects[$type])) {
+ $objects[$type] = _trigger_normalize_user_context($type, $account);
+ }
+ $context['user'] = $account;
+ actions_do($aid, $objects[$type], $context);
+ }
+ else {
+ actions_do($aid, $account, $context, $category);
+ }
+ }
+}
+
+/**
+ * Helper functions for the module interface to test the triggersomething
+ * trigger.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function trigger_example_help($path, $arg) {
+ switch ($path) {
+ case 'examples/trigger_example':
+ $explanation = t(
+ 'Click the button on this page to call trigger_example_triggersomething()
+ and fire the triggersomething event. First, you need to create an action
+ and assign it to the "After the triggersomething button is clicked" trigger,
+ or nothing will happen. Use the Actions settings page
+ and assign these actions to the triggersomething event on the
+ Triggers settings page.
+ The other example is the "user never logged in before" example. For that one,
+ assign an action to the "After a user has logged in for the first time" trigger
+ and then log a user in.', array('@actions-url' => url('admin/config/system/actions'), '@triggers-url' => url('admin/structure/trigger/trigger_example')));
+ return "
$explanation
";
+
+ case 'admin/structure/trigger/system':
+ return t('you can assign actions to run everytime an email is sent by Drupal');
+
+ case 'admin/structure/trigger/trigger_example':
+ $explanation = t(
+ "A trigger is a system event. For the trigger example, it's just a button-press.
+ To demonstrate the trigger example, choose to associate the 'display a message to the user'
+ action with the 'after the triggersomething button is pressed' trigger."
+ );
+ return "
$explanation
";
+ }
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Provides a form that can be used to fire the module's triggers.
+ */
+function trigger_example_menu() {
+ $items['examples/trigger_example'] = array(
+ 'title' => 'Trigger Example',
+ 'description' => 'Provides a form to demonstrate the trigger example.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('trigger_example_form'),
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Trigger example test form.
+ *
+ * Provides a button to run the triggersomething event.
+ */
+function trigger_example_form($form_state) {
+ $form['triggersomething'] = array(
+ '#type' => 'submit',
+ '#value' => t('Run triggersomething event'),
+ );
+ return $form;
+}
+
+/**
+ * Submit handler for the trigger_example_form().
+ */
+function trigger_example_form_submit($form, $form_state) {
+ // If the user clicked the button, then run the triggersomething trigger.
+ if ($form_state['values']['op'] == t('Run triggersomething event')) {
+ trigger_example_triggersomething();
+ }
+}
+
+
+/**
+ * Optional usage of hook_trigger_info_alter().
+ *
+ * This function is not required to write your own triggers, but it may be
+ * useful when you want to alter existing triggers.
+ */
+
+/**
+ * Implements hook_trigger_info_alter().
+ *
+ * We call hook_trigger_info_alter when we want to change an existing trigger.
+ * As mentioned earlier, this hook is not required to create your own triggers,
+ * and should only be used when you need to alter current existing triggers. In
+ * this example implementation a little change is done to the existing trigger
+ * provided by core: 'cron'
+ *
+ * @see hook_trigger_info()
+ */
+function trigger_example_trigger_info_alter(&$triggers) {
+ // Make a simple change to an existing core trigger, altering the label
+ // "When cron runs" to our custom label "On cron execution"
+ $triggers['system']['cron']['label'] = t('On cron execution');
+}
diff --git a/sites/all/modules/examples/trigger_example/trigger_example.test b/sites/all/modules/examples/trigger_example/trigger_example.test
new file mode 100644
index 00000000..0c4c8c53
--- /dev/null
+++ b/sites/all/modules/examples/trigger_example/trigger_example.test
@@ -0,0 +1,89 @@
+ 'Trigger example',
+ 'description' => 'Perform various tests on trigger_example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('trigger', 'trigger_example');
+ }
+
+ /**
+ * Test assigning a configurable action to the triggersomething event.
+ */
+ public function testTriggersomethingEvent() {
+ // Create an administrative user.
+ $test_user = $this->drupalCreateUser(array('administer actions'));
+ $this->drupalLogin($test_user);
+
+ // Create a configurable action for display a message to the user.
+ $hash = drupal_hash_base64('system_message_action');
+ $action_label = $this->randomName();
+ $edit = array(
+ 'actions_label' => $action_label,
+ 'message' => $action_label,
+ );
+ $this->drupalPost('admin/config/system/actions/configure/' . $hash, $edit, t('Save'));
+ $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'system_message_action'))->fetchField();
+ // $aid is likely 3 but if we add more uses for the sequences table in
+ // core it might break, so it is easier to get the value from the database.
+ $edit = array('aid' => drupal_hash_base64($aid));
+
+ // Note that this only works because there's just one item on the page.
+ $this->drupalPost('admin/structure/trigger/trigger_example', $edit, t('Assign'));
+
+ // Request triggersomething form and submit.
+ $this->drupalPost('examples/trigger_example', array(), t('Run triggersomething event'));
+ // Verify the message is shown to the user.
+ $this->assertText($action_label, 'The triggersomething event executed the action.');
+ }
+
+ /**
+ * Test triggers at user login.
+ */
+ public function testUserLogin() {
+ // Create an administrative user.
+ $admin_user = $this->drupalCreateUser(array('administer actions'));
+ $this->drupalLogin($admin_user);
+
+ // Create a configurable action for display a message to the user.
+ $hash = drupal_hash_base64('system_message_action');
+ $action_label = $this->randomName();
+ $edit = array(
+ 'actions_label' => $action_label,
+ 'message' => $action_label,
+ );
+ $this->drupalPost('admin/config/system/actions/configure/' . $hash, $edit, t('Save'));
+ $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'system_message_action'))->fetchField();
+ $edit = array('aid' => drupal_hash_base64($aid));
+
+ // Find the correct trigger.
+ $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-first-time-login-assign-form');
+
+ $test_user = $this->drupalCreateUser();
+ $this->drupalLogin($test_user);
+ $this->assertText($action_label);
+ }
+}
diff --git a/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.info b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.info
new file mode 100644
index 00000000..22259820
--- /dev/null
+++ b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.info
@@ -0,0 +1,12 @@
+name = Vertical tabs example
+description = Show how to use vertical tabs for enhancing user experience.
+package = Example modules
+core = 7.x
+files[] = vertical_tabs_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.js b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.js
new file mode 100644
index 00000000..b578020a
--- /dev/null
+++ b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.js
@@ -0,0 +1,23 @@
+(function ($) {
+
+/**
+ * Update the summary for the module's vertical tab.
+ */
+Drupal.behaviors.vertical_tabs_exampleFieldsetSummaries = {
+ attach: function (context) {
+ // Use the fieldset class to identify the vertical tab element
+ $('fieldset#edit-vertical-tabs-example', context).drupalSetSummary(function (context) {
+ // Depending on the checkbox status, the settings will be customized, so
+ // update the summary with the custom setting textfield string or a use a
+ // default string.
+ if ($('#edit-vertical-tabs-example-enabled', context).attr('checked')) {
+ return Drupal.checkPlain($('#edit-vertical-tabs-example-custom-setting', context).val());
+ }
+ else {
+ return Drupal.t('Using default');
+ }
+ });
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.module b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.module
new file mode 100644
index 00000000..571b96c6
--- /dev/null
+++ b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.module
@@ -0,0 +1,114 @@
+ 'Vertical tabs example',
+ 'description' => 'Shows how vertical tabs can best be supported by a custom module',
+ 'page callback' => '_vertical_tabs_example_explanation',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_form_alter().
+ *
+ * Adds custom fieldset to the node form, and attach ajax behaviour for vertical
+ * panels to update the settings description.
+ *
+ * @see vertical_tabs_example.js
+ */
+function vertical_tabs_example_form_alter(&$form, $form_state, $form_id) {
+ // Only include on node add/edit forms.
+ if (!empty($form['#node_edit_form'])) {
+
+ // Create a fieldset that will be included in the vertical tab.
+ $form['vertical_tabs_example'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Example vertical tab'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#tree' => TRUE,
+ // Send this tab to the top of the list.
+ '#weight' => -99,
+ // The #group value must match the name of the vertical tabs element.
+ // In most cases, this is 'additional_settings'.
+ '#group' => 'additional_settings',
+ // Vertical tabs provide its most usable appearance when they are used to
+ // include a summary of the information contained in the fieldset. To do
+ // this, we attach additional JavaScript to handle changing the summary
+ // when form settings are changed.
+ '#attached' => array(
+ 'js' => array(
+ 'vertical-tabs' => drupal_get_path('module', 'vertical_tabs_example') . '/vertical_tabs_example.js',
+ ),
+ ),
+ );
+
+ // The form elements below provide a demonstration of how a fieldset
+ // summary can be displayed in a collapsed tab.
+ //
+ // This checkbox is used to show or hide the custom settings form using
+ // javascript (altering states of a container defined later).
+ $form['vertical_tabs_example']['enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Change this setting'),
+ '#default_value' => FALSE,
+ );
+
+ // This container will be used to store the whole form for our custom
+ // settings. This way, showing/hiding the form using javascript is easier,
+ // as only one element should be set visible.
+ $form['vertical_tabs_example']['vertical_tabs_examplecontainer'] = array(
+ '#type' => 'container',
+ '#parents' => array('vertical_tabs_example'),
+ '#states' => array(
+ 'invisible' => array(
+ // If the checkbox is not enabled, show the container.
+ 'input[name="vertical_tabs_example[enabled]"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+
+ // The string of this textfield will be shown as summary in the vertical
+ // tab.
+ $form['vertical_tabs_example']['vertical_tabs_examplecontainer']['custom_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Use this setting instead'),
+ '#default_value' => 'I am a setting with a summary',
+ '#description' => t('As you type into this field, the summary will be updated in the tab.'),
+ );
+ }
+}
+
+/**
+ * Simple explanation page.
+ */
+function _vertical_tabs_example_explanation() {
+ return t("
The Vertical Tabs Example shows how a custom module can add a vertical tab to a node edit form, and support its summary field with JavaScript.
To see the effects of this module, add a piece of content and look at the set of tabs at the bottom. We've added one called 'Example vertical tab.'
", array('!node_add' => url('node/add')));
+}
+/**
+ * @} End of "defgroup vertical_tabs_example".
+ */
diff --git a/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.test b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.test
new file mode 100644
index 00000000..41e69d80
--- /dev/null
+++ b/sites/all/modules/examples/vertical_tabs_example/vertical_tabs_example.test
@@ -0,0 +1,45 @@
+ 'Vertical Tabs Example',
+ 'description' => 'Functional tests for the Vertical Tabs Example module.' ,
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('vertical_tabs_example');
+ }
+
+ /**
+ * Tests the menu paths defined in vertical_tabs_example module.
+ */
+ public function testVerticalTabsExampleMenus() {
+ $paths = array(
+ 'examples/vertical_tabs',
+ );
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertResponse(200, '200 response for path: ' . $path);
+ }
+ }
+}
diff --git a/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.info b/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.info
new file mode 100644
index 00000000..88bab7c2
--- /dev/null
+++ b/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.info
@@ -0,0 +1,12 @@
+name = XMLRPC example
+description = This is an example of how to implement client and server communications using XML-RPC.
+package = Example modules
+core = 7.x
+files[] = xmlrpc_example.test
+
+; Information added by Drupal.org packaging script on 2017-01-10
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "examples"
+datestamp = "1484076787"
+
diff --git a/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.module b/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.module
new file mode 100644
index 00000000..4d24e098
--- /dev/null
+++ b/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.module
@@ -0,0 +1,707 @@
+ 'XML-RPC Example',
+ 'description' => 'Information about the XML-RPC example',
+ 'page callback' => 'xmlrpc_example_info',
+ 'access callback' => TRUE,
+ );
+ // This is the server configuration form menu entry. This form can be used to
+ // configure the settings of the exposed services. An XML-RPC server does not
+ // require a configuration form, and has been included here as an example.
+ $items['examples/xmlrpc/server'] = array(
+ 'title' => 'XML-RPC Server configuration',
+ 'description' => 'Server configuration form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('xmlrpc_example_server_form'),
+ 'access callback' => TRUE,
+ 'weight' => 0,
+ );
+ // This is the client form menu entry. This form is used to allow user
+ // interaction with the services, but again, user interface is not required
+ // to create an XML-RPC client with Drupal.
+ $items['examples/xmlrpc/client'] = array(
+ 'title' => 'XML-RPC Client form',
+ 'description' => 'Demonstrates client side XML-RPC calls with Drupal',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('xmlrpc_example_client_form'),
+ 'access callback' => TRUE,
+ 'weight' => 1,
+ );
+ // This part is completely optional. It allows the modification of services
+ // defined by this or other modules. This configuration form is used to
+ // enable the hook_xmlrpc_alter API and alter current existing services.
+ $items['examples/xmlrpc/alter'] = array(
+ 'title' => 'XML-RPC Alterations',
+ 'description' => 'Demonstrates how to alter defined XML-RPC services',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('xmlrpc_example_alter_form'),
+ 'access callback' => TRUE,
+ 'weight' => 2,
+ );
+ return $items;
+}
+
+/**
+ * A simple landing-page information function.
+ */
+function xmlrpc_example_info() {
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+
+ $options = array(
+ 'system.listMethods' => array(),
+ );
+ // Make the xmlrpc request and process the results.
+ $supported_methods = xmlrpc($server, $options);
+ if ($supported_methods === FALSE) {
+ drupal_set_message(t('Error return from xmlrpc(): Error: @errno, Message: @message', array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())));
+ }
+
+ return array(
+ 'basic' => array(
+ '#markup' => t('This XML-RPC example presents code that shows
',
+ array(
+ '!server' => url('examples/xmlrpc/server'),
+ '!client' => url('examples/xmlrpc/client'),
+ '!alter' => url('examples/xmlrpc/alter'),
+ )
+ ),
+ ),
+ 'method_array' => array(
+ '#markup' => theme(
+ 'item_list',
+ array(
+ 'title' => t('These methods are supported by !server',
+ array('!server' => $server)
+ ),
+ 'items' => $supported_methods,
+ )
+ ),
+ ),
+ );
+}
+
+// This is the server part of the module, implementing a simple and little
+// server with just two simple services. The server is divided in two
+// different parts: the XML-RPC implementation (required) and a webform
+// interface (optional) to configure some settings in the server side.
+//
+// The XMLRPC server will define two different services:
+//
+// - subtract: perform the subtraction of two numbers. The minimum and maximum
+// values returned by the server can be configured in the server configuration
+// form.
+// - add: perform the addition of two numbers. The minimum and maximum values
+// returned by the server can be configured in the server configuration form.
+//
+// If the result value for the operation is over the maximum limit, a custom
+// error number 10001 is returned. This is an arbitrary number and could be any
+// number.
+//
+// If the result value for the operation is below the minimum limit, a custom
+// error number 10002 is returned. Again, this value is arbitrary and could be
+// any other number. Client applications must know the meaning of the error
+// numbers returned by the server.
+//
+// The following code is the XML-RPC implementation of the server part.
+// The first step is to define the methods. This methods should be associated
+// to callbacks that will be defined later.
+//
+/**
+ * Implements hook_xmlrpc().
+ *
+ * Provides Drupal with an array to map XML-RPC callbacks to existing functions.
+ * These functions may be defined in other modules. The example implementation
+ * defines specific functions for the example services.
+ *
+ * Note: Drupal's built-in XML-RPC server already includes several methods by
+ * default:
+ *
+ * Service dicovery methods:
+ * - system.listMethods: return a list of the methods the server has, by name.
+ * - system.methodSignature: return a description of the argument format a
+ * - system.methodHelp: returns a text description of a particular method.
+ * particular method expects.
+ *
+ * Other:
+ * - system.multicall: perform several method calls in a single xmlrpc request.
+ * - system.getCapabilities: determine if a given capability is supported.
+ *
+ * The methods defined by hook_xmlrpc() will be added to those provided by
+ * default by Drupal's XML-RPC server.
+ *
+ * @see hook_xmlrpc()
+ */
+function xmlrpc_example_xmlrpc() {
+ $methods[] = array(
+ // First argument is the method name.
+ 'xmlrpc_example.add',
+ // Callback to execute when this method is requested.
+ '_xmlrpc_example_server_add',
+ // An array defines the types of output and input values for this method.
+ array(
+ // The first value is the return type, an integer in this case.
+ 'int',
+ // First operand is an integer.
+ 'int',
+ // Second operand is an integer.
+ 'int',
+ ),
+ // Include a little description that is shown when XML-RPC server is
+ // requested for the implemented methods list.
+ // Method description.
+ t('Returns the sum of the two arguments.'),
+ );
+ // The subtract method is similar to the addition, only the method name,
+ // callback and description are different.
+ $methods[] = array(
+ 'xmlrpc_example.subtract',
+ '_xmlrpc_example_server_subtract',
+ array('int', 'int', 'int'),
+ t('Return difference of the two arguments.'),
+ );
+
+ return $methods;
+}
+
+// The following code for the server is optional if the callbacks already exist.
+// A server may implement methods associated to callbacks like node_load(),
+// variable_get() or any other existing function (php functions as well).
+//
+// If the callbacks associated to the methods don't exist they must be
+// created. This implementation requires two specific callbacks:
+// - _xmlrpc_example_server_add()
+// - _xmlrpc_example_server_subtract()
+//
+//
+/**
+ * This is the callback for the xmlrpc_example.add method.
+ *
+ * Sum the two arguments and return value or an error if the result is out of
+ * the configured limits.
+ *
+ * @param int|float $num1
+ * The first number to be summed.
+ * @param int|float $num2
+ * The second number to be summed.
+ *
+ * @return int|float
+ * The sum of the arguments, or error if it is not in server defined bounds.
+ *
+ * @see xmlrpc_error()
+ */
+function _xmlrpc_example_server_add($num1, $num2) {
+ $sum = $num1 + $num2;
+ // If result is not within maximum and minimum limits,
+ // return corresponding error.
+ $max = variable_get('xmlrpc_example_server_max', 10);
+ $min = variable_get('xmlrpc_example_server_min', 0);
+ if ($sum > $max) {
+ return xmlrpc_error(10001, t('Result is over the upper limit (@max) defined by the server.', array('@max' => $max)));
+ }
+ if ($sum < $min) {
+ return xmlrpc_error(10002, t('Result is below the lower limit defined by the server (@min).', array('@min' => $min)));
+ }
+ // Otherwise return the result.
+ return $sum;
+}
+
+/**
+ * This is the callback for the xmlrpc_example.subtract xmlrpc method.
+ *
+ * Return the difference of the two arguments, or an error if the result is out
+ * of the configured limits.
+ *
+ * @param int|float $num1
+ * First number
+ * @param int|float $num2
+ * Second number
+ *
+ * @return int|float
+ * The difference of the two arguments, or error if it is not in server
+ * defined bounds.
+ *
+ * @see xmlrpc_error()
+ */
+function _xmlrpc_example_server_subtract($num1, $num2) {
+ $diference = $num1 - $num2;
+ $max = variable_get('xmlrpc_example_server_max', 10);
+ $min = variable_get('xmlrpc_example_server_min', 0);
+
+ // If result is not within maximum and minimum limits,
+ // return corresponding error.
+ if ($diference > $max) {
+ return xmlrpc_error(10001, t('Result is above the upper limit (@max) defined by the server.', array('@max' => $max)));
+ }
+ if ($diference < $min) {
+ return xmlrpc_error(10002, t('Result is below the lower limit (@min) defined by the server.', array('@min' => $min)));
+ }
+ // Otherwise return the result.
+ return $diference;
+}
+
+// User interface for the XML-RPC Server part.
+// A server does not require an interface at all. In this implementation we
+// use a server configuration form to set the limits available for the addition
+// and subtraction operations.
+//
+/**
+ * Returns form array to configure the service options.
+ *
+ * Present a form to configure the service options. In this case the maximum
+ * and minimum values for any of the operations (add or subtraction).
+ */
+function xmlrpc_example_server_form() {
+ $form = array();
+ $form['explanation'] = array(
+ '#markup' => '
' . t('This is the XML-RPC server configuration page. Here you may define the maximum and minimum values for the addition or subtraction exposed services. ') . '
',
+ );
+ $form['xmlrpc_example_server_min'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter the minimum value returned by the subtraction or addition methods'),
+ '#description' => t('If the result of the operation is lower than this value, a custom XML-RPC error will be returned: 10002.'),
+ '#default_value' => variable_get('xmlrpc_example_server_min', 0),
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ $form['xmlrpc_example_server_max'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter the maximum value returned by sub or add methods'),
+ '#description' => t('if the result of the operation is bigger than this value, a custom XML-RPC error will be returned: 10001.'),
+ '#default_value' => variable_get('xmlrpc_example_server_max', 10),
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ $form['info'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('Use the XML-RPC Client example form to experiment', array('!link' => url('examples/xmlrpc/client'))) . '
' . t('Just a note of warning: The alter form has been used to disable the limits, so you may want to turn that off if you do not want it.', array('!link' => url('examples/xmlrpc/alter'))) . '
',
+ );
+ }
+ return system_settings_form($form);
+}
+
+
+// The server part of the module ends here.
+//
+// This is the client part of the module. If defines a form with two input
+// fields to call xmlrpc_example.add or xmlrpc_example.subtract methods on this
+// host. Please note that having a user interface to query an XML-RPC service is
+// not required. A method can be requested to a server using the xmlrpc()
+// function directly. We have included an user interface to make the testing
+// easier.
+//
+// The client user interface part of the module starts here.
+//
+/**
+ * Returns a form array to take input for two arguments.
+ *
+ * Present a form to get two arguments, and make a call to an XML-RPC server
+ * using these arguments as input, showing the result in a message.
+ */
+function xmlrpc_example_client_form() {
+ $form = array();
+ $form['explanation'] = array(
+ '#markup' => '
' . t('This example demonstrates how to make XML-RPC calls with Drupal. The "Request methods" button makes a request to the server and asks for the available list of methods, as a service discovery request. The "Add integers" and "Subtract integers" use the xmlrpc() function to act as a client, calling the XML-RPC server defined in this same example for some defined methods. An XML-RPC error will result if the result in the addition or subtraction requested is out of bounds defined by the server. These error numbers are defined by the server. The "Add and Subtract" button performs a multicall operation on the XML-RPC server: several requests in a single XML-RPC call. ') . '
',
+ );
+ // We are going to call add and subtract methods, and
+ // they work with integer values.
+ $form['num1'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter an integer'),
+ '#default_value' => 2,
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ $form['num2'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Enter a second integer'),
+ '#default_value' => 2,
+ '#size' => 5,
+ '#required' => TRUE,
+ );
+ // Include several buttons, each of them calling a different method.
+ // This button submits a XML-RPC call to the system.listMethods method.
+ $form['information'] = array(
+ '#type' => 'submit',
+ '#value' => t('Request methods'),
+ '#submit' => array('xmlrpc_example_client_request_methods_submit'),
+ );
+ // This button submits a XML-RPC call to the xmlrpc_example.add method.
+ $form['add'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add the integers'),
+ '#submit' => array('xmlrpc_example_client_add_submit'),
+ );
+ // This button submits a XML-RPC call to the xmlrpc_example.subtract method.
+ $form['subtract'] = array(
+ '#type' => 'submit',
+ '#value' => t('Subtract the integers'),
+ '#submit' => array('xmlrpc_example_client_subtract_submit'),
+ );
+ // This button submits a XML-RPC call to the system.multicall method.
+ $form['add_subtract'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add and Subtract'),
+ '#submit' => array('xmlrpc_example_client_multicall_submit'),
+ );
+ if (variable_get('xmlrpc_example_alter_enabled', FALSE)) {
+ $form['overridden'] = array(
+ '#type' => 'markup',
+ '#markup' => '
' . t('Just a note of warning: The alter form has been used to disable the limits, so you may want to turn that off if you do not want it.', array('!link' => url('examples/xmlrpc/alter'))) . '
',
+ );
+ }
+ return $form;
+}
+
+/**
+ * Submit handler to query system.listMethods.
+ *
+ * Submit: query the XML-RPC endpoint for the method system.listMethods
+ * and report the result as a Drupal message. The result is a list of the
+ * available methods in this XML-RPC server.
+ *
+ * Important note: Not all XML-RPC servers implement this method. Drupal's
+ * built-in XML-RPC server implements this method by default.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ */
+function xmlrpc_example_client_request_methods_submit($form, &$form_state) {
+ // First define the endpoint of the XML-RPC service, in this case this is our
+ // own server.
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ // Then we should define the method to call. xmlrpc() requires that all the
+ // information related to the called method be passed as an array in the form
+ // of 'method_name' => arguments_array
+ $options = array(
+ 'system.listMethods' => array(),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())),
+ 'error'
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response:
@response
',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+/**
+ * Submit handler to query xmlrpc_example.add.
+ *
+ * Submit: query the XML-RPC endpoint for the method xmlrpc_example.add
+ * and report the result as a Drupal message.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ */
+function xmlrpc_example_client_add_submit($form, &$form_state) {
+ // First define the endpoint of the XML-RPC service, in this case is our
+ // own server.
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ // Then we should define the method to call. xmlrpc() requires that all the
+ // information related to the called method is passed as an array in the form
+ // of 'method_name' => arguments_array
+ $options = array(
+ 'xmlrpc_example.add' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())),
+ 'error'
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response: @response',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+/**
+ * Submit handler to query xmlrpc_example.subtract.
+ *
+ * Submit: query the XML-RPC endpoint for the method xmlrpc_example.subtract
+ * and report the result as a Drupal message.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ * @see xmlrpc_example_client_add_submit()
+ */
+function xmlrpc_example_client_subtract_submit($form, &$form_state) {
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ $options = array(
+ 'xmlrpc_example.subtract' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg())),
+ 'error'
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response: @response',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+/**
+ * Submit a multicall request.
+ *
+ * Submit a multicall request: query the XML-RPC endpoint for the methods
+ * xmlrpc_example.add and xmlrpc_example.subtract and report the result as a
+ * Drupal message. Drupal's XML-RPC client builds the system.multicall request
+ * automatically when there is more than one method to call.
+ *
+ * @param array $form
+ * Form array.
+ * @param array $form_state
+ * Form_state array.
+ *
+ * @see xmlrpc()
+ * @see xmlrpc_errno()
+ * @see xmlrpc_error_msg()
+ * @see xmlrpc_example_client_multicall_submit()
+ */
+function xmlrpc_example_client_multicall_submit($form, &$form_state) {
+ $server = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+
+ /*
+ * Drupal's built-in xmlrpc server supports the system.multicall method.
+ *
+ * To make a multicall request, the main invoked method should be the
+ * function 'system.multicall', and the arguments to make this call must be
+ * defined as an array of single method calls, being the array keys the
+ * service methods to be called, and the array elements the method arguments.
+ *
+ * See the code below this comment as example.
+ */
+
+ // Build an array of several calls, Drupal's xmlrpc built-in support will
+ // construct the correct system.multicall request for the server.
+ $options = array(
+ 'xmlrpc_example.add' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ 'xmlrpc_example.subtract' => array(
+ (int) $form_state['values']['num1'],
+ (int) $form_state['values']['num2'],
+ ),
+ );
+ // Make the xmlrpc request and process the results.
+ $result = xmlrpc($server, $options);
+
+ if ($result === FALSE) {
+ drupal_set_message(
+ t('Error return from xmlrpc(): Error: @errno, Message: @message',
+ array('@errno' => xmlrpc_errno(), '@message' => xmlrpc_error_msg()))
+ );
+ }
+ else {
+ drupal_set_message(
+ t('The XML-RPC server returned this response:
@response
',
+ array('@response' => print_r($result, TRUE)))
+ );
+ }
+}
+
+// The client part of the module ends here.
+//
+// The alteration part of the module starts here. hook_xmlrpc_alter() is
+// useful when you want to extend, limit or alter methods defined by other
+// modules. This part is not required to have an XML-RPC server or client
+// working, but is useful to understand what can we do using current xmlrpc
+// API provided by drupal.
+//
+// This code can be defined in other module to alter the methods exposed by
+// this xmlrpc demonstration server, or can be used to alter methods defined
+// by other modules implementing hook_xmlrpc()
+//
+// As with the rest of the example module, an user interface is not required to
+// make use of this hook. A configuration form is included to enable/disable
+// this functionality, but this part is optional if you want to implement
+// hook_xmlrpc_alter()
+//
+// This is the XML-RPC code for the alteration part. It will check if an option
+// to enable the functionality is enabled and then alter it. We alter the
+// 'xmlrpc_example.add' and 'xmlrpc_example.subtract' methods, changing the
+// associated callback with custom functions. The modified methods (with
+// new callbacks associated) will perform the addition or subtraction of the
+// integer inputs, but will never check for limits nor return errors.
+/**
+ * Implements hook_xmlrpc_alter().
+ *
+ * Check to see if xmlrpc_example.add and xmlrpc_example.subtract methods are
+ * defined and replace their callbacks with custom code.
+ *
+ * @see hook_xmlrpc_alter()
+ */
+function xmlrpc_example_xmlrpc_alter(&$methods) {
+
+ // Only perform alterations if instructed to do so.
+ if (!variable_get('xmlrpc_example_alter_enabled', 0)) {
+ return;
+ }
+ // Loop all defined methods (other modules may include additional methods).
+ foreach ($methods as $index => $method) {
+ // First element in the method array is the method name.
+ if ($method[0] == 'xmlrpc_example.add') {
+ // Replace current callback with custom callback
+ // (second argument of the array).
+ $methods[$index][1] = '_xmlrpc_example_alter_add';
+ }
+ // Do the same for the substraction method.
+ if ($method[0] == 'xmlrpc_example.subtract') {
+ $methods[$index][1] = '_xmlrpc_example_alter_subtract';
+ }
+ }
+}
+
+// Now we define the custom callbacks replacing the original defined by the
+// altered methods: xmlrpc_example.add and _xmlrpc_example.subtract. These
+// new callbacks will not check if the result of the operation is within the
+// limits defined by the server and will always return value of the operation.
+/**
+ * Sum the two arguments without limit checking.
+ *
+ * This is the replacement callback for the xmlrpc_example.add xmlrpc method.
+ *
+ * @param int|float $num1
+ * First number
+ * @param int|float $num2
+ * Second Number
+ *
+ * @return int|float
+ * The sum of the arguments
+ */
+function _xmlrpc_example_alter_add($num1, $num2) {
+ return $num1 + $num2;
+}
+
+/**
+ * Return the difference of the two arguments without limit checking.
+ *
+ * This is the replacement callback for xmlrpc_example.subtract xmlrpc method.
+ *
+ * @param int|float $num1
+ * First number
+ * @param int|float $num2
+ * Second Number
+ *
+ * @return int|float
+ * The difference of the two arguments
+ */
+function _xmlrpc_example_alter_subtract($num1, $num2) {
+ return $num1 - $num2;
+}
+
+
+// Our implementation of hook_xmlrpc_alter will work only if a system variable
+// is set to true, and we need a configuration form to enable/disable this
+// 'feature'. This is the user interface to enable or disable the
+// hook_xmlrpc_alter operations.
+/**
+ * Present a form to enable/disable the code implemented in hook_xmlrpc_alter.
+ */
+function xmlrpc_example_alter_form() {
+ $form = array();
+ $form['explanation'] = array(
+ '#markup' => '
' . t('This is a configuration form to enable the alteration of XML-RPC methods using hook_xmlrpc_alter. hook_xmlrpc_alter() can be used to alter the current defined methods by other modules. In this case as demonstration, we will overide current add and subtraction methods with others not being limited. Remember that this hook is optional and is not required to create XMLRPC services. ') . '
',
+ );
+ $form['xmlrpc_example_alter_enabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Overide current xmlrpc_example.add and xmlrpc_example.subtraction methods'),
+ '#description' => t('If this checkbox is enabled, the default methods will be replaced with custom methods that ignore the XML-RPC server maximum and minimum restrictions.'),
+ '#default_value' => variable_get('xmlrpc_example_alter_enabled', 0),
+ );
+ $form['info'] = array(
+ '#markup' => '
' . t('Use the client submission form to see the results of checking this checkbox', array('!link' => url('examples/xmlrpc/client'))) . '
',
+ );
+ return system_settings_form($form);
+}
+/**
+ * @} End of "defgroup xmlrpc_example".
+ */
diff --git a/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.test b/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.test
new file mode 100644
index 00000000..01b15f90
--- /dev/null
+++ b/sites/all/modules/examples/xmlrpc_example/xmlrpc_example.test
@@ -0,0 +1,139 @@
+ 'XMLRPC example functionality',
+ 'description' => 'Test xmlrpc service implementation.',
+ 'group' => 'Examples',
+ );
+ }
+
+ /**
+ * Enable module.
+ */
+ public function setUp() {
+ parent::setUp('xmlrpc_example');
+
+ // Init common variables.
+ global $base_url;
+ $this->xmlRpcUrl = url($GLOBALS['base_url'] . '/xmlrpc.php', array('external' => TRUE));
+ }
+
+ /**
+ * Perform several calls to the XML-RPC interface to test the services.
+ */
+ public function testXmlrpcExampleBasic() {
+ // Unit test functionality.
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(3, 4)));
+ $this->assertEqual($result, 7, 'Successfully added 3+4 = 7');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(4, 3)));
+ $this->assertEqual($result, 1, 'Successfully subtracted 4-3 = 1');
+
+ // Make a multicall request.
+ $options = array(
+ 'xmlrpc_example.add' => array(5, 2),
+ 'xmlrpc_example.subtract' => array(5, 2),
+ );
+ $expected = array(7, 3);
+ $result = xmlrpc($this->xmlRpcUrl, $options);
+ $this->assertEqual($result, $expected, 'Successfully called multicall request');
+
+ // Verify default limits.
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(3, 4)));
+ $this->assertEqual(xmlrpc_errno(), 10002, 'Results below minimum return custom error: 10002');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(7, 4)));
+ $this->assertEqual(xmlrpc_errno(), 10001, 'Results beyond maximum return custom error: 10001');
+ }
+
+ /**
+ * Perform several calls using XML-RPC web client.
+ */
+ public function testXmlrpcExampleClient() {
+ // Now test the UI.
+ // Add the integers.
+ $edit = array('num1' => 3, 'num2' => 5);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Add the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 8)));
+ // Subtract the integers.
+ $edit = array('num1' => 8, 'num2' => 3);
+ $result = $this->drupalPost('examples/xmlrpc/client', $edit, t('Subtract the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 5)));
+ // Request available methods.
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Request methods'));
+ $this->assertText('xmlrpc_example.add', 'The XML-RPC Add method was found.');
+ $this->assertText('xmlrpc_example.subtract', 'The XML-RPC Subtract method was found.');
+ // Before testing multicall, verify that method exists.
+ $this->assertText('system.multicall', 'The XML-RPC Multicall method was found.');
+ // Verify multicall request.
+ $edit = array('num1' => 5, 'num2' => 2);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Add and Subtract'));
+ $this->assertText('[0] => 7', 'The XML-RPC server returned the addition result.');
+ $this->assertText('[1] => 3', 'The XML-RPC server returned the subtraction result.');
+ }
+
+ /**
+ * Perform several XML-RPC requests with different server settings.
+ */
+ public function testXmlrpcExampleServer() {
+ // Set different minimum and maxmimum valuesI.
+ $options = array('xmlrpc_example_server_min' => 3, 'xmlrpc_example_server_max' => 7);
+ $this->drupalPost('examples/xmlrpc/server', $options, t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved'), 'Results limited to >= 3 and <= 7');
+
+ $edit = array('num1' => 8, 'num2' => 3);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Subtract the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 5)));
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(3, 4)));
+ $this->assertEqual($result, 7, 'Successfully added 3+4 = 7');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(4, 3)));
+ $this->assertEqual(xmlrpc_errno(), 10002, 'subtracting 4-3 = 1 returns custom error: 10002');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(7, 4)));
+ $this->assertEqual(xmlrpc_errno(), 10001, 'Adding 7 + 4 = 11 returns custom error: 10001');
+ }
+
+ /**
+ * Test XML-RPC requests with hook_xmlrpc_alter() functionality.
+ *
+ * Perform several XML-RPC requests altering the server behaviour with
+ * hook_xmlrpc_alter API.
+ */
+ public function testXmlrpcExampleAlter() {
+ // Enable XML-RPC service altering functionality.
+ $options = array('xmlrpc_example_alter_enabled' => TRUE);
+ $this->drupalPost('examples/xmlrpc/alter', $options, t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved'), 'Results are not limited due to methods alteration');
+
+ // After altering the functionality, the add and subtract methods have no
+ // limits and should not return any error.
+ $edit = array('num1' => 80, 'num2' => 3);
+ $this->drupalPost('examples/xmlrpc/client', $edit, t('Subtract the integers'));
+ $this->assertText(t('The XML-RPC server returned this response: @num', array('@num' => 77)));
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.add' => array(30, 4)));
+ $this->assertEqual($result, 34, 'Successfully added 30+4 = 34');
+
+ $result = xmlrpc($this->xmlRpcUrl, array('xmlrpc_example.subtract' => array(4, 30)));
+ $this->assertEqual($result, -26, 'Successfully subtracted 4-30 = -26');
+ }
+}
diff --git a/themes/bartik/bartik.info b/themes/bartik/bartik.info
index 4a45034f..76336801 100755
--- a/themes/bartik/bartik.info
+++ b/themes/bartik/bartik.info
@@ -34,8 +34,8 @@ regions[footer] = Footer
settings[shortcut_module_link] = 0
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/themes/garland/garland.info b/themes/garland/garland.info
index 8328a74e..3a81d19f 100755
--- a/themes/garland/garland.info
+++ b/themes/garland/garland.info
@@ -7,8 +7,8 @@ stylesheets[all][] = style.css
stylesheets[print][] = print.css
settings[garland_width] = fluid
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/themes/seven/seven.info b/themes/seven/seven.info
index 9d1d080e..ce455f5a 100755
--- a/themes/seven/seven.info
+++ b/themes/seven/seven.info
@@ -13,8 +13,8 @@ regions[page_bottom] = Page bottom
regions[sidebar_first] = First sidebar
regions_hidden[] = sidebar_first
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"
diff --git a/themes/stark/stark.info b/themes/stark/stark.info
index 345a505a..6c1bc204 100755
--- a/themes/stark/stark.info
+++ b/themes/stark/stark.info
@@ -5,8 +5,8 @@ version = VERSION
core = 7.x
stylesheets[all][] = layout.css
-; Information added by Drupal.org packaging script on 2017-06-21
-version = "7.56"
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
project = "drupal"
-datestamp = "1498069849"
+datestamp = "1522264019"