';
+ $this->assertTrue(strpos($this->drupalGetContent(), $bbb_project_link) < strpos($this->drupalGetContent(), $ccc_project_link), "'BBB Update test' project is listed before the 'CCC Update test' project");
+ }
+
+ /**
+ * Tests that subthemes are notified about security updates for base themes.
+ */
+ function testUpdateBaseThemeSecurityUpdate() {
+ // Only enable the subtheme, not the base theme.
+ db_update('system')
+ ->fields(array('status' => 1))
+ ->condition('type', 'theme')
+ ->condition('name', 'update_test_subtheme')
+ ->execute();
+
+ // Define the initial state for core and the subtheme.
+ $system_info = array(
+ // We want core to be version 7.0.
+ '#all' => array(
+ 'version' => '7.0',
+ ),
+ // Show the update_test_basetheme
+ 'update_test_basetheme' => array(
+ 'project' => 'update_test_basetheme',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ // Show the update_test_subtheme
+ 'update_test_subtheme' => array(
+ 'project' => 'update_test_subtheme',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ );
+ variable_set('update_test_system_info', $system_info);
+ $xml_mapping = array(
+ 'drupal' => '0',
+ 'update_test_subtheme' => '1_0',
+ 'update_test_basetheme' => '1_1-sec',
+ );
+ $this->refreshUpdateStatus($xml_mapping);
+ $this->assertText(t('Security update required!'));
+ $this->assertRaw(l(t('Update test base theme'), 'http://example.com/project/update_test_basetheme'), 'Link to the Update test base theme project appears.');
+ }
+
+ /**
+ * Tests that the admin theme is always notified about security updates.
+ */
+ function testUpdateAdminThemeSecurityUpdate() {
+ // Disable the admin theme.
+ db_update('system')
+ ->fields(array('status' => 0))
+ ->condition('type', 'theme')
+ ->condition('name', 'update_test_%', 'LIKE')
+ ->execute();
+
+ variable_set('admin_theme', 'update_test_admintheme');
+
+ // Define the initial state for core and the themes.
+ $system_info = array(
+ '#all' => array(
+ 'version' => '7.0',
+ ),
+ 'update_test_admintheme' => array(
+ 'project' => 'update_test_admintheme',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ 'update_test_basetheme' => array(
+ 'project' => 'update_test_basetheme',
+ 'version' => '7.x-1.1',
+ 'hidden' => FALSE,
+ ),
+ 'update_test_subtheme' => array(
+ 'project' => 'update_test_subtheme',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ );
+ variable_set('update_test_system_info', $system_info);
+ variable_set('update_check_disabled', FALSE);
+ $xml_mapping = array(
+ // This is enough because we don't check the update status of the admin
+ // theme. We want to check that the admin theme is included in the list.
+ 'drupal' => '0',
+ );
+ $this->refreshUpdateStatus($xml_mapping);
+ // The admin theme is displayed even if it's disabled.
+ $this->assertText('update_test_admintheme', "The admin theme is checked for update even if it's disabled");
+ // The other disabled themes are not displayed.
+ $this->assertNoText('update_test_basetheme', 'Disabled theme is not checked for update in the list.');
+ $this->assertNoText('update_test_subtheme', 'Disabled theme is not checked for update in the list.');
+ }
+
+ /**
+ * Tests that disabled themes are only shown when desired.
+ */
+ function testUpdateShowDisabledThemes() {
+ // Make sure all the update_test_* themes are disabled.
+ db_update('system')
+ ->fields(array('status' => 0))
+ ->condition('type', 'theme')
+ ->condition('name', 'update_test_%', 'LIKE')
+ ->execute();
+
+ // Define the initial state for core and the test contrib themes.
+ $system_info = array(
+ // We want core to be version 7.0.
+ '#all' => array(
+ 'version' => '7.0',
+ ),
+ // The update_test_basetheme should be visible and up to date.
+ 'update_test_basetheme' => array(
+ 'project' => 'update_test_basetheme',
+ 'version' => '7.x-1.1',
+ 'hidden' => FALSE,
+ ),
+ // The update_test_subtheme should be visible and up to date.
+ 'update_test_subtheme' => array(
+ 'project' => 'update_test_subtheme',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ );
+ // When there are contributed modules in the site's file system, the
+ // total number of attempts made in the test may exceed the default value
+ // of update_max_fetch_attempts. Therefore this variable is set very high
+ // to avoid test failures in those cases.
+ variable_set('update_max_fetch_attempts', 99999);
+ variable_set('update_test_system_info', $system_info);
+ $xml_mapping = array(
+ 'drupal' => '0',
+ 'update_test_subtheme' => '1_0',
+ 'update_test_basetheme' => '1_1-sec',
+ );
+ $base_theme_project_link = l(t('Update test base theme'), 'http://example.com/project/update_test_basetheme');
+ $sub_theme_project_link = l(t('Update test subtheme'), 'http://example.com/project/update_test_subtheme');
+ foreach (array(TRUE, FALSE) as $check_disabled) {
+ variable_set('update_check_disabled', $check_disabled);
+ $this->refreshUpdateStatus($xml_mapping);
+ // In neither case should we see the "Themes" heading for enabled themes.
+ $this->assertNoText(t('Themes'));
+ if ($check_disabled) {
+ $this->assertText(t('Disabled themes'));
+ $this->assertRaw($base_theme_project_link, 'Link to the Update test base theme project appears.');
+ $this->assertRaw($sub_theme_project_link, 'Link to the Update test subtheme project appears.');
+ }
+ else {
+ $this->assertNoText(t('Disabled themes'));
+ $this->assertNoRaw($base_theme_project_link, 'Link to the Update test base theme project does not appear.');
+ $this->assertNoRaw($sub_theme_project_link, 'Link to the Update test subtheme project does not appear.');
+ }
+ }
+ }
+
+ /**
+ * Makes sure that if we fetch from a broken URL, sane things happen.
+ */
+ function testUpdateBrokenFetchURL() {
+ $system_info = array(
+ '#all' => array(
+ 'version' => '7.0',
+ ),
+ 'aaa_update_test' => array(
+ 'project' => 'aaa_update_test',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ 'bbb_update_test' => array(
+ 'project' => 'bbb_update_test',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ 'ccc_update_test' => array(
+ 'project' => 'ccc_update_test',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ );
+ variable_set('update_test_system_info', $system_info);
+
+ $xml_mapping = array(
+ 'drupal' => '0',
+ 'aaa_update_test' => '1_0',
+ 'bbb_update_test' => 'does-not-exist',
+ 'ccc_update_test' => '1_0',
+ );
+ $this->refreshUpdateStatus($xml_mapping);
+
+ $this->assertText(t('Up to date'));
+ // We're expecting the report to say most projects are up to date, so we
+ // hope that 'Up to date' is not unique.
+ $this->assertNoUniqueText(t('Up to date'));
+ // It should say we failed to get data, not that we're missing an update.
+ $this->assertNoText(t('Update available'));
+
+ // We need to check that this string is found as part of a project row,
+ // not just in the "Failed to get available update data for ..." message
+ // at the top of the page.
+ $this->assertRaw('
' . t('Failed to get available update data'));
+
+ // We should see the output messages from fetching manually.
+ $this->assertUniqueText(t('Checked available update data for 3 projects.'));
+ $this->assertUniqueText(t('Failed to get available update data for one project.'));
+
+ // The other two should be listed as projects.
+ $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.');
+ $this->assertNoRaw(l(t('BBB Update test'), 'http://example.com/project/bbb_update_test'), 'Link to bbb_update_test project does not appear.');
+ $this->assertRaw(l(t('CCC Update test'), 'http://example.com/project/ccc_update_test'), 'Link to bbb_update_test project appears.');
+ }
+
+ /**
+ * Checks that hook_update_status_alter() works to change a status.
+ *
+ * We provide the same external data as if aaa_update_test 7.x-1.0 were
+ * installed and that was the latest release. Then we use
+ * hook_update_status_alter() to try to mark this as missing a security
+ * update, then assert if we see the appropriate warnings on the right pages.
+ */
+ function testHookUpdateStatusAlter() {
+ variable_set('allow_authorize_operations', TRUE);
+ $update_admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer software updates'));
+ $this->drupalLogin($update_admin_user);
+
+ $system_info = array(
+ '#all' => array(
+ 'version' => '7.0',
+ ),
+ 'aaa_update_test' => array(
+ 'project' => 'aaa_update_test',
+ 'version' => '7.x-1.0',
+ 'hidden' => FALSE,
+ ),
+ );
+ variable_set('update_test_system_info', $system_info);
+ $update_status = array(
+ 'aaa_update_test' => array(
+ 'status' => UPDATE_NOT_SECURE,
+ ),
+ );
+ variable_set('update_test_update_status', $update_status);
+ $this->refreshUpdateStatus(
+ array(
+ 'drupal' => '0',
+ 'aaa_update_test' => '1_0',
+ )
+ );
+ $this->drupalGet('admin/reports/updates');
+ $this->assertRaw('
' . t('Modules') . ' ');
+ $this->assertText(t('Security update required!'));
+ $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.');
+
+ // Visit the reports page again without the altering and make sure the
+ // status is back to normal.
+ variable_set('update_test_update_status', array());
+ $this->drupalGet('admin/reports/updates');
+ $this->assertRaw('
' . t('Modules') . ' ');
+ $this->assertNoText(t('Security update required!'));
+ $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.');
+
+ // Turn the altering back on and visit the Update manager UI.
+ variable_set('update_test_update_status', $update_status);
+ $this->drupalGet('admin/modules/update');
+ $this->assertText(t('Security update'));
+
+ // Turn the altering back off and visit the Update manager UI.
+ variable_set('update_test_update_status', array());
+ $this->drupalGet('admin/modules/update');
+ $this->assertNoText(t('Security update'));
+ }
+
+}
+
+/**
+ * Tests project upload and extract functionality.
+ */
+class UpdateTestUploadCase extends UpdateTestHelper {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Upload and extract module functionality',
+ 'description' => 'Tests the Update Manager module\'s upload and extraction functionality.',
+ 'group' => 'Update',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp('update', 'update_test');
+ variable_set('allow_authorize_operations', TRUE);
+ $admin_user = $this->drupalCreateUser(array('administer software updates', 'administer site configuration'));
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Tests upload and extraction of a module.
+ */
+ public function testUploadModule() {
+ // Images are not valid archives, so get one and try to install it. We
+ // need an extra variable to store the result of drupalGetTestFiles()
+ // since reset() takes an argument by reference and passing in a constant
+ // emits a notice in strict mode.
+ $imageTestFiles = $this->drupalGetTestFiles('image');
+ $invalidArchiveFile = reset($imageTestFiles);
+ $edit = array(
+ 'files[project_upload]' => $invalidArchiveFile->uri,
+ );
+ // This also checks that the correct archive extensions are allowed.
+ $this->drupalPost('admin/modules/install', $edit, t('Install'));
+ $this->assertText(t('Only files with the following extensions are allowed: @archive_extensions.', array('@archive_extensions' => archiver_get_extensions())),'Only valid archives can be uploaded.');
+
+ // Check to ensure an existing module can't be reinstalled. Also checks that
+ // the archive was extracted since we can't know if the module is already
+ // installed until after extraction.
+ $validArchiveFile = drupal_get_path('module', 'update') . '/tests/aaa_update_test.tar.gz';
+ $edit = array(
+ 'files[project_upload]' => $validArchiveFile,
+ );
+ $this->drupalPost('admin/modules/install', $edit, t('Install'));
+ $this->assertText(t('@module_name is already installed.', array('@module_name' => 'AAA Update test')), 'Existing module was extracted and not reinstalled.');
+ }
+
+ /**
+ * Ensures that archiver extensions are properly merged in the UI.
+ */
+ function testFileNameExtensionMerging() {
+ $this->drupalGet('admin/modules/install');
+ // Make sure the bogus extension supported by update_test.module is there.
+ $this->assertPattern('/file extensions are supported:.*update-test-extension/', "Found 'update-test-extension' extension");
+ // Make sure it didn't clobber the first option from core.
+ $this->assertPattern('/file extensions are supported:.*tar/', "Found 'tar' extension");
+ }
+
+ /**
+ * Checks the messages on update manager pages when missing a security update.
+ */
+ function testUpdateManagerCoreSecurityUpdateMessages() {
+ $setting = array(
+ '#all' => array(
+ 'version' => '7.0',
+ ),
+ );
+ variable_set('update_test_system_info', $setting);
+ variable_set('update_fetch_url', url('update-test', array('absolute' => TRUE)));
+ variable_set('update_test_xml_map', array('drupal' => '2-sec'));
+ // Initialize the update status.
+ $this->drupalGet('admin/reports/updates');
+
+ // Now, make sure none of the Update manager pages have duplicate messages
+ // about core missing a security update.
+
+ $this->drupalGet('admin/modules/install');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+
+ $this->drupalGet('admin/modules/update');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+
+ $this->drupalGet('admin/appearance/install');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+
+ $this->drupalGet('admin/appearance/update');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+
+ $this->drupalGet('admin/reports/updates/install');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+
+ $this->drupalGet('admin/reports/updates/update');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+
+ $this->drupalGet('admin/update/ready');
+ $this->assertNoText(t('There is a security update available for your version of Drupal.'));
+ }
+
+}
+
+/**
+ * Tests update functionality unrelated to the database.
+ */
+class UpdateCoreUnitTestCase extends DrupalUnitTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => "Unit tests",
+ 'description' => 'Test update funcionality unrelated to the database.',
+ 'group' => 'Update',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('update');
+ module_load_include('inc', 'update', 'update.fetch');
+ }
+
+ /**
+ * Tests that _update_build_fetch_url() builds the URL correctly.
+ */
+ function testUpdateBuildFetchUrl() {
+ //first test that we didn't break the trivial case
+ $project['name'] = 'update_test';
+ $project['project_type'] = '';
+ $project['info']['version'] = '';
+ $project['info']['project status url'] = 'http://www.example.com';
+ $project['includes'] = array('module1' => 'Module 1', 'module2' => 'Module 2');
+ $site_key = '';
+ $expected = 'http://www.example.com/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY;
+ $url = _update_build_fetch_url($project, $site_key);
+ $this->assertEqual($url, $expected, "'$url' when no site_key provided should be '$expected'.");
+
+ //For disabled projects it shouldn't add the site key either.
+ $site_key = 'site_key';
+ $project['project_type'] = 'disabled';
+ $expected = 'http://www.example.com/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY;
+ $url = _update_build_fetch_url($project, $site_key);
+ $this->assertEqual($url, $expected, "'$url' should be '$expected' for disabled projects.");
+
+ //for enabled projects, adding the site key
+ $project['project_type'] = '';
+ $expected = 'http://www.example.com/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY;
+ $expected .= '?site_key=site_key';
+ $expected .= '&list=' . rawurlencode('module1,module2');
+ $url = _update_build_fetch_url($project, $site_key);
+ $this->assertEqual($url, $expected, "When site_key provided, '$url' should be '$expected'.");
+
+ // http://drupal.org/node/1481156 test incorrect logic when URL contains
+ // a question mark.
+ $project['info']['project status url'] = 'http://www.example.com/?project=';
+ $expected = 'http://www.example.com/?project=/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY;
+ $expected .= '&site_key=site_key';
+ $expected .= '&list=' . rawurlencode('module1,module2');
+ $url = _update_build_fetch_url($project, $site_key);
+ $this->assertEqual($url, $expected, "When ? is present, '$url' should be '$expected'.");
+
+ }
+}
diff --git a/modules/user/tests/user_form_test.info b/modules/user/tests/user_form_test.info
new file mode 100644
index 0000000..17ce34a
--- /dev/null
+++ b/modules/user/tests/user_form_test.info
@@ -0,0 +1,12 @@
+name = "User module form tests"
+description = "Support module for user form testing."
+package = Testing
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
+project = "drupal"
+datestamp = "1522264019"
+
diff --git a/modules/user/tests/user_form_test.module b/modules/user/tests/user_form_test.module
new file mode 100644
index 0000000..382bc57
--- /dev/null
+++ b/modules/user/tests/user_form_test.module
@@ -0,0 +1,82 @@
+ 'User form test for current password validation',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_form_test_current_password',1),
+ 'access arguments' => array('administer users'),
+ 'type' => MENU_SUGGESTED_ITEM,
+ );
+ return $items;
+}
+
+/**
+ * A test form for user_validate_current_pass().
+ */
+function user_form_test_current_password($form, &$form_state, $account) {
+ $account->user_form_test_field = '';
+ $form['#user'] = $account;
+
+ $form['user_form_test_field'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Test field'),
+ '#description' => t('A field that would require a correct password to change.'),
+ '#required' => TRUE,
+ );
+
+ $form['current_pass'] = array(
+ '#type' => 'password',
+ '#title' => t('Current password'),
+ '#size' => 25,
+ '#description' => t('Enter your current password'),
+ );
+
+ $form['current_pass_required_values'] = array(
+ '#type' => 'value',
+ '#value' => array('user_form_test_field' => t('Test field')),
+ );
+
+ $form['#validate'][] = 'user_validate_current_pass';
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Test'),
+ );
+ return $form;
+}
+
+/**
+ * Submit function for the test form for user_validate_current_pass().
+ */
+function user_form_test_current_password_submit($form, &$form_state) {
+ drupal_set_message(t('The password has been validated and the form submitted successfully.'));
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function user_form_test_form_user_profile_form_alter(&$form, &$form_state) {
+ if (variable_get('user_form_test_user_profile_form_rebuild', FALSE)) {
+ $form['#submit'][] = 'user_form_test_user_account_submit';
+ }
+}
+
+/**
+ * Submit function for user_profile_form().
+ */
+function user_form_test_user_account_submit($form, &$form_state) {
+ // Rebuild the form instead of letting the process end. This allows us to
+ // test for bugs that can be triggered in contributed modules.
+ $form_state['rebuild'] = TRUE;
+}
diff --git a/modules/user/user-picture.tpl.php b/modules/user/user-picture.tpl.php
new file mode 100644
index 0000000..11d92cc
--- /dev/null
+++ b/modules/user/user-picture.tpl.php
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/modules/user/user-profile-category.tpl.php b/modules/user/user-profile-category.tpl.php
new file mode 100644
index 0000000..0a86c76
--- /dev/null
+++ b/modules/user/user-profile-category.tpl.php
@@ -0,0 +1,33 @@
+
+
+
+
+
+
>
+
+
diff --git a/modules/user/user-profile-item.tpl.php b/modules/user/user-profile-item.tpl.php
new file mode 100644
index 0000000..042d43a
--- /dev/null
+++ b/modules/user/user-profile-item.tpl.php
@@ -0,0 +1,26 @@
+
+
>
+
>
diff --git a/modules/user/user-profile.tpl.php b/modules/user/user-profile.tpl.php
new file mode 100644
index 0000000..0a64fed
--- /dev/null
+++ b/modules/user/user-profile.tpl.php
@@ -0,0 +1,39 @@
+field_example has a
+ * variable $field_example defined. When needing to access a field's raw
+ * values, developers/themers are strongly encouraged to use these
+ * variables. Otherwise they will have to explicitly specify the desired
+ * field language, e.g. $account->field_example['en'], thus overriding any
+ * language negotiation rule that was previously applied.
+ *
+ * @see user-profile-category.tpl.php
+ * Where the html is handled for the group.
+ * @see user-profile-item.tpl.php
+ * Where the html is handled for each item in the group.
+ * @see template_preprocess_user_profile()
+ *
+ * @ingroup themeable
+ */
+?>
+
>
+
+
diff --git a/modules/user/user-rtl.css b/modules/user/user-rtl.css
new file mode 100644
index 0000000..642c943
--- /dev/null
+++ b/modules/user/user-rtl.css
@@ -0,0 +1,34 @@
+
+#permissions td.permission {
+ padding-left: 0;
+ padding-right: 1.5em;
+}
+
+#user-admin-roles .form-item-name {
+ float: right;
+ margin-left: 1em;
+ margin-right: 0;
+}
+
+/**
+ * Password strength indicator.
+ */
+.password-strength {
+ float: left;
+}
+.password-strength-text {
+ float: left;
+}
+div.password-confirm {
+ float: left;
+}
+.confirm-parent,
+.password-parent {
+ clear: right;
+}
+
+/* Generated by user.module but used by profile.module: */
+.profile .user-picture {
+ float: left;
+ margin: 0 0 1em 1em;
+}
diff --git a/modules/user/user.admin.inc b/modules/user/user.admin.inc
new file mode 100644
index 0000000..6ca330b
--- /dev/null
+++ b/modules/user/user.admin.inc
@@ -0,0 +1,1053 @@
+ 'fieldset',
+ '#title' => t('Show only users where'),
+ '#theme' => 'exposed_filters__user',
+ );
+ foreach ($session as $filter) {
+ list($type, $value) = $filter;
+ if ($type == 'permission') {
+ // Merge arrays of module permissions into one.
+ // Slice past the first element '[any]' whose value is not an array.
+ $options = call_user_func_array('array_merge', array_slice($filters[$type]['options'], 1));
+ $value = $options[$value];
+ }
+ else {
+ $value = $filters[$type]['options'][$value];
+ }
+ $t_args = array('%property' => $filters[$type]['title'], '%value' => $value);
+ if ($i++) {
+ $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args));
+ }
+ else {
+ $form['filters']['current'][] = array('#markup' => t('%property is %value', $t_args));
+ }
+ }
+
+ $form['filters']['status'] = array(
+ '#type' => 'container',
+ '#attributes' => array('class' => array('clearfix')),
+ '#prefix' => ($i ? '
' . t('and where') . '
' : ''),
+ );
+ $form['filters']['status']['filters'] = array(
+ '#type' => 'container',
+ '#attributes' => array('class' => array('filters')),
+ );
+ foreach ($filters as $key => $filter) {
+ $form['filters']['status']['filters'][$key] = array(
+ '#type' => 'select',
+ '#options' => $filter['options'],
+ '#title' => $filter['title'],
+ '#default_value' => '[any]',
+ );
+ }
+
+ $form['filters']['status']['actions'] = array(
+ '#type' => 'actions',
+ '#attributes' => array('class' => array('container-inline')),
+ );
+ $form['filters']['status']['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => (count($session) ? t('Refine') : t('Filter')),
+ );
+ if (count($session)) {
+ $form['filters']['status']['actions']['undo'] = array(
+ '#type' => 'submit',
+ '#value' => t('Undo'),
+ );
+ $form['filters']['status']['actions']['reset'] = array(
+ '#type' => 'submit',
+ '#value' => t('Reset'),
+ );
+ }
+
+ drupal_add_library('system', 'drupal.form');
+
+ return $form;
+}
+
+/**
+ * Process result from user administration filter form.
+ */
+function user_filter_form_submit($form, &$form_state) {
+ $op = $form_state['values']['op'];
+ $filters = user_filters();
+ switch ($op) {
+ case t('Filter'):
+ case t('Refine'):
+ // Apply every filter that has a choice selected other than 'any'.
+ foreach ($filters as $filter => $options) {
+ if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') {
+ // Merge an array of arrays into one if necessary.
+ $options = ($filter == 'permission') ? form_options_flatten($filters[$filter]['options']) : $filters[$filter]['options'];
+ // Only accept valid selections offered on the dropdown, block bad input.
+ if (isset($options[$form_state['values'][$filter]])) {
+ $_SESSION['user_overview_filter'][] = array($filter, $form_state['values'][$filter]);
+ }
+ }
+ }
+ break;
+ case t('Undo'):
+ array_pop($_SESSION['user_overview_filter']);
+ break;
+ case t('Reset'):
+ $_SESSION['user_overview_filter'] = array();
+ break;
+ case t('Update'):
+ return;
+ }
+
+ $form_state['redirect'] = 'admin/people';
+ return;
+}
+
+/**
+ * Form builder; User administration page.
+ *
+ * @ingroup forms
+ * @see user_admin_account_validate()
+ * @see user_admin_account_submit()
+ */
+function user_admin_account() {
+
+ $header = array(
+ 'username' => array('data' => t('Username'), 'field' => 'u.name'),
+ 'status' => array('data' => t('Status'), 'field' => 'u.status'),
+ 'roles' => array('data' => t('Roles')),
+ 'member_for' => array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc'),
+ 'access' => array('data' => t('Last access'), 'field' => 'u.access'),
+ 'operations' => array('data' => t('Operations')),
+ );
+
+ $query = db_select('users', 'u');
+ $query->condition('u.uid', 0, '<>');
+ user_build_filter_query($query);
+
+ $count_query = clone $query;
+ $count_query->addExpression('COUNT(u.uid)');
+
+ $query = $query->extend('PagerDefault')->extend('TableSort');
+ $query
+ ->fields('u', array('uid', 'name', 'status', 'created', 'access'))
+ ->limit(50)
+ ->orderByHeader($header)
+ ->setCountQuery($count_query);
+ $result = $query->execute();
+
+ $form['options'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Update options'),
+ '#attributes' => array('class' => array('container-inline')),
+ );
+ $options = array();
+ foreach (module_invoke_all('user_operations') as $operation => $array) {
+ $options[$operation] = $array['label'];
+ }
+ $form['options']['operation'] = array(
+ '#type' => 'select',
+ '#title' => t('Operation'),
+ '#title_display' => 'invisible',
+ '#options' => $options,
+ '#default_value' => 'unblock',
+ );
+ $options = array();
+ $form['options']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Update'),
+ );
+
+ $destination = drupal_get_destination();
+
+ $status = array(t('blocked'), t('active'));
+ $roles = array_map('check_plain', user_roles(TRUE));
+ $accounts = array();
+ foreach ($result as $account) {
+ $users_roles = array();
+ $roles_result = db_query('SELECT rid FROM {users_roles} WHERE uid = :uid', array(':uid' => $account->uid));
+ foreach ($roles_result as $user_role) {
+ $users_roles[] = $roles[$user_role->rid];
+ }
+ asort($users_roles);
+
+ $options[$account->uid] = array(
+ 'username' => theme('username', array('account' => $account)),
+ 'status' => $status[$account->status],
+ 'roles' => theme('item_list', array('items' => $users_roles)),
+ 'member_for' => format_interval(REQUEST_TIME - $account->created),
+ 'access' => $account->access ? t('@time ago', array('@time' => format_interval(REQUEST_TIME - $account->access))) : t('never'),
+ 'operations' => array('data' => array('#type' => 'link', '#title' => t('edit'), '#href' => "user/$account->uid/edit", '#options' => array('query' => $destination))),
+ );
+ }
+
+ $form['accounts'] = array(
+ '#type' => 'tableselect',
+ '#header' => $header,
+ '#options' => $options,
+ '#empty' => t('No people available.'),
+ );
+ $form['pager'] = array('#markup' => theme('pager'));
+
+ return $form;
+}
+
+/**
+ * Submit the user administration update form.
+ */
+function user_admin_account_submit($form, &$form_state) {
+ $operations = module_invoke_all('user_operations', $form, $form_state);
+ $operation = $operations[$form_state['values']['operation']];
+ // Filter out unchecked accounts.
+ $accounts = array_filter($form_state['values']['accounts']);
+ if ($function = $operation['callback']) {
+ // Add in callback arguments if present.
+ if (isset($operation['callback arguments'])) {
+ $args = array_merge(array($accounts), $operation['callback arguments']);
+ }
+ else {
+ $args = array($accounts);
+ }
+ call_user_func_array($function, $args);
+
+ drupal_set_message(t('The update has been performed.'));
+ }
+}
+
+function user_admin_account_validate($form, &$form_state) {
+ $form_state['values']['accounts'] = array_filter($form_state['values']['accounts']);
+ if (count($form_state['values']['accounts']) == 0) {
+ form_set_error('', t('No users selected.'));
+ }
+}
+
+/**
+ * Form builder; Configure user settings for this site.
+ *
+ * @ingroup forms
+ * @see system_settings_form()
+ */
+function user_admin_settings() {
+ // Settings for anonymous users.
+ $form['anonymous_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Anonymous users'),
+ );
+ $form['anonymous_settings']['anonymous'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#default_value' => variable_get('anonymous', t('Anonymous')),
+ '#description' => t('The name used to indicate anonymous users.'),
+ '#required' => TRUE,
+ );
+
+ // Administrative role option.
+ $form['admin_role'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Administrator role'),
+ );
+
+ // Do not allow users to set the anonymous or authenticated user roles as the
+ // administrator role.
+ $roles = user_roles();
+ unset($roles[DRUPAL_ANONYMOUS_RID]);
+ unset($roles[DRUPAL_AUTHENTICATED_RID]);
+ $roles[0] = t('disabled');
+
+ $form['admin_role']['user_admin_role'] = array(
+ '#type' => 'select',
+ '#title' => t('Administrator role'),
+ '#default_value' => variable_get('user_admin_role', 0),
+ '#options' => $roles,
+ '#description' => t('This role will be automatically assigned new permissions whenever a module is enabled. Changing this setting will not affect existing permissions.'),
+ );
+
+ // User registration settings.
+ $form['registration_cancellation'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Registration and cancellation'),
+ );
+ $form['registration_cancellation']['user_register'] = array(
+ '#type' => 'radios',
+ '#title' => t('Who can register accounts?'),
+ '#default_value' => variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL),
+ '#options' => array(
+ USER_REGISTER_ADMINISTRATORS_ONLY => t('Administrators only'),
+ USER_REGISTER_VISITORS => t('Visitors'),
+ USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL => t('Visitors, but administrator approval is required'),
+ )
+ );
+ $form['registration_cancellation']['user_email_verification'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require e-mail verification when a visitor creates an account.'),
+ '#default_value' => variable_get('user_email_verification', TRUE),
+ '#description' => t('New users will be required to validate their e-mail address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.')
+ );
+ module_load_include('inc', 'user', 'user.pages');
+ $form['registration_cancellation']['user_cancel_method'] = array(
+ '#type' => 'item',
+ '#title' => t('When cancelling a user account'),
+ '#description' => t('Users with the %select-cancel-method or %administer-users
permissions can override this default method.', array('%select-cancel-method' => t('Select method for cancelling account'), '%administer-users' => t('Administer users'), '@permissions-url' => url('admin/people/permissions'))),
+ );
+ $form['registration_cancellation']['user_cancel_method'] += user_cancel_methods();
+ foreach (element_children($form['registration_cancellation']['user_cancel_method']) as $element) {
+ // Remove all account cancellation methods that have #access defined, as
+ // those cannot be configured as default method.
+ if (isset($form['registration_cancellation']['user_cancel_method'][$element]['#access'])) {
+ $form['registration_cancellation']['user_cancel_method'][$element]['#access'] = FALSE;
+ }
+ // Remove the description (only displayed on the confirmation form).
+ else {
+ unset($form['registration_cancellation']['user_cancel_method'][$element]['#description']);
+ }
+ }
+
+ // Account settings.
+ $form['personalization'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Personalization'),
+ );
+ $form['personalization']['user_signatures'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable signatures.'),
+ '#default_value' => variable_get('user_signatures', 0),
+ );
+ // If picture support is enabled, check whether the picture directory exists.
+ if (variable_get('user_pictures', 0)) {
+ $picture_path = file_default_scheme() . '://' . variable_get('user_picture_path', 'pictures');
+ if (!file_prepare_directory($picture_path, FILE_CREATE_DIRECTORY)) {
+ form_set_error('user_picture_path', t('The directory %directory does not exist or is not writable.', array('%directory' => $picture_path)));
+ watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $picture_path), WATCHDOG_ERROR);
+ }
+ }
+ $picture_support = variable_get('user_pictures', 0);
+ $form['personalization']['user_pictures'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable user pictures.'),
+ '#default_value' => $picture_support,
+ );
+ drupal_add_js(drupal_get_path('module', 'user') . '/user.js');
+ $form['personalization']['pictures'] = array(
+ '#type' => 'container',
+ '#states' => array(
+ // Hide the additional picture settings when user pictures are disabled.
+ 'invisible' => array(
+ 'input[name="user_pictures"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+ $form['personalization']['pictures']['user_picture_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Picture directory'),
+ '#default_value' => variable_get('user_picture_path', 'pictures'),
+ '#size' => 30,
+ '#maxlength' => 255,
+ '#description' => t('Subdirectory in the file upload directory where pictures will be stored.'),
+ );
+ $form['personalization']['pictures']['user_picture_default'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default picture'),
+ '#default_value' => variable_get('user_picture_default', ''),
+ '#size' => 30,
+ '#maxlength' => 255,
+ '#description' => t('URL of picture to display for users with no custom picture selected. Leave blank for none.'),
+ );
+ if (module_exists('image')) {
+ $form['personalization']['pictures']['settings']['user_picture_style'] = array(
+ '#type' => 'select',
+ '#title' => t('Picture display style'),
+ '#options' => image_style_options(TRUE, PASS_THROUGH),
+ '#default_value' => variable_get('user_picture_style', ''),
+ '#description' => t('The style selected will be used on display, while the original image is retained. Styles may be configured in the
Image styles administration area.', array('!url' => url('admin/config/media/image-styles'))),
+ );
+ }
+ $form['personalization']['pictures']['user_picture_dimensions'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Picture upload dimensions'),
+ '#default_value' => variable_get('user_picture_dimensions', '85x85'),
+ '#size' => 10,
+ '#maxlength' => 10,
+ '#field_suffix' => ' ' . t('pixels'),
+ '#description' => t('Pictures larger than this will be scaled down to this size.'),
+ );
+ $form['personalization']['pictures']['user_picture_file_size'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Picture upload file size'),
+ '#default_value' => variable_get('user_picture_file_size', '30'),
+ '#size' => 10,
+ '#maxlength' => 10,
+ '#field_suffix' => ' ' . t('KB'),
+ '#description' => t('Maximum allowed file size for uploaded pictures. Upload size is normally limited only by the PHP maximum post and file upload settings, and images are automatically scaled down to the dimensions specified above.'),
+ '#element_validate' => array('element_validate_integer_positive'),
+ );
+ $form['personalization']['pictures']['user_picture_guidelines'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Picture guidelines'),
+ '#default_value' => variable_get('user_picture_guidelines', ''),
+ '#description' => t("This text is displayed at the picture upload form in addition to the default guidelines. It's useful for helping or instructing your users."),
+ );
+
+ $form['email_title'] = array(
+ '#type' => 'item',
+ '#title' => t('E-mails'),
+ );
+ $form['email'] = array(
+ '#type' => 'vertical_tabs',
+ );
+ // These email tokens are shared for all settings, so just define
+ // the list once to help ensure they stay in sync.
+ $email_token_help = t('Available variables are: [site:name], [site:url], [user:name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
+
+ $form['email_admin_created'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Welcome (new user created by administrator)'),
+ '#collapsible' => TRUE,
+ '#collapsed' => (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) != USER_REGISTER_ADMINISTRATORS_ONLY),
+ '#description' => t('Edit the welcome e-mail messages sent to new member accounts created by an administrator.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_admin_created']['user_mail_register_admin_created_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('register_admin_created_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_admin_created']['user_mail_register_admin_created_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('register_admin_created_body', NULL, array(), FALSE),
+ '#rows' => 15,
+ );
+
+ $form['email_pending_approval'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Welcome (awaiting approval)'),
+ '#collapsible' => TRUE,
+ '#collapsed' => (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) != USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL),
+ '#description' => t('Edit the welcome e-mail messages sent to new members upon registering, when administrative approval is required.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_pending_approval']['user_mail_register_pending_approval_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('register_pending_approval_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_pending_approval']['user_mail_register_pending_approval_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('register_pending_approval_body', NULL, array(), FALSE),
+ '#rows' => 8,
+ );
+
+ $form['email_no_approval_required'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Welcome (no approval required)'),
+ '#collapsible' => TRUE,
+ '#collapsed' => (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) != USER_REGISTER_VISITORS),
+ '#description' => t('Edit the welcome e-mail messages sent to new members upon registering, when no administrator approval is required.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_no_approval_required']['user_mail_register_no_approval_required_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('register_no_approval_required_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_no_approval_required']['user_mail_register_no_approval_required_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('register_no_approval_required_body', NULL, array(), FALSE),
+ '#rows' => 15,
+ );
+
+ $form['email_password_reset'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Password recovery'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Edit the e-mail messages sent to users who request a new password.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ '#weight' => 10,
+ );
+ $form['email_password_reset']['user_mail_password_reset_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('password_reset_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_password_reset']['user_mail_password_reset_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('password_reset_body', NULL, array(), FALSE),
+ '#rows' => 12,
+ );
+
+ $form['email_activated'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Account activation'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Enable and edit e-mail messages sent to users upon account activation (when an administrator activates an account of a user who has already registered, on a site where administrative approval is required).') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_activated']['user_mail_status_activated_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is activated.'),
+ '#default_value' => variable_get('user_mail_status_activated_notify', TRUE),
+ );
+ $form['email_activated']['settings'] = array(
+ '#type' => 'container',
+ '#states' => array(
+ // Hide the additional settings when this email is disabled.
+ 'invisible' => array(
+ 'input[name="user_mail_status_activated_notify"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+ $form['email_activated']['settings']['user_mail_status_activated_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('status_activated_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_activated']['settings']['user_mail_status_activated_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('status_activated_body', NULL, array(), FALSE),
+ '#rows' => 15,
+ );
+
+ $form['email_blocked'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Account blocked'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Enable and edit e-mail messages sent to users when their accounts are blocked.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_blocked']['user_mail_status_blocked_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is blocked.'),
+ '#default_value' => variable_get('user_mail_status_blocked_notify', FALSE),
+ );
+ $form['email_blocked']['settings'] = array(
+ '#type' => 'container',
+ '#states' => array(
+ // Hide the additional settings when the blocked email is disabled.
+ 'invisible' => array(
+ 'input[name="user_mail_status_blocked_notify"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+ $form['email_blocked']['settings']['user_mail_status_blocked_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('status_blocked_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_blocked']['settings']['user_mail_status_blocked_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('status_blocked_body', NULL, array(), FALSE),
+ '#rows' => 3,
+ );
+
+ $form['email_cancel_confirm'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Account cancellation confirmation'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Edit the e-mail messages sent to users when they attempt to cancel their accounts.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_cancel_confirm']['user_mail_cancel_confirm_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('cancel_confirm_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_cancel_confirm']['user_mail_cancel_confirm_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('cancel_confirm_body', NULL, array(), FALSE),
+ '#rows' => 3,
+ );
+
+ $form['email_canceled'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Account canceled'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Enable and edit e-mail messages sent to users when their accounts are canceled.') . ' ' . $email_token_help,
+ '#group' => 'email',
+ );
+ $form['email_canceled']['user_mail_status_canceled_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is canceled.'),
+ '#default_value' => variable_get('user_mail_status_canceled_notify', FALSE),
+ );
+ $form['email_canceled']['settings'] = array(
+ '#type' => 'container',
+ '#states' => array(
+ // Hide the settings when the cancel notify checkbox is disabled.
+ 'invisible' => array(
+ 'input[name="user_mail_status_canceled_notify"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+ $form['email_canceled']['settings']['user_mail_status_canceled_subject'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#default_value' => _user_mail_text('status_canceled_subject', NULL, array(), FALSE),
+ '#maxlength' => 180,
+ );
+ $form['email_canceled']['settings']['user_mail_status_canceled_body'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Body'),
+ '#default_value' => _user_mail_text('status_canceled_body', NULL, array(), FALSE),
+ '#rows' => 3,
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Menu callback: administer permissions.
+ *
+ * @ingroup forms
+ * @see user_admin_permissions_submit()
+ * @see theme_user_admin_permissions()
+ */
+function user_admin_permissions($form, $form_state, $rid = NULL) {
+
+ // Retrieve role names for columns.
+ $role_names = user_roles();
+ if (is_numeric($rid)) {
+ $role_names = array($rid => $role_names[$rid]);
+ }
+ // Fetch permissions for all roles or the one selected role.
+ $role_permissions = user_role_permissions($role_names);
+
+ // Store $role_names for use when saving the data.
+ $form['role_names'] = array(
+ '#type' => 'value',
+ '#value' => $role_names,
+ );
+ // Render role/permission overview:
+ $options = array();
+ $module_info = system_get_info('module');
+ $hide_descriptions = system_admin_compact_mode();
+
+ // Get a list of all the modules implementing a hook_permission() and sort by
+ // display name.
+ $modules = array();
+ foreach (module_implements('permission') as $module) {
+ $modules[$module] = $module_info[$module]['name'];
+ }
+ asort($modules);
+
+ foreach ($modules as $module => $display_name) {
+ if ($permissions = module_invoke($module, 'permission')) {
+ $form['permission'][] = array(
+ '#markup' => $module_info[$module]['name'],
+ '#id' => $module,
+ );
+ foreach ($permissions as $perm => $perm_item) {
+ // Fill in default values for the permission.
+ $perm_item += array(
+ 'description' => '',
+ 'restrict access' => FALSE,
+ 'warning' => !empty($perm_item['restrict access']) ? t('Warning: Give to trusted roles only; this permission has security implications.') : '',
+ );
+ $options[$perm] = '';
+ $form['permission'][$perm] = array(
+ '#type' => 'item',
+ '#markup' => $perm_item['title'],
+ '#description' => theme('user_permission_description', array('permission_item' => $perm_item, 'hide' => $hide_descriptions)),
+ );
+ foreach ($role_names as $rid => $name) {
+ // Builds arrays for checked boxes for each role
+ if (isset($role_permissions[$rid][$perm])) {
+ $status[$rid][] = $perm;
+ }
+ }
+ }
+ }
+ }
+
+ // Have to build checkboxes here after checkbox arrays are built
+ foreach ($role_names as $rid => $name) {
+ $form['checkboxes'][$rid] = array(
+ '#type' => 'checkboxes',
+ '#options' => $options,
+ '#default_value' => isset($status[$rid]) ? $status[$rid] : array(),
+ '#attributes' => array('class' => array('rid-' . $rid)),
+ );
+ $form['role_names'][$rid] = array('#markup' => check_plain($name), '#tree' => TRUE);
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save permissions'));
+
+ $form['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.permissions.js';
+
+ return $form;
+}
+
+/**
+ * Save permissions selected on the administer permissions page.
+ *
+ * @see user_admin_permissions()
+ */
+function user_admin_permissions_submit($form, &$form_state) {
+ foreach ($form_state['values']['role_names'] as $rid => $name) {
+ user_role_change_permissions($rid, $form_state['values'][$rid]);
+ }
+
+ drupal_set_message(t('The changes have been saved.'));
+
+ // Clear the cached pages and blocks.
+ cache_clear_all();
+}
+
+/**
+ * Returns HTML for the administer permissions page.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_user_admin_permissions($variables) {
+ $form = $variables['form'];
+
+ $roles = user_roles();
+ foreach (element_children($form['permission']) as $key) {
+ $row = array();
+ // Module name
+ if (is_numeric($key)) {
+ $row[] = array('data' => drupal_render($form['permission'][$key]), 'class' => array('module'), 'id' => 'module-' . $form['permission'][$key]['#id'], 'colspan' => count($form['role_names']['#value']) + 1);
+ }
+ else {
+ // Permission row.
+ $row[] = array(
+ 'data' => drupal_render($form['permission'][$key]),
+ 'class' => array('permission'),
+ );
+ foreach (element_children($form['checkboxes']) as $rid) {
+ $form['checkboxes'][$rid][$key]['#title'] = $roles[$rid] . ': ' . $form['permission'][$key]['#markup'];
+ $form['checkboxes'][$rid][$key]['#title_display'] = 'invisible';
+ $row[] = array('data' => drupal_render($form['checkboxes'][$rid][$key]), 'class' => array('checkbox'));
+ }
+ }
+ $rows[] = $row;
+ }
+ $header[] = (t('Permission'));
+ foreach (element_children($form['role_names']) as $rid) {
+ $header[] = array('data' => drupal_render($form['role_names'][$rid]), 'class' => array('checkbox'));
+ }
+ $output = theme('system_compact_link');
+ $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'permissions')));
+ $output .= drupal_render_children($form);
+ return $output;
+}
+
+/**
+ * Returns HTML for an individual permission description.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - permission_item: An associative array representing the permission whose
+ * description is being themed. Useful keys include:
+ * - description: The text of the permission description.
+ * - warning: A security-related warning message about the permission (if
+ * there is one).
+ * - hide: A boolean indicating whether or not the permission description was
+ * requested to be hidden rather than shown.
+ *
+ * @ingroup themeable
+ */
+function theme_user_permission_description($variables) {
+ if (!$variables['hide']) {
+ $description = array();
+ $permission_item = $variables['permission_item'];
+ if (!empty($permission_item['description'])) {
+ $description[] = $permission_item['description'];
+ }
+ if (!empty($permission_item['warning'])) {
+ $description[] = '
' . $permission_item['warning'] . ' ';
+ }
+ if (!empty($description)) {
+ return implode(' ', $description);
+ }
+ }
+}
+
+/**
+ * Form to re-order roles or add a new one.
+ *
+ * @ingroup forms
+ * @see theme_user_admin_roles()
+ */
+function user_admin_roles($form, $form_state) {
+ $roles = user_roles();
+
+ $form['roles'] = array(
+ '#tree' => TRUE,
+ );
+ $order = 0;
+ foreach ($roles as $rid => $name) {
+ $form['roles'][$rid]['#role'] = (object) array(
+ 'rid' => $rid,
+ 'name' => $name,
+ 'weight' => $order,
+ );
+ $form['roles'][$rid]['#weight'] = $order;
+ $form['roles'][$rid]['weight'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Weight for @title', array('@title' => $name)),
+ '#title_display' => 'invisible',
+ '#size' => 4,
+ '#default_value' => $order,
+ '#attributes' => array('class' => array('role-weight')),
+ );
+ $order++;
+ }
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#title_display' => 'invisible',
+ '#size' => 32,
+ '#maxlength' => 64,
+ );
+ $form['add'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add role'),
+ '#validate' => array('user_admin_role_validate'),
+ '#submit' => array('user_admin_role_submit'),
+ );
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save order'),
+ '#submit' => array('user_admin_roles_order_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Form submit function. Update the role weights.
+ */
+function user_admin_roles_order_submit($form, &$form_state) {
+ foreach ($form_state['values']['roles'] as $rid => $role_values) {
+ $role = $form['roles'][$rid]['#role'];
+ $role->weight = $role_values['weight'];
+ user_role_save($role);
+ }
+ drupal_set_message(t('The role settings have been updated.'));
+}
+
+/**
+ * Returns HTML for the role order and new role form.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_user_admin_roles($variables) {
+ $form = $variables['form'];
+
+ $header = array(t('Name'), t('Weight'), array('data' => t('Operations'), 'colspan' => 2));
+ foreach (element_children($form['roles']) as $rid) {
+ $name = $form['roles'][$rid]['#role']->name;
+ $row = array();
+ if (in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+ $row[] = t('@name
(locked) ', array('@name' => $name));
+ $row[] = drupal_render($form['roles'][$rid]['weight']);
+ $row[] = '';
+ $row[] = l(t('edit permissions'), 'admin/people/permissions/' . $rid);
+ }
+ else {
+ $row[] = check_plain($name);
+ $row[] = drupal_render($form['roles'][$rid]['weight']);
+ $row[] = l(t('edit role'), 'admin/people/permissions/roles/edit/' . $rid);
+ $row[] = l(t('edit permissions'), 'admin/people/permissions/' . $rid);
+ }
+ $rows[] = array('data' => $row, 'class' => array('draggable'));
+ }
+ $rows[] = array(array('data' => drupal_render($form['name']) . drupal_render($form['add']), 'colspan' => 4, 'class' => 'edit-name'));
+
+ drupal_add_tabledrag('user-roles', 'order', 'sibling', 'role-weight');
+
+ $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'user-roles')));
+ $output .= drupal_render_children($form);
+
+ return $output;
+}
+
+/**
+ * Form to configure a single role.
+ *
+ * @ingroup forms
+ * @see user_admin_role_validate()
+ * @see user_admin_role_submit()
+ */
+function user_admin_role($form, $form_state, $role) {
+ if ($role->rid == DRUPAL_ANONYMOUS_RID || $role->rid == DRUPAL_AUTHENTICATED_RID) {
+ drupal_goto('admin/people/permissions/roles');
+ }
+
+ // Display the edit role form.
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Role name'),
+ '#default_value' => $role->name,
+ '#size' => 30,
+ '#required' => TRUE,
+ '#maxlength' => 64,
+ '#description' => t('The name for this role. Example: "moderator", "editorial board", "site architect".'),
+ );
+ $form['rid'] = array(
+ '#type' => 'value',
+ '#value' => $role->rid,
+ );
+ $form['weight'] = array(
+ '#type' => 'value',
+ '#value' => $role->weight,
+ );
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save role'),
+ );
+ $form['actions']['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete role'),
+ '#submit' => array('user_admin_role_delete_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Form validation handler for the user_admin_role() form.
+ */
+function user_admin_role_validate($form, &$form_state) {
+ if (!empty($form_state['values']['name'])) {
+ if ($form_state['values']['op'] == t('Save role')) {
+ $role = user_role_load_by_name($form_state['values']['name']);
+ if ($role && $role->rid != $form_state['values']['rid']) {
+ form_set_error('name', t('The role name %name already exists. Choose another role name.', array('%name' => $form_state['values']['name'])));
+ }
+ }
+ elseif ($form_state['values']['op'] == t('Add role')) {
+ if (user_role_load_by_name($form_state['values']['name'])) {
+ form_set_error('name', t('The role name %name already exists. Choose another role name.', array('%name' => $form_state['values']['name'])));
+ }
+ }
+ }
+ else {
+ form_set_error('name', t('You must specify a valid role name.'));
+ }
+}
+
+/**
+ * Form submit handler for the user_admin_role() form.
+ */
+function user_admin_role_submit($form, &$form_state) {
+ $role = (object) $form_state['values'];
+ if ($form_state['values']['op'] == t('Save role')) {
+ user_role_save($role);
+ drupal_set_message(t('The role has been renamed.'));
+ }
+ elseif ($form_state['values']['op'] == t('Add role')) {
+ user_role_save($role);
+ drupal_set_message(t('The role has been added.'));
+ }
+ $form_state['redirect'] = 'admin/people/permissions/roles';
+ return;
+}
+
+/**
+ * Form submit handler for the user_admin_role() form.
+ */
+function user_admin_role_delete_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/people/permissions/roles/delete/' . $form_state['values']['rid'];
+}
+
+/**
+ * Form to confirm role delete operation.
+ */
+function user_admin_role_delete_confirm($form, &$form_state, $role) {
+ $form['rid'] = array(
+ '#type' => 'value',
+ '#value' => $role->rid,
+ );
+ return confirm_form($form, t('Are you sure you want to delete the role %name ?', array('%name' => $role->name)), 'admin/people/permissions/roles', t('This action cannot be undone.'), t('Delete'));
+}
+
+/**
+ * Form submit handler for user_admin_role_delete_confirm().
+ */
+function user_admin_role_delete_confirm_submit($form, &$form_state) {
+ user_role_delete((int) $form_state['values']['rid']);
+ drupal_set_message(t('The role has been deleted.'));
+ $form_state['redirect'] = 'admin/people/permissions/roles';
+}
+
diff --git a/modules/user/user.api.php b/modules/user/user.api.php
new file mode 100644
index 0000000..f205a85
--- /dev/null
+++ b/modules/user/user.api.php
@@ -0,0 +1,477 @@
+ array_keys($users)));
+ foreach ($result as $record) {
+ $users[$record->uid]->foo = $record->foo;
+ }
+}
+
+/**
+ * Respond to user deletion.
+ *
+ * This hook is invoked from user_delete_multiple() before field_attach_delete()
+ * is called and before users are actually removed from the database.
+ *
+ * Modules should additionally implement hook_user_cancel() to process stored
+ * user data for other account cancellation methods.
+ *
+ * @param $account
+ * The account that is being deleted.
+ *
+ * @see user_delete_multiple()
+ */
+function hook_user_delete($account) {
+ db_delete('mytable')
+ ->condition('uid', $account->uid)
+ ->execute();
+}
+
+/**
+ * Act on user account cancellations.
+ *
+ * This hook is invoked from user_cancel() before a user account is canceled.
+ * Depending on the account cancellation method, the module should either do
+ * nothing, unpublish content, or anonymize content. See user_cancel_methods()
+ * for the list of default account cancellation methods provided by User module.
+ * Modules may add further methods via hook_user_cancel_methods_alter().
+ *
+ * This hook is NOT invoked for the 'user_cancel_delete' account cancellation
+ * method. To react on this method, implement hook_user_delete() instead.
+ *
+ * Expensive operations should be added to the global account cancellation batch
+ * by using batch_set().
+ *
+ * @param $edit
+ * The array of form values submitted by the user.
+ * @param $account
+ * The user object on which the operation is being performed.
+ * @param $method
+ * The account cancellation method.
+ *
+ * @see user_cancel_methods()
+ * @see hook_user_cancel_methods_alter()
+ */
+function hook_user_cancel($edit, $account, $method) {
+ switch ($method) {
+ case 'user_cancel_block_unpublish':
+ // Unpublish nodes (current revisions).
+ module_load_include('inc', 'node', 'node.admin');
+ $nodes = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->condition('uid', $account->uid)
+ ->execute()
+ ->fetchCol();
+ node_mass_update($nodes, array('status' => 0));
+ break;
+
+ case 'user_cancel_reassign':
+ // Anonymize nodes (current revisions).
+ module_load_include('inc', 'node', 'node.admin');
+ $nodes = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->condition('uid', $account->uid)
+ ->execute()
+ ->fetchCol();
+ node_mass_update($nodes, array('uid' => 0));
+ // Anonymize old revisions.
+ db_update('node_revision')
+ ->fields(array('uid' => 0))
+ ->condition('uid', $account->uid)
+ ->execute();
+ // Clean history.
+ db_delete('history')
+ ->condition('uid', $account->uid)
+ ->execute();
+ break;
+ }
+}
+
+/**
+ * Modify account cancellation methods.
+ *
+ * By implementing this hook, modules are able to add, customize, or remove
+ * account cancellation methods. All defined methods are turned into radio
+ * button form elements by user_cancel_methods() after this hook is invoked.
+ * The following properties can be defined for each method:
+ * - title: The radio button's title.
+ * - description: (optional) A description to display on the confirmation form
+ * if the user is not allowed to select the account cancellation method. The
+ * description is NOT used for the radio button, but instead should provide
+ * additional explanation to the user seeking to cancel their account.
+ * - access: (optional) A boolean value indicating whether the user can access
+ * a method. If access is defined, the method cannot be configured as the
+ * default method.
+ *
+ * @param $methods
+ * An array containing user account cancellation methods, keyed by method id.
+ *
+ * @see user_cancel_methods()
+ * @see user_cancel_confirm_form()
+ */
+function hook_user_cancel_methods_alter(&$methods) {
+ // Limit access to disable account and unpublish content method.
+ $methods['user_cancel_block_unpublish']['access'] = user_access('administer site configuration');
+
+ // Remove the content re-assigning method.
+ unset($methods['user_cancel_reassign']);
+
+ // Add a custom zero-out method.
+ $methods['mymodule_zero_out'] = array(
+ 'title' => t('Delete the account and remove all content.'),
+ 'description' => t('All your content will be replaced by empty strings.'),
+ // access should be used for administrative methods only.
+ 'access' => user_access('access zero-out account cancellation method'),
+ );
+}
+
+/**
+ * Add mass user operations.
+ *
+ * This hook enables modules to inject custom operations into the mass operations
+ * dropdown found at admin/people, by associating a callback function with
+ * the operation, which is called when the form is submitted. The callback function
+ * receives one initial argument, which is an array of the checked users.
+ *
+ * @return
+ * An array of operations. Each operation is an associative array that may
+ * contain the following key-value pairs:
+ * - "label": Required. The label for the operation, displayed in the dropdown menu.
+ * - "callback": Required. The function to call for the operation.
+ * - "callback arguments": Optional. An array of additional arguments to pass to
+ * the callback function.
+ *
+ */
+function hook_user_operations() {
+ $operations = array(
+ 'unblock' => array(
+ 'label' => t('Unblock the selected users'),
+ 'callback' => 'user_user_operations_unblock',
+ ),
+ 'block' => array(
+ 'label' => t('Block the selected users'),
+ 'callback' => 'user_user_operations_block',
+ ),
+ 'cancel' => array(
+ 'label' => t('Cancel the selected user accounts'),
+ ),
+ );
+ return $operations;
+}
+
+/**
+ * Define a list of user settings or profile information categories.
+ *
+ * There are two steps to using hook_user_categories():
+ * - Create the category with hook_user_categories().
+ * - Display that category on the form ID of "user_profile_form" with
+ * hook_form_FORM_ID_alter().
+ *
+ * Step one builds out the category but it won't be visible on your form until
+ * you explicitly tell it to do so.
+ *
+ * The function in step two should contain the following code in order to
+ * display your new category:
+ * @code
+ * if ($form['#user_category'] == 'mycategory') {
+ * // Return your form here.
+ * }
+ * @endcode
+ *
+ * @return
+ * An array of associative arrays. Each inner array has elements:
+ * - "name": The internal name of the category.
+ * - "title": The human-readable, localized name of the category.
+ * - "weight": An integer specifying the category's sort ordering.
+ * - "access callback": Name of the access callback function to use to
+ * determine whether the user can edit the category. Defaults to using
+ * user_edit_access(). See hook_menu() for more information on access
+ * callbacks.
+ * - "access arguments": Arguments for the access callback function. Defaults
+ * to array(1).
+ */
+function hook_user_categories() {
+ return array(array(
+ 'name' => 'account',
+ 'title' => t('Account settings'),
+ 'weight' => 1,
+ ));
+}
+
+/**
+ * A user account is about to be created or updated.
+ *
+ * This hook is primarily intended for modules that want to store properties in
+ * the serialized {users}.data column, which is automatically loaded whenever a
+ * user account object is loaded, modules may add to $edit['data'] in order
+ * to have their data serialized on save.
+ *
+ * @param $edit
+ * The array of form values submitted by the user. Assign values to this
+ * array to save changes in the database.
+ * @param $account
+ * The user object on which the operation is performed. Values assigned in
+ * this object will not be saved in the database.
+ * @param $category
+ * The active category of user information being edited.
+ *
+ * @see hook_user_insert()
+ * @see hook_user_update()
+ */
+function hook_user_presave(&$edit, $account, $category) {
+ // Make sure that our form value 'mymodule_foo' is stored as
+ // 'mymodule_bar' in the 'data' (serialized) column.
+ if (isset($edit['mymodule_foo'])) {
+ $edit['data']['mymodule_bar'] = $edit['mymodule_foo'];
+ }
+}
+
+/**
+ * A user account was created.
+ *
+ * The module should save its custom additions to the user object into the
+ * database.
+ *
+ * @param $edit
+ * The array of form values submitted by the user.
+ * @param $account
+ * The user object on which the operation is being performed.
+ * @param $category
+ * The active category of user information being edited.
+ *
+ * @see hook_user_presave()
+ * @see hook_user_update()
+ */
+function hook_user_insert(&$edit, $account, $category) {
+ db_insert('mytable')
+ ->fields(array(
+ 'myfield' => $edit['myfield'],
+ 'uid' => $account->uid,
+ ))
+ ->execute();
+}
+
+/**
+ * A user account was updated.
+ *
+ * Modules may use this hook to update their user data in a custom storage
+ * after a user account has been updated.
+ *
+ * @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.
+ *
+ * @see hook_user_presave()
+ * @see hook_user_insert()
+ */
+function hook_user_update(&$edit, $account, $category) {
+ db_insert('user_changes')
+ ->fields(array(
+ 'uid' => $account->uid,
+ 'changed' => time(),
+ ))
+ ->execute();
+}
+
+/**
+ * The user just logged in.
+ *
+ * @param $edit
+ * The array of form values submitted by the user.
+ * @param $account
+ * The user object on which the operation was just performed.
+ */
+function hook_user_login(&$edit, $account) {
+ // If the user has a NULL time zone, notify them to set a time zone.
+ if (!$account->timezone && variable_get('configurable_timezones', 1) && variable_get('empty_timezone_message', 0)) {
+ drupal_set_message(t('Configure your
account time zone setting .', array('@user-edit' => url("user/$account->uid/edit", array('query' => drupal_get_destination(), 'fragment' => 'edit-timezone')))));
+ }
+}
+
+/**
+ * The user just logged out.
+ *
+ * Note that when this hook is invoked, the changes have not yet been written to
+ * the database, because a database transaction is still in progress. The
+ * transaction is not finalized until the save operation is entirely completed
+ * and user_save() goes out of scope. You should not rely on data in the
+ * database at this time as it is not updated yet. You should also note that any
+ * write/update database queries executed from this hook are also not committed
+ * immediately. Check user_save() and db_transaction() for more info.
+ *
+ * @param $account
+ * The user object on which the operation was just performed.
+ */
+function hook_user_logout($account) {
+ db_insert('logouts')
+ ->fields(array(
+ 'uid' => $account->uid,
+ 'time' => time(),
+ ))
+ ->execute();
+}
+
+/**
+ * The user's account information is being displayed.
+ *
+ * The module should format its custom additions for display and add them to the
+ * $account->content array.
+ *
+ * @param $account
+ * The user object on which the operation is being performed.
+ * @param $view_mode
+ * View mode, e.g. 'full'.
+ * @param $langcode
+ * The language code used for rendering.
+ *
+ * @see hook_user_view_alter()
+ * @see hook_entity_view()
+ */
+function hook_user_view($account, $view_mode, $langcode) {
+ if (user_access('create blog content', $account)) {
+ $account->content['summary']['blog'] = array(
+ '#type' => 'user_profile_item',
+ '#title' => t('Blog'),
+ '#markup' => l(t('View recent blog entries'), "blog/$account->uid", array('attributes' => array('title' => t("Read !username's latest blog entries.", array('!username' => format_username($account)))))),
+ '#attributes' => array('class' => array('blog')),
+ );
+ }
+}
+
+/**
+ * The user was built; the module may modify the structured content.
+ *
+ * This hook is called after the content has been assembled in a structured array
+ * and may be used for doing processing which requires that the complete user
+ * content structure has been built.
+ *
+ * If the module wishes to act on the rendered HTML of the user rather than the
+ * structured content array, it may use this hook to add a #post_render callback.
+ * Alternatively, it could also implement hook_preprocess_user_profile(). See
+ * drupal_render() and theme() documentation respectively for details.
+ *
+ * @param $build
+ * A renderable array representing the user.
+ *
+ * @see user_view()
+ * @see hook_entity_view_alter()
+ */
+function hook_user_view_alter(&$build) {
+ // Check for the existence of a field added by another module.
+ if (isset($build['an_additional_field'])) {
+ // Change its weight.
+ $build['an_additional_field']['#weight'] = -10;
+ }
+
+ // Add a #post_render callback to act on the rendered HTML of the user.
+ $build['#post_render'][] = 'my_module_user_post_render';
+}
+
+/**
+ * Act on a user role being inserted or updated.
+ *
+ * Modules implementing this hook can act on the user role object before
+ * it has been saved to the database.
+ *
+ * @param $role
+ * A user role object.
+ *
+ * @see hook_user_role_insert()
+ * @see hook_user_role_update()
+ */
+function hook_user_role_presave($role) {
+ // Set a UUID for the user role if it doesn't already exist
+ if (empty($role->uuid)) {
+ $role->uuid = uuid_uuid();
+ }
+}
+
+/**
+ * Respond to creation of a new user role.
+ *
+ * Modules implementing this hook can act on the user role object when saved to
+ * the database. It's recommended that you implement this hook if your module
+ * adds additional data to user roles object. The module should save its custom
+ * additions to the database.
+ *
+ * @param $role
+ * A user role object.
+ */
+function hook_user_role_insert($role) {
+ // Save extra fields provided by the module to user roles.
+ db_insert('my_module_table')
+ ->fields(array(
+ 'rid' => $role->rid,
+ 'role_description' => $role->description,
+ ))
+ ->execute();
+}
+
+/**
+ * Respond to updates to a user role.
+ *
+ * Modules implementing this hook can act on the user role object when updated.
+ * It's recommended that you implement this hook if your module adds additional
+ * data to user roles object. The module should save its custom additions to
+ * the database.
+ *
+ * @param $role
+ * A user role object.
+ */
+function hook_user_role_update($role) {
+ // Save extra fields provided by the module to user roles.
+ db_merge('my_module_table')
+ ->key(array('rid' => $role->rid))
+ ->fields(array(
+ 'role_description' => $role->description
+ ))
+ ->execute();
+}
+
+/**
+ * Respond to user role deletion.
+ *
+ * This hook allows you act when a user role has been deleted.
+ * If your module stores references to roles, it's recommended that you
+ * implement this hook and delete existing instances of the deleted role
+ * in your module database tables.
+ *
+ * @param $role
+ * The $role object being deleted.
+ */
+function hook_user_role_delete($role) {
+ // Delete existing instances of the deleted role.
+ db_delete('my_module_table')
+ ->condition('rid', $role->rid)
+ ->execute();
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/modules/user/user.css b/modules/user/user.css
new file mode 100644
index 0000000..079ec38
--- /dev/null
+++ b/modules/user/user.css
@@ -0,0 +1,102 @@
+
+#permissions td.module {
+ font-weight: bold;
+}
+#permissions td.permission {
+ padding-left: 1.5em; /* LTR */
+}
+#permissions tr.odd .form-item,
+#permissions tr.even .form-item {
+ white-space: normal;
+}
+#user-admin-settings fieldset .fieldset-description {
+ font-size: 0.85em;
+ padding-bottom: .5em;
+}
+
+/**
+ * Override default textfield float to put the "Add role" button next to
+ * the input textfield.
+ */
+#user-admin-roles td.edit-name {
+ clear: both;
+}
+#user-admin-roles .form-item-name {
+ float: left; /* LTR */
+ margin-right: 1em; /* LTR */
+}
+
+/**
+ * Password strength indicator.
+ */
+.password-strength {
+ width: 17em;
+ float: right; /* LTR */
+ margin-top: 1.4em;
+}
+.password-strength-title {
+ display: inline;
+}
+.password-strength-text {
+ float: right; /* LTR */
+ font-weight: bold;
+}
+.password-indicator {
+ background-color: #C4C4C4;
+ height: 0.3em;
+ width: 100%;
+}
+.password-indicator div {
+ height: 100%;
+ width: 0%;
+ background-color: #47C965;
+}
+input.password-confirm,
+input.password-field {
+ width: 16em;
+ margin-bottom: 0.4em;
+}
+div.password-confirm {
+ float: right; /* LTR */
+ margin-top: 1.5em;
+ visibility: hidden;
+ width: 17em;
+}
+div.form-item div.password-suggestions {
+ padding: 0.2em 0.5em;
+ margin: 0.7em 0;
+ width: 38.5em;
+ border: 1px solid #B4B4B4;
+}
+div.password-suggestions ul {
+ margin-bottom: 0;
+}
+.confirm-parent,
+.password-parent {
+ clear: left; /* LTR */
+ margin: 0;
+ width: 36.3em;
+}
+
+/* Generated by user.module but used by profile.module: */
+.profile {
+ clear: both;
+ margin: 1em 0;
+}
+.profile .user-picture {
+ float: right; /* LTR */
+ margin: 0 1em 1em 0; /* LTR */
+}
+.profile h3 {
+ border-bottom: 1px solid #ccc;
+}
+.profile dl {
+ margin: 0 0 1.5em 0;
+}
+.profile dt {
+ margin: 0 0 0.2em 0;
+ font-weight: bold;
+}
+.profile dd {
+ margin: 0 0 1em 0;
+}
diff --git a/modules/user/user.info b/modules/user/user.info
new file mode 100644
index 0000000..83e2b8e
--- /dev/null
+++ b/modules/user/user.info
@@ -0,0 +1,16 @@
+name = User
+description = Manages the user registration and login system.
+package = Core
+version = VERSION
+core = 7.x
+files[] = user.module
+files[] = user.test
+required = TRUE
+configure = admin/config/people
+stylesheets[all][] = user.css
+
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
+project = "drupal"
+datestamp = "1522264019"
+
diff --git a/modules/user/user.install b/modules/user/user.install
new file mode 100644
index 0000000..7a74766
--- /dev/null
+++ b/modules/user/user.install
@@ -0,0 +1,927 @@
+ 'Stores distributed authentication mapping.',
+ 'fields' => array(
+ 'aid' => array(
+ 'description' => 'Primary Key: Unique authmap ID.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'uid' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "User's {users}.uid.",
+ ),
+ 'authname' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Unique authentication name.',
+ ),
+ 'module' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Module which is controlling the authentication.',
+ ),
+ ),
+ 'unique keys' => array(
+ 'authname' => array('authname'),
+ ),
+ 'primary key' => array('aid'),
+ 'foreign keys' => array(
+ 'user' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ 'indexes' => array(
+ 'uid_module' => array('uid', 'module'),
+ ),
+ );
+
+ $schema['role_permission'] = array(
+ 'description' => 'Stores the permissions assigned to user roles.',
+ 'fields' => array(
+ 'rid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Foreign Key: {role}.rid.',
+ ),
+ 'permission' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'A single permission granted to the role identified by rid.',
+ ),
+ 'module' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "The module declaring the permission.",
+ ),
+ ),
+ 'primary key' => array('rid', 'permission'),
+ 'indexes' => array(
+ 'permission' => array('permission'),
+ ),
+ 'foreign keys' => array(
+ 'role' => array(
+ 'table' => 'role',
+ 'columns' => array('rid' => 'rid'),
+ ),
+ ),
+ );
+
+ $schema['role'] = array(
+ 'description' => 'Stores user roles.',
+ 'fields' => array(
+ 'rid' => array(
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique role ID.',
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Unique role name.',
+ 'translatable' => TRUE,
+ ),
+ 'weight' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The weight of this role in listings and the user interface.',
+ ),
+ ),
+ 'unique keys' => array(
+ 'name' => array('name'),
+ ),
+ 'primary key' => array('rid'),
+ 'indexes' => array(
+ 'name_weight' => array('name', 'weight'),
+ ),
+ );
+
+ // The table name here is plural, despite Drupal table naming standards,
+ // because "user" is a reserved word in many databases.
+ $schema['users'] = array(
+ 'description' => 'Stores user data.',
+ 'fields' => array(
+ 'uid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique user ID.',
+ 'default' => 0,
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 60,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Unique user name.',
+ ),
+ 'pass' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "User's password (hashed).",
+ ),
+ 'mail' => array(
+ 'type' => 'varchar',
+ 'length' => 254,
+ 'not null' => FALSE,
+ 'default' => '',
+ 'description' => "User's e-mail address.",
+ ),
+ 'theme' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "User's default theme.",
+ ),
+ 'signature' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "User's signature.",
+ ),
+ 'signature_format' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ 'description' => 'The {filter_format}.format of the signature.',
+ ),
+ 'created' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Timestamp for when user was created.',
+ ),
+ 'access' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Timestamp for previous time user accessed the site.',
+ ),
+ 'login' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "Timestamp for user's last login.",
+ ),
+ 'status' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ 'description' => 'Whether the user is active(1) or blocked(0).',
+ ),
+ 'timezone' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => FALSE,
+ 'description' => "User's time zone.",
+ ),
+ 'language' => array(
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "User's default language.",
+ ),
+ 'picture' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "Foreign key: {file_managed}.fid of user's picture.",
+ ),
+ 'init' => array(
+ 'type' => 'varchar',
+ 'length' => 254,
+ 'not null' => FALSE,
+ 'default' => '',
+ 'description' => 'E-mail address used for initial account creation.',
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of name value pairs that are related to the user. Any form values posted during user edit are stored and are loaded into the $user object during user_load(). Use of this field is discouraged and it will likely disappear in a future version of Drupal.',
+ ),
+ ),
+ 'indexes' => array(
+ 'access' => array('access'),
+ 'created' => array('created'),
+ 'mail' => array('mail'),
+ 'picture' => array('picture'),
+ ),
+ 'unique keys' => array(
+ 'name' => array('name'),
+ ),
+ 'primary key' => array('uid'),
+ 'foreign keys' => array(
+ 'signature_format' => array(
+ 'table' => 'filter_format',
+ 'columns' => array('signature_format' => 'format'),
+ ),
+ ),
+ );
+
+ $schema['users_roles'] = array(
+ 'description' => 'Maps users to roles.',
+ 'fields' => array(
+ 'uid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Primary Key: {users}.uid for user.',
+ ),
+ 'rid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Primary Key: {role}.rid for role.',
+ ),
+ ),
+ 'primary key' => array('uid', 'rid'),
+ 'indexes' => array(
+ 'rid' => array('rid'),
+ ),
+ 'foreign keys' => array(
+ 'user' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ 'role' => array(
+ 'table' => 'role',
+ 'columns' => array('rid' => 'rid'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function user_install() {
+ // Insert a row for the anonymous user.
+ db_insert('users')
+ ->fields(array(
+ 'uid' => 0,
+ 'name' => '',
+ 'mail' => '',
+ ))
+ ->execute();
+
+ // We need some placeholders here as name and mail are uniques and data is
+ // presumed to be a serialized array. This will be changed by the settings
+ // form in the installer.
+ db_insert('users')
+ ->fields(array(
+ 'uid' => 1,
+ 'name' => 'placeholder-for-uid-1',
+ 'mail' => 'placeholder-for-uid-1',
+ 'created' => REQUEST_TIME,
+ 'status' => 1,
+ 'data' => NULL,
+ ))
+ ->execute();
+
+ // Built-in roles.
+ $rid_anonymous = db_insert('role')
+ ->fields(array('name' => 'anonymous user', 'weight' => 0))
+ ->execute();
+ $rid_authenticated = db_insert('role')
+ ->fields(array('name' => 'authenticated user', 'weight' => 1))
+ ->execute();
+
+ // Sanity check to ensure the anonymous and authenticated role IDs are the
+ // same as the drupal defined constants. In certain situations, this will
+ // not be true.
+ if ($rid_anonymous != DRUPAL_ANONYMOUS_RID) {
+ db_update('role')
+ ->fields(array('rid' => DRUPAL_ANONYMOUS_RID))
+ ->condition('rid', $rid_anonymous)
+ ->execute();
+ }
+ if ($rid_authenticated != DRUPAL_AUTHENTICATED_RID) {
+ db_update('role')
+ ->fields(array('rid' => DRUPAL_AUTHENTICATED_RID))
+ ->condition('rid', $rid_authenticated)
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_update_dependencies().
+ */
+function user_update_dependencies() {
+ // user_update_7006() updates data in the {role_permission} table, so it must
+ // run after system_update_7007(), which populates that table.
+ $dependencies['user'][7006] = array(
+ 'system' => 7007,
+ );
+
+ // user_update_7010() needs to query the {filter_format} table to get a list
+ // of existing text formats, so it must run after filter_update_7000(), which
+ // creates that table.
+ $dependencies['user'][7010] = array(
+ 'filter' => 7000,
+ );
+
+ // user_update_7012() uses the file API and inserts records into the
+ // {file_managed} table, so it therefore must run after system_update_7061(),
+ // which inserts files with specific IDs into the table and therefore relies
+ // on the table being empty (otherwise it would accidentally overwrite
+ // existing records).
+ $dependencies['user'][7012] = array(
+ 'system' => 7061,
+ );
+
+ // user_update_7013() uses the file usage API, which relies on the
+ // {file_usage} table, so it must run after system_update_7059(), which
+ // creates that table.
+ $dependencies['user'][7013] = array(
+ 'system' => 7059,
+ );
+
+ return $dependencies;
+}
+
+/**
+ * Utility function: grant a set of permissions to a role during update.
+ *
+ * This function is valid for a database schema version 7000.
+ *
+ * @param $rid
+ * The role ID.
+ * @param $permissions
+ * An array of permissions names.
+ * @param $module
+ * The name of the module defining the permissions.
+ * @ingroup update_api
+ */
+function _update_7000_user_role_grant_permissions($rid, array $permissions, $module) {
+ // Grant new permissions for the role.
+ foreach ($permissions as $name) {
+ db_merge('role_permission')
+ ->key(array(
+ 'rid' => $rid,
+ 'permission' => $name,
+ ))
+ ->fields(array(
+ 'module' => $module,
+ ))
+ ->execute();
+ }
+}
+
+/**
+ * @addtogroup updates-6.x-to-7.x
+ * @{
+ */
+
+/**
+ * Increase the length of the password field to accommodate better hashes.
+ *
+ * Also re-hashes all current passwords to improve security. This may be a
+ * lengthy process, and is performed batch-wise.
+ */
+function user_update_7000(&$sandbox) {
+ $sandbox['#finished'] = 0;
+ // Lower than DRUPAL_HASH_COUNT to make the update run at a reasonable speed.
+ $hash_count_log2 = 11;
+ // Multi-part update.
+ if (!isset($sandbox['user_from'])) {
+ db_change_field('users', 'pass', 'pass', array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''));
+ $sandbox['user_from'] = 0;
+ $sandbox['user_count'] = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+ }
+ else {
+ require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
+ // Hash again all current hashed passwords.
+ $has_rows = FALSE;
+ // Update this many per page load.
+ $count = 1000;
+ $result = db_query_range("SELECT uid, pass FROM {users} WHERE uid > 0 ORDER BY uid", $sandbox['user_from'], $count);
+ foreach ($result as $account) {
+ $has_rows = TRUE;
+
+ // If the $account->pass value is not a MD5 hash (a 32 character
+ // hexadecimal string) then skip it.
+ if (!preg_match('/^[0-9a-f]{32}$/', $account->pass)) {
+ continue;
+ }
+
+ $new_hash = user_hash_password($account->pass, $hash_count_log2);
+ if ($new_hash) {
+ // Indicate an updated password.
+ $new_hash = 'U' . $new_hash;
+ db_update('users')
+ ->fields(array('pass' => $new_hash))
+ ->condition('uid', $account->uid)
+ ->execute();
+ }
+ }
+ $sandbox['#finished'] = $sandbox['user_from']/$sandbox['user_count'];
+ $sandbox['user_from'] += $count;
+ if (!$has_rows) {
+ $sandbox['#finished'] = 1;
+ return t('User passwords rehashed to improve security');
+ }
+ }
+}
+
+/**
+ * Remove the 'threshold', 'mode' and 'sort' columns from the {users} table.
+ *
+ * These fields were previously used to store per-user comment settings.
+ */
+
+function user_update_7001() {
+ db_drop_field('users', 'threshold');
+ db_drop_field('users', 'mode');
+ db_drop_field('users', 'sort');
+}
+
+/**
+ * Convert user time zones from time zone offsets to time zone names.
+ */
+function user_update_7002(&$sandbox) {
+ $sandbox['#finished'] = 0;
+
+ // Multi-part update.
+ if (!isset($sandbox['user_from'])) {
+ db_change_field('users', 'timezone', 'timezone', array('type' => 'varchar', 'length' => 32, 'not null' => FALSE));
+ $sandbox['user_from'] = 0;
+ $sandbox['user_count'] = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+ $sandbox['user_not_migrated'] = 0;
+ }
+ else {
+ $timezones = system_time_zones();
+ // Update this many per page load.
+ $count = 10000;
+ $contributed_date_module = db_field_exists('users', 'timezone_name');
+ $contributed_event_module = db_field_exists('users', 'timezone_id');
+
+ $results = db_query_range("SELECT uid FROM {users} ORDER BY uid", $sandbox['user_from'], $count);
+ foreach ($results as $account) {
+ $timezone = NULL;
+ // If the contributed Date module has created a users.timezone_name
+ // column, use this data to set each user's time zone.
+ if ($contributed_date_module) {
+ $date_timezone = db_query("SELECT timezone_name FROM {users} WHERE uid = :uid", array(':uid' => $account->uid))->fetchField();
+ if (isset($timezones[$date_timezone])) {
+ $timezone = $date_timezone;
+ }
+ }
+ // If the contributed Event module has stored user time zone information
+ // use that information to update the user accounts.
+ if (!$timezone && $contributed_event_module) {
+ try {
+ $event_timezone = db_query("SELECT t.name FROM {users} u LEFT JOIN {event_timezones} t ON u.timezone_id = t.timezone WHERE u.uid = :uid", array(':uid' => $account->uid))->fetchField();
+ $event_timezone = str_replace(' ', '_', $event_timezone);
+ if (isset($timezones[$event_timezone])) {
+ $timezone = $event_timezone;
+ }
+ }
+ catch (PDOException $e) {
+ // Ignore error if event_timezones table does not exist or unexpected
+ // schema found.
+ }
+ }
+ if ($timezone) {
+ db_update('users')
+ ->fields(array('timezone' => $timezone))
+ ->condition('uid', $account->uid)
+ ->execute();
+ }
+ else {
+ $sandbox['user_not_migrated']++;
+ db_update('users')
+ ->fields(array('timezone' => NULL))
+ ->condition('uid', $account->uid)
+ ->execute();
+ }
+ $sandbox['user_from']++;
+ }
+
+ $sandbox['#finished'] = $sandbox['user_from'] / $sandbox['user_count'];
+ if ($sandbox['user_from'] == $sandbox['user_count']) {
+ if ($sandbox['user_not_migrated'] > 0) {
+ variable_set('empty_timezone_message', 1);
+ drupal_set_message(format_string('Some user time zones have been emptied and need to be set to the correct values. Use the new
time zone options to choose whether to remind users at login to set the correct time zone.', array('@config-url' => url('admin/config/regional/settings'))), 'warning');
+ }
+ return t('Migrated user time zones');
+ }
+ }
+}
+
+/**
+ * Update user settings for cancelling user accounts.
+ *
+ * Prior to 7.x, users were not able to cancel their accounts. When
+ * administrators deleted an account, all contents were assigned to uid 0,
+ * which is the same as the 'user_cancel_reassign' method now.
+ */
+function user_update_7003() {
+ // Set the default account cancellation method.
+ variable_set('user_cancel_method', 'user_cancel_reassign');
+ // Re-assign notification setting.
+ if ($setting = variable_get('user_mail_status_deleted_notify', FALSE)) {
+ variable_set('user_mail_status_canceled_notify', $setting);
+ variable_del('user_mail_status_deleted_notify');
+ }
+ // Re-assign "Account deleted" mail strings to "Account canceled" mail.
+ if ($setting = variable_get('user_mail_status_deleted_subject', FALSE)) {
+ variable_set('user_mail_status_canceled_subject', $setting);
+ variable_del('user_mail_status_deleted_subject');
+ }
+ if ($setting = variable_get('user_mail_status_deleted_body', FALSE)) {
+ variable_set('user_mail_status_canceled_body', $setting);
+ variable_del('user_mail_status_deleted_body');
+ }
+}
+
+/**
+ * Changes the users table to allow longer e-mail addresses.
+ */
+function user_update_7005(&$sandbox) {
+ $mail_field = array(
+ 'type' => 'varchar',
+ 'length' => 254,
+ 'not null' => FALSE,
+ 'default' => '',
+ 'description' => "User's e-mail address.",
+ );
+ $init_field = array(
+ 'type' => 'varchar',
+ 'length' => 254,
+ 'not null' => FALSE,
+ 'default' => '',
+ 'description' => 'E-mail address used for initial account creation.',
+ );
+ db_drop_index('users', 'mail');
+ db_change_field('users', 'mail', 'mail', $mail_field, array('indexes' => array('mail' => array('mail'))));
+ db_change_field('users', 'init', 'init', $init_field);
+}
+
+/**
+ * Add module data to {role_permission}.
+ */
+function user_update_7006(&$sandbox) {
+ $module_field = array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "The module declaring the permission.",
+ );
+ // Check that the field hasn't been updated in an aborted run of this
+ // update.
+ if (!db_field_exists('role_permission', 'module')) {
+ // Add a new field for the fid.
+ db_add_field('role_permission', 'module', $module_field);
+ }
+}
+
+/**
+ * Add a weight column to user roles.
+ */
+function user_update_7007() {
+ db_add_field('role', 'weight', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
+ db_add_index('role', 'name_weight', array('name', 'weight'));
+}
+
+/**
+ * If 'user_register' variable was unset in Drupal 6, set it to be the same as
+ * the Drupal 6 default setting.
+ */
+function user_update_7008() {
+ if (!isset($GLOBALS['conf']['user_register'])) {
+ // Set to the Drupal 6 default, "visitors can create accounts".
+ variable_set('user_register', USER_REGISTER_VISITORS);
+ }
+}
+
+/**
+ * Converts fields that store serialized variables from text to blob.
+ */
+function user_update_7009() {
+ $spec = array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of name value pairs that are related to the user. Any form values posted during user edit are stored and are loaded into the $user object during user_load(). Use of this field is discouraged and it will likely disappear in a future version of Drupal.',
+ );
+ db_change_field('users', 'data', 'data', $spec);
+}
+
+/**
+ * Update the {user}.signature_format column.
+ */
+function user_update_7010() {
+ // Update the database column to allow NULL values.
+ db_change_field('users', 'signature_format', 'signature_format', array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ 'description' => 'The {filter_format}.format of the signature.',
+ ));
+
+ // Replace the signature format with NULL if the signature is empty and does
+ // not already have a stored text format.
+ //
+ // In Drupal 6, "0" (the former FILTER_FORMAT_DEFAULT constant) could be used
+ // to indicate this situation, but in Drupal 7, only NULL is supported. This
+ // update therefore preserves the ability of user accounts which were never
+ // given a signature (for example, if the site did not have user signatures
+ // enabled, or if the user never edited their account information) to not
+ // have a particular text format assumed for them the first time the
+ // signature is edited.
+ db_update('users')
+ ->fields(array('signature_format' => NULL))
+ ->condition('signature', '')
+ ->condition('signature_format', 0)
+ ->execute();
+
+ // There are a number of situations in which a Drupal 6 site could store
+ // content with a nonexistent text format. This includes text formats that
+ // had later been deleted, or non-empty content stored with a value of "0"
+ // (the former FILTER_FORMAT_DEFAULT constant). Drupal 6 would filter this
+ // content using whatever the site-wide default text format was at the moment
+ // the text was being displayed.
+ //
+ // In Drupal 7, this behavior is no longer supported, and all content must be
+ // stored with an explicit text format (or it will not be displayed when it
+ // is filtered). Therefore, to preserve the behavior of the site after the
+ // upgrade, we must replace all instances described above with the current
+ // value of the (old) site-wide default format at the moment of the upgrade.
+ $existing_formats = db_query("SELECT format FROM {filter_format}")->fetchCol();
+ $default_format = variable_get('filter_default_format', 1);
+ db_update('users')
+ ->fields(array('signature_format' => $default_format))
+ ->isNotNull('signature_format')
+ ->condition('signature_format', $existing_formats, 'NOT IN')
+ ->execute();
+}
+
+/**
+ * Placeholder function.
+ *
+ * As a fix for user_update_7011() not updating email templates to use the new
+ * tokens, user_update_7017() now targets email templates of Drupal 6 sites and
+ * already upgraded sites.
+ */
+function user_update_7011() {
+}
+
+/**
+ * Add the user's pictures to the {file_managed} table and make them managed
+ * files.
+ */
+function user_update_7012(&$sandbox) {
+
+ $picture_field = array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "Foreign key: {file_managed}.fid of user's picture.",
+ );
+
+ if (!isset($sandbox['progress'])) {
+ // Check that the field hasn't been updated in an aborted run of this
+ // update.
+ if (!db_field_exists('users', 'picture_fid')) {
+ // Add a new field for the fid.
+ db_add_field('users', 'picture_fid', $picture_field);
+ }
+
+ // Initialize batch update information.
+ $sandbox['progress'] = 0;
+ $sandbox['last_user_processed'] = -1;
+ $sandbox['max'] = db_query("SELECT COUNT(*) FROM {users} WHERE picture <> ''")->fetchField();
+ }
+
+ // As a batch operation move the photos into the {file_managed} table and
+ // update the {users} records.
+ $limit = 500;
+ $result = db_query_range("SELECT uid, picture FROM {users} WHERE picture <> '' AND uid > :uid ORDER BY uid", 0, $limit, array(':uid' => $sandbox['last_user_processed']));
+ foreach ($result as $user) {
+ // Don't bother adding files that don't exist.
+ if (file_exists($user->picture)) {
+
+ // Check if the file already exists.
+ $files = file_load_multiple(array(), array('uri' => $user->picture));
+ if (count($files)) {
+ $file = reset($files);
+ }
+ else {
+ // Create a file object.
+ $file = new stdClass();
+ $file->uri = $user->picture;
+ $file->filename = drupal_basename($file->uri);
+ $file->filemime = file_get_mimetype($file->uri);
+ $file->uid = $user->uid;
+ $file->status = FILE_STATUS_PERMANENT;
+ $file = file_save($file);
+ }
+
+ db_update('users')
+ ->fields(array('picture_fid' => $file->fid))
+ ->condition('uid', $user->uid)
+ ->execute();
+ }
+
+ // Update our progress information for the batch update.
+ $sandbox['progress']++;
+ $sandbox['last_user_processed'] = $user->uid;
+ }
+
+ // Indicate our current progress to the batch update system. If there's no
+ // max value then there's nothing to update and we're finished.
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+
+ // When we're finished, drop the old picture field and rename the new one to
+ // replace it.
+ if (isset($sandbox['#finished']) && $sandbox['#finished'] == 1) {
+ db_drop_field('users', 'picture');
+ db_change_field('users', 'picture_fid', 'picture', $picture_field);
+ }
+}
+
+/**
+ * Add user module file usage entries.
+ */
+function user_update_7013(&$sandbox) {
+ if (!isset($sandbox['progress'])) {
+ // Initialize batch update information.
+ $sandbox['progress'] = 0;
+ $sandbox['last_uid_processed'] = -1;
+ $sandbox['max'] = db_query("SELECT COUNT(*) FROM {users} u WHERE u.picture <> 0")->fetchField();
+ }
+
+ // Add usage entries for the user picture files.
+ $limit = 500;
+ $result = db_query_range('SELECT f.*, u.uid as user_uid FROM {users} u INNER JOIN {file_managed} f ON u.picture = f.fid WHERE u.picture <> 0 AND u.uid > :uid ORDER BY u.uid', 0, $limit, array(':uid' => $sandbox['last_uid_processed']))->fetchAllAssoc('fid', PDO::FETCH_ASSOC);
+ foreach ($result as $row) {
+ $uid = $row['user_uid'];
+ $file = (object) $row;
+ file_usage_add($file, 'user', 'user', $uid);
+
+ // Update our progress information for the batch update.
+ $sandbox['progress']++;
+ $sandbox['last_uid_processed'] = $uid;
+ }
+
+ // Indicate our current progress to the batch update system.
+ $sandbox['#finished'] = empty($sandbox['max']) || ($sandbox['progress'] / $sandbox['max']);
+}
+
+/**
+ * Rename the 'post comments without approval' permission.
+ *
+ * In Drupal 7, this permission has been renamed to 'skip comment approval'.
+ */
+function user_update_7014() {
+ db_update('role_permission')
+ ->fields(array('permission' => 'skip comment approval'))
+ ->condition('permission', 'post comments without approval')
+ ->execute();
+
+ return t("Renamed the 'post comments without approval' permission to 'skip comment approval'.");
+}
+
+/**
+ * Change {users}.signature_format into varchar.
+ */
+function user_update_7015() {
+ db_change_field('users', 'signature_format', 'signature_format', array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ 'description' => 'The {filter_format}.format of the signature.',
+ ));
+}
+
+/**
+ * @} End of "addtogroup updates-6.x-to-7.x".
+ */
+
+/**
+ * @addtogroup updates-7.x-extra
+ * @{
+ */
+
+/**
+ * Update the database to match the schema.
+ */
+function user_update_7016() {
+ // Add field default.
+ db_change_field('users', 'uid', 'uid', array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ));
+}
+
+/**
+ * Update email templates to use new tokens.
+ *
+ * This function upgrades customized email templates from the old !token format
+ * to the new core tokens format. Additionally, in Drupal 7 we no longer e-mail
+ * plain text passwords to users, and there is no token for a plain text
+ * password in the new token system. Therefore, it also modifies any saved
+ * templates using the old '!password' token such that the token is removed, and
+ * displays a warning to users that they may need to go and modify the wording
+ * of their templates.
+ */
+function user_update_7017() {
+ $message = '';
+
+ $tokens = array(
+ '!site' => '[site:name]',
+ '!username' => '[user:name]',
+ '!mailto' => '[user:mail]',
+ '!login_uri' => '[site:login-url]',
+ '!uri_brief' => '[site:url-brief]',
+ '!edit_uri' => '[user:edit-url]',
+ '!login_url' => '[user:one-time-login-url]',
+ '!uri' => '[site:url]',
+ '!date' => '[date:medium]',
+ '!password' => '',
+ );
+
+ $result = db_select('variable', 'v')
+ ->fields('v', array('name'))
+ ->condition('name', db_like('user_mail_') . '%', 'LIKE')
+ ->execute();
+
+ foreach ($result as $row) {
+ // Use variable_get() to get the unserialized value for free.
+ if ($value = variable_get($row->name, FALSE)) {
+
+ if (empty($message) && (strpos($value, '!password') !== FALSE)) {
+ $message = t('The ability to send users their passwords in plain text has been removed in Drupal 7. Your existing email templates have been modified to remove it. You should
review these templates to make sure they read properly.', array('@template-url' => url('admin/config/people/accounts')));
+ }
+
+ variable_set($row->name, str_replace(array_keys($tokens), $tokens, $value));
+ }
+ }
+
+ return $message;
+}
+
+/**
+ * Ensure there is an index on {users}.picture.
+ */
+function user_update_7018() {
+ if (!db_index_exists('users', 'picture')) {
+ db_add_index('users', 'picture', array('picture'));
+ }
+}
+
+/**
+ * Ensure there is a combined index on {authmap}.uid and {authmap}.module.
+ */
+function user_update_7019() {
+ // Check first in case it was already added manually.
+ if (!db_index_exists('authmap', 'uid_module')) {
+ db_add_index('authmap', 'uid_module', array('uid', 'module'));
+ }
+}
+/**
+ * @} End of "addtogroup updates-7.x-extra".
+ */
diff --git a/modules/user/user.js b/modules/user/user.js
new file mode 100644
index 0000000..4cf9816
--- /dev/null
+++ b/modules/user/user.js
@@ -0,0 +1,198 @@
+(function ($) {
+
+/**
+ * Attach handlers to evaluate the strength of any password fields and to check
+ * that its confirmation is correct.
+ */
+Drupal.behaviors.password = {
+ attach: function (context, settings) {
+ var translate = settings.password;
+ $('input.password-field', context).once('password', function () {
+ var passwordInput = $(this);
+ var innerWrapper = $(this).parent();
+ var outerWrapper = $(this).parent().parent();
+
+ // Add identifying class to password element parent.
+ innerWrapper.addClass('password-parent');
+
+ // Add the password confirmation layer.
+ $('input.password-confirm', outerWrapper).parent().prepend('
' + translate['confirmTitle'] + '
').addClass('confirm-parent');
+ var confirmInput = $('input.password-confirm', outerWrapper);
+ var confirmResult = $('div.password-confirm', outerWrapper);
+ var confirmChild = $('span', confirmResult);
+
+ // Add the description box.
+ var passwordMeter = '
' + translate['strengthTitle'] + '
';
+ $(confirmInput).parent().after('
');
+ $(innerWrapper).prepend(passwordMeter);
+ var passwordDescription = $('div.password-suggestions', outerWrapper).hide();
+
+ // Check the password strength.
+ var passwordCheck = function () {
+
+ // Evaluate the password strength.
+ var result = Drupal.evaluatePasswordStrength(passwordInput.val(), settings.password);
+
+ // Update the suggestions for how to improve the password.
+ if (passwordDescription.html() != result.message) {
+ passwordDescription.html(result.message);
+ }
+
+ // Only show the description box if there is a weakness in the password.
+ if (result.strength == 100) {
+ passwordDescription.hide();
+ }
+ else {
+ passwordDescription.show();
+ }
+
+ // Adjust the length of the strength indicator.
+ $(innerWrapper).find('.indicator').css('width', result.strength + '%');
+
+ // Update the strength indication text.
+ $(innerWrapper).find('.password-strength-text').html(result.indicatorText);
+
+ passwordCheckMatch();
+ };
+
+ // Check that password and confirmation inputs match.
+ var passwordCheckMatch = function () {
+
+ if (confirmInput.val()) {
+ var success = passwordInput.val() === confirmInput.val();
+
+ // Show the confirm result.
+ confirmResult.css({ visibility: 'visible' });
+
+ // Remove the previous styling if any exists.
+ if (this.confirmClass) {
+ confirmChild.removeClass(this.confirmClass);
+ }
+
+ // Fill in the success message and set the class accordingly.
+ var confirmClass = success ? 'ok' : 'error';
+ confirmChild.html(translate['confirm' + (success ? 'Success' : 'Failure')]).addClass(confirmClass);
+ this.confirmClass = confirmClass;
+ }
+ else {
+ confirmResult.css({ visibility: 'hidden' });
+ }
+ };
+
+ // Monitor keyup and blur events.
+ // Blur must be used because a mouse paste does not trigger keyup.
+ passwordInput.keyup(passwordCheck).focus(passwordCheck).blur(passwordCheck);
+ confirmInput.keyup(passwordCheckMatch).blur(passwordCheckMatch);
+ });
+ }
+};
+
+/**
+ * Evaluate the strength of a user's password.
+ *
+ * Returns the estimated strength and the relevant output message.
+ */
+Drupal.evaluatePasswordStrength = function (password, translate) {
+ password = $.trim(password);
+
+ var weaknesses = 0, strength = 100, msg = [];
+
+ var hasLowercase = /[a-z]+/.test(password);
+ var hasUppercase = /[A-Z]+/.test(password);
+ var hasNumbers = /[0-9]+/.test(password);
+ var hasPunctuation = /[^a-zA-Z0-9]+/.test(password);
+
+ // If there is a username edit box on the page, compare password to that, otherwise
+ // use value from the database.
+ var usernameBox = $('input.username');
+ var username = (usernameBox.length > 0) ? usernameBox.val() : translate.username;
+
+ // Lose 5 points for every character less than 6, plus a 30 point penalty.
+ if (password.length < 6) {
+ msg.push(translate.tooShort);
+ strength -= ((6 - password.length) * 5) + 30;
+ }
+
+ // Count weaknesses.
+ if (!hasLowercase) {
+ msg.push(translate.addLowerCase);
+ weaknesses++;
+ }
+ if (!hasUppercase) {
+ msg.push(translate.addUpperCase);
+ weaknesses++;
+ }
+ if (!hasNumbers) {
+ msg.push(translate.addNumbers);
+ weaknesses++;
+ }
+ if (!hasPunctuation) {
+ msg.push(translate.addPunctuation);
+ weaknesses++;
+ }
+
+ // Apply penalty for each weakness (balanced against length penalty).
+ switch (weaknesses) {
+ case 1:
+ strength -= 12.5;
+ break;
+
+ case 2:
+ strength -= 25;
+ break;
+
+ case 3:
+ strength -= 40;
+ break;
+
+ case 4:
+ strength -= 40;
+ break;
+ }
+
+ // Check if password is the same as the username.
+ if (password !== '' && password.toLowerCase() === username.toLowerCase()) {
+ msg.push(translate.sameAsUsername);
+ // Passwords the same as username are always very weak.
+ strength = 5;
+ }
+
+ // Based on the strength, work out what text should be shown by the password strength meter.
+ if (strength < 60) {
+ indicatorText = translate.weak;
+ } else if (strength < 70) {
+ indicatorText = translate.fair;
+ } else if (strength < 80) {
+ indicatorText = translate.good;
+ } else if (strength <= 100) {
+ indicatorText = translate.strong;
+ }
+
+ // Assemble the final message.
+ msg = translate.hasWeaknesses + '
';
+ return { strength: strength, message: msg, indicatorText: indicatorText };
+
+};
+
+/**
+ * Field instance settings screen: force the 'Display on registration form'
+ * checkbox checked whenever 'Required' is checked.
+ */
+Drupal.behaviors.fieldUserRegistration = {
+ attach: function (context, settings) {
+ var $checkbox = $('form#field-ui-field-edit-form input#edit-instance-settings-user-register-form');
+
+ if ($checkbox.length) {
+ $('input#edit-instance-required', context).once('user-register-form-checkbox', function () {
+ $(this).bind('change', function (e) {
+ if ($(this).attr('checked')) {
+ $checkbox.attr('checked', true);
+ }
+ });
+ });
+
+ }
+ }
+};
+
+})(jQuery);
diff --git a/modules/user/user.module b/modules/user/user.module
new file mode 100644
index 0000000..12ca280
--- /dev/null
+++ b/modules/user/user.module
@@ -0,0 +1,4085 @@
+' . t('About') . '';
+ $output .= '
' . t('The User module allows users to register, log in, and log out. It also allows users with proper permissions to manage user roles (used to classify users) and permissions associated with those roles. For more information, see the online handbook entry for User module .', array('@user' => 'http://drupal.org/documentation/modules/user')) . '
';
+ $output .= '
' . t('Uses') . ' ';
+ $output .= '
';
+ $output .= '' . t('Creating and managing users') . ' ';
+ $output .= '' . t('The User module allows users with the appropriate permissions to create user accounts through the People administration page , where they can also assign users to one or more roles, and block or delete user accounts. If allowed, users without accounts (anonymous users) can create their own accounts on the Create new account page.', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-user')), '@people' => url('admin/people'), '@register' => url('user/register'))) . ' ';
+ $output .= '' . t('User roles and permissions') . ' ';
+ $output .= '' . t('Roles are used to group and classify users; each user can be assigned one or more roles. By default there are two roles: anonymous user (users that are not logged in) and authenticated user (users that are registered and logged in). Depending on choices you made when you installed Drupal, the installation process may have defined more roles, and you can create additional custom roles on the Roles page . After creating roles, you can set permissions for each role on the Permissions page . Granting a permission allows users who have been assigned a particular role to perform an action on the site, such as viewing a particular type of content, editing or creating content, administering settings for a particular module, or using a particular function of the site (such as search).', array('@permissions_user' => url('admin/people/permissions'), '@roles' => url('admin/people/permissions/roles'))) . ' ';
+ $output .= '' . t('Account settings') . ' ';
+ $output .= '' . t('The Account settings page allows you to manage settings for the displayed name of the anonymous user role, personal contact forms, user registration, and account cancellation. On this page you can also manage settings for account personalization (including signatures and user pictures), and adapt the text for the e-mail messages that are sent automatically during the user registration process.', array('@accounts' => url('admin/config/people/accounts'))) . ' ';
+ $output .= ' ';
+ return $output;
+ case 'admin/people/create':
+ return '
' . t("This web page allows administrators to register new users. Users' e-mail addresses and usernames must be unique.") . '
';
+ case 'admin/people/permissions':
+ return '
' . t('Permissions let you control what users can do and see on your site. You can define a specific set of permissions for each role. (See the Roles page to create a role). Two important roles to consider are Authenticated Users and Administrators. Any permissions granted to the Authenticated Users role will be given to any user who can log into your site. You can make any role the Administrator role for the site, meaning this will be granted all new permissions automatically. You can do this on the User Settings page. You should be careful to ensure that only trusted users are given this access and level of control of your site.', array('@role' => url('admin/people/permissions/roles'), '@settings' => url('admin/config/people/accounts'))) . '
';
+ case 'admin/people/permissions/roles':
+ $output = '
' . t('Roles allow you to fine tune the security and administration of Drupal. A role defines a group of users that have certain privileges as defined on the permissions page . Examples of roles include: anonymous user, authenticated user, moderator, administrator and so on. In this area you will define the names and order of the roles on your site. It is recommended to order your roles from least permissive (anonymous user) to most permissive (administrator). To delete a role choose "edit role".', array('@permissions' => url('admin/people/permissions'))) . '
';
+ $output .= '
' . t('By default, Drupal comes with two user roles:') . '
';
+ $output .= '
';
+ $output .= '' . t("Anonymous user: this role is used for users that don't have a user account or that are not authenticated.") . ' ';
+ $output .= '' . t('Authenticated user: this role is automatically granted to all logged in users.') . ' ';
+ $output .= ' ';
+ return $output;
+ case 'admin/config/people/accounts/fields':
+ return '
' . t('This form lets administrators add, edit, and arrange fields for storing user data.') . '
';
+ case 'admin/config/people/accounts/display':
+ return '
' . t('This form lets administrators configure how fields should be displayed when rendering a user profile page.') . '
';
+ case 'admin/people/search':
+ return '
' . t('Enter a simple pattern ("*" may be used as a wildcard match) to search for a username or e-mail address. For example, one may search for "br" and Drupal might return "brian", "brad", and "brenda@example.com".') . '
';
+ }
+}
+
+/**
+ * Invokes a user hook in every module.
+ *
+ * We cannot use module_invoke() for this, because the arguments need to
+ * be passed by reference.
+ *
+ * @param $type
+ * A text string that controls which user hook to invoke. Valid choices are:
+ * - cancel: Invokes hook_user_cancel().
+ * - insert: Invokes hook_user_insert().
+ * - login: Invokes hook_user_login().
+ * - presave: Invokes hook_user_presave().
+ * - update: Invokes hook_user_update().
+ * @param $edit
+ * An associative array variable containing form values to be passed
+ * as the first parameter of the hook function.
+ * @param $account
+ * The user account object to be passed as the second parameter of the hook
+ * function.
+ * @param $category
+ * The category of user information being acted upon.
+ */
+function user_module_invoke($type, &$edit, $account, $category = NULL) {
+ foreach (module_implements('user_' . $type) as $module) {
+ $function = $module . '_user_' . $type;
+ $function($edit, $account, $category);
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function user_theme() {
+ return array(
+ 'user_picture' => array(
+ 'variables' => array('account' => NULL),
+ 'template' => 'user-picture',
+ ),
+ 'user_profile' => array(
+ 'render element' => 'elements',
+ 'template' => 'user-profile',
+ 'file' => 'user.pages.inc',
+ ),
+ 'user_profile_category' => array(
+ 'render element' => 'element',
+ 'template' => 'user-profile-category',
+ 'file' => 'user.pages.inc',
+ ),
+ 'user_profile_item' => array(
+ 'render element' => 'element',
+ 'template' => 'user-profile-item',
+ 'file' => 'user.pages.inc',
+ ),
+ 'user_list' => array(
+ 'variables' => array('users' => NULL, 'title' => NULL),
+ ),
+ 'user_admin_permissions' => array(
+ 'render element' => 'form',
+ 'file' => 'user.admin.inc',
+ ),
+ 'user_admin_roles' => array(
+ 'render element' => 'form',
+ 'file' => 'user.admin.inc',
+ ),
+ 'user_permission_description' => array(
+ 'variables' => array('permission_item' => NULL, 'hide' => NULL),
+ 'file' => 'user.admin.inc',
+ ),
+ 'user_signature' => array(
+ 'variables' => array('signature' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function user_entity_info() {
+ $return = array(
+ 'user' => array(
+ 'label' => t('User'),
+ 'controller class' => 'UserController',
+ 'base table' => 'users',
+ 'uri callback' => 'user_uri',
+ 'label callback' => 'format_username',
+ 'fieldable' => TRUE,
+ // $user->language is only the preferred user language for the user
+ // interface textual elements. As it is not necessarily related to the
+ // language assigned to fields, we do not define it as the entity language
+ // key.
+ 'entity keys' => array(
+ 'id' => 'uid',
+ ),
+ 'bundles' => array(
+ 'user' => array(
+ 'label' => t('User'),
+ 'admin' => array(
+ 'path' => 'admin/config/people/accounts',
+ 'access arguments' => array('administer users'),
+ ),
+ ),
+ ),
+ 'view modes' => array(
+ 'full' => array(
+ 'label' => t('User account'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ ),
+ );
+ return $return;
+}
+
+/**
+ * Implements callback_entity_info_uri().
+ */
+function user_uri($user) {
+ return array(
+ 'path' => 'user/' . $user->uid,
+ );
+}
+
+/**
+ * Implements hook_field_info_alter().
+ */
+function user_field_info_alter(&$info) {
+ // Add the 'user_register_form' instance setting to all field types.
+ foreach ($info as $field_type => &$field_type_info) {
+ $field_type_info += array('instance_settings' => array());
+ $field_type_info['instance_settings'] += array(
+ 'user_register_form' => FALSE,
+ );
+ }
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function user_field_extra_fields() {
+ $return['user']['user'] = array(
+ 'form' => array(
+ 'account' => array(
+ 'label' => t('User name and password'),
+ 'description' => t('User module account form elements.'),
+ 'weight' => -10,
+ ),
+ 'timezone' => array(
+ 'label' => t('Timezone'),
+ 'description' => t('User module timezone form element.'),
+ 'weight' => 6,
+ ),
+ ),
+ 'display' => array(
+ 'summary' => array(
+ 'label' => t('History'),
+ 'description' => t('User module history view element.'),
+ 'weight' => 5,
+ ),
+ ),
+ );
+
+ return $return;
+}
+
+/**
+ * Fetches a user object based on an external authentication source.
+ *
+ * @param string $authname
+ * The external authentication username.
+ *
+ * @return
+ * A fully-loaded user object if the user is found or FALSE if not found.
+ */
+function user_external_load($authname) {
+ $uid = db_query("SELECT uid FROM {authmap} WHERE authname = :authname", array(':authname' => $authname))->fetchField();
+
+ if ($uid) {
+ return user_load($uid);
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Load multiple users based on certain conditions.
+ *
+ * This function should be used whenever you need to load more than one user
+ * from the database. Users are loaded into memory and will not require
+ * database access if loaded again during the same page request.
+ *
+ * @param $uids
+ * An array of user IDs.
+ * @param $conditions
+ * (deprecated) An associative array of conditions on the {users}
+ * table, where the keys are the database fields and the values are the
+ * values those fields must have. Instead, it is preferable to use
+ * EntityFieldQuery to retrieve a list of entity IDs loadable by
+ * this function.
+ * @param $reset
+ * A boolean indicating that the internal cache should be reset. Use this if
+ * loading a user object which has been altered during the page request.
+ *
+ * @return
+ * An array of user objects, indexed by uid.
+ *
+ * @see entity_load()
+ * @see user_load()
+ * @see user_load_by_mail()
+ * @see user_load_by_name()
+ * @see EntityFieldQuery
+ *
+ * @todo Remove $conditions in Drupal 8.
+ */
+function user_load_multiple($uids = array(), $conditions = array(), $reset = FALSE) {
+ return entity_load('user', $uids, $conditions, $reset);
+}
+
+/**
+ * Controller class for users.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for user objects.
+ */
+class UserController extends DrupalDefaultEntityController {
+
+ function attachLoad(&$queried_users, $revision_id = FALSE) {
+ // Build an array of user picture IDs so that these can be fetched later.
+ $picture_fids = array();
+ foreach ($queried_users as $key => $record) {
+ $picture_fids[] = $record->picture;
+ $queried_users[$key]->data = unserialize($record->data);
+ $queried_users[$key]->roles = array();
+ if ($record->uid) {
+ $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
+ }
+ else {
+ $queried_users[$record->uid]->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
+ }
+ }
+
+ // Add any additional roles from the database.
+ $result = db_query('SELECT r.rid, r.name, ur.uid FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid IN (:uids)', array(':uids' => array_keys($queried_users)));
+ foreach ($result as $record) {
+ $queried_users[$record->uid]->roles[$record->rid] = $record->name;
+ }
+
+ // Add the full file objects for user pictures if enabled.
+ if (!empty($picture_fids) && variable_get('user_pictures', 0)) {
+ $pictures = file_load_multiple($picture_fids);
+ foreach ($queried_users as $account) {
+ if (!empty($account->picture) && isset($pictures[$account->picture])) {
+ $account->picture = $pictures[$account->picture];
+ }
+ else {
+ $account->picture = NULL;
+ }
+ }
+ }
+ // Call the default attachLoad() method. This will add fields and call
+ // hook_user_load().
+ parent::attachLoad($queried_users, $revision_id);
+ }
+}
+
+/**
+ * Loads a user object.
+ *
+ * Drupal has a global $user object, which represents the currently-logged-in
+ * user. So to avoid confusion and to avoid clobbering the global $user object,
+ * it is a good idea to assign the result of this function to a different local
+ * variable, generally $account. If you actually do want to act as the user you
+ * are loading, it is essential to call drupal_save_session(FALSE); first.
+ * See
+ * @link http://drupal.org/node/218104 Safely impersonating another user @endlink
+ * for more information.
+ *
+ * @param $uid
+ * Integer specifying the user ID to load.
+ * @param $reset
+ * TRUE to reset the internal cache and load from the database; FALSE
+ * (default) to load from the internal cache, if set.
+ *
+ * @return
+ * A fully-loaded user object upon successful user load, or FALSE if the user
+ * cannot be loaded.
+ *
+ * @see user_load_multiple()
+ */
+function user_load($uid, $reset = FALSE) {
+ $users = user_load_multiple(array($uid), array(), $reset);
+ return reset($users);
+}
+
+/**
+ * Fetch a user object by email address.
+ *
+ * @param $mail
+ * String with the account's e-mail address.
+ * @return
+ * A fully-loaded $user object upon successful user load or FALSE if user
+ * cannot be loaded.
+ *
+ * @see user_load_multiple()
+ */
+function user_load_by_mail($mail) {
+ $users = user_load_multiple(array(), array('mail' => $mail));
+ return reset($users);
+}
+
+/**
+ * Fetch a user object by account name.
+ *
+ * @param $name
+ * String with the account's user name.
+ * @return
+ * A fully-loaded $user object upon successful user load or FALSE if user
+ * cannot be loaded.
+ *
+ * @see user_load_multiple()
+ */
+function user_load_by_name($name) {
+ $users = user_load_multiple(array(), array('name' => $name));
+ return reset($users);
+}
+
+/**
+ * Save changes to a user account or add a new user.
+ *
+ * @param $account
+ * (optional) The user object to modify or add. If you want to modify
+ * an existing user account, you will need to ensure that (a) $account
+ * is an object, and (b) you have set $account->uid to the numeric
+ * user ID of the user account you wish to modify. If you
+ * want to create a new user account, you can set $account->is_new to
+ * TRUE or omit the $account->uid field.
+ * @param $edit
+ * An array of fields and values to save. For example array('name'
+ * => 'My name'). Key / value pairs added to the $edit['data'] will be
+ * serialized and saved in the {users.data} column.
+ * @param $category
+ * (optional) The category for storing profile information in.
+ *
+ * @return
+ * A fully-loaded $user object upon successful save or FALSE if the save failed.
+ */
+function user_save($account, $edit = array(), $category = 'account') {
+ $transaction = db_transaction();
+ try {
+ if (isset($edit['pass']) && strlen(trim($edit['pass'])) > 0) {
+ // Allow alternate password hashing schemes.
+ require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
+ $edit['pass'] = user_hash_password(trim($edit['pass']));
+ // Abort if the hashing failed and returned FALSE.
+ if (!$edit['pass']) {
+ return FALSE;
+ }
+ }
+ else {
+ // Avoid overwriting an existing password with a blank password.
+ unset($edit['pass']);
+ }
+ if (isset($edit['mail'])) {
+ $edit['mail'] = trim($edit['mail']);
+ }
+
+ // Load the stored entity, if any.
+ if (!empty($account->uid) && !isset($account->original)) {
+ $account->original = entity_load_unchanged('user', $account->uid);
+ }
+
+ if (empty($account)) {
+ $account = new stdClass();
+ }
+ if (!isset($account->is_new)) {
+ $account->is_new = empty($account->uid);
+ }
+ // Prepopulate $edit['data'] with the current value of $account->data.
+ // Modules can add to or remove from this array in hook_user_presave().
+ if (!empty($account->data)) {
+ $edit['data'] = !empty($edit['data']) ? array_merge($account->data, $edit['data']) : $account->data;
+ }
+
+ // Invoke hook_user_presave() for all modules.
+ user_module_invoke('presave', $edit, $account, $category);
+
+ // Invoke presave operations of Field Attach API and Entity API. Those APIs
+ // require a fully-fledged and updated entity object. Therefore, we need to
+ // copy any new property values of $edit into it.
+ foreach ($edit as $key => $value) {
+ $account->$key = $value;
+ }
+ field_attach_presave('user', $account);
+ module_invoke_all('entity_presave', $account, 'user');
+
+ if (is_object($account) && !$account->is_new) {
+ // Process picture uploads.
+ if (!empty($account->picture->fid) && (!isset($account->original->picture->fid) || $account->picture->fid != $account->original->picture->fid)) {
+ $picture = $account->picture;
+ // If the picture is a temporary file move it to its final location and
+ // make it permanent.
+ if (!$picture->status) {
+ $info = image_get_info($picture->uri);
+ $picture_directory = file_default_scheme() . '://' . variable_get('user_picture_path', 'pictures');
+
+ // Prepare the pictures directory.
+ file_prepare_directory($picture_directory, FILE_CREATE_DIRECTORY);
+ $destination = file_stream_wrapper_uri_normalize($picture_directory . '/picture-' . $account->uid . '-' . REQUEST_TIME . '.' . $info['extension']);
+
+ // Move the temporary file into the final location.
+ if ($picture = file_move($picture, $destination, FILE_EXISTS_RENAME)) {
+ $picture->status = FILE_STATUS_PERMANENT;
+ $account->picture = file_save($picture);
+ file_usage_add($picture, 'user', 'user', $account->uid);
+ }
+ }
+ // Delete the previous picture if it was deleted or replaced.
+ if (!empty($account->original->picture->fid)) {
+ file_usage_delete($account->original->picture, 'user', 'user', $account->uid);
+ file_delete($account->original->picture);
+ }
+ }
+ elseif (isset($edit['picture_delete']) && $edit['picture_delete']) {
+ file_usage_delete($account->original->picture, 'user', 'user', $account->uid);
+ file_delete($account->original->picture);
+ }
+ // Save the picture object, if it is set. drupal_write_record() expects
+ // $account->picture to be a FID.
+ $picture = empty($account->picture) ? NULL : $account->picture;
+ $account->picture = empty($account->picture->fid) ? 0 : $account->picture->fid;
+
+ // Do not allow 'uid' to be changed.
+ $account->uid = $account->original->uid;
+ // Save changes to the user table.
+ $success = drupal_write_record('users', $account, 'uid');
+ // Restore the picture object.
+ $account->picture = $picture;
+ if ($success === FALSE) {
+ // The query failed - better to abort the save than risk further
+ // data loss.
+ return FALSE;
+ }
+
+ // Reload user roles if provided.
+ if ($account->roles != $account->original->roles) {
+ db_delete('users_roles')
+ ->condition('uid', $account->uid)
+ ->execute();
+
+ $query = db_insert('users_roles')->fields(array('uid', 'rid'));
+ foreach (array_keys($account->roles) as $rid) {
+ if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+ $query->values(array(
+ 'uid' => $account->uid,
+ 'rid' => $rid,
+ ));
+ }
+ }
+ $query->execute();
+ }
+
+ // Delete a blocked user's sessions to kick them if they are online.
+ if ($account->original->status != $account->status && $account->status == 0) {
+ drupal_session_destroy_uid($account->uid);
+ }
+
+ // If the password changed, delete all open sessions and recreate
+ // the current one.
+ if ($account->pass != $account->original->pass) {
+ drupal_session_destroy_uid($account->uid);
+ if ($account->uid == $GLOBALS['user']->uid) {
+ drupal_session_regenerate();
+ }
+ }
+
+ // Save Field data.
+ field_attach_update('user', $account);
+
+ // Send emails after we have the new user object.
+ if ($account->status != $account->original->status) {
+ // The user's status is changing; conditionally send notification email.
+ $op = $account->status == 1 ? 'status_activated' : 'status_blocked';
+ _user_mail_notify($op, $account);
+ }
+
+ // Update $edit with any interim changes to $account.
+ foreach ($account as $key => $value) {
+ if (!property_exists($account->original, $key) || $value !== $account->original->$key) {
+ $edit[$key] = $value;
+ }
+ }
+ user_module_invoke('update', $edit, $account, $category);
+ module_invoke_all('entity_update', $account, 'user');
+ }
+ else {
+ // Allow 'uid' to be set by the caller. There is no danger of writing an
+ // existing user as drupal_write_record will do an INSERT.
+ if (empty($account->uid)) {
+ $account->uid = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField());
+ }
+ // Allow 'created' to be set by the caller.
+ if (!isset($account->created)) {
+ $account->created = REQUEST_TIME;
+ }
+ $success = drupal_write_record('users', $account);
+ if ($success === FALSE) {
+ // On a failed INSERT some other existing user's uid may be returned.
+ // We must abort to avoid overwriting their account.
+ return FALSE;
+ }
+
+ // Make sure $account is properly initialized.
+ $account->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
+
+ field_attach_insert('user', $account);
+ $edit = (array) $account;
+ user_module_invoke('insert', $edit, $account, $category);
+ module_invoke_all('entity_insert', $account, 'user');
+
+ // Save user roles. Skip built-in roles, and ones that were already saved
+ // to the database during hook calls.
+ $rids_to_skip = array_merge(array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID), db_query('SELECT rid FROM {users_roles} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol());
+ if ($rids_to_save = array_diff(array_keys($account->roles), $rids_to_skip)) {
+ $query = db_insert('users_roles')->fields(array('uid', 'rid'));
+ foreach ($rids_to_save as $rid) {
+ $query->values(array(
+ 'uid' => $account->uid,
+ 'rid' => $rid,
+ ));
+ }
+ $query->execute();
+ }
+ }
+ // Clear internal properties.
+ unset($account->is_new);
+ unset($account->original);
+ // Clear the static loading cache.
+ entity_get_controller('user')->resetCache(array($account->uid));
+
+ return $account;
+ }
+ catch (Exception $e) {
+ $transaction->rollback();
+ watchdog_exception('user', $e);
+ throw $e;
+ }
+}
+
+/**
+ * Verify the syntax of the given name.
+ */
+function user_validate_name($name) {
+ if (!$name) {
+ return t('You must enter a username.');
+ }
+ if (substr($name, 0, 1) == ' ') {
+ return t('The username cannot begin with a space.');
+ }
+ if (substr($name, -1) == ' ') {
+ return t('The username cannot end with a space.');
+ }
+ if (strpos($name, ' ') !== FALSE) {
+ return t('The username cannot contain multiple spaces in a row.');
+ }
+ if (preg_match('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', $name)) {
+ return t('The username contains an illegal character.');
+ }
+ if (preg_match('/[\x{80}-\x{A0}' . // Non-printable ISO-8859-1 + NBSP
+ '\x{AD}' . // Soft-hyphen
+ '\x{2000}-\x{200F}' . // Various space characters
+ '\x{2028}-\x{202F}' . // Bidirectional text overrides
+ '\x{205F}-\x{206F}' . // Various text hinting characters
+ '\x{FEFF}' . // Byte order mark
+ '\x{FF01}-\x{FF60}' . // Full-width latin
+ '\x{FFF9}-\x{FFFD}' . // Replacement characters
+ '\x{0}-\x{1F}]/u', // NULL byte and control characters
+ $name)) {
+ return t('The username contains an illegal character.');
+ }
+ if (drupal_strlen($name) > USERNAME_MAX_LENGTH) {
+ return t('The username %name is too long: it must be %max characters or less.', array('%name' => $name, '%max' => USERNAME_MAX_LENGTH));
+ }
+}
+
+/**
+ * Validates a user's email address.
+ *
+ * Checks that a user's email address exists and follows all standard
+ * validation rules. Returns error messages when the address is invalid.
+ *
+ * @param $mail
+ * A user's email address.
+ *
+ * @return
+ * If the address is invalid, a human-readable error message is returned.
+ * If the address is valid, nothing is returned.
+ */
+function user_validate_mail($mail) {
+ if (!$mail) {
+ return t('You must enter an e-mail address.');
+ }
+ if (!valid_email_address($mail)) {
+ return t('The e-mail address %mail is not valid.', array('%mail' => $mail));
+ }
+}
+
+/**
+ * Validates an image uploaded by a user.
+ *
+ * @see user_account_form()
+ */
+function user_validate_picture(&$form, &$form_state) {
+ // If required, validate the uploaded picture.
+ $validators = array(
+ 'file_validate_is_image' => array(),
+ 'file_validate_image_resolution' => array(variable_get('user_picture_dimensions', '85x85')),
+ 'file_validate_size' => array(variable_get('user_picture_file_size', '30') * 1024),
+ );
+
+ // Save the file as a temporary file.
+ $file = file_save_upload('picture_upload', $validators);
+ if ($file === FALSE) {
+ form_set_error('picture_upload', t("Failed to upload the picture image; the %directory directory doesn't exist or is not writable.", array('%directory' => variable_get('user_picture_path', 'pictures'))));
+ }
+ elseif ($file !== NULL) {
+ $form_state['values']['picture_upload'] = $file;
+ }
+}
+
+/**
+ * Generate a random alphanumeric password.
+ */
+function user_password($length = 10) {
+ // This variable contains the list of allowable characters for the
+ // password. Note that the number 0 and the letter 'O' have been
+ // removed to avoid confusion between the two. The same is true
+ // of 'I', 1, and 'l'.
+ $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+
+ // Zero-based count of characters in the allowable list:
+ $len = strlen($allowable_characters) - 1;
+
+ // Declare the password as a blank string.
+ $pass = '';
+
+ // Loop the number of times specified by $length.
+ for ($i = 0; $i < $length; $i++) {
+ do {
+ // Find a secure random number within the range needed.
+ $index = ord(drupal_random_bytes(1));
+ } while ($index > $len);
+
+ // Each iteration, pick a random character from the
+ // allowable string and append it to the password:
+ $pass .= $allowable_characters[$index];
+ }
+
+ return $pass;
+}
+
+/**
+ * Determine the permissions for one or more roles.
+ *
+ * @param $roles
+ * An array whose keys are the role IDs of interest, such as $user->roles.
+ *
+ * @return
+ * If $roles is a non-empty array, an array indexed by role ID is returned.
+ * Each value is an array whose keys are the permission strings for the given
+ * role ID. If $roles is empty nothing is returned.
+ */
+function user_role_permissions($roles = array()) {
+ $cache = &drupal_static(__FUNCTION__, array());
+
+ $role_permissions = $fetch = array();
+
+ if ($roles) {
+ foreach ($roles as $rid => $name) {
+ if (isset($cache[$rid])) {
+ $role_permissions[$rid] = $cache[$rid];
+ }
+ else {
+ // Add this rid to the list of those needing to be fetched.
+ $fetch[] = $rid;
+ // Prepare in case no permissions are returned.
+ $cache[$rid] = array();
+ }
+ }
+
+ if ($fetch) {
+ // Get from the database permissions that were not in the static variable.
+ // Only role IDs with at least one permission assigned will return rows.
+ $result = db_query("SELECT rid, permission FROM {role_permission} WHERE rid IN (:fetch)", array(':fetch' => $fetch));
+
+ foreach ($result as $row) {
+ $cache[$row->rid][$row->permission] = TRUE;
+ }
+ foreach ($fetch as $rid) {
+ // For every rid, we know we at least assigned an empty array.
+ $role_permissions[$rid] = $cache[$rid];
+ }
+ }
+ }
+
+ return $role_permissions;
+}
+
+/**
+ * Determine whether the user has a given privilege.
+ *
+ * @param $string
+ * The permission, such as "administer nodes", being checked for.
+ * @param $account
+ * (optional) The account to check, if not given use currently logged in user.
+ *
+ * @return
+ * Boolean TRUE if the user has the requested permission.
+ *
+ * All permission checks in Drupal should go through this function. This
+ * way, we guarantee consistent behavior, and ensure that the superuser
+ * can perform all actions.
+ */
+function user_access($string, $account = NULL) {
+ global $user;
+
+ if (!isset($account)) {
+ $account = $user;
+ }
+
+ // User #1 has all privileges:
+ if ($account->uid == 1) {
+ return TRUE;
+ }
+
+ // To reduce the number of SQL queries, we cache the user's permissions
+ // in a static variable.
+ // Use the advanced drupal_static() pattern, since this is called very often.
+ static $drupal_static_fast;
+ if (!isset($drupal_static_fast)) {
+ $drupal_static_fast['perm'] = &drupal_static(__FUNCTION__);
+ }
+ $perm = &$drupal_static_fast['perm'];
+ if (!isset($perm[$account->uid])) {
+ $role_permissions = user_role_permissions($account->roles);
+
+ $perms = array();
+ foreach ($role_permissions as $one_role) {
+ $perms += $one_role;
+ }
+ $perm[$account->uid] = $perms;
+ }
+
+ return isset($perm[$account->uid][$string]);
+}
+
+/**
+ * Checks for usernames blocked by user administration.
+ *
+ * @param $name
+ * A string containing a name of the user.
+ *
+ * @return
+ * Object with property 'name' (the user name), if the user is blocked;
+ * FALSE if the user is not blocked.
+ */
+function user_is_blocked($name) {
+ return db_select('users')
+ ->fields('users', array('name'))
+ ->condition('name', db_like($name), 'LIKE')
+ ->condition('status', 0)
+ ->execute()->fetchObject();
+}
+
+/**
+ * Checks if a user has a role.
+ *
+ * @param int $rid
+ * A role ID.
+ *
+ * @param object|null $account
+ * (optional) A user account. Defaults to the current user.
+ *
+ * @return bool
+ * TRUE if the user has the role, or FALSE if not.
+ */
+function user_has_role($rid, $account = NULL) {
+ if (!$account) {
+ $account = $GLOBALS['user'];
+ }
+
+ return isset($account->roles[$rid]);
+}
+
+/**
+ * Implements hook_permission().
+ */
+function user_permission() {
+ return array(
+ 'administer permissions' => array(
+ 'title' => t('Administer permissions'),
+ 'restrict access' => TRUE,
+ ),
+ 'administer users' => array(
+ 'title' => t('Administer users'),
+ 'restrict access' => TRUE,
+ ),
+ 'access user profiles' => array(
+ 'title' => t('View user profiles'),
+ ),
+ 'change own username' => array(
+ 'title' => t('Change own username'),
+ ),
+ 'cancel account' => array(
+ 'title' => t('Cancel own user account'),
+ 'description' => t('Note: content may be kept, unpublished, deleted or transferred to the %anonymous-name user depending on the configured
user settings .', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')), '@user-settings-url' => url('admin/config/people/accounts'))),
+ ),
+ 'select account cancellation method' => array(
+ 'title' => t('Select method for cancelling own account'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_file_download().
+ *
+ * Ensure that user pictures (avatars) are always downloadable.
+ */
+function user_file_download($uri) {
+ if (strpos(file_uri_target($uri), variable_get('user_picture_path', 'pictures') . '/picture-') === 0) {
+ $info = image_get_info($uri);
+ return array('Content-Type' => $info['mime_type']);
+ }
+}
+
+/**
+ * Implements hook_file_move().
+ */
+function user_file_move($file, $source) {
+ // If a user's picture is replaced with a new one, update the record in
+ // the users table.
+ if (isset($file->fid) && isset($source->fid) && $file->fid != $source->fid) {
+ db_update('users')
+ ->fields(array(
+ 'picture' => $file->fid,
+ ))
+ ->condition('picture', $source->fid)
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_file_delete().
+ */
+function user_file_delete($file) {
+ // Remove any references to the file.
+ db_update('users')
+ ->fields(array('picture' => 0))
+ ->condition('picture', $file->fid)
+ ->execute();
+}
+
+/**
+ * Implements hook_search_info().
+ */
+function user_search_info() {
+ return array(
+ 'title' => 'Users',
+ );
+}
+
+/**
+ * Implements hook_search_access().
+ */
+function user_search_access() {
+ return user_access('access user profiles');
+}
+
+/**
+ * Implements hook_search_execute().
+ */
+function user_search_execute($keys = NULL, $conditions = NULL) {
+ $find = array();
+ // Escape for LIKE matching.
+ $keys = db_like($keys);
+ // Replace wildcards with MySQL/PostgreSQL wildcards.
+ $keys = preg_replace('!\*+!', '%', $keys);
+ $query = db_select('users')->extend('PagerDefault');
+ $query->fields('users', array('uid'));
+ if (user_access('administer users')) {
+ // Administrators can also search in the otherwise private email field,
+ // and they don't need to be restricted to only active users.
+ $query->fields('users', array('mail'));
+ $query->condition(db_or()->
+ condition('name', '%' . $keys . '%', 'LIKE')->
+ condition('mail', '%' . $keys . '%', 'LIKE'));
+ }
+ else {
+ // Regular users can only search via usernames, and we do not show them
+ // blocked accounts.
+ $query->condition('name', '%' . $keys . '%', 'LIKE')
+ ->condition('status', 1);
+ }
+ $uids = $query
+ ->limit(15)
+ ->execute()
+ ->fetchCol();
+ $accounts = user_load_multiple($uids);
+
+ $results = array();
+ foreach ($accounts as $account) {
+ $result = array(
+ 'title' => format_username($account),
+ 'link' => url('user/' . $account->uid, array('absolute' => TRUE)),
+ );
+ if (user_access('administer users')) {
+ $result['title'] .= ' (' . $account->mail . ')';
+ }
+ $results[] = $result;
+ }
+
+ return $results;
+}
+
+/**
+ * Implements hook_element_info().
+ */
+function user_element_info() {
+ $types['user_profile_category'] = array(
+ '#theme_wrappers' => array('user_profile_category'),
+ );
+ $types['user_profile_item'] = array(
+ '#theme' => 'user_profile_item',
+ );
+ return $types;
+}
+
+/**
+ * Implements hook_user_view().
+ */
+function user_user_view($account) {
+ $account->content['user_picture'] = array(
+ '#markup' => theme('user_picture', array('account' => $account)),
+ '#weight' => -10,
+ );
+ if (!isset($account->content['summary'])) {
+ $account->content['summary'] = array();
+ }
+ $account->content['summary'] += array(
+ '#type' => 'user_profile_category',
+ '#attributes' => array('class' => array('user-member')),
+ '#weight' => 5,
+ '#title' => t('History'),
+ );
+ $account->content['summary']['member_for'] = array(
+ '#type' => 'user_profile_item',
+ '#title' => t('Member for'),
+ '#markup' => format_interval(REQUEST_TIME - $account->created),
+ );
+}
+
+/**
+ * Helper function to add default user account fields to user registration and edit form.
+ *
+ * @see user_account_form_validate()
+ * @see user_validate_current_pass()
+ * @see user_validate_picture()
+ * @see user_validate_mail()
+ */
+function user_account_form(&$form, &$form_state) {
+ global $user;
+
+ $account = $form['#user'];
+ $register = ($form['#user']->uid > 0 ? FALSE : TRUE);
+
+ $admin = user_access('administer users');
+
+ $form['#validate'][] = 'user_account_form_validate';
+
+ // Account information.
+ $form['account'] = array(
+ '#type' => 'container',
+ '#weight' => -10,
+ );
+ // Only show name field on registration form or user can change own username.
+ $form['account']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Username'),
+ '#maxlength' => USERNAME_MAX_LENGTH,
+ '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'),
+ '#required' => TRUE,
+ '#attributes' => array('class' => array('username')),
+ '#default_value' => (!$register ? $account->name : ''),
+ '#access' => ($register || ($user->uid == $account->uid && user_access('change own username')) || $admin),
+ '#weight' => -10,
+ );
+
+ $form['account']['mail'] = array(
+ '#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#maxlength' => EMAIL_MAX_LENGTH,
+ '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
+ '#required' => TRUE,
+ '#default_value' => (!$register ? $account->mail : ''),
+ );
+
+ // Display password field only for existing users or when user is allowed to
+ // assign a password during registration.
+ if (!$register) {
+ $form['account']['pass'] = array(
+ '#type' => 'password_confirm',
+ '#size' => 25,
+ '#description' => t('To change the current user password, enter the new password in both fields.'),
+ );
+ // To skip the current password field, the user must have logged in via a
+ // one-time link and have the token in the URL. Store this in $form_state
+ // so it persists even on subsequent Ajax requests.
+ if (!isset($form_state['user_pass_reset'])) {
+ $form_state['user_pass_reset'] = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]);
+ }
+ $protected_values = array();
+ $current_pass_description = '';
+ // The user may only change their own password without their current
+ // password if they logged in via a one-time login link.
+ if (!$form_state['user_pass_reset']) {
+ $protected_values['mail'] = $form['account']['mail']['#title'];
+ $protected_values['pass'] = t('Password');
+ $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
+ $current_pass_description = t('Enter your current password to change the %mail or %pass. !request_new.', array('%mail' => $protected_values['mail'], '%pass' => $protected_values['pass'], '!request_new' => $request_new));
+ }
+ // The user must enter their current password to change to a new one.
+ if ($user->uid == $account->uid) {
+ $form['account']['current_pass_required_values'] = array(
+ '#type' => 'value',
+ '#value' => $protected_values,
+ );
+ $form['account']['current_pass'] = array(
+ '#type' => 'password',
+ '#title' => t('Current password'),
+ '#size' => 25,
+ '#access' => !empty($protected_values),
+ '#description' => $current_pass_description,
+ '#weight' => -5,
+ // Do not let web browsers remember this password, since we are trying
+ // to confirm that the person submitting the form actually knows the
+ // current one.
+ '#attributes' => array('autocomplete' => 'off'),
+ );
+ $form['#validate'][] = 'user_validate_current_pass';
+ }
+ }
+ elseif (!variable_get('user_email_verification', TRUE) || $admin) {
+ $form['account']['pass'] = array(
+ '#type' => 'password_confirm',
+ '#size' => 25,
+ '#description' => t('Provide a password for the new account in both fields.'),
+ '#required' => TRUE,
+ );
+ }
+
+ if ($admin) {
+ $status = isset($account->status) ? $account->status : 1;
+ }
+ else {
+ $status = $register ? variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS : $account->status;
+ }
+ $form['account']['status'] = array(
+ '#type' => 'radios',
+ '#title' => t('Status'),
+ '#default_value' => $status,
+ '#options' => array(t('Blocked'), t('Active')),
+ '#access' => $admin,
+ );
+
+ $roles = array_map('check_plain', user_roles(TRUE));
+ // The disabled checkbox subelement for the 'authenticated user' role
+ // must be generated separately and added to the checkboxes element,
+ // because of a limitation in Form API not supporting a single disabled
+ // checkbox within a set of checkboxes.
+ // @todo This should be solved more elegantly. See issue #119038.
+ $checkbox_authenticated = array(
+ '#type' => 'checkbox',
+ '#title' => $roles[DRUPAL_AUTHENTICATED_RID],
+ '#default_value' => TRUE,
+ '#disabled' => TRUE,
+ );
+ unset($roles[DRUPAL_AUTHENTICATED_RID]);
+ $form['account']['roles'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Roles'),
+ '#default_value' => (!$register && !empty($account->roles) ? array_keys(array_filter($account->roles)) : array()),
+ '#options' => $roles,
+ '#access' => $roles && user_access('administer permissions'),
+ DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated,
+ );
+
+ $form['account']['notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user of new account'),
+ '#access' => $register && $admin,
+ );
+
+ // Signature.
+ $form['signature_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Signature settings'),
+ '#weight' => 1,
+ '#access' => (!$register && variable_get('user_signatures', 0)),
+ );
+
+ $form['signature_settings']['signature'] = array(
+ '#type' => 'text_format',
+ '#title' => t('Signature'),
+ '#default_value' => isset($account->signature) ? $account->signature : '',
+ '#description' => t('Your signature will be publicly displayed at the end of your comments.'),
+ '#format' => isset($account->signature_format) ? $account->signature_format : NULL,
+ );
+
+ // Picture/avatar.
+ $form['picture'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Picture'),
+ '#weight' => 1,
+ '#access' => (!$register && variable_get('user_pictures', 0)),
+ );
+ $form['picture']['picture'] = array(
+ '#type' => 'value',
+ '#value' => isset($account->picture) ? $account->picture : NULL,
+ );
+ $form['picture']['picture_current'] = array(
+ '#markup' => theme('user_picture', array('account' => $account)),
+ );
+ $form['picture']['picture_delete'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Delete picture'),
+ '#access' => !empty($account->picture->fid),
+ '#description' => t('Check this box to delete your current picture.'),
+ );
+ $form['picture']['picture_upload'] = array(
+ '#type' => 'file',
+ '#title' => t('Upload picture'),
+ '#size' => 48,
+ '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')),
+ );
+ $form['#validate'][] = 'user_validate_picture';
+}
+
+/**
+ * Form validation handler for the current password on the user_account_form().
+ *
+ * @see user_account_form()
+ */
+function user_validate_current_pass(&$form, &$form_state) {
+ $account = $form['#user'];
+ foreach ($form_state['values']['current_pass_required_values'] as $key => $name) {
+ // This validation only works for required textfields (like mail) or
+ // form values like password_confirm that have their own validation
+ // that prevent them from being empty if they are changed.
+ if ((strlen(trim($form_state['values'][$key])) > 0) && ($form_state['values'][$key] != $account->$key)) {
+ require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
+ $current_pass_failed = strlen(trim($form_state['values']['current_pass'])) == 0 || !user_check_password($form_state['values']['current_pass'], $account);
+ if ($current_pass_failed) {
+ form_set_error('current_pass', t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => $name)));
+ form_set_error($key);
+ }
+ // We only need to check the password once.
+ break;
+ }
+ }
+}
+
+/**
+ * Form validation handler for user_account_form().
+ *
+ * @see user_account_form()
+ */
+function user_account_form_validate($form, &$form_state) {
+ if ($form['#user_category'] == 'account' || $form['#user_category'] == 'register') {
+ $account = $form['#user'];
+ // Validate new or changing username.
+ if (isset($form_state['values']['name'])) {
+ if ($error = user_validate_name($form_state['values']['name'])) {
+ form_set_error('name', $error);
+ }
+ elseif ((bool) db_select('users')->fields('users', array('uid'))->condition('uid', $account->uid, '<>')->condition('name', db_like($form_state['values']['name']), 'LIKE')->range(0, 1)->execute()->fetchField()) {
+ form_set_error('name', t('The name %name is already taken.', array('%name' => $form_state['values']['name'])));
+ }
+ }
+
+ // Trim whitespace from mail, to prevent confusing 'e-mail not valid'
+ // warnings often caused by cutting and pasting.
+ $mail = trim($form_state['values']['mail']);
+ form_set_value($form['account']['mail'], $mail, $form_state);
+
+ // Validate the e-mail address, and check if it is taken by an existing user.
+ if ($error = user_validate_mail($form_state['values']['mail'])) {
+ form_set_error('mail', $error);
+ }
+ elseif ((bool) db_select('users')->fields('users', array('uid'))->condition('uid', $account->uid, '<>')->condition('mail', db_like($form_state['values']['mail']), 'LIKE')->range(0, 1)->execute()->fetchField()) {
+ // Format error message dependent on whether the user is logged in or not.
+ if ($GLOBALS['user']->uid) {
+ form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $form_state['values']['mail'])));
+ }
+ else {
+ form_set_error('mail', t('The e-mail address %email is already registered.
Have you forgotten your password? ', array('%email' => $form_state['values']['mail'], '@password' => url('user/password'))));
+ }
+ }
+
+ // Make sure the signature isn't longer than the size of the database field.
+ // Signatures are disabled by default, so make sure it exists first.
+ if (isset($form_state['values']['signature'])) {
+ // Move text format for user signature into 'signature_format'.
+ $form_state['values']['signature_format'] = $form_state['values']['signature']['format'];
+ // Move text value for user signature into 'signature'.
+ $form_state['values']['signature'] = $form_state['values']['signature']['value'];
+
+ $user_schema = drupal_get_schema('users');
+ if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) {
+ form_set_error('signature', t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length'])));
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_user_presave().
+ */
+function user_user_presave(&$edit, $account, $category) {
+ if ($category == 'account' || $category == 'register') {
+ if (!empty($edit['picture_upload'])) {
+ $edit['picture'] = $edit['picture_upload'];
+ }
+ // Delete picture if requested, and if no replacement picture was given.
+ elseif (!empty($edit['picture_delete'])) {
+ $edit['picture'] = NULL;
+ }
+ }
+
+ // Filter out roles with empty values to avoid granting extra roles when
+ // processing custom form submissions.
+ if (isset($edit['roles'])) {
+ $edit['roles'] = array_filter($edit['roles']);
+ }
+
+ // Move account cancellation information into $user->data.
+ foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
+ if (isset($edit[$key])) {
+ $edit['data'][$key] = $edit[$key];
+ }
+ }
+}
+
+/**
+ * Implements hook_user_categories().
+ */
+function user_user_categories() {
+ return array(array(
+ 'name' => 'account',
+ 'title' => t('Account settings'),
+ 'weight' => 1,
+ ));
+}
+
+function user_login_block($form) {
+ $form['#action'] = url(current_path(), array('query' => drupal_get_destination(), 'external' => FALSE));
+ $form['#id'] = 'user-login-form';
+ $form['#validate'] = user_login_default_validators();
+ $form['#submit'][] = 'user_login_submit';
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#maxlength' => USERNAME_MAX_LENGTH,
+ '#size' => 15,
+ '#required' => TRUE,
+ );
+ $form['pass'] = array('#type' => 'password',
+ '#title' => t('Password'),
+ '#size' => 15,
+ '#required' => TRUE,
+ );
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit',
+ '#value' => t('Log in'),
+ );
+ $items = array();
+ if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) {
+ $items[] = l(t('Create new account'), 'user/register', array('attributes' => array('title' => t('Create a new user account.'))));
+ }
+ $items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
+ $form['links'] = array('#markup' => theme('item_list', array('items' => $items)));
+ return $form;
+}
+
+/**
+ * Implements hook_block_info().
+ */
+function user_block_info() {
+ global $user;
+
+ $blocks['login']['info'] = t('User login');
+ // Not worth caching.
+ $blocks['login']['cache'] = DRUPAL_NO_CACHE;
+
+ $blocks['new']['info'] = t('Who\'s new');
+ $blocks['new']['properties']['administrative'] = TRUE;
+
+ // Too dynamic to cache.
+ $blocks['online']['info'] = t('Who\'s online');
+ $blocks['online']['cache'] = DRUPAL_NO_CACHE;
+ $blocks['online']['properties']['administrative'] = TRUE;
+
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_configure().
+ */
+function user_block_configure($delta = '') {
+ global $user;
+
+ switch ($delta) {
+ case 'new':
+ $form['user_block_whois_new_count'] = array(
+ '#type' => 'select',
+ '#title' => t('Number of users to display'),
+ '#default_value' => variable_get('user_block_whois_new_count', 5),
+ '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)),
+ );
+ return $form;
+
+ case 'online':
+ $period = drupal_map_assoc(array(30, 60, 120, 180, 300, 600, 900, 1800, 2700, 3600, 5400, 7200, 10800, 21600, 43200, 86400), 'format_interval');
+ $form['user_block_seconds_online'] = array('#type' => 'select', '#title' => t('User activity'), '#default_value' => variable_get('user_block_seconds_online', 900), '#options' => $period, '#description' => t('A user is considered online for this long after they have last viewed a page.'));
+ $form['user_block_max_list_count'] = array('#type' => 'select', '#title' => t('User list length'), '#default_value' => variable_get('user_block_max_list_count', 10), '#options' => drupal_map_assoc(array(0, 5, 10, 15, 20, 25, 30, 40, 50, 75, 100)), '#description' => t('Maximum number of currently online users to display.'));
+ return $form;
+ }
+}
+
+/**
+ * Implements hook_block_save().
+ */
+function user_block_save($delta = '', $edit = array()) {
+ global $user;
+
+ switch ($delta) {
+ case 'new':
+ variable_set('user_block_whois_new_count', $edit['user_block_whois_new_count']);
+ break;
+
+ case 'online':
+ variable_set('user_block_seconds_online', $edit['user_block_seconds_online']);
+ variable_set('user_block_max_list_count', $edit['user_block_max_list_count']);
+ break;
+ }
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function user_block_view($delta = '') {
+ global $user;
+
+ $block = array();
+
+ switch ($delta) {
+ case 'login':
+ // For usability's sake, avoid showing two login forms on one page.
+ if (!$user->uid && !(arg(0) == 'user' && !is_numeric(arg(1)))) {
+
+ $block['subject'] = t('User login');
+ $block['content'] = drupal_get_form('user_login_block');
+ }
+ return $block;
+
+ case 'new':
+ if (user_access('access content')) {
+ // Retrieve a list of new users who have subsequently accessed the site successfully.
+ $items = db_query_range('SELECT uid, name FROM {users} WHERE status <> 0 AND access <> 0 ORDER BY created DESC', 0, variable_get('user_block_whois_new_count', 5))->fetchAll();
+ $output = theme('user_list', array('users' => $items));
+
+ $block['subject'] = t('Who\'s new');
+ $block['content'] = $output;
+ }
+ return $block;
+
+ case 'online':
+ if (user_access('access content')) {
+ // Count users active within the defined period.
+ $interval = REQUEST_TIME - variable_get('user_block_seconds_online', 900);
+
+ // Perform database queries to gather online user lists. We use s.timestamp
+ // rather than u.access because it is much faster.
+ $authenticated_count = db_query("SELECT COUNT(DISTINCT s.uid) FROM {sessions} s WHERE s.timestamp >= :timestamp AND s.uid > 0", array(':timestamp' => $interval))->fetchField();
+
+ $output = '
' . format_plural($authenticated_count, 'There is currently 1 user online.', 'There are currently @count users online.') . '
';
+
+ // Display a list of currently online users.
+ $max_users = variable_get('user_block_max_list_count', 10);
+ if ($authenticated_count && $max_users) {
+ $items = db_query_range('SELECT u.uid, u.name, MAX(s.timestamp) AS max_timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.timestamp >= :interval AND s.uid > 0 GROUP BY u.uid, u.name ORDER BY max_timestamp DESC', 0, $max_users, array(':interval' => $interval))->fetchAll();
+ $output .= theme('user_list', array('users' => $items));
+ }
+
+ $block['subject'] = t('Who\'s online');
+ $block['content'] = $output;
+ }
+ return $block;
+ }
+}
+
+/**
+ * Process variables for user-picture.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $account: A user, node or comment object with 'name', 'uid' and 'picture'
+ * fields.
+ *
+ * @see user-picture.tpl.php
+ */
+function template_preprocess_user_picture(&$variables) {
+ $variables['user_picture'] = '';
+ if (variable_get('user_pictures', 0)) {
+ $account = $variables['account'];
+ if (!empty($account->picture)) {
+ // @TODO: Ideally this function would only be passed file objects, but
+ // since there's a lot of legacy code that JOINs the {users} table to
+ // {node} or {comments} and passes the results into this function if we
+ // a numeric value in the picture field we'll assume it's a file id
+ // and load it for them. Once we've got user_load_multiple() and
+ // comment_load_multiple() functions the user module will be able to load
+ // the picture files in mass during the object's load process.
+ if (is_numeric($account->picture)) {
+ $account->picture = file_load($account->picture);
+ }
+ if (!empty($account->picture->uri)) {
+ $filepath = $account->picture->uri;
+ }
+ }
+ elseif (variable_get('user_picture_default', '')) {
+ $filepath = variable_get('user_picture_default', '');
+ }
+ if (isset($filepath)) {
+ $alt = t("@user's picture", array('@user' => format_username($account)));
+ // If the image does not have a valid Drupal scheme (for eg. HTTP),
+ // don't load image styles.
+ if (module_exists('image') && file_valid_uri($filepath) && $style = variable_get('user_picture_style', '')) {
+ $variables['user_picture'] = theme('image_style', array('style_name' => $style, 'path' => $filepath, 'alt' => $alt, 'title' => $alt));
+ }
+ else {
+ $variables['user_picture'] = theme('image', array('path' => $filepath, 'alt' => $alt, 'title' => $alt));
+ }
+ if (!empty($account->uid) && user_access('access user profiles')) {
+ $attributes = array('attributes' => array('title' => t('View user profile.')), 'html' => TRUE);
+ $variables['user_picture'] = l($variables['user_picture'], "user/$account->uid", $attributes);
+ }
+ }
+ }
+}
+
+/**
+ * Returns HTML for a list of users.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - users: An array with user objects. Should contain at least the name and
+ * uid.
+ * - title: (optional) Title to pass on to theme_item_list().
+ *
+ * @ingroup themeable
+ */
+function theme_user_list($variables) {
+ $users = $variables['users'];
+ $title = $variables['title'];
+ $items = array();
+
+ if (!empty($users)) {
+ foreach ($users as $user) {
+ $items[] = theme('username', array('account' => $user));
+ }
+ }
+ return theme('item_list', array('items' => $items, 'title' => $title));
+}
+
+/**
+ * Determines if the current user is anonymous.
+ *
+ * @return bool
+ * TRUE if the user is anonymous, FALSE if the user is authenticated.
+ */
+function user_is_anonymous() {
+ // Menu administrators can see items for anonymous when administering.
+ return !$GLOBALS['user']->uid || !empty($GLOBALS['menu_admin']);
+}
+
+/**
+ * Determines if the current user is logged in.
+ *
+ * @return bool
+ * TRUE if the user is logged in, FALSE if the user is anonymous.
+ */
+function user_is_logged_in() {
+ return (bool) $GLOBALS['user']->uid;
+}
+
+/**
+ * Determines if the current user has access to the user registration page.
+ *
+ * @return bool
+ * TRUE if the user is not already logged in and can register for an account.
+ */
+function user_register_access() {
+ return user_is_anonymous() && variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+}
+
+/**
+ * User view access callback.
+ *
+ * @param $account
+ * Can either be a full user object or a $uid.
+ */
+function user_view_access($account) {
+ $uid = is_object($account) ? $account->uid : (int) $account;
+
+ // Never allow access to view the anonymous user account.
+ if ($uid) {
+ // Admins can view all, users can view own profiles at all times.
+ if ($GLOBALS['user']->uid == $uid || user_access('administer users')) {
+ return TRUE;
+ }
+ elseif (user_access('access user profiles')) {
+ // At this point, load the complete account object.
+ if (!is_object($account)) {
+ $account = user_load($uid);
+ }
+ return (is_object($account) && $account->status);
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * Access callback for user account editing.
+ */
+function user_edit_access($account) {
+ return (($GLOBALS['user']->uid == $account->uid) || user_access('administer users')) && $account->uid > 0;
+}
+
+/**
+ * Menu access callback; limit access to account cancellation pages.
+ *
+ * Limit access to users with the 'cancel account' permission or administrative
+ * users, and prevent the anonymous user from cancelling the account.
+ */
+function user_cancel_access($account) {
+ return ((($GLOBALS['user']->uid == $account->uid) && user_access('cancel account')) || user_access('administer users')) && $account->uid > 0;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function user_menu() {
+ $items['user/autocomplete'] = array(
+ 'title' => 'User autocomplete',
+ 'page callback' => 'user_autocomplete',
+ 'access callback' => 'user_access',
+ 'access arguments' => array('access user profiles'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'user.pages.inc',
+ );
+
+ // Registration and login pages.
+ $items['user'] = array(
+ 'title' => 'User account',
+ 'title callback' => 'user_menu_title',
+ 'page callback' => 'user_page',
+ 'access callback' => TRUE,
+ 'file' => 'user.pages.inc',
+ 'weight' => -10,
+ 'menu_name' => 'user-menu',
+ );
+
+ $items['user/login'] = array(
+ 'title' => 'Log in',
+ 'access callback' => 'user_is_anonymous',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ );
+
+ $items['user/register'] = array(
+ 'title' => 'Create new account',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_register_form'),
+ 'access callback' => 'user_register_access',
+ 'type' => MENU_LOCAL_TASK,
+ );
+
+ $items['user/password'] = array(
+ 'title' => 'Request new password',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_pass'),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'user.pages.inc',
+ );
+ $items['user/reset/%/%/%'] = array(
+ 'title' => 'Reset password',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_pass_reset', 2, 3, 4),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ 'file' => 'user.pages.inc',
+ );
+
+ $items['user/logout'] = array(
+ 'title' => 'Log out',
+ 'access callback' => 'user_is_logged_in',
+ 'page callback' => 'user_logout',
+ 'weight' => 10,
+ 'menu_name' => 'user-menu',
+ 'file' => 'user.pages.inc',
+ );
+
+ // User listing pages.
+ $items['admin/people'] = array(
+ 'title' => 'People',
+ 'description' => 'Manage user accounts, roles, and permissions.',
+ 'page callback' => 'user_admin',
+ 'page arguments' => array('list'),
+ 'access arguments' => array('administer users'),
+ 'position' => 'left',
+ 'weight' => -4,
+ 'file' => 'user.admin.inc',
+ );
+ $items['admin/people/people'] = array(
+ 'title' => 'List',
+ 'description' => 'Find and manage people interacting with your site.',
+ 'access arguments' => array('administer users'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ 'file' => 'user.admin.inc',
+ );
+
+ // Permissions and role forms.
+ $items['admin/people/permissions'] = array(
+ 'title' => 'Permissions',
+ 'description' => 'Determine access to features by selecting permissions for roles.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_admin_permissions'),
+ 'access arguments' => array('administer permissions'),
+ 'file' => 'user.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ );
+ $items['admin/people/permissions/list'] = array(
+ 'title' => 'Permissions',
+ 'description' => 'Determine access to features by selecting permissions for roles.',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -8,
+ );
+ $items['admin/people/permissions/roles'] = array(
+ 'title' => 'Roles',
+ 'description' => 'List, edit, or add user roles.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_admin_roles'),
+ 'access arguments' => array('administer permissions'),
+ 'file' => 'user.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -5,
+ );
+ $items['admin/people/permissions/roles/edit/%user_role'] = array(
+ 'title' => 'Edit role',
+ 'page arguments' => array('user_admin_role', 5),
+ 'access callback' => 'user_role_edit_access',
+ 'access arguments' => array(5),
+ );
+ $items['admin/people/permissions/roles/delete/%user_role'] = array(
+ 'title' => 'Delete role',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_admin_role_delete_confirm', 5),
+ 'access callback' => 'user_role_edit_access',
+ 'access arguments' => array(5),
+ 'file' => 'user.admin.inc',
+ );
+
+ $items['admin/people/create'] = array(
+ 'title' => 'Add user',
+ 'page callback' => 'user_admin',
+ 'page arguments' => array('create'),
+ 'access arguments' => array('administer users'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'file' => 'user.admin.inc',
+ );
+
+ // Administration pages.
+ $items['admin/config/people'] = array(
+ 'title' => 'People',
+ 'description' => 'Configure user accounts.',
+ 'position' => 'left',
+ 'weight' => -20,
+ 'page callback' => 'system_admin_menu_block_page',
+ 'access arguments' => array('access administration pages'),
+ 'file' => 'system.admin.inc',
+ 'file path' => drupal_get_path('module', 'system'),
+ );
+ $items['admin/config/people/accounts'] = array(
+ 'title' => 'Account settings',
+ 'description' => 'Configure default behavior of users, including registration requirements, e-mails, fields, and user pictures.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_admin_settings'),
+ 'access arguments' => array('administer users'),
+ 'file' => 'user.admin.inc',
+ 'weight' => -10,
+ );
+ $items['admin/config/people/accounts/settings'] = array(
+ 'title' => 'Settings',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ $items['user/%user'] = array(
+ 'title' => 'My account',
+ 'title callback' => 'user_page_title',
+ 'title arguments' => array(1),
+ 'page callback' => 'user_view_page',
+ 'page arguments' => array(1),
+ 'access callback' => 'user_view_access',
+ 'access arguments' => array(1),
+ // By assigning a different menu name, this item (and all registered child
+ // paths) are no longer considered as children of 'user'. When accessing the
+ // user account pages, the preferred menu link that is used to build the
+ // active trail (breadcrumb) will be found in this menu (unless there is
+ // more specific link), so the link to 'user' will not be in the breadcrumb.
+ 'menu_name' => 'navigation',
+ );
+
+ $items['user/%user/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ $items['user/%user/cancel'] = array(
+ 'title' => 'Cancel account',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_cancel_confirm_form', 1),
+ 'access callback' => 'user_cancel_access',
+ 'access arguments' => array(1),
+ 'file' => 'user.pages.inc',
+ );
+
+ $items['user/%user/cancel/confirm/%/%'] = array(
+ 'title' => 'Confirm account cancellation',
+ 'page callback' => 'user_cancel_confirm',
+ 'page arguments' => array(1, 4, 5),
+ 'access callback' => 'user_cancel_access',
+ 'access arguments' => array(1),
+ 'file' => 'user.pages.inc',
+ );
+
+ $items['user/%user/edit'] = array(
+ 'title' => 'Edit',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_profile_form', 1),
+ 'access callback' => 'user_edit_access',
+ 'access arguments' => array(1),
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'user.pages.inc',
+ );
+
+ $items['user/%user_category/edit/account'] = array(
+ 'title' => 'Account',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'load arguments' => array('%map', '%index'),
+ );
+
+ if (($categories = _user_categories()) && (count($categories) > 1)) {
+ foreach ($categories as $key => $category) {
+ // 'account' is already handled by the MENU_DEFAULT_LOCAL_TASK.
+ if ($category['name'] != 'account') {
+ $items['user/%user_category/edit/' . $category['name']] = array(
+ 'title callback' => 'check_plain',
+ 'title arguments' => array($category['title']),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('user_profile_form', 1, 3),
+ 'access callback' => isset($category['access callback']) ? $category['access callback'] : 'user_edit_access',
+ 'access arguments' => isset($category['access arguments']) ? $category['access arguments'] : array(1),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => $category['weight'],
+ 'load arguments' => array('%map', '%index'),
+ 'tab_parent' => 'user/%/edit',
+ 'file' => 'user.pages.inc',
+ );
+ }
+ }
+ }
+ return $items;
+}
+
+/**
+ * Implements hook_menu_site_status_alter().
+ */
+function user_menu_site_status_alter(&$menu_site_status, $path) {
+ if ($menu_site_status == MENU_SITE_OFFLINE) {
+ // If the site is offline, log out unprivileged users.
+ if (user_is_logged_in() && !user_access('access site in maintenance mode')) {
+ module_load_include('pages.inc', 'user', 'user');
+ user_logout();
+ }
+
+ if (user_is_anonymous()) {
+ switch ($path) {
+ case 'user':
+ // Forward anonymous user to login page.
+ drupal_goto('user/login');
+ case 'user/login':
+ case 'user/password':
+ // Disable offline mode.
+ $menu_site_status = MENU_SITE_ONLINE;
+ break;
+ default:
+ if (strpos($path, 'user/reset/') === 0) {
+ // Disable offline mode.
+ $menu_site_status = MENU_SITE_ONLINE;
+ }
+ break;
+ }
+ }
+ }
+ if (user_is_logged_in()) {
+ if ($path == 'user/login') {
+ // If user is logged in, redirect to 'user' instead of giving 403.
+ drupal_goto('user');
+ }
+ if ($path == 'user/register') {
+ // Authenticated user should be redirected to user edit page.
+ drupal_goto('user/' . $GLOBALS['user']->uid . '/edit');
+ }
+ }
+}
+
+/**
+ * Implements hook_menu_link_alter().
+ */
+function user_menu_link_alter(&$link) {
+ // The path 'user' must be accessible for anonymous users, but only visible
+ // for authenticated users. Authenticated users should see "My account", but
+ // anonymous users should not see it at all. Therefore, invoke
+ // user_translated_menu_link_alter() to conditionally hide the link.
+ if ($link['link_path'] == 'user' && isset($link['module']) && $link['module'] == 'system') {
+ $link['options']['alter'] = TRUE;
+ }
+
+ // Force the Logout link to appear on the top-level of 'user-menu' menu by
+ // default (i.e., unless it has been customized).
+ if ($link['link_path'] == 'user/logout' && isset($link['module']) && $link['module'] == 'system' && empty($link['customized'])) {
+ $link['plid'] = 0;
+ }
+}
+
+/**
+ * Implements hook_translated_menu_link_alter().
+ */
+function user_translated_menu_link_alter(&$link) {
+ // Hide the "User account" link for anonymous users.
+ if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) {
+ $link['hidden'] = 1;
+ }
+}
+
+/**
+ * Implements hook_admin_paths().
+ */
+function user_admin_paths() {
+ $paths = array(
+ 'user/*/cancel' => TRUE,
+ 'user/*/edit' => TRUE,
+ 'user/*/edit/*' => TRUE,
+ );
+ return $paths;
+}
+
+/**
+ * Returns $arg or the user ID of the current user if $arg is '%' or empty.
+ *
+ * Deprecated. Use %user_uid_optional instead.
+ *
+ * @todo D8: Remove.
+ */
+function user_uid_only_optional_to_arg($arg) {
+ return user_uid_optional_to_arg($arg);
+}
+
+/**
+ * Load either a specified or the current user account.
+ *
+ * @param $uid
+ * An optional user ID of the user to load. If not provided, the current
+ * user's ID will be used.
+ * @return
+ * A fully-loaded $user object upon successful user load, FALSE if user
+ * cannot be loaded.
+ *
+ * @see user_load()
+ * @todo rethink the naming of this in Drupal 8.
+ */
+function user_uid_optional_load($uid = NULL) {
+ if (!isset($uid)) {
+ $uid = $GLOBALS['user']->uid;
+ }
+ return user_load($uid);
+}
+
+/**
+ * Return a user object after checking if any profile category in the path exists.
+ */
+function user_category_load($uid, &$map, $index) {
+ static $user_categories, $accounts;
+
+ // Cache $account - this load function will get called for each profile tab.
+ if (!isset($accounts[$uid])) {
+ $accounts[$uid] = user_load($uid);
+ }
+ $valid = TRUE;
+ if ($account = $accounts[$uid]) {
+ // Since the path is like user/%/edit/category_name, the category name will
+ // be at a position 2 beyond the index corresponding to the % wildcard.
+ $category_index = $index + 2;
+ // Valid categories may contain slashes, and hence need to be imploded.
+ $category_path = implode('/', array_slice($map, $category_index));
+ if ($category_path) {
+ // Check that the requested category exists.
+ $valid = FALSE;
+ if (!isset($user_categories)) {
+ $user_categories = _user_categories();
+ }
+ foreach ($user_categories as $category) {
+ if ($category['name'] == $category_path) {
+ $valid = TRUE;
+ // Truncate the map array in case the category name had slashes.
+ $map = array_slice($map, 0, $category_index);
+ // Assign the imploded category name to the last map element.
+ $map[$category_index] = $category_path;
+ break;
+ }
+ }
+ }
+ }
+ return $valid ? $account : FALSE;
+}
+
+/**
+ * Returns $arg or the user ID of the current user if $arg is '%' or empty.
+ *
+ * @todo rethink the naming of this in Drupal 8.
+ */
+function user_uid_optional_to_arg($arg) {
+ // Give back the current user uid when called from eg. tracker, aka.
+ // with an empty arg. Also use the current user uid when called from
+ // the menu with a % for the current account link.
+ return empty($arg) || $arg == '%' ? $GLOBALS['user']->uid : $arg;
+}
+
+/**
+ * Menu item title callback for the 'user' path.
+ *
+ * Anonymous users should see "User account", but authenticated users are
+ * expected to see "My account".
+ */
+function user_menu_title() {
+ return user_is_logged_in() ? t('My account') : t('User account');
+}
+
+/**
+ * Menu item title callback - use the user name.
+ */
+function user_page_title($account) {
+ return is_object($account) ? format_username($account) : '';
+}
+
+/**
+ * Discover which external authentication module(s) authenticated a username.
+ *
+ * @param $authname
+ * A username used by an external authentication module.
+ * @return
+ * An associative array with module as key and username as value.
+ */
+function user_get_authmaps($authname = NULL) {
+ $authmaps = db_query("SELECT module, authname FROM {authmap} WHERE authname = :authname", array(':authname' => $authname))->fetchAllKeyed();
+ return count($authmaps) ? $authmaps : 0;
+}
+
+/**
+ * Save mappings of which external authentication module(s) authenticated
+ * a user. Maps external usernames to user ids in the users table.
+ *
+ * @param $account
+ * A user object.
+ * @param $authmaps
+ * An associative array with a compound key and the username as the value.
+ * The key is made up of 'authname_' plus the name of the external authentication
+ * module.
+ * @see user_external_login_register()
+ */
+function user_set_authmaps($account, $authmaps) {
+ foreach ($authmaps as $key => $value) {
+ $module = explode('_', $key, 2);
+ if ($value) {
+ db_merge('authmap')
+ ->key(array(
+ 'uid' => $account->uid,
+ 'module' => $module[1],
+ ))
+ ->fields(array('authname' => $value))
+ ->execute();
+ }
+ else {
+ db_delete('authmap')
+ ->condition('uid', $account->uid)
+ ->condition('module', $module[1])
+ ->execute();
+ }
+ }
+}
+
+/**
+ * Form builder; the main user login form.
+ *
+ * @ingroup forms
+ */
+function user_login($form, &$form_state) {
+ global $user;
+
+ // If we are already logged on, go to the user page instead.
+ if ($user->uid) {
+ drupal_goto('user/' . $user->uid);
+ }
+
+ // Display login form:
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#size' => 60,
+ '#maxlength' => USERNAME_MAX_LENGTH,
+ '#required' => TRUE,
+ );
+
+ $form['name']['#description'] = t('Enter your @s username.', array('@s' => variable_get('site_name', 'Drupal')));
+ $form['pass'] = array('#type' => 'password',
+ '#title' => t('Password'),
+ '#description' => t('Enter the password that accompanies your username.'),
+ '#required' => TRUE,
+ );
+ $form['#validate'] = user_login_default_validators();
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
+
+ return $form;
+}
+
+/**
+ * Set up a series for validators which check for blocked users,
+ * then authenticate against local database, then return an error if
+ * authentication fails. Distributed authentication modules are welcome
+ * to use hook_form_alter() to change this series in order to
+ * authenticate against their user database instead of the local users
+ * table. If a distributed authentication module is successful, it
+ * should set $form_state['uid'] to a user ID.
+ *
+ * We use three validators instead of one since external authentication
+ * modules usually only need to alter the second validator.
+ *
+ * @see user_login_name_validate()
+ * @see user_login_authenticate_validate()
+ * @see user_login_final_validate()
+ * @return array
+ * A simple list of validate functions.
+ */
+function user_login_default_validators() {
+ return array('user_login_name_validate', 'user_login_authenticate_validate', 'user_login_final_validate');
+}
+
+/**
+ * A FAPI validate handler. Sets an error if supplied username has been blocked.
+ */
+function user_login_name_validate($form, &$form_state) {
+ if (!empty($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) {
+ // Blocked in user administration.
+ form_set_error('name', t('The username %name has not been activated or is blocked.', array('%name' => $form_state['values']['name'])));
+ }
+}
+
+/**
+ * A validate handler on the login form. Check supplied username/password
+ * against local users table. If successful, $form_state['uid']
+ * is set to the matching user ID.
+ */
+function user_login_authenticate_validate($form, &$form_state) {
+ $password = trim($form_state['values']['pass']);
+ if (!empty($form_state['values']['name']) && strlen(trim($password)) > 0) {
+ // Do not allow any login from the current user's IP if the limit has been
+ // reached. Default is 50 failed attempts allowed in one hour. This is
+ // independent of the per-user limit to catch attempts from one IP to log
+ // in to many different user accounts. We have a reasonably high limit
+ // since there may be only one apparent IP for all users at an institution.
+ if (!flood_is_allowed('failed_login_attempt_ip', variable_get('user_failed_login_ip_limit', 50), variable_get('user_failed_login_ip_window', 3600))) {
+ $form_state['flood_control_triggered'] = 'ip';
+ return;
+ }
+ $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject();
+ if ($account) {
+ if (variable_get('user_failed_login_identifier_uid_only', FALSE)) {
+ // Register flood events based on the uid only, so they apply for any
+ // IP address. This is the most secure option.
+ $identifier = $account->uid;
+ }
+ else {
+ // The default identifier is a combination of uid and IP address. This
+ // is less secure but more resistant to denial-of-service attacks that
+ // could lock out all users with public user names.
+ $identifier = $account->uid . '-' . ip_address();
+ }
+ $form_state['flood_control_user_identifier'] = $identifier;
+
+ // Don't allow login if the limit for this user has been reached.
+ // Default is to allow 5 failed attempts every 6 hours.
+ if (!flood_is_allowed('failed_login_attempt_user', variable_get('user_failed_login_user_limit', 5), variable_get('user_failed_login_user_window', 21600), $identifier)) {
+ $form_state['flood_control_triggered'] = 'user';
+ return;
+ }
+ }
+ // We are not limited by flood control, so try to authenticate.
+ // Set $form_state['uid'] as a flag for user_login_final_validate().
+ $form_state['uid'] = user_authenticate($form_state['values']['name'], $password);
+ }
+}
+
+/**
+ * The final validation handler on the login form.
+ *
+ * Sets a form error if user has not been authenticated, or if too many
+ * logins have been attempted. This validation function should always
+ * be the last one.
+ */
+function user_login_final_validate($form, &$form_state) {
+ if (empty($form_state['uid'])) {
+ // Always register an IP-based failed login event.
+ flood_register_event('failed_login_attempt_ip', variable_get('user_failed_login_ip_window', 3600));
+ // Register a per-user failed login event.
+ if (isset($form_state['flood_control_user_identifier'])) {
+ flood_register_event('failed_login_attempt_user', variable_get('user_failed_login_user_window', 21600), $form_state['flood_control_user_identifier']);
+ }
+
+ if (isset($form_state['flood_control_triggered'])) {
+ if ($form_state['flood_control_triggered'] == 'user') {
+ form_set_error('name', format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or
request a new password .', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or
request a new password .', array('@url' => url('user/password'))));
+ }
+ else {
+ // We did not find a uid, so the limit is IP-based.
+ form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or
request a new password .', array('@url' => url('user/password'))));
+ }
+ }
+ else {
+ // Use $form_state['input']['name'] here to guarantee that we send
+ // exactly what the user typed in. $form_state['values']['name'] may have
+ // been modified by validation handlers that ran earlier than this one.
+ $query = isset($form_state['input']['name']) ? array('name' => $form_state['input']['name']) : array();
+ form_set_error('name', t('Sorry, unrecognized username or password.
Have you forgotten your password? ', array('@password' => url('user/password', array('query' => $query)))));
+ watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
+ }
+ }
+ elseif (isset($form_state['flood_control_user_identifier'])) {
+ // Clear past failures for this user so as not to block a user who might
+ // log in and out more than once in an hour.
+ flood_clear_event('failed_login_attempt_user', $form_state['flood_control_user_identifier']);
+ }
+}
+
+/**
+ * Try to validate the user's login credentials locally.
+ *
+ * @param $name
+ * User name to authenticate.
+ * @param $password
+ * A plain-text password, such as trimmed text from form values.
+ * @return
+ * The user's uid on success, or FALSE on failure to authenticate.
+ */
+function user_authenticate($name, $password) {
+ $uid = FALSE;
+ if (!empty($name) && strlen(trim($password)) > 0) {
+ $account = user_load_by_name($name);
+ if ($account) {
+ // Allow alternate password hashing schemes.
+ require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
+ if (user_check_password($password, $account)) {
+ // Successful authentication.
+ $uid = $account->uid;
+
+ // Update user to new password scheme if needed.
+ if (user_needs_new_hash($account)) {
+ user_save($account, array('pass' => $password));
+ }
+ }
+ }
+ }
+ return $uid;
+}
+
+/**
+ * Finalize the login process. Must be called when logging in a user.
+ *
+ * The function records a watchdog message about the new session, saves the
+ * login timestamp, calls hook_user_login(), and generates a new session.
+ *
+ * @param array $edit
+ * The array of form values submitted by the user.
+ *
+ * @see hook_user_login()
+ */
+function user_login_finalize(&$edit = array()) {
+ global $user;
+ watchdog('user', 'Session opened for %name.', array('%name' => $user->name));
+ // Update the user table timestamp noting user has logged in.
+ // This is also used to invalidate one-time login links.
+ $user->login = REQUEST_TIME;
+ db_update('users')
+ ->fields(array('login' => $user->login))
+ ->condition('uid', $user->uid)
+ ->execute();
+
+ // Regenerate the session ID to prevent against session fixation attacks.
+ // This is called before hook_user in case one of those functions fails
+ // or incorrectly does a redirect which would leave the old session in place.
+ drupal_session_regenerate();
+
+ user_module_invoke('login', $edit, $user);
+}
+
+/**
+ * Submit handler for the login form. Load $user object and perform standard login
+ * tasks. The user is then redirected to the My Account page. Setting the
+ * destination in the query string overrides the redirect.
+ */
+function user_login_submit($form, &$form_state) {
+ global $user;
+ $user = user_load($form_state['uid']);
+ $form_state['redirect'] = 'user/' . $user->uid;
+
+ user_login_finalize($form_state);
+}
+
+/**
+ * Helper function for authentication modules. Either logs in or registers
+ * the current user, based on username. Either way, the global $user object is
+ * populated and login tasks are performed.
+ */
+function user_external_login_register($name, $module) {
+ $account = user_external_load($name);
+ if (!$account) {
+ // Register this new user.
+ $userinfo = array(
+ 'name' => $name,
+ 'pass' => user_password(),
+ 'init' => $name,
+ 'status' => 1,
+ 'access' => REQUEST_TIME
+ );
+ $account = user_save(drupal_anonymous_user(), $userinfo);
+ // Terminate if an error occurred during user_save().
+ if (!$account) {
+ drupal_set_message(t("Error saving user account."), 'error');
+ return;
+ }
+ user_set_authmaps($account, array("authname_$module" => $name));
+ }
+
+ // Log user in.
+ $form_state['uid'] = $account->uid;
+ user_login_submit(array(), $form_state);
+}
+
+/**
+ * Generates a unique URL for a user to login and reset their password.
+ *
+ * @param object $account
+ * An object containing the user account, which must contain at least the
+ * following properties:
+ * - uid: The user ID number.
+ * - login: The UNIX timestamp of the user's last login.
+ *
+ * @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) {
+ $timestamp = REQUEST_TIME;
+ return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE));
+}
+
+/**
+ * Generates a URL to confirm an account cancellation request.
+ *
+ * @param object $account
+ * The user account object, which must contain at least the following
+ * properties:
+ * - uid: The user ID number.
+ * - pass: The hashed user password string.
+ * - login: The UNIX timestamp of the user's last login.
+ *
+ * @return
+ * A unique URL that may be used to confirm the cancellation of the user
+ * account.
+ *
+ * @see user_mail_tokens()
+ * @see user_cancel_confirm()
+ */
+function user_cancel_url($account) {
+ $timestamp = REQUEST_TIME;
+ return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE));
+}
+
+/**
+ * Creates a unique hash value for use in time-dependent per-user URLs.
+ *
+ * This hash is normally used to build a unique and secure URL that is sent to
+ * the user by email for purposes such as resetting the user's password. In
+ * order to validate the URL, the same hash can be generated again, from the
+ * same information, and compared to the hash value from the URL. The URL
+ * normally contains both the time stamp and the numeric user ID. The login
+ * timestamp and hashed password are retrieved from the database as necessary.
+ * For a usage example, see user_cancel_url() and user_cancel_confirm().
+ *
+ * @param string $password
+ * The hashed user account password value.
+ * @param int $timestamp
+ * A UNIX timestamp, typically REQUEST_TIME.
+ * @param int $login
+ * The UNIX timestamp of the user's last login.
+ * @param int $uid
+ * The user ID of the user account.
+ *
+ * @return
+ * A string that is safe for use in URLs and SQL statements.
+ */
+function user_pass_rehash($password, $timestamp, $login, $uid) {
+ // Backwards compatibility: Try to determine a $uid if one was not passed.
+ // (Since $uid is a required parameter to this function, a PHP warning will
+ // be generated if it's not provided, which is an indication that the calling
+ // code should be updated. But the code below will try to generate a correct
+ // hash in the meantime.)
+ if (!isset($uid)) {
+ $uids = db_query_range('SELECT uid FROM {users} WHERE pass = :password AND login = :login AND uid > 0', 0, 2, array(':password' => $password, ':login' => $login))->fetchCol();
+ // If exactly one user account matches the provided password and login
+ // timestamp, proceed with that $uid.
+ if (count($uids) == 1) {
+ $uid = reset($uids);
+ }
+ // Otherwise there is no safe hash to return, so return a random string
+ // that will never be treated as a valid token.
+ else {
+ return drupal_random_key();
+ }
+ }
+
+ return drupal_hmac_base64($timestamp . $login . $uid, drupal_get_hash_salt() . $password);
+}
+
+/**
+ * Cancel a user account.
+ *
+ * Since the user cancellation process needs to be run in a batch, either
+ * Form API will invoke it, or batch_process() needs to be invoked after calling
+ * this function and should define the path to redirect to.
+ *
+ * @param $edit
+ * An array of submitted form values.
+ * @param $uid
+ * The user ID of the user account to cancel.
+ * @param $method
+ * The account cancellation method to use.
+ *
+ * @see _user_cancel()
+ */
+function user_cancel($edit, $uid, $method) {
+ global $user;
+
+ $account = user_load($uid);
+
+ if (!$account) {
+ drupal_set_message(t('The user account %id does not exist.', array('%id' => $uid)), 'error');
+ watchdog('user', 'Attempted to cancel non-existing user account: %id.', array('%id' => $uid), WATCHDOG_ERROR);
+ return;
+ }
+
+ // Initialize batch (to set title).
+ $batch = array(
+ 'title' => t('Cancelling account'),
+ 'operations' => array(),
+ );
+ batch_set($batch);
+
+ // Modules use hook_user_delete() to respond to deletion.
+ if ($method != 'user_cancel_delete') {
+ // Allow modules to add further sets to this batch.
+ module_invoke_all('user_cancel', $edit, $account, $method);
+ }
+
+ // Finish the batch and actually cancel the account.
+ $batch = array(
+ 'title' => t('Cancelling user account'),
+ 'operations' => array(
+ array('_user_cancel', array($edit, $account, $method)),
+ ),
+ );
+
+ // After cancelling account, ensure that user is logged out.
+ if ($account->uid == $user->uid) {
+ // Batch API stores data in the session, so use the finished operation to
+ // manipulate the current user's session id.
+ $batch['finished'] = '_user_cancel_session_regenerate';
+ }
+
+ batch_set($batch);
+
+ // Batch processing is either handled via Form API or has to be invoked
+ // manually.
+}
+
+/**
+ * Implements callback_batch_operation().
+ *
+ * Last step for cancelling a user account.
+ *
+ * Since batch and session API require a valid user account, the actual
+ * cancellation of a user account needs to happen last.
+ *
+ * @see user_cancel()
+ */
+function _user_cancel($edit, $account, $method) {
+ global $user;
+
+ switch ($method) {
+ case 'user_cancel_block':
+ case 'user_cancel_block_unpublish':
+ default:
+ // Send account blocked notification if option was checked.
+ if (!empty($edit['user_cancel_notify'])) {
+ _user_mail_notify('status_blocked', $account);
+ }
+ user_save($account, array('status' => 0));
+ drupal_set_message(t('%name has been disabled.', array('%name' => $account->name)));
+ watchdog('user', 'Blocked user: %name %email.', array('%name' => $account->name, '%email' => '<' . $account->mail . '>'), WATCHDOG_NOTICE);
+ break;
+
+ case 'user_cancel_reassign':
+ case 'user_cancel_delete':
+ // Send account canceled notification if option was checked.
+ if (!empty($edit['user_cancel_notify'])) {
+ _user_mail_notify('status_canceled', $account);
+ }
+ user_delete($account->uid);
+ drupal_set_message(t('%name has been deleted.', array('%name' => $account->name)));
+ watchdog('user', 'Deleted user: %name %email.', array('%name' => $account->name, '%email' => '<' . $account->mail . '>'), WATCHDOG_NOTICE);
+ break;
+ }
+
+ // After cancelling account, ensure that user is logged out. We can't destroy
+ // their session though, as we might have information in it, and we can't
+ // regenerate it because batch API uses the session ID, we will regenerate it
+ // in _user_cancel_session_regenerate().
+ if ($account->uid == $user->uid) {
+ $user = drupal_anonymous_user();
+ }
+
+ // Clear the cache for anonymous users.
+ cache_clear_all();
+}
+
+/**
+ * Implements callback_batch_finished().
+ *
+ * Finished batch processing callback for cancelling a user account.
+ *
+ * @see user_cancel()
+ */
+function _user_cancel_session_regenerate() {
+ // Regenerate the users session instead of calling session_destroy() as we
+ // want to preserve any messages that might have been set.
+ drupal_session_regenerate();
+}
+
+/**
+ * Delete a user.
+ *
+ * @param $uid
+ * A user ID.
+ */
+function user_delete($uid) {
+ user_delete_multiple(array($uid));
+}
+
+/**
+ * Delete multiple user accounts.
+ *
+ * @param $uids
+ * An array of user IDs.
+ */
+function user_delete_multiple(array $uids) {
+ if (!empty($uids)) {
+ $accounts = user_load_multiple($uids, array());
+
+ $transaction = db_transaction();
+ try {
+ foreach ($accounts as $uid => $account) {
+ module_invoke_all('user_delete', $account);
+ module_invoke_all('entity_delete', $account, 'user');
+ field_attach_delete('user', $account);
+ drupal_session_destroy_uid($account->uid);
+ }
+
+ db_delete('users')
+ ->condition('uid', $uids, 'IN')
+ ->execute();
+ db_delete('users_roles')
+ ->condition('uid', $uids, 'IN')
+ ->execute();
+ db_delete('authmap')
+ ->condition('uid', $uids, 'IN')
+ ->execute();
+ }
+ catch (Exception $e) {
+ $transaction->rollback();
+ watchdog_exception('user', $e);
+ throw $e;
+ }
+ entity_get_controller('user')->resetCache();
+ }
+}
+
+/**
+ * Page callback wrapper for user_view().
+ */
+function user_view_page($account) {
+ // An administrator may try to view a non-existent account,
+ // so we give them a 404 (versus a 403 for non-admins).
+ return is_object($account) ? user_view($account) : MENU_NOT_FOUND;
+}
+
+/**
+ * Generate an array for rendering the given user.
+ *
+ * When viewing a user profile, the $page array contains:
+ *
+ * - $page['content']['Profile Category']:
+ * Profile categories keyed by their human-readable names.
+ * - $page['content']['Profile Category']['profile_machine_name']:
+ * Profile fields keyed by their machine-readable names.
+ * - $page['content']['user_picture']:
+ * User's rendered picture.
+ * - $page['content']['summary']:
+ * Contains the default "History" profile data for a user.
+ * - $page['content']['#account']:
+ * The user account of the profile being viewed.
+ *
+ * To theme user profiles, copy modules/user/user-profile.tpl.php
+ * to your theme directory, and edit it as instructed in that file's comments.
+ *
+ * @param $account
+ * A user object.
+ * @param $view_mode
+ * View mode, e.g. 'full'.
+ * @param $langcode
+ * (optional) A language code to use for rendering. Defaults to the global
+ * content language of the current request.
+ *
+ * @return
+ * An array as expected by drupal_render().
+ */
+function user_view($account, $view_mode = 'full', $langcode = NULL) {
+ if (!isset($langcode)) {
+ $langcode = $GLOBALS['language_content']->language;
+ }
+
+ // Retrieve all profile fields and attach to $account->content.
+ user_build_content($account, $view_mode, $langcode);
+
+ $build = $account->content;
+ // We don't need duplicate rendering info in account->content.
+ unset($account->content);
+
+ $build += array(
+ '#theme' => 'user_profile',
+ '#account' => $account,
+ '#view_mode' => $view_mode,
+ '#language' => $langcode,
+ );
+
+ // Allow modules to modify the structured user.
+ $type = 'user';
+ drupal_alter(array('user_view', 'entity_view'), $build, $type);
+
+ return $build;
+}
+
+/**
+ * Builds a structured array representing the profile content.
+ *
+ * @param $account
+ * A user object.
+ * @param $view_mode
+ * View mode, e.g. 'full'.
+ * @param $langcode
+ * (optional) A language code to use for rendering. Defaults to the global
+ * content language of the current request.
+ */
+function user_build_content($account, $view_mode = 'full', $langcode = NULL) {
+ if (!isset($langcode)) {
+ $langcode = $GLOBALS['language_content']->language;
+ }
+
+ // Remove previously built content, if exists.
+ $account->content = array();
+
+ // Allow modules to change the view mode.
+ $view_mode = key(entity_view_mode_prepare('user', array($account->uid => $account), $view_mode, $langcode));
+
+ // Build fields content.
+ field_attach_prepare_view('user', array($account->uid => $account), $view_mode, $langcode);
+ entity_prepare_view('user', array($account->uid => $account), $langcode);
+ $account->content += field_attach_view('user', $account, $view_mode, $langcode);
+
+ // Populate $account->content with a render() array.
+ module_invoke_all('user_view', $account, $view_mode, $langcode);
+ module_invoke_all('entity_view', $account, 'user', $view_mode, $langcode);
+
+ // Make sure the current view mode is stored if no module has already
+ // populated the related key.
+ $account->content += array('#view_mode' => $view_mode);
+}
+
+/**
+ * Implements hook_mail().
+ */
+function user_mail($key, &$message, $params) {
+ $language = $message['language'];
+ $variables = array('user' => $params['account']);
+ $message['subject'] .= _user_mail_text($key . '_subject', $language, $variables);
+ $message['body'][] = _user_mail_text($key . '_body', $language, $variables);
+}
+
+/**
+ * Returns a mail string for a variable name.
+ *
+ * Used by user_mail() and the settings forms to retrieve strings.
+ */
+function _user_mail_text($key, $language = NULL, $variables = array(), $replace = TRUE) {
+ $langcode = isset($language) ? $language->language : NULL;
+
+ if ($admin_setting = variable_get('user_mail_' . $key, FALSE)) {
+ // An admin setting overrides the default string.
+ $text = $admin_setting;
+ }
+ else {
+ // No override, return default string.
+ switch ($key) {
+ case 'register_no_approval_required_subject':
+ $text = t('Account details for [user:name] at [site:name]', array(), array('langcode' => $langcode));
+ break;
+ case 'register_no_approval_required_body':
+ $text = t("[user:name],
+
+Thank you for registering at [site:name]. You may now log in by clicking this link or copying and pasting it to your browser:
+
+[user:one-time-login-url]
+
+This link can only be used once to log in and will lead you to a page where you can set your password.
+
+After setting your password, you will be able to log in at [site:login-url] in the future using:
+
+username: [user:name]
+password: Your password
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+
+ case 'register_admin_created_subject':
+ $text = t('An administrator created an account for you at [site:name]', array(), array('langcode' => $langcode));
+ break;
+ case 'register_admin_created_body':
+ $text = t("[user:name],
+
+A site administrator at [site:name] has created an account for you. You may now log in by clicking this link or copying and pasting it to your browser:
+
+[user:one-time-login-url]
+
+This link can only be used once to log in and will lead you to a page where you can set your password.
+
+After setting your password, you will be able to log in at [site:login-url] in the future using:
+
+username: [user:name]
+password: Your password
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+
+ case 'register_pending_approval_subject':
+ case 'register_pending_approval_admin_subject':
+ $text = t('Account details for [user:name] at [site:name] (pending admin approval)', array(), array('langcode' => $langcode));
+ break;
+ case 'register_pending_approval_body':
+ $text = t("[user:name],
+
+Thank you for registering at [site:name]. Your application for an account is currently pending approval. Once it has been approved, you will receive another e-mail containing information about how to log in, set your password, and other details.
+
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+ case 'register_pending_approval_admin_body':
+ $text = t("[user:name] has applied for an account.
+
+[user:edit-url]", array(), array('langcode' => $langcode));
+ break;
+
+ case 'password_reset_subject':
+ $text = t('Replacement login information for [user:name] at [site:name]', array(), array('langcode' => $langcode));
+ break;
+ case 'password_reset_body':
+ $text = t("[user:name],
+
+A request to reset the password for your account has been made at [site:name].
+
+You may now log in by clicking this link or copying and pasting it to your browser:
+
+[user:one-time-login-url]
+
+This link can only be used once to log in and will lead you to a page where you can set your password. It expires after one day and nothing will happen if it's not used.
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+
+ case 'status_activated_subject':
+ $text = t('Account details for [user:name] at [site:name] (approved)', array(), array('langcode' => $langcode));
+ break;
+ case 'status_activated_body':
+ $text = t("[user:name],
+
+Your account at [site:name] has been activated.
+
+You may now log in by clicking this link or copying and pasting it into your browser:
+
+[user:one-time-login-url]
+
+This link can only be used once to log in and will lead you to a page where you can set your password.
+
+After setting your password, you will be able to log in at [site:login-url] in the future using:
+
+username: [user:name]
+password: Your password
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+
+ case 'status_blocked_subject':
+ $text = t('Account details for [user:name] at [site:name] (blocked)', array(), array('langcode' => $langcode));
+ break;
+ case 'status_blocked_body':
+ $text = t("[user:name],
+
+Your account on [site:name] has been blocked.
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+
+ case 'cancel_confirm_subject':
+ $text = t('Account cancellation request for [user:name] at [site:name]', array(), array('langcode' => $langcode));
+ break;
+ case 'cancel_confirm_body':
+ $text = t("[user:name],
+
+A request to cancel your account has been made at [site:name].
+
+You may now cancel your account on [site:url-brief] by clicking this link or copying and pasting it into your browser:
+
+[user:cancel-url]
+
+NOTE: The cancellation of your account is not reversible.
+
+This link expires in one day and nothing will happen if it is not used.
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+
+ case 'status_canceled_subject':
+ $text = t('Account details for [user:name] at [site:name] (canceled)', array(), array('langcode' => $langcode));
+ break;
+ case 'status_canceled_body':
+ $text = t("[user:name],
+
+Your account on [site:name] has been canceled.
+
+-- [site:name] team", array(), array('langcode' => $langcode));
+ break;
+ }
+ }
+
+ 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, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
+ }
+
+ return $text;
+}
+
+/**
+ * Token callback to add unsafe tokens for user mails.
+ *
+ * This function is used by the token_replace() call at the end of
+ * _user_mail_text() to set up some additional tokens that can be
+ * used in email messages generated by user_mail().
+ *
+ * @param $replacements
+ * An associative array variable containing mappings from token names to
+ * values (for use with strtr()).
+ * @param $data
+ * An associative array of token replacement values. If the 'user' element
+ * exists, it must contain a user account object with the following
+ * properties:
+ * - login: The UNIX timestamp of the user's last login.
+ * - pass: The hashed account login password.
+ * @param $options
+ * Unused parameter required by the token_replace() function.
+ */
+function user_mail_tokens(&$replacements, $data, $options) {
+ if (isset($data['user'])) {
+ $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user']);
+ $replacements['[user:cancel-url]'] = user_cancel_url($data['user']);
+ }
+}
+
+/*** Administrative features ***********************************************/
+
+/**
+ * Retrieve an array of roles matching specified conditions.
+ *
+ * @param $membersonly
+ * Set this to TRUE to exclude the 'anonymous' role.
+ * @param $permission
+ * A string containing a permission. If set, only roles containing that
+ * permission are returned.
+ *
+ * @return
+ * An associative array with the role id as the key and the role name as
+ * value.
+ */
+function user_roles($membersonly = FALSE, $permission = NULL) {
+ $query = db_select('role', 'r');
+ $query->addTag('translatable');
+ $query->fields('r', array('rid', 'name'));
+ $query->orderBy('weight');
+ $query->orderBy('name');
+ if (!empty($permission)) {
+ $query->innerJoin('role_permission', 'p', 'r.rid = p.rid');
+ $query->condition('p.permission', $permission);
+ }
+ $result = $query->execute();
+
+ $roles = array();
+ foreach ($result as $role) {
+ switch ($role->rid) {
+ // We only translate the built in role names
+ case DRUPAL_ANONYMOUS_RID:
+ if (!$membersonly) {
+ $roles[$role->rid] = t($role->name);
+ }
+ break;
+ case DRUPAL_AUTHENTICATED_RID:
+ $roles[$role->rid] = t($role->name);
+ break;
+ default:
+ $roles[$role->rid] = $role->name;
+ }
+ }
+
+ return $roles;
+}
+
+/**
+ * Fetches a user role by role ID.
+ *
+ * @param $rid
+ * An integer representing the role ID.
+ *
+ * @return
+ * A fully-loaded role object if a role with the given ID exists, or FALSE
+ * otherwise.
+ *
+ * @see user_role_load_by_name()
+ */
+function user_role_load($rid) {
+ return db_select('role', 'r')
+ ->fields('r')
+ ->condition('rid', $rid)
+ ->execute()
+ ->fetchObject();
+}
+
+/**
+ * Fetches a user role by role name.
+ *
+ * @param $role_name
+ * A string representing the role name.
+ *
+ * @return
+ * A fully-loaded role object if a role with the given name exists, or FALSE
+ * otherwise.
+ *
+ * @see user_role_load()
+ */
+function user_role_load_by_name($role_name) {
+ return db_select('role', 'r')
+ ->fields('r')
+ ->condition('name', $role_name)
+ ->execute()
+ ->fetchObject();
+}
+
+/**
+ * Save a user role to the database.
+ *
+ * @param $role
+ * A role object to modify or add. If $role->rid is not specified, a new
+ * role will be created.
+ * @return
+ * Status constant indicating if role was created or updated.
+ * Failure to write the user role record will return FALSE. Otherwise.
+ * SAVED_NEW or SAVED_UPDATED is returned depending on the operation
+ * performed.
+ */
+function user_role_save($role) {
+ if ($role->name) {
+ // Prevent leading and trailing spaces in role names.
+ $role->name = trim($role->name);
+ }
+ if (!isset($role->weight)) {
+ // Set a role weight to make this new role last.
+ $query = db_select('role');
+ $query->addExpression('MAX(weight)');
+ $role->weight = $query->execute()->fetchField() + 1;
+ }
+
+ // Let modules modify the user role before it is saved to the database.
+ module_invoke_all('user_role_presave', $role);
+
+ if (!empty($role->rid) && $role->name) {
+ $status = drupal_write_record('role', $role, 'rid');
+ module_invoke_all('user_role_update', $role);
+ }
+ else {
+ $status = drupal_write_record('role', $role);
+ module_invoke_all('user_role_insert', $role);
+ }
+
+ // Clear the user access cache.
+ drupal_static_reset('user_access');
+ drupal_static_reset('user_role_permissions');
+
+ return $status;
+}
+
+/**
+ * Delete a user role from database.
+ *
+ * @param $role
+ * A string with the role name, or an integer with the role ID.
+ */
+function user_role_delete($role) {
+ if (is_int($role)) {
+ $role = user_role_load($role);
+ }
+ else {
+ $role = user_role_load_by_name($role);
+ }
+
+ // If this is the administrator role, delete the user_admin_role variable.
+ if ($role->rid == variable_get('user_admin_role')) {
+ variable_del('user_admin_role');
+ }
+
+ db_delete('role')
+ ->condition('rid', $role->rid)
+ ->execute();
+ db_delete('role_permission')
+ ->condition('rid', $role->rid)
+ ->execute();
+ // Update the users who have this role set:
+ db_delete('users_roles')
+ ->condition('rid', $role->rid)
+ ->execute();
+
+ module_invoke_all('user_role_delete', $role);
+
+ // Clear the user access cache.
+ drupal_static_reset('user_access');
+ drupal_static_reset('user_role_permissions');
+}
+
+/**
+ * Menu access callback for user role editing.
+ */
+function user_role_edit_access($role) {
+ // Prevent the system-defined roles from being altered or removed.
+ if ($role->rid == DRUPAL_ANONYMOUS_RID || $role->rid == DRUPAL_AUTHENTICATED_RID) {
+ return FALSE;
+ }
+
+ return user_access('administer permissions');
+}
+
+/**
+ * Determine the modules that permissions belong to.
+ *
+ * @return
+ * An associative array in the format $permission => $module.
+ */
+function user_permission_get_modules() {
+ $permissions = array();
+ foreach (module_implements('permission') as $module) {
+ $perms = module_invoke($module, 'permission');
+ foreach ($perms as $key => $value) {
+ $permissions[$key] = $module;
+ }
+ }
+ return $permissions;
+}
+
+/**
+ * Change permissions for a user role.
+ *
+ * This function may be used to grant and revoke multiple permissions at once.
+ * For example, when a form exposes checkboxes to configure permissions for a
+ * role, the form submit handler may directly pass the submitted values for the
+ * checkboxes form element to this function.
+ *
+ * @param $rid
+ * The ID of a user role to alter.
+ * @param $permissions
+ * An associative array, where the key holds the permission name and the value
+ * determines whether to grant or revoke that permission. Any value that
+ * evaluates to TRUE will cause the permission to be granted. Any value that
+ * evaluates to FALSE will cause the permission to be revoked.
+ * @code
+ * array(
+ * 'administer nodes' => 0, // Revoke 'administer nodes'
+ * 'administer blocks' => FALSE, // Revoke 'administer blocks'
+ * 'access user profiles' => 1, // Grant 'access user profiles'
+ * 'access content' => TRUE, // Grant 'access content'
+ * 'access comments' => 'access comments', // Grant 'access comments'
+ * )
+ * @endcode
+ * Existing permissions are not changed, unless specified in $permissions.
+ *
+ * @see user_role_grant_permissions()
+ * @see user_role_revoke_permissions()
+ */
+function user_role_change_permissions($rid, array $permissions = array()) {
+ // Grant new permissions for the role.
+ $grant = array_filter($permissions);
+ if (!empty($grant)) {
+ user_role_grant_permissions($rid, array_keys($grant));
+ }
+ // Revoke permissions for the role.
+ $revoke = array_diff_assoc($permissions, $grant);
+ if (!empty($revoke)) {
+ user_role_revoke_permissions($rid, array_keys($revoke));
+ }
+}
+
+/**
+ * Grant permissions to a user role.
+ *
+ * @param $rid
+ * The ID of a user role to alter.
+ * @param $permissions
+ * A list of permission names to grant.
+ *
+ * @see user_role_change_permissions()
+ * @see user_role_revoke_permissions()
+ */
+function user_role_grant_permissions($rid, array $permissions = array()) {
+ $modules = user_permission_get_modules();
+ // Grant new permissions for the role.
+ foreach ($permissions as $name) {
+ db_merge('role_permission')
+ ->key(array(
+ 'rid' => $rid,
+ 'permission' => $name,
+ ))
+ ->fields(array(
+ 'module' => $modules[$name],
+ ))
+ ->execute();
+ }
+
+ // Clear the user access cache.
+ drupal_static_reset('user_access');
+ drupal_static_reset('user_role_permissions');
+}
+
+/**
+ * Revoke permissions from a user role.
+ *
+ * @param $rid
+ * The ID of a user role to alter.
+ * @param $permissions
+ * A list of permission names to revoke.
+ *
+ * @see user_role_change_permissions()
+ * @see user_role_grant_permissions()
+ */
+function user_role_revoke_permissions($rid, array $permissions = array()) {
+ // Revoke permissions for the role.
+ db_delete('role_permission')
+ ->condition('rid', $rid)
+ ->condition('permission', $permissions, 'IN')
+ ->execute();
+
+ // Clear the user access cache.
+ drupal_static_reset('user_access');
+ drupal_static_reset('user_role_permissions');
+}
+
+/**
+ * Implements hook_user_operations().
+ */
+function user_user_operations($form = array(), $form_state = array()) {
+ $operations = array(
+ 'unblock' => array(
+ 'label' => t('Unblock the selected users'),
+ 'callback' => 'user_user_operations_unblock',
+ ),
+ 'block' => array(
+ 'label' => t('Block the selected users'),
+ 'callback' => 'user_user_operations_block',
+ ),
+ 'cancel' => array(
+ 'label' => t('Cancel the selected user accounts'),
+ ),
+ );
+
+ if (user_access('administer permissions')) {
+ $roles = user_roles(TRUE);
+ unset($roles[DRUPAL_AUTHENTICATED_RID]); // Can't edit authenticated role.
+
+ $add_roles = array();
+ foreach ($roles as $key => $value) {
+ $add_roles['add_role-' . $key] = $value;
+ }
+
+ $remove_roles = array();
+ foreach ($roles as $key => $value) {
+ $remove_roles['remove_role-' . $key] = $value;
+ }
+
+ if (count($roles)) {
+ $role_operations = array(
+ t('Add a role to the selected users') => array(
+ 'label' => $add_roles,
+ ),
+ t('Remove a role from the selected users') => array(
+ 'label' => $remove_roles,
+ ),
+ );
+
+ $operations += $role_operations;
+ }
+ }
+
+ // If the form has been posted, we need to insert the proper data for
+ // role editing if necessary.
+ if (!empty($form_state['submitted'])) {
+ $operation_rid = explode('-', $form_state['values']['operation']);
+ $operation = $operation_rid[0];
+ if ($operation == 'add_role' || $operation == 'remove_role') {
+ $rid = $operation_rid[1];
+ if (user_access('administer permissions')) {
+ $operations[$form_state['values']['operation']] = array(
+ 'callback' => 'user_multiple_role_edit',
+ 'callback arguments' => array($operation, $rid),
+ );
+ }
+ else {
+ watchdog('security', 'Detected malicious attempt to alter protected user fields.', array(), WATCHDOG_WARNING);
+ return;
+ }
+ }
+ }
+
+ return $operations;
+}
+
+/**
+ * Callback function for admin mass unblocking users.
+ */
+function user_user_operations_unblock($accounts) {
+ $accounts = user_load_multiple($accounts);
+ foreach ($accounts as $account) {
+ // Skip unblocking user if they are already unblocked.
+ if ($account !== FALSE && $account->status == 0) {
+ user_save($account, array('status' => 1));
+ }
+ }
+}
+
+/**
+ * Callback function for admin mass blocking users.
+ */
+function user_user_operations_block($accounts) {
+ $accounts = user_load_multiple($accounts);
+ foreach ($accounts as $account) {
+ // Skip blocking user if they are already blocked.
+ if ($account !== FALSE && $account->status == 1) {
+ // For efficiency manually save the original account before applying any
+ // changes.
+ $account->original = clone $account;
+ user_save($account, array('status' => 0));
+ }
+ }
+}
+
+/**
+ * Callback function for admin mass adding/deleting a user role.
+ */
+function user_multiple_role_edit($accounts, $operation, $rid) {
+ // The role name is not necessary as user_save() will reload the user
+ // object, but some modules' hook_user() may look at this first.
+ $role_name = db_query('SELECT name FROM {role} WHERE rid = :rid', array(':rid' => $rid))->fetchField();
+
+ switch ($operation) {
+ case 'add_role':
+ $accounts = user_load_multiple($accounts);
+ foreach ($accounts as $account) {
+ // Skip adding the role to the user if they already have it.
+ if ($account !== FALSE && !isset($account->roles[$rid])) {
+ $roles = $account->roles + array($rid => $role_name);
+ // For efficiency manually save the original account before applying
+ // any changes.
+ $account->original = clone $account;
+ user_save($account, array('roles' => $roles));
+ }
+ }
+ break;
+ case 'remove_role':
+ $accounts = user_load_multiple($accounts);
+ foreach ($accounts as $account) {
+ // Skip removing the role from the user if they already don't have it.
+ if ($account !== FALSE && isset($account->roles[$rid])) {
+ $roles = array_diff($account->roles, array($rid => $role_name));
+ // For efficiency manually save the original account before applying
+ // any changes.
+ $account->original = clone $account;
+ user_save($account, array('roles' => $roles));
+ }
+ }
+ break;
+ }
+}
+
+function user_multiple_cancel_confirm($form, &$form_state) {
+ $edit = $form_state['input'];
+
+ $form['accounts'] = array('#prefix' => '
', '#tree' => TRUE);
+ $accounts = user_load_multiple(array_keys(array_filter($edit['accounts'])));
+ foreach ($accounts as $uid => $account) {
+ // Prevent user 1 from being canceled.
+ if ($uid <= 1) {
+ continue;
+ }
+ $form['accounts'][$uid] = array(
+ '#type' => 'hidden',
+ '#value' => $uid,
+ '#prefix' => '
',
+ '#suffix' => check_plain($account->name) . " \n",
+ );
+ }
+
+ // Output a notice that user 1 cannot be canceled.
+ if (isset($accounts[1])) {
+ $redirect = (count($accounts) == 1);
+ $message = t('The user account %name cannot be cancelled.', array('%name' => $accounts[1]->name));
+ drupal_set_message($message, $redirect ? 'error' : 'warning');
+ // If only user 1 was selected, redirect to the overview.
+ if ($redirect) {
+ drupal_goto('admin/people');
+ }
+ }
+
+ $form['operation'] = array('#type' => 'hidden', '#value' => 'cancel');
+
+ module_load_include('inc', 'user', 'user.pages');
+ $form['user_cancel_method'] = array(
+ '#type' => 'item',
+ '#title' => t('When cancelling these accounts'),
+ );
+ $form['user_cancel_method'] += user_cancel_methods();
+ // Remove method descriptions.
+ foreach (element_children($form['user_cancel_method']) as $element) {
+ unset($form['user_cancel_method'][$element]['#description']);
+ }
+
+ // Allow to send the account cancellation confirmation mail.
+ $form['user_cancel_confirm'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require e-mail confirmation to cancel account.'),
+ '#default_value' => FALSE,
+ '#description' => t('When enabled, the user must confirm the account cancellation via e-mail.'),
+ );
+ // Also allow to send account canceled notification mail, if enabled.
+ $form['user_cancel_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is canceled.'),
+ '#default_value' => FALSE,
+ '#access' => variable_get('user_mail_status_canceled_notify', FALSE),
+ '#description' => t('When enabled, the user will receive an e-mail notification after the account has been cancelled.'),
+ );
+
+ return confirm_form($form,
+ t('Are you sure you want to cancel these user accounts?'),
+ 'admin/people', t('This action cannot be undone.'),
+ t('Cancel accounts'), t('Cancel'));
+}
+
+/**
+ * Submit handler for mass-account cancellation form.
+ *
+ * @see user_multiple_cancel_confirm()
+ * @see user_cancel_confirm_form_submit()
+ */
+function user_multiple_cancel_confirm_submit($form, &$form_state) {
+ global $user;
+
+ if ($form_state['values']['confirm']) {
+ foreach ($form_state['values']['accounts'] as $uid => $value) {
+ // Prevent programmatic form submissions from cancelling user 1.
+ if ($uid <= 1) {
+ continue;
+ }
+ // Prevent user administrators from deleting themselves without confirmation.
+ if ($uid == $user->uid) {
+ $admin_form_state = $form_state;
+ unset($admin_form_state['values']['user_cancel_confirm']);
+ $admin_form_state['values']['_account'] = $user;
+ user_cancel_confirm_form_submit(array(), $admin_form_state);
+ }
+ else {
+ user_cancel($form_state['values'], $uid, $form_state['values']['user_cancel_method']);
+ }
+ }
+ }
+ $form_state['redirect'] = 'admin/people';
+}
+
+/**
+ * Retrieve a list of all user setting/information categories and sort them by weight.
+ */
+function _user_categories() {
+ $categories = module_invoke_all('user_categories');
+ usort($categories, '_user_sort');
+
+ return $categories;
+}
+
+function _user_sort($a, $b) {
+ $a = (array) $a + array('weight' => 0, 'title' => '');
+ $b = (array) $b + array('weight' => 0, 'title' => '');
+ return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : ($a['title'] < $b['title'] ? -1 : 1));
+}
+
+/**
+ * List user administration filters that can be applied.
+ */
+function user_filters() {
+ // Regular filters
+ $filters = array();
+ $roles = user_roles(TRUE);
+ unset($roles[DRUPAL_AUTHENTICATED_RID]); // Don't list authorized role.
+ if (count($roles)) {
+ $filters['role'] = array(
+ 'title' => t('role'),
+ 'field' => 'ur.rid',
+ 'options' => array(
+ '[any]' => t('any'),
+ ) + $roles,
+ );
+ }
+
+ $options = array();
+ foreach (module_implements('permission') as $module) {
+ $function = $module . '_permission';
+ if ($permissions = $function()) {
+ asort($permissions);
+ foreach ($permissions as $permission => $description) {
+ $options[t('@module module', array('@module' => $module))][$permission] = t($permission);
+ }
+ }
+ }
+ ksort($options);
+ $filters['permission'] = array(
+ 'title' => t('permission'),
+ 'options' => array(
+ '[any]' => t('any'),
+ ) + $options,
+ );
+
+ $filters['status'] = array(
+ 'title' => t('status'),
+ 'field' => 'u.status',
+ 'options' => array(
+ '[any]' => t('any'),
+ 1 => t('active'),
+ 0 => t('blocked'),
+ ),
+ );
+ return $filters;
+}
+
+/**
+ * Extends a query object for user administration filters based on session.
+ *
+ * @param $query
+ * Query object that should be filtered.
+ */
+function user_build_filter_query(SelectQuery $query) {
+ $filters = user_filters();
+ // Extend Query with filter conditions.
+ foreach (isset($_SESSION['user_overview_filter']) ? $_SESSION['user_overview_filter'] : array() as $filter) {
+ list($key, $value) = $filter;
+ // This checks to see if this permission filter is an enabled permission for
+ // the authenticated role. If so, then all users would be listed, and we can
+ // skip adding it to the filter query.
+ if ($key == 'permission') {
+ $account = new stdClass();
+ $account->uid = 'user_filter';
+ $account->roles = array(DRUPAL_AUTHENTICATED_RID => 1);
+ if (user_access($value, $account)) {
+ continue;
+ }
+ $users_roles_alias = $query->join('users_roles', 'ur', '%alias.uid = u.uid');
+ $permission_alias = $query->join('role_permission', 'p', $users_roles_alias . '.rid = %alias.rid');
+ $query->condition($permission_alias . '.permission', $value);
+ }
+ elseif ($key == 'role') {
+ $users_roles_alias = $query->join('users_roles', 'ur', '%alias.uid = u.uid');
+ $query->condition($users_roles_alias . '.rid' , $value);
+ }
+ else {
+ $query->condition($filters[$key]['field'], $value);
+ }
+ }
+}
+
+/**
+ * Implements hook_comment_view().
+ */
+function user_comment_view($comment) {
+ if (variable_get('user_signatures', 0) && !empty($comment->signature)) {
+ // @todo This alters and replaces the original object value, so a
+ // hypothetical process of loading, viewing, and saving will hijack the
+ // stored data. Consider renaming to $comment->signature_safe or similar
+ // here and elsewhere in Drupal 8.
+ $comment->signature = check_markup($comment->signature, $comment->signature_format, '', TRUE);
+ }
+ else {
+ $comment->signature = '';
+ }
+}
+
+/**
+ * Returns HTML for a user signature.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - signature: The user's signature.
+ *
+ * @ingroup themeable
+ */
+function theme_user_signature($variables) {
+ $signature = $variables['signature'];
+ $output = '';
+
+ if ($signature) {
+ $output .= '
';
+ $output .= '
—
';
+ $output .= $signature;
+ $output .= '
';
+ }
+
+ return $output;
+}
+
+/**
+ * Get the language object preferred by the user. This user preference can
+ * be set on the user account editing page, and is only available if there
+ * are more than one languages enabled on the site. If the user did not
+ * choose a preferred language, or is the anonymous user, the $default
+ * value, or if it is not set, the site default language will be returned.
+ *
+ * @param $account
+ * User account to look up language for.
+ * @param $default
+ * Optional default language object to return if the account
+ * has no valid language.
+ */
+function user_preferred_language($account, $default = NULL) {
+ $language_list = language_list();
+ if (!empty($account->language) && isset($language_list[$account->language])) {
+ return $language_list[$account->language];
+ }
+ else {
+ return $default ? $default : language_default();
+ }
+}
+
+/**
+ * Conditionally create and send a notification email when a certain
+ * operation happens on the given user account.
+ *
+ * @see user_mail_tokens()
+ * @see drupal_mail()
+ *
+ * @param $op
+ * The operation being performed on the account. Possible values:
+ * - 'register_admin_created': Welcome message for user created by the admin.
+ * - 'register_no_approval_required': Welcome message when user
+ * self-registers.
+ * - 'register_pending_approval': Welcome message, user pending admin
+ * approval.
+ * - 'password_reset': Password recovery request.
+ * - 'status_activated': Account activated.
+ * - 'status_blocked': Account blocked.
+ * - 'cancel_confirm': Account cancellation request.
+ * - 'status_canceled': Account canceled.
+ *
+ * @param $account
+ * The user object of the account being notified. Must contain at
+ * least the fields 'uid', 'name', and 'mail'.
+ * @param $language
+ * Optional language to use for the notification, overriding account language.
+ *
+ * @return
+ * The return value from drupal_mail_system()->mail(), if ends up being
+ * called.
+ */
+function _user_mail_notify($op, $account, $language = NULL) {
+ // By default, we always notify except for canceled and blocked.
+ $default_notify = ($op != 'status_canceled' && $op != 'status_blocked');
+ $notify = variable_get('user_mail_' . $op . '_notify', $default_notify);
+ if ($notify) {
+ $params['account'] = $account;
+ $language = $language ? $language : user_preferred_language($account);
+ $mail = drupal_mail('user', $op, $account->mail, $language, $params);
+ if ($op == 'register_pending_approval') {
+ // If a user registered requiring admin approval, notify the admin, too.
+ // We use the site default language for this.
+ drupal_mail('user', 'register_pending_approval_admin', variable_get('site_mail', ini_get('sendmail_from')), language_default(), $params);
+ }
+ }
+ return empty($mail) ? NULL : $mail['result'];
+}
+
+/**
+ * Form element process handler for client-side password validation.
+ *
+ * This #process handler is automatically invoked for 'password_confirm' form
+ * elements to add the JavaScript and string translations for dynamic password
+ * validation.
+ *
+ * @see system_element_info()
+ */
+function user_form_process_password_confirm($element) {
+ global $user;
+
+ $js_settings = array(
+ 'password' => array(
+ 'strengthTitle' => t('Password strength:'),
+ 'hasWeaknesses' => t('To make your password stronger:'),
+ 'tooShort' => t('Make it at least 6 characters'),
+ 'addLowerCase' => t('Add lowercase letters'),
+ 'addUpperCase' => t('Add uppercase letters'),
+ 'addNumbers' => t('Add numbers'),
+ 'addPunctuation' => t('Add punctuation'),
+ 'sameAsUsername' => t('Make it different from your username'),
+ 'confirmSuccess' => t('yes'),
+ 'confirmFailure' => t('no'),
+ 'weak' => t('Weak'),
+ 'fair' => t('Fair'),
+ 'good' => t('Good'),
+ 'strong' => t('Strong'),
+ 'confirmTitle' => t('Passwords match:'),
+ 'username' => (isset($user->name) ? $user->name : ''),
+ ),
+ );
+
+ $element['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.js';
+ $element['#attached']['js'][] = array('data' => $js_settings, 'type' => 'setting');
+
+ return $element;
+}
+
+/**
+ * Implements hook_node_load().
+ */
+function user_node_load($nodes, $types) {
+ // Build an array of all uids for node authors, keyed by nid.
+ $uids = array();
+ foreach ($nodes as $nid => $node) {
+ $uids[$nid] = $node->uid;
+ }
+
+ // Fetch name, picture, and data for these users.
+ $user_fields = db_query("SELECT uid, name, picture, data FROM {users} WHERE uid IN (:uids)", array(':uids' => $uids))->fetchAllAssoc('uid');
+
+ // Add these values back into the node objects.
+ foreach ($uids as $nid => $uid) {
+ $nodes[$nid]->name = $user_fields[$uid]->name;
+ $nodes[$nid]->picture = $user_fields[$uid]->picture;
+ $nodes[$nid]->data = $user_fields[$uid]->data;
+ }
+}
+
+/**
+ * Implements hook_image_style_delete().
+ */
+function user_image_style_delete($style) {
+ // If a style is deleted, update the variables.
+ // Administrators choose a replacement style when deleting.
+ user_image_style_save($style);
+}
+
+/**
+ * Implements hook_image_style_save().
+ */
+function user_image_style_save($style) {
+ // If a style is renamed, update the variables that use it.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('user_picture_style', '')) {
+ variable_set('user_picture_style', $style['name']);
+ }
+}
+
+/**
+ * Implements hook_action_info().
+ */
+function user_action_info() {
+ return array(
+ 'user_block_user_action' => array(
+ 'label' => t('Block current user'),
+ 'type' => 'user',
+ 'configurable' => FALSE,
+ 'triggers' => array('any'),
+ ),
+ );
+}
+
+/**
+ * Blocks a specific user or the current user, if one is not specified.
+ *
+ * @param $entity
+ * (optional) An entity object; if it is provided and it has a uid property,
+ * the user with that ID is blocked.
+ * @param $context
+ * (optional) An associative array; if no user ID is found in $entity, the
+ * 'uid' element of this array determines the user to block.
+ *
+ * @ingroup actions
+ */
+function user_block_user_action(&$entity, $context = array()) {
+ // First priority: If there is a $entity->uid, block that user.
+ // This is most likely a user object or the author if a node or comment.
+ 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' => 0));
+ watchdog('action', 'Blocked user %name.', array('%name' => $account->name));
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Add a checkbox for the 'user_register_form' instance settings on the 'Edit
+ * field instance' form.
+ */
+function user_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
+ $instance = $form['#instance'];
+
+ if ($instance['entity_type'] == 'user' && !$form['#field']['locked']) {
+ $form['instance']['settings']['user_register_form'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display on user registration form.'),
+ '#description' => t("This is compulsory for 'required' fields."),
+ // Field instances created in D7 beta releases before the setting was
+ // introduced might be set as 'required' and 'not shown on user_register
+ // form'. We make sure the checkbox comes as 'checked' for those.
+ '#default_value' => $instance['settings']['user_register_form'] || $instance['required'],
+ // Display just below the 'required' checkbox.
+ '#weight' => $form['instance']['required']['#weight'] + .1,
+ // Disabled when the 'required' checkbox is checked.
+ '#states' => array(
+ 'enabled' => array('input[name="instance[required]"]' => array('checked' => FALSE)),
+ ),
+ // Checked when the 'required' checkbox is checked. This is done through
+ // a custom behavior, since the #states system would also synchronize on
+ // uncheck.
+ '#attached' => array(
+ 'js' => array(drupal_get_path('module', 'user') . '/user.js'),
+ ),
+ );
+
+ array_unshift($form['#submit'], 'user_form_field_ui_field_edit_form_submit');
+ }
+}
+
+/**
+ * Additional submit handler for the 'Edit field instance' form.
+ *
+ * Make sure the 'user_register_form' setting is set for required fields.
+ */
+function user_form_field_ui_field_edit_form_submit($form, &$form_state) {
+ $instance = $form_state['values']['instance'];
+
+ if (!empty($instance['required'])) {
+ form_set_value($form['instance']['settings']['user_register_form'], 1, $form_state);
+ }
+}
+
+/**
+ * Form builder; the user registration form.
+ *
+ * @ingroup forms
+ * @see user_account_form()
+ * @see user_account_form_validate()
+ * @see user_register_submit()
+ */
+function user_register_form($form, &$form_state) {
+ global $user;
+
+ $admin = user_access('administer users');
+
+ // Pass access information to the submit handler. Running an access check
+ // inside the submit function interferes with form processing and breaks
+ // hook_form_alter().
+ $form['administer_users'] = array(
+ '#type' => 'value',
+ '#value' => $admin,
+ );
+
+ // If we aren't admin but already logged on, go to the user page instead.
+ if (!$admin && $user->uid) {
+ drupal_goto('user/' . $user->uid);
+ }
+
+ $form['#user'] = drupal_anonymous_user();
+ $form['#user_category'] = 'register';
+
+ $form['#attached']['library'][] = array('system', 'jquery.cookie');
+ $form['#attributes']['class'][] = 'user-info-from-cookie';
+
+ // Start with the default user account fields.
+ user_account_form($form, $form_state);
+
+ // Attach field widgets, and hide the ones where the 'user_register_form'
+ // setting is not on.
+ $langcode = entity_language('user', $form['#user']);
+ field_attach_form('user', $form['#user'], $form, $form_state, $langcode);
+ foreach (field_info_instances('user', 'user') as $field_name => $instance) {
+ if (empty($instance['settings']['user_register_form'])) {
+ $form[$field_name]['#access'] = FALSE;
+ }
+ }
+
+ if ($admin) {
+ // Redirect back to page which initiated the create request;
+ // usually admin/people/create.
+ $form_state['redirect'] = $_GET['q'];
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create new account'),
+ );
+
+ $form['#validate'][] = 'user_register_validate';
+ // Add the final user registration form submit handler.
+ $form['#submit'][] = 'user_register_submit';
+
+ return $form;
+}
+
+/**
+ * Validation function for the user registration form.
+ */
+function user_register_validate($form, &$form_state) {
+ entity_form_field_validate('user', $form, $form_state);
+}
+
+/**
+ * Submit handler for the user registration form.
+ *
+ * This function is shared by the installation form and the normal registration form,
+ * which is why it can't be in the user.pages.inc file.
+ *
+ * @see user_register_form()
+ */
+function user_register_submit($form, &$form_state) {
+ $admin = $form_state['values']['administer_users'];
+
+ if (!variable_get('user_email_verification', TRUE) || $admin) {
+ $pass = $form_state['values']['pass'];
+ }
+ else {
+ $pass = user_password();
+ }
+ $notify = !empty($form_state['values']['notify']);
+
+ // Remove unneeded values.
+ form_state_values_clean($form_state);
+
+ $form_state['values']['pass'] = $pass;
+ $form_state['values']['init'] = $form_state['values']['mail'];
+
+ $account = $form['#user'];
+
+ entity_form_submit_build_entity('user', $account, $form, $form_state);
+
+ // Populate $edit with the properties of $account, which have been edited on
+ // this form by taking over all values, which appear in the form values too.
+ $edit = array_intersect_key((array) $account, $form_state['values']);
+ $account = user_save($account, $edit);
+
+ // Terminate if an error occurred during user_save().
+ if (!$account) {
+ drupal_set_message(t("Error saving user account."), 'error');
+ $form_state['redirect'] = '';
+ return;
+ }
+ $form_state['user'] = $account;
+ $form_state['values']['uid'] = $account->uid;
+
+ watchdog('user', 'New user: %name (%email).', array('%name' => $form_state['values']['name'], '%email' => $form_state['values']['mail']), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));
+
+ // Add plain text password into user account to generate mail tokens.
+ $account->password = $pass;
+
+ // New administrative account without notification.
+ $uri = entity_uri('user', $account);
+ if ($admin && !$notify) {
+ drupal_set_message(t('Created a new user account for
%name . No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
+ }
+ // No e-mail verification required; log in user immediately.
+ elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) {
+ _user_mail_notify('register_no_approval_required', $account);
+ $form_state['uid'] = $account->uid;
+ user_login_submit(array(), $form_state);
+ drupal_set_message(t('Registration successful. You are now logged in.'));
+ $form_state['redirect'] = '';
+ }
+ // No administrator approval required.
+ elseif ($account->status || $notify) {
+ $op = $notify ? 'register_admin_created' : 'register_no_approval_required';
+ _user_mail_notify($op, $account);
+ if ($notify) {
+ drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user
%name .', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
+ }
+ else {
+ drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));
+ $form_state['redirect'] = '';
+ }
+ }
+ // Administrator approval required.
+ else {
+ _user_mail_notify('register_pending_approval', $account);
+ drupal_set_message(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.
In the meantime, a welcome message with further instructions has been sent to your e-mail address.'));
+ $form_state['redirect'] = '';
+ }
+}
+
+/**
+ * Implements hook_modules_installed().
+ */
+function user_modules_installed($modules) {
+ // Assign all available permissions to the administrator role.
+ $rid = variable_get('user_admin_role', 0);
+ if ($rid) {
+ $permissions = array();
+ foreach ($modules as $module) {
+ if ($module_permissions = module_invoke($module, 'permission')) {
+ $permissions = array_merge($permissions, array_keys($module_permissions));
+ }
+ }
+ if (!empty($permissions)) {
+ user_role_grant_permissions($rid, $permissions);
+ }
+ }
+}
+
+/**
+ * Implements hook_modules_uninstalled().
+ */
+function user_modules_uninstalled($modules) {
+ db_delete('role_permission')
+ ->condition('module', $modules, 'IN')
+ ->execute();
+}
+
+/**
+ * Helper function to rewrite the destination to avoid redirecting to login page after login.
+ *
+ * Third-party authentication modules may use this function to determine the
+ * proper destination after a user has been properly logged in.
+ */
+function user_login_destination() {
+ $destination = drupal_get_destination();
+ if ($destination['destination'] == 'user/login') {
+ $destination['destination'] = 'user';
+ }
+ return $destination;
+}
+
+/**
+ * Saves visitor information as a cookie so it can be reused.
+ *
+ * @param $values
+ * An array of key/value pairs to be saved into a cookie.
+ */
+function user_cookie_save(array $values) {
+ foreach ($values as $field => $value) {
+ // Set cookie for 365 days.
+ setrawcookie('Drupal.visitor.' . $field, rawurlencode($value), REQUEST_TIME + 31536000, '/');
+ }
+}
+
+/**
+ * Delete a visitor information cookie.
+ *
+ * @param $cookie_name
+ * A cookie name such as 'homepage'.
+ */
+function user_cookie_delete($cookie_name) {
+ setrawcookie('Drupal.visitor.' . $cookie_name, '', REQUEST_TIME - 3600, '/');
+}
+
+/**
+ * Implements hook_rdf_mapping().
+ */
+function user_rdf_mapping() {
+ return array(
+ array(
+ 'type' => 'user',
+ 'bundle' => RDF_DEFAULT_BUNDLE,
+ 'mapping' => array(
+ 'rdftype' => array('sioc:UserAccount'),
+ 'name' => array(
+ 'predicates' => array('foaf:name'),
+ ),
+ 'homepage' => array(
+ 'predicates' => array('foaf:page'),
+ 'type' => 'rel',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_file_download_access().
+ */
+function user_file_download_access($field, $entity_type, $entity) {
+ if ($entity_type == 'user') {
+ return user_view_access($entity);
+ }
+}
+
+/**
+ * Implements hook_system_info_alter().
+ *
+ * Drupal 7 ships with two methods to add additional fields to users: Profile
+ * module, a legacy module dating back from 2002, and Field API integration
+ * with users. While Field API support for users currently provides less end
+ * user features, the inefficient data storage mechanism of Profile module, as
+ * well as its lack of consistency with the rest of the entity / field based
+ * systems in Drupal 7, make this a sub-optimal solution to those who were not
+ * using it in previous releases of Drupal.
+ *
+ * To prevent new Drupal 7 sites from installing Profile module, and
+ * unwittingly ending up with two completely different and incompatible methods
+ * of extending users, only make the Profile module available if the profile_*
+ * tables are present.
+ *
+ * @todo: Remove in D8, pending upgrade path.
+ */
+function user_system_info_alter(&$info, $file, $type) {
+ if ($type == 'module' && $file->name == 'profile' && db_table_exists('profile_field')) {
+ $info['hidden'] = FALSE;
+ }
+}
diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc
new file mode 100644
index 0000000..2a1b291
--- /dev/null
+++ b/modules/user/user.pages.inc
@@ -0,0 +1,601 @@
+fields('users', array('name'))->condition('name', db_like($string) . '%', 'LIKE')->range(0, 10)->execute();
+ foreach ($result as $user) {
+ $matches[$user->name] = check_plain($user->name);
+ }
+ }
+
+ drupal_json_output($matches);
+}
+
+/**
+ * Form builder; Request a password reset.
+ *
+ * @ingroup forms
+ * @see user_pass_validate()
+ * @see user_pass_submit()
+ */
+function user_pass() {
+ global $user;
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Username or e-mail address'),
+ '#size' => 60,
+ '#maxlength' => max(USERNAME_MAX_LENGTH, EMAIL_MAX_LENGTH),
+ '#required' => TRUE,
+ '#default_value' => isset($_GET['name']) ? $_GET['name'] : '',
+ );
+ // Allow logged in users to request this also.
+ if ($user->uid > 0) {
+ $form['name']['#type'] = 'value';
+ $form['name']['#value'] = $user->mail;
+ $form['mail'] = array(
+ '#prefix' => '
',
+ // As of https://www.drupal.org/node/889772 the user no longer must log
+ // out (if they are still logged in when using the password reset link,
+ // they will be logged out automatically then), but this text is kept as
+ // is to avoid breaking translations as well as to encourage the user to
+ // log out manually at a time of their own choosing (when it will not
+ // interrupt anything else they may have been in the middle of doing).
+ '#markup' => t('Password reset instructions will be mailed to %email. You must log out to use the password reset link in the e-mail.', array('%email' => $user->mail)),
+ '#suffix' => '
',
+ );
+ }
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('E-mail new password'));
+
+ return $form;
+}
+
+/**
+ * Form validation handler for user_pass().
+ *
+ * @see user_pass_submit()
+ */
+function user_pass_validate($form, &$form_state) {
+ $name = trim($form_state['values']['name']);
+ // Try to load by email.
+ $users = user_load_multiple(array(), array('mail' => $name, 'status' => '1'));
+ $account = reset($users);
+ if (!$account) {
+ // No success, try to load by name.
+ $users = user_load_multiple(array(), array('name' => $name, 'status' => '1'));
+ $account = reset($users);
+ }
+ if (isset($account->uid)) {
+ form_set_value(array('#parents' => array('account')), $account, $form_state);
+ }
+ else {
+ form_set_error('name', t('Sorry, %name is not recognized as a user name or an e-mail address.', array('%name' => $name)));
+ }
+}
+
+/**
+ * Form submission handler for user_pass().
+ *
+ * @see user_pass_validate()
+ */
+function user_pass_submit($form, &$form_state) {
+ global $language;
+
+ $account = $form_state['values']['account'];
+ // Mail one time login URL and instructions using current language.
+ $mail = _user_mail_notify('password_reset', $account, $language);
+ if (!empty($mail)) {
+ watchdog('user', 'Password reset instructions mailed to %name at %email.', array('%name' => $account->name, '%email' => $account->mail));
+ drupal_set_message(t('Further instructions have been sent to your e-mail address.'));
+ }
+
+ $form_state['redirect'] = 'user';
+ return;
+}
+
+/**
+ * Menu callback; process one time login link and redirects to the user page on success.
+ */
+function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $action = NULL) {
+ global $user;
+
+ // When processing the one-time login link, we have to make sure that a user
+ // isn't already logged in.
+ if ($user->uid) {
+ // The existing user is already logged in. Log them out and reload the
+ // current page so the password reset process can continue.
+ if ($user->uid == $uid) {
+ // Preserve the current destination (if any) and ensure the redirect goes
+ // back to the current page; any custom destination set in
+ // hook_user_logout() and intended for regular logouts would not be
+ // appropriate here.
+ $destination = array();
+ if (isset($_GET['destination'])) {
+ $destination = drupal_get_destination();
+ }
+ user_logout_current_user();
+ unset($_GET['destination']);
+ drupal_goto(current_path(), array('query' => drupal_get_query_parameters() + $destination));
+ }
+ // A different user is already logged in on the computer.
+ else {
+ $reset_link_account = user_load($uid);
+ if (!empty($reset_link_account)) {
+ drupal_set_message(t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please
logout and try using the link again.',
+ array('%other_user' => $user->name, '%resetting_user' => $reset_link_account->name, '!logout' => url('user/logout'))), 'warning');
+ } else {
+ // Invalid one-time link specifies an unknown user.
+ drupal_set_message(t('The one-time login link you clicked is invalid.'), 'error');
+ }
+ drupal_goto();
+ }
+ }
+ else {
+ // Time out, in seconds, until login URL expires. Defaults to 24 hours =
+ // 86400 seconds.
+ $timeout = variable_get('user_password_reset_timeout', 86400);
+ $current = REQUEST_TIME;
+ // Some redundant checks for extra security ?
+ $users = user_load_multiple(array($uid), array('status' => '1'));
+ if ($timestamp <= $current && $account = reset($users)) {
+ // No time out for first time login.
+ if ($account->login && $current - $timestamp > $timeout) {
+ drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error');
+ drupal_goto('user/password');
+ }
+ elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
+ // First stage is a confirmation form, then login
+ if ($action == 'login') {
+ // Set the new user.
+ $user = $account;
+ // user_login_finalize() also updates the login timestamp of the
+ // user, which invalidates further use of the one-time login link.
+ user_login_finalize();
+ watchdog('user', 'User %name used one-time login link at time %timestamp.', array('%name' => $account->name, '%timestamp' => $timestamp));
+ drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'));
+ // Let the user's password be changed without the current password check.
+ $token = drupal_random_key();
+ $_SESSION['pass_reset_' . $user->uid] = $token;
+ drupal_goto('user/' . $user->uid . '/edit', array('query' => array('pass-reset-token' => $token)));
+ }
+ else {
+ $form['message'] = array('#markup' => t('
This is a one-time login for %user_name and will expire on %expiration_date.
Click on this button to log in to the site and change your password.
', array('%user_name' => $account->name, '%expiration_date' => format_date($timestamp + $timeout))));
+ $form['help'] = array('#markup' => '
' . t('This login can be used only once.') . '
');
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
+ $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login");
+ return $form;
+ }
+ }
+ else {
+ drupal_set_message(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'error');
+ drupal_goto('user/password');
+ }
+ }
+ else {
+ // Deny access, no more clues.
+ // Everything will be in the watchdog's URL for the administrator to check.
+ drupal_access_denied();
+ drupal_exit();
+ }
+ }
+}
+
+/**
+ * Menu callback; logs the current user out, and redirects to the home page.
+ */
+function user_logout() {
+ user_logout_current_user();
+ drupal_goto();
+}
+
+/**
+ * Logs the current user out.
+ */
+function user_logout_current_user() {
+ global $user;
+
+ watchdog('user', 'Session closed for %name.', array('%name' => $user->name));
+
+ module_invoke_all('user_logout', $user);
+
+ // Destroy the current session, and reset $user to the anonymous user.
+ session_destroy();
+}
+
+/**
+ * Process variables for user-profile.tpl.php.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - elements: An associative array containing the user information and any
+ * fields attached to the user. Properties used:
+ * - #account: The user account of the profile being viewed.
+ *
+ * @see user-profile.tpl.php
+ */
+function template_preprocess_user_profile(&$variables) {
+ $account = $variables['elements']['#account'];
+
+ // Helpful $user_profile variable for templates.
+ foreach (element_children($variables['elements']) as $key) {
+ $variables['user_profile'][$key] = $variables['elements'][$key];
+ }
+
+ // Preprocess fields.
+ field_attach_preprocess('user', $account, $variables['elements'], $variables);
+}
+
+/**
+ * Process variables for user-profile-item.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $element
+ *
+ * @see user-profile-item.tpl.php
+ */
+function template_preprocess_user_profile_item(&$variables) {
+ $variables['title'] = $variables['element']['#title'];
+ $variables['value'] = $variables['element']['#markup'];
+ $variables['attributes'] = '';
+ if (isset($variables['element']['#attributes'])) {
+ $variables['attributes'] = drupal_attributes($variables['element']['#attributes']);
+ }
+}
+
+/**
+ * Process variables for user-profile-category.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $element
+ *
+ * @see user-profile-category.tpl.php
+ */
+function template_preprocess_user_profile_category(&$variables) {
+ $variables['title'] = check_plain($variables['element']['#title']);
+ $variables['profile_items'] = $variables['element']['#children'];
+ $variables['attributes'] = '';
+ if (isset($variables['element']['#attributes'])) {
+ $variables['attributes'] = drupal_attributes($variables['element']['#attributes']);
+ }
+}
+
+/**
+ * Form builder; edit a user account or one of their profile categories.
+ *
+ * @ingroup forms
+ * @see user_account_form()
+ * @see user_account_form_validate()
+ * @see user_profile_form_validate()
+ * @see user_profile_form_submit()
+ * @see user_cancel_confirm_form_submit()
+ */
+function user_profile_form($form, &$form_state, $account, $category = 'account') {
+ global $user;
+
+ // During initial form build, add the entity to the form state for use during
+ // form building and processing. During a rebuild, use what is in the form
+ // state.
+ if (!isset($form_state['user'])) {
+ $form_state['user'] = $account;
+ }
+ else {
+ $account = $form_state['user'];
+ }
+
+ // @todo Legacy support. Modules are encouraged to access the entity using
+ // $form_state. Remove in Drupal 8.
+ $form['#user'] = $account;
+ $form['#user_category'] = $category;
+
+ if ($category == 'account') {
+ user_account_form($form, $form_state);
+ // Attach field widgets.
+ $langcode = entity_language('user', $account);
+ field_attach_form('user', $account, $form, $form_state, $langcode);
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+ if ($category == 'account') {
+ $form['actions']['cancel'] = array(
+ '#type' => 'submit',
+ '#value' => t('Cancel account'),
+ '#submit' => array('user_edit_cancel_submit'),
+ '#access' => $account->uid > 1 && (($account->uid == $user->uid && user_access('cancel account')) || user_access('administer users')),
+ );
+ }
+
+ $form['#validate'][] = 'user_profile_form_validate';
+ // Add the final user profile form submit handler.
+ $form['#submit'][] = 'user_profile_form_submit';
+
+ return $form;
+}
+
+/**
+ * Form validation handler for user_profile_form().
+ *
+ * @see user_profile_form_submit()
+ */
+function user_profile_form_validate($form, &$form_state) {
+ entity_form_field_validate('user', $form, $form_state);
+}
+
+/**
+ * Form submission handler for user_profile_form().
+ *
+ * @see user_profile_form_validate()
+ */
+function user_profile_form_submit($form, &$form_state) {
+ $account = $form_state['user'];
+ $category = $form['#user_category'];
+ // Remove unneeded values.
+ form_state_values_clean($form_state);
+
+ // Before updating the account entity, keep an unchanged copy for use with
+ // user_save() later. This is necessary for modules implementing the user
+ // hooks to be able to react on changes by comparing the values of $account
+ // and $edit.
+ $account_unchanged = clone $account;
+
+ entity_form_submit_build_entity('user', $account, $form, $form_state);
+
+ // Populate $edit with the properties of $account, which have been edited on
+ // this form by taking over all values, which appear in the form values too.
+ $edit = array_intersect_key((array) $account, $form_state['values']);
+
+ user_save($account_unchanged, $edit, $category);
+ $form_state['values']['uid'] = $account->uid;
+
+ if ($category == 'account' && !empty($edit['pass'])) {
+ // Remove the password reset tag since a new password was saved.
+ unset($_SESSION['pass_reset_'. $account->uid]);
+ }
+ // Clear the page cache because pages can contain usernames and/or profile information:
+ cache_clear_all();
+
+ drupal_set_message(t('The changes have been saved.'));
+}
+
+/**
+ * Submit function for the 'Cancel account' button on the user edit form.
+ */
+function user_edit_cancel_submit($form, &$form_state) {
+ $destination = array();
+ if (isset($_GET['destination'])) {
+ $destination = drupal_get_destination();
+ unset($_GET['destination']);
+ }
+ // Note: We redirect from user/uid/edit to user/uid/cancel to make the tabs disappear.
+ $form_state['redirect'] = array("user/" . $form['#user']->uid . "/cancel", array('query' => $destination));
+}
+
+/**
+ * Form builder; confirm form for cancelling user account.
+ *
+ * @ingroup forms
+ * @see user_edit_cancel_submit()
+ */
+function user_cancel_confirm_form($form, &$form_state, $account) {
+ global $user;
+
+ $form['_account'] = array('#type' => 'value', '#value' => $account);
+
+ // Display account cancellation method selection, if allowed.
+ $admin_access = user_access('administer users');
+ $can_select_method = $admin_access || user_access('select account cancellation method');
+ $form['user_cancel_method'] = array(
+ '#type' => 'item',
+ '#title' => ($account->uid == $user->uid ? t('When cancelling your account') : t('When cancelling the account')),
+ '#access' => $can_select_method,
+ );
+ $form['user_cancel_method'] += user_cancel_methods();
+
+ // Allow user administrators to skip the account cancellation confirmation
+ // mail (by default), as long as they do not attempt to cancel their own
+ // account.
+ $override_access = $admin_access && ($account->uid != $user->uid);
+ $form['user_cancel_confirm'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require e-mail confirmation to cancel account.'),
+ '#default_value' => ($override_access ? FALSE : TRUE),
+ '#access' => $override_access,
+ '#description' => t('When enabled, the user must confirm the account cancellation via e-mail.'),
+ );
+ // Also allow to send account canceled notification mail, if enabled.
+ $default_notify = variable_get('user_mail_status_canceled_notify', FALSE);
+ $form['user_cancel_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is canceled.'),
+ '#default_value' => ($override_access ? FALSE : $default_notify),
+ '#access' => $override_access && $default_notify,
+ '#description' => t('When enabled, the user will receive an e-mail notification after the account has been cancelled.'),
+ );
+
+ // Prepare confirmation form page title and description.
+ if ($account->uid == $user->uid) {
+ $question = t('Are you sure you want to cancel your account?');
+ }
+ else {
+ $question = t('Are you sure you want to cancel the account %name?', array('%name' => $account->name));
+ }
+ $description = '';
+ if ($can_select_method) {
+ $description = t('Select the method to cancel the account above.');
+ foreach (element_children($form['user_cancel_method']) as $element) {
+ unset($form['user_cancel_method'][$element]['#description']);
+ }
+ }
+ else {
+ // The radio button #description is used as description for the confirmation
+ // form.
+ foreach (element_children($form['user_cancel_method']) as $element) {
+ if ($form['user_cancel_method'][$element]['#default_value'] == $form['user_cancel_method'][$element]['#return_value']) {
+ $description = $form['user_cancel_method'][$element]['#description'];
+ }
+ unset($form['user_cancel_method'][$element]['#description']);
+ }
+ }
+
+ // Always provide entity id in the same form key as in the entity edit form.
+ $form['uid'] = array('#type' => 'value', '#value' => $account->uid);
+ return confirm_form($form,
+ $question,
+ 'user/' . $account->uid,
+ $description . ' ' . t('This action cannot be undone.'),
+ t('Cancel account'), t('Cancel'));
+}
+
+/**
+ * Submit handler for the account cancellation confirm form.
+ *
+ * @see user_cancel_confirm_form()
+ * @see user_multiple_cancel_confirm_submit()
+ */
+function user_cancel_confirm_form_submit($form, &$form_state) {
+ global $user;
+ $account = $form_state['values']['_account'];
+
+ // Cancel account immediately, if the current user has administrative
+ // privileges, no confirmation mail shall be sent, and the user does not
+ // attempt to cancel the own account.
+ if (user_access('administer users') && empty($form_state['values']['user_cancel_confirm']) && $account->uid != $user->uid) {
+ user_cancel($form_state['values'], $account->uid, $form_state['values']['user_cancel_method']);
+
+ $form_state['redirect'] = 'admin/people';
+ }
+ else {
+ // Store cancelling method and whether to notify the user in $account for
+ // user_cancel_confirm().
+ $edit = array(
+ 'user_cancel_method' => $form_state['values']['user_cancel_method'],
+ 'user_cancel_notify' => $form_state['values']['user_cancel_notify'],
+ );
+ $account = user_save($account, $edit);
+ _user_mail_notify('cancel_confirm', $account);
+ drupal_set_message(t('A confirmation request to cancel your account has been sent to your e-mail address.'));
+ watchdog('user', 'Sent account cancellation request to %name %email.', array('%name' => $account->name, '%email' => '<' . $account->mail . '>'), WATCHDOG_NOTICE);
+
+ $form_state['redirect'] = "user/$account->uid";
+ }
+}
+
+/**
+ * Helper function to return available account cancellation methods.
+ *
+ * See documentation of hook_user_cancel_methods_alter().
+ *
+ * @return
+ * An array containing all account cancellation methods as form elements.
+ *
+ * @see hook_user_cancel_methods_alter()
+ * @see user_admin_settings()
+ * @see user_cancel_confirm_form()
+ * @see user_multiple_cancel_confirm()
+ */
+function user_cancel_methods() {
+ $methods = array(
+ 'user_cancel_block' => array(
+ 'title' => t('Disable the account and keep its content.'),
+ 'description' => t('Your account will be blocked and you will no longer be able to log in. All of your content will remain attributed to your user name.'),
+ ),
+ 'user_cancel_block_unpublish' => array(
+ 'title' => t('Disable the account and unpublish its content.'),
+ 'description' => t('Your account will be blocked and you will no longer be able to log in. All of your content will be hidden from everyone but administrators.'),
+ ),
+ 'user_cancel_reassign' => array(
+ 'title' => t('Delete the account and make its content belong to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))),
+ 'description' => t('Your account will be removed and all account information deleted. All of your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))),
+ ),
+ 'user_cancel_delete' => array(
+ 'title' => t('Delete the account and its content.'),
+ 'description' => t('Your account will be removed and all account information deleted. All of your content will also be deleted.'),
+ 'access' => user_access('administer users'),
+ ),
+ );
+ // Allow modules to customize account cancellation methods.
+ drupal_alter('user_cancel_methods', $methods);
+
+ // Turn all methods into real form elements.
+ $default_method = variable_get('user_cancel_method', 'user_cancel_block');
+ foreach ($methods as $name => $method) {
+ $form[$name] = array(
+ '#type' => 'radio',
+ '#title' => $method['title'],
+ '#description' => (isset($method['description']) ? $method['description'] : NULL),
+ '#return_value' => $name,
+ '#default_value' => $default_method,
+ '#parents' => array('user_cancel_method'),
+ );
+ }
+ return $form;
+}
+
+/**
+ * Menu callback; Cancel a user account via e-mail confirmation link.
+ *
+ * @see user_cancel_confirm_form()
+ * @see user_cancel_url()
+ */
+function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
+ // Time out in seconds until cancel URL expires; 24 hours = 86400 seconds.
+ $timeout = 86400;
+ $current = REQUEST_TIME;
+
+ // Basic validation of arguments.
+ if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
+ // Validate expiration and hashed password/login.
+ if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
+ $edit = array(
+ 'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE),
+ );
+ user_cancel($edit, $account->uid, $account->data['user_cancel_method']);
+ // Since user_cancel() is not invoked via Form API, batch processing needs
+ // to be invoked manually and should redirect to the front page after
+ // completion.
+ batch_process('');
+ }
+ else {
+ drupal_set_message(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'error');
+ drupal_goto("user/$account->uid/cancel");
+ }
+ }
+ return MENU_ACCESS_DENIED;
+}
+
+/**
+ * Page callback: Displays the user page.
+ *
+ * Displays user profile if user is logged in, or login form for anonymous
+ * users.
+ *
+ * @return
+ * A render array for either a user profile or a login form.
+ *
+ * @see user_view_page()
+ * @see user_login()
+ */
+function user_page() {
+ global $user;
+ if ($user->uid) {
+ menu_set_active_item('user/' . $user->uid);
+ return menu_execute_active_handler(NULL, FALSE);
+ }
+ else {
+ return drupal_get_form('user_login');
+ }
+}
diff --git a/modules/user/user.permissions.js b/modules/user/user.permissions.js
new file mode 100644
index 0000000..988820e
--- /dev/null
+++ b/modules/user/user.permissions.js
@@ -0,0 +1,69 @@
+(function ($) {
+
+/**
+ * Shows checked and disabled checkboxes for inherited permissions.
+ */
+Drupal.behaviors.permissions = {
+ attach: function (context) {
+ var self = this;
+ $('table#permissions').once('permissions', function () {
+ // On a site with many roles and permissions, this behavior initially has
+ // to perform thousands of DOM manipulations to inject checkboxes and hide
+ // them. By detaching the table from the DOM, all operations can be
+ // performed without triggering internal layout and re-rendering processes
+ // in the browser.
+ var $table = $(this);
+ if ($table.prev().length) {
+ var $ancestor = $table.prev(), method = 'after';
+ }
+ else {
+ var $ancestor = $table.parent(), method = 'append';
+ }
+ $table.detach();
+
+ // Create dummy checkboxes. We use dummy checkboxes instead of reusing
+ // the existing checkboxes here because new checkboxes don't alter the
+ // submitted form. If we'd automatically check existing checkboxes, the
+ // permission table would be polluted with redundant entries. This
+ // is deliberate, but desirable when we automatically check them.
+ var $dummy = $('
')
+ .attr('title', Drupal.t("This permission is inherited from the authenticated user role."))
+ .hide();
+
+ $('input[type=checkbox]', this).not('.rid-2, .rid-1').addClass('real-checkbox').each(function () {
+ $dummy.clone().insertAfter(this);
+ });
+
+ // Initialize the authenticated user checkbox.
+ $('input[type=checkbox].rid-2', this)
+ .bind('click.permissions', self.toggle)
+ // .triggerHandler() cannot be used here, as it only affects the first
+ // element.
+ .each(self.toggle);
+
+ // Re-insert the table into the DOM.
+ $ancestor[method]($table);
+ });
+ },
+
+ /**
+ * Toggles all dummy checkboxes based on the checkboxes' state.
+ *
+ * If the "authenticated user" checkbox is checked, the checked and disabled
+ * checkboxes are shown, the real checkboxes otherwise.
+ */
+ toggle: function () {
+ var authCheckbox = this, $row = $(this).closest('tr');
+ // jQuery performs too many layout calculations for .hide() and .show(),
+ // leading to a major page rendering lag on sites with many roles and
+ // permissions. Therefore, we toggle visibility directly.
+ $row.find('.real-checkbox').each(function () {
+ this.style.display = (authCheckbox.checked ? 'none' : '');
+ });
+ $row.find('.dummy-checkbox').each(function () {
+ this.style.display = (authCheckbox.checked ? '' : 'none');
+ });
+ }
+};
+
+})(jQuery);
diff --git a/modules/user/user.test b/modules/user/user.test
new file mode 100644
index 0000000..0875e0a
--- /dev/null
+++ b/modules/user/user.test
@@ -0,0 +1,2636 @@
+ 'User registration',
+ 'description' => 'Test registration of user under different configurations.',
+ 'group' => 'User'
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+ }
+
+ function testRegistrationWithEmailVerification() {
+ // Require e-mail verification.
+ variable_set('user_email_verification', TRUE);
+
+ // Set registration to administrator only.
+ variable_set('user_register', USER_REGISTER_ADMINISTRATORS_ONLY);
+ $this->drupalGet('user/register');
+ $this->assertResponse(403, 'Registration page is inaccessible when only administrators can create accounts.');
+
+ // Allow registration by site visitors without administrator approval.
+ variable_set('user_register', USER_REGISTER_VISITORS);
+ $edit = array();
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $this->assertText(t('A welcome message with further instructions has been sent to your e-mail address.'), 'User registered successfully.');
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $this->assertTrue($new_user->status, 'New account is active after registration.');
+
+ // Allow registration by site visitors, but require administrator approval.
+ variable_set('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+ $edit = array();
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $this->assertFalse($new_user->status, 'New account is blocked until approved by an administrator.');
+ }
+
+ function testRegistrationWithoutEmailVerification() {
+ // Don't require e-mail verification.
+ variable_set('user_email_verification', FALSE);
+
+ // Allow registration by site visitors without administrator approval.
+ variable_set('user_register', USER_REGISTER_VISITORS);
+ $edit = array();
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+
+ // Try entering a mismatching password.
+ $edit['pass[pass1]'] = '99999.0';
+ $edit['pass[pass2]'] = '99999';
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $this->assertText(t('The specified passwords do not match.'), 'Typing mismatched passwords displays an error message.');
+
+ // Enter a correct password.
+ $edit['pass[pass1]'] = $new_pass = $this->randomName();
+ $edit['pass[pass2]'] = $new_pass;
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $this->assertText(t('Registration successful. You are now logged in.'), 'Users are logged in after registering.');
+ $this->drupalLogout();
+
+ // Allow registration by site visitors, but require administrator approval.
+ variable_set('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+ $edit = array();
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $edit['pass[pass1]'] = $pass = $this->randomName();
+ $edit['pass[pass2]'] = $pass;
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $this->assertText(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.'), 'Users are notified of pending approval');
+
+ // Try to login before administrator approval.
+ $auth = array(
+ 'name' => $name,
+ 'pass' => $pass,
+ );
+ $this->drupalPost('user/login', $auth, t('Log in'));
+ $this->assertText(t('The username @name has not been activated or is blocked.', array('@name' => $name)), 'User cannot login yet.');
+
+ // Activate the new account.
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $admin_user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($admin_user);
+ $edit = array(
+ 'status' => 1,
+ );
+ $this->drupalPost('user/' . $new_user->uid . '/edit', $edit, t('Save'));
+ $this->drupalLogout();
+
+ // Login after administrator approval.
+ $this->drupalPost('user/login', $auth, t('Log in'));
+ $this->assertText(t('Member for'), 'User can log in after administrator approval.');
+ }
+
+ function testRegistrationEmailDuplicates() {
+ // Don't require e-mail verification.
+ variable_set('user_email_verification', FALSE);
+
+ // Allow registration by site visitors without administrator approval.
+ variable_set('user_register', USER_REGISTER_VISITORS);
+
+ // Set up a user to check for duplicates.
+ $duplicate_user = $this->drupalCreateUser();
+
+ $edit = array();
+ $edit['name'] = $this->randomName();
+ $edit['mail'] = $duplicate_user->mail;
+
+ // Attempt to create a new account using an existing e-mail address.
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $this->assertText(t('The e-mail address @email is already registered.', array('@email' => $duplicate_user->mail)), 'Supplying an exact duplicate email address displays an error message');
+
+ // Attempt to bypass duplicate email registration validation by adding spaces.
+ $edit['mail'] = ' ' . $duplicate_user->mail . ' ';
+
+ $this->drupalPost('user/register', $edit, t('Create new account'));
+ $this->assertText(t('The e-mail address @email is already registered.', array('@email' => $duplicate_user->mail)), 'Supplying a duplicate email address with added whitespace displays an error message');
+ }
+
+ function testRegistrationDefaultValues() {
+ // Allow registration by site visitors without administrator approval.
+ variable_set('user_register', USER_REGISTER_VISITORS);
+
+ // Don't require e-mail verification.
+ variable_set('user_email_verification', FALSE);
+
+ // Set the default timezone to Brussels.
+ variable_set('configurable_timezones', 1);
+ variable_set('date_default_timezone', 'Europe/Brussels');
+
+ // Check that the account information fieldset's options are not displayed
+ // is a fieldset if there is not more than one fieldset in the form.
+ $this->drupalGet('user/register');
+ $this->assertNoRaw('
Account information ', 'Account settings fieldset was hidden.');
+
+ $edit = array();
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $edit['pass[pass1]'] = $new_pass = $this->randomName();
+ $edit['pass[pass2]'] = $new_pass;
+ $this->drupalPost(NULL, $edit, t('Create new account'));
+
+ // Check user fields.
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $this->assertEqual($new_user->name, $name, 'Username matches.');
+ $this->assertEqual($new_user->mail, $mail, 'E-mail address matches.');
+ $this->assertEqual($new_user->theme, '', 'Correct theme field.');
+ $this->assertEqual($new_user->signature, '', 'Correct signature field.');
+ $this->assertTrue(($new_user->created > REQUEST_TIME - 20 ), 'Correct creation time.');
+ $this->assertEqual($new_user->status, variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS ? 1 : 0, 'Correct status field.');
+ $this->assertEqual($new_user->timezone, variable_get('date_default_timezone'), 'Correct time zone field.');
+ $this->assertEqual($new_user->language, '', 'Correct language field.');
+ $this->assertEqual($new_user->picture, '', 'Correct picture field.');
+ $this->assertEqual($new_user->init, $mail, 'Correct init field.');
+ }
+
+ /**
+ * Tests Field API fields on user registration forms.
+ */
+ function testRegistrationWithUserFields() {
+ // Create a field, and an instance on 'user' entity type.
+ $field = array(
+ 'type' => 'test_field',
+ 'field_name' => 'test_user_field',
+ 'cardinality' => 1,
+ );
+ field_create_field($field);
+ $instance = array(
+ 'field_name' => 'test_user_field',
+ 'entity_type' => 'user',
+ 'label' => 'Some user field',
+ 'bundle' => 'user',
+ 'required' => TRUE,
+ 'settings' => array('user_register_form' => FALSE),
+ );
+ field_create_instance($instance);
+
+ // Check that the field does not appear on the registration form.
+ $this->drupalGet('user/register');
+ $this->assertNoText($instance['label'], 'The field does not appear on user registration form');
+
+ // Have the field appear on the registration form.
+ $instance['settings']['user_register_form'] = TRUE;
+ field_update_instance($instance);
+ $this->drupalGet('user/register');
+ $this->assertText($instance['label'], 'The field appears on user registration form');
+
+ // Check that validation errors are correctly reported.
+ $edit = array();
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ // Missing input in required field.
+ $edit['test_user_field[und][0][value]'] = '';
+ $this->drupalPost(NULL, $edit, t('Create new account'));
+ $this->assertRaw(t('@name field is required.', array('@name' => $instance['label'])), 'Field validation error was correctly reported.');
+ // Invalid input.
+ $edit['test_user_field[und][0][value]'] = '-1';
+ $this->drupalPost(NULL, $edit, t('Create new account'));
+ $this->assertRaw(t('%name does not accept the value -1.', array('%name' => $instance['label'])), 'Field validation error was correctly reported.');
+
+ // Submit with valid data.
+ $value = rand(1, 255);
+ $edit['test_user_field[und][0][value]'] = $value;
+ $this->drupalPost(NULL, $edit, t('Create new account'));
+ // Check user fields.
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][0]['value'], $value, 'The field value was correctly saved.');
+
+ // Check that the 'add more' button works.
+ $field['cardinality'] = FIELD_CARDINALITY_UNLIMITED;
+ field_update_field($field);
+ foreach (array('js', 'nojs') as $js) {
+ $this->drupalGet('user/register');
+ // Add two inputs.
+ $value = rand(1, 255);
+ $edit = array();
+ $edit['test_user_field[und][0][value]'] = $value;
+ if ($js == 'js') {
+ $this->drupalPostAJAX(NULL, $edit, 'test_user_field_add_more');
+ $this->drupalPostAJAX(NULL, $edit, 'test_user_field_add_more');
+ }
+ else {
+ $this->drupalPost(NULL, $edit, t('Add another item'));
+ $this->drupalPost(NULL, $edit, t('Add another item'));
+ }
+ // Submit with three values.
+ $edit['test_user_field[und][1][value]'] = $value + 1;
+ $edit['test_user_field[und][2][value]'] = $value + 2;
+ $edit['name'] = $name = $this->randomName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $this->drupalPost(NULL, $edit, t('Create new account'));
+ // Check user fields.
+ $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+ $new_user = reset($accounts);
+ $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][0]['value'], $value, format_string('@js : The field value was correclty saved.', array('@js' => $js)));
+ $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][1]['value'], $value + 1, format_string('@js : The field value was correclty saved.', array('@js' => $js)));
+ $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][2]['value'], $value + 2, format_string('@js : The field value was correclty saved.', array('@js' => $js)));
+ }
+ }
+}
+
+class UserValidationTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Username/e-mail validation',
+ 'description' => 'Verify that username/email validity checks behave as designed.',
+ 'group' => 'User'
+ );
+ }
+
+ // Username validation.
+ function testUsernames() {
+ $test_cases = array( // '' => array('', 'assert'),
+ 'foo' => array('Valid username', 'assertNull'),
+ 'FOO' => array('Valid username', 'assertNull'),
+ 'Foo O\'Bar' => array('Valid username', 'assertNull'),
+ 'foo@bar' => array('Valid username', 'assertNull'),
+ 'foo@example.com' => array('Valid username', 'assertNull'),
+ 'foo@-example.com' => array('Valid username', 'assertNull'), // invalid domains are allowed in usernames
+ 'þòøÇߪř€' => array('Valid username', 'assertNull'),
+ 'ᚠᛇᚻ᛫ᛒᛦᚦ' => array('Valid UTF8 username', 'assertNull'), // runes
+ ' foo' => array('Invalid username that starts with a space', 'assertNotNull'),
+ 'foo ' => array('Invalid username that ends with a space', 'assertNotNull'),
+ 'foo bar' => array('Invalid username that contains 2 spaces \' \'', 'assertNotNull'),
+ '' => array('Invalid empty username', 'assertNotNull'),
+ 'foo/' => array('Invalid username containing invalid chars', 'assertNotNull'),
+ 'foo' . chr(0) . 'bar' => array('Invalid username containing chr(0)', 'assertNotNull'), // NULL
+ 'foo' . chr(13) . 'bar' => array('Invalid username containing chr(13)', 'assertNotNull'), // CR
+ str_repeat('x', USERNAME_MAX_LENGTH + 1) => array('Invalid excessively long username', 'assertNotNull'),
+ );
+ foreach ($test_cases as $name => $test_case) {
+ list($description, $test) = $test_case;
+ $result = user_validate_name($name);
+ $this->$test($result, $description . ' (' . $name . ')');
+ }
+ }
+
+ // Mail validation. More extensive tests can be found at common.test
+ function testMailAddresses() {
+ $test_cases = array( // '' => array('', 'assert'),
+ '' => array('Empty mail address', 'assertNotNull'),
+ 'foo' => array('Invalid mail address', 'assertNotNull'),
+ 'foo@example.com' => array('Valid mail address', 'assertNull'),
+ );
+ foreach ($test_cases as $name => $test_case) {
+ list($description, $test) = $test_case;
+ $result = user_validate_mail($name);
+ $this->$test($result, $description . ' (' . $name . ')');
+ }
+ }
+}
+
+/**
+ * Functional tests for user logins, including rate limiting of login attempts.
+ */
+class UserLoginTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User login',
+ 'description' => 'Ensure that login works as expected.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Test the global login flood control.
+ */
+ function testGlobalLoginFloodControl() {
+ // Set the global login limit.
+ variable_set('user_failed_login_ip_limit', 10);
+ // Set a high per-user limit out so that it is not relevant in the test.
+ variable_set('user_failed_login_user_limit', 4000);
+
+ $user1 = $this->drupalCreateUser(array());
+ $incorrect_user1 = clone $user1;
+ $incorrect_user1->pass_raw .= 'incorrect';
+
+ // Try 2 failed logins.
+ for ($i = 0; $i < 2; $i++) {
+ $this->assertFailedLogin($incorrect_user1);
+ }
+
+ // A successful login will not reset the IP-based flood control count.
+ $this->drupalLogin($user1);
+ $this->drupalLogout();
+
+ // Try 8 more failed logins, they should not trigger the flood control
+ // mechanism.
+ for ($i = 0; $i < 8; $i++) {
+ $this->assertFailedLogin($incorrect_user1);
+ }
+
+ // The next login trial should result in an IP-based flood error message.
+ $this->assertFailedLogin($incorrect_user1, 'ip');
+
+ // A login with the correct password should also result in a flood error
+ // message.
+ $this->assertFailedLogin($user1, 'ip');
+ }
+
+ /**
+ * Test the per-user login flood control.
+ */
+ function testPerUserLoginFloodControl() {
+ // Set a high global limit out so that it is not relevant in the test.
+ variable_set('user_failed_login_ip_limit', 4000);
+ // Set the per-user login limit.
+ variable_set('user_failed_login_user_limit', 3);
+
+ $user1 = $this->drupalCreateUser(array());
+ $incorrect_user1 = clone $user1;
+ $incorrect_user1->pass_raw .= 'incorrect';
+
+ $user2 = $this->drupalCreateUser(array());
+
+ // Try 2 failed logins.
+ for ($i = 0; $i < 2; $i++) {
+ $this->assertFailedLogin($incorrect_user1);
+ }
+
+ // A successful login will reset the per-user flood control count.
+ $this->drupalLogin($user1);
+ $this->drupalLogout();
+
+ // Try 3 failed logins for user 1, they will not trigger flood control.
+ for ($i = 0; $i < 3; $i++) {
+ $this->assertFailedLogin($incorrect_user1);
+ }
+
+ // Try one successful attempt for user 2, it should not trigger any
+ // flood control.
+ $this->drupalLogin($user2);
+ $this->drupalLogout();
+
+ // Try one more attempt for user 1, it should be rejected, even if the
+ // correct password has been used.
+ $this->assertFailedLogin($user1, 'user');
+ }
+
+ /**
+ * Test that user password is re-hashed upon login after changing $count_log2.
+ */
+ function testPasswordRehashOnLogin() {
+ // Load password hashing API.
+ require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
+ // Set initial $count_log2 to the default, DRUPAL_HASH_COUNT.
+ variable_set('password_count_log2', DRUPAL_HASH_COUNT);
+ // Create a new user and authenticate.
+ $account = $this->drupalCreateUser(array());
+ $password = $account->pass_raw;
+ $this->drupalLogin($account);
+ $this->drupalLogout();
+ // Load the stored user. The password hash should reflect $count_log2.
+ $account = user_load($account->uid);
+ $this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_HASH_COUNT);
+ // Change $count_log2 and log in again.
+ variable_set('password_count_log2', DRUPAL_HASH_COUNT + 1);
+ $account->pass_raw = $password;
+ $this->drupalLogin($account);
+ // Load the stored user, which should have a different password hash now.
+ $account = user_load($account->uid, TRUE);
+ $this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_HASH_COUNT + 1);
+ }
+
+ /**
+ * Make an unsuccessful login attempt.
+ *
+ * @param $account
+ * A user object with name and pass_raw attributes for the login attempt.
+ * @param $flood_trigger
+ * Whether or not to expect that the flood control mechanism will be
+ * triggered.
+ */
+ function assertFailedLogin($account, $flood_trigger = NULL) {
+ $edit = array(
+ 'name' => $account->name,
+ 'pass' => $account->pass_raw,
+ );
+ $this->drupalPost('user', $edit, t('Log in'));
+ $this->assertNoFieldByXPath("//input[@name='pass' and @value!='']", NULL, 'Password value attribute is blank.');
+ if (isset($flood_trigger)) {
+ if ($flood_trigger == 'user') {
+ $this->assertRaw(format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or request a new password .', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password .', array('@url' => url('user/password'))));
+ }
+ else {
+ // No uid, so the limit is IP-based.
+ $this->assertRaw(t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password .', array('@url' => url('user/password'))));
+ }
+ }
+ else {
+ $this->assertText(t('Sorry, unrecognized username or password. Have you forgotten your password?'));
+ }
+ }
+}
+
+/**
+ * Tests resetting a user password.
+ */
+class UserPasswordResetTestCase extends DrupalWebTestCase {
+ protected $profile = 'standard';
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Reset password',
+ 'description' => 'Ensure that password reset methods work as expected.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Retrieves password reset email and extracts the login link.
+ */
+ public function getResetURL() {
+ // Assume the most recent email.
+ $_emails = $this->drupalGetMails();
+ $email = end($_emails);
+ $urls = array();
+ preg_match('#.+user/reset/.+#', $email['body'], $urls);
+
+ return $urls[0];
+ }
+
+ /**
+ * Tests password reset functionality.
+ */
+ function testUserPasswordReset() {
+ // Create a user.
+ $account = $this->drupalCreateUser();
+ $this->drupalLogin($account);
+ $this->drupalLogout();
+ // Attempt to reset password.
+ $edit = array('name' => $account->name);
+ $this->drupalPost('user/password', $edit, t('E-mail new password'));
+ // Confirm the password reset.
+ $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.');
+
+ // Create an image field to enable an Ajax request on the user profile page.
+ $field = array(
+ 'field_name' => 'field_avatar',
+ 'type' => 'image',
+ 'settings' => array(),
+ 'cardinality' => 1,
+ );
+ field_create_field($field);
+
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => 'user',
+ 'label' => 'Avatar',
+ 'bundle' => 'user',
+ 'required' => FALSE,
+ 'settings' => array(),
+ 'widget' => array(
+ 'type' => 'image_image',
+ 'settings' => array(),
+ ),
+ );
+ field_create_instance($instance);
+
+ $resetURL = $this->getResetURL();
+ $this->drupalGet($resetURL);
+
+ // Check successful login.
+ $this->drupalPost(NULL, NULL, t('Log in'));
+
+ // Make sure the Ajax request from uploading a file does not invalidate the
+ // reset token.
+ $image = current($this->drupalGetTestFiles('image'));
+ $edit = array(
+ 'files[field_avatar_und_0]' => drupal_realpath($image->uri),
+ );
+ $this->drupalPostAJAX(NULL, $edit, 'field_avatar_und_0_upload_button');
+
+ // Change the forgotten password.
+ $password = user_password();
+ $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.');
+ }
+
+ /**
+ * Test user password reset while logged in.
+ */
+ function testUserPasswordResetLoggedIn() {
+ $account = $this->drupalCreateUser();
+ $this->drupalLogin($account);
+ // Make sure the test account has a valid password.
+ user_save($account, array('pass' => user_password()));
+
+ // Generate one time login link.
+ $reset_url = user_pass_reset_url($account);
+ $this->drupalGet($reset_url);
+
+ $this->assertText('Reset password');
+ $this->drupalPost(NULL, NULL, t('Log in'));
+
+ $this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.');
+
+ $pass = user_password();
+ $edit = array(
+ 'pass[pass1]' => $pass,
+ 'pass[pass2]' => $pass,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ $this->assertText('The changes have been saved.');
+ }
+
+ /**
+ * Attempts login using an expired password reset link.
+ */
+ function testUserPasswordResetExpired() {
+ // Set password reset timeout variable to 43200 seconds = 12 hours.
+ $timeout = 43200;
+ variable_set('user_password_reset_timeout', $timeout);
+
+ // Create a user.
+ $account = $this->drupalCreateUser();
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid, TRUE);
+ $this->drupalLogout();
+
+ // To attempt an expired password reset, create a password reset link as if
+ // its request time was 60 seconds older than the allowed limit of timeout.
+ $bogus_timestamp = REQUEST_TIME - variable_get('user_password_reset_timeout', 86400) - 60;
+ $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid));
+ $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.');
+ }
+
+ /**
+ * Prefill the text box on incorrect login via link to password reset page.
+ */
+ function testUserPasswordTextboxFilled() {
+ $this->drupalGet('user/login');
+ $edit = array(
+ 'name' => $this->randomName(),
+ 'pass' => $this->randomName(),
+ );
+ $this->drupalPost('user', $edit, t('Log in'));
+ $this->assertRaw(t('Sorry, unrecognized username or password. Have you forgotten your password? ',
+ array('@password' => url('user/password', array('query' => array('name' => $edit['name']))))));
+ unset($edit['pass']);
+ $this->drupalGet('user/password', array('query' => array('name' => $edit['name'])));
+ $this->assertFieldByName('name', $edit['name'], 'User name found.');
+ }
+
+ /**
+ * Make sure that users cannot forge password reset URLs of other users.
+ */
+ function testResetImpersonation() {
+ // Make sure user 1 has a valid password, so it does not interfere with the
+ // test user accounts that are created below.
+ $account = user_load(1);
+ user_save($account, array('pass' => user_password()));
+
+ // Create two identical user accounts except for the user name. They must
+ // have the same empty password, so we can't use $this->drupalCreateUser().
+ $edit = array();
+ $edit['name'] = $this->randomName();
+ $edit['mail'] = $edit['name'] . '@example.com';
+ $edit['status'] = 1;
+
+ $user1 = user_save(drupal_anonymous_user(), $edit);
+
+ $edit['name'] = $this->randomName();
+ $user2 = user_save(drupal_anonymous_user(), $edit);
+
+ // The password reset URL must not be valid for the second user when only
+ // the user ID is changed in the URL.
+ $reset_url = user_pass_reset_url($user1);
+ $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url);
+ $this->drupalGet($attack_reset_url);
+ $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.');
+ $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
+ $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
+
+ // When legacy code calls user_pass_rehash() without providing the $uid
+ // parameter, neither password reset URL should be valid since it is
+ // impossible for the system to determine which user account the token was
+ // intended for.
+ $timestamp = REQUEST_TIME;
+ // Pass an explicit NULL for the $uid parameter of user_pass_rehash()
+ // rather than not passing it at all, to avoid triggering PHP warnings in
+ // the test.
+ $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL);
+ $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE));
+ $this->drupalGet($reset_url);
+ $this->assertNoText($user1->name, 'The invalid password reset page does not show the user name.');
+ $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
+ $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
+ $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url);
+ $this->drupalGet($attack_reset_url);
+ $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.');
+ $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
+ $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
+
+ // To verify that user_pass_rehash() never returns a valid result in the
+ // above situation (even if legacy code also called it to attempt to
+ // validate the token, rather than just to generate the URL), check that a
+ // second call with the same parameters produces a different result.
+ $new_reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL);
+ $this->assertNotEqual($reset_url_token, $new_reset_url_token);
+
+ // However, when the duplicate account is removed, the password reset URL
+ // should be valid.
+ user_delete($user2->uid);
+ $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL);
+ $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE));
+ $this->drupalGet($reset_url);
+ $this->assertText($user1->name, 'The valid password reset page shows the user name.');
+ $this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.');
+ $this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
+ }
+
+}
+
+/**
+ * Test cancelling a user.
+ */
+class UserCancelTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Cancel account',
+ 'description' => 'Ensure that account cancellation methods work as expected.',
+ 'group' => 'User',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('comment');
+ }
+
+ /**
+ * Attempt to cancel account without permission.
+ */
+ function testUserCancelWithoutPermission() {
+ variable_set('user_cancel_method', 'user_cancel_reassign');
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array());
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid, TRUE);
+
+ // Create a node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->assertNoRaw(t('Cancel account'), 'No cancel account button displayed.');
+
+ // Attempt bogus account cancellation request confirmation.
+ $timestamp = $account->login;
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+ $this->assertResponse(403, 'Bogus cancelling request rejected.');
+ $account = user_load($account->uid);
+ $this->assertTrue($account->status == 1, 'User account was not canceled.');
+
+ // Confirm user's content has not been altered.
+ $test_node = node_load($node->nid, NULL, TRUE);
+ $this->assertTrue(($test_node->uid == $account->uid && $test_node->status == 1), 'Node of the user has not been altered.');
+ }
+
+ /**
+ * Tests that user account for uid 1 cannot be cancelled.
+ *
+ * This should never be possible, or the site owner would become unable to
+ * administer the site.
+ */
+ function testUserCancelUid1() {
+ // Update uid 1's name and password to we know it.
+ $password = user_password();
+ require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
+ $account = array(
+ 'name' => 'user1',
+ 'pass' => user_hash_password(trim($password)),
+ );
+ // We cannot use user_save() here or the password would be hashed again.
+ db_update('users')
+ ->fields($account)
+ ->condition('uid', 1)
+ ->execute();
+
+ // Reload and log in uid 1.
+ $user1 = user_load(1, TRUE);
+ $user1->pass_raw = $password;
+
+ // Try to cancel uid 1's account with a different user.
+ $this->admin_user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($this->admin_user);
+ $edit = array(
+ 'operation' => 'cancel',
+ 'accounts[1]' => TRUE,
+ );
+ $this->drupalPost('admin/people', $edit, t('Update'));
+
+ // Verify that uid 1's account was not cancelled.
+ $user1 = user_load(1, TRUE);
+ $this->assertEqual($user1->status, 1, 'User #1 still exists and is not blocked.');
+ }
+
+ /**
+ * Attempt invalid account cancellations.
+ */
+ function testUserCancelInvalid() {
+ variable_set('user_cancel_method', 'user_cancel_reassign');
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid, TRUE);
+
+ // Create a node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Attempt to cancel account.
+ $this->drupalPost('user/' . $account->uid . '/edit', NULL, t('Cancel account'));
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+
+ // Attempt bogus account cancellation request confirmation.
+ $bogus_timestamp = $timestamp + 60;
+ $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid));
+ $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Bogus cancelling request rejected.');
+ $account = user_load($account->uid);
+ $this->assertTrue($account->status == 1, 'User account was not canceled.');
+
+ // Attempt expired account cancellation request confirmation.
+ $bogus_timestamp = $timestamp - 86400 - 60;
+ $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid));
+ $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Expired cancel account request rejected.');
+ $accounts = user_load_multiple(array($account->uid), array('status' => 1));
+ $this->assertTrue(reset($accounts), 'User account was not canceled.');
+
+ // Confirm user's content has not been altered.
+ $test_node = node_load($node->nid, NULL, TRUE);
+ $this->assertTrue(($test_node->uid == $account->uid && $test_node->status == 1), 'Node of the user has not been altered.');
+ }
+
+ /**
+ * Disable account and keep all content.
+ */
+ function testUserBlock() {
+ variable_set('user_cancel_method', 'user_cancel_block');
+
+ // Create a user.
+ $web_user = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($web_user);
+
+ // Load real user object.
+ $account = user_load($web_user->uid, TRUE);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.');
+ $this->assertText(t('Your account will be blocked and you will no longer be able to log in. All of your content will remain attributed to your user name.'), 'Informs that all content will be remain as is.');
+ $this->assertNoText(t('Select the method to cancel the account above.'), 'Does not allow user to select account cancellation method.');
+
+ // Confirm account cancellation.
+ $timestamp = time();
+
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+ $account = user_load($account->uid, TRUE);
+ $this->assertTrue($account->status == 0, 'User has been blocked.');
+
+ // Confirm that the confirmation message made it through to the end user.
+ $this->assertRaw(t('%name has been disabled.', array('%name' => $account->name)), "Confirmation message displayed to user.");
+ }
+
+ /**
+ * Disable account and unpublish all content.
+ */
+ function testUserBlockUnpublish() {
+ variable_set('user_cancel_method', 'user_cancel_block_unpublish');
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid, TRUE);
+
+ // Create a node with two revisions.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+ $settings = get_object_vars($node);
+ $settings['revision'] = 1;
+ $node = $this->drupalCreateNode($settings);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.');
+ $this->assertText(t('Your account will be blocked and you will no longer be able to log in. All of your content will be hidden from everyone but administrators.'), 'Informs that all content will be unpublished.');
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+ $account = user_load($account->uid, TRUE);
+ $this->assertTrue($account->status == 0, 'User has been blocked.');
+
+ // Confirm user's content has been unpublished.
+ $test_node = node_load($node->nid, NULL, TRUE);
+ $this->assertTrue($test_node->status == 0, 'Node of the user has been unpublished.');
+ $test_node = node_load($node->nid, $node->vid, TRUE);
+ $this->assertTrue($test_node->status == 0, 'Node revision of the user has been unpublished.');
+
+ // Confirm that the confirmation message made it through to the end user.
+ $this->assertRaw(t('%name has been disabled.', array('%name' => $account->name)), "Confirmation message displayed to user.");
+ }
+
+ /**
+ * Delete account and anonymize all content.
+ */
+ function testUserAnonymize() {
+ variable_set('user_cancel_method', 'user_cancel_reassign');
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid, TRUE);
+
+ // Create a simple node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Create a node with two revisions, the initial one belonging to the
+ // cancelling user.
+ $revision_node = $this->drupalCreateNode(array('uid' => $account->uid));
+ $revision = $revision_node->vid;
+ $settings = get_object_vars($revision_node);
+ $settings['revision'] = 1;
+ $settings['uid'] = 1; // Set new/current revision to someone else.
+ $revision_node = $this->drupalCreateNode($settings);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.');
+ $this->assertRaw(t('Your account will be removed and all account information deleted. All of your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))), 'Informs that all content will be attributed to anonymous account.');
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+ $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.');
+
+ // Confirm that user's content has been attributed to anonymous user.
+ $test_node = node_load($node->nid, NULL, TRUE);
+ $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), 'Node of the user has been attributed to anonymous user.');
+ $test_node = node_load($revision_node->nid, $revision, TRUE);
+ $this->assertTrue(($test_node->revision_uid == 0 && $test_node->status == 1), 'Node revision of the user has been attributed to anonymous user.');
+ $test_node = node_load($revision_node->nid, NULL, TRUE);
+ $this->assertTrue(($test_node->uid != 0 && $test_node->status == 1), "Current revision of the user's node was not attributed to anonymous user.");
+
+ // Confirm that the confirmation message made it through to the end user.
+ $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), "Confirmation message displayed to user.");
+ }
+
+ /**
+ * Delete account and remove all content.
+ */
+ function testUserDelete() {
+ variable_set('user_cancel_method', 'user_cancel_delete');
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account', 'post comments', 'skip comment approval'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid, TRUE);
+
+ // Create a simple node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Create comment.
+ $langcode = LANGUAGE_NONE;
+ $edit = array();
+ $edit['subject'] = $this->randomName(8);
+ $edit['comment_body[' . $langcode . '][0][value]'] = $this->randomName(16);
+
+ $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
+ $this->drupalPost(NULL, array(), t('Save'));
+ $this->assertText(t('Your comment has been posted.'));
+ $comments = comment_load_multiple(array(), array('subject' => $edit['subject']));
+ $comment = reset($comments);
+ $this->assertTrue($comment->cid, 'Comment found.');
+
+ // Create a node with two revisions, the initial one belonging to the
+ // cancelling user.
+ $revision_node = $this->drupalCreateNode(array('uid' => $account->uid));
+ $revision = $revision_node->vid;
+ $settings = get_object_vars($revision_node);
+ $settings['revision'] = 1;
+ $settings['uid'] = 1; // Set new/current revision to someone else.
+ $revision_node = $this->drupalCreateNode($settings);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.');
+ $this->assertText(t('Your account will be removed and all account information deleted. All of your content will also be deleted.'), 'Informs that all content will be deleted.');
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+ $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.');
+
+ // Confirm that user's content has been deleted.
+ $this->assertFalse(node_load($node->nid, NULL, TRUE), 'Node of the user has been deleted.');
+ $this->assertFalse(node_load($node->nid, $revision, TRUE), 'Node revision of the user has been deleted.');
+ $this->assertTrue(node_load($revision_node->nid, NULL, TRUE), "Current revision of the user's node was not deleted.");
+ $this->assertFalse(comment_load($comment->cid), 'Comment of the user has been deleted.');
+
+ // Confirm that the confirmation message made it through to the end user.
+ $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), "Confirmation message displayed to user.");
+ }
+
+ /**
+ * Create an administrative user and delete another user.
+ */
+ function testUserCancelByAdmin() {
+ variable_set('user_cancel_method', 'user_cancel_reassign');
+
+ // Create a regular user.
+ $account = $this->drupalCreateUser(array());
+
+ // Create administrative user.
+ $admin_user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($admin_user);
+
+ // Delete regular user.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertRaw(t('Are you sure you want to cancel the account %name?', array('%name' => $account->name)), 'Confirmation form to cancel account displayed.');
+ $this->assertText(t('Select the method to cancel the account above.'), 'Allows to select account cancellation method.');
+
+ // Confirm deletion.
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), 'User deleted.');
+ $this->assertFalse(user_load($account->uid), 'User is not found in the database.');
+ }
+
+ /**
+ * Create an administrative user and mass-delete other users.
+ */
+ function testMassUserCancelByAdmin() {
+ variable_set('user_cancel_method', 'user_cancel_reassign');
+ // Enable account cancellation notification.
+ variable_set('user_mail_status_canceled_notify', TRUE);
+
+ // Create administrative user.
+ $admin_user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($admin_user);
+
+ // Create some users.
+ $users = array();
+ for ($i = 0; $i < 3; $i++) {
+ $account = $this->drupalCreateUser(array());
+ $users[$account->uid] = $account;
+ }
+
+ // Cancel user accounts, including own one.
+ $edit = array();
+ $edit['operation'] = 'cancel';
+ foreach ($users as $uid => $account) {
+ $edit['accounts[' . $uid . ']'] = TRUE;
+ }
+ $edit['accounts[' . $admin_user->uid . ']'] = TRUE;
+ // Also try to cancel uid 1.
+ $edit['accounts[1]'] = TRUE;
+ $this->drupalPost('admin/people', $edit, t('Update'));
+ $this->assertText(t('Are you sure you want to cancel these user accounts?'), 'Confirmation form to cancel accounts displayed.');
+ $this->assertText(t('When cancelling these accounts'), 'Allows to select account cancellation method.');
+ $this->assertText(t('Require e-mail confirmation to cancel account.'), 'Allows to send confirmation mail.');
+ $this->assertText(t('Notify user when account is canceled.'), 'Allows to send notification mail.');
+
+ // Confirm deletion.
+ $this->drupalPost(NULL, NULL, t('Cancel accounts'));
+ $status = TRUE;
+ foreach ($users as $account) {
+ $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $account->name))) !== FALSE);
+ $status = $status && !user_load($account->uid, TRUE);
+ }
+ $this->assertTrue($status, 'Users deleted and not found in the database.');
+
+ // Ensure that admin account was not cancelled.
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+ $admin_user = user_load($admin_user->uid);
+ $this->assertTrue($admin_user->status == 1, 'Administrative user is found in the database and enabled.');
+
+ // Verify that uid 1's account was not cancelled.
+ $user1 = user_load(1, TRUE);
+ $this->assertEqual($user1->status, 1, 'User #1 still exists and is not blocked.');
+ }
+}
+
+class UserPictureTestCase extends DrupalWebTestCase {
+ protected $user;
+ protected $_directory_test;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Upload user picture',
+ 'description' => 'Assure that dimension check, extension check and image scaling work as designed.',
+ 'group' => 'User'
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ // Enable user pictures.
+ variable_set('user_pictures', 1);
+
+ $this->user = $this->drupalCreateUser();
+
+ // Test if directories specified in settings exist in filesystem.
+ $file_dir = 'public://';
+ $file_check = file_prepare_directory($file_dir, FILE_CREATE_DIRECTORY);
+ // TODO: Test public and private methods?
+
+ $picture_dir = variable_get('user_picture_path', 'pictures');
+ $picture_path = $file_dir . $picture_dir;
+
+ $pic_check = file_prepare_directory($picture_path, FILE_CREATE_DIRECTORY);
+ $this->_directory_test = is_writable($picture_path);
+ $this->assertTrue($this->_directory_test, "The directory $picture_path doesn't exist or is not writable. Further tests won't be made.");
+ }
+
+ function testNoPicture() {
+ $this->drupalLogin($this->user);
+
+ // Try to upload a file that is not an image for the user picture.
+ $not_an_image = current($this->drupalGetTestFiles('html'));
+ $this->saveUserPicture($not_an_image);
+ $this->assertRaw(t('Only JPEG, PNG and GIF images are allowed.'), 'Non-image files are not accepted.');
+ }
+
+ /**
+ * Do the test:
+ * GD Toolkit is installed
+ * Picture has invalid dimension
+ *
+ * results: The image should be uploaded because ImageGDToolkit resizes the picture
+ */
+ function testWithGDinvalidDimension() {
+ if ($this->_directory_test && image_get_toolkit()) {
+ $this->drupalLogin($this->user);
+
+ $image = current($this->drupalGetTestFiles('image'));
+ $info = image_get_info($image->uri);
+
+ // Set new variables: invalid dimensions, valid filesize (0 = no limit).
+ $test_dim = ($info['width'] - 10) . 'x' . ($info['height'] - 10);
+ variable_set('user_picture_dimensions', $test_dim);
+ variable_set('user_picture_file_size', 0);
+
+ $pic_path = $this->saveUserPicture($image);
+ // Check that the image was resized and is being displayed on the
+ // user's profile page.
+ $text = t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $test_dim));
+ $this->assertRaw($text, 'Image was resized.');
+ $alt = t("@user's picture", array('@user' => format_username($this->user)));
+ $style = variable_get('user_picture_style', '');
+ $this->assertRaw(check_plain(image_style_url($style, $pic_path)), "Image is displayed in user's edit page");
+
+ // Check if file is located in proper directory.
+ $this->assertTrue(is_file($pic_path), "File is located in proper directory");
+ }
+ }
+
+ /**
+ * Do the test:
+ * GD Toolkit is installed
+ * Picture has invalid size
+ *
+ * results: The image should be uploaded because ImageGDToolkit resizes the picture
+ */
+ function testWithGDinvalidSize() {
+ if ($this->_directory_test && image_get_toolkit()) {
+ $this->drupalLogin($this->user);
+
+ // Images are sorted first by size then by name. We need an image
+ // bigger than 1 KB so we'll grab the last one.
+ $files = $this->drupalGetTestFiles('image');
+ $image = end($files);
+ $info = image_get_info($image->uri);
+
+ // Set new variables: valid dimensions, invalid filesize.
+ $test_dim = ($info['width'] + 10) . 'x' . ($info['height'] + 10);
+ $test_size = 1;
+ variable_set('user_picture_dimensions', $test_dim);
+ variable_set('user_picture_file_size', $test_size);
+
+ $pic_path = $this->saveUserPicture($image);
+
+ // Test that the upload failed and that the correct reason was cited.
+ $text = t('The specified file %filename could not be uploaded.', array('%filename' => $image->filename));
+ $this->assertRaw($text, 'Upload failed.');
+ $text = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size(filesize($image->uri)), '%maxsize' => format_size($test_size * 1024)));
+ $this->assertRaw($text, 'File size cited as reason for failure.');
+
+ // Check if file is not uploaded.
+ $this->assertFalse(is_file($pic_path), 'File was not uploaded.');
+ }
+ }
+
+ /**
+ * Do the test:
+ * GD Toolkit is not installed
+ * Picture has invalid size
+ *
+ * results: The image shouldn't be uploaded
+ */
+ function testWithoutGDinvalidDimension() {
+ if ($this->_directory_test && !image_get_toolkit()) {
+ $this->drupalLogin($this->user);
+
+ $image = current($this->drupalGetTestFiles('image'));
+ $info = image_get_info($image->uri);
+
+ // Set new variables: invalid dimensions, valid filesize (0 = no limit).
+ $test_dim = ($info['width'] - 10) . 'x' . ($info['height'] - 10);
+ variable_set('user_picture_dimensions', $test_dim);
+ variable_set('user_picture_file_size', 0);
+
+ $pic_path = $this->saveUserPicture($image);
+
+ // Test that the upload failed and that the correct reason was cited.
+ $text = t('The specified file %filename could not be uploaded.', array('%filename' => $image->filename));
+ $this->assertRaw($text, 'Upload failed.');
+ $text = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $test_dim));
+ $this->assertRaw($text, 'Checking response on invalid image (dimensions).');
+
+ // Check if file is not uploaded.
+ $this->assertFalse(is_file($pic_path), 'File was not uploaded.');
+ }
+ }
+
+ /**
+ * Do the test:
+ * GD Toolkit is not installed
+ * Picture has invalid size
+ *
+ * results: The image shouldn't be uploaded
+ */
+ function testWithoutGDinvalidSize() {
+ if ($this->_directory_test && !image_get_toolkit()) {
+ $this->drupalLogin($this->user);
+
+ $image = current($this->drupalGetTestFiles('image'));
+ $info = image_get_info($image->uri);
+
+ // Set new variables: valid dimensions, invalid filesize.
+ $test_dim = ($info['width'] + 10) . 'x' . ($info['height'] + 10);
+ $test_size = 1;
+ variable_set('user_picture_dimensions', $test_dim);
+ variable_set('user_picture_file_size', $test_size);
+
+ $pic_path = $this->saveUserPicture($image);
+
+ // Test that the upload failed and that the correct reason was cited.
+ $text = t('The specified file %filename could not be uploaded.', array('%filename' => $image->filename));
+ $this->assertRaw($text, 'Upload failed.');
+ $text = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size(filesize($image->uri)), '%maxsize' => format_size($test_size * 1024)));
+ $this->assertRaw($text, 'File size cited as reason for failure.');
+
+ // Check if file is not uploaded.
+ $this->assertFalse(is_file($pic_path), 'File was not uploaded.');
+ }
+ }
+
+ /**
+ * Do the test:
+ * Picture is valid (proper size and dimension)
+ *
+ * results: The image should be uploaded
+ */
+ function testPictureIsValid() {
+ if ($this->_directory_test) {
+ $this->drupalLogin($this->user);
+
+ $image = current($this->drupalGetTestFiles('image'));
+ $info = image_get_info($image->uri);
+
+ // Set new variables: valid dimensions, valid filesize (0 = no limit).
+ $test_dim = ($info['width'] + 10) . 'x' . ($info['height'] + 10);
+ variable_set('user_picture_dimensions', $test_dim);
+ variable_set('user_picture_file_size', 0);
+
+ $pic_path = $this->saveUserPicture($image);
+
+ // Check if image is displayed in user's profile page.
+ $this->drupalGet('user');
+ $this->assertRaw(file_uri_target($pic_path), "Image is displayed in user's profile page");
+
+ // Check if file is located in proper directory.
+ $this->assertTrue(is_file($pic_path), 'File is located in proper directory');
+
+ // Set new picture dimensions.
+ $test_dim = ($info['width'] + 5) . 'x' . ($info['height'] + 5);
+ variable_set('user_picture_dimensions', $test_dim);
+
+ $pic_path2 = $this->saveUserPicture($image);
+ $this->assertNotEqual($pic_path, $pic_path2, 'Filename of second picture is different.');
+
+ // Check if user picture has a valid file ID after saving the user.
+ $account = user_load($this->user->uid, TRUE);
+ $this->assertTrue(is_object($account->picture), 'User picture object is valid after user load.');
+ $this->assertNotNull($account->picture->fid, 'User picture object has a FID after user load.');
+ $this->assertTrue(is_file($account->picture->uri), 'File is located in proper directory after user load.');
+ user_save($account);
+ // Verify that the user save does not destroy the user picture object.
+ $this->assertTrue(is_object($account->picture), 'User picture object is valid after user save.');
+ $this->assertNotNull($account->picture->fid, 'User picture object has a FID after user save.');
+ $this->assertTrue(is_file($account->picture->uri), 'File is located in proper directory after user save.');
+ }
+ }
+
+ /**
+ * Test HTTP schema working with user pictures.
+ */
+ function testExternalPicture() {
+ $this->drupalLogin($this->user);
+ // Set the default picture to an URI with a HTTP schema.
+ $images = $this->drupalGetTestFiles('image');
+ $image = $images[0];
+ $pic_path = file_create_url($image->uri);
+ variable_set('user_picture_default', $pic_path);
+
+ // Check if image is displayed in user's profile page.
+ $this->drupalGet('user');
+
+ // Get the user picture image via xpath.
+ $elements = $this->xpath('//div[@class="user-picture"]/img');
+ $this->assertEqual(count($elements), 1, "There is exactly one user picture on the user's profile page");
+ $this->assertEqual($pic_path, (string) $elements[0]['src'], "User picture source is correct.");
+ }
+
+ /**
+ * Tests deletion of user pictures.
+ */
+ function testDeletePicture() {
+ $this->drupalLogin($this->user);
+
+ $image = current($this->drupalGetTestFiles('image'));
+ $info = image_get_info($image->uri);
+
+ // Set new variables: valid dimensions, valid filesize (0 = no limit).
+ $test_dim = ($info['width'] + 10) . 'x' . ($info['height'] + 10);
+ variable_set('user_picture_dimensions', $test_dim);
+ variable_set('user_picture_file_size', 0);
+
+ // Save a new picture.
+ $edit = array('files[picture_upload]' => drupal_realpath($image->uri));
+ $this->drupalPost('user/' . $this->user->uid . '/edit', $edit, t('Save'));
+
+ // Load actual user data from database.
+ $account = user_load($this->user->uid, TRUE);
+ $pic_path = isset($account->picture) ? $account->picture->uri : NULL;
+
+ // Check if image is displayed in user's profile page.
+ $this->drupalGet('user');
+ $this->assertRaw(file_uri_target($pic_path), "Image is displayed in user's profile page");
+
+ // Check if file is located in proper directory.
+ $this->assertTrue(is_file($pic_path), 'File is located in proper directory');
+
+ $edit = array('picture_delete' => 1);
+ $this->drupalPost('user/' . $this->user->uid . '/edit', $edit, t('Save'));
+
+ // Load actual user data from database.
+ $account1 = user_load($this->user->uid, TRUE);
+ $this->assertNull($account1->picture, 'User object has no picture');
+
+ $file = file_load($account->picture->fid);
+ $this->assertFalse($file, 'File is removed from database');
+
+ // Clear out PHP's file stat cache so we see the current value.
+ clearstatcache();
+ $this->assertFalse(is_file($pic_path), 'File is removed from file system');
+ }
+
+ function saveUserPicture($image) {
+ $edit = array('files[picture_upload]' => drupal_realpath($image->uri));
+ $this->drupalPost('user/' . $this->user->uid . '/edit', $edit, t('Save'));
+
+ // Load actual user data from database.
+ $account = user_load($this->user->uid, TRUE);
+ return isset($account->picture) ? $account->picture->uri : NULL;
+ }
+
+ /**
+ * Tests the admin form validates user picture settings.
+ */
+ function testUserPictureAdminFormValidation() {
+ $this->drupalLogin($this->drupalCreateUser(array('administer users')));
+
+ // The default values are valid.
+ $this->drupalPost('admin/config/people/accounts', array(), t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved.'), 'The default values are valid.');
+
+ // The form does not save with an invalid file size.
+ $edit = array(
+ 'user_picture_file_size' => $this->randomName(),
+ );
+ $this->drupalPost('admin/config/people/accounts', $edit, t('Save configuration'));
+ $this->assertNoText(t('The configuration options have been saved.'), 'The form does not save with an invalid file size.');
+ }
+}
+
+
+class UserPermissionsTestCase extends DrupalWebTestCase {
+ protected $admin_user;
+ protected $rid;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Role permissions',
+ 'description' => 'Verify that role permissions can be added and removed via the permissions page.',
+ 'group' => 'User'
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+
+ $this->admin_user = $this->drupalCreateUser(array('administer permissions', 'access user profiles', 'administer site configuration', 'administer modules', 'administer users'));
+
+ // Find the new role ID - it must be the maximum.
+ $all_rids = array_keys($this->admin_user->roles);
+ sort($all_rids);
+ $this->rid = array_pop($all_rids);
+ }
+
+ /**
+ * Change user permissions and check user_access().
+ */
+ function testUserPermissionChanges() {
+ $this->drupalLogin($this->admin_user);
+ $rid = $this->rid;
+ $account = $this->admin_user;
+
+ // Add a permission.
+ $this->assertFalse(user_access('administer nodes', $account), 'User does not have "administer nodes" permission.');
+ $edit = array();
+ $edit[$rid . '[administer nodes]'] = TRUE;
+ $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
+ $this->assertText(t('The changes have been saved.'), 'Successful save message displayed.');
+ drupal_static_reset('user_access');
+ drupal_static_reset('user_role_permissions');
+ $this->assertTrue(user_access('administer nodes', $account), 'User now has "administer nodes" permission.');
+
+ // Remove a permission.
+ $this->assertTrue(user_access('access user profiles', $account), 'User has "access user profiles" permission.');
+ $edit = array();
+ $edit[$rid . '[access user profiles]'] = FALSE;
+ $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
+ $this->assertText(t('The changes have been saved.'), 'Successful save message displayed.');
+ drupal_static_reset('user_access');
+ drupal_static_reset('user_role_permissions');
+ $this->assertFalse(user_access('access user profiles', $account), 'User no longer has "access user profiles" permission.');
+ }
+
+ /**
+ * Test assigning of permissions for the administrator role.
+ */
+ function testAdministratorRole() {
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet('admin/config/people/accounts');
+
+ // Set the user's role to be the administrator role.
+ $edit = array();
+ $edit['user_admin_role'] = $this->rid;
+ $this->drupalPost('admin/config/people/accounts', $edit, t('Save configuration'));
+
+ // Enable aggregator module and ensure the 'administer news feeds'
+ // permission is assigned by default.
+ $edit = array();
+ $edit['modules[Core][aggregator][enable]'] = TRUE;
+ $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+ $this->assertTrue(user_access('administer news feeds', $this->admin_user), 'The permission was automatically assigned to the administrator role');
+ }
+
+ /**
+ * Verify proper permission changes by user_role_change_permissions().
+ */
+ function testUserRoleChangePermissions() {
+ $rid = $this->rid;
+ $account = $this->admin_user;
+
+ // Verify current permissions.
+ $this->assertFalse(user_access('administer nodes', $account), 'User does not have "administer nodes" permission.');
+ $this->assertTrue(user_access('access user profiles', $account), 'User has "access user profiles" permission.');
+ $this->assertTrue(user_access('administer site configuration', $account), 'User has "administer site configuration" permission.');
+
+ // Change permissions.
+ $permissions = array(
+ 'administer nodes' => 1,
+ 'access user profiles' => 0,
+ );
+ user_role_change_permissions($rid, $permissions);
+
+ // Verify proper permission changes.
+ $this->assertTrue(user_access('administer nodes', $account), 'User now has "administer nodes" permission.');
+ $this->assertFalse(user_access('access user profiles', $account), 'User no longer has "access user profiles" permission.');
+ $this->assertTrue(user_access('administer site configuration', $account), 'User still has "administer site configuration" permission.');
+ }
+}
+
+class UserAdminTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User administration',
+ 'description' => 'Test user administration page functionality.',
+ 'group' => 'User'
+ );
+ }
+
+ /**
+ * Registers a user and deletes it.
+ */
+ function testUserAdmin() {
+
+ $user_a = $this->drupalCreateUser(array());
+ $user_b = $this->drupalCreateUser(array('administer taxonomy'));
+ $user_c = $this->drupalCreateUser(array('administer taxonomy'));
+
+ // Create admin user to delete registered user.
+ $admin_user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($admin_user);
+ $this->drupalGet('admin/people');
+ $this->assertText($user_a->name, 'Found user A on admin users page');
+ $this->assertText($user_b->name, 'Found user B on admin users page');
+ $this->assertText($user_c->name, 'Found user C on admin users page');
+ $this->assertText($admin_user->name, 'Found Admin user on admin users page');
+
+ // Test for existence of edit link in table.
+ $link = l(t('edit'), "user/$user_a->uid/edit", array('query' => array('destination' => 'admin/people')));
+ $this->assertRaw($link, 'Found user A edit link on admin users page');
+
+ // Filter the users by permission 'administer taxonomy'.
+ $edit = array();
+ $edit['permission'] = 'administer taxonomy';
+ $this->drupalPost('admin/people', $edit, t('Filter'));
+
+ // Check if the correct users show up.
+ $this->assertNoText($user_a->name, 'User A not on filtered by perm admin users page');
+ $this->assertText($user_b->name, 'Found user B on filtered by perm admin users page');
+ $this->assertText($user_c->name, 'Found user C on filtered by perm admin users page');
+
+ // Filter the users by role. Grab the system-generated role name for User C.
+ $edit['role'] = max(array_flip($user_c->roles));
+ $this->drupalPost('admin/people', $edit, t('Refine'));
+
+ // Check if the correct users show up when filtered by role.
+ $this->assertNoText($user_a->name, 'User A not on filtered by role on admin users page');
+ $this->assertNoText($user_b->name, 'User B not on filtered by role on admin users page');
+ $this->assertText($user_c->name, 'User C on filtered by role on admin users page');
+
+ // Test blocking of a user.
+ $account = user_load($user_c->uid);
+ $this->assertEqual($account->status, 1, 'User C not blocked');
+ $edit = array();
+ $edit['operation'] = 'block';
+ $edit['accounts[' . $account->uid . ']'] = TRUE;
+ $this->drupalPost('admin/people', $edit, t('Update'));
+ $account = user_load($user_c->uid, TRUE);
+ $this->assertEqual($account->status, 0, 'User C blocked');
+
+ // Test unblocking of a user from /admin/people page and sending of activation mail
+ $editunblock = array();
+ $editunblock['operation'] = 'unblock';
+ $editunblock['accounts[' . $account->uid . ']'] = TRUE;
+ $this->drupalPost('admin/people', $editunblock, t('Update'));
+ $account = user_load($user_c->uid, TRUE);
+ $this->assertEqual($account->status, 1, 'User C unblocked');
+ $this->assertMail("to", $account->mail, "Activation mail sent to user C");
+
+ // Test blocking and unblocking another user from /user/[uid]/edit form and sending of activation mail
+ $user_d = $this->drupalCreateUser(array());
+ $account1 = user_load($user_d->uid, TRUE);
+ $this->drupalPost('user/' . $account1->uid . '/edit', array('status' => 0), t('Save'));
+ $account1 = user_load($user_d->uid, TRUE);
+ $this->assertEqual($account1->status, 0, 'User D blocked');
+ $this->drupalPost('user/' . $account1->uid . '/edit', array('status' => TRUE), t('Save'));
+ $account1 = user_load($user_d->uid, TRUE);
+ $this->assertEqual($account1->status, 1, 'User D unblocked');
+ $this->assertMail("to", $account1->mail, "Activation mail sent to user D");
+ }
+}
+
+/**
+ * Tests for user-configurable time zones.
+ */
+class UserTimeZoneFunctionalTest extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User time zones',
+ 'description' => 'Set a user time zone and verify that dates are displayed in local time.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Tests the display of dates and time when user-configurable time zones are set.
+ */
+ function testUserTimeZone() {
+ // Setup date/time settings for Los Angeles time.
+ variable_set('date_default_timezone', 'America/Los_Angeles');
+ variable_set('configurable_timezones', 1);
+
+ // Override the 'medium' date format, which is the default for node
+ // creation time. Since we are testing time zones with Daylight Saving
+ // Time, and need to future proof against changes to the zoneinfo database,
+ // we choose the 'I' format placeholder instead of a human-readable zone
+ // name. With 'I', a 1 means the date is in DST, and 0 if not.
+ variable_set('date_format_medium', 'Y-m-d H:i I');
+
+ // Create a user account and login.
+ $web_user = $this->drupalCreateUser();
+ $this->drupalLogin($web_user);
+
+ // Create some nodes with different authored-on dates.
+ // Two dates in PST (winter time):
+ $date1 = '2007-03-09 21:00:00 -0800';
+ $date2 = '2007-03-11 01:00:00 -0800';
+ // One date in PDT (summer time):
+ $date3 = '2007-03-20 21:00:00 -0700';
+ $node1 = $this->drupalCreateNode(array('created' => strtotime($date1), 'type' => 'article'));
+ $node2 = $this->drupalCreateNode(array('created' => strtotime($date2), 'type' => 'article'));
+ $node3 = $this->drupalCreateNode(array('created' => strtotime($date3), 'type' => 'article'));
+
+ // Confirm date format and time zone.
+ $this->drupalGet("node/$node1->nid");
+ $this->assertText('2007-03-09 21:00 0', 'Date should be PST.');
+ $this->drupalGet("node/$node2->nid");
+ $this->assertText('2007-03-11 01:00 0', 'Date should be PST.');
+ $this->drupalGet("node/$node3->nid");
+ $this->assertText('2007-03-20 21:00 1', 'Date should be PDT.');
+
+ // Change user time zone to Santiago time.
+ $edit = array();
+ $edit['mail'] = $web_user->mail;
+ $edit['timezone'] = 'America/Santiago';
+ $this->drupalPost("user/$web_user->uid/edit", $edit, t('Save'));
+ $this->assertText(t('The changes have been saved.'), 'Time zone changed to Santiago time.');
+
+ // Confirm date format and time zone.
+ $this->drupalGet("node/$node1->nid");
+ $this->assertText('2007-03-10 02:00 1', 'Date should be Chile summer time; five hours ahead of PST.');
+ $this->drupalGet("node/$node2->nid");
+ $this->assertText('2007-03-11 05:00 0', 'Date should be Chile time; four hours ahead of PST');
+ $this->drupalGet("node/$node3->nid");
+ $this->assertText('2007-03-21 00:00 0', 'Date should be Chile time; three hours ahead of PDT.');
+ }
+}
+
+/**
+ * Test user autocompletion.
+ */
+class UserAutocompleteTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User autocompletion',
+ 'description' => 'Test user autocompletion functionality.',
+ 'group' => 'User'
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+
+ // Set up two users with different permissions to test access.
+ $this->unprivileged_user = $this->drupalCreateUser();
+ $this->privileged_user = $this->drupalCreateUser(array('access user profiles'));
+ }
+
+ /**
+ * Tests access to user autocompletion and verify the correct results.
+ */
+ function testUserAutocomplete() {
+ // Check access from unprivileged user, should be denied.
+ $this->drupalLogin($this->unprivileged_user);
+ $this->drupalGet('user/autocomplete/' . $this->unprivileged_user->name[0]);
+ $this->assertResponse(403, 'Autocompletion access denied to user without permission.');
+
+ // Check access from privileged user.
+ $this->drupalLogout();
+ $this->drupalLogin($this->privileged_user);
+ $this->drupalGet('user/autocomplete/' . $this->unprivileged_user->name[0]);
+ $this->assertResponse(200, 'Autocompletion access allowed.');
+
+ // Using first letter of the user's name, make sure the user's full name is in the results.
+ $this->assertRaw($this->unprivileged_user->name, 'User name found in autocompletion results.');
+ }
+}
+
+
+/**
+ * Tests user links in the secondary menu.
+ */
+class UserAccountLinksUnitTests extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User account links',
+ 'description' => 'Test user-account links.',
+ 'group' => 'User'
+ );
+ }
+
+ function setUp() {
+ parent::setUp('menu');
+ }
+
+ /**
+ * Tests the secondary menu.
+ */
+ function testSecondaryMenu() {
+ // Create a regular user.
+ $user = $this->drupalCreateUser(array());
+
+ // Log in and get the homepage.
+ $this->drupalLogin($user);
+ $this->drupalGet('');
+
+ // For a logged-in user, expect the secondary menu to have links for "My
+ // account" and "Log out".
+ $link = $this->xpath('//ul[@id=:menu_id]/li/a[contains(@href, :href) and text()=:text]', array(
+ ':menu_id' => 'secondary-menu-links',
+ ':href' => 'user',
+ ':text' => 'My account',
+ ));
+ $this->assertEqual(count($link), 1, 'My account link is in secondary menu.');
+
+ $link = $this->xpath('//ul[@id=:menu_id]/li/a[contains(@href, :href) and text()=:text]', array(
+ ':menu_id' => 'secondary-menu-links',
+ ':href' => 'user/logout',
+ ':text' => 'Log out',
+ ));
+ $this->assertEqual(count($link), 1, 'Log out link is in secondary menu.');
+
+ // Log out and get the homepage.
+ $this->drupalLogout();
+ $this->drupalGet('');
+
+ // For a logged-out user, expect no secondary links.
+ $element = $this->xpath('//ul[@id=:menu_id]', array(':menu_id' => 'secondary-menu-links'));
+ $this->assertEqual(count($element), 0, 'No secondary-menu for logged-out users.');
+ }
+
+ /**
+ * Tests disabling the 'My account' link.
+ */
+ function testDisabledAccountLink() {
+ // Create an admin user and log in.
+ $this->drupalLogin($this->drupalCreateUser(array('access administration pages', 'administer menu')));
+
+ // Verify that the 'My account' link is enabled.
+ $this->drupalGet('admin/structure/menu/manage/user-menu');
+ $label = $this->xpath('//label[contains(.,:text)]/@for', array(':text' => 'Enable My account menu link'));
+ $this->assertFieldChecked((string) $label[0], "The 'My account' link is enabled by default.");
+
+ // Disable the 'My account' link.
+ $input = $this->xpath('//input[@id=:field_id]/@name', array(':field_id' => (string)$label[0]));
+ $edit = array(
+ (string) $input[0] => FALSE,
+ );
+ $this->drupalPost('admin/structure/menu/manage/user-menu', $edit, t('Save configuration'));
+
+ // Get the homepage.
+ $this->drupalGet('');
+
+ // Verify that the 'My account' link does not appear when disabled.
+ $link = $this->xpath('//ul[@id=:menu_id]/li/a[contains(@href, :href) and text()=:text]', array(
+ ':menu_id' => 'secondary-menu-links',
+ ':href' => 'user',
+ ':text' => 'My account',
+ ));
+ $this->assertEqual(count($link), 0, 'My account link is not in the secondary menu.');
+ }
+
+}
+
+/**
+ * Test user blocks.
+ */
+class UserBlocksUnitTests extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User blocks',
+ 'description' => 'Test user blocks.',
+ 'group' => 'User'
+ );
+ }
+
+ /**
+ * Test the user login block.
+ */
+ function testUserLoginBlock() {
+ // Create a user with some permission that anonymous users lack.
+ $user = $this->drupalCreateUser(array('administer permissions'));
+
+ // Log in using the block.
+ $edit = array();
+ $edit['name'] = $user->name;
+ $edit['pass'] = $user->pass_raw;
+ $this->drupalPost('admin/people/permissions', $edit, t('Log in'));
+ $this->assertNoText(t('User login'), 'Logged in.');
+
+ // Check that we are still on the same page.
+ $this->assertEqual(url('admin/people/permissions', array('absolute' => TRUE)), $this->getUrl(), 'Still on the same page after login for access denied page');
+
+ // Now, log out and repeat with a non-403 page.
+ $this->drupalLogout();
+ $this->drupalPost('filter/tips', $edit, t('Log in'));
+ $this->assertNoText(t('User login'), 'Logged in.');
+ $this->assertPattern('!!', 'Still on the same page after login for allowed page');
+
+ // Check that the user login block is not vulnerable to information
+ // disclosure to third party sites.
+ $this->drupalLogout();
+ $this->drupalPost('http://example.com/', $edit, t('Log in'), array('external' => FALSE));
+ // Check that we remain on the site after login.
+ $this->assertEqual(url('user/' . $user->uid, array('absolute' => TRUE)), $this->getUrl(), 'Redirected to user profile page after login from the frontpage');
+ }
+
+ /**
+ * Test the Who's Online block.
+ */
+ function testWhosOnlineBlock() {
+ // Generate users and make sure there are no current user sessions.
+ $user1 = $this->drupalCreateUser(array());
+ $user2 = $this->drupalCreateUser(array());
+ $user3 = $this->drupalCreateUser(array());
+ $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions}")->fetchField(), 0, 'Sessions table is empty.');
+
+ // Insert a user with two sessions.
+ $this->insertSession(array('uid' => $user1->uid));
+ $this->insertSession(array('uid' => $user1->uid));
+ $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions} WHERE uid = :uid", array(':uid' => $user1->uid))->fetchField(), 2, 'Duplicate user session has been inserted.');
+
+ // Insert a user with only one session.
+ $this->insertSession(array('uid' => $user2->uid, 'timestamp' => REQUEST_TIME + 1));
+
+ // Insert an inactive logged-in user who should not be seen in the block.
+ $this->insertSession(array('uid' => $user3->uid, 'timestamp' => (REQUEST_TIME - variable_get('user_block_seconds_online', 900) - 1)));
+
+ // Insert two anonymous user sessions.
+ $this->insertSession();
+ $this->insertSession();
+
+ // Test block output.
+ $block = user_block_view('online');
+ $this->drupalSetContent($block['content']);
+ $this->assertRaw(t('2 users'), 'Correct number of online users (2 users).');
+ $this->assertText($user1->name, 'Active user 1 found in online list.');
+ $this->assertText($user2->name, 'Active user 2 found in online list.');
+ $this->assertNoText($user3->name, "Inactive user not found in online list.");
+ $this->assertTrue(strpos($this->drupalGetContent(), $user1->name) > strpos($this->drupalGetContent(), $user2->name), 'Online users are ordered correctly.');
+ }
+
+ /**
+ * Insert a user session into the {sessions} table. This function is used
+ * since we cannot log in more than one user at the same time in tests.
+ */
+ private function insertSession(array $fields = array()) {
+ $fields += array(
+ 'uid' => 0,
+ 'sid' => drupal_hash_base64(uniqid(mt_rand(), TRUE)),
+ 'timestamp' => REQUEST_TIME,
+ );
+ db_insert('sessions')
+ ->fields($fields)
+ ->execute();
+ $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions} WHERE uid = :uid AND sid = :sid AND timestamp = :timestamp", array(':uid' => $fields['uid'], ':sid' => $fields['sid'], ':timestamp' => $fields['timestamp']))->fetchField(), 1, 'Session record inserted.');
+ }
+}
+
+/**
+ * Tests saving a user account.
+ */
+class UserSaveTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User save test',
+ 'description' => 'Test user_save() for arbitrary new uid.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Test creating a user with arbitrary uid.
+ */
+ function testUserImport() {
+ // User ID must be a number that is not in the database.
+ $max_uid = db_query('SELECT MAX(uid) FROM {users}')->fetchField();
+ $test_uid = $max_uid + mt_rand(1000, 1000000);
+ $test_name = $this->randomName();
+
+ // Create the base user, based on drupalCreateUser().
+ $user = array(
+ 'name' => $test_name,
+ 'uid' => $test_uid,
+ 'mail' => $test_name . '@example.com',
+ 'is_new' => TRUE,
+ 'pass' => user_password(),
+ 'status' => 1,
+ );
+ $user_by_return = user_save(drupal_anonymous_user(), $user);
+ $this->assertTrue($user_by_return, 'Loading user by return of user_save().');
+
+ // Test if created user exists.
+ $user_by_uid = user_load($test_uid);
+ $this->assertTrue($user_by_uid, 'Loading user by uid.');
+
+ $user_by_name = user_load_by_name($test_name);
+ $this->assertTrue($user_by_name, 'Loading user by name.');
+ }
+}
+
+/**
+ * Test the create user administration page.
+ */
+class UserCreateTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User create',
+ 'description' => 'Test the create user administration page.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Create a user through the administration interface and ensure that it
+ * displays in the user list.
+ */
+ protected function testUserAdd() {
+ $user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($user);
+
+ foreach (array(FALSE, TRUE) as $notify) {
+ $edit = array(
+ 'name' => $this->randomName(),
+ 'mail' => $this->randomName() . '@example.com',
+ 'pass[pass1]' => $pass = $this->randomString(),
+ 'pass[pass2]' => $pass,
+ 'notify' => $notify,
+ );
+ $this->drupalPost('admin/people/create', $edit, t('Create new account'));
+
+ if ($notify) {
+ $this->assertText(t('A welcome message with further instructions has been e-mailed to the new user @name.', array('@name' => $edit['name'])), 'User created');
+ $this->assertEqual(count($this->drupalGetMails()), 1, 'Notification e-mail sent');
+ }
+ else {
+ $this->assertText(t('Created a new user account for @name. No e-mail has been sent.', array('@name' => $edit['name'])), 'User created');
+ $this->assertEqual(count($this->drupalGetMails()), 0, 'Notification e-mail not sent');
+ }
+
+ $this->drupalGet('admin/people');
+ $this->assertText($edit['name'], 'User found in list of users');
+ }
+
+ // Test that the password '0' is considered a password.
+ $name = $this->randomName();
+ $edit = array(
+ 'name' => $name,
+ 'mail' => $name . '@example.com',
+ 'pass[pass1]' => 0,
+ 'pass[pass2]' => 0,
+ 'notify' => FALSE,
+ );
+ $this->drupalPost('admin/people/create', $edit, t('Create new account'));
+ $this->assertText(t('Created a new user account for @name. No e-mail has been sent.', array('@name' => $edit['name'])), 'User created with password 0');
+ $this->assertNoText('Password field is required');
+ }
+}
+
+/**
+ * Tests editing a user account.
+ */
+class UserEditTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User edit',
+ 'description' => 'Test user edit page.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Test user edit page.
+ */
+ function testUserEdit() {
+ // Test user edit functionality with user pictures disabled.
+ variable_set('user_pictures', 0);
+ $user1 = $this->drupalCreateUser(array('change own username'));
+ $user2 = $this->drupalCreateUser(array());
+ $this->drupalLogin($user1);
+
+ // Test that error message appears when attempting to use a non-unique user name.
+ $edit['name'] = $user2->name;
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t('The name %name is already taken.', array('%name' => $edit['name'])));
+
+ // Repeat the test with user pictures enabled, which modifies the form.
+ variable_set('user_pictures', 1);
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t('The name %name is already taken.', array('%name' => $edit['name'])));
+
+ // Check that filling out a single password field does not validate.
+ $edit = array();
+ $edit['pass[pass1]'] = '';
+ $edit['pass[pass2]'] = $this->randomName();
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertText(t("The specified passwords do not match."), 'Typing mismatched passwords displays an error message.');
+
+ $edit['pass[pass1]'] = $this->randomName();
+ $edit['pass[pass2]'] = '';
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertText(t("The specified passwords do not match."), 'Typing mismatched passwords displays an error message.');
+
+ // Test that the error message appears when attempting to change the mail or
+ // pass without the current password.
+ $edit = array();
+ $edit['mail'] = $this->randomName() . '@new.example.com';
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => t('E-mail address'))));
+
+ $edit['current_pass'] = $user1->pass_raw;
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+
+ // Test that the user must enter current password before changing passwords.
+ $edit = array();
+ $edit['pass[pass1]'] = $new_pass = $this->randomName();
+ $edit['pass[pass2]'] = $new_pass;
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => t('Password'))));
+
+ // Try again with the current password.
+ $edit['current_pass'] = $user1->pass_raw;
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+
+ // Make sure the user can log in with their new password.
+ $this->drupalLogout();
+ $user1->pass_raw = $new_pass;
+ $this->drupalLogin($user1);
+ $this->drupalLogout();
+ }
+
+ /**
+ * Tests setting the password to "0".
+ */
+ public function testUserWith0Password() {
+ $admin = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($admin);
+ // Create a regular user.
+ $user1 = $this->drupalCreateUser(array());
+
+ $edit = array('pass[pass1]' => '0', 'pass[pass2]' => '0');
+ $this->drupalPost("user/" . $user1->uid . "/edit", $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+
+ $this->drupalLogout();
+ $user1->pass_raw = '0';
+ $this->drupalLogin($user1);
+ $this->drupalLogout();
+ }
+}
+
+/**
+ * Tests editing a user account with and without a form rebuild.
+ */
+class UserEditRebuildTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User edit with form rebuild',
+ 'description' => 'Test user edit page when a form rebuild is triggered.',
+ 'group' => 'User',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('user_form_test');
+ }
+
+ /**
+ * Test user edit page when the form is set to rebuild.
+ */
+ function testUserEditFormRebuild() {
+ $user1 = $this->drupalCreateUser(array('change own username'));
+ $this->drupalLogin($user1);
+
+ $roles = array_keys($user1->roles);
+ // Save the user form twice.
+ $edit = array();
+ $edit['current_pass'] = $user1->pass_raw;
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+ $saved_user1 = entity_load_unchanged('user', $user1->uid);
+ $this->assertEqual(count($roles), count($saved_user1->roles), 'Count of user roles in database matches original count.');
+ $diff = array_diff(array_keys($saved_user1->roles), $roles);
+ $this->assertTrue(empty($diff), format_string('User roles in database match original: @roles', array('@roles' => implode(', ', $saved_user1->roles))));
+ // Set variable that causes the form to be rebuilt in user_form_test.module.
+ variable_set('user_form_test_user_profile_form_rebuild', TRUE);
+ $this->drupalPost("user/$user1->uid/edit", $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertRaw(t("The changes have been saved."));
+ $saved_user1 = entity_load_unchanged('user', $user1->uid);
+ $this->assertEqual(count($roles), count($saved_user1->roles), 'Count of user roles in database matches original count.');
+ $diff = array_diff(array_keys($saved_user1->roles), $roles);
+ $this->assertTrue(empty($diff), format_string('User roles in database match original: @roles', array('@roles' => implode(', ', $saved_user1->roles))));
+ }
+}
+
+/**
+ * Test case for user signatures.
+ */
+class UserSignatureTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User signatures',
+ 'description' => 'Test user signatures.',
+ 'group' => 'User',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('comment');
+
+ // Enable user signatures.
+ variable_set('user_signatures', 1);
+
+ // Prefetch text formats.
+ $this->full_html_format = filter_format_load('full_html');
+ $this->plain_text_format = filter_format_load('plain_text');
+
+ // Create regular and administrative users.
+ $this->web_user = $this->drupalCreateUser(array());
+ $admin_permissions = array('administer comments');
+ foreach (filter_formats() as $format) {
+ if ($permission = filter_permission_name($format)) {
+ $admin_permissions[] = $permission;
+ }
+ }
+ $this->admin_user = $this->drupalCreateUser($admin_permissions);
+ }
+
+ /**
+ * Test that a user can change their signature format and that it is respected
+ * upon display.
+ */
+ function testUserSignature() {
+ // Create a new node with comments on.
+ $node = $this->drupalCreateNode(array('comment' => COMMENT_NODE_OPEN));
+
+ // Verify that user signature field is not displayed on registration form.
+ $this->drupalGet('user/register');
+ $this->assertNoText(t('Signature'));
+
+ // Log in as a regular user and create a signature.
+ $this->drupalLogin($this->web_user);
+ $signature_text = "" . $this->randomName() . " ";
+ $edit = array(
+ 'signature[value]' => $signature_text,
+ 'signature[format]' => $this->plain_text_format->format,
+ );
+ $this->drupalPost('user/' . $this->web_user->uid . '/edit', $edit, t('Save'));
+
+ // Verify that values were stored.
+ $this->assertFieldByName('signature[value]', $edit['signature[value]'], 'Submitted signature text found.');
+ $this->assertFieldByName('signature[format]', $edit['signature[format]'], 'Submitted signature format found.');
+
+ // Create a comment.
+ $langcode = LANGUAGE_NONE;
+ $edit = array();
+ $edit['subject'] = $this->randomName(8);
+ $edit['comment_body[' . $langcode . '][0][value]'] = $this->randomName(16);
+ $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
+ $this->drupalPost(NULL, array(), t('Save'));
+
+ // Get the comment ID. (This technique is the same one used in the Comment
+ // module's CommentHelperCase test case.)
+ preg_match('/#comment-([0-9]+)/', $this->getURL(), $match);
+ $comment_id = $match[1];
+
+ // Log in as an administrator and edit the comment to use Full HTML, so
+ // that the comment text itself is not filtered at all.
+ $this->drupalLogin($this->admin_user);
+ $edit['comment_body[' . $langcode . '][0][format]'] = $this->full_html_format->format;
+ $this->drupalPost('comment/' . $comment_id . '/edit', $edit, t('Save'));
+
+ // Assert that the signature did not make it through unfiltered.
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertNoRaw($signature_text, 'Unfiltered signature text not found.');
+ $this->assertRaw(check_markup($signature_text, $this->plain_text_format->format), 'Filtered signature text found.');
+ }
+}
+
+/*
+ * Test that a user, having editing their own account, can still log in.
+ */
+class UserEditedOwnAccountTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User edited own account',
+ 'description' => 'Test user edited own account can still log in.',
+ 'group' => 'User',
+ );
+ }
+
+ function testUserEditedOwnAccount() {
+ // Change account setting 'Who can register accounts?' to Administrators
+ // only.
+ variable_set('user_register', USER_REGISTER_ADMINISTRATORS_ONLY);
+
+ // Create a new user account and log in.
+ $account = $this->drupalCreateUser(array('change own username'));
+ $this->drupalLogin($account);
+
+ // Change own username.
+ $edit = array();
+ $edit['name'] = $this->randomName();
+ $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
+
+ // Log out.
+ $this->drupalLogout();
+
+ // Set the new name on the user account and attempt to log back in.
+ $account->name = $edit['name'];
+ $this->drupalLogin($account);
+ }
+}
+
+/**
+ * Test case to test adding, editing and deleting roles.
+ */
+class UserRoleAdminTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User role administration',
+ 'description' => 'Test adding, editing and deleting user roles and changing role weights.',
+ 'group' => 'User',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ $this->admin_user = $this->drupalCreateUser(array('administer permissions', 'administer users'));
+ }
+
+ /**
+ * Test adding, renaming and deleting roles.
+ */
+ function testRoleAdministration() {
+ $this->drupalLogin($this->admin_user);
+
+ // Test adding a role. (In doing so, we use a role name that happens to
+ // correspond to an integer, to test that the role administration pages
+ // correctly distinguish between role names and IDs.)
+ $role_name = '123';
+ $edit = array('name' => $role_name);
+ $this->drupalPost('admin/people/permissions/roles', $edit, t('Add role'));
+ $this->assertText(t('The role has been added.'), 'The role has been added.');
+ $role = user_role_load_by_name($role_name);
+ $this->assertTrue(is_object($role), 'The role was successfully retrieved from the database.');
+
+ // Try adding a duplicate role.
+ $this->drupalPost(NULL, $edit, t('Add role'));
+ $this->assertRaw(t('The role name %name already exists. Choose another role name.', array('%name' => $role_name)), 'Duplicate role warning displayed.');
+
+ // Test renaming a role.
+ $old_name = $role_name;
+ $role_name = '456';
+ $edit = array('name' => $role_name);
+ $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", $edit, t('Save role'));
+ $this->assertText(t('The role has been renamed.'), 'The role has been renamed.');
+ $this->assertFalse(user_role_load_by_name($old_name), 'The role can no longer be retrieved from the database using its old name.');
+ $this->assertTrue(is_object(user_role_load_by_name($role_name)), 'The role can be retrieved from the database using its new name.');
+
+ // Test deleting the default administrator role.
+ $role_name = 'administrator';
+ $role = user_role_load_by_name($role_name);
+ $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", NULL, t('Delete role'));
+ $this->drupalPost(NULL, NULL, t('Delete'));
+ $this->assertText(t('The role has been deleted.'), 'The role has been deleted');
+ $this->assertNoLinkByHref("admin/people/permissions/roles/edit/{$role->rid}", 'Role edit link removed.');
+ $this->assertFalse(user_role_load_by_name($role_name), 'A deleted role can no longer be loaded.');
+ // Make sure this role is no longer configured as the administrator role.
+ $this->assertNull(variable_get('user_admin_role'), 'The administrator role is no longer configured as the administrator role.');
+
+ // Make sure that the system-defined roles cannot be edited via the user
+ // interface.
+ $this->drupalGet('admin/people/permissions/roles/edit/' . DRUPAL_ANONYMOUS_RID);
+ $this->assertResponse(403, 'Access denied when trying to edit the built-in anonymous role.');
+ $this->drupalGet('admin/people/permissions/roles/edit/' . DRUPAL_AUTHENTICATED_RID);
+ $this->assertResponse(403, 'Access denied when trying to edit the built-in authenticated role.');
+ }
+
+ /**
+ * Test user role weight change operation.
+ */
+ function testRoleWeightChange() {
+ $this->drupalLogin($this->admin_user);
+
+ // Pick up a random role and get its weight.
+ $rid = array_rand(user_roles());
+ $role = user_role_load($rid);
+ $old_weight = $role->weight;
+
+ // Change the role weight and submit the form.
+ $edit = array('roles['. $rid .'][weight]' => $old_weight + 1);
+ $this->drupalPost('admin/people/permissions/roles', $edit, t('Save order'));
+ $this->assertText(t('The role settings have been updated.'), 'The role settings form submitted successfully.');
+
+ // Retrieve the saved role and compare its weight.
+ $role = user_role_load($rid);
+ $new_weight = $role->weight;
+ $this->assertTrue(($old_weight + 1) == $new_weight, 'Role weight updated successfully.');
+ }
+}
+
+/**
+ * Test user token replacement in strings.
+ */
+class UserTokenReplaceTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User token replacement',
+ 'description' => 'Generates text using placeholders for dummy content to check user token replacement.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Creates a user, then tests the tokens generated from it.
+ */
+ function testUserTokenReplacement() {
+ global $language;
+ $url_options = array(
+ 'absolute' => TRUE,
+ 'language' => $language,
+ );
+
+ // Create two users and log them in one after another.
+ $user1 = $this->drupalCreateUser(array());
+ $user2 = $this->drupalCreateUser(array());
+ $this->drupalLogin($user1);
+ $this->drupalLogout();
+ $this->drupalLogin($user2);
+
+ $account = user_load($user1->uid);
+ $global_account = user_load($GLOBALS['user']->uid);
+
+ // Generate and test sanitized tokens.
+ $tests = array();
+ $tests['[user:uid]'] = $account->uid;
+ $tests['[user:name]'] = check_plain(format_username($account));
+ $tests['[user:mail]'] = check_plain($account->mail);
+ $tests['[user:url]'] = url("user/$account->uid", $url_options);
+ $tests['[user:edit-url]'] = url("user/$account->uid/edit", $url_options);
+ $tests['[user:last-login]'] = format_date($account->login, 'medium', '', NULL, $language->language);
+ $tests['[user:last-login:short]'] = format_date($account->login, 'short', '', NULL, $language->language);
+ $tests['[user:created]'] = format_date($account->created, 'medium', '', NULL, $language->language);
+ $tests['[user:created:short]'] = format_date($account->created, 'short', '', NULL, $language->language);
+ $tests['[current-user:name]'] = check_plain(format_username($global_account));
+
+ // Test to make sure that we generated something for each token.
+ $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
+
+ foreach ($tests as $input => $expected) {
+ $output = token_replace($input, array('user' => $account), array('language' => $language));
+ $this->assertEqual($output, $expected, format_string('Sanitized user token %token replaced.', array('%token' => $input)));
+ }
+
+ // Generate and test unsanitized tokens.
+ $tests['[user:name]'] = format_username($account);
+ $tests['[user:mail]'] = $account->mail;
+ $tests['[current-user:name]'] = format_username($global_account);
+
+ foreach ($tests as $input => $expected) {
+ $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)));
+ }
+ }
+}
+
+/**
+ * Test user search.
+ */
+class UserUserSearchTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User search',
+ 'description' => 'Tests the user search page and verifies that sensitive information is hidden from unauthorized users.',
+ 'group' => 'User',
+ );
+ }
+
+ function testUserSearch() {
+ $user1 = $this->drupalCreateUser(array('access user profiles', 'search content', 'use advanced search'));
+ $this->drupalLogin($user1);
+ $keys = $user1->mail;
+ $edit = array('keys' => $keys);
+ $this->drupalPost('search/user/', $edit, t('Search'));
+ $this->assertNoText($keys);
+ $this->drupalLogout();
+
+ $user2 = $this->drupalCreateUser(array('administer users', 'access user profiles', 'search content', 'use advanced search'));
+ $this->drupalLogin($user2);
+ $keys = $user2->mail;
+ $edit = array('keys' => $keys);
+ $this->drupalPost('search/user/', $edit, t('Search'));
+ $this->assertText($keys);
+
+ // Verify that wildcard search works.
+ $keys = $user1->name;
+ $keys = substr($keys, 0, 2) . '*' . substr($keys, 4, 2);
+ $edit = array('keys' => $keys);
+ $this->drupalPost('search/user/', $edit, t('Search'));
+ $this->assertText($user1->name, 'Search for username wildcard resulted in user name on page for administrative user.');
+
+ // Verify that wildcard search works for email.
+ $keys = $user1->mail;
+ $keys = substr($keys, 0, 2) . '*' . substr($keys, 4, 2);
+ $edit = array('keys' => $keys);
+ $this->drupalPost('search/user/', $edit, t('Search'));
+ $this->assertText($user1->name, 'Search for email wildcard resulted in user name on page for administrative user.');
+
+ // Create a blocked user.
+ $blocked_user = $this->drupalCreateUser();
+ $edit = array('status' => 0);
+ $blocked_user = user_save($blocked_user, $edit);
+
+ // Verify that users with "administer users" permissions can see blocked
+ // accounts in search results.
+ $edit = array('keys' => $blocked_user->name);
+ $this->drupalPost('search/user/', $edit, t('Search'));
+ $this->assertText($blocked_user->name, 'Blocked users are listed on the user search results for users with the "administer users" permission.');
+
+ // Verify that users without "administer users" permissions do not see
+ // blocked accounts in search results.
+ $this->drupalLogin($user1);
+ $edit = array('keys' => $blocked_user->name);
+ $this->drupalPost('search/user/', $edit, t('Search'));
+ $this->assertNoText($blocked_user->name, 'Blocked users are hidden from the user search results.');
+
+ $this->drupalLogout();
+ }
+}
+
+/**
+ * Test role assignment.
+ */
+class UserRolesAssignmentTestCase extends DrupalWebTestCase {
+ protected $admin_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Role assignment',
+ 'description' => 'Tests that users can be assigned and unassigned roles.',
+ 'group' => 'User'
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ $this->admin_user = $this->drupalCreateUser(array('administer permissions', 'administer users'));
+ $this->drupalLogin($this->admin_user);
+ }
+
+ /**
+ * Tests that a user can be assigned a role and that the role can be removed
+ * again.
+ */
+ function testAssignAndRemoveRole() {
+ $rid = $this->drupalCreateRole(array('administer content types'));
+ $account = $this->drupalCreateUser();
+
+ // Assign the role to the user.
+ $this->drupalPost('user/' . $account->uid . '/edit', array("roles[$rid]" => $rid), t('Save'));
+ $this->assertText(t('The changes have been saved.'));
+ $this->assertFieldChecked('edit-roles-' . $rid, 'Role is assigned.');
+ $this->userLoadAndCheckRoleAssigned($account, $rid);
+
+ // Remove the role from the user.
+ $this->drupalPost('user/' . $account->uid . '/edit', array("roles[$rid]" => FALSE), t('Save'));
+ $this->assertText(t('The changes have been saved.'));
+ $this->assertNoFieldChecked('edit-roles-' . $rid, 'Role is removed from user.');
+ $this->userLoadAndCheckRoleAssigned($account, $rid, FALSE);
+ }
+
+ /**
+ * Tests that when creating a user the role can be assigned. And that it can
+ * be removed again.
+ */
+ function testCreateUserWithRole() {
+ $rid = $this->drupalCreateRole(array('administer content types'));
+ // Create a new user and add the role at the same time.
+ $edit = array(
+ 'name' => $this->randomName(),
+ 'mail' => $this->randomName() . '@example.com',
+ 'pass[pass1]' => $pass = $this->randomString(),
+ 'pass[pass2]' => $pass,
+ "roles[$rid]" => $rid,
+ );
+ $this->drupalPost('admin/people/create', $edit, t('Create new account'));
+ $this->assertText(t('Created a new user account for !name.', array('!name' => $edit['name'])));
+ // Get the newly added user.
+ $account = user_load_by_name($edit['name']);
+
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->assertFieldChecked('edit-roles-' . $rid, 'Role is assigned.');
+ $this->userLoadAndCheckRoleAssigned($account, $rid);
+
+ // Remove the role again.
+ $this->drupalPost('user/' . $account->uid . '/edit', array("roles[$rid]" => FALSE), t('Save'));
+ $this->assertText(t('The changes have been saved.'));
+ $this->assertNoFieldChecked('edit-roles-' . $rid, 'Role is removed from user.');
+ $this->userLoadAndCheckRoleAssigned($account, $rid, FALSE);
+ }
+
+ /**
+ * Check role on user object.
+ *
+ * @param object $account
+ * The user account to check.
+ * @param string $rid
+ * The role ID to search for.
+ * @param bool $is_assigned
+ * (optional) Whether to assert that $rid exists (TRUE) or not (FALSE).
+ * Defaults to TRUE.
+ */
+ private function userLoadAndCheckRoleAssigned($account, $rid, $is_assigned = TRUE) {
+ $account = user_load($account->uid, TRUE);
+ if ($is_assigned) {
+ $this->assertTrue(array_key_exists($rid, $account->roles), 'The role is present in the user object.');
+ }
+ else {
+ $this->assertFalse(array_key_exists($rid, $account->roles), 'The role is not present in the user object.');
+ }
+ }
+}
+
+
+/**
+ * Unit test for authmap assignment.
+ */
+class UserAuthmapAssignmentTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Authmap assignment',
+ 'description' => 'Tests that users can be assigned and unassigned authmaps.',
+ 'group' => 'User'
+ );
+ }
+
+ /**
+ * Test authmap assignment and retrieval.
+ */
+ function testAuthmapAssignment() {
+ $account = $this->drupalCreateUser();
+
+ // Assign authmaps to the user.
+ $authmaps = array(
+ 'authname_poll' => 'external username one',
+ 'authname_book' => 'external username two',
+ );
+ user_set_authmaps($account, $authmaps);
+
+ // Test for expected authmaps.
+ $expected_authmaps = array(
+ 'external username one' => array(
+ 'poll' => 'external username one',
+ ),
+ 'external username two' => array(
+ 'book' => 'external username two',
+ ),
+ );
+ foreach ($expected_authmaps as $authname => $expected_output) {
+ $this->assertIdentical(user_get_authmaps($authname), $expected_output, format_string('Authmap for authname %authname was set correctly.', array('%authname' => $authname)));
+ }
+
+ // Remove authmap for module poll, add authmap for module blog.
+ $authmaps = array(
+ 'authname_poll' => NULL,
+ 'authname_blog' => 'external username three',
+ );
+ user_set_authmaps($account, $authmaps);
+
+ // Assert that external username one does not have authmaps.
+ $remove_username = 'external username one';
+ unset($expected_authmaps[$remove_username]);
+ $this->assertFalse(user_get_authmaps($remove_username), format_string('Authmap for %authname was removed.', array('%authname' => $remove_username)));
+
+ // Assert that a new authmap was created for external username three, and
+ // existing authmaps for external username two were unchanged.
+ $expected_authmaps['external username three'] = array('blog' => 'external username three');
+ foreach ($expected_authmaps as $authname => $expected_output) {
+ $this->assertIdentical(user_get_authmaps($authname), $expected_output, format_string('Authmap for authname %authname was set correctly.', array('%authname' => $authname)));
+ }
+ }
+}
+
+/**
+ * Tests user_validate_current_pass on a custom form.
+ */
+class UserValidateCurrentPassCustomForm extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User validate current pass custom form',
+ 'description' => 'Test that user_validate_current_pass is usable on a custom form.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * User with permission to view content.
+ */
+ protected $accessUser;
+
+ /**
+ * User permission to administer users.
+ */
+ protected $adminUser;
+
+ function setUp() {
+ parent::setUp('user_form_test');
+ // Create two users
+ $this->accessUser = $this->drupalCreateUser(array('access content'));
+ $this->adminUser = $this->drupalCreateUser(array('administer users'));
+ }
+
+ /**
+ * Tests that user_validate_current_pass can be reused on a custom form.
+ */
+ function testUserValidateCurrentPassCustomForm() {
+ $this->drupalLogin($this->adminUser);
+
+ // Submit the custom form with the admin user using the access user's password.
+ $edit = array();
+ $edit['user_form_test_field'] = $this->accessUser->name;
+ $edit['current_pass'] = $this->accessUser->pass_raw;
+ $this->drupalPost('user_form_test_current_password/' . $this->accessUser->uid, $edit, t('Test'));
+ $this->assertText(t('The password has been validated and the form submitted successfully.'));
+ }
+}
diff --git a/modules/user/user.tokens.inc b/modules/user/user.tokens.inc
new file mode 100644
index 0000000..8dcea4b
--- /dev/null
+++ b/modules/user/user.tokens.inc
@@ -0,0 +1,131 @@
+ t('Users'),
+ 'description' => t('Tokens related to individual user accounts.'),
+ 'needs-data' => 'user',
+ );
+ $types['current-user'] = array(
+ 'name' => t('Current user'),
+ 'description' => t('Tokens related to the currently logged in user.'),
+ 'type' => 'user',
+ );
+
+ $user['uid'] = array(
+ 'name' => t('User ID'),
+ 'description' => t("The unique ID of the user account."),
+ );
+ $user['name'] = array(
+ 'name' => t("Name"),
+ 'description' => t("The login name of the user account."),
+ );
+ $user['mail'] = array(
+ 'name' => t("Email"),
+ 'description' => t("The email address of the user account."),
+ );
+ $user['url'] = array(
+ 'name' => t("URL"),
+ 'description' => t("The URL of the account profile page."),
+ );
+ $user['edit-url'] = array(
+ 'name' => t("Edit URL"),
+ 'description' => t("The URL of the account edit page."),
+ );
+
+ $user['last-login'] = array(
+ 'name' => t("Last login"),
+ 'description' => t("The date the user last logged in to the site."),
+ 'type' => 'date',
+ );
+ $user['created'] = array(
+ 'name' => t("Created"),
+ 'description' => t("The date the user account was created."),
+ 'type' => 'date',
+ );
+
+ return array(
+ 'types' => $types,
+ 'tokens' => array('user' => $user),
+ );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function user_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+ $sanitize = !empty($options['sanitize']);
+
+ $replacements = array();
+
+ if ($type == 'user' && !empty($data['user'])) {
+ $account = $data['user'];
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ // Basic user account information.
+ case 'uid':
+ // In the case of hook user_presave uid is not set yet.
+ $replacements[$original] = !empty($account->uid) ? $account->uid : t('not yet assigned');
+ break;
+
+ case 'name':
+ $name = format_username($account);
+ $replacements[$original] = $sanitize ? check_plain($name) : $name;
+ break;
+
+ case 'mail':
+ $replacements[$original] = $sanitize ? check_plain($account->mail) : $account->mail;
+ break;
+
+ case 'url':
+ $replacements[$original] = !empty($account->uid) ? url("user/$account->uid", $url_options) : t('not yet assigned');
+ break;
+
+ case 'edit-url':
+ $replacements[$original] = !empty($account->uid) ? url("user/$account->uid/edit", $url_options) : t('not yet assigned');
+ break;
+
+ // These tokens are default variations on the chained tokens handled below.
+ case 'last-login':
+ $replacements[$original] = !empty($account->login) ? format_date($account->login, 'medium', '', NULL, $language_code) : t('never');
+ break;
+
+ case 'created':
+ // In the case of user_presave the created date may not yet be set.
+ $replacements[$original] = !empty($account->created) ? format_date($account->created, 'medium', '', NULL, $language_code) : t('not yet created');
+ break;
+ }
+ }
+
+ if ($login_tokens = token_find_with_prefix($tokens, 'last-login')) {
+ $replacements += token_generate('date', $login_tokens, array('date' => $account->login), $options);
+ }
+
+ if ($registered_tokens = token_find_with_prefix($tokens, 'created')) {
+ $replacements += token_generate('date', $registered_tokens, array('date' => $account->created), $options);
+ }
+ }
+
+ if ($type == 'current-user') {
+ $account = user_load($GLOBALS['user']->uid);
+ $replacements += token_generate('user', $tokens, array('user' => $account), $options);
+ }
+
+ return $replacements;
+}
diff --git a/profiles/README.txt b/profiles/README.txt
new file mode 100644
index 0000000..91d012b
--- /dev/null
+++ b/profiles/README.txt
@@ -0,0 +1,28 @@
+Installation profiles define additional steps that run after the base
+installation provided by Drupal core when Drupal is first installed.
+
+WHAT TO PLACE IN THIS DIRECTORY?
+--------------------------------
+
+Place downloaded and custom installation profiles in this directory.
+Installation profiles are generally provided as part of a Drupal distribution.
+They only impact the installation of your site. They do not have any effect on
+an already running site.
+
+DOWNLOAD ADDITIONAL DISTRIBUTIONS
+---------------------------------
+
+Contributed distributions from the Drupal community may be downloaded at
+https://www.drupal.org/project/project_distribution.
+
+MULTISITE CONFIGURATION
+-----------------------
+
+In multisite configurations, installation profiles found in this directory are
+available to all sites during their initial site installation.
+
+MORE INFORMATION
+----------------
+
+Refer to the "Installation profiles" section of the README.txt in the Drupal
+root directory for further information on extending Drupal with custom profiles.
diff --git a/profiles/minimal/minimal.info b/profiles/minimal/minimal.info
new file mode 100644
index 0000000..1b363ab
--- /dev/null
+++ b/profiles/minimal/minimal.info
@@ -0,0 +1,12 @@
+name = Minimal
+description = Start with only a few modules enabled.
+version = VERSION
+core = 7.x
+dependencies[] = block
+dependencies[] = dblog
+
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
+project = "drupal"
+datestamp = "1522264019"
+
diff --git a/profiles/minimal/minimal.install b/profiles/minimal/minimal.install
new file mode 100644
index 0000000..9cf4fa2
--- /dev/null
+++ b/profiles/minimal/minimal.install
@@ -0,0 +1,81 @@
+ 'system',
+ 'delta' => 'main',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'content',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'user',
+ 'delta' => 'login',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'sidebar_first',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'navigation',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'sidebar_first',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'management',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 1,
+ 'region' => 'sidebar_first',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'help',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'help',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ );
+ $query = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'pages', 'cache'));
+ foreach ($values as $record) {
+ $query->values($record);
+ }
+ $query->execute();
+
+ // Allow visitor account creation, but with administrative approval.
+ variable_set('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+
+ // Enable default permissions for system roles.
+ user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access content'));
+ user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('access content'));
+}
diff --git a/profiles/minimal/minimal.profile b/profiles/minimal/minimal.profile
new file mode 100644
index 0000000..fe6da8c
--- /dev/null
+++ b/profiles/minimal/minimal.profile
@@ -0,0 +1,15 @@
+ 'filtered_html',
+ 'name' => 'Filtered HTML',
+ 'weight' => 0,
+ 'filters' => array(
+ // URL filter.
+ 'filter_url' => array(
+ 'weight' => 0,
+ 'status' => 1,
+ ),
+ // HTML filter.
+ 'filter_html' => array(
+ 'weight' => 1,
+ 'status' => 1,
+ ),
+ // Line break filter.
+ 'filter_autop' => array(
+ 'weight' => 2,
+ 'status' => 1,
+ ),
+ // HTML corrector filter.
+ 'filter_htmlcorrector' => array(
+ 'weight' => 10,
+ 'status' => 1,
+ ),
+ ),
+ );
+ $filtered_html_format = (object) $filtered_html_format;
+ filter_format_save($filtered_html_format);
+
+ $full_html_format = array(
+ 'format' => 'full_html',
+ 'name' => 'Full HTML',
+ 'weight' => 1,
+ 'filters' => array(
+ // URL filter.
+ 'filter_url' => array(
+ 'weight' => 0,
+ 'status' => 1,
+ ),
+ // Line break filter.
+ 'filter_autop' => array(
+ 'weight' => 1,
+ 'status' => 1,
+ ),
+ // HTML corrector filter.
+ 'filter_htmlcorrector' => array(
+ 'weight' => 10,
+ 'status' => 1,
+ ),
+ ),
+ );
+ $full_html_format = (object) $full_html_format;
+ filter_format_save($full_html_format);
+
+ // Enable some standard blocks.
+ $default_theme = variable_get('theme_default', 'bartik');
+ $admin_theme = 'seven';
+ $blocks = array(
+ array(
+ 'module' => 'system',
+ 'delta' => 'main',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'content',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'search',
+ 'delta' => 'form',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => -1,
+ 'region' => 'sidebar_first',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'node',
+ 'delta' => 'recent',
+ 'theme' => $admin_theme,
+ 'status' => 1,
+ 'weight' => 10,
+ 'region' => 'dashboard_main',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'user',
+ 'delta' => 'login',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'sidebar_first',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'navigation',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'sidebar_first',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'powered-by',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 10,
+ 'region' => 'footer',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'help',
+ 'theme' => $default_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'help',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'main',
+ 'theme' => $admin_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'content',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'system',
+ 'delta' => 'help',
+ 'theme' => $admin_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'help',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'user',
+ 'delta' => 'login',
+ 'theme' => $admin_theme,
+ 'status' => 1,
+ 'weight' => 10,
+ 'region' => 'content',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'user',
+ 'delta' => 'new',
+ 'theme' => $admin_theme,
+ 'status' => 1,
+ 'weight' => 0,
+ 'region' => 'dashboard_sidebar',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ array(
+ 'module' => 'search',
+ 'delta' => 'form',
+ 'theme' => $admin_theme,
+ 'status' => 1,
+ 'weight' => -10,
+ 'region' => 'dashboard_sidebar',
+ 'pages' => '',
+ 'cache' => -1,
+ ),
+ );
+ $query = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'pages', 'cache'));
+ foreach ($blocks as $block) {
+ $query->values($block);
+ }
+ $query->execute();
+
+ // Insert default pre-defined node types into the database. For a complete
+ // list of available node type attributes, refer to the node type API
+ // documentation at: http://api.drupal.org/api/HEAD/function/hook_node_info.
+ $types = array(
+ array(
+ 'type' => 'page',
+ 'name' => st('Basic page'),
+ 'base' => 'node_content',
+ 'description' => st("Use basic pages for your static content, such as an 'About us' page."),
+ 'custom' => 1,
+ 'modified' => 1,
+ 'locked' => 0,
+ ),
+ array(
+ 'type' => 'article',
+ 'name' => st('Article'),
+ 'base' => 'node_content',
+ 'description' => st('Use articles for time-sensitive content like news, press releases or blog posts.'),
+ 'custom' => 1,
+ 'modified' => 1,
+ 'locked' => 0,
+ ),
+ );
+
+ foreach ($types as $type) {
+ $type = node_type_set_defaults($type);
+ node_type_save($type);
+ node_add_body_field($type);
+ }
+
+ // Insert default pre-defined RDF mapping into the database.
+ $rdf_mappings = array(
+ array(
+ 'type' => 'node',
+ 'bundle' => 'page',
+ 'mapping' => array(
+ 'rdftype' => array('foaf:Document'),
+ ),
+ ),
+ array(
+ 'type' => 'node',
+ 'bundle' => 'article',
+ 'mapping' => array(
+ 'field_image' => array(
+ 'predicates' => array('og:image', 'rdfs:seeAlso'),
+ 'type' => 'rel',
+ ),
+ 'field_tags' => array(
+ 'predicates' => array('dc:subject'),
+ 'type' => 'rel',
+ ),
+ ),
+ ),
+ );
+ foreach ($rdf_mappings as $rdf_mapping) {
+ rdf_mapping_save($rdf_mapping);
+ }
+
+ // Default "Basic page" to not be promoted and have comments disabled.
+ variable_set('node_options_page', array('status'));
+ variable_set('comment_page', COMMENT_NODE_HIDDEN);
+
+ // Don't display date and author information for "Basic page" nodes by default.
+ variable_set('node_submitted_page', FALSE);
+
+ // Enable user picture support and set the default to a square thumbnail option.
+ variable_set('user_pictures', '1');
+ variable_set('user_picture_dimensions', '1024x1024');
+ variable_set('user_picture_file_size', '800');
+ variable_set('user_picture_style', 'thumbnail');
+
+ // Allow visitor account creation with administrative approval.
+ variable_set('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+
+ // Create a default vocabulary named "Tags", enabled for the 'article' content type.
+ $description = st('Use tags to group articles on similar topics into categories.');
+ $vocabulary = (object) array(
+ 'name' => st('Tags'),
+ 'description' => $description,
+ 'machine_name' => 'tags',
+ );
+ taxonomy_vocabulary_save($vocabulary);
+
+ $field = array(
+ 'field_name' => 'field_' . $vocabulary->machine_name,
+ 'type' => 'taxonomy_term_reference',
+ // Set cardinality to unlimited for tagging.
+ 'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+ 'settings' => array(
+ 'allowed_values' => array(
+ array(
+ 'vocabulary' => $vocabulary->machine_name,
+ 'parent' => 0,
+ ),
+ ),
+ ),
+ );
+ field_create_field($field);
+
+ $help = st('Enter a comma-separated list of words to describe your content.');
+ $instance = array(
+ 'field_name' => 'field_' . $vocabulary->machine_name,
+ 'entity_type' => 'node',
+ 'label' => 'Tags',
+ 'bundle' => 'article',
+ 'description' => $help,
+ 'widget' => array(
+ 'type' => 'taxonomy_autocomplete',
+ 'weight' => -4,
+ ),
+ 'display' => array(
+ 'default' => array(
+ 'type' => 'taxonomy_term_reference_link',
+ 'weight' => 10,
+ ),
+ 'teaser' => array(
+ 'type' => 'taxonomy_term_reference_link',
+ 'weight' => 10,
+ ),
+ ),
+ );
+ field_create_instance($instance);
+
+
+ // Create an image field named "Image", enabled for the 'article' content type.
+ // Many of the following values will be defaulted, they're included here as an illustrative examples.
+ // See http://api.drupal.org/api/function/field_create_field/7
+
+ $field = array(
+ 'field_name' => 'field_image',
+ 'type' => 'image',
+ 'cardinality' => 1,
+ 'locked' => FALSE,
+ 'indexes' => array('fid' => array('fid')),
+ 'settings' => array(
+ 'uri_scheme' => 'public',
+ 'default_image' => FALSE,
+ ),
+ 'storage' => array(
+ 'type' => 'field_sql_storage',
+ 'settings' => array(),
+ ),
+ );
+ field_create_field($field);
+
+
+ // Many of the following values will be defaulted, they're included here as an illustrative examples.
+ // See http://api.drupal.org/api/function/field_create_instance/7
+ $instance = array(
+ 'field_name' => 'field_image',
+ 'entity_type' => 'node',
+ 'label' => 'Image',
+ 'bundle' => 'article',
+ 'description' => st('Upload an image to go with this article.'),
+ 'required' => FALSE,
+
+ 'settings' => array(
+ 'file_directory' => 'field/image',
+ 'file_extensions' => 'png gif jpg jpeg',
+ 'max_filesize' => '',
+ 'max_resolution' => '',
+ 'min_resolution' => '',
+ 'alt_field' => TRUE,
+ 'title_field' => '',
+ ),
+
+ 'widget' => array(
+ 'type' => 'image_image',
+ 'settings' => array(
+ 'progress_indicator' => 'throbber',
+ 'preview_image_style' => 'thumbnail',
+ ),
+ 'weight' => -1,
+ ),
+
+ 'display' => array(
+ 'default' => array(
+ 'label' => 'hidden',
+ 'type' => 'image',
+ 'settings' => array('image_style' => 'large', 'image_link' => ''),
+ 'weight' => -1,
+ ),
+ 'teaser' => array(
+ 'label' => 'hidden',
+ 'type' => 'image',
+ 'settings' => array('image_style' => 'medium', 'image_link' => 'content'),
+ 'weight' => -1,
+ ),
+ ),
+ );
+ field_create_instance($instance);
+
+ // Enable default permissions for system roles.
+ $filtered_html_permission = filter_permission_name($filtered_html_format);
+ user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access content', 'access comments', $filtered_html_permission));
+ user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('access content', 'access comments', 'post comments', 'skip comment approval', $filtered_html_permission));
+
+ // Create a default role for site administrators, with all available permissions assigned.
+ $admin_role = new stdClass();
+ $admin_role->name = 'administrator';
+ $admin_role->weight = 2;
+ user_role_save($admin_role);
+ user_role_grant_permissions($admin_role->rid, array_keys(module_invoke_all('permission')));
+ // Set this as the administrator role.
+ variable_set('user_admin_role', $admin_role->rid);
+
+ // Assign user 1 the "administrator" role.
+ db_insert('users_roles')
+ ->fields(array('uid' => 1, 'rid' => $admin_role->rid))
+ ->execute();
+
+ // Create a Home link in the main menu.
+ $item = array(
+ 'link_title' => st('Home'),
+ 'link_path' => '',
+ 'menu_name' => 'main-menu',
+ );
+ menu_link_save($item);
+
+ // Update the menu router information.
+ menu_rebuild();
+
+ // Enable the admin theme.
+ db_update('system')
+ ->fields(array('status' => 1))
+ ->condition('type', 'theme')
+ ->condition('name', 'seven')
+ ->execute();
+ variable_set('admin_theme', 'seven');
+ variable_set('node_admin_theme', '1');
+}
diff --git a/profiles/standard/standard.profile b/profiles/standard/standard.profile
new file mode 100644
index 0000000..d554c93
--- /dev/null
+++ b/profiles/standard/standard.profile
@@ -0,0 +1,15 @@
+ 'Installation profile module tests helper',
+ 'description' => 'Verifies that tests in installation profile modules are found and may use another profile for running tests.',
+ 'group' => 'Installation profile',
+ );
+ }
+
+ function setUp() {
+ // Attempt to install a module in Testing profile, while this test runs with
+ // a different profile.
+ parent::setUp(array('drupal_system_listing_compatible_test'));
+ }
+
+ /**
+ * Non-empty test* method required to executed the test case class.
+ */
+ function testDrupalSystemListing() {
+ $this->pass(__CLASS__ . ' test executed.');
+ }
+}
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
new file mode 100644
index 0000000..d2bd947
--- /dev/null
+++ b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info
@@ -0,0 +1,15 @@
+name = "Drupal system listing incompatible test"
+description = "Support module for testing the drupal_system_listing function."
+package = Testing
+version = VERSION
+; This deliberately has the wrong core version, to test that it does not take
+; precedence over the version of the same module that is in the
+; modules/simpletest/tests directory.
+core = 6.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2018-03-28
+version = "7.58"
+project = "drupal"
+datestamp = "1522264019"
+
diff --git a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module
@@ -0,0 +1 @@
+ /dev/null 2>&1
diff --git a/scripts/drupal.sh b/scripts/drupal.sh
new file mode 100644
index 0000000..76bd750
--- /dev/null
+++ b/scripts/drupal.sh
@@ -0,0 +1,144 @@
+#!/usr/bin/env php
+"
+Example: {$script} "http://mysite.org/node"
+
+All arguments are long options.
+
+ --help This page.
+
+ --root Set the working directory for the script to the specified path.
+ To execute Drupal this has to be the root directory of your
+ Drupal installation, f.e. /home/www/foo/drupal (assuming Drupal
+ running on Unix). Current directory is not required.
+ Use surrounding quotation marks on Windows.
+
+ --verbose This option displays the options as they are set, but will
+ produce errors from setting the session.
+
+ URI The URI to execute, i.e. http://default/foo/bar for executing
+ the path '/foo/bar' in your site 'default'. URI has to be
+ enclosed by quotation marks if there are ampersands in it
+ (f.e. index.php?q=node&foo=bar). Prefix 'http://' is required,
+ and the domain must exist in Drupal's sites-directory.
+
+ If the given path and file exists it will be executed directly,
+ i.e. if URI is set to http://default/bar/foo.php
+ and bar/foo.php exists, this script will be executed without
+ bootstrapping Drupal. To execute Drupal's cron.php, specify
+ http://default/cron.php as the URI.
+
+
+To run this script without --root argument invoke it from the root directory
+of your Drupal installation with
+
+ ./scripts/{$script}
+\n
+EOF;
+ exit;
+}
+
+// define default settings
+$cmd = 'index.php';
+$_SERVER['HTTP_HOST'] = 'default';
+$_SERVER['PHP_SELF'] = '/index.php';
+$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+$_SERVER['SERVER_SOFTWARE'] = NULL;
+$_SERVER['REQUEST_METHOD'] = 'GET';
+$_SERVER['QUERY_STRING'] = '';
+$_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] = '/';
+$_SERVER['HTTP_USER_AGENT'] = 'console';
+
+// toggle verbose mode
+if (in_array('--verbose', $_SERVER['argv'])) {
+ $_verbose_mode = true;
+}
+else {
+ $_verbose_mode = false;
+}
+
+// parse invocation arguments
+while ($param = array_shift($_SERVER['argv'])) {
+ switch ($param) {
+ case '--root':
+ // change working directory
+ $path = array_shift($_SERVER['argv']);
+ if (is_dir($path)) {
+ chdir($path);
+ if ($_verbose_mode) {
+ echo "cwd changed to: {$path}\n";
+ }
+ }
+ else {
+ echo "\nERROR: {$path} not found.\n\n";
+ }
+ break;
+
+ default:
+ if (substr($param, 0, 2) == '--') {
+ // ignore unknown options
+ break;
+ }
+ else {
+ // parse the URI
+ $path = parse_url($param);
+
+ // set site name
+ if (isset($path['host'])) {
+ $_SERVER['HTTP_HOST'] = $path['host'];
+ }
+
+ // set query string
+ if (isset($path['query'])) {
+ $_SERVER['QUERY_STRING'] = $path['query'];
+ parse_str($path['query'], $_GET);
+ $_REQUEST = $_GET;
+ }
+
+ // set file to execute or Drupal path (clean URLs enabled)
+ if (isset($path['path']) && file_exists(substr($path['path'], 1))) {
+ $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] = $path['path'];
+ $cmd = substr($path['path'], 1);
+ }
+ elseif (isset($path['path'])) {
+ if (!isset($_GET['q'])) {
+ $_REQUEST['q'] = $_GET['q'] = $path['path'];
+ }
+ }
+
+ // display setup in verbose mode
+ if ($_verbose_mode) {
+ echo "Hostname set to: {$_SERVER['HTTP_HOST']}\n";
+ echo "Script name set to: {$cmd}\n";
+ echo "Path set to: {$_GET['q']}\n";
+ }
+ }
+ break;
+ }
+}
+
+if (file_exists($cmd)) {
+ include $cmd;
+}
+else {
+ echo "\nERROR: {$cmd} not found.\n\n";
+}
+exit();
diff --git a/scripts/dump-database-d6.sh b/scripts/dump-database-d6.sh
new file mode 100644
index 0000000..41146b0
--- /dev/null
+++ b/scripts/dump-database-d6.sh
@@ -0,0 +1,101 @@
+#!/usr/bin/env php
+ $data) {
+ // Remove descriptions to save time and code.
+ unset($data['description']);
+ foreach ($data['fields'] as &$field) {
+ unset($field['description']);
+ }
+
+ // Dump the table structure.
+ $output .= "db_create_table('" . $table . "', " . drupal_var_export($data) . ");\n";
+
+ // Don't output values for those tables.
+ if (substr($table, 0, 5) == 'cache' || $table == 'sessions' || $table == 'watchdog') {
+ $output .= "\n";
+ continue;
+ }
+
+ // Prepare the export of values.
+ $result = db_query('SELECT * FROM {'. $table .'}');
+ $insert = '';
+ while ($record = db_fetch_array($result)) {
+ // users.uid is a serial and inserting 0 into a serial can break MySQL.
+ // So record uid + 1 instead of uid for every uid and once all records
+ // are in place, fix them up.
+ if ($table == 'users') {
+ $record['uid']++;
+ }
+ $insert .= '->values('. drupal_var_export($record) .")\n";
+ }
+
+ // Dump the values if there are some.
+ if ($insert) {
+ $output .= "db_insert('". $table . "')->fields(". drupal_var_export(array_keys($data['fields'])) .")\n";
+ $output .= $insert;
+ $output .= "->execute();\n";
+ }
+
+ // Add the statement fixing the serial in the user table.
+ if ($table == 'users') {
+ $output .= "db_query('UPDATE {users} SET uid = uid - 1');\n";
+ }
+
+ $output .= "\n";
+}
+
+print $output;
diff --git a/scripts/dump-database-d7.sh b/scripts/dump-database-d7.sh
new file mode 100644
index 0000000..7692c40
--- /dev/null
+++ b/scripts/dump-database-d7.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env php
+ $data) {
+ // Remove descriptions to save time and code.
+ unset($data['description']);
+ foreach ($data['fields'] as &$field) {
+ unset($field['description']);
+ }
+
+ // Dump the table structure.
+ $output .= "db_create_table('" . $table . "', " . drupal_var_export($data) . ");\n";
+
+ // Don't output values for those tables.
+ if (substr($table, 0, 5) == 'cache' || $table == 'sessions' || $table == 'watchdog') {
+ $output .= "\n";
+ continue;
+ }
+
+ // Prepare the export of values.
+ $result = db_query('SELECT * FROM {'. $table .'}', array(), array('fetch' => PDO::FETCH_ASSOC));
+ $insert = '';
+ foreach ($result as $record) {
+ $insert .= '->values('. drupal_var_export($record) .")\n";
+ }
+
+ // Dump the values if there are some.
+ if ($insert) {
+ $output .= "db_insert('". $table . "')->fields(". drupal_var_export(array_keys($data['fields'])) .")\n";
+ $output .= $insert;
+ $output .= "->execute();\n";
+ }
+
+ $output .= "\n";
+}
+
+print $output;
diff --git a/scripts/generate-d6-content.sh b/scripts/generate-d6-content.sh
new file mode 100644
index 0000000..cd33e4d
--- /dev/null
+++ b/scripts/generate-d6-content.sh
@@ -0,0 +1,207 @@
+#!/usr/bin/env php
+ 11 ? array('page' => TRUE) : array();
+ $vocabulary['multiple'] = $multiple[$i % 12];
+ $vocabulary['required'] = $required[$i % 12];
+ $vocabulary['relations'] = 1;
+ $vocabulary['hierarchy'] = $hierarchy[$i % 12];
+ $vocabulary['weight'] = $i;
+ taxonomy_save_vocabulary($vocabulary);
+ $parents = array();
+ // Vocabularies without hierarchy get one term, single parent vocabularies get
+ // one parent and one child term. Multiple parent vocabularies get three
+ // terms: t0, t1, t2 where t0 is a parent of both t1 and t2.
+ for ($j = 0; $j < $vocabulary['hierarchy'] + 1; $j++) {
+ $term = array();
+ $term['vid'] = $vocabulary['vid'];
+ // For multiple parent vocabularies, omit the t0-t1 relation, otherwise
+ // every parent in the vocabulary is a parent.
+ $term['parent'] = $vocabulary['hierarchy'] == 2 && i == 1 ? array() : $parents;
+ ++$term_id;
+ $term['name'] = "term $term_id of vocabulary $voc_id (j=$j)";
+ $term['description'] = 'description of ' . $term['name'];
+ $term['weight'] = $i * 3 + $j;
+ taxonomy_save_term($term);
+ $terms[] = $term['tid'];
+ $parents[] = $term['tid'];
+ }
+}
+
+$node_id = 0;
+$revision_id = 0;
+module_load_include('inc', 'node', 'node.pages');
+for ($i = 0; $i < 24; $i++) {
+ $uid = intval($i / 8) + 3;
+ $user = user_load($uid);
+ $node = new stdClass();
+ $node->uid = $uid;
+ $node->type = $i < 12 ? 'page' : 'story';
+ $node->sticky = 0;
+ ++$node_id;
+ ++$revision_id;
+ $node->title = "node title $node_id rev $revision_id (i=$i)";
+ $type = node_get_types('type', $node->type);
+ if ($type->has_body) {
+ $node->body = str_repeat("node body ($node->type) - $i", 100);
+ $node->teaser = node_teaser($node->body);
+ $node->filter = variable_get('filter_default_format', 1);
+ $node->format = FILTER_FORMAT_DEFAULT;
+ }
+ $node->status = intval($i / 4) % 2;
+ $node->language = '';
+ $node->revision = $i < 12;
+ $node->promote = $i % 2;
+ $node->created = $now + $i * 86400;
+ $node->log = "added $i node";
+ // Make every term association different a little. For nodes with revisions,
+ // make the initial revision have a different set of terms than the
+ // newest revision.
+ $node_terms = $terms;
+ unset($node_terms[$i], $node_terms[47 - $i]);
+ if ($node->revision) {
+ $node->taxonomy = array($i => $terms[$i], 47-$i => $terms[47 - $i]);
+ }
+ else {
+ $node->taxonomy = $node_terms;
+ }
+ node_save($node);
+ path_set_alias("node/$node->nid", "content/$node->created");
+ if ($node->revision) {
+ $user = user_load($uid + 3);
+ ++$revision_id;
+ $node->title .= " rev2 $revision_id";
+ $node->body = str_repeat("node revision body ($node->type) - $i", 100);
+ $node->log = "added $i revision";
+ $node->taxonomy = $node_terms;
+ node_save($node);
+ }
+}
+
+// Create poll content
+for ($i = 0; $i < 12; $i++) {
+ $uid = intval($i / 4) + 3;
+ $user = user_load($uid);
+ $node = new stdClass();
+ $node->uid = $uid;
+ $node->type = 'poll';
+ $node->sticky = 0;
+ $node->title = "poll title $i";
+ $type = node_get_types('type', $node->type);
+ if ($type->has_body) {
+ $node->body = str_repeat("node body ($node->type) - $i", 100);
+ $node->teaser = node_teaser($node->body);
+ $node->filter = variable_get('filter_default_format', 1);
+ $node->format = FILTER_FORMAT_DEFAULT;
+ }
+ $node->status = intval($i / 2) % 2;
+ $node->language = '';
+ $node->revision = 1;
+ $node->promote = $i % 2;
+ $node->created = $now + $i * 43200;
+ $node->log = "added $i poll";
+
+ $nbchoices = ($i % 4) + 2;
+ for ($c = 0; $c < $nbchoices; $c++) {
+ $node->choice[] = array('chtext' => "Choice $c for poll $i");
+ }
+ node_save($node);
+ path_set_alias("node/$node->nid", "content/poll/$i");
+ path_set_alias("node/$node->nid/results", "content/poll/$i/results");
+
+ // Add some votes
+ for ($v = 0; $v < ($i % 4) + 5; $v++) {
+ $c = $v % $nbchoices;
+ $form_state = array();
+ $form_state['values']['choice'] = $c;
+ $form_state['values']['op'] = t('Vote');
+ drupal_execute('poll_view_voting', $form_state, $node);
+ }
+}
+
+$uid = 6;
+$user = user_load($uid);
+$node = new stdClass();
+$node->uid = $uid;
+$node->type = 'broken';
+$node->sticky = 0;
+$node->title = "node title 24";
+$node->body = str_repeat("node body ($node->type) - 37", 100);
+$node->teaser = node_teaser($node->body);
+$node->filter = variable_get('filter_default_format', 1);
+$node->format = FILTER_FORMAT_DEFAULT;
+$node->status = 1;
+$node->language = '';
+$node->revision = 0;
+$node->promote = 0;
+$node->created = 1263769200;
+$node->log = "added $i node";
+node_save($node);
+path_set_alias("node/$node->nid", "content/1263769200");
diff --git a/scripts/generate-d7-content.sh b/scripts/generate-d7-content.sh
new file mode 100644
index 0000000..1e1d13f
--- /dev/null
+++ b/scripts/generate-d7-content.sh
@@ -0,0 +1,320 @@
+#!/usr/bin/env php
+fields(array('uid', 'name', 'pass', 'mail', 'status', 'created', 'access'));
+for ($i = 0; $i < 6; $i++) {
+ $name = "test user $i";
+ $pass = md5("test PassW0rd $i !(.)");
+ $mail = "test$i@example.com";
+ $now = mktime(0, 0, 0, 1, $i + 1, 2010);
+ $query->values(array(db_next_id(), $name, user_hash_password($pass), $mail, 1, $now, $now));
+}
+$query->execute();
+
+// Create vocabularies and terms.
+
+if (module_exists('taxonomy')) {
+ $terms = array();
+
+ // All possible combinations of these vocabulary properties.
+ $hierarchy = array(0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2);
+ $multiple = array(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1);
+ $required = array(0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1);
+
+ $voc_id = 0;
+ $term_id = 0;
+ for ($i = 0; $i < 24; $i++) {
+ $vocabulary = new stdClass;
+ ++$voc_id;
+ $vocabulary->name = "vocabulary $voc_id (i=$i)";
+ $vocabulary->machine_name = 'vocabulary_' . $voc_id . '_' . $i;
+ $vocabulary->description = "description of ". $vocabulary->name;
+ $vocabulary->multiple = $multiple[$i % 12];
+ $vocabulary->required = $required[$i % 12];
+ $vocabulary->relations = 1;
+ $vocabulary->hierarchy = $hierarchy[$i % 12];
+ $vocabulary->weight = $i;
+ taxonomy_vocabulary_save($vocabulary);
+ $field = array(
+ 'field_name' => 'taxonomy_'. $vocabulary->machine_name,
+ 'module' => 'taxonomy',
+ 'type' => 'taxonomy_term_reference',
+ 'cardinality' => $vocabulary->multiple || $vocabulary->tags ? FIELD_CARDINALITY_UNLIMITED : 1,
+ 'settings' => array(
+ 'required' => $vocabulary->required ? TRUE : FALSE,
+ 'allowed_values' => array(
+ array(
+ 'vocabulary' => $vocabulary->machine_name,
+ 'parent' => 0,
+ ),
+ ),
+ ),
+ );
+ field_create_field($field);
+ $node_types = $i > 11 ? array('page') : array_keys(node_type_get_types());
+ foreach ($node_types as $bundle) {
+ $instance = array(
+ 'label' => $vocabulary->name,
+ 'field_name' => $field['field_name'],
+ 'bundle' => $bundle,
+ 'entity_type' => 'node',
+ 'settings' => array(),
+ 'description' => $vocabulary->help,
+ 'required' => $vocabulary->required,
+ 'widget' => array(),
+ 'display' => array(
+ 'default' => array(
+ 'type' => 'taxonomy_term_reference_link',
+ 'weight' => 10,
+ ),
+ 'teaser' => array(
+ 'type' => 'taxonomy_term_reference_link',
+ 'weight' => 10,
+ ),
+ ),
+ );
+ if ($vocabulary->tags) {
+ $instance['widget'] = array(
+ 'type' => 'taxonomy_autocomplete',
+ 'module' => 'taxonomy',
+ 'settings' => array(
+ 'size' => 60,
+ 'autocomplete_path' => 'taxonomy/autocomplete',
+ ),
+ );
+ }
+ else {
+ $instance['widget'] = array(
+ 'type' => 'options_select',
+ 'settings' => array(),
+ );
+ }
+ field_create_instance($instance);
+ }
+ $parents = array();
+ // Vocabularies without hierarchy get one term; single parent vocabularies
+ // get one parent and one child term. Multiple parent vocabularies get
+ // three terms: t0, t1, t2 where t0 is a parent of both t1 and t2.
+ for ($j = 0; $j < $vocabulary->hierarchy + 1; $j++) {
+ $term = new stdClass;
+ $term->vocabulary_machine_name = $vocabulary->machine_name;
+ // For multiple parent vocabularies, omit the t0-t1 relation, otherwise
+ // every parent in the vocabulary is a parent.
+ $term->parent = $vocabulary->hierarchy == 2 && i == 1 ? array() : $parents;
+ ++$term_id;
+ $term->name = "term $term_id of vocabulary $voc_id (j=$j)";
+ $term->description = 'description of ' . $term->name;
+ $term->format = 'filtered_html';
+ $term->weight = $i * 3 + $j;
+ taxonomy_term_save($term);
+ $terms[] = $term->tid;
+ $term_vocabs[$term->tid] = 'taxonomy_' . $vocabulary->machine_name;
+ $parents[] = $term->tid;
+ }
+ }
+}
+
+$node_id = 0;
+$revision_id = 0;
+module_load_include('inc', 'node', 'node.pages');
+for ($i = 0; $i < 24; $i++) {
+ $uid = intval($i / 8) + 3;
+ $user = user_load($uid);
+ $node = new stdClass();
+ $node->uid = $uid;
+ $node->type = $i < 12 ? 'page' : 'story';
+ $node->sticky = 0;
+ ++$node_id;
+ ++$revision_id;
+ $node->title = "node title $node_id rev $revision_id (i=$i)";
+ $node->language = LANGUAGE_NONE;
+ $body_text = str_repeat("node body ($node->type) - $i", 100);
+ $node->body[$node->language][0]['value'] = $body_text;
+ $node->body[$node->language][0]['summary'] = text_summary($body_text);
+ $node->body[$node->language][0]['format'] = 'filtered_html';
+ $node->status = intval($i / 4) % 2;
+ $node->revision = $i < 12;
+ $node->promote = $i % 2;
+ $node->created = $now + $i * 86400;
+ $node->log = "added $i node";
+ // Make every term association different a little. For nodes with revisions,
+ // make the initial revision have a different set of terms than the
+ // newest revision.
+ $items = array();
+ if (module_exists('taxonomy')) {
+ if ($node->revision) {
+ $node_terms = array($terms[$i], $terms[47-$i]);
+ }
+ else {
+ $node_terms = $terms;
+ unset($node_terms[$i], $node_terms[47 - $i]);
+ }
+ foreach ($node_terms as $tid) {
+ $field_name = $term_vocabs[$tid];
+ $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $tid);
+ }
+ }
+ $node->path = array('alias' => "content/$node->created");
+ node_save($node);
+ if ($node->revision) {
+ $user = user_load($uid + 3);
+ ++$revision_id;
+ $node->title .= " rev2 $revision_id";
+ $body_text = str_repeat("node revision body ($node->type) - $i", 100);
+ $node->body[$node->language][0]['value'] = $body_text;
+ $node->body[$node->language][0]['summary'] = text_summary($body_text);
+ $node->body[$node->language][0]['format'] = 'filtered_html';
+ $node->log = "added $i revision";
+ $node_terms = $terms;
+ unset($node_terms[$i], $node_terms[47 - $i]);
+ foreach ($node_terms as $tid) {
+ $field_name = $term_vocabs[$tid];
+ $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $tid);
+ }
+ node_save($node);
+ }
+}
+
+if (module_exists('poll')) {
+ // Create poll content.
+ for ($i = 0; $i < 12; $i++) {
+ $uid = intval($i / 4) + 3;
+ $user = user_load($uid);
+ $node = new stdClass();
+ $node->uid = $uid;
+ $node->type = 'poll';
+ $node->sticky = 0;
+ $node->title = "poll title $i";
+ $node->language = LANGUAGE_NONE;
+ $node->status = intval($i / 2) % 2;
+ $node->revision = 1;
+ $node->promote = $i % 2;
+ $node->created = REQUEST_TIME + $i * 43200;
+ $node->runtime = 0;
+ $node->active = 1;
+ $node->log = "added $i poll";
+ $node->path = array('alias' => "content/poll/$i");
+
+ $nbchoices = ($i % 4) + 2;
+ for ($c = 0; $c < $nbchoices; $c++) {
+ $node->choice[] = array('chtext' => "Choice $c for poll $i", 'chvotes' => 0, 'weight' => 0);
+ }
+ node_save($node);
+ $path = array(
+ 'alias' => "content/poll/$i/results",
+ 'source' => "node/$node->nid/results",
+ );
+ path_save($path);
+
+ // Add some votes.
+ $node = node_load($node->nid);
+ $choices = array_keys($node->choice);
+ $original_user = $GLOBALS['user'];
+ for ($v = 0; $v < ($i % 4); $v++) {
+ drupal_static_reset('ip_address');
+ $_SERVER['REMOTE_ADDR'] = "127.0.$v.1";
+ $GLOBALS['user'] = drupal_anonymous_user();// We should have already allowed anon to vote.
+ $c = $v % $nbchoices;
+ $form_state = array();
+ $form_state['values']['choice'] = $choices[$c];
+ $form_state['values']['op'] = t('Vote');
+ drupal_form_submit('poll_view_voting', $form_state, $node);
+ }
+ }
+}
+
+// Test that upgrade works even on a bundle whose parent module was disabled.
+// This is simulated by creating an existing content type and changing the
+// bundle to another type through direct database update queries.
+$node_type = 'broken';
+$uid = 6;
+$user = user_load($uid);
+$node = new stdClass();
+$node->uid = $uid;
+$node->type = 'article';
+$body_text = str_repeat("node body ($node_type) - 37", 100);
+$node->sticky = 0;
+$node->title = "node title 24";
+$node->language = LANGUAGE_NONE;
+$node->body[$node->language][0]['value'] = $body_text;
+$node->body[$node->language][0]['summary'] = text_summary($body_text);
+$node->body[$node->language][0]['format'] = 'filtered_html';
+$node->status = 1;
+$node->revision = 0;
+$node->promote = 0;
+$node->created = 1263769200;
+$node->log = "added a broken node";
+$node->path = array('alias' => "content/1263769200");
+node_save($node);
+db_update('node')
+ ->fields(array(
+ 'type' => $node_type,
+ ))
+ ->condition('nid', $node->nid)
+ ->execute();
+if (db_table_exists('field_data_body')) {
+ db_update('field_data_body')
+ ->fields(array(
+ 'bundle' => $node_type,
+ ))
+ ->condition('entity_id', $node->nid)
+ ->condition('entity_type', 'node')
+ ->execute();
+ db_update('field_revision_body')
+ ->fields(array(
+ 'bundle' => $node_type,
+ ))
+ ->condition('entity_id', $node->nid)
+ ->condition('entity_type', 'node')
+ ->execute();
+}
+db_update('field_config_instance')
+ ->fields(array(
+ 'bundle' => $node_type,
+ ))
+ ->condition('bundle', 'article')
+ ->execute();
diff --git a/scripts/password-hash.sh b/scripts/password-hash.sh
new file mode 100644
index 0000000..1afe438
--- /dev/null
+++ b/scripts/password-hash.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env php
+"
+Example: {$script} "mynewpassword"
+
+All arguments are long options.
+
+ --help Print this page.
+
+ --root
+
+ Set the working directory for the script to the specified path.
+ To execute this script this has to be the root directory of your
+ Drupal installation, e.g. /home/www/foo/drupal (assuming Drupal
+ running on Unix). Use surrounding quotation marks on Windows.
+
+ "" ["" ["" ...]]
+
+ One or more plan-text passwords enclosed by double quotes. The
+ output hash may be manually entered into the {users}.pass field to
+ change a password via SQL to a known value.
+
+To run this script without the --root argument invoke it from the root directory
+of your Drupal installation as
+
+ ./scripts/{$script}
+\n
+EOF;
+ exit;
+}
+
+$passwords = array();
+
+// Parse invocation arguments.
+while ($param = array_shift($_SERVER['argv'])) {
+ switch ($param) {
+ case '--root':
+ // Change the working directory.
+ $path = array_shift($_SERVER['argv']);
+ if (is_dir($path)) {
+ chdir($path);
+ }
+ break;
+ default:
+ // Add a password to the list to be processed.
+ $passwords[] = $param;
+ break;
+ }
+}
+
+define('DRUPAL_ROOT', getcwd());
+
+include_once DRUPAL_ROOT . '/includes/password.inc';
+include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+
+foreach ($passwords as $password) {
+ print("\npassword: $password \t\thash: ". user_hash_password($password) ."\n");
+}
+print("\n");
+
diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh
new file mode 100644
index 0000000..a42215e
--- /dev/null
+++ b/scripts/run-tests.sh
@@ -0,0 +1,790 @@
+ $tests) {
+ $all_tests = array_merge($all_tests, array_keys($tests));
+}
+$test_list = array();
+
+if ($args['list']) {
+ // Display all available tests.
+ echo "\nAvailable test groups & classes\n";
+ echo "-------------------------------\n\n";
+ foreach ($groups as $group => $tests) {
+ echo $group . "\n";
+ foreach ($tests as $class => $info) {
+ echo " - " . $info['name'] . ' (' . $class . ')' . "\n";
+ }
+ }
+ exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
+}
+
+$test_list = simpletest_script_get_test_list();
+
+// Try to allocate unlimited time to run the tests.
+drupal_set_time_limit(0);
+
+simpletest_script_reporter_init();
+
+// Setup database for test results.
+$test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
+
+// Execute tests.
+$status = simpletest_script_execute_batch($test_id, simpletest_script_get_test_list());
+
+// Retrieve the last database prefix used for testing and the last test class
+// that was run from. Use the information to read the lgo file in case any
+// fatal errors caused the test to crash.
+list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
+simpletest_log_read($test_id, $last_prefix, $last_test_class);
+
+// Stop the timer.
+simpletest_script_reporter_timer_stop();
+
+// Display results before database is cleared.
+simpletest_script_reporter_display_results();
+
+if ($args['xml']) {
+ simpletest_script_reporter_write_xml_results();
+}
+
+// Cleanup our test results.
+simpletest_clean_results_table($test_id);
+
+// Test complete, exit.
+exit($status);
+
+/**
+ * Print help text.
+ */
+function simpletest_script_help() {
+ global $args;
+
+ echo <<
+Example: {$args['script']} Profile
+
+All arguments are long options.
+
+ --help Print this page.
+
+ --list Display all available test groups.
+
+ --clean Cleans up database tables or directories from previous, failed,
+ tests and then exits (no tests are run).
+
+ --url Immediately precedes a URL to set the host and path. You will
+ need this parameter if Drupal is in a subdirectory on your
+ localhost and you have not set \$base_url in settings.php. Tests
+ can be run under SSL by including https:// in the URL.
+
+ --php The absolute path to the PHP executable. Usually not needed.
+
+ --concurrency [num]
+
+ Run tests in parallel, up to [num] tests at a time.
+
+ --all Run all available tests.
+
+ --class Run tests identified by specific class names, instead of group names.
+
+ --file Run tests identified by specific file names, instead of group names.
+ Specify the path and the extension (i.e. 'modules/user/user.test').
+
+ --directory Run all tests found within the specified file directory.
+
+ --xml
+
+ If provided, test results will be written as xml files to this path.
+
+ --color Output text format results with color highlighting.
+
+ --verbose Output detailed assertion messages in addition to summary.
+
+ [,[, ...]]
+
+ One or more tests to be run. By default, these are interpreted
+ as the names of test groups as shown at
+ ?q=admin/config/development/testing.
+ These group names typically correspond to module names like "User"
+ or "Profile" or "System", but there is also a group "XML-RPC".
+ If --class is specified then these are interpreted as the names of
+ specific test classes whose test methods will be run. Tests must
+ be separated by commas. Ignored if --all is specified.
+
+To run this script you will normally invoke it from the root directory of your
+Drupal installation as the webserver user (differs per configuration), or root:
+
+sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
+ --url http://example.com/ --all
+sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
+ --url http://example.com/ --class BlockTestCase
+\n
+EOF;
+}
+
+/**
+ * Parse execution argument and ensure that all are valid.
+ *
+ * @return The list of arguments.
+ */
+function simpletest_script_parse_args() {
+ // Set default values.
+ $args = array(
+ 'script' => '',
+ 'help' => FALSE,
+ 'list' => FALSE,
+ 'clean' => FALSE,
+ 'url' => '',
+ 'php' => '',
+ 'concurrency' => 1,
+ 'all' => FALSE,
+ 'class' => FALSE,
+ 'file' => FALSE,
+ 'directory' => '',
+ 'color' => FALSE,
+ 'verbose' => FALSE,
+ 'test_names' => array(),
+ // Used internally.
+ 'test-id' => 0,
+ 'execute-test' => '',
+ 'xml' => '',
+ );
+
+ // Override with set values.
+ $args['script'] = basename(array_shift($_SERVER['argv']));
+
+ $count = 0;
+ while ($arg = array_shift($_SERVER['argv'])) {
+ if (preg_match('/--(\S+)/', $arg, $matches)) {
+ // Argument found.
+ if (array_key_exists($matches[1], $args)) {
+ // Argument found in list.
+ $previous_arg = $matches[1];
+ if (is_bool($args[$previous_arg])) {
+ $args[$matches[1]] = TRUE;
+ }
+ else {
+ $args[$matches[1]] = array_shift($_SERVER['argv']);
+ }
+ // Clear extraneous values.
+ $args['test_names'] = array();
+ $count++;
+ }
+ else {
+ // Argument not found in list.
+ simpletest_script_print_error("Unknown argument '$arg'.");
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+ }
+ else {
+ // Values found without an argument should be test names.
+ $args['test_names'] += explode(',', $arg);
+ $count++;
+ }
+ }
+
+ // Validate the concurrency argument
+ if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
+ simpletest_script_print_error("--concurrency must be a strictly positive integer.");
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+
+ return array($args, $count);
+}
+
+/**
+ * Initialize script variables and perform general setup requirements.
+ */
+function simpletest_script_init($server_software) {
+ global $args, $php;
+
+ $host = 'localhost';
+ $path = '';
+ // Determine location of php command automatically, unless a command line argument is supplied.
+ if (!empty($args['php'])) {
+ $php = $args['php'];
+ }
+ elseif ($php_env = getenv('_')) {
+ // '_' is an environment variable set by the shell. It contains the command that was executed.
+ $php = $php_env;
+ }
+ elseif ($sudo = getenv('SUDO_COMMAND')) {
+ // 'SUDO_COMMAND' is an environment variable set by the sudo program.
+ // Extract only the PHP interpreter, not the rest of the command.
+ list($php, ) = explode(' ', $sudo, 2);
+ }
+ else {
+ simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
+ simpletest_script_help();
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+
+ // Get URL from arguments.
+ if (!empty($args['url'])) {
+ $parsed_url = parse_url($args['url']);
+ $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
+ $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
+
+ // If the passed URL schema is 'https' then setup the $_SERVER variables
+ // properly so that testing will run under HTTPS.
+ if ($parsed_url['scheme'] == 'https') {
+ $_SERVER['HTTPS'] = 'on';
+ }
+ }
+
+ $_SERVER['HTTP_HOST'] = $host;
+ $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+ $_SERVER['SERVER_ADDR'] = '127.0.0.1';
+ $_SERVER['SERVER_SOFTWARE'] = $server_software;
+ $_SERVER['SERVER_NAME'] = 'localhost';
+ $_SERVER['REQUEST_URI'] = $path .'/';
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
+ $_SERVER['PHP_SELF'] = $path .'/index.php';
+ $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
+
+ if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
+ // Ensure that any and all environment variables are changed to https://.
+ foreach ($_SERVER as $key => $value) {
+ $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
+ }
+ }
+
+ chdir(realpath(dirname(__FILE__) . '/..'));
+ define('DRUPAL_ROOT', getcwd());
+ require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+}
+
+/**
+ * Execute a batch of tests.
+ */
+function simpletest_script_execute_batch($test_id, $test_classes) {
+ global $args;
+
+ $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+
+ // Multi-process execution.
+ $children = array();
+ while (!empty($test_classes) || !empty($children)) {
+ while (count($children) < $args['concurrency']) {
+ if (empty($test_classes)) {
+ break;
+ }
+
+ // Fork a child process.
+ $test_class = array_shift($test_classes);
+ $command = simpletest_script_command($test_id, $test_class);
+ $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
+
+ if (!is_resource($process)) {
+ echo "Unable to fork test process. Aborting.\n";
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+
+ // Register our new child.
+ $children[] = array(
+ 'process' => $process,
+ 'class' => $test_class,
+ 'pipes' => $pipes,
+ );
+ }
+
+ // Wait for children every 200ms.
+ usleep(200000);
+
+ // Check if some children finished.
+ foreach ($children as $cid => $child) {
+ $status = proc_get_status($child['process']);
+ if (empty($status['running'])) {
+ // The child exited, unregister it.
+ proc_close($child['process']);
+ if ($status['exitcode'] == SIMPLETEST_SCRIPT_EXIT_FAILURE) {
+ if ($status['exitcode'] > $total_status) {
+ $total_status = $status['exitcode'];
+ }
+ }
+ elseif ($status['exitcode']) {
+ $total_status = $status['exitcode'];
+ echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
+ }
+
+ // Remove this child.
+ unset($children[$cid]);
+ }
+ }
+ }
+ return $total_status;
+}
+
+/**
+ * Bootstrap Drupal and run a single test.
+ */
+function simpletest_script_run_one_test($test_id, $test_class) {
+ try {
+ // Bootstrap Drupal.
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+
+ simpletest_classloader_register();
+
+ $test = new $test_class($test_id);
+ $test->run();
+ $info = $test->getInfo();
+
+ $had_fails = (isset($test->results['#fail']) && $test->results['#fail'] > 0);
+ $had_exceptions = (isset($test->results['#exception']) && $test->results['#exception'] > 0);
+ $status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
+ simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status));
+
+ // Finished, kill this runner.
+ if ($had_fails || $had_exceptions) {
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+ exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
+ }
+ catch (Exception $e) {
+ echo (string) $e;
+ exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+ }
+}
+
+/**
+ * Return a command used to run a test in a separate process.
+ *
+ * @param $test_id
+ * The current test ID.
+ * @param $test_class
+ * The name of the test class to run.
+ */
+function simpletest_script_command($test_id, $test_class) {
+ global $args, $php;
+
+ $command = escapeshellarg($php) . ' ' . escapeshellarg('./scripts/' . $args['script']) . ' --url ' . escapeshellarg($args['url']);
+ if ($args['color']) {
+ $command .= ' --color';
+ }
+ $command .= " --php " . escapeshellarg($php) . " --test-id $test_id --execute-test " . escapeshellarg($test_class);
+ return $command;
+}
+
+/**
+ * Get list of tests based on arguments. If --all specified then
+ * returns all available tests, otherwise reads list of tests.
+ *
+ * Will print error and exit if no valid tests were found.
+ *
+ * @return List of tests.
+ */
+function simpletest_script_get_test_list() {
+ global $args, $all_tests, $groups;
+
+ $test_list = array();
+ if ($args['all']) {
+ $test_list = $all_tests;
+ }
+ else {
+ if ($args['class']) {
+ // Check for valid class names.
+ $test_list = array();
+ foreach ($args['test_names'] as $test_class) {
+ if (class_exists($test_class)) {
+ $test_list[] = $test_class;
+ }
+ else {
+ $groups = simpletest_test_get_all();
+ $all_classes = array();
+ foreach ($groups as $group) {
+ $all_classes = array_merge($all_classes, array_keys($group));
+ }
+ simpletest_script_print_error('Test class not found: ' . $test_class);
+ simpletest_script_print_alternatives($test_class, $all_classes, 6);
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+ }
+ }
+ elseif ($args['file']) {
+ $files = array();
+ foreach ($args['test_names'] as $file) {
+ $files[drupal_realpath($file)] = 1;
+ }
+
+ // Check for valid class names.
+ foreach ($all_tests as $class_name) {
+ $refclass = new ReflectionClass($class_name);
+ $file = $refclass->getFileName();
+ if (isset($files[$file])) {
+ $test_list[] = $class_name;
+ }
+ }
+ }
+ elseif ($args['directory']) {
+ // Extract test case class names from specified directory.
+ // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
+ // Since we do not want to hard-code too many structural file/directory
+ // assumptions about PSR-0/4 files and directories, we check for the
+ // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
+ // its path.
+ // Ignore anything from third party vendors, and ignore template files used in tests.
+ // And any api.php files.
+ $ignore = array('nomask' => '/vendor|\.tpl\.php|\.api\.php/');
+ $files = array();
+ if ($args['directory'][0] === '/') {
+ $directory = $args['directory'];
+ }
+ else {
+ $directory = DRUPAL_ROOT . "/" . $args['directory'];
+ }
+ $file_list = file_scan_directory($directory, '/\.php|\.test$/', $ignore);
+ foreach ($file_list as $file) {
+ // '/Tests/' can be contained anywhere in the file's path (there can be
+ // sub-directories below /Tests), but must be contained literally.
+ // Case-insensitive to match all Simpletest and PHPUnit tests:
+ // ./lib/Drupal/foo/Tests/Bar/Baz.php
+ // ./foo/src/Tests/Bar/Baz.php
+ // ./foo/tests/Drupal/foo/Tests/FooTest.php
+ // ./foo/tests/src/FooTest.php
+ // $file->filename doesn't give us a directory, so we use $file->uri
+ // Strip the drupal root directory and trailing slash off the URI
+ $filename = substr($file->uri, strlen(DRUPAL_ROOT)+1);
+ if (stripos($filename, '/Tests/')) {
+ $files[drupal_realpath($filename)] = 1;
+ } else if (stripos($filename, '.test')){
+ $files[drupal_realpath($filename)] = 1;
+ }
+ }
+
+ // Check for valid class names.
+ foreach ($all_tests as $class_name) {
+ $refclass = new ReflectionClass($class_name);
+ $classfile = $refclass->getFileName();
+ if (isset($files[$classfile])) {
+ $test_list[] = $class_name;
+ }
+ }
+ }
+ else {
+ // Check for valid group names and get all valid classes in group.
+ foreach ($args['test_names'] as $group_name) {
+ if (isset($groups[$group_name])) {
+ $test_list = array_merge($test_list, array_keys($groups[$group_name]));
+ }
+ else {
+ simpletest_script_print_error('Test group not found: ' . $group_name);
+ simpletest_script_print_alternatives($group_name, array_keys($groups));
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+ }
+ }
+ }
+
+ if (empty($test_list)) {
+ simpletest_script_print_error('No valid tests were specified.');
+ exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+ }
+ return $test_list;
+}
+
+/**
+ * Initialize the reporter.
+ */
+function simpletest_script_reporter_init() {
+ global $args, $all_tests, $test_list, $results_map;
+
+ $results_map = array(
+ 'pass' => 'Pass',
+ 'fail' => 'Fail',
+ 'exception' => 'Exception'
+ );
+
+ echo "\n";
+ echo "Drupal test run\n";
+ echo "---------------\n";
+ echo "\n";
+
+ // Tell the user about what tests are to be run.
+ if ($args['all']) {
+ echo "All tests will run.\n\n";
+ }
+ else {
+ echo "Tests to be run:\n";
+ foreach ($test_list as $class_name) {
+ $info = call_user_func(array($class_name, 'getInfo'));
+ echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
+ }
+ echo "\n";
+ }
+
+ echo "Test run started:\n";
+ echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
+ timer_start('run-tests');
+ echo "\n";
+
+ echo "Test summary\n";
+ echo "------------\n";
+ echo "\n";
+}
+
+/**
+ * Display jUnit XML test results.
+ */
+function simpletest_script_reporter_write_xml_results() {
+ global $args, $test_id, $results_map;
+
+ $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));
+
+ $test_class = '';
+ $xml_files = array();
+
+ foreach ($results as $result) {
+ if (isset($results_map[$result->status])) {
+ if ($result->test_class != $test_class) {
+ // We've moved onto a new class, so write the last classes results to a file:
+ if (isset($xml_files[$test_class])) {
+ file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
+ unset($xml_files[$test_class]);
+ }
+ $test_class = $result->test_class;
+ if (!isset($xml_files[$test_class])) {
+ $doc = new DomDocument('1.0');
+ $root = $doc->createElement('testsuite');
+ $root = $doc->appendChild($root);
+ $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
+ }
+ }
+
+ // For convenience:
+ $dom_document = &$xml_files[$test_class]['doc'];
+
+ // Create the XML element for this test case:
+ $case = $dom_document->createElement('testcase');
+ $case->setAttribute('classname', $test_class);
+ list($class, $name) = explode('->', $result->function, 2);
+ $case->setAttribute('name', $name);
+
+ // Passes get no further attention, but failures and exceptions get to add more detail:
+ if ($result->status == 'fail') {
+ $fail = $dom_document->createElement('failure');
+ $fail->setAttribute('type', 'failure');
+ $fail->setAttribute('message', $result->message_group);
+ $text = $dom_document->createTextNode($result->message);
+ $fail->appendChild($text);
+ $case->appendChild($fail);
+ }
+ elseif ($result->status == 'exception') {
+ // In the case of an exception the $result->function may not be a class
+ // method so we record the full function name:
+ $case->setAttribute('name', $result->function);
+
+ $fail = $dom_document->createElement('error');
+ $fail->setAttribute('type', 'exception');
+ $fail->setAttribute('message', $result->message_group);
+ $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
+ $text = $dom_document->createTextNode($full_message);
+ $fail->appendChild($text);
+ $case->appendChild($fail);
+ }
+ // Append the test case XML to the test suite:
+ $xml_files[$test_class]['suite']->appendChild($case);
+ }
+ }
+ // The last test case hasn't been saved to a file yet, so do that now:
+ if (isset($xml_files[$test_class])) {
+ file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
+ unset($xml_files[$test_class]);
+ }
+}
+
+/**
+ * Stop the test timer.
+ */
+function simpletest_script_reporter_timer_stop() {
+ echo "\n";
+ $end = timer_stop('run-tests');
+ echo "Test run duration: " . format_interval($end['time'] / 1000);
+ echo "\n\n";
+}
+
+/**
+ * Display test results.
+ */
+function simpletest_script_reporter_display_results() {
+ global $args, $test_id, $results_map;
+
+ if ($args['verbose']) {
+ // Report results.
+ echo "Detailed test results\n";
+ echo "---------------------\n";
+
+ $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));
+ $test_class = '';
+ foreach ($results as $result) {
+ if (isset($results_map[$result->status])) {
+ if ($result->test_class != $test_class) {
+ // Display test class every time results are for new test class.
+ echo "\n\n---- $result->test_class ----\n\n\n";
+ $test_class = $result->test_class;
+
+ // Print table header.
+ echo "Status Group Filename Line Function \n";
+ echo "--------------------------------------------------------------------------------\n";
+ }
+
+ simpletest_script_format_result($result);
+ }
+ }
+ }
+}
+
+/**
+ * Format the result so that it fits within the default 80 character
+ * terminal size.
+ *
+ * @param $result The result object to format.
+ */
+function simpletest_script_format_result($result) {
+ global $results_map, $color;
+
+ $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n",
+ $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);
+
+ simpletest_script_print($summary, simpletest_script_color_code($result->status));
+
+ $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
+ foreach ($lines as $line) {
+ echo " $line\n";
+ }
+}
+
+/**
+ * Print error message prefixed with " ERROR: " and displayed in fail color
+ * if color output is enabled.
+ *
+ * @param $message The message to print.
+ */
+function simpletest_script_print_error($message) {
+ simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
+}
+
+/**
+ * Print a message to the console, if color is enabled then the specified
+ * color code will be used.
+ *
+ * @param $message The message to print.
+ * @param $color_code The color code to use for coloring.
+ */
+function simpletest_script_print($message, $color_code) {
+ global $args;
+ if ($args['color']) {
+ echo "\033[" . $color_code . "m" . $message . "\033[0m";
+ }
+ else {
+ echo $message;
+ }
+}
+
+/**
+ * Get the color code associated with the specified status.
+ *
+ * @param $status The status string to get code for.
+ * @return Color code.
+ */
+function simpletest_script_color_code($status) {
+ switch ($status) {
+ case 'pass':
+ return SIMPLETEST_SCRIPT_COLOR_PASS;
+ case 'fail':
+ return SIMPLETEST_SCRIPT_COLOR_FAIL;
+ case 'exception':
+ return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
+ }
+ return 0; // Default formatting.
+}
+
+/**
+ * Prints alternative test names.
+ *
+ * Searches the provided array of string values for close matches based on the
+ * Levenshtein algorithm.
+ *
+ * @see http://php.net/manual/en/function.levenshtein.php
+ *
+ * @param string $string
+ * A string to test.
+ * @param array $array
+ * A list of strings to search.
+ * @param int $degree
+ * The matching strictness. Higher values return fewer matches. A value of
+ * 4 means that the function will return strings from $array if the candidate
+ * string in $array would be identical to $string by changing 1/4 or fewer of
+ * its characters.
+ */
+function simpletest_script_print_alternatives($string, $array, $degree = 4) {
+ $alternatives = array();
+ foreach ($array as $item) {
+ $lev = levenshtein($string, $item);
+ if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
+ $alternatives[] = $item;
+ }
+ }
+ if (!empty($alternatives)) {
+ simpletest_script_print(" Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
+ foreach ($alternatives as $alternative) {
+ simpletest_script_print(" - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
+ }
+ }
+}
diff --git a/scripts/test.script b/scripts/test.script
new file mode 100644
index 0000000..a45f3f0
--- /dev/null
+++ b/scripts/test.script
@@ -0,0 +1,4 @@
+This file is for testing purposes only.
+
+It is used to test the functionality of drupal_get_filename(). See
+BootstrapGetFilenameTestCase::testDrupalGetFilename() for more information.
diff --git a/sites/README.txt b/sites/README.txt
new file mode 100644
index 0000000..9aecef2
--- /dev/null
+++ b/sites/README.txt
@@ -0,0 +1,19 @@
+This directory structure contains the settings and configuration files specific
+to your site or sites and is an integral part of multisite configuration.
+
+The sites/all/ subdirectory structure should be used to place your custom and
+downloaded extensions including modules, themes, and third party libraries.
+
+Downloaded installation profiles should be placed in the /profiles directory
+in the Drupal root.
+
+In multisite configuration, extensions found in the sites/all directory
+structure are available to all sites. Alternatively, the sites/your_site_name/
+subdirectory pattern may be used to restrict extensions to a specific
+site instance.
+
+See the respective README.txt files in sites/all/themes and sites/all/modules
+for additional information about obtaining and organizing extensions.
+
+See INSTALL.txt in the Drupal root for information about single-site
+installation or multisite configuration.
diff --git a/sites/all/libraries/README.txt b/sites/all/libraries/README.txt
new file mode 100644
index 0000000..de5d585
--- /dev/null
+++ b/sites/all/libraries/README.txt
@@ -0,0 +1,2 @@
+This directory should be used to place downloaded and custom libraries (such as
+JavaScript libraries) which are used by contributed or custom modules.
diff --git a/sites/all/modules/README.txt b/sites/all/modules/README.txt
new file mode 100644
index 0000000..b19849b
--- /dev/null
+++ b/sites/all/modules/README.txt
@@ -0,0 +1,37 @@
+Modules extend your site functionality beyond Drupal core.
+
+WHAT TO PLACE IN THIS DIRECTORY?
+--------------------------------
+
+Placing downloaded and custom modules in this directory separates downloaded and
+custom modules from Drupal core's modules. This allows Drupal core to be updated
+without overwriting these files.
+
+DOWNLOAD ADDITIONAL MODULES
+---------------------------
+
+Contributed modules from the Drupal community may be downloaded at
+https://www.drupal.org/project/project_module.
+
+ORGANIZING MODULES IN THIS DIRECTORY
+------------------------------------
+
+You may create subdirectories in this directory, to organize your added modules,
+without breaking the site. Some common subdirectories include "contrib" for
+contributed modules, and "custom" for custom modules. Note that if you move a
+module to a subdirectory after it has been enabled, you may need to clear the
+Drupal cache so it can be found. (Alternatively, you can disable the module
+before moving it and then re-enable it after the move.)
+
+MULTISITE CONFIGURATION
+-----------------------
+
+In multisite configurations, modules found in this directory are available to
+all sites. Alternatively, the sites/your_site_name/modules directory pattern
+may be used to restrict modules to a specific site instance.
+
+MORE INFORMATION
+----------------
+
+Refer to the "Developing for Drupal" section of the README.txt in the Drupal
+root directory for further information on extending Drupal with custom modules.
diff --git a/sites/all/modules/checklistapi/LICENSE.txt b/sites/all/modules/checklistapi/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/sites/all/modules/checklistapi/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/checklistapi/README.txt b/sites/all/modules/checklistapi/README.txt
new file mode 100644
index 0000000..20c149f
--- /dev/null
+++ b/sites/all/modules/checklistapi/README.txt
@@ -0,0 +1,47 @@
+
+CONTENTS OF THIS FILE
+---------------------
+
+ * Introduction
+ * Installation
+ * Implementation
+ * Drush
+
+
+INTRODUCTION
+------------
+
+Current Maintainer: Travis Carden
+
+Checklist API provides a simple interface for modules to create fillable,
+persistent checklists that track progress with completion times and users. See
+checklistapi_example.module for an example implementation.
+
+
+INSTALLATION
+------------
+
+Checklist API is installed in the usual way. See
+http://drupal.org/documentation/install/modules-themes/modules-7.
+
+
+IMPLEMENTATION
+--------------
+
+Checklists are declared as multidimensional arrays using
+hook_checklistapi_checklist_info(). They can be altered using
+hook_checklistapi_checklist_info_alter(). Checklist API handles creation of menu
+items and permissions. Progress details are saved in one Drupal variable per
+checklist. (Note: it is the responsibility of implementing modules to remove
+their own variables on hook_uninstall().)
+
+See checklistapi.api.php for more details.
+
+
+DRUSH
+-----
+
+Checklist API provides Drush commands. They require Drush 6 or later. To see the
+list of available commands, run `drush --filter=checklistapi`. For more about
+Drush, including installation instructions, visit
+https://github.com/drush-ops/drush.
diff --git a/sites/all/modules/checklistapi/checklistapi.admin.inc b/sites/all/modules/checklistapi/checklistapi.admin.inc
new file mode 100644
index 0000000..382e566
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.admin.inc
@@ -0,0 +1,56 @@
+ $definition) {
+ $checklist = checklistapi_checklist_load($id);
+ $row = array();
+ $row[] = array(
+ 'data' => ($checklist->userHasAccess()) ? l($checklist->title, $checklist->path) : drupal_placeholder($checklist->title),
+ 'title' => (!empty($checklist->description)) ? $checklist->description : '',
+ );
+ $row[] = t('@completed of @total (@percent%)', array(
+ '@completed' => $checklist->getNumberCompleted(),
+ '@total' => $checklist->getNumberOfItems(),
+ '@percent' => round($checklist->getPercentComplete()),
+ ));
+ $row[] = $checklist->getLastUpdatedDate();
+ $row[] = $checklist->getLastUpdatedUser();
+ $row[] = ($checklist->userHasAccess('edit') && $checklist->hasSavedProgress()) ? l(t('clear saved progress'), $checklist->path . '/clear', array(
+ 'query' => array('destination' => 'admin/reports/checklistapi'),
+ )) : '';
+ $rows[] = $row;
+ }
+
+ // Compile table.
+ $table = array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'empty' => t('No checklists available.'),
+ );
+
+ return theme('table', $table);
+}
diff --git a/sites/all/modules/checklistapi/checklistapi.api.php b/sites/all/modules/checklistapi/checklistapi.api.php
new file mode 100644
index 0000000..bc02a71
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.api.php
@@ -0,0 +1,133 @@
+ t('Example checklist'),
+ '#path' => 'example-checklist',
+ '#description' => t('An example checklist.'),
+ '#help' => t('This is an example checklist.
'),
+ 'example_group' => array(
+ '#title' => t('Example group'),
+ '#description' => t('Here are some example items.
'),
+ 'example_item_1' => array(
+ '#title' => t('Example item 1'),
+ 'example_link' => array(
+ '#text' => t('Example.com'),
+ '#path' => 'http://www.example.com/',
+ ),
+ ),
+ 'example_item_2' => array(
+ '#title' => t('Example item 2'),
+ ),
+ ),
+ );
+ return $definitions;
+}
+
+/**
+ * Alter checklist definitions.
+ *
+ * This hook is invoked by checklistapi_get_checklist_info(). The checklist
+ * definitions are passed in by reference. Additional checklists may be added,
+ * or existing checklists may be altered or removed.
+ *
+ * @param array $definitions
+ * The multidimensional array of checklist definitions returned by
+ * hook_checklistapi_checklist_info().
+ *
+ * For a working example, see checklistapi_example.module.
+ *
+ * @see checklistapi_get_checklist_info()
+ * @see hook_checklistapi_checklist_info()
+ */
+function hook_checklistapi_checklist_info_alter(array &$definitions) {
+ // Add an item.
+ $definitions['example_checklist']['example_group']['new_item'] = array(
+ 'title' => t('New item'),
+ );
+ // Add a group.
+ $definitions['example_checklist']['new_group'] = array(
+ '#title' => t('New group'),
+ );
+ // Move an item.
+ $definitions['example_checklist']['new_group']['example_item_1'] = $definitions['example_checklist']['example_group']['example_item_1'];
+ unset($definitions['example_checklist']['example_group']['example_item_1']);
+ // Remove an item.
+ unset($definitions['example_checklist']['example_group']['example_item_2']);
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/sites/all/modules/checklistapi/checklistapi.css b/sites/all/modules/checklistapi/checklistapi.css
new file mode 100644
index 0000000..b7cc6ea
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.css
@@ -0,0 +1,25 @@
+
+#checklistapi-checklist-form div.description p {
+ margin: .5em 0;
+}
+#checklistapi-checklist-form span.completion-details {
+ font-style: italic;
+ opacity: 0.66;
+}
+#checklistapi-checklist-form.compact-mode div.description p {
+ display: none;
+}
+
+/**
+ * Progress bar.
+ *
+ * @see system.theme.css
+ */
+#checklistapi-checklist-form .progress {
+ font-weight: normal;
+ margin-bottom: 0.5em;
+}
+#checklistapi-checklist-form .progress .bar,
+#checklistapi-checklist-form .progress .filled {
+ background-image: none;
+}
diff --git a/sites/all/modules/checklistapi/checklistapi.drush.inc b/sites/all/modules/checklistapi/checklistapi.drush.inc
new file mode 100644
index 0000000..4b2fe45
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.drush.inc
@@ -0,0 +1,157 @@
+ array('capi-list', 'capil'),
+ 'description' => 'Get an overview of your installed checklists with progress details.',
+ );
+ $items['checklistapi-info'] = array(
+ 'aliases' => array('capi-info', 'capii'),
+ 'description' => 'Show detailed info for a given checklist.',
+ 'arguments' => array(
+ 'checklist' => 'The checklist machine name, e.g., "example_checklist".',
+ ),
+ 'required-arguments' => TRUE,
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Implements hook_drush_help().
+ */
+function checklistapi_drush_help($section) {
+ switch ($section) {
+ case 'meta:checklistapi:title':
+ return dt('Checklist API commands');
+ }
+}
+
+/**
+ * Drush callback for checklist listing.
+ */
+function drush_checklistapi_list() {
+ $definitions = checklistapi_get_checklist_info();
+
+ if (empty($definitions)) {
+ return drush_print(dt('No checklists available.'));
+ }
+
+ // Build table rows.
+ $rows = array();
+ // The first row is the table header.
+ $rows[] = array(
+ dt('Checklist'),
+ dt('Progress'),
+ dt('Last updated'),
+ dt('Last updated by'),
+ );
+ foreach ($definitions as $id => $definition) {
+ $checklist = checklistapi_checklist_load($id);
+ $row = array();
+ $row[] = dt('!title (!id)', array(
+ '!title' => strip_tags($checklist->title),
+ '!id' => $id,
+ ));
+ $row[] = dt('@completed of @total (@percent%)', array(
+ '@completed' => $checklist->getNumberCompleted(),
+ '@total' => $checklist->getNumberOfItems(),
+ '@percent' => round($checklist->getPercentComplete()),
+ ));
+ $row[] = $checklist->getLastUpdatedDate();
+ $row[] = strip_tags($checklist->getLastUpdatedUser());
+ $rows[] = $row;
+ }
+
+ return drush_format_table($rows, TRUE);
+}
+
+/**
+ * Implements drush_hook_COMMAND_validate().
+ *
+ * @see drush_checklistapi_info()
+ */
+function drush_checklistapi_info_validate() {
+ $arguments = drush_get_arguments();
+ $id = $arguments[1];
+ $checklist = checklistapi_checklist_load($id);
+
+ // Make sure the given checklist exists.
+ if (!$checklist) {
+ drush_set_error('', dt('No such checklist "!id".', array(
+ '!id' => $id,
+ )));
+ }
+
+ drush_set_context('checklist', $checklist);
+}
+
+/**
+ * Drush callback for checklist info.
+ */
+function drush_checklistapi_info() {
+ $checklist = drush_get_context('checklist');
+
+ // Print the help.
+ if (!empty($checklist->help)) {
+ drush_print(strip_tags($checklist->help));
+ }
+
+ // Print last updated and progress details.
+ if ($checklist->hasSavedProgress()) {
+ drush_print('');
+ drush_print(dt('Last updated @date by !user', array(
+ '@date' => $checklist->getLastUpdatedDate(),
+ '!user' => strip_tags($checklist->getLastUpdatedUser()),
+ )));
+ drush_print(dt('@completed of @total (@percent%) complete', array(
+ '@completed' => $checklist->getNumberCompleted(),
+ '@total' => $checklist->getNumberOfItems(),
+ '@percent' => round($checklist->getPercentComplete()),
+ )));
+ }
+
+ // Loop through groups.
+ $groups = $checklist->items;
+ foreach (element_children($groups) as $group_key) {
+ $group = &$groups[$group_key];
+
+ // Print group title.
+ drush_print('');
+ drush_print(strip_tags($group['#title']) . ':');
+
+ // Loop through items.
+ foreach (element_children($group) as $item_key) {
+ $item = &$group[$item_key];
+ $saved_item = !empty($checklist->savedProgress[$item_key]) ? $checklist->savedProgress[$item_key] : 0;
+ // Build title.
+ $title = strip_tags($item['#title']);
+ if ($saved_item) {
+ // Append completion details.
+ $user = user_load($saved_item['#uid']);
+ $title .= t(' - Completed @time by !user', array(
+ '@time' => format_date($saved_item['#completed'], 'short'),
+ '!user' => strip_tags($user->name),
+ ));
+ }
+ // Print the list item.
+ drush_print(dt(' [!x] !title', array(
+ '!x' => ($saved_item) ? 'x' : ' ',
+ '!title' => $title,
+ )));
+ }
+ }
+}
diff --git a/sites/all/modules/checklistapi/checklistapi.info b/sites/all/modules/checklistapi/checklistapi.info
new file mode 100644
index 0000000..0517f35
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.info
@@ -0,0 +1,14 @@
+name = Checklist API
+description = Provides an API for creating fillable, persistent checklists.
+core = 7.x
+package = Other
+files[] = lib/Drupal/checklistapi/ChecklistapiChecklist.php
+files[] = tests/checklistapi.test
+configure = admin/reports/checklistapi
+
+; Information added by Drupal.org packaging script on 2014-08-25
+version = "7.x-1.2"
+core = "7.x"
+project = "checklistapi"
+datestamp = "1409004834"
+
diff --git a/sites/all/modules/checklistapi/checklistapi.install b/sites/all/modules/checklistapi/checklistapi.install
new file mode 100644
index 0000000..a42f1b0
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.install
@@ -0,0 +1,17 @@
+condition('name', db_like('checklistapi_') . '%', 'LIKE')
+ ->execute();
+ cache_clear_all('variables', 'cache_bootstrap');
+}
diff --git a/sites/all/modules/checklistapi/checklistapi.js b/sites/all/modules/checklistapi/checklistapi.js
new file mode 100644
index 0000000..cbc1665
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.js
@@ -0,0 +1,87 @@
+(function ($) {
+ "use strict";
+
+ /**
+ * Updates the progress bar as checkboxes are changed.
+ */
+ Drupal.behaviors.checklistapiUpdateProgressBar = {
+ attach: function (context) {
+ var total_items = $(':checkbox.checklistapi-item', context).size(),
+ progress_bar = $('#checklistapi-checklist-form .progress .bar .filled', context),
+ progress_percentage = $('#checklistapi-checklist-form .progress .percentage', context);
+ $(':checkbox.checklistapi-item', context).change(function () {
+ var num_items_checked = $(':checkbox.checklistapi-item:checked', context).size(),
+ percent_complete = Math.round(num_items_checked / total_items * 100),
+ args = {};
+ progress_bar.css('width', percent_complete + '%');
+ args['@complete'] = num_items_checked;
+ args['@total'] = total_items;
+ args['@percent'] = percent_complete;
+ progress_percentage.html(Drupal.t('@complete of @total (@percent%)', args));
+ });
+ }
+ };
+
+ /**
+ * Provides the summary information for the checklist form vertical tabs.
+ */
+ Drupal.behaviors.checklistapiFieldsetSummaries = {
+ attach: function (context) {
+ $('#checklistapi-checklist-form .vertical-tabs-panes > fieldset', context).drupalSetSummary(function (context) {
+ var total = $(':checkbox.checklistapi-item', context).size(),
+ args = {};
+ if (total) {
+ args['@complete'] = $(':checkbox.checklistapi-item:checked', context).size();
+ args['@total'] = total;
+ args['@percent'] = Math.round(args['@complete'] / args['@total'] * 100);
+ return Drupal.t('@complete of @total (@percent%)', args);
+ }
+ });
+ }
+ };
+
+ /**
+ * Adds dynamic item descriptions toggling.
+ */
+ Drupal.behaviors.checklistapiCompactModeLink = {
+ attach: function (context) {
+ $('#checklistapi-checklist-form .compact-link a', context).click(function () {
+ $(this).closest('#checklistapi-checklist-form').toggleClass('compact-mode');
+ var is_compact_mode = $(this).closest('#checklistapi-checklist-form').hasClass('compact-mode');
+ $(this)
+ .text(is_compact_mode ? Drupal.t('Show item descriptions') : Drupal.t('Hide item descriptions'))
+ .attr('title', is_compact_mode ? Drupal.t('Expand layout to include item descriptions.') : Drupal.t('Compress layout by hiding item descriptions.'));
+ document.cookie = 'Drupal.visitor.checklistapi_compact_mode=' + (is_compact_mode ? 1 : 0);
+ return false;
+ });
+ }
+ };
+
+ /**
+ * Prompts the user if they try to leave the page with unsaved changes.
+ *
+ * Note: Auto-checked items are not considered unsaved changes for the purpose
+ * of this feature.
+ */
+ Drupal.behaviors.checklistapiPromptBeforeLeaving = {
+ getFormState: function () {
+ return $('#checklistapi-checklist-form :checkbox.checklistapi-item').serializeArray().toString();
+ },
+ attach: function () {
+ var beginningState = this.getFormState();
+ $(window).bind('beforeunload', function () {
+ var endingState = Drupal.behaviors.checklistapiPromptBeforeLeaving.getFormState();
+ if (beginningState !== endingState) {
+ return Drupal.t('Your changes will be lost if you leave the page without saving.');
+ }
+ });
+ $('#checklistapi-checklist-form').submit(function () {
+ $(window).unbind('beforeunload');
+ });
+ $('#checklistapi-checklist-form .clear-saved-progress').click(function () {
+ $(window).unbind('beforeunload');
+ });
+ }
+ };
+
+})(jQuery);
diff --git a/sites/all/modules/checklistapi/checklistapi.module b/sites/all/modules/checklistapi/checklistapi.module
new file mode 100644
index 0000000..c229177
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.module
@@ -0,0 +1,297 @@
+ $operation,
+ )));
+ }
+
+ $access['view'] = user_access('view any checklistapi checklist') || user_access('view ' . $id . ' checklistapi checklist');
+ $access['edit'] = user_access('edit any checklistapi checklist') || user_access('edit ' . $id . ' checklistapi checklist');
+ $access['any'] = $access['view'] || $access['edit'];
+ return $access[$operation];
+}
+
+/**
+ * Loads a checklist object.
+ *
+ * @param string $id
+ * The checklist ID.
+ *
+ * @return ChecklistapiChecklist|false
+ * A fully-loaded checklist object, or FALSE if the checklist is not found.
+ */
+function checklistapi_checklist_load($id) {
+ $definition = checklistapi_get_checklist_info($id);
+ return ($definition) ? new ChecklistapiChecklist($definition) : FALSE;
+}
+
+/**
+ * Gets checklist definitions.
+ *
+ * @param string $id
+ * (optional) A checklist ID. Defaults to NULL.
+ *
+ * @return array|false
+ * The definition of the specified checklist, or FALSE if no such checklist
+ * exists, or an array of all checklist definitions if none is specified.
+ */
+function checklistapi_get_checklist_info($id = NULL) {
+ $definitions = &drupal_static(__FUNCTION__);
+ if (!isset($definitions)) {
+ // Get definitions.
+ $definitions = module_invoke_all('checklistapi_checklist_info');
+ $definitions = checklistapi_sort_array($definitions);
+ // Let other modules alter them.
+ drupal_alter('checklistapi_checklist_info', $definitions);
+ $definitions = checklistapi_sort_array($definitions);
+ // Inject checklist IDs.
+ foreach ($definitions as $key => $value) {
+ $definitions[$key] = array('#id' => $key) + $definitions[$key];
+ }
+ }
+ if (!empty($id)) {
+ return (!empty($definitions[$id])) ? $definitions[$id] : FALSE;
+ }
+ return $definitions;
+}
+
+/**
+ * Implements hook_help().
+ */
+function checklistapi_help($path, $arg) {
+ foreach (checklistapi_get_checklist_info() as $definition) {
+ if ($definition['#path'] == $path && !empty($definition['#help'])) {
+ return $definition['#help'];
+ }
+ }
+}
+
+/**
+ * Implements hook_init().
+ */
+function checklistapi_init() {
+ // Disable page caching on all Checklist API module paths.
+ $module_paths = array_keys(checklistapi_menu());
+ if (in_array(current_path(), $module_paths)) {
+ drupal_page_is_cacheable(FALSE);
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function checklistapi_menu() {
+ $items = array();
+
+ // Checklists report.
+ $items['admin/reports/checklistapi'] = array(
+ 'title' => 'Checklists',
+ 'page callback' => 'checklistapi_report_form',
+ 'access arguments' => array('view checklistapi checklists report'),
+ 'description' => 'Get an overview of your installed checklists with progress details.',
+ 'file' => 'checklistapi.admin.inc',
+ );
+
+ // Individual checklists.
+ foreach (checklistapi_get_checklist_info() as $id => $definition) {
+ if (empty($definition['#path']) || empty($definition['#title'])) {
+ continue;
+ }
+
+ // View/edit checklist.
+ $items[$definition['#path']] = array(
+ 'title' => $definition['#title'],
+ 'description' => (!empty($definition['#description'])) ? $definition['#description'] : '',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('checklistapi_checklist_form', $id),
+ 'access callback' => 'checklistapi_checklist_access',
+ 'access arguments' => array($id),
+ 'file' => 'checklistapi.pages.inc',
+ );
+ if (!empty($definition['#menu_name'])) {
+ $items[$definition['#path']]['menu_name'] = $definition['#menu_name'];
+ }
+
+ // Clear saved progress.
+ $items[$definition['#path'] . '/clear'] = array(
+ 'title' => 'Clear',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('checklistapi_checklist_clear_confirm', $id),
+ 'access callback' => 'checklistapi_checklist_access',
+ 'access arguments' => array($id, 'edit'),
+ 'file' => 'checklistapi.pages.inc',
+ 'type' => MENU_CALLBACK,
+ );
+
+ // Toggle compact mode.
+ $items[$definition['#path'] . '/compact'] = array(
+ 'title' => 'Compact mode',
+ 'page callback' => 'checklistapi_compact_page',
+ 'access callback' => 'checklistapi_checklist_access',
+ 'access arguments' => array($id),
+ 'file' => 'checklistapi.pages.inc',
+ 'type' => MENU_CALLBACK,
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function checklistapi_permission() {
+ $perms = array();
+
+ // Universal permissions.
+ $perms['view checklistapi checklists report'] = array(
+ 'title' => t(
+ 'View the !name report',
+ array('!name' => (user_access('view checklistapi checklists report')) ? l(t('Checklists'), 'admin/reports/checklistapi') : drupal_placeholder('Checklists'))
+ ),
+ );
+ $perms['view any checklistapi checklist'] = array(
+ 'title' => t('View any checklist'),
+ 'description' => $view_checklist_perm_description = t('Read-only access: View list items and saved progress.'),
+ );
+ $perms['edit any checklistapi checklist'] = array(
+ 'title' => t('Edit any checklist'),
+ 'description' => $edit_checklist_perm_description = t('Check and uncheck list items and save changes, or clear saved progress.'),
+ );
+
+ // Per checklist permissions.
+ foreach (checklistapi_get_checklist_info() as $id => $definition) {
+ if (empty($id)) {
+ continue;
+ }
+ $perms['view ' . $id . ' checklistapi checklist'] = array(
+ 'title' => t(
+ 'View the !name checklist',
+ array('!name' => (checklistapi_checklist_access($id)) ? l($definition['#title'], $definition['#path']) : drupal_placeholder($definition['#title']))
+ ),
+ 'description' => $view_checklist_perm_description,
+ );
+ $perms['edit ' . $id . ' checklistapi checklist'] = array(
+ 'title' => t(
+ 'Edit the !name checklist',
+ array('!name' => (checklistapi_checklist_access($id)) ? l($definition['#title'], $definition['#path']) : drupal_placeholder($definition['#title']))
+ ),
+ 'description' => $edit_checklist_perm_description,
+ );
+ }
+
+ return $perms;
+}
+
+/**
+ * Recursively sorts array elements by #weight.
+ *
+ * @param array $array
+ * A nested array of elements and properties, such as the checklist
+ * definitions returned by hook_checklistapi_checklist_info().
+ *
+ * @return array
+ * The input array sorted recursively by #weight.
+ *
+ * @see checklistapi_get_checklist_info()
+ */
+function checklistapi_sort_array(array $array) {
+ $child_keys = element_children($array);
+
+ if (!count($child_keys)) {
+ // No children to sort.
+ return $array;
+ }
+
+ $incrementer = 0;
+ $children = array();
+ foreach ($child_keys as $key) {
+ // Move child to a temporary array for sorting.
+ $children[$key] = $array[$key];
+ unset($array[$key]);
+ // Supply a default weight if missing or invalid.
+ if (empty($children[$key]['#weight']) || !is_numeric($children[$key]['#weight'])) {
+ $children[$key]['#weight'] = 0;
+ }
+ // Increase each weight incrementally to preserve the original order when
+ // not overridden. This accounts for undefined behavior in PHP's uasort()
+ // function when its comparison callback finds two values equal.
+ $children[$key]['#weight'] += ($incrementer++ / 1000);
+ // Descend into child.
+ $children[$key] = checklistapi_sort_array($children[$key]);
+ }
+ // Sort by weight.
+ uasort($children, 'element_sort');
+ // Remove incremental weight hack.
+ foreach ($children as $key => $child) {
+ $children[$key]['#weight'] = floor($children[$key]['#weight']);
+ }
+ // Put children back in the main array.
+ $array += $children;
+
+ return $array;
+}
+
+/**
+ * Converts a string to lowerCamel case, suitably for a class property name.
+ *
+ * @param string $string
+ * The input string.
+ *
+ * @return string
+ * The input string converted to camelCase.
+ */
+function checklistapi_strtolowercamel($string) {
+ $string = str_replace('_', ' ', $string);
+ $string = ucwords($string);
+ $string = str_replace(' ', '', $string);
+ // Lowercase first character. lcfirst($string) would be nicer, but let's not
+ // create a dependency on PHP 5.3 just for that.
+ $string[0] = strtolower($string[0]);
+ return $string;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function checklistapi_theme() {
+ return array(
+ 'checklistapi_compact_link' => array(
+ 'file' => 'checklistapi.pages.inc',
+ ),
+ 'checklistapi_progress_bar' => array(
+ 'path' => drupal_get_path('module', 'checklistapi') . '/templates',
+ 'template' => 'checklistapi-progress-bar',
+ 'variables' => array(
+ 'message' => ' ',
+ 'number_complete' => 0,
+ 'number_of_items' => 0,
+ 'percent_complete' => 0,
+ ),
+ ),
+ );
+}
diff --git a/sites/all/modules/checklistapi/checklistapi.pages.inc b/sites/all/modules/checklistapi/checklistapi.pages.inc
new file mode 100644
index 0000000..9b832f3
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi.pages.inc
@@ -0,0 +1,258 @@
+ $checklist->title,
+ ));
+ $description = t('All progress details will be erased. This action cannot be undone.');
+ $yes = t('Clear');
+ $no = t('Cancel');
+ return confirm_form($form, $question, $checklist->path, $description, $yes, $no);
+}
+
+/**
+ * Form submission handler for checklistapi_checklist_clear_confirm().
+ */
+function checklistapi_checklist_clear_confirm_submit($form, &$form_state) {
+ // If user confirmed, clear saved progress.
+ if ($form_state['values']['confirm']) {
+ $form['#checklist']->clearSavedProgress();
+ }
+
+ // Redirect back to checklist.
+ $form_state['redirect'] = $form['#checklist']->path;
+}
+
+/**
+ * Page callback: Form constructor for the checklist form.
+ *
+ * @param string $id
+ * The checklist ID.
+ *
+ * @see checklistapi_checklist_form_submit()
+ * @see checklistapi_menu()
+ *
+ * @ingroup forms
+ */
+function checklistapi_checklist_form($form, &$form_state, $id) {
+ $form['#checklist'] = $checklist = checklistapi_checklist_load($id);
+
+ $form['progress_bar'] = array(
+ '#type' => 'markup',
+ '#markup' => theme('checklistapi_progress_bar', array(
+ 'message' => ($checklist->hasSavedProgress()) ? t('Last updated @date by !user', array(
+ '@date' => $checklist->getLastUpdatedDate(),
+ '!user' => $checklist->getLastUpdatedUser(),
+ )) : ' ',
+ 'number_complete' => $checklist->getNumberCompleted(),
+ 'number_of_items' => $checklist->getNumberOfItems(),
+ 'percent_complete' => round($checklist->getPercentComplete()),
+ )),
+ );
+ if (checklistapi_compact_mode()) {
+ $form['#attributes']['class'] = array('compact-mode');
+ }
+ $form['compact_mode_link'] = array(
+ '#markup' => theme('checklistapi_compact_link'),
+ );
+
+ $form['checklistapi'] = array(
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'checklistapi') . '/checklistapi.css'),
+ 'js' => array(drupal_get_path('module', 'checklistapi') . '/checklistapi.js'),
+ ),
+ '#tree' => TRUE,
+ '#type' => 'vertical_tabs',
+ );
+
+ // Loop through groups.
+ $num_autochecked_items = 0;
+ $groups = $checklist->items;
+ foreach (element_children($groups) as $group_key) {
+ $group = &$groups[$group_key];
+ $form['checklistapi'][$group_key] = array(
+ '#title' => filter_xss($group['#title']),
+ '#type' => 'fieldset',
+ );
+ if (!empty($group['#description'])) {
+ $form['checklistapi'][$group_key]['#description'] = filter_xss_admin($group['#description']);
+ }
+
+ // Loop through items.
+ foreach (element_children($group) as $item_key) {
+ $item = &$group[$item_key];
+ $saved_item = !empty($checklist->savedProgress[$item_key]) ? $checklist->savedProgress[$item_key] : 0;
+ // Build title.
+ $title = filter_xss($item['#title']);
+ if ($saved_item) {
+ // Append completion details.
+ $user = user_load($saved_item['#uid']);
+ $title .= t(
+ ' - Completed @time by !user',
+ array(
+ '@time' => format_date($saved_item['#completed'], 'short'),
+ '!user' => theme('username', array('account' => $user)),
+ )
+ );
+ }
+ // Set default value.
+ $default_value = FALSE;
+ if ($saved_item) {
+ $default_value = TRUE;
+ }
+ elseif (!empty($item['#default_value'])) {
+ if ($default_value = $item['#default_value']) {
+ $num_autochecked_items++;
+ }
+ }
+ // Get description.
+ $description = (isset($item['#description'])) ? '' . filter_xss_admin($item['#description']) . '
' : '';
+ // Append links.
+ $links = array();
+ foreach (element_children($item) as $link_key) {
+ $link = &$item[$link_key];
+ $options = (!empty($link['#options']) && is_array($link['#options'])) ? $link['#options'] : array();
+ $links[] = l($link['#text'], $link['#path'], $options);
+ }
+ if (count($links)) {
+ $description .= '' . implode(' | ', $links) . '
';
+ }
+ // Compile the list item.
+ $form['checklistapi'][$group_key][$item_key] = array(
+ '#attributes' => array('class' => array('checklistapi-item')),
+ '#default_value' => $default_value,
+ '#description' => filter_xss_admin($description),
+ '#disabled' => !($user_has_edit_access = $checklist->userHasAccess('edit')),
+ '#title' => filter_xss_admin($title),
+ '#type' => 'checkbox',
+ );
+ }
+ }
+
+ $form['actions'] = array(
+ '#access' => $user_has_edit_access,
+ '#type' => 'actions',
+ '#weight' => 100,
+ 'save' => array(
+ '#submit' => array('checklistapi_checklist_form_submit'),
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ ),
+ 'clear' => array(
+ '#access' => $checklist->hasSavedProgress(),
+ '#attributes' => array('class' => array('clear-saved-progress')),
+ '#href' => $checklist->path . '/clear',
+ '#title' => t('Clear saved progress'),
+ '#type' => 'link',
+ ),
+ );
+
+ // Alert the user of autochecked items. Only set the message on GET requests
+ // to prevent it from reappearing after saving the form. (Testing the request
+ // method may not be the "correct" way to accomplish this.)
+ if ($num_autochecked_items && $_SERVER['REQUEST_METHOD'] == 'GET') {
+ $args = array(
+ '%checklist' => $checklist->title,
+ '@num' => $num_autochecked_items,
+ );
+ $message = format_plural(
+ $num_autochecked_items,
+ t('%checklist found 1 unchecked item that was already completed and checked it for you. Save the form to record the change.', $args),
+ t('%checklist found @num unchecked items that were already completed and checked them for you. Save the form to record the changes.', $args)
+ );
+ drupal_set_message($message, 'status');
+ }
+
+ return $form;
+}
+
+/**
+ * Form submission handler for checklistapi_checklist_form().
+ */
+function checklistapi_checklist_form_submit($form, &$form_state) {
+ $form['#checklist']->saveProgress($form_state['values']['checklistapi']);
+}
+
+/**
+ * Determines whether the current user is in compact mode.
+ *
+ * Compact mode shows checklist forms with less description text.
+ *
+ * Whether the user is in compact mode is determined by a cookie, which is set
+ * for the user by checklistapi_compact_page().
+ *
+ * If the user does not have the cookie, the default value is given by the
+ * system variable 'checklistapi_compact_mode', which itself defaults to FALSE.
+ * This does not have a user interface to set it: it is a hidden variable which
+ * can be set in the settings.php file.
+ *
+ * @return bool
+ * TRUE when in compact mode, or FALSE when in expanded mode.
+ */
+function checklistapi_compact_mode() {
+ return isset($_COOKIE['Drupal_visitor_checklistapi_compact_mode']) ? $_COOKIE['Drupal_visitor_checklistapi_compact_mode'] : variable_get('checklistapi_compact_mode', FALSE);
+}
+
+/**
+ * Menu callback: Sets whether the admin menu is in compact mode or not.
+ *
+ * @param string $mode
+ * (optional) The mode to set compact mode to. Accepted values are "on" and
+ * "off". Defaults to "off".
+ */
+function checklistapi_compact_page($mode = 'off') {
+ user_cookie_save(array('checklistapi_compact_mode' => ($mode == 'on')));
+ drupal_goto();
+}
+
+/**
+ * Returns HTML for a link to show or hide inline item descriptions.
+ *
+ * @ingroup themeable
+ */
+function theme_checklistapi_compact_link() {
+ $output = '';
+ if (checklistapi_compact_mode()) {
+ $output .= l(
+ t('Show item descriptions'),
+ current_path() . '/compact/off',
+ array(
+ 'attributes' => array(
+ 'title' => t('Expand layout to include item descriptions.'),
+ ),
+ 'query' => drupal_get_destination(),
+ )
+ );
+ }
+ else {
+ $output .= l(
+ t('Hide item descriptions'),
+ current_path() . '/compact/on',
+ array(
+ 'attributes' => array(
+ 'title' => t('Compress layout by hiding item descriptions.'),
+ ),
+ 'query' => drupal_get_destination(),
+ )
+ );
+ }
+ $output .= '
';
+ return $output;
+}
diff --git a/sites/all/modules/checklistapi/checklistapi_example/checklistapi_example.info b/sites/all/modules/checklistapi/checklistapi_example/checklistapi_example.info
new file mode 100644
index 0000000..d3f3ea8
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi_example/checklistapi_example.info
@@ -0,0 +1,13 @@
+name = Checklist API example
+description = Provides an example implementation of the Checklist API.
+core = 7.x
+package = Example modules
+dependencies[] = checklistapi
+configure = admin/config/development/checklistapi-example
+
+; Information added by Drupal.org packaging script on 2014-08-25
+version = "7.x-1.2"
+core = "7.x"
+project = "checklistapi"
+datestamp = "1409004834"
+
diff --git a/sites/all/modules/checklistapi/checklistapi_example/checklistapi_example.install b/sites/all/modules/checklistapi/checklistapi_example/checklistapi_example.install
new file mode 100644
index 0000000..cf4d6bb
--- /dev/null
+++ b/sites/all/modules/checklistapi/checklistapi_example/checklistapi_example.install
@@ -0,0 +1,15 @@
+ t('Checklist API example'),
+ '#path' => 'admin/config/development/checklistapi-example',
+ '#description' => t('An example implementation of the Checklist API.'),
+ '#help' => t('This checklist based on Dries Buytaert\'s Drupal learning curve is an example implementation of the Checklist API .
'),
+ 'i_suck' => array(
+ '#title' => t('I suck'),
+ '#description' => t('Gain these skills to pass the suck threshold and start being creative with Drupal.
'),
+ 'install_configure' => array(
+ '#title' => t('Installation and configuration of Drupal core'),
+ '#description' => t('Prepare for installation, run the installation script, and take the steps that should be done after the installation script has completed.'),
+ 'handbook_page' => array(
+ '#text' => t('Installation Guide'),
+ '#path' => 'http://drupal.org/documentation/install',
+ ),
+ ),
+ 'node_system' => array(
+ '#title' => t('Node system'),
+ '#description' => t('Perform a variety of operations on one or more nodes.'),
+ 'handbook_page' => array(
+ '#text' => t('Manage nodes'),
+ '#path' => 'http://drupal.org/node/306808',
+ ),
+ ),
+ 'block_system' => array(
+ '#title' => t('Block system'),
+ '#description' => t('Create blocks and adjust their appearance, shape, size and position.'),
+ 'handbook_page' => array(
+ '#text' => t('Working with blocks (content in regions)'),
+ '#path' => 'http://drupal.org/documentation/modules/block',
+ ),
+ ),
+ 'users' => array(
+ '#title' => t('Users, roles and permissions'),
+ '#description' => t('Create and manage users and access control.'),
+ 'handbook_page' => array(
+ '#text' => t('Managing users'),
+ '#path' => 'http://drupal.org/node/627158',
+ ),
+ ),
+ 'contrib' => array(
+ '#title' => t('Installing contributed themes and modules'),
+ '#description' => t('Customize Drupal to your tastes by adding modules and themes.'),
+ 'handbook_page' => array(
+ '#text' => t('Installing modules and themes'),
+ '#path' => 'http://drupal.org/documentation/install/modules-themes',
+ ),
+ ),
+ ),
+ 'i_get_by' => array(
+ '#title' => t('I get by'),
+ '#description' => t('Gain these skills to pass the passion threshold and start kicking butt with Drupal.
'),
+ 'upgrade_patch_monitor' => array(
+ '#title' => t('Upgrading, patching, (security) monitoring'),
+ 'handbook_page_upgrading' => array(
+ '#text' => t('Upgrading from previous versions'),
+ '#path' => 'http://drupal.org/upgrade',
+ ),
+ 'handbook_page_patching' => array(
+ '#text' => t('Applying patches'),
+ '#path' => 'http://drupal.org/patch/apply',
+ ),
+ 'security_advisories' => array(
+ '#text' => t('Security advisories'),
+ '#path' => 'http://drupal.org/security',
+ ),
+ 'handbook_page_monitoring' => array(
+ '#text' => t('Monitoring a site'),
+ '#path' => 'http://drupal.org/node/627162',
+ ),
+ ),
+ 'navigation_menus_taxonomy' => array(
+ '#title' => t('Navigation, menus, taxonomy'),
+ 'handbook_page_menus' => array(
+ '#text' => t('Working with Menus'),
+ '#path' => 'http://drupal.org/documentation/modules/menu',
+ ),
+ 'handbook_page_taxonomy' => array(
+ '#text' => t('Organizing content with taxonomy'),
+ '#path' => 'http://drupal.org/documentation/modules/taxonomy',
+ ),
+ ),
+ 'locale_i18n' => array(
+ '#title' => t('Locale and internationalization'),
+ 'handbook_page' => array(
+ '#text' => t('Multilingual Guide'),
+ '#path' => 'http://drupal.org/documentation/multilingual',
+ ),
+ ),
+ 'customize_front_page' => array(
+ '#title' => t('Drastically customize front page'),
+ 'handbook_page' => array(
+ '#text' => t('Totally customize the LOOK of your front page'),
+ '#path' => 'http://drupal.org/node/317461',
+ ),
+ ),
+ 'theme_modification' => array(
+ '#title' => t('Theme and template modifications'),
+ 'handbook_page' => array(
+ '#text' => t('Theming Guide'),
+ '#path' => 'http://drupal.org/documentation/theme',
+ ),
+ ),
+ ),
+ 'i_kick_butt' => array(
+ '#title' => t('I kick butt'),
+ 'contribute_docs_support' => array(
+ '#title' => t('Contributing documentation and support'),
+ 'handbook_page_docs' => array(
+ '#text' => t('Contribute to documentation'),
+ '#path' => 'http://drupal.org/contribute/documentation',
+ ),
+ 'handbook_page_support' => array(
+ '#text' => t('Provide online support'),
+ '#path' => 'http://drupal.org/contribute/support',
+ ),
+ ),
+ 'content_types_views' => array(
+ '#title' => t('Content types and views'),
+ 'handbook_page_content_types' => array(
+ '#text' => t('Working with nodes, content types and fields'),
+ '#path' => 'http://drupal.org/node/717120',
+ ),
+ 'handbook_page_views' => array(
+ '#text' => t('Working with Views'),
+ '#path' => 'http://drupal.org/documentation/modules/views',
+ ),
+ ),
+ 'actions_workflows' => array(
+ '#title' => t('Actions and workflows'),
+ 'handbook_page' => array(
+ '#text' => t('Actions and Workflows'),
+ '#path' => 'http://drupal.org/node/924538',
+ ),
+ ),
+ 'development' => array(
+ '#title' => t('Theme and module development'),
+ 'handbook_page_theming' => array(
+ '#text' => t('Theming Guide'),
+ '#path' => 'http://drupal.org/documentation/theme',
+ ),
+ 'handbook_page_development' => array(
+ '#text' => t('Develop for Drupal'),
+ '#path' => 'http://drupal.org/documentation/develop',
+ ),
+ ),
+ 'advanced_tasks' => array(
+ '#title' => t('jQuery, Form API, security audits, performance tuning'),
+ 'handbook_page_jquery' => array(
+ '#text' => t('JavaScript and jQuery'),
+ '#path' => 'http://drupal.org/node/171213',
+ ),
+ 'handbook_page_form_api' => array(
+ '#text' => t('Form API'),
+ '#path' => 'http://drupal.org/node/37775',
+ ),
+ 'handbook_page_security' => array(
+ '#text' => t('Securing your site'),
+ '#path' => 'http://drupal.org/security/secure-configuration',
+ ),
+ 'handbook_page_performance' => array(
+ '#text' => t('Managing site performance'),
+ '#path' => 'http://drupal.org/node/627252',
+ ),
+ ),
+ 'contribute_code' => array(
+ '#title' => t('Contributing code, designs and patches back to Drupal'),
+ 'handbook_page' => array(
+ '#text' => t('Contribute to development'),
+ '#path' => 'http://drupal.org/contribute/development',
+ ),
+ ),
+ 'professional' => array(
+ '#title' => t('Drupal consultant or working for a Drupal shop'),
+ ),
+ 'chx_or_unconed' => array(
+ '#title' => t(
+ "I'm a !chx or !UnConeD.",
+ array(
+ '!chx' => l(t('chx'), 'http://drupal.org/user/9446'),
+ '!UnConeD' => l(t('UnConeD'), 'http://drupal.org/user/10'),
+ )
+ ),
+ ),
+ ),
+ );
+ return $definitions;
+}
+
+/**
+ * Implements hook_checklistapi_checklist_info_alter().
+ *
+ * Alters the checklist from checklistapi_example_checklistapi_checklist_info()
+ * according to
+ * @link http://www.unleashedmind.com/files/drupal-learning-curve.png sun's modifications @endlink
+ * of
+ * @link http://buytaert.net/drupal-learning-curve Dries Buytaert's Drupal learning curve @endlink
+ * .
+ */
+function checklistapi_example_checklistapi_checklist_info_alter(&$definitions) {
+ $definitions['example_checklist']['#help'] = t('This checklist based on sun\'s modification of Dries Buytaert\'s Drupal learning curve is an example implementation of the Checklist API .
');
+ $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['#title'] = t('jQuery, Form API, theme and module development');
+ $definitions['example_checklist']['i_kick_butt']['advanced_tasks'] += $definitions['example_checklist']['i_kick_butt']['development'];
+ unset($definitions['example_checklist']['i_kick_butt']['development']);
+ $definitions['example_checklist']['i_kick_butt']['contribute_code']['#title'] = t('Contributing code, designs and patches back to Drupal contrib');
+ unset($definitions['example_checklist']['i_kick_butt']['chx_or_unconed']);
+ $definitions['example_checklist']['core_contributor'] = array(
+ '#title' => t("I'm a core contributor"),
+ 'contribute_core_code' => array(
+ '#title' => t('Contribute code and patches to Drupal core'),
+ 'handbook_page' => array(
+ '#text' => t('Core contribution mentoring (core office hours)'),
+ '#path' => 'http://drupal.org/core-office-hours',
+ ),
+ 'issue_queue' => array(
+ '#text' => t('Core issue queue'),
+ '#path' => 'http://drupal.org/project/issues/drupal',
+ ),
+ ),
+ 'unit_tests' => array(
+ '#title' => t('Write unit tests to get own patch committed.'),
+ 'handbook_page' => array(
+ '#text' => t('Unit Testing with Simpletest'),
+ '#path' => 'http://drupal.org/node/811254',
+ ),
+ ),
+ 'review_core_patches' => array(
+ '#title' => t("Review other people's core patches, understanding coding standards."),
+ 'pending_patches' => array(
+ '#text' => t('Pending patches'),
+ '#path' => 'http://drupal.org/project/issues/search/drupal?status[]=8&status[]=13&status[]=14',
+ ),
+ 'handbook_page' => array(
+ '#text' => t('Coding standards'),
+ '#path' => 'http://drupal.org/coding-standards',
+ ),
+ ),
+ 'security_performance' => array(
+ '#title' => t('Security audits, performance tuning.'),
+ 'handbook_page_security' => $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_security'],
+ 'handbook_page_performance' => $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_performance'],
+ ),
+ );
+ unset($definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_security']);
+ unset($definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_performance']);
+ $definitions['example_checklist']['core_maintainer'] = array(
+ '#title' => t("I'm trustworthy for core maintainership"),
+ 'add_sub_system' => array(
+ '#title' => t('Rewrite or add a Drupal core sub-system.'),
+ ),
+ 'sub_system_maintainer' => array(
+ '#title' => t('Sub-system maintainer.'),
+ ),
+ 'core_branch_maintainer' => array(
+ '#title' => t('Core branch maintainer'),
+ ),
+ );
+ $definitions['example_checklist']['know_every_bit_of_core'] = array(
+ '#title' => t('I know every bit of core'),
+ 'im_chx' => array(
+ '#title' => t(
+ "I'm !chx.",
+ array('!chx' => l(t('chx'), 'http://drupal.org/user/9446'))
+ ),
+ ),
+ );
+ $definitions['example_checklist']['understand_all_core_patch_implications'] = array(
+ '#title' => t('I understand all implications of a core patch'),
+ 'im_chuck_norris' => array(
+ '#title' => t("I'm Chuck Norris."),
+ ),
+ );
+}
diff --git a/sites/all/modules/checklistapi/lib/Drupal/checklistapi/ChecklistapiChecklist.php b/sites/all/modules/checklistapi/lib/Drupal/checklistapi/ChecklistapiChecklist.php
new file mode 100644
index 0000000..6a06b33
--- /dev/null
+++ b/sites/all/modules/checklistapi/lib/Drupal/checklistapi/ChecklistapiChecklist.php
@@ -0,0 +1,283 @@
+numberOfItems += count(element_children($definition[$group_key]));
+ $this->items[$group_key] = $definition[$group_key];
+ unset($definition[$group_key]);
+ }
+ foreach ($definition as $property_key => $value) {
+ $property_name = checklistapi_strtolowercamel(drupal_substr($property_key, 1));
+ $this->$property_name = $value;
+ }
+ $this->savedProgress = variable_get($this->getSavedProgressVariableName(), array());
+ }
+
+ /**
+ * Gets the total number of completed items.
+ *
+ * @return int
+ * The number of completed items.
+ */
+ public function getNumberCompleted() {
+ return (!empty($this->savedProgress['#completed_items'])) ? $this->savedProgress['#completed_items'] : 0;
+ }
+
+ /**
+ * Gets the total number of items.
+ *
+ * @return int
+ * The number of items.
+ */
+ public function getNumberOfItems() {
+ return $this->numberOfItems;
+ }
+
+ /**
+ * Gets the name of the last user to update the checklist.
+ *
+ * @return string
+ * The themed name of the last user to update the checklist, or 'n/a' if
+ * there is no record of such a user.
+ */
+ public function getLastUpdatedUser() {
+ if (isset($this->savedProgress['#changed_by'])) {
+ $last_updated_user = user_load($this->savedProgress['#changed_by']);
+ return theme('username', array('account' => $last_updated_user));
+ }
+ else {
+ return t('n/a');
+ }
+ }
+
+ /**
+ * Gets the last updated date.
+ *
+ * @return string
+ * The last updated date formatted with format_date(), or 'n/a' if there is
+ * no saved progress.
+ */
+ public function getLastUpdatedDate() {
+ return (!empty($this->savedProgress['#changed'])) ? format_date($this->savedProgress['#changed']) : t('n/a');
+ }
+
+ /**
+ * Gets the percentage of items complete.
+ *
+ * @return float
+ * The percent complete.
+ */
+ public function getPercentComplete() {
+ if ($this->getNumberOfItems() == 0) {
+ return 100;
+ }
+ return ($this->getNumberCompleted() / $this->getNumberOfItems()) * 100;
+ }
+
+ /**
+ * Clears the saved progress for the checklist.
+ *
+ * Deletes the Drupal variable containing the checklist's saved progress.
+ */
+ public function clearSavedProgress() {
+ variable_del($this->getSavedProgressVariableName());
+ drupal_set_message(t('%title saved progress has been cleared.', array(
+ '%title' => $this->title,
+ )));
+ }
+
+ /**
+ * Gets the name of the Drupal variable for the checklist's saved progress.
+ *
+ * @return string
+ * The Drupal variable name.
+ */
+ public function getSavedProgressVariableName() {
+ return "checklistapi_checklist_{$this->id}";
+ }
+
+ /**
+ * Determines whether the checklist has saved progress.
+ *
+ * @return bool
+ * TRUE if the checklist has saved progress, or FALSE if it doesn't.
+ */
+ public function hasSavedProgress() {
+ return (bool) variable_get($this->getSavedProgressVariableName(), FALSE);
+ }
+
+ /**
+ * Saves checklist progress to a Drupal variable.
+ *
+ * @param array $values
+ * A multidimensional array of form state checklist values.
+ *
+ * @see checklistapi_checklist_form_submit()
+ */
+ public function saveProgress(array $values) {
+ global $user;
+ $time = time();
+ $num_changed_items = 0;
+ $progress = array(
+ '#changed' => $time,
+ '#changed_by' => $user->uid,
+ '#completed_items' => 0,
+ );
+
+ // Loop through groups.
+ foreach ($values as $group_key => $group) {
+ if (!is_array($group)) {
+ continue;
+ }
+ // Loop through items.
+ foreach ($group as $item_key => $item) {
+ $definition = checklistapi_get_checklist_info($this->id);
+ if (!in_array($item_key, array_keys($definition[$group_key]))) {
+ // This item wasn't in the checklist definition. Don't include it with
+ // saved progress.
+ continue;
+ }
+ $old_item = (!empty($this->savedProgress[$item_key])) ? $this->savedProgress[$item_key] : 0;
+ if ($item == 1) {
+ // Item is checked.
+ $progress['#completed_items']++;
+ if ($old_item) {
+ // Item was previously checked. Use saved value.
+ $new_item = $old_item;
+ }
+ else {
+ // Item is newly checked. Set new value.
+ $new_item = array(
+ '#completed' => $time,
+ '#uid' => $user->uid,
+ );
+ $num_changed_items++;
+ }
+ }
+ else {
+ // Item is unchecked.
+ $new_item = 0;
+ if ($old_item) {
+ // Item was previously checked.
+ $num_changed_items++;
+ }
+ }
+ $progress[$item_key] = $new_item;
+ }
+ }
+
+ // Sort array elements alphabetically so changes to the order of items in
+ // checklist definitions over time don't affect the order of elements in the
+ // saved progress variable. This simplifies use with Strongarm.
+ ksort($progress);
+
+ variable_set($this->getSavedProgressVariableName(), $progress);
+ drupal_set_message(format_plural(
+ $num_changed_items,
+ '%title progress has been saved. 1 item changed.',
+ '%title progress has been saved. @count items changed.',
+ array('%title' => $this->title)
+ ));
+ }
+
+ /**
+ * Determines whether the current user has access to the checklist.
+ *
+ * @param string $operation
+ * The operation to test access for. Possible values are "view", "edit", and
+ * "any". Defaults to "any".
+ *
+ * @return bool
+ * Returns TRUE if the user has access, or FALSE if not.
+ */
+ public function userHasAccess($operation = 'any') {
+ return checklistapi_checklist_access($this->id, $operation);
+ }
+
+}
diff --git a/sites/all/modules/checklistapi/run-tests.sh b/sites/all/modules/checklistapi/run-tests.sh
new file mode 100644
index 0000000..ba2afac
--- /dev/null
+++ b/sites/all/modules/checklistapi/run-tests.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env sh
+
+# @file
+# This script runs automated tests for Checklist API module.
+
+# Usage: $ ./run-tests.sh [web-server-shell-user] (defaults to "www-data")
+# e.g., $ ./run-tests.sh
+# or $ ./run-tests.sh apache
+
+server_user=${1:-www-data}
+
+drush test-run ChecklistapiUnitTestCase,ChecklistapiWebTestCase sudo -u ${server_user}
diff --git a/sites/all/modules/checklistapi/templates/checklistapi-progress-bar.tpl.php b/sites/all/modules/checklistapi/templates/checklistapi-progress-bar.tpl.php
new file mode 100644
index 0000000..383dbf8
--- /dev/null
+++ b/sites/all/modules/checklistapi/templates/checklistapi-progress-bar.tpl.php
@@ -0,0 +1,22 @@
+
+
diff --git a/sites/all/modules/checklistapi/tests/checklistapi.test b/sites/all/modules/checklistapi/tests/checklistapi.test
new file mode 100644
index 0000000..fab9955
--- /dev/null
+++ b/sites/all/modules/checklistapi/tests/checklistapi.test
@@ -0,0 +1,141 @@
+ 'Unit tests',
+ 'description' => 'Test Checklist API classes and functions.',
+ 'group' => 'Checklist API',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ drupal_load('module', 'checklistapi');
+ drupal_load('module', 'checklistapi_test');
+ parent::setUp();
+ }
+
+ /**
+ * Test checklistapi_sort_array().
+ */
+ public function testChecklistapiSortArray() {
+ $checklistapi_test_definition = checklistapi_test_checklistapi_checklist_info();
+ $input = array_pop($checklistapi_test_definition);
+ $output = checklistapi_sort_array($input);
+ $this->assertEqual($output['group_two']['#weight'], 0, 'Supplied a default for omitted element weight.');
+ $this->assertEqual($output['group_three']['#weight'], 0, 'Supplied a default in place of invalid element weight.');
+ $this->assertEqual($output['group_one']['#weight'], -1, 'Retained a valid element weight.');
+ $this->assertEqual(
+ element_children($output),
+ array('group_one', 'group_two', 'group_three', 'group_four'),
+ 'Sorted elements by weight.'
+ );
+ $this->assertEqual(
+ element_children($output['group_one']['item_one']),
+ array('link_one', 'link_two', 'link_three'),
+ 'Recursed through element descendants.'
+ );
+ }
+
+ /**
+ * Test checklistapi_strtolowercamel().
+ */
+ public function testChecklistapiStrtolowercamel() {
+ $this->assertEqual(checklistapi_strtolowercamel('Abc def_ghi'), 'abcDefGhi', 'Converted string to lowerCamel case.');
+ }
+
+}
+
+/**
+ * Functional tests for Checklist API.
+ *
+ * @todo Add tests for vertical tabs progress indicators.
+ * @todo Add tests for saving and retrieving checklist progress.
+ * @todo Add tests for clearing saved progress.
+ */
+class ChecklistapiWebTestCase extends DrupalWebTestCase {
+ protected $privilegedUser;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Functional tests',
+ 'description' => 'Test the functionality of Checklist API.',
+ 'group' => 'Checklist API',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp('checklistapi_example');
+ $permissions = array('edit any checklistapi checklist', 'view checklistapi checklists report');
+ $this->privilegedUser = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->privilegedUser);
+ }
+
+ /**
+ * Test checklist access.
+ */
+ public function testAccessChecklist() {
+ $this->drupalGet('admin/config/development/checklistapi-example');
+ $this->assertResponse(200, 'Granted access to user with "edit any checklistapi checklist" permission.');
+
+ $permissions = array('edit example_checklist checklistapi checklist');
+ $semi_privileged_user = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($semi_privileged_user);
+ $this->drupalGet('admin/config/development/checklistapi-example');
+ $this->assertResponse(200, 'Granted access to user with checklist-specific permission.');
+
+ $this->drupalLogout();
+ $this->drupalGet('admin/config/development/checklistapi-example');
+ $this->assertResponse(403, 'Denied access to nonprivileged user.');
+ }
+
+ /**
+ * Test report access.
+ */
+ public function testAccessReport() {
+ $report_path = 'admin/reports/checklistapi';
+
+ $this->drupalGet($report_path);
+ $this->assertResponse(200, 'Granted access to user with "view checklistapi checklists report" permission.');
+
+ $this->drupalLogout();
+ $this->drupalGet($report_path);
+ $this->assertResponse(403, 'Denied access to nonprivileged user.');
+ }
+
+ /**
+ * Test checklist composition.
+ */
+ public function testChecklistComposition() {
+ $menu_item = menu_get_item('admin/config/development/checklistapi-example');
+ $this->assertEqual($menu_item['path'], 'admin/config/development/checklistapi-example', 'Created per-checklist menu item.');
+
+ $permissions = array('edit example_checklist checklistapi checklist');
+ $this->assertTrue($this->checkPermissions($permissions), 'Created per-checklist permission.');
+
+ $this->drupalGet('admin/config/development/checklistapi-example');
+ $this->assertRaw('id="block-system-help"', 'Created per-checklist help block.');
+ }
+
+}
diff --git a/sites/all/modules/checklistapi/tests/modules/checklistapi_test/checklistapi_test.info b/sites/all/modules/checklistapi/tests/modules/checklistapi_test/checklistapi_test.info
new file mode 100644
index 0000000..e7c20c3
--- /dev/null
+++ b/sites/all/modules/checklistapi/tests/modules/checklistapi_test/checklistapi_test.info
@@ -0,0 +1,14 @@
+name = Checklist API test module
+description = Provides an implementation of the Checklist API for testing.
+package = Testing
+version = VERSION
+core = 7.x
+dependencies[] = checklistapi
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2014-08-25
+version = "7.x-1.2"
+core = "7.x"
+project = "checklistapi"
+datestamp = "1409004834"
+
diff --git a/sites/all/modules/checklistapi/tests/modules/checklistapi_test/checklistapi_test.module b/sites/all/modules/checklistapi/tests/modules/checklistapi_test/checklistapi_test.module
new file mode 100644
index 0000000..01c9dbe
--- /dev/null
+++ b/sites/all/modules/checklistapi/tests/modules/checklistapi_test/checklistapi_test.module
@@ -0,0 +1,63 @@
+ t('Checklist API test'),
+ '#path' => 'admin/config/development/checklistapi-test',
+ '#description' => t('A test checklist.'),
+ '#help' => t('This is a test checklist.
'),
+ 'group_two' => array(
+ '#title' => t('Group two'),
+ ),
+ 'group_one' => array(
+ '#title' => t('Group one'),
+ '#description' => t('Group one description.
'),
+ '#weight' => -1,
+ 'item_three' => array(
+ '#title' => t('Item three'),
+ '#weight' => 1,
+ ),
+ 'item_one' => array(
+ '#title' => t('Item one'),
+ '#description' => t('Item one description'),
+ '#weight' => -1,
+ 'link_three' => array(
+ '#text' => t('Link three'),
+ '#path' => 'http://example.com/three',
+ '#weight' => 3,
+ ),
+ 'link_two' => array(
+ '#text' => t('Link two'),
+ '#path' => 'http://example.com/two',
+ '#weight' => 2,
+ ),
+ 'link_one' => array(
+ '#text' => t('Link one'),
+ '#path' => 'http://example.com/one',
+ '#weight' => 1,
+ ),
+ ),
+ 'item_two' => array(
+ '#title' => t('Item two'),
+ ),
+ ),
+ 'group_four' => array(
+ '#title' => t('Group four'),
+ '#weight' => 1,
+ ),
+ 'group_three' => array(
+ '#title' => t('Group three'),
+ '#weight' => 'invalid',
+ ),
+ );
+ return $definitions;
+}
diff --git a/sites/all/modules/ctools/API.txt b/sites/all/modules/ctools/API.txt
new file mode 100644
index 0000000..b698b79
--- /dev/null
+++ b/sites/all/modules/ctools/API.txt
@@ -0,0 +1,54 @@
+Current API Version: 2.0.8
+
+Please note that the API version is an internal number and does not match release numbers. It is entirely possible that releases will not increase the API version number, and increasing this number too often would burden contrib module maintainers who need to keep up with API changes.
+
+This file contains a log of changes to the API.
+API Version 2.0.9
+Changed import permissions to use the new 'use ctools import' permission.
+
+API Version 2.0.8
+ Introduce ctools_class_add().
+ Introduce ctools_class_remove().
+
+API Version 2.0.7
+ All ctools object cache database functions can now accept session_id as an optional
+ argument to facilitate using non-session id keys.
+
+API Version 2.0.6
+ Introduce a hook to alter the implementors of a certain api via hook_[ctools_api_hook]_alter.
+
+API Version 2.0.5
+ Introduce ctools_fields_get_fields_by_type().
+ Add language.inc
+ Introduce hook_ctools_content_subtype_alter($subtype, $plugin);
+
+API Version 2.0.4
+ Introduce ctools_form_include_file()
+
+API Version 2.0.3
+ Introduce ctools_field_invoke_field() and ctools_field_invoke_field_default().
+
+API Version 2.0.2
+ Introduce ctools_export_crud_load_multiple() and 'load multiple callback' to
+ export schema.
+
+API Version 2.0.1
+ Introduce ctools_export_crud_enable(), ctools_export_crud_disable() and
+ ctools_export_crud_set_status() and requisite changes.
+ Introduce 'object factory' to export schema, allowing modules to control
+ how the exportable objects are instantiated.
+ Introduce 'hook_ctools_math_expression_functions_alter'.
+
+API Version 2.0
+ Remove the deprecated callback-based behavior of the 'defaults' property on
+ plugin types; array addition is now the only option. If you need more
+ complex logic, do it with the 'process' callback.
+ Introduce a global plugin type registration hook and remove the per-plugin
+ type magic callbacks.
+ Introduce $owner . '_' . $api . '_hook_name' allowing modules to use their own
+ API hook in place of 'hook_ctools_plugin_api'.
+ Introduce ctools_plugin_api_get_hook() to get the hook name above.
+ Introduce 'cache defaults' and 'default cache bin' keys to export.inc
+
+Versions prior to 2.0 have been removed from this document. See the D6 version
+for that information.
diff --git a/sites/all/modules/ctools/LICENSE.txt b/sites/all/modules/ctools/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/sites/all/modules/ctools/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/ctools/UPGRADE.txt b/sites/all/modules/ctools/UPGRADE.txt
new file mode 100644
index 0000000..2fbfa4f
--- /dev/null
+++ b/sites/all/modules/ctools/UPGRADE.txt
@@ -0,0 +1,63 @@
+Upgrading from ctools-6.x-1.x to ctools-7.x-2.x:
+
+ - Remove ctools_ajax_associate_url_to_element as it shouldn't be necessary
+ with the new AJAX api's in Drupal core.
+
+ - All calls to the ctools_ajax_command_prepend() should be replace with
+ the core function ajax_command_prepend();
+ This is also the case for append, insert, after, before, replace, html,
+ and remove commands.
+ Each of these commands have been incorporated into the
+ Drupal.ajax.prototype.commands.insert
+ function with a corresponding parameter specifying which method to use.
+
+ - All calls to ctools_ajax_render() should be replaced with calls to core
+ ajax_render(). Note that ctools_ajax_render() printed the json object and
+ exited, ajax_render() gives you this responsibility.
+
+ ctools_ajax_render()
+
+ becomes
+
+ print ajax_render();
+ exit;
+
+ - All calls to ctools_static*() should be replaced with corresponding calls
+ to drupal_static*().
+
+ - All calls to ctools_css_add_css should be replaced with calls to
+ drupal_add_css(). Note that the arguments to drupal_add_css() have changed.
+
+ - All wizard form builder functions must now return a form array().
+
+ - ctools_build_form is very close to being removed. In anticipation of this,
+ all $form_state['wrapper callback']s must now be
+ $form_state['wrapper_callback']. In addition to this $form_state['args']
+ must now be $form_state['build_info']['args'].
+
+ NOTE: Previously checking to see if the return from ctools_build_form()
+ is empty would be enough to see if the form was submitted. This is no
+ longer true. Please check for $form_state['executed']. If using a wizard
+ check for $form_state['complete'].
+
+ - Plugin types now must be explicitly registered via a registration hook,
+ hook_ctools_plugin_type(); info once provided in magically-named functions
+ (e.g., ctools_ctools_plugin_content_types() was the old function to
+ provide plugin type info for ctools' content_type plugins) now must be
+ provided in that global hook. See http://drupal.org/node/910538 for more
+ details.
+
+ - Plugins that use 'theme arguments' now use 'theme variables' instead.
+
+ - Context, argument and relationship plugins now use 'add form' and/or
+ 'edit form' rather than 'settings form'. These plugins now support
+ form wizards just like content plugins. These forms now all take
+ $form, &$form_state as arguments, and the configuration for the plugin
+ can be found in $form_state['conf'].
+
+ For all these forms, the submit handler MUST put appropriate data in
+ $form_state['conf']. Data will no longer be stored automatically.
+
+ For all of these forms, the separate settings #trees in the form are now
+ gone, so form ids may be adjusted. Also, these are now all real forms
+ using CTools form wizard instead of fake subforms as previously.
diff --git a/sites/all/modules/ctools/bulk_export/bulk_export.css b/sites/all/modules/ctools/bulk_export/bulk_export.css
new file mode 100644
index 0000000..45a172d
--- /dev/null
+++ b/sites/all/modules/ctools/bulk_export/bulk_export.css
@@ -0,0 +1,18 @@
+.export-container {
+ width: 48%;
+ float: left;
+ padding: 5px 1% 0;
+}
+.export-container table {
+ width: 100%;
+}
+.export-container table input,
+.export-container table th,
+.export-container table td {
+ padding: 0 0 .2em .5em;
+ margin: 0;
+ vertical-align: middle;
+}
+.export-container .select-all {
+ width: 1.5em;
+}
diff --git a/sites/all/modules/ctools/bulk_export/bulk_export.info b/sites/all/modules/ctools/bulk_export/bulk_export.info
new file mode 100644
index 0000000..388cd6f
--- /dev/null
+++ b/sites/all/modules/ctools/bulk_export/bulk_export.info
@@ -0,0 +1,13 @@
+name = Bulk Export
+description = Performs bulk exporting of data objects known about by Chaos tools.
+core = 7.x
+dependencies[] = ctools
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+
+; Information added by Drupal.org packaging script on 2018-02-24
+version = "7.x-1.14"
+core = "7.x"
+project = "ctools"
+datestamp = "1519455788"
+
diff --git a/sites/all/modules/ctools/bulk_export/bulk_export.js b/sites/all/modules/ctools/bulk_export/bulk_export.js
new file mode 100644
index 0000000..a4fb3f2
--- /dev/null
+++ b/sites/all/modules/ctools/bulk_export/bulk_export.js
@@ -0,0 +1,29 @@
+
+/**
+ * @file
+ * CTools Bulk Export javascript functions.
+ */
+
+(function ($) {
+
+Drupal.behaviors.CToolsBulkExport = {
+ attach: function (context) {
+
+ $('#bulk-export-export-form .vertical-tabs-pane', context).drupalSetSummary(function (context) {
+
+ // Check if any individual checkbox is checked.
+ if ($('.bulk-selection input:checked', context).length > 0) {
+ return Drupal.t('Exportables selected');
+ }
+
+ return '';
+ });
+
+ // Special bind click on the select-all checkbox.
+ $('.select-all').bind('click', function(context) {
+ $(this, '.vertical-tabs-pane').drupalSetSummary(context);
+ });
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/ctools/bulk_export/bulk_export.module b/sites/all/modules/ctools/bulk_export/bulk_export.module
new file mode 100644
index 0000000..1050caa
--- /dev/null
+++ b/sites/all/modules/ctools/bulk_export/bulk_export.module
@@ -0,0 +1,278 @@
+ array(
+ 'title' => t('Access Bulk Exporter'),
+ 'description' => t('Export various system objects into code.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function bulk_export_menu() {
+ $items['admin/structure/bulk-export'] = array(
+ 'title' => 'Bulk Exporter',
+ 'description' => 'Bulk-export multiple CTools-handled data objects to code.',
+ 'access arguments' => array('use bulk exporter'),
+ 'page callback' => 'bulk_export_export',
+ );
+ $items['admin/structure/bulk-export/results'] = array(
+ 'access arguments' => array('use bulk exporter'),
+ 'page callback' => 'bulk_export_export',
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+}
+
+/**
+ * FAPI gateway to the bulk exporter.
+ *
+ * @param $cli
+ * Whether this function is called from command line.
+ * @param $options
+ * A collection of options, only passed in by drush_ctools_export().
+ */
+function bulk_export_export($cli = FALSE, $options = array()) {
+ ctools_include('export');
+ $form = array();
+ $schemas = ctools_export_get_schemas(TRUE);
+ $exportables = $export_tables = array();
+
+ foreach ($schemas as $table => $schema) {
+ if (!empty($schema['export']['list callback']) && function_exists($schema['export']['list callback'])) {
+ $exportables[$table] = $schema['export']['list callback']();
+ }
+ else {
+ $exportables[$table] = ctools_export_default_list($table, $schema);
+ }
+ natcasesort($exportables[$table]);
+ $export_tables[$table] = $schema['module'];
+ }
+ if ($exportables) {
+ $form_state = array(
+ 're_render' => FALSE,
+ 'no_redirect' => TRUE,
+ 'exportables' => $exportables,
+ 'export_tables' => $export_tables,
+ 'name' => '',
+ 'code' => '',
+ 'module' => '',
+ );
+
+ // If called from drush_ctools_export, get the module name and
+ // select all exportables and call the submit function directly.
+ if ($cli) {
+ $module_name = $options['name'];
+ $form_state['values']['name'] = $module_name;
+ if (isset($options['selections'])) {
+ $exportables = $options['selections'];
+ }
+ $form_state['values']['tables'] = array();
+ foreach ($exportables as $table => $names) {
+ if (!empty($names)) {
+ $form_state['values']['tables'][] = $table;
+ $form_state['values'][$table] = array();
+ foreach ($names as $name => $title) {
+ $form_state['values'][$table][$name] = $name;
+ }
+ }
+ }
+ $output = bulk_export_export_form_submit($form, $form_state);
+ }
+ else {
+ $output = drupal_build_form('bulk_export_export_form', $form_state);
+ $module_name = $form_state['module'];
+ }
+
+ if (!empty($form_state['submitted']) || $cli) {
+ drupal_set_title(t('Bulk export results'));
+ $output = '';
+ $module_code = '';
+ $api_code = array();
+ $dependencies = $file_data = array();
+ foreach ($form_state['code'] as $module => $api_info) {
+ if ($module == 'general') {
+ $module_code .= $api_info;
+ }
+ else {
+ foreach ($api_info as $api => $info) {
+ $api_hook = ctools_plugin_api_get_hook($module, $api);
+ if (empty($api_code[$api_hook])) {
+ $api_code[$api_hook] = '';
+ }
+ $api_code[$api_hook] .= " if (\$module == '$module' && \$api == '$api') {\n";
+ $api_code[$api_hook] .= " return array('version' => $info[version]);\n";
+ $api_code[$api_hook] .= " }\n";
+ $dependencies[$module] = TRUE;
+
+ $file = $module_name . '.' . $api . '.inc';
+ $code = " $file)));
+ $output .= drupal_render($export_form);
+ }
+ }
+ }
+ }
+
+ // Add hook_ctools_plugin_api at the top of the module code, if there is any.
+ if ($api_code) {
+ foreach ($api_code as $api_hook => $text) {
+ $api = "\n/**\n";
+ $api .= " * Implements hook_$api_hook().\n";
+ $api .= " */\n";
+ $api .= "function {$module_name}_$api_hook(\$module, \$api) {\n";
+ $api .= $text;
+ $api .= "}\n";
+ $module_code = $api . $module_code;
+ }
+ }
+
+ if ($module_code) {
+ $module = " $form_state['module'] . '.module')));
+ $output = drupal_render($export_form) . $output;
+ }
+ }
+
+ $info = strtr("name = @module export module\n", array('@module' => $form_state['module']));
+ $info .= strtr("description = Export objects from CTools\n", array('@module' => $form_state['values']['name']));
+ foreach ($dependencies as $module => $junk) {
+ $info .= "dependencies[] = $module\n";
+ }
+ $info .= "package = Chaos tool suite\n";
+ $info .= "core = 7.x\n";
+ if ($cli) {
+ $file_data[$module_name . '.info'] = $info;
+ }
+ else {
+ $export_form = drupal_get_form('ctools_export_form', $info, t('Place this in @file', array('@file' => $form_state['module'] . '.info')));
+ $output = drupal_render($export_form) . $output;
+ }
+ }
+
+ if ($cli) {
+ return $file_data;
+ }
+ else {
+ return $output;
+ }
+ }
+ else {
+ return t('There are no objects to be exported at this time.');
+ }
+}
+
+/**
+ * FAPI definition for the bulk exporter form.
+ */
+function bulk_export_export_form($form, &$form_state) {
+
+ $files = system_rebuild_module_data();
+
+ $form['additional_settings'] = array(
+ '#type' => 'vertical_tabs',
+ );
+
+ $options = $tables = array();
+ foreach ($form_state['exportables'] as $table => $list) {
+ if (empty($list)) {
+ continue;
+ }
+
+ foreach ($list as $id => $title) {
+ $options[$table][$id] = array($title);
+ $options[$table][$id]['#attributes'] = array('class' => array('bulk-selection'));
+ }
+
+ $module = $form_state['export_tables'][$table];
+ $header = array($table);
+ $module_name = $files[$module]->info['name'];
+ $tables[] = $table;
+
+ if (!isset($form[$module_name])) {
+ $form[$files[$module]->info['name']] = array(
+ '#type' => 'fieldset',
+ '#group' => 'additional_settings',
+ '#title' => $module_name,
+ );
+ }
+
+ $form[$module_name]['tables'][$table] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#type' => 'tableselect',
+ '#header' => $header,
+ '#options' => $options[$table],
+ );
+ }
+
+ $form['tables'] = array(
+ '#type' => 'value',
+ '#value' => $tables,
+ );
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Module name'),
+ '#description' => t('Enter the module name to export code to.'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Export'),
+ );
+
+ $form['#action'] = url('admin/structure/bulk-export/results');
+ $form['#attached']['css'][] = drupal_get_path('module', 'bulk_export') . '/bulk_export.css';
+ $form['#attached']['js'][] = drupal_get_path('module', 'bulk_export') . '/bulk_export.js';
+ return $form;
+}
+
+/**
+ * Process the bulk export submit form and make the results available.
+ */
+function bulk_export_export_form_submit($form, &$form_state) {
+ $code = array();
+ $name = empty($form_state['values']['name']) ? 'foo' : $form_state['values']['name'];
+ $tables = $form_state['values']['tables'];
+
+ foreach ($tables as $table) {
+ $names = array_keys(array_filter($form_state['values'][$table]));
+ if ($names) {
+ natcasesort($names);
+ ctools_export_to_hook_code($code, $table, $names, $name);
+ }
+ }
+
+ $form_state['code'] = $code;
+ $form_state['module'] = $name;
+}
diff --git a/sites/all/modules/ctools/css/button.css b/sites/all/modules/ctools/css/button.css
new file mode 100644
index 0000000..de21408
--- /dev/null
+++ b/sites/all/modules/ctools/css/button.css
@@ -0,0 +1,30 @@
+.ctools-button-processed {
+ border-style: solid;
+ border-width: 1px;
+ display: inline-block;
+ line-height: 1;
+}
+
+.ctools-button-processed:hover {
+ cursor: pointer;
+}
+
+.ctools-button-processed .ctools-content {
+ padding-bottom: 2px;
+ padding-top: 2px;
+}
+
+.ctools-no-js .ctools-content ul,
+.ctools-button-processed .ctools-content ul {
+ list-style: none none;
+ margin-left: 0;
+}
+
+.ctools-button-processed li {
+ line-height: 1.3333;
+}
+
+.ctools-button li a {
+ padding-left: 12px;
+ padding-right: 12px;
+}
diff --git a/sites/all/modules/ctools/css/collapsible-div.css b/sites/all/modules/ctools/css/collapsible-div.css
new file mode 100644
index 0000000..5366a0a
--- /dev/null
+++ b/sites/all/modules/ctools/css/collapsible-div.css
@@ -0,0 +1,23 @@
+.ctools-collapsible-container .ctools-toggle {
+ float: left;
+ width: 21px;
+ height: 21px;
+ cursor: pointer;
+ background: url(../images/collapsible-expanded.png) no-repeat 7px 7px;
+}
+
+.ctools-collapsible-container .ctools-collapsible-handle {
+ display: none;
+}
+
+html.js .ctools-collapsible-container .ctools-collapsible-handle {
+ display: block;
+}
+
+.ctools-collapsible-container .ctools-collapsible-handle {
+ cursor: pointer;
+}
+
+.ctools-collapsible-container .ctools-toggle-collapsed {
+ background-image: url(../images/collapsible-collapsed.png);
+}
diff --git a/sites/all/modules/ctools/css/context.css b/sites/all/modules/ctools/css/context.css
new file mode 100644
index 0000000..5093104
--- /dev/null
+++ b/sites/all/modules/ctools/css/context.css
@@ -0,0 +1,10 @@
+.ctools-context-holder .ctools-context-title {
+ float: left;
+ width: 49%;
+ font-style: italic;
+}
+
+.ctools-context-holder .ctools-context-content {
+ float: right;
+ width: 49%;
+}
diff --git a/sites/all/modules/ctools/css/ctools.css b/sites/all/modules/ctools/css/ctools.css
new file mode 100644
index 0000000..66dcd59
--- /dev/null
+++ b/sites/all/modules/ctools/css/ctools.css
@@ -0,0 +1,25 @@
+.ctools-locked {
+ color: red;
+ border: 1px solid red;
+ padding: 1em;
+}
+
+.ctools-owns-lock {
+ background: #ffffdd none repeat scroll 0 0;
+ border: 1px solid #f0c020;
+ padding: 1em;
+}
+
+a.ctools-ajaxing,
+input.ctools-ajaxing,
+button.ctools-ajaxing,
+select.ctools-ajaxing {
+ padding-right: 18px !important;
+ background: url(../images/status-active.gif) right center no-repeat;
+}
+
+div.ctools-ajaxing {
+ float: left;
+ width: 18px;
+ background: url(../images/status-active.gif) center center no-repeat;
+}
diff --git a/sites/all/modules/ctools/css/dropbutton.css b/sites/all/modules/ctools/css/dropbutton.css
new file mode 100644
index 0000000..376fc67
--- /dev/null
+++ b/sites/all/modules/ctools/css/dropbutton.css
@@ -0,0 +1,65 @@
+.ctools-dropbutton-processed {
+ padding-right: 18px;
+ position: relative;
+ background-color: inherit;
+}
+
+.ctools-dropbutton-processed.open {
+ z-index: 200;
+}
+
+.ctools-dropbutton-processed .ctools-content li,
+.ctools-dropbutton-processed .ctools-content a {
+ display: block;
+}
+
+.ctools-dropbutton-processed .ctools-link {
+ bottom: 0;
+ display: block;
+ height: auto;
+ position: absolute;
+ right: 0;
+ text-indent: -9999px; /* LTR */
+ top: 0;
+ width: 17px;
+}
+
+.ctools-dropbutton-processed .ctools-link a {
+ overflow: hidden;
+}
+
+.ctools-dropbutton-processed .ctools-content ul {
+ margin: 0;
+ overflow: hidden;
+}
+
+.ctools-dropbutton-processed.open li + li {
+ padding-top: 4px;
+}
+
+/**
+ * This creates the dropbutton arrow and inherits the link color
+ */
+.ctools-twisty {
+ border-bottom-color: transparent;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-style: solid;
+ border-width: 4px 4px 0;
+ line-height: 0;
+ right: 6px;
+ position: absolute;
+ top: 0.75em;
+}
+
+.ctools-dropbutton-processed.open .ctools-twisty {
+ border-bottom: 4px solid;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-top-color: transparent;
+ top: 0.5em;
+}
+
+.ctools-no-js .ctools-twisty {
+ display: none;
+}
diff --git a/sites/all/modules/ctools/css/dropdown.css b/sites/all/modules/ctools/css/dropdown.css
new file mode 100644
index 0000000..d63bb7b
--- /dev/null
+++ b/sites/all/modules/ctools/css/dropdown.css
@@ -0,0 +1,73 @@
+html.js div.ctools-dropdown div.ctools-dropdown-container {
+ z-index: 1001;
+ display: none;
+ text-align: left;
+ position: absolute;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li a {
+ display: block;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li {
+ display: block;
+ /* prevent excess right margin in IE */
+ margin-right: 0;
+ margin-left: 0;
+ padding-right: 0;
+ padding-left: 0;
+ background-image: none; /* prevent list backgrounds from mucking things up */
+}
+
+.ctools-dropdown-no-js .ctools-dropdown-link,
+.ctools-dropdown-no-js span.text {
+ display: none;
+}
+
+/* Everything from here down is purely visual style and can be overridden. */
+
+html.js div.ctools-dropdown a.ctools-dropdown-text-link {
+ background: url(../images/collapsible-expanded.png) 3px 5px no-repeat;
+ padding-left: 12px;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container {
+ width: 175px;
+ background: #fff;
+ border: 1px solid black;
+ margin: 4px 1px 0 0;
+ padding: 0;
+ color: #494949;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li li a {
+ padding-left: 25px;
+ width: 150px;
+ color: #027ac6;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li a {
+ text-decoration: none;
+ padding-left: 5px;
+ width: 170px;
+ color: #027ac6;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li span {
+ display: block;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li span.text {
+ font-style: italic;
+ padding-left: 5px;
+}
+
+html.js .ctools-dropdown-hover {
+ background-color: #ececec;
+}
diff --git a/sites/all/modules/ctools/css/export-ui-list.css b/sites/all/modules/ctools/css/export-ui-list.css
new file mode 100644
index 0000000..170d128
--- /dev/null
+++ b/sites/all/modules/ctools/css/export-ui-list.css
@@ -0,0 +1,45 @@
+body form#ctools-export-ui-list-form {
+ margin: 0 0 20px 0;
+}
+
+#ctools-export-ui-list-form .form-item {
+ padding-right: 1em; /* LTR */
+ float: left; /* LTR */
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#ctools-export-ui-list-items {
+ width: 100%;
+}
+
+#edit-order-wrapper {
+ clear: left; /* LTR */
+}
+
+#ctools-export-ui-list-form .form-submit {
+ margin-top: 1.65em;
+ float: left; /* LTR */
+}
+
+tr.ctools-export-ui-disabled td {
+ color: #999;
+}
+
+th.ctools-export-ui-operations,
+td.ctools-export-ui-operations {
+ text-align: right; /* LTR */
+ vertical-align: top;
+}
+
+/* Force the background color to inherit so that the dropbuttons do not need
+ a specific background color. */
+td.ctools-export-ui-operations {
+ background-color: inherit;
+}
+
+td.ctools-export-ui-operations .ctools-dropbutton {
+ text-align: left; /* LTR */
+ position: absolute;
+ right: 10px;
+}
diff --git a/sites/all/modules/ctools/css/modal-rtl.css b/sites/all/modules/ctools/css/modal-rtl.css
new file mode 100644
index 0000000..5231b9f
--- /dev/null
+++ b/sites/all/modules/ctools/css/modal-rtl.css
@@ -0,0 +1,60 @@
+div.ctools-modal-content {
+ text-align: right;
+}
+
+div.ctools-modal-content .modal-header {
+ background-color: #2385c2;
+ padding: 0 1em 0 .25em;
+}
+
+div.ctools-modal-content a.close {
+ color: white;
+ float: left;
+}
+
+/** modal forms CSS **/
+div.ctools-modal-content .form-item label {
+ float: right;
+}
+
+div.ctools-modal-content .form-item .description {
+ clear: right;
+}
+
+div.ctools-modal-content .form-item .description .tips {
+ margin-left: 0;
+ margin-right: 2em;
+}
+
+div.ctools-modal-content fieldset,
+div.ctools-modal-content .form-radios,
+div.ctools-modal-content .form-checkboxes {
+ clear: right;
+}
+
+div.ctools-modal-content .resizable-textarea {
+ margin-left: 5em;
+ margin-right: 15em;
+}
+
+div.ctools-modal-content .container-inline .form-item {
+ margin-right: 0;
+ margin-left: 2em;
+}
+
+div.ctools-modal-content label.hidden-options {
+ background-position: left;
+ padding-right: 0;
+ padding-left: 12px;
+}
+
+div.ctools-modal-content label.expanded-options {
+ background-position: left;
+ padding-right: 0;
+ padding-left: 16px;
+}
+
+div.ctools-modal-content .dependent-options {
+ padding-left: 0;
+ padding-right: 30px;
+}
diff --git a/sites/all/modules/ctools/css/modal.css b/sites/all/modules/ctools/css/modal.css
new file mode 100644
index 0000000..0045ecc
--- /dev/null
+++ b/sites/all/modules/ctools/css/modal.css
@@ -0,0 +1,130 @@
+div.ctools-modal-content {
+ background: #fff;
+ color: #000;
+ padding: 0;
+ margin: 2px;
+ border: 1px solid #000;
+ width: 600px;
+ text-align: left;
+}
+
+div.ctools-modal-content .modal-title {
+ font-size: 120%;
+ font-weight: bold;
+ color: white;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+div.ctools-modal-content .modal-header {
+ background-color: #2385c2;
+ padding: 0 .25em 0 1em;
+}
+
+div.ctools-modal-content .modal-header a {
+ color: white;
+}
+
+div.ctools-modal-content .modal-content {
+ padding: 1em 1em 0 1em;
+ overflow: auto;
+ position: relative; /* Keeps IE7 from flowing outside the modal. */
+}
+
+div.ctools-modal-content .modal-form {
+}
+
+div.ctools-modal-content a.close {
+ color: white;
+ float: right;
+}
+
+div.ctools-modal-content a.close:hover {
+ text-decoration: none;
+}
+
+div.ctools-modal-content a.close img {
+ position: relative;
+ top: 1px;
+}
+
+div.ctools-modal-content .modal-content .modal-throbber-wrapper {
+ text-align: center;
+}
+
+div.ctools-modal-content .modal-content .modal-throbber-wrapper img {
+ margin-top: 160px;
+}
+
+/** modal forms CSS **/
+div.ctools-modal-content .form-item label {
+ width: 15em;
+ float: left;
+}
+
+div.ctools-modal-content .form-item label.option {
+ width: auto;
+ float: none;
+}
+
+div.ctools-modal-content .form-item .description {
+ clear: left;
+}
+
+div.ctools-modal-content .form-item .description .tips {
+ margin-left: 2em;
+}
+
+div.ctools-modal-content .no-float .form-item * {
+ float: none;
+}
+
+div.ctools-modal-content .modal-form .no-float label {
+ width: auto;
+}
+
+div.ctools-modal-content fieldset,
+div.ctools-modal-content .form-radios,
+div.ctools-modal-content .form-checkboxes {
+ clear: left;
+}
+
+div.ctools-modal-content .vertical-tabs-panes > fieldset {
+ clear: none;
+}
+
+div.ctools-modal-content .resizable-textarea {
+ width: auto;
+ margin-left: 15em;
+ margin-right: 5em;
+}
+
+div.ctools-modal-content .container-inline .form-item {
+ margin-right: 2em;
+}
+
+#views-exposed-pane-wrapper .form-item {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+div.ctools-modal-content label.hidden-options {
+ background: transparent url(../images/arrow-active.png) no-repeat right;
+ height: 12px;
+ padding-right: 12px;
+}
+
+div.ctools-modal-content label.expanded-options {
+ background: transparent url(../images/expanded-options.png) no-repeat right;
+ height: 12px;
+ padding-right: 16px;
+}
+
+div.ctools-modal-content .option-text-aligner label.expanded-options,
+div.ctools-modal-content .option-text-aligner label.hidden-options {
+ background: none;
+}
+
+div.ctools-modal-content .dependent-options {
+ padding-left: 30px;
+}
diff --git a/sites/all/modules/ctools/css/ruleset.css b/sites/all/modules/ctools/css/ruleset.css
new file mode 100644
index 0000000..891455f
--- /dev/null
+++ b/sites/all/modules/ctools/css/ruleset.css
@@ -0,0 +1,11 @@
+.ctools-right-container {
+ float: right;
+ padding: 0 0 0 .5em;
+ margin: 0;
+ width: 48.5%;
+}
+
+.ctools-left-container {
+ padding-right: .5em;
+ width: 48.5%;
+}
diff --git a/sites/all/modules/ctools/css/stylizer.css b/sites/all/modules/ctools/css/stylizer.css
new file mode 100644
index 0000000..fc8dcb2
--- /dev/null
+++ b/sites/all/modules/ctools/css/stylizer.css
@@ -0,0 +1,129 @@
+/* Farbtastic placement */
+.color-form {
+ max-width: 50em;
+ position: relative;
+ min-height: 195px;
+}
+#placeholder {
+/*
+ position: absolute;
+ top: 0;
+ right: 0;
+*/
+ margin: 0 auto;
+ width: 195px;
+}
+
+/* Palette */
+.color-form .form-item {
+ height: 2em;
+ line-height: 2em;
+ padding-left: 1em; /* LTR */
+ margin: 0.5em 0;
+}
+
+.color-form .form-item input {
+ margin-top: .2em;
+}
+
+.color-form label {
+ float: left; /* LTR */
+ clear: left; /* LTR */
+ width: 14em;
+}
+.color-form .form-text,
+.color-form .form-select {
+ float: left; /* LTR */
+}
+.color-form .form-text {
+ text-align: center;
+ margin-right: 5px; /* LTR */
+ cursor: pointer;
+}
+
+#palette .hook {
+ float: left; /* LTR */
+ margin-top: 3px;
+ width: 16px;
+ height: 16px;
+}
+#palette .up {
+ background-position: 100% -27px; /* LTR */
+}
+#palette .both {
+ background-position: 100% -54px; /* LTR */
+}
+
+
+#palette .form-item {
+ width: 24em;
+}
+#palette .item-selected {
+ background: #eee;
+}
+
+/* Preview */
+#preview {
+ width: 45%;
+ float: right;
+ margin: 0;
+}
+
+#ctools_stylizer_color_scheme_form {
+ float: left;
+ width: 45%;
+ margin: 0;
+}
+
+/* general style for the layout-icon */
+.ctools-style-icon .caption {
+ width: 100px;
+ margin-bottom: 1em;
+ line-height: 1em;
+ text-align: center;
+ cursor: default;
+}
+
+.ctools-style-icons .form-item {
+ width: 100px;
+ float: left;
+ margin: 0 3px !important;
+}
+
+.ctools-style-icons .form-item .ctools-style-icon {
+ float: none;
+ height: 150px;
+ width: 100px;
+}
+
+.ctools-style-icons .form-item label.option {
+ width: 100px;
+ display: block;
+ text-align: center;
+}
+
+.ctools-style-icons .form-item label.option input {
+ margin: 0 auto;
+}
+
+.ctools-style-icons .ctools-style-category {
+ height: 190px;
+}
+
+.ctools-style-icons .ctools-style-category label {
+ font-weight: bold;
+ width: 100%;
+ float: left;
+}
+
+/**
+ * Stylizer font editor widget
+ */
+.ctools-stylizer-spacing-form .form-item {
+ float: left;
+ margin: .25em;
+}
+
+#edit-font-font {
+ width: 9em;
+}
diff --git a/sites/all/modules/ctools/css/wizard.css b/sites/all/modules/ctools/css/wizard.css
new file mode 100644
index 0000000..ab80def
--- /dev/null
+++ b/sites/all/modules/ctools/css/wizard.css
@@ -0,0 +1,7 @@
+.wizard-trail {
+ font-size: 120%;
+}
+
+.wizard-trail-current {
+ font-weight: bold;
+}
diff --git a/sites/all/modules/ctools/ctools.api.php b/sites/all/modules/ctools/ctools.api.php
new file mode 100644
index 0000000..4481291
--- /dev/null
+++ b/sites/all/modules/ctools/ctools.api.php
@@ -0,0 +1,313 @@
+ TRUE,
+ );
+
+ return $plugins;
+}
+
+/**
+ * This hook is used to inform the CTools plugin system about the location of a
+ * directory that should be searched for files containing plugins of a
+ * particular type. CTools invokes this same hook for all plugins, using the
+ * two passed parameters to indicate the specific type of plugin for which it
+ * is searching.
+ *
+ * The $plugin_type parameter is self-explanatory - it is the string name of the
+ * plugin type (e.g., Panels' 'layouts' or 'styles'). The $owner parameter is
+ * necessary because CTools internally namespaces plugins by the module that
+ * owns them. This is an extension of Drupal best practices on avoiding global
+ * namespace pollution by prepending your module name to all its functions.
+ * Consequently, it is possible for two different modules to create a plugin
+ * type with exactly the same name and have them operate in harmony. In fact,
+ * this system renders it impossible for modules to encroach on other modules'
+ * plugin namespaces.
+ *
+ * Given this namespacing, it is important that implementations of this hook
+ * check BOTH the $owner and $plugin_type parameters before returning a path.
+ * If your module does not implement plugins for the requested module/plugin
+ * combination, it is safe to return nothing at all (or NULL). As a convenience,
+ * it is also safe to return a path that does not exist for plugins your module
+ * does not implement - see form 2 for a use case.
+ *
+ * Note that modules implementing a plugin also must implement this hook to
+ * instruct CTools as to the location of the plugins. See form 3 for a use case.
+ *
+ * The conventional structure to return is "plugins/$plugin_type" - that is, a
+ * 'plugins' subdirectory in your main module directory, with individual
+ * directories contained therein named for the plugin type they contain.
+ *
+ * @param string $owner
+ * The system name of the module owning the plugin type for which a base
+ * directory location is being requested.
+ * @param string $plugin_type
+ * The name of the plugin type for which a base directory is being requested.
+ *
+ * @return string
+ * The path where CTools' plugin system should search for plugin files,
+ * relative to your module's root. Omit leading and trailing slashes.
+ */
+function hook_ctools_plugin_directory($owner, $plugin_type) {
+ // Form 1 - for a module implementing only the 'content_types' plugin owned
+ // by CTools, this would cause the plugin system to search the
+ // /plugins/content_types directory for .inc plugin files.
+ if ($owner == 'ctools' && $plugin_type == 'content_types') {
+ return 'plugins/content_types';
+ }
+
+ // Form 2 - if your module implements only Panels plugins, and has 'layouts'
+ // and 'styles' plugins but no 'cache' or 'display_renderers', it is OK to be
+ // lazy and return a directory for a plugin you don't actually implement (so
+ // long as that directory doesn't exist). This lets you avoid ugly in_array()
+ // logic in your conditional, and also makes it easy to add plugins of those
+ // types later without having to change this hook implementation.
+ if ($owner == 'panels') {
+ return "plugins/$plugin_type";
+ }
+
+ // Form 3 - CTools makes no assumptions about where your plugins are located,
+ // so you still have to implement this hook even for plugins created by your
+ // own module.
+ if ($owner == 'mymodule') {
+ // Yes, this is exactly like Form 2 - just a different reasoning for it.
+ return "plugins/$plugin_type";
+ }
+ // Finally, if nothing matches, it's safe to return nothing at all (== NULL).
+}
+
+/**
+ * Alter a plugin before it has been processed.
+ *
+ * This hook is useful for altering flags or other information that will be
+ * used or possibly overriden by the process hook if defined.
+ *
+ * @param $plugin
+ * An associative array defining a plugin.
+ * @param $info
+ * An associative array of plugin type info.
+ */
+function hook_ctools_plugin_pre_alter(&$plugin, &$info) {
+ // Override a function defined by the plugin.
+ if ($info['type'] == 'my_type') {
+ $plugin['my_flag'] = 'new_value';
+ }
+}
+
+/**
+ * Alter a plugin after it has been processed.
+ *
+ * This hook is useful for overriding the final values for a plugin after it
+ * has been processed.
+ *
+ * @param $plugin
+ * An associative array defining a plugin.
+ * @param $info
+ * An associative array of plugin type info.
+ */
+function hook_ctools_plugin_post_alter(&$plugin, &$info) {
+ // Override a function defined by the plugin.
+ if ($info['type'] == 'my_type') {
+ $plugin['my_function'] = 'new_function';
+ }
+}
+
+/**
+ * Alter the list of modules/themes which implement a certain api.
+ *
+ * The hook named here is just an example, as the real existing hooks are named
+ * for example 'hook_views_api_alter'.
+ *
+ * @param array $list
+ * An array of informations about the implementors of a certain api.
+ * The key of this array are the module names/theme names.
+ */
+function hook_ctools_api_hook_alter(&$list) {
+ // Alter the path of the node implementation.
+ $list['node']['path'] = drupal_get_path('module', 'node');
+}
+
+/**
+ * Alter the available functions to be used in ctools math expression api.
+ *
+ * One usecase would be to create your own function in your module and
+ * allow to use it in the math expression api.
+ *
+ * @param $functions
+ * An array which has the functions as value.
+ */
+function hook_ctools_math_expression_functions_alter(&$functions) {
+ // Allow to convert from degrees to radiant.
+ $functions[] = 'deg2rad';
+}
+
+/**
+ * Alter everything.
+ *
+ * @param $info
+ * An associative array containing the following keys:
+ * - content: The rendered content.
+ * - title: The content's title.
+ * - no_blocks: A boolean to decide if blocks should be displayed.
+ * @param $page
+ * If TRUE then this renderer owns the page and can use theme('page')
+ * for no blocks; if false, output is returned regardless of any no
+ * blocks settings.
+ * @param $context
+ * An associative array containing the following keys:
+ * - args: The raw arguments behind the contexts.
+ * - contexts: The context objects in use.
+ * - task: The task object in use.
+ * - subtask: The subtask object in use.
+ * - handler: The handler object in use.
+ */
+function hook_ctools_render_alter(&$info, &$page, &$context) {
+ if ($context['handler']->name == 'my_handler') {
+ ctools_add_css('my_module.theme', 'my_module');
+ }
+}
+
+/**
+ * Alter a content plugin subtype.
+ *
+ * While content types can be altered via hook_ctools_plugin_pre_alter() or
+ * hook_ctools_plugin_post_alter(), the subtypes that content types rely on
+ * are special and require their own hook.
+ *
+ * This hook can be used to add things like 'render last' or change icons
+ * or categories or to rename content on specific sites.
+ */
+function hook_ctools_content_subtype_alter($subtype, $plugin) {
+ // Force a particular subtype of a particular plugin to render last.
+ if ($plugin['module'] === 'some_plugin_module'
+ && $plugin['name'] === 'some_plugin_name'
+ && $subtype['subtype_id'] === 'my_subtype_id'
+ ) {
+ $subtype['render last'] = TRUE;
+ }
+}
+
+/**
+ * Alter the definition of an entity context plugin.
+ *
+ * @param array $plugin
+ * An associative array defining a plugin.
+ * @param array $entity
+ * The entity info array of a specific entity type.
+ * @param string $plugin_id
+ * The plugin ID, in the format NAME:KEY.
+ */
+function hook_ctools_entity_context_alter(&$plugin, &$entity, $plugin_id) {
+ ctools_include('context');
+ switch ($plugin_id) {
+ case 'entity_id:taxonomy_term':
+ $plugin['no ui'] = TRUE;
+ case 'entity:user':
+ $plugin = ctools_get_context('user');
+ unset($plugin['no ui']);
+ unset($plugin['no required context ui']);
+ break;
+ }
+}
+
+/**
+ * Alter the conversion of context items by ctools context plugin convert()s.
+ *
+ * @param ctools_context $context
+ * The current context plugin object. If this implemented a 'convert'
+ * function, the value passed in has been processed by that function.
+ * @param string $converter
+ * A string associated with the plugin type, identifying the operation.
+ * @param string $value
+ * The value being converted; this is the only return from the function.
+ * @param $converter_options
+ * Array of key-value pairs to pass to a converter function from higher
+ * levels.
+ *
+ * @see ctools_context_convert_context()
+ */
+function hook_ctools_context_converter_alter($context, $converter, &$value, $converter_options) {
+ if ($converter === 'mystring') {
+ $value = 'fixed';
+ }
+}
+
+/**
+ * Alter the definition of entity context plugins.
+ *
+ * @param array $plugins
+ * An associative array of plugin definitions, keyed by plugin ID.
+ *
+ * @see hook_ctools_entity_context_alter()
+ */
+function hook_ctools_entity_contexts_alter(&$plugins) {
+ $plugins['entity_id:taxonomy_term']['no ui'] = TRUE;
+}
+
+/**
+ * Change cleanstring settings.
+ *
+ * @param array $settings
+ * An associative array of cleanstring settings.
+ *
+ * @see ctools_cleanstring()
+ */
+function hook_ctools_cleanstring_alter(&$settings) {
+ // Convert all strings to lower case.
+ $settings['lower case'] = TRUE;
+}
+
+/**
+ * Change cleanstring settings for a specific clean ID.
+ *
+ * @param array $settings
+ * An associative array of cleanstring settings.
+ *
+ * @see ctools_cleanstring()
+ */
+function hook_ctools_cleanstring_CLEAN_ID_alter(&$settings) {
+ // Convert all strings to lower case.
+ $settings['lower case'] = TRUE;
+}
+
+/**
+ * Let other modules modify the context handler before it is rendered.
+ *
+ * @param object $handler
+ * A handler for a current task and subtask.
+ * @param array $contexts
+ * An associative array of contexts.
+ * @param array $args
+ * An array for current args.
+ *
+ * @see ctools_context_handler_pre_render()
+ */
+function ctools_context_handler_pre_render($handler, $contexts, $args) {
+ $handler->conf['css_id'] = 'my-id';
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/sites/all/modules/ctools/ctools.info b/sites/all/modules/ctools/ctools.info
new file mode 100644
index 0000000..c9d0c63
--- /dev/null
+++ b/sites/all/modules/ctools/ctools.info
@@ -0,0 +1,27 @@
+name = Chaos tools
+description = A library of helpful tools by Merlin of Chaos.
+core = 7.x
+package = Chaos tool suite
+files[] = includes/context.inc
+files[] = includes/css-cache.inc
+files[] = includes/math-expr.inc
+files[] = includes/stylizer.inc
+
+; Tests.
+files[] = tests/context.test
+files[] = tests/css.test
+files[] = tests/css_cache.test
+files[] = tests/ctools.plugins.test
+files[] = tests/ctools.test
+files[] = tests/math_expression.test
+files[] = tests/math_expression_stack.test
+files[] = tests/object_cache.test
+files[] = tests/object_cache_unit.test
+files[] = tests/page_tokens.test
+
+; Information added by Drupal.org packaging script on 2018-02-24
+version = "7.x-1.14"
+core = "7.x"
+project = "ctools"
+datestamp = "1519455788"
+
diff --git a/sites/all/modules/ctools/ctools.install b/sites/all/modules/ctools/ctools.install
new file mode 100644
index 0000000..d50bc9f
--- /dev/null
+++ b/sites/all/modules/ctools/ctools.install
@@ -0,0 +1,299 @@
+ $t('CTools CSS Cache'),
+ 'severity' => REQUIREMENT_OK,
+ 'value' => $t('Exists'),
+ );
+
+ $path = 'public://ctools/css';
+ if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
+ $requirements['ctools_css_cache']['description'] = $t('The CTools CSS cache directory, %path could not be created due to a misconfigured files directory. Please ensure that the files directory is correctly configured and that the webserver has permission to create directories.', array('%path' => file_uri_target($path)));
+ $requirements['ctools_css_cache']['severity'] = REQUIREMENT_ERROR;
+ $requirements['ctools_css_cache']['value'] = $t('Unable to create');
+ }
+
+ if (!function_exists('error_get_last')) {
+ $requirements['ctools_php_52']['title'] = $t('CTools PHP requirements');
+ $requirements['ctools_php_52']['description'] = $t('CTools requires certain features only available in PHP 5.2.0 or higher.');
+ $requirements['ctools_php_52']['severity'] = REQUIREMENT_WARNING;
+ $requirements['ctools_php_52']['value'] = $t('PHP !version', array('!version' => phpversion()));
+ }
+ }
+
+ return $requirements;
+}
+
+/**
+ * Implements hook_schema().
+ */
+function ctools_schema() {
+ return ctools_schema_4();
+}
+
+/**
+ * Version 4 of the CTools schema.
+ */
+function ctools_schema_4() {
+ $schema = ctools_schema_3();
+
+ // Removed due to alternative database configuration issues.
+ // @see https://www.drupal.org/project/ctools/issues/2941920
+
+ return $schema;
+}
+
+/**
+ * Version 3 of the CTools schema.
+ */
+function ctools_schema_3() {
+ $schema = ctools_schema_2();
+
+ // Update the 'obj' field to be 128 bytes long:
+ $schema['ctools_object_cache']['fields']['obj']['length'] = 128;
+
+ return $schema;
+}
+
+/**
+ * Version 2 of the CTools schema.
+ */
+function ctools_schema_2() {
+ $schema = ctools_schema_1();
+
+ // Update the 'name' field to be 128 bytes long:
+ $schema['ctools_object_cache']['fields']['name']['length'] = 128;
+
+ // Update the 'data' field to be type 'blob'.
+ $schema['ctools_object_cache']['fields']['data'] = array(
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'description' => 'Serialized data being stored.',
+ 'serialize' => TRUE,
+ );
+
+ // DO NOT MODIFY THIS TABLE -- this definition is used to create the table.
+ // Changes to this table must be made in schema_3 or higher.
+ $schema['ctools_css_cache'] = array(
+ 'description' => 'A special cache used to store CSS that must be non-volatile.',
+ 'fields' => array(
+ 'cid' => array(
+ 'type' => 'varchar',
+ 'length' => '128',
+ 'description' => 'The CSS ID this cache object belongs to.',
+ 'not null' => TRUE,
+ ),
+ 'filename' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'The filename this CSS is stored in.',
+ ),
+ 'css' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'CSS being stored.',
+ 'serialize' => TRUE,
+ ),
+ 'filter' => array(
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'description' => 'Whether or not this CSS needs to be filtered.',
+ ),
+ ),
+ 'primary key' => array('cid'),
+ );
+
+ return $schema;
+}
+
+/**
+ * CTools' initial schema; separated for the purposes of updates.
+ *
+ * DO NOT MAKE CHANGES HERE. This schema version is locked.
+ */
+function ctools_schema_1() {
+ $schema['ctools_object_cache'] = array(
+ 'description' => t('A special cache used to store objects that are being edited; it serves to save state in an ordinarily stateless environment.'),
+ 'fields' => array(
+ 'sid' => array(
+ 'type' => 'varchar',
+ 'length' => '64',
+ 'not null' => TRUE,
+ 'description' => 'The session ID this cache object belongs to.',
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => '32',
+ 'not null' => TRUE,
+ 'description' => 'The name of the object this cache is attached to.',
+ ),
+ 'obj' => array(
+ 'type' => 'varchar',
+ 'length' => '32',
+ 'not null' => TRUE,
+ 'description' => 'The type of the object this cache is attached to; this essentially represents the owner so that several sub-systems can use this cache.',
+ ),
+ 'updated' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The time this cache was created or updated.',
+ ),
+ 'data' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Serialized data being stored.',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('sid', 'obj', 'name'),
+ 'indexes' => array('updated' => array('updated')),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function ctools_install() {
+ // Activate our custom cache handler for the CSS cache.
+ variable_set('cache_class_cache_ctools_css', 'CToolsCssCache');
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function ctools_uninstall() {
+ variable_del('cache_class_cache_ctools_css');
+}
+
+/**
+ * Enlarge the ctools_object_cache.name column to prevent truncation and weird
+ * errors.
+ */
+function ctools_update_6001() {
+ // Perform updates like this to reduce code duplication.
+ $schema = ctools_schema_2();
+
+ db_change_field('ctools_object_cache', 'name', 'name', $schema['ctools_object_cache']['fields']['name']);
+}
+
+/**
+ * Add the new css cache table.
+ */
+function ctools_update_6002() {
+ // Schema 2 is locked and should not be changed.
+ $schema = ctools_schema_2();
+
+ db_create_table('ctools_css_cache', $schema['ctools_css_cache']);
+}
+
+/**
+ * Take over for the panels_views module if it was on.
+ */
+function ctools_update_6003() {
+ $result = db_query('SELECT status FROM {system} WHERE name = :name', array(':name' => 'panels_views'))->fetchField();
+ if ($result) {
+ db_delete('system')->condition('name', 'panels_views')->execute();
+ module_enable(array('views_content'), TRUE);
+ }
+}
+
+/**
+ * Add primary key to the ctools_object_cache table.
+ */
+function ctools_update_6004() {
+ db_add_primary_key('ctools_object_cache', array('sid', 'obj', 'name'));
+ db_drop_index('ctools_object_cache', 'sid_obj_name');
+}
+
+/**
+ * Removed update.
+ */
+function ctools_update_6005() {
+ return array();
+}
+
+/**
+ * The ctools_custom_content table was originally here, but is now moved to
+ * its own module.
+ */
+function ctools_update_6007() {
+ $ret = array();
+ if (db_table_exists('ctools_custom_content')) {
+ // Enable the module to make everything as seamless as possible.
+ module_enable(array('ctools_custom_content'), TRUE);
+ }
+
+ return $ret;
+}
+
+/**
+ * The ctools_object_cache needs to be defined as a blob.
+ */
+function ctools_update_6008() {
+ db_delete('ctools_object_cache')
+ ->execute();
+
+ db_change_field('ctools_object_cache', 'data', 'data', array(
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'description' => 'Serialized data being stored.',
+ 'serialize' => TRUE,
+ )
+ );
+}
+
+/**
+ * Enable the custom CSS cache handler.
+ */
+function ctools_update_7000() {
+ variable_set('cache_class_cache_ctools_css', 'CToolsCssCache');
+}
+
+/**
+ * Increase the length of the ctools_object_cache.obj column.
+ */
+function ctools_update_7001() {
+ db_change_field('ctools_object_cache', 'obj', 'obj', array(
+ 'type' => 'varchar',
+ 'length' => '128',
+ 'not null' => TRUE,
+ 'description' => 'The type of the object this cache is attached to; this essentially represents the owner so that several sub-systems can use this cache.',
+ ));
+}
+
+/**
+ * Increase the length of the ctools_object_cache.name column to 255.
+ */
+function ctools_update_7002() {
+ // Removed due to alternative database configuration issues.
+ // @see https://www.drupal.org/project/ctools/issues/2941920
+}
+
+/**
+ * Revert the length of the ctools_object_cache.name column back to 128.
+ */
+function ctools_update_7003() {
+ db_delete('ctools_object_cache')->execute();
+ db_change_field('ctools_object_cache', 'name', 'name', array(
+ 'type' => 'varchar',
+ 'length' => '128',
+ 'not null' => TRUE,
+ 'description' => 'The name of the object this cache is attached to.',
+ ));
+}
diff --git a/sites/all/modules/ctools/ctools.module b/sites/all/modules/ctools/ctools.module
new file mode 100644
index 0000000..3a80580
--- /dev/null
+++ b/sites/all/modules/ctools/ctools.module
@@ -0,0 +1,1202 @@
+ 7.x-1.x.
+ *
+ * To define a specific version of CTools as a dependency for another module,
+ * simply include a dependency line in that module's info file, e.g.:
+ * ; Requires CTools v7.x-1.4 or newer.
+ * dependencies[] = ctools (>=1.4)
+ */
+define('CTOOLS_MODULE_VERSION', '7.x-1.13');
+
+/**
+ * Test the CTools API version.
+ *
+ * This function can always be used to safely test if CTools has the minimum
+ * API version that your module can use. It can also try to protect you from
+ * running if the CTools API version is too new, but if you do that you need
+ * to be very quick about watching CTools API releases and release new versions
+ * of your software as soon as the new release is made, or people might end up
+ * updating CTools and having your module shut down without any recourse.
+ *
+ * It is recommended that every hook of your module that might use CTools or
+ * might lead to a use of CTools be guarded like this:
+ *
+ * @code
+ * if (!module_invoke('ctools', 'api_version', '1.0')) {
+ * return;
+ * }
+ * @endcode
+ *
+ * Note that some hooks such as _menu() or _theme() must return an array().
+ *
+ * You can use it in your hook_requirements to report this error condition
+ * like this:
+ *
+ * @code
+ * define('MODULENAME_MINIMUM_CTOOLS_API_VERSION', '1.0');
+ * define('MODULENAME_MAXIMUM_CTOOLS_API_VERSION', '1.1');
+ *
+ * function MODULENAME_requirements($phase) {
+ * $requirements = array();
+ * if (!module_invoke('ctools', 'api_version', MODULENAME_MINIMUM_CTOOLS_API_VERSION, MODULENAME_MAXIMUM_CTOOLS_API_VERSION)) {
+ * $requirements['MODULENAME_ctools'] = array(
+ * 'title' => $t('MODULENAME required Chaos Tool Suite (CTools) API Version'),
+ * 'value' => t('Between @a and @b', array('@a' => MODULENAME_MINIMUM_CTOOLS_API_VERSION, '@b' => MODULENAME_MAXIMUM_CTOOLS_API_VERSION)),
+ * 'severity' => REQUIREMENT_ERROR,
+ * );
+ * }
+ * return $requirements;
+ * }
+ * @endcode
+ *
+ * Please note that the version is a string, not an floating point number.
+ * This will matter once CTools reaches version 1.10.
+ *
+ * A CTools API changes history will be kept in API.txt. Not every new
+ * version of CTools will necessarily update the API version.
+ * @param $minimum
+ * The minimum version of CTools necessary for your software to run with it.
+ * @param $maximum
+ * The maximum version of CTools allowed for your software to run with it.
+ *
+ * @return bool
+ * TRUE if the running ctools is usable, FALSE otherwise.
+ */
+function ctools_api_version($minimum, $maximum = NULL) {
+ if (version_compare(CTOOLS_API_VERSION, $minimum, '<')) {
+ return FALSE;
+ }
+
+ if (isset($maximum) && version_compare(CTOOLS_API_VERSION, $maximum, '>')) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+// -----------------------------------------------------------------------
+// General utility functions.
+/**
+ * Include .inc files as necessary.
+ *
+ * This fuction is helpful for including .inc files for your module. The
+ * general case is including ctools funcitonality like this:
+ *
+ * @code
+ * ctools_include('plugins');
+ * @endcode
+ *
+ * Similar funcitonality can be used for other modules by providing the $module
+ * and $dir arguments like this:
+ *
+ * @code
+ * // include mymodule/includes/import.inc
+ * ctools_include('import', 'mymodule');
+ * // include mymodule/plugins/foobar.inc
+ * ctools_include('foobar', 'mymodule', 'plugins');
+ * @endcode
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+
+function ctools_include($file, $module = 'ctools', $dir = 'includes') {
+ static $used = array();
+
+ $dir = '/' . ($dir ? $dir . '/' : '');
+
+ if (!isset($used[$module][$dir][$file])) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "$dir$file.inc";
+ $used[$module][$dir][$file] = TRUE;
+ }
+}
+
+/**
+ * Include .inc files in a form context.
+ *
+ * This is a variant of ctools_include that will save information in the
+ * the form_state so that cached forms will properly include things.
+ */
+function ctools_form_include(&$form_state, $file, $module = 'ctools', $dir = 'includes') {
+ if (!isset($form_state['build_info']['args'])) {
+ $form_state['build_info']['args'] = array();
+ }
+
+ $dir = '/' . ($dir ? $dir . '/' : '');
+ form_load_include($form_state, 'inc', $module, $dir . $file);
+}
+
+/**
+ * Add an arbitrary path to the $form_state so it can work with form cache.
+ *
+ * The module_load_include() function uses an unfortunately annoying syntax to
+ * work, making it difficult to translate the more simple $path + $file syntax.
+ */
+function ctools_form_include_file(&$form_state, $filename) {
+ if (!isset($form_state['build_info']['args'])) {
+ $form_state['build_info']['args'] = array();
+ }
+
+ // Now add this to the build info files so that AJAX requests will know to load it.
+ $form_state['build_info']['files']["$filename"] = $filename;
+ require_once DRUPAL_ROOT . '/' . $filename;
+}
+
+/**
+ * Provide the proper path to an image as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $image
+ * The base file name (with extension) of the image to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ *
+ * @return string
+ * A string containing the appropriate path from drupal root.
+ */
+function ctools_image_path($image, $module = 'ctools', $dir = 'images') {
+ return drupal_get_path('module', $module) . "/$dir/" . $image;
+}
+
+/**
+ * Include css files as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_add_css($file, $module = 'ctools', $dir = 'css') {
+ drupal_add_css(drupal_get_path('module', $module) . "/$dir/$file.css");
+}
+
+/**
+ * Format a css file name for use with $form['#attached']['css'].
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @code
+ * $form['#attached']['css'] = array(ctools_attach_css('collapsible-div'));
+ * $form['#attached']['css'][ctools_attach_css('collapsible-div')] = array('preprocess' => FALSE);
+ * @endcode
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ *
+ * @return string
+ * A string containing the appropriate path from drupal root.
+ */
+function ctools_attach_css($file, $module = 'ctools', $dir = 'css') {
+ return drupal_get_path('module', $module) . "/$dir/$file.css";
+}
+
+/**
+ * Include js files as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_add_js($file, $module = 'ctools', $dir = 'js') {
+ drupal_add_js(drupal_get_path('module', $module) . "/$dir/$file.js");
+}
+
+/**
+ * Format a javascript file name for use with $form['#attached']['js'].
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @code
+ * $form['#attached']['js'] = array(ctools_attach_js('auto-submit'));
+ * @endcode
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ *
+ * @return string
+ * A string containing the appropriate path from drupal root.
+ */
+function ctools_attach_js($file, $module = 'ctools', $dir = 'js') {
+ return drupal_get_path('module', $module) . "/$dir/$file.js";
+}
+
+/**
+ * Get a list of roles in the system.
+ *
+ * @return
+ * An array of role names keyed by role ID.
+ *
+ * @deprecated
+ * user_roles() should be used instead.
+ */
+function ctools_get_roles() {
+ return user_roles();
+}
+
+/**
+ * Parse integer sequences of the form "x,y,z" or "x+y+z" into separate values.
+ *
+ * A string with integers separated by comma (,) is reported as an 'and' set;
+ * separation by a plus sign (+) or a space ( ) is an 'or' set. The meaning
+ * of this is up to the caller. Negative or fractional numbers are not
+ * recognised.
+ *
+ * Additional space characters within or around the sequence are not allowed.
+ *
+ * @param $str
+ * The string to parse.
+ *
+ * @return object
+ * An object containing the properties:
+ *
+ * - operator: Either 'and' or 'or' when there are multiple matched values.
+ * Absent when invalid_input is TRUE or there is only one value.
+ * - value: An array of integers (never strings) from $str. An empty array is
+ * returned if the input is empty. A single integer input is returned
+ * as a single value, but no 'operator' is defined.
+ * - invalid_input: TRUE if input could not be parsed and the values array
+ * will contain just -1. This property is otherwise absent.
+ */
+function ctools_break_phrase($str) {
+ $object = new stdClass();
+
+ if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
+ // The '+' character in a query string may be parsed as ' '.
+ $object->operator = 'or';
+ $object->value = preg_split('/[+ ]/', $str);
+ }
+ elseif (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
+ $object->operator = 'and';
+ $object->value = explode(',', $str);
+ }
+
+ // Keep an 'error' value if invalid strings were given.
+ if (!empty($str) && (empty($object->value) || !is_array($object->value))) {
+ $object->value = array(-1);
+ $object->invalid_input = TRUE;
+ return $object;
+ }
+
+ if (empty($object->value)) {
+ $object->value = array();
+ }
+
+ // Doubly ensure that all values are numeric only.
+ foreach ($object->value as $id => $value) {
+ $object->value[$id] = (int) $value;
+ }
+
+ return $object;
+}
+
+/**
+ * Set a token/value pair to be replaced later in the request, specifically in
+ * ctools_page_token_processing().
+ *
+ * @param string $token
+ * The token to be replaced later, during page rendering. This should
+ * ideally be a string inside of an HTML comment, so that if there is
+ * no replacement, the token will not render on the page.
+ * If $token is NULL, the token set is not changed, but is still
+ * returned.
+ * @param string $type
+ * The type of the token. Can be either 'variable', which will pull data
+ * directly from the page variables, or 'callback', which causes a function
+ * to be called to calculate the value. No other values are supported.
+ * @param string|array $argument
+ * For $type of:
+ * - 'variable': argument should be the key to fetch from the $variables.
+ * - 'callback': then it should either be the callback function name as a
+ * string, or an array that will be sent to call_user_func_array(). Argument
+ * arrays must not use array keys (i.e. $a[0] is the first and $a[1] the
+ * second element, etc.)
+ *
+ * @return array
+ * A array of token/variable names to be replaced.
+ */
+function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
+ $tokens = &drupal_static('ctools_set_page_token', array());
+
+ if (isset($token)) {
+ $tokens[$token] = array($type, $argument);
+ }
+ return $tokens;
+}
+
+/**
+ * Reset the defined page tokens within this request.
+ *
+ * Introduced for simpletest purposes. Normally not needed.
+ */
+function ctools_reset_page_tokens() {
+ drupal_static_reset('ctools_set_page_token');
+}
+
+/**
+ * Set a replacement token from the containing element's children during #post_render.
+ *
+ * This function can be used like this:
+ * $token = ctools_set_variable_token('tabs');
+ *
+ * The token "" would then be replaced by the value of
+ * this element's (sibling) render array key 'tabs' during post-render (or be
+ * deleted if there was no such key by that point).
+ *
+ * @param string $token
+ * The token string for the page callback, e.g. 'title'.
+ *
+ * @return string
+ * The constructed token.
+ *
+ * @see ctools_set_callback_token()
+ * @see ctools_page_token_processing()
+ */
+function ctools_set_variable_token($token) {
+ $string = '';
+ ctools_set_page_token($string, 'variable', $token);
+ return $string;
+}
+
+/**
+ * Set a replacement token from the value of a function during #post_render.
+ *
+ * This function can be used like this:
+ * $token = ctools_set_callback_token('id', 'mymodule_myfunction');
+ *
+ * Or this (from its use in ctools_page_title_content_type_render):
+ * $token = ctools_set_callback_token('title', array(
+ * 'ctools_page_title_content_type_token', $conf['markup'], $conf['id'], $conf['class']
+ * )
+ * );
+ *
+ * The token (e.g: "")
+ * would then be replaced during post-render by the return value of:
+ *
+ * ctools_page_title_content_type_token($value_markup, $value_id, $value_class);
+ *
+ * @param string $token
+ * The token string for the page callback, e.g. 'title'.
+ *
+ * @param string|array $callback
+ * For callback functions that require no args, the name of the function as a
+ * string; otherwise an array of two or more elements: the function name
+ * followed by one or more function arguments.
+ *
+ * NB: the value of $callback must be a procedural (non-class) function that
+ * passes the php function_exists() check.
+ *
+ * The callback function itself will be called with args dependent
+ * on $callback. If:
+ * - $callback is a string, the function is called with a reference to the
+ * render array;
+ * - $callback is an array, the function is called with $callback merged
+ * with an array containing a reference to the render array.
+ *
+ * @return string
+ * The constructed token.
+ *
+ * @see ctools_set_variable_token()
+ * @see ctools_page_token_processing()
+ */
+function ctools_set_callback_token($token, $callback) {
+ // If the callback uses arguments they are considered in the token.
+ if (is_array($callback)) {
+ $token .= '-' . md5(serialize($callback));
+ }
+ $string = '';
+ ctools_set_page_token($string, 'callback', $callback);
+ return $string;
+}
+
+/**
+ * Tell CTools that sidebar blocks should not be rendered.
+ *
+ * It is often desirable to not display sidebars when rendering a page,
+ * particularly when using Panels. This informs CTools to alter out any
+ * sidebar regions during block render.
+ */
+function ctools_set_no_blocks($blocks = FALSE) {
+ $status = &drupal_static(__FUNCTION__, TRUE);
+ $status = $blocks;
+}
+
+/**
+ * Wrapper function to create UUIDs via ctools, falls back on UUID module
+ * if it is enabled. This code is a copy of uuid.inc from the uuid module.
+ *
+ * @see http://php.net/uniqid#65879
+ */
+function ctools_uuid_generate() {
+ if (!module_exists('uuid')) {
+ ctools_include('uuid');
+
+ $callback = drupal_static(__FUNCTION__);
+
+ if (empty($callback)) {
+ if (function_exists('uuid_create') && !function_exists('uuid_make')) {
+ $callback = '_ctools_uuid_generate_pecl';
+ }
+ elseif (function_exists('com_create_guid')) {
+ $callback = '_ctools_uuid_generate_com';
+ }
+ else {
+ $callback = '_ctools_uuid_generate_php';
+ }
+ }
+ return $callback();
+ }
+ else {
+ return uuid_generate();
+ }
+}
+
+/**
+ * Check that a string appears to be in the format of a UUID.
+ *
+ * @see http://drupal.org/project/uuid
+ *
+ * @param $uuid
+ * The string to test.
+ *
+ * @return
+ * TRUE if the string is well formed.
+ */
+function ctools_uuid_is_valid($uuid = '') {
+ if (empty($uuid)) {
+ return FALSE;
+ }
+ if (function_exists('uuid_is_valid') || module_exists('uuid')) {
+ return uuid_is_valid($uuid);
+ }
+ else {
+ ctools_include('uuid');
+ return uuid_is_valid($uuid);
+ }
+}
+
+/**
+ * Add an array of classes to the body.
+ *
+ * @param mixed $classes
+ * A string or an array of class strings to add.
+ * @param string $hook
+ * The theme hook to add the class to. The default is 'html' which will
+ * affect the body tag.
+ */
+function ctools_class_add($classes, $hook = 'html') {
+ if (!is_array($classes)) {
+ $classes = array($classes);
+ }
+
+ $static = &drupal_static('ctools_process_classes', array());
+ if (!isset($static[$hook]['add'])) {
+ $static[$hook]['add'] = array();
+ }
+ foreach ($classes as $class) {
+ $static[$hook]['add'][] = $class;
+ }
+}
+
+/**
+ * Remove an array of classes from the body.
+ *
+ * @param mixed $classes
+ * A string or an array of class strings to remove.
+ * @param string $hook
+ * The theme hook to remove the class from. The default is 'html' which will
+ * affect the body tag.
+ */
+function ctools_class_remove($classes, $hook = 'html') {
+ if (!is_array($classes)) {
+ // @todo Consider using explode(' ', $classes);
+ // @todo Consider checking that $classes is a string before adding.
+ $classes = array($classes);
+ }
+
+ $static = &drupal_static('ctools_process_classes', array());
+ if (!isset($static[$hook]['remove'])) {
+ $static[$hook]['remove'] = array();
+ }
+ foreach ($classes as $class) {
+ $static[$hook]['remove'][] = $class;
+ }
+}
+
+/**
+ * Reset the storage used for ctools_class_add and ctools_class_remove.
+ *
+ * @see ctools_class_add()
+ * @see ctools_class_remove()
+ */
+function ctools_class_reset() {
+ drupal_static_reset('ctools_process_classes');
+}
+
+/**
+ * Return the classes for the body (added by ctools_class_add).
+ *
+ * @return array
+ * A copy of the array of classes to add to the body tag. If none have been
+ * added, this will be an empty array.
+ *
+ * @see ctools_class_add()
+ */
+function ctools_get_classes() {
+ return drupal_static('ctools_process_classes', array());
+}
+
+// -----------------------------------------------------------------------
+// Drupal core hooks.
+/**
+ * Implement hook_init to keep our global CSS at the ready.
+ */
+
+function ctools_init() {
+ ctools_add_css('ctools');
+ // If we are sure that CTools' AJAX is in use, change the error handling.
+ if (!empty($_REQUEST['ctools_ajax'])) {
+ ini_set('display_errors', 0);
+ register_shutdown_function('ctools_shutdown_handler');
+ }
+
+ // Clear plugin cache on the module page submit.
+ if ($_GET['q'] == 'admin/modules/list/confirm' && !empty($_POST)) {
+ cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+ }
+}
+
+/**
+ * Shutdown handler used during ajax operations to help catch fatal errors.
+ */
+function ctools_shutdown_handler() {
+ if (function_exists('error_get_last') && ($error = error_get_last())) {
+ switch ($error['type']) {
+ case E_ERROR:
+ case E_CORE_ERROR:
+ case E_COMPILE_ERROR:
+ case E_USER_ERROR:
+ // Do this manually because including files here is dangerous.
+ $commands = array(
+ array(
+ 'command' => 'alert',
+ 'title' => t('Error'),
+ 'text' => t('Unable to complete operation. Fatal error in @file on line @line: @message', array(
+ '@file' => $error['file'],
+ '@line' => $error['line'],
+ '@message' => $error['message'],
+ )),
+ ),
+ );
+
+ // Change the status code so that the client will read the AJAX returned.
+ header('HTTP/1.1 200 OK');
+ drupal_json($commands);
+ }
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function ctools_theme() {
+ ctools_include('utility');
+ $items = array();
+ ctools_passthrough('ctools', 'theme', $items);
+ return $items;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function ctools_menu() {
+ ctools_include('utility');
+ $items = array();
+ ctools_passthrough('ctools', 'menu', $items);
+ return $items;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function ctools_permission() {
+ return array(
+ 'use ctools import' => array(
+ 'title' => t('Use CTools importer'),
+ 'description' => t('The import functionality allows users to execute arbitrary PHP code, so extreme caution must be taken.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_cron. Clean up old caches.
+ */
+function ctools_cron() {
+ ctools_include('utility');
+ $items = array();
+ ctools_passthrough('ctools', 'cron', $items);
+}
+
+/**
+ * Implements hook_flush_caches().
+ */
+function ctools_flush_caches() {
+ // Only return the CSS cache bin if it has been activated, to avoid
+ // drupal_flush_all_caches() from trying to truncate a non-existing table.
+ return variable_get('cache_class_cache_ctools_css', FALSE) ? array('cache_ctools_css') : array();
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function ctools_element_info_alter(&$type) {
+ ctools_include('dependent');
+ ctools_dependent_element_info_alter($type);
+}
+
+/**
+ * Implementation of hook_file_download()
+ *
+ * When using the private file system, we have to let Drupal know it's ok to
+ * download CSS and image files from our temporary directory.
+ */
+function ctools_file_download($filepath) {
+ if (strpos($filepath, 'ctools') === 0) {
+ $mime = file_get_mimetype($filepath);
+ // For safety's sake, we allow only text and images.
+ if (strpos($mime, 'text') === 0 || strpos($mime, 'image') === 0) {
+ return array('Content-type:' . $mime);
+ }
+ }
+}
+
+/**
+ * Implements hook_registry_files_alter().
+ *
+ * Alter the registry of files to automagically include all classes in
+ * class-based plugins.
+ */
+function ctools_registry_files_alter(&$files, $indexed_modules) {
+ ctools_include('registry');
+ return _ctools_registry_files_alter($files, $indexed_modules);
+}
+
+// -----------------------------------------------------------------------
+// FAPI hooks that must be in the .module file.
+/**
+ * Alter the comment form to get a little more control over it.
+ */
+
+function ctools_form_comment_form_alter(&$form, &$form_state) {
+ if (!empty($form_state['ctools comment alter'])) {
+ // Force the form to post back to wherever we are.
+ $form['#action'] = url($_GET['q'], array('fragment' => 'comment-form'));
+ if (empty($form['#submit'])) {
+ $form['#submit'] = array('comment_form_submit');
+ }
+ $form['#submit'][] = 'ctools_node_comment_form_submit';
+ }
+}
+
+function ctools_node_comment_form_submit(&$form, &$form_state) {
+ $form_state['redirect'][0] = $_GET['q'];
+}
+
+// -----------------------------------------------------------------------
+// CTools hook implementations.
+/**
+ * Implementation of hook_ctools_plugin_directory() to let the system know
+ * where all our own plugins are.
+ */
+
+function ctools_ctools_plugin_directory($owner, $plugin_type) {
+ if ($owner == 'ctools') {
+ return 'plugins/' . $plugin_type;
+ }
+}
+
+/**
+ * Implements hook_ctools_plugin_type().
+ */
+function ctools_ctools_plugin_type() {
+ ctools_include('utility');
+ $items = array();
+ // Add all the plugins that have their own declaration space elsewhere.
+ ctools_passthrough('ctools', 'plugin-type', $items);
+
+ return $items;
+}
+
+// -----------------------------------------------------------------------
+// Drupal theme preprocess hooks that must be in the .module file.
+/**
+ * A theme preprocess function to automatically allow panels-based node
+ * templates based upon input when the panel was configured.
+ */
+
+function ctools_preprocess_node(&$vars) {
+ // The 'ctools_template_identifier' attribute of the node is added when the pane is
+ // rendered.
+ if (!empty($vars['node']->ctools_template_identifier)) {
+ $vars['ctools_template_identifier'] = check_plain($vars['node']->ctools_template_identifier);
+ $vars['theme_hook_suggestions'][] = 'node__panel__' . check_plain($vars['node']->ctools_template_identifier);
+ }
+}
+
+/**
+ * Implements hook_page_alter().
+ *
+ * Last ditch attempt to remove sidebar regions if the "no blocks"
+ * functionality has been activated.
+ *
+ * @see ctools_block_list_alter()
+ */
+function ctools_page_alter(&$page) {
+ $check = drupal_static('ctools_set_no_blocks', TRUE);
+ if (!$check) {
+ foreach ($page as $region_id => $region) {
+ // @todo -- possibly we can set configuration for this so that users can
+ // specify which blocks will not get rendered.
+ if (strpos($region_id, 'sidebar') !== FALSE) {
+ unset($page[$region_id]);
+ }
+ }
+ }
+ $page['#post_render'][] = 'ctools_page_token_processing';
+}
+
+/**
+ * A theme post_render callback to allow content type plugins to use page
+ * template variables which are not yet available when the content type is
+ * rendered.
+ */
+function ctools_page_token_processing($children, $elements) {
+ $tokens = ctools_set_page_token();
+ if (!empty($tokens)) {
+ foreach ($tokens as $token => $key) {
+ list($type, $argument) = $key;
+ switch ($type) {
+ case 'variable':
+ $tokens[$token] = isset($elements[$argument]) ? $elements[$argument] : '';
+ break;
+
+ case 'callback':
+ if (is_string($argument) && function_exists($argument)) {
+ $tokens[$token] = $argument($elements);
+ }
+ if (is_array($argument) && function_exists($argument[0])) {
+ $function = array_shift($argument);
+ $argument = array_merge(array(&$elements), $argument);
+ $tokens[$token] = call_user_func_array($function, $argument);
+ }
+ break;
+ }
+ }
+ $children = strtr($children, $tokens);
+ }
+ return $children;
+}
+
+/**
+ * Implements hook_process().
+ *
+ * Add and remove CSS classes from the variables array. We use process so that
+ * we alter anything added in the preprocess hooks.
+ */
+function ctools_process(&$variables, $hook) {
+ if (!isset($variables['classes'])) {
+ return;
+ }
+
+ $classes = ctools_get_classes();
+
+ // Process the classses to add.
+ if (!empty($classes[$hook]['add'])) {
+ $add_classes = array_map('drupal_clean_css_identifier', $classes[$hook]['add']);
+ $variables['classes_array'] = array_unique(array_merge($variables['classes_array'], $add_classes));
+ }
+
+ // Process the classes to remove.
+ if (!empty($classes[$hook]['remove'])) {
+ $remove_classes = array_map('drupal_clean_css_identifier', $classes[$hook]['remove']);
+ $variables['classes_array'] = array_diff($variables['classes_array'], $remove_classes);
+ }
+
+ // Since this runs after template_process(), we need to re-implode the
+ // classes array.
+ $variables['classes'] = implode(' ', $variables['classes_array']);
+}
+
+// -----------------------------------------------------------------------
+// Menu callbacks that must be in the .module file.
+/**
+ * Determine if the current user has access via a plugin.
+ *
+ * This function is meant to be embedded in the Drupal menu system, and
+ * therefore is in the .module file since sub files can't be loaded, and
+ * takes arguments a little bit more haphazardly than ctools_access().
+ *
+ * @param $access
+ * An access control array which contains the following information:
+ * - 'logic': and or or. Whether all tests must pass or one must pass.
+ * - 'plugins': An array of access plugins. Each contains:
+ * - - 'name': The name of the plugin
+ * - - 'settings': The settings from the plugin UI.
+ * - - 'context': Which context to use.
+ * @param ...
+ * zero or more context arguments generated from argument plugins. These
+ * contexts must have an 'id' attached to them so that they can be
+ * properly associated. The argument plugin system should set this, but
+ * if the context is coming from elsewhere it will need to be set manually.
+ *
+ * @return
+ * TRUE if access is granted, false if otherwise.
+ */
+
+function ctools_access_menu($access) {
+ // Short circuit everything if there are no access tests.
+ if (empty($access['plugins'])) {
+ return TRUE;
+ }
+
+ $contexts = array();
+ foreach (func_get_args() as $arg) {
+ if (is_object($arg) && get_class($arg) == 'ctools_context') {
+ $contexts[$arg->id] = $arg;
+ }
+ }
+
+ ctools_include('context');
+ return ctools_access($access, $contexts);
+}
+
+/**
+ * Determine if the current user has access via checks to multiple different
+ * permissions.
+ *
+ * This function is a thin wrapper around user_access that allows multiple
+ * permissions to be easily designated for use on, for example, a menu callback.
+ *
+ * @param ...
+ * An indexed array of zero or more permission strings to be checked by
+ * user_access().
+ *
+ * @return bool
+ * Iff all checks pass will this function return TRUE. If an invalid argument
+ * is passed (e.g., not a string), this function errs on the safe said and
+ * returns FALSE.
+ */
+function ctools_access_multiperm() {
+ foreach (func_get_args() as $arg) {
+ if (!is_string($arg) || !user_access($arg)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Check to see if the incoming menu item is js capable or not.
+ *
+ * This can be used as %ctools_js as part of a path in hook menu. CTools
+ * ajax functions will automatically change the phrase 'nojs' to 'ajax'
+ * when it attaches ajax to a link. This can be used to autodetect if
+ * that happened.
+ */
+function ctools_js_load($js) {
+ if ($js == 'ajax') {
+ return TRUE;
+ }
+ return 0;
+}
+
+/**
+ * Provides the default value for %ctools_js.
+ *
+ * This allows drupal_valid_path() to work with %ctools_js.
+ */
+function ctools_js_to_arg($arg) {
+ return empty($arg) || $arg == '%' ? 'nojs' : $arg;
+}
+
+/**
+ * Menu _load hook.
+ *
+ * This function will be called to load an object as a replacement for
+ * %ctools_export_ui in menu paths.
+ */
+function ctools_export_ui_load($item_name, $plugin_name) {
+ $return = &drupal_static(__FUNCTION__, FALSE);
+
+ if (!$return) {
+ ctools_include('export-ui');
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ return $handler->load_item($item_name);
+ }
+ }
+
+ return $return;
+}
+
+// -----------------------------------------------------------------------
+// Caching callbacks on behalf of export-ui.
+/**
+ * Menu access callback for various tasks of export-ui.
+ */
+function ctools_export_ui_task_access($plugin_name, $op, $item = NULL) {
+ ctools_include('export-ui');
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ return $handler->access($op, $item);
+ }
+
+ // Deny access if the handler cannot be found.
+ return FALSE;
+}
+
+/**
+ * Callback for access control ajax form on behalf of export ui.
+ *
+ * Returns the cached access config and contexts used.
+ * Note that this is assuming that access will be in $item->access -- if it
+ * is not, an export UI plugin will have to make its own callbacks.
+ */
+function ctools_export_ui_ctools_access_get($argument) {
+ ctools_include('export-ui');
+ list($plugin_name, $key) = explode(':', $argument, 2);
+
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ ctools_include('context');
+ $item = $handler->edit_cache_get($key);
+ if (!$item) {
+ $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+ }
+
+ $contexts = ctools_context_load_contexts($item);
+ return array($item->access, $contexts);
+ }
+}
+
+/**
+ * Callback for access control ajax form on behalf of export ui.
+ *
+ * Returns the cached access config and contexts used.
+ * Note that this is assuming that access will be in $item->access -- if it
+ * is not, an export UI plugin will have to make its own callbacks.
+ */
+function ctools_export_ui_ctools_access_set($argument, $access) {
+ ctools_include('export-ui');
+ list($plugin_name, $key) = explode(':', $argument, 2);
+
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ ctools_include('context');
+ $item = $handler->edit_cache_get($key);
+ if (!$item) {
+ $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+ }
+ $item->access = $access;
+ return $handler->edit_cache_set_key($item, $key);
+ }
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function ctools_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ ctools_include('menu');
+ _ctools_menu_add_dynamic_items($data, $router_item, $root_path);
+}
+
+/**
+ * Implements hook_block_list_alter().
+ *
+ * Used to potentially remove blocks.
+ * This exists in order to replicate Drupal 6's "no blocks" functionality.
+ */
+function ctools_block_list_alter(&$blocks) {
+ $check = drupal_static('ctools_set_no_blocks', TRUE);
+ if (!$check) {
+ foreach ($blocks as $block_id => $block) {
+ // @todo -- possibly we can set configuration for this so that users can
+ // specify which blocks will not get rendered.
+ if (strpos($block->region, 'sidebar') !== FALSE) {
+ unset($blocks[$block_id]);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_modules_enabled().
+ *
+ * Clear caches for detecting new plugins.
+ */
+function ctools_modules_enabled($modules) {
+ ctools_include('plugins');
+ ctools_get_plugins_reset();
+ cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+}
+
+/**
+ * Implements hook_modules_disabled().
+ *
+ * Clear caches for removing disabled plugins.
+ */
+function ctools_modules_disabled($modules) {
+ ctools_include('plugins');
+ ctools_get_plugins_reset();
+ cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+}
+
+/**
+ * Menu theme callback.
+ *
+ * This simply ensures that Panels ajax calls are rendered in the same
+ * theme as the original page to prevent .css file confusion.
+ *
+ * To use this, set this as the theme callback on AJAX related menu
+ * items. Since the ajax page state won't be sent during ajax requests,
+ * it should be safe to use even if ajax isn't invoked.
+ */
+function ctools_ajax_theme_callback() {
+ if (!empty($_POST['ajax_page_state']['theme'])) {
+ return $_POST['ajax_page_state']['theme'];
+ }
+}
+
+/**
+ * Implements hook_ctools_entity_context_alter().
+ */
+function ctools_ctools_entity_context_alter(&$plugin, &$entity, $plugin_id) {
+ ctools_include('context');
+ switch ($plugin_id) {
+ case 'entity_id:taxonomy_term':
+ $plugin['no ui'] = TRUE;
+ break;
+
+ case 'entity:user':
+ $plugin = ctools_get_context('user');
+ unset($plugin['no ui']);
+ unset($plugin['no required context ui']);
+ break;
+ }
+
+ // Apply restrictions on taxonomy term reverse relationships whose
+ // restrictions are in the settings on the field.
+ if (!empty($plugin['parent']) &&
+ $plugin['parent'] == 'entity_from_field' &&
+ !empty($plugin['reverse']) &&
+ $plugin['to entity'] == 'taxonomy_term') {
+ $field = field_info_field($plugin['field name']);
+ if (isset($field['settings']['allowed_values'][0]['vocabulary'])) {
+ $plugin['required context']->restrictions = array('vocabulary' => array($field['settings']['allowed_values'][0]['vocabulary']));
+ }
+ }
+}
+
+/**
+ * Implements hook_field_create_field().
+ */
+function ctools_field_create_field($field) {
+ ctools_flush_field_caches();
+}
+
+/**
+ * Implements hook_field_create_instance().
+ */
+function ctools_field_create_instance($instance) {
+ ctools_flush_field_caches();
+}
+
+/**
+ * Implements hook_field_delete_field().
+ */
+function ctools_field_delete_field($field) {
+ ctools_flush_field_caches();
+}
+
+/**
+ * Implements hook_field_delete_instance().
+ */
+function ctools_field_delete_instance($instance) {
+ ctools_flush_field_caches();
+}
+
+/**
+ * Implements hook_field_update_field().
+ */
+function ctools_field_update_field($field, $prior_field, $has_data) {
+ ctools_flush_field_caches();
+}
+
+/**
+ * Implements hook_field_update_instance().
+ */
+function ctools_field_update_instance($instance, $prior_instance) {
+ ctools_flush_field_caches();
+}
+
+/**
+ * Clear field related caches.
+ */
+function ctools_flush_field_caches() {
+ // Clear caches of 'Entity field' content type plugin.
+ cache_clear_all('ctools_entity_field_content_type_content_types', 'cache');
+}
diff --git a/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.info b/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.info
new file mode 100644
index 0000000..84a922f
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.info
@@ -0,0 +1,13 @@
+name = Custom rulesets
+description = Create custom, exportable, reusable access rulesets for applications like Panels.
+core = 7.x
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+dependencies[] = ctools
+
+; Information added by Drupal.org packaging script on 2018-02-24
+version = "7.x-1.14"
+core = "7.x"
+project = "ctools"
+datestamp = "1519455788"
+
diff --git a/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.install b/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.install
new file mode 100644
index 0000000..70afb3c
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.install
@@ -0,0 +1,86 @@
+ 'Contains exportable customized access rulesets.',
+ 'export' => array(
+ 'identifier' => 'ruleset',
+ 'bulk export' => TRUE,
+ 'primary key' => 'rsid',
+ 'api' => array(
+ 'owner' => 'ctools_access_ruleset',
+ 'api' => 'ctools_rulesets',
+ 'minimum_version' => 1,
+ 'current_version' => 1,
+ ),
+ ),
+ 'fields' => array(
+ 'rsid' => array(
+ 'type' => 'serial',
+ 'description' => 'A database primary key to ensure uniqueness',
+ 'not null' => TRUE,
+ 'no export' => TRUE,
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Unique ID for this ruleset. Used to identify it programmatically.',
+ ),
+ 'admin_title' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Administrative title for this ruleset.',
+ ),
+ 'admin_description' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Administrative description for this ruleset.',
+ 'object default' => '',
+ ),
+ 'requiredcontexts' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Any required contexts for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ 'contexts' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Any embedded contexts for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ 'relationships' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Any relationships for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ 'access' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'The actual group of access plugins for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ ),
+ 'primary key' => array('rsid'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.module b/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.module
new file mode 100644
index 0000000..47a5d87
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_access_ruleset/ctools_access_ruleset.module
@@ -0,0 +1,85 @@
+ array(
+ 'title' => t('Administer access rulesets'),
+ 'description' => t('Add, delete and edit custom access rulesets.'),
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_ctools_plugin_directory() to let the system know
+ * we implement task and task_handler plugins.
+ */
+function ctools_access_ruleset_ctools_plugin_directory($module, $plugin) {
+ // Most of this module is implemented as an export ui plugin, and the
+ // rest is in ctools/includes/ctools_access_ruleset.inc.
+ if ($module == 'ctools' && ($plugin == 'export_ui' || $plugin == 'access')) {
+ return 'plugins/' . $plugin;
+ }
+}
+
+/**
+ * Implementation of hook_panels_dashboard_blocks().
+ *
+ * Adds page information to the Panels dashboard.
+ */
+function ctools_access_ruleset_panels_dashboard_blocks(&$vars) {
+ $vars['links']['ctools_access_ruleset'] = array(
+ 'title' => l(t('Custom ruleset'), 'admin/structure/ctools-rulesets/add'),
+ 'description' => t('Custom rulesets are combinations of access plugins you can use for access control, selection criteria and pane visibility.'),
+ );
+
+ // Load all mini panels and their displays.
+ ctools_include('export');
+ $items = ctools_export_crud_load_all('ctools_access_ruleset');
+ $count = 0;
+ $rows = array();
+
+ foreach ($items as $item) {
+ $rows[] = array(
+ check_plain($item->admin_title),
+ array(
+ 'data' => l(t('Edit'), "admin/structure/ctools-rulesets/list/$item->name/edit"),
+ 'class' => 'links',
+ ),
+ );
+
+ // Only show 10.
+ if (++$count >= 10) {
+ break;
+ }
+ }
+
+ if ($rows) {
+ $content = theme('table', array('rows' => $rows, 'attributes' => array('class' => 'panels-manage')));
+ }
+ else {
+ $content = '' . t('There are no custom rulesets.') . '
';
+ }
+
+ $vars['blocks']['ctools_access_ruleset'] = array(
+ 'title' => t('Manage custom rulesets'),
+ 'link' => l(t('Go to list'), 'admin/structure/ctools-rulesets'),
+ 'content' => $content,
+ 'class' => 'dashboard-ruleset',
+ 'section' => 'right',
+ );
+}
diff --git a/sites/all/modules/ctools/ctools_access_ruleset/plugins/access/ruleset.inc b/sites/all/modules/ctools/ctools_access_ruleset/plugins/access/ruleset.inc
new file mode 100644
index 0000000..95f32c3
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_access_ruleset/plugins/access/ruleset.inc
@@ -0,0 +1,108 @@
+ '',
+ 'description' => '',
+ 'callback' => 'ctools_ruleset_ctools_access_check',
+ 'settings form' => 'ctools_ruleset_ctools_access_settings',
+ 'summary' => 'ctools_ruleset_ctools_access_summary',
+
+ // This access plugin actually just contains child plugins that are
+ // exportable, UI configured rulesets.
+ 'get child' => 'ctools_ruleset_ctools_access_get_child',
+ 'get children' => 'ctools_ruleset_ctools_access_get_children',
+);
+
+/**
+ * Merge the main access plugin with a loaded ruleset to form a child plugin.
+ */
+function ctools_ruleset_ctools_access_merge_plugin($plugin, $parent, $item) {
+ $plugin['name'] = $parent . ':' . $item->name;
+ $plugin['title'] = check_plain($item->admin_title);
+ $plugin['description'] = check_plain($item->admin_description);
+
+ // TODO: Generalize this in CTools.
+ if (!empty($item->requiredcontexts)) {
+ $plugin['required context'] = array();
+ foreach ($item->requiredcontexts as $context) {
+ $info = ctools_get_context($context['name']);
+ // TODO: allow an optional setting.
+ $plugin['required context'][] = new ctools_context_required($context['identifier'], $info['context name']);
+ }
+ }
+
+ // Store the loaded ruleset in the plugin.
+ $plugin['ruleset'] = $item;
+ return $plugin;
+}
+
+/**
+ * Get a single child access plugin.
+ */
+function ctools_ruleset_ctools_access_get_child($plugin, $parent, $child) {
+ ctools_include('export');
+ $item = ctools_export_crud_load('ctools_access_ruleset', $child);
+ if ($item) {
+ return ctools_ruleset_ctools_access_merge_plugin($plugin, $parent, $item);
+ }
+}
+
+/**
+ * Get all child access plugins.
+ */
+function ctools_ruleset_ctools_access_get_children($plugin, $parent) {
+ $plugins = array();
+ ctools_include('export');
+ $items = ctools_export_crud_load_all('ctools_access_ruleset');
+ foreach ($items as $name => $item) {
+ $child = ctools_ruleset_ctools_access_merge_plugin($plugin, $parent, $item);
+ $plugins[$child['name']] = $child;
+ }
+
+ return $plugins;
+}
+
+/**
+ * Settings form for the 'by ruleset' access plugin.
+ */
+function ctools_ruleset_ctools_access_settings(&$form, &$form_state, $conf) {
+ if (!empty($form_state['plugin']['ruleset']->admin_description)) {
+ $form['markup'] = array(
+ '#markup' => '' . check_plain($form_state['plugin']['ruleset']->admin_description) . '
',
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Check for access.
+ */
+function ctools_ruleset_ctools_access_check($conf, $context, $plugin) {
+ // Load up any contexts we might be using.
+ $contexts = ctools_context_match_required_contexts($plugin['ruleset']->requiredcontexts, $context);
+ $contexts = ctools_context_load_contexts($plugin['ruleset'], FALSE, $contexts);
+
+ return ctools_access($plugin['ruleset']->access, $contexts);
+}
+
+/**
+ * Provide a summary description based upon the checked roles.
+ */
+function ctools_ruleset_ctools_access_summary($conf, $context, $plugin) {
+ if (!empty($plugin['ruleset']->admin_description)) {
+ return check_plain($plugin['ruleset']->admin_description);
+ }
+ else {
+ return check_plain($plugin['ruleset']->admin_title);
+ }
+}
diff --git a/sites/all/modules/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset.inc b/sites/all/modules/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset.inc
new file mode 100644
index 0000000..2589ac3
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset.inc
@@ -0,0 +1,32 @@
+ 'ctools_access_ruleset',
+ 'access' => 'administer ctools access ruleset',
+
+ 'menu' => array(
+ 'menu item' => 'ctools-rulesets',
+ 'menu title' => 'Custom access rulesets',
+ 'menu description' => 'Add, edit or delete custom access rulesets for use with Panels and other systems that utilize CTools content plugins.',
+ ),
+
+ 'title singular' => t('ruleset'),
+ 'title singular proper' => t('Ruleset'),
+ 'title plural' => t('rulesets'),
+ 'title plural proper' => t('Rulesets'),
+
+ 'handler' => 'ctools_access_ruleset_ui',
+
+ 'use wizard' => TRUE,
+ 'form info' => array(
+ 'order' => array(
+ 'basic' => t('Basic information'),
+ 'context' => t('Contexts'),
+ 'rules' => t('Rules'),
+ ),
+ ),
+);
diff --git a/sites/all/modules/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset_ui.class.php b/sites/all/modules/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset_ui.class.php
new file mode 100644
index 0000000..c9f8c20
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset_ui.class.php
@@ -0,0 +1,54 @@
+ '',
+ '#suffix' => '
',
+ );
+
+ $form['left'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ // Set this up and we can use CTools' Export UI's built in wizard caching,
+ // which already has callbacks for the context cache under this name.
+ $module = 'export_ui::' . $this->plugin['name'];
+ $name = $this->edit_cache_get_key($form_state['item'], $form_state['form type']);
+
+ ctools_context_add_context_form($module, $form, $form_state, $form['right']['contexts_table'], $form_state['item'], $name);
+ ctools_context_add_required_context_form($module, $form, $form_state, $form['left']['required_contexts_table'], $form_state['item'], $name);
+ ctools_context_add_relationship_form($module, $form, $form_state, $form['right']['relationships_table'], $form_state['item'], $name);
+ }
+
+ public function edit_form_rules(&$form, &$form_state) {
+ // The 'access' UI passes everything via $form_state, unlike the 'context' UI.
+ // The main difference is that one is about 3 years newer than the other.
+ ctools_include('context');
+ ctools_include('context-access-admin');
+
+ $form_state['access'] = $form_state['item']->access;
+ $form_state['contexts'] = ctools_context_load_contexts($form_state['item']);
+
+ $form_state['module'] = 'ctools_export_ui';
+ $form_state['callback argument'] = $form_state['object']->plugin['name'] . ':' . $form_state['object']->edit_cache_get_key($form_state['item'], $form_state['form type']);
+ $form_state['no buttons'] = TRUE;
+
+ $form = ctools_access_admin_form($form, $form_state);
+ }
+
+ public function edit_form_rules_submit(&$form, &$form_state) {
+ $form_state['item']->access['logic'] = $form_state['values']['logic'];
+ }
+
+ public function edit_form_submit(&$form, &$form_state) {
+ parent::edit_form_submit($form, $form_state);
+ }
+
+}
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/css/ctools-ajax-sample.css b/sites/all/modules/ctools/ctools_ajax_sample/css/ctools-ajax-sample.css
new file mode 100644
index 0000000..c312e99
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_ajax_sample/css/ctools-ajax-sample.css
@@ -0,0 +1,149 @@
+div.ctools-sample-modal-content {
+ background: none;
+ border: 0;
+ color: #000;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+}
+div.ctools-sample-modal-content .modal-scroll {
+ overflow: hidden;
+ overflow-y: auto;
+}
+div.ctools-sample-modal-content #popups-overlay {
+ background-color: transparent;
+}
+div.ctools-sample-modal-content #popups-loading {
+ width: 248px;
+ position: absolute;
+ display: none;
+ opacity: 1;
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 8px;
+ z-index: 99;
+}
+div.ctools-sample-modal-content #popups-loading span.popups-loading-message {
+ background: #fff url(../images/loading-large.gif) no-repeat 8px center;
+ display: block;
+ color: #444;
+ font-family: Arial, serif;
+ font-size: 22px;
+ font-weight: bold;
+ height: 36px;
+ line-height: 36px;
+ padding: 0 40px;
+}
+div.ctools-sample-modal-content #popups-loading table,
+div.ctools-sample-modal-content .popups-box table {
+ margin: 0;
+}
+div.ctools-sample-modal-content #popups-loading tbody,
+div.ctools-sample-modal-content .popups-box tbody {
+ border: none;
+}
+div.ctools-sample-modal-content .popups-box tr {
+ background-color: transparent;
+}
+div.ctools-sample-modal-content td.popups-border {
+ background: url(../images/popups-border.png);
+ background-color: transparent;
+ border: none;
+}
+div.ctools-sample-modal-content td.popups-tl,
+div.ctools-sample-modal-content td.popups-tr,
+div.ctools-sample-modal-content td.popups-bl,
+div.ctools-sample-modal-content td.popups-br {
+ background-repeat: no-repeat;
+ height: 10px;
+ padding: 0;
+}
+div.ctools-sample-modal-content td.popups-tl {
+ background-position: 0 0;
+}
+div.ctools-sample-modal-content td.popups-t,
+div.ctools-sample-modal-content td.popups-b {
+ background-position: 0 -40px;
+ background-repeat: repeat-x;
+}
+div.ctools-sample-modal-content td.popups-tr {
+ background-position: 0 -10px;
+ width: 10px;
+}
+div.ctools-sample-modal-content td.popups-cl,
+div.ctools-sample-modal-content td.popups-cr {
+ background-position: -10px 0;
+ background-repeat: repeat-y;
+ width: 10px;
+}
+div.ctools-sample-modal-content td.popups-cl,
+div.ctools-sample-modal-content td.popups-cr,
+div.ctools-sample-modal-content td.popups-c {
+ padding: 0;
+ border: none;
+}
+div.ctools-sample-modal-content td.popups-c {
+ background: #fff;
+}
+div.ctools-sample-modal-content td.popups-bl {
+ background-position: 0 -20px;
+}
+div.ctools-sample-modal-content td.popups-br {
+ background-position: 0 -30px;
+ width: 10px;
+}
+
+div.ctools-sample-modal-content .popups-box,
+div.ctools-sample-modal-content #popups-loading {
+ border: 0 solid #454545;
+ opacity: 1;
+ overflow: hidden;
+ padding: 0;
+ background-color: transparent;
+}
+div.ctools-sample-modal-content .popups-container {
+ overflow: hidden;
+ height: 100%;
+ background-color: #fff;
+}
+div.ctools-sample-modal-content div.popups-title {
+ -moz-border-radius-topleft: 0;
+ -webkit-border-radius-topleft: 0;
+ margin-bottom: 0;
+ background-color: #ff7200;
+ border: 1px solid #ce5c00;
+ padding: 4px 10px 5px;
+ color: white;
+ font-size: 1em;
+ font-weight: bold;
+}
+div.ctools-sample-modal-content .popups-body {
+ background-color: #fff;
+ padding: 8px;
+}
+div.ctools-sample-modal-content .popups-box .popups-buttons,
+div.ctools-sample-modal-content .popups-box .popups-footer {
+ background-color: #fff;
+}
+div.ctools-sample-modal-content .popups-title a.close {
+ color: #fff;
+ text-decoration: none;
+}
+div.ctools-sample-modal-content .popups-close {
+ font-size: 120%;
+ float: right;
+ text-align: right;
+}
+div.ctools-sample-modal-content .modal-loading-wrapper {
+ width: 220px;
+ height: 19px;
+ margin: 0 auto;
+ margin-top: 2%;
+}
+
+div.ctools-sample-modal-content tbody {
+ border: none;
+}
+
+div.ctools-sample-modal-content .modal-content .modal-throbber-wrapper img {
+ margin-top: 100px;
+}
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.info b/sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.info
new file mode 100644
index 0000000..4bb8943
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.info
@@ -0,0 +1,13 @@
+name = Chaos Tools (CTools) AJAX Example
+description = Shows how to use the power of Chaos AJAX.
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+dependencies[] = ctools
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2018-02-24
+version = "7.x-1.14"
+core = "7.x"
+project = "ctools"
+datestamp = "1519455788"
+
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.install b/sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.install
new file mode 100644
index 0000000..e0fdfc6
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_ajax_sample/ctools_ajax_sample.install
@@ -0,0 +1,19 @@
+ 'Chaos Tools AJAX Demo',
+ 'page callback' => 'ctools_ajax_sample_page',
+ 'access callback' => TRUE,
+ 'type' => MENU_NORMAL_ITEM,
+ );
+ $items['ctools_ajax_sample/simple_form'] = array(
+ 'title' => 'Simple Form',
+ 'page callback' => 'ctools_ajax_simple_form',
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/hello'] = array(
+ 'title' => 'Hello World',
+ 'page callback' => 'ctools_ajax_sample_hello',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/tablenix/%'] = array(
+ 'title' => 'Hello World',
+ 'page callback' => 'ctools_ajax_sample_tablenix',
+ 'page arguments' => array(1, 3),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/login'] = array(
+ 'title' => 'Login',
+ 'page callback' => 'ctools_ajax_sample_login',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/animal'] = array(
+ 'title' => 'Animal',
+ 'page callback' => 'ctools_ajax_sample_animal',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/login/%'] = array(
+ 'title' => 'Post-Login Action',
+ 'page callback' => 'ctools_ajax_sample_login_success',
+ 'page arguments' => array(1, 3),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/jumped'] = array(
+ 'title' => 'Successful Jumping',
+ 'page callback' => 'ctools_ajax_sample_jump_menu_page',
+ 'access callback' => TRUE,
+ 'type' => MENU_NORMAL_ITEM,
+ );
+
+ return $items;
+}
+
+function ctools_ajax_simple_form() {
+ ctools_include('content');
+ ctools_include('context');
+ $node = node_load(1);
+ $context = ctools_context_create('node', $node);
+ $context = array('context_node_1' => $context);
+ return ctools_content_render('node_comment_form', 'node_comment_form', ctools_ajax_simple_form_pane(), array(), array(), $context);
+}
+
+function ctools_ajax_simple_form_pane() {
+ $configuration = array(
+ 'anon_links' => 0,
+ 'context' => 'context_node_1',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ );
+ return $configuration;
+}
+
+/**
+ * Implementation of hook_theme()
+ *
+ * Render some basic output for this module.
+ */
+function ctools_ajax_sample_theme() {
+ return array(
+ // Sample theme functions.
+ 'ctools_ajax_sample_container' => array(
+ 'arguments' => array('content' => NULL),
+ ),
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Page callbacks.
+/**
+ * Page callback to display links and render a container for AJAX stuff.
+ */
+
+function ctools_ajax_sample_page() {
+ global $user;
+
+ // Include the CTools tools that we need.
+ ctools_include('ajax');
+ ctools_include('modal');
+
+ // Add CTools' javascript to the page.
+ ctools_modal_add_js();
+
+ // Create our own javascript that will be used to theme a modal.
+ $sample_style = array(
+ 'ctools-sample-style' => array(
+ 'modalSize' => array(
+ 'type' => 'fixed',
+ 'width' => 500,
+ 'height' => 300,
+ 'addWidth' => 20,
+ 'addHeight' => 15,
+ ),
+ 'modalOptions' => array(
+ 'opacity' => .5,
+ 'background-color' => '#000',
+ ),
+ 'animation' => 'fadeIn',
+ 'modalTheme' => 'CToolsSampleModal',
+ 'throbber' => theme('image', array('path' => ctools_image_path('ajax-loader.gif', 'ctools_ajax_sample'), 'alt' => t('Loading...'), 'title' => t('Loading'))),
+ ),
+ );
+
+ drupal_add_js($sample_style, 'setting');
+
+ // Since we have our js, css and images in well-known named directories,
+ // CTools makes it easy for us to just use them without worrying about
+ // using drupal_get_path() and all that ugliness.
+ ctools_add_js('ctools-ajax-sample', 'ctools_ajax_sample');
+ ctools_add_css('ctools-ajax-sample', 'ctools_ajax_sample');
+
+ // Create a list of clickable links.
+ $links = array();
+
+ // Only show login links to the anonymous user.
+ if ($user->uid == 0) {
+ $links[] = ctools_modal_text_button(t('Modal Login (default style)'), 'ctools_ajax_sample/nojs/login', t('Login via modal'));
+
+ // The extra class points to the info in ctools-sample-style which we added
+ // to the settings, prefixed with 'ctools-modal'.
+ $links[] = ctools_modal_text_button(t('Modal Login (custom style)'), 'ctools_ajax_sample/nojs/login', t('Login via modal'), 'ctools-modal-ctools-sample-style');
+ }
+
+ // Four ways to do our animal picking wizard.
+ $button_form = ctools_ajax_sample_ajax_button_form();
+ $links[] = l(t('Wizard (no modal)'), 'ctools_ajax_sample/nojs/animal');
+ $links[] = ctools_modal_text_button(t('Wizard (default modal)'), 'ctools_ajax_sample/nojs/animal', t('Pick an animal'));
+ $links[] = ctools_modal_text_button(t('Wizard (custom modal)'), 'ctools_ajax_sample/nojs/animal', t('Pick an animal'), 'ctools-modal-ctools-sample-style');
+ $links[] = drupal_render($button_form);
+
+ $links[] = ctools_ajax_text_button(t('Hello world!'), "ctools_ajax_sample/nojs/hello", t('Replace text with "hello world"'));
+
+ $output = theme('item_list', array('items' => $links, 'title' => t('Actions')));
+
+ // This container will have data AJAXed into it.
+ $output .= theme('ctools_ajax_sample_container', array('content' => '' . t('Sample Content') . ' '));
+
+ // Create a table that we can have data removed from via AJAX.
+ $header = array(t('Row'), t('Content'), t('Actions'));
+ $rows = array();
+ for ($i = 1; $i < 11; $i++) {
+ $rows[] = array(
+ 'class' => array('ajax-sample-row-' . $i),
+ 'data' => array(
+ $i,
+ md5($i),
+ ctools_ajax_text_button("remove", "ctools_ajax_sample/nojs/tablenix/$i", t('Delete this row')),
+ ),
+ );
+ }
+
+ $output .= theme('table', array('header' => $header, 'rows' => $rows, array('class' => array('ajax-sample-table'))));
+
+ // Show examples of ctools javascript widgets.
+ $output .= '' . t('CTools Javascript Widgets') . ' ';
+
+ // Create a drop down menu.
+ $links = array();
+ $links[] = array('title' => t('Link 1'), 'href' => $_GET['q']);
+ $links[] = array('title' => t('Link 2'), 'href' => $_GET['q']);
+ $links[] = array('title' => t('Link 3'), 'href' => $_GET['q']);
+
+ $output .= '' . t('Drop Down Menu') . ' ';
+ $output .= theme('ctools_dropdown', array('title' => t('Click to Drop Down'), 'links' => $links));
+
+ // Create a collapsible div.
+ $handle = t('Click to Collapse');
+ $content = 'Nulla ligula ante, aliquam at adipiscing egestas, varius vel arcu. Etiam laoreet elementum mi vel consequat. Etiam scelerisque lorem vel neque consequat quis bibendum libero congue. Nulla facilisi. Mauris a elit a leo feugiat porta. Phasellus placerat cursus est vitae elementum.';
+ $output .= '' . t('Collapsible Div') . ' ';
+ $output .= theme('ctools_collapsible', array('handle' => $handle, 'content' => $content, 'collapsed' => FALSE));
+
+ // Create a jump menu.
+ ctools_include('jump-menu');
+ $form = drupal_get_form('ctools_ajax_sample_jump_menu_form');
+ $output .= '' . t('Jump Menu') . ' ';
+ $output .= drupal_render($form);
+
+ return array('markup' => array('#markup' => $output));
+}
+
+/**
+ * Returns a "take it all over" hello world style request.
+ */
+function ctools_ajax_sample_hello($js = NULL) {
+ $output = '' . t('Hello World') . ' ';
+ if ($js) {
+ ctools_include('ajax');
+ $commands = array();
+ $commands[] = ajax_command_html('#ctools-sample', $output);
+ // This function exits.
+ print ajax_render($commands);
+ exit;
+ }
+ else {
+ return $output;
+ }
+}
+
+/**
+ * Nix a row from a table and restripe.
+ */
+function ctools_ajax_sample_tablenix($js, $row) {
+ if (!$js) {
+ // We don't support degrading this from js because we're not
+ // using the server to remember the state of the table.
+ return MENU_ACCESS_DENIED;
+ }
+ ctools_include('ajax');
+
+ $commands = array();
+ $commands[] = ajax_command_remove("tr.ajax-sample-row-$row");
+ $commands[] = ajax_command_restripe("table.ajax-sample-table");
+ print ajax_render($commands);
+ exit;
+}
+
+/**
+ * A modal login callback.
+ */
+function ctools_ajax_sample_login($js = NULL) {
+ // Fall back if $js is not set.
+ if (!$js) {
+ return drupal_get_form('user_login');
+ }
+
+ ctools_include('modal');
+ ctools_include('ajax');
+ $form_state = array(
+ 'title' => t('Login'),
+ 'ajax' => TRUE,
+ );
+ $output = ctools_modal_form_wrapper('user_login', $form_state);
+ if (!empty($form_state['executed'])) {
+ // We'll just overwrite the form output if it was successful.
+ $output = array();
+ $inplace = ctools_ajax_text_button(t('remain here'), 'ctools_ajax_sample/nojs/login/inplace', t('Go to your account'));
+ $account = ctools_ajax_text_button(t('your account'), 'ctools_ajax_sample/nojs/login/user', t('Go to your account'));
+ $output[] = ctools_modal_command_display(t('Login Success'), 'Login successful. You can now choose whether to ' . $inplace . ', or go to ' . $account . '.
');
+ }
+ print ajax_render($output);
+ exit;
+}
+
+/**
+ * Post-login processor: should we go to the user account or stay in place?
+ */
+function ctools_ajax_sample_login_success($js, $action) {
+ if (!$js) {
+ // We should never be here out of ajax context.
+ return MENU_NOT_FOUND;
+ }
+
+ ctools_include('ajax');
+ ctools_add_js('ajax-responder');
+ $commands = array();
+ if ($action == 'inplace') {
+ // Stay here.
+ $commands[] = ctools_ajax_command_reload();
+ }
+ else {
+ // Bounce bounce.
+ $commands[] = ctools_ajax_command_redirect('user');
+ }
+ print ajax_render($commands);
+ exit;
+}
+
+/**
+ * A modal login callback.
+ */
+function ctools_ajax_sample_animal($js = NULL, $step = NULL) {
+ if ($js) {
+ ctools_include('modal');
+ ctools_include('ajax');
+ }
+
+ $form_info = array(
+ 'id' => 'animals',
+ 'path' => "ctools_ajax_sample/" . ($js ? 'ajax' : 'nojs') . "/animal/%step",
+ 'show trail' => TRUE,
+ 'show back' => TRUE,
+ 'show cancel' => TRUE,
+ 'show return' => FALSE,
+ 'next callback' => 'ctools_ajax_sample_wizard_next',
+ 'finish callback' => 'ctools_ajax_sample_wizard_finish',
+ 'cancel callback' => 'ctools_ajax_sample_wizard_cancel',
+ // This controls order, as well as form labels.
+ 'order' => array(
+ 'start' => t('Choose animal'),
+ ),
+ // Here we map a step to a form id.
+ 'forms' => array(
+ // e.g. this for the step at wombat/create.
+ 'start' => array(
+ 'form id' => 'ctools_ajax_sample_start',
+ ),
+ ),
+ );
+
+ // We're not using any real storage here, so we're going to set our
+ // object_id to 1. When using wizard forms, id management turns
+ // out to be one of the hardest parts. Editing an object with an id
+ // is easy, but new objects don't usually have ids until somewhere
+ // in creation.
+ //
+ // We skip all this here by just using an id of 1.
+ $object_id = 1;
+
+ if (empty($step)) {
+ // We reset the form when $step is NULL because that means they have
+ // for whatever reason started over.
+ ctools_ajax_sample_cache_clear($object_id);
+ $step = 'start';
+ }
+
+ // This automatically gets defaults if there wasn't anything saved.
+ $object = ctools_ajax_sample_cache_get($object_id);
+
+ $animals = ctools_ajax_sample_animals();
+
+ // Make sure we can't somehow accidentally go to an invalid animal.
+ if (empty($animals[$object->type])) {
+ $object->type = 'unknown';
+ }
+
+ // Now that we have our object, dynamically add the animal's form.
+ if ($object->type == 'unknown') {
+ // If they haven't selected a type, add a form that doesn't exist yet.
+ $form_info['order']['unknown'] = t('Configure animal');
+ $form_info['forms']['unknown'] = array('form id' => 'nothing');
+ }
+ else {
+ // Add the selected animal to the order so that it shows up properly in the trail.
+ $form_info['order'][$object->type] = $animals[$object->type]['config title'];
+ }
+
+ // Make sure all animals forms are represented so that the next stuff can
+ // work correctly:
+ foreach ($animals as $id => $animal) {
+ $form_info['forms'][$id] = array('form id' => $animals[$id]['form']);
+ }
+
+ $form_state = array(
+ 'ajax' => $js,
+ // Put our object and ID into the form state cache so we can easily find
+ // it.
+ 'object_id' => $object_id,
+ 'object' => &$object,
+ );
+
+ // Send this all off to our form. This is like drupal_get_form only wizardy.
+ ctools_include('wizard');
+ $form = ctools_wizard_multistep_form($form_info, $step, $form_state);
+ $output = drupal_render($form);
+
+ if ($output === FALSE || !empty($form_state['complete'])) {
+ // This creates a string based upon the animal and its setting using
+ // function indirection.
+ $animal = $animals[$object->type]['output']($object);
+ }
+
+ // If $output is FALSE, there was no actual form.
+ if ($js) {
+ // If javascript is active, we have to use a render array.
+ $commands = array();
+ if ($output === FALSE || !empty($form_state['complete'])) {
+ // Dismiss the modal.
+ $commands[] = ajax_command_html('#ctools-sample', $animal);
+ $commands[] = ctools_modal_command_dismiss();
+ }
+ elseif (!empty($form_state['cancel'])) {
+ // If cancelling, return to the activity.
+ $commands[] = ctools_modal_command_dismiss();
+ }
+ else {
+ $commands = ctools_modal_form_render($form_state, $output);
+ }
+ print ajax_render($commands);
+ exit;
+ }
+ else {
+ if ($output === FALSE || !empty($form_state['complete'])) {
+ return $animal;
+ }
+ elseif (!empty($form_state['cancel'])) {
+ drupal_goto('ctools_ajax_sample');
+ }
+ else {
+ return $output;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Themes.
+/**
+ * Theme function for main rendered output.
+ */
+
+function theme_ctools_ajax_sample_container($vars) {
+ $output = '';
+ $output .= $vars['content'];
+ $output .= '
';
+
+ return $output;
+}
+
+// ---------------------------------------------------------------------------
+// Stuff needed for our little wizard.
+/**
+ * Get a list of our animals and associated forms.
+ *
+ * What we're doing is making it easy to add more animals in just one place,
+ * which is often how it will work in the real world. If using CTools, what
+ * you would probably really have, here, is a set of plugins for each animal.
+ */
+
+function ctools_ajax_sample_animals() {
+ return array(
+ 'sheep' => array(
+ 'title' => t('Sheep'),
+ 'config title' => t('Configure sheep'),
+ 'form' => 'ctools_ajax_sample_configure_sheep',
+ 'output' => 'ctools_ajax_sample_show_sheep',
+ ),
+ 'lizard' => array(
+ 'title' => t('Lizard'),
+ 'config title' => t('Configure lizard'),
+ 'form' => 'ctools_ajax_sample_configure_lizard',
+ 'output' => 'ctools_ajax_sample_show_lizard',
+ ),
+ 'raptor' => array(
+ 'title' => t('Raptor'),
+ 'config title' => t('Configure raptor'),
+ 'form' => 'ctools_ajax_sample_configure_raptor',
+ 'output' => 'ctools_ajax_sample_show_raptor',
+ ),
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Wizard caching helpers.
+/**
+ * Store our little cache so that we can retain data from form to form.
+ */
+
+function ctools_ajax_sample_cache_set($id, $object) {
+ ctools_include('object-cache');
+ ctools_object_cache_set('ctools_ajax_sample', $id, $object);
+}
+
+/**
+ * Get the current object from the cache, or default.
+ */
+function ctools_ajax_sample_cache_get($id) {
+ ctools_include('object-cache');
+ $object = ctools_object_cache_get('ctools_ajax_sample', $id);
+ if (!$object) {
+ // Create a default object.
+ $object = new stdClass();
+ $object->type = 'unknown';
+ $object->name = '';
+ }
+
+ return $object;
+}
+
+/**
+ * Clear the wizard cache.
+ */
+function ctools_ajax_sample_cache_clear($id) {
+ ctools_include('object-cache');
+ ctools_object_cache_clear('ctools_ajax_sample', $id);
+}
+
+// ---------------------------------------------------------------------------
+// Wizard in-between helpers; what to do between or after forms.
+/**
+ * Handle the 'next' click on the add/edit pane form wizard.
+ *
+ * All we need to do is store the updated pane in the cache.
+ */
+
+function ctools_ajax_sample_wizard_next(&$form_state) {
+ ctools_ajax_sample_cache_set($form_state['object_id'], $form_state['object']);
+}
+
+/**
+ * Handle the 'finish' click on the add/edit pane form wizard.
+ *
+ * All we need to do is set a flag so the return can handle adding
+ * the pane.
+ */
+function ctools_ajax_sample_wizard_finish(&$form_state) {
+ $form_state['complete'] = TRUE;
+}
+
+/**
+ * Handle the 'cancel' click on the add/edit pane form wizard.
+ */
+function ctools_ajax_sample_wizard_cancel(&$form_state) {
+ $form_state['cancel'] = TRUE;
+}
+
+// ---------------------------------------------------------------------------
+// Wizard forms for our simple info collection wizard.
+/**
+ * Wizard start form. Choose an animal.
+ */
+
+function ctools_ajax_sample_start($form, &$form_state) {
+ $form_state['title'] = t('Choose animal');
+
+ $animals = ctools_ajax_sample_animals();
+ foreach ($animals as $id => $animal) {
+ $options[$id] = $animal['title'];
+ }
+
+ $form['type'] = array(
+ '#title' => t('Choose your animal'),
+ '#type' => 'radios',
+ '#options' => $options,
+ '#default_value' => $form_state['object']->type,
+ '#required' => TRUE,
+ );
+
+ return $form;
+}
+
+/**
+ * They have selected a sheep. Set it.
+ */
+function ctools_ajax_sample_start_submit(&$form, &$form_state) {
+ $form_state['object']->type = $form_state['values']['type'];
+ // Override where to go next based on the animal selected.
+ $form_state['clicked_button']['#next'] = $form_state['values']['type'];
+}
+
+/**
+ * Wizard form to configure your sheep.
+ */
+function ctools_ajax_sample_configure_sheep($form, &$form_state) {
+ $form_state['title'] = t('Configure sheep');
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name your sheep'),
+ '#default_value' => $form_state['object']->name,
+ '#required' => TRUE,
+ );
+
+ $form['sheep'] = array(
+ '#title' => t('What kind of sheep'),
+ '#type' => 'radios',
+ '#options' => array(
+ t('Wensleydale') => t('Wensleydale'),
+ t('Merino') => t('Merino'),
+ t('Corriedale') => t('Coriedale'),
+ ),
+ '#default_value' => !empty($form_state['object']->sheep) ? $form_state['object']->sheep : '',
+ '#required' => TRUE,
+ );
+ return $form;
+}
+
+/**
+ * Submit the sheep and store the values from the form.
+ */
+function ctools_ajax_sample_configure_sheep_submit(&$form, &$form_state) {
+ $form_state['object']->name = $form_state['values']['name'];
+ $form_state['object']->sheep = $form_state['values']['sheep'];
+}
+
+/**
+ * Provide some output for our sheep.
+ */
+function ctools_ajax_sample_show_sheep($object) {
+ return t('You have a @type sheep named "@name".', array(
+ '@type' => $object->sheep,
+ '@name' => $object->name,
+ ));
+}
+
+/**
+ * Wizard form to configure your lizard.
+ */
+function ctools_ajax_sample_configure_lizard($form, &$form_state) {
+ $form_state['title'] = t('Configure lizard');
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name your lizard'),
+ '#default_value' => $form_state['object']->name,
+ '#required' => TRUE,
+ );
+
+ $form['lizard'] = array(
+ '#title' => t('Venomous'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($form_state['object']->lizard),
+ );
+ return $form;
+}
+
+/**
+ * Submit the lizard and store the values from the form.
+ */
+function ctools_ajax_sample_configure_lizard_submit(&$form, &$form_state) {
+ $form_state['object']->name = $form_state['values']['name'];
+ $form_state['object']->lizard = $form_state['values']['lizard'];
+}
+
+/**
+ * Provide some output for our raptor.
+ */
+function ctools_ajax_sample_show_lizard($object) {
+ return t('You have a @type lizard named "@name".', array(
+ '@type' => empty($object->lizard) ? t('non-venomous') : t('venomous'),
+ '@name' => $object->name,
+ ));
+}
+
+/**
+ * Wizard form to configure your raptor.
+ */
+function ctools_ajax_sample_configure_raptor($form, &$form_state) {
+ $form_state['title'] = t('Configure raptor');
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name your raptor'),
+ '#default_value' => $form_state['object']->name,
+ '#required' => TRUE,
+ );
+
+ $form['raptor'] = array(
+ '#title' => t('What kind of raptor'),
+ '#type' => 'radios',
+ '#options' => array(
+ t('Eagle') => t('Eagle'),
+ t('Hawk') => t('Hawk'),
+ t('Owl') => t('Owl'),
+ t('Buzzard') => t('Buzzard'),
+ ),
+ '#default_value' => !empty($form_state['object']->raptor) ? $form_state['object']->raptor : '',
+ '#required' => TRUE,
+ );
+
+ $form['domesticated'] = array(
+ '#title' => t('Domesticated'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($form_state['object']->domesticated),
+ );
+ return $form;
+}
+
+/**
+ * Submit the raptor and store the values from the form.
+ */
+function ctools_ajax_sample_configure_raptor_submit(&$form, &$form_state) {
+ $form_state['object']->name = $form_state['values']['name'];
+ $form_state['object']->raptor = $form_state['values']['raptor'];
+ $form_state['object']->domesticated = $form_state['values']['domesticated'];
+}
+
+/**
+ * Provide some output for our raptor.
+ */
+function ctools_ajax_sample_show_raptor($object) {
+ return t('You have a @type @raptor named "@name".', array(
+ '@type' => empty($object->domesticated) ? t('wild') : t('domesticated'),
+ '@raptor' => $object->raptor,
+ '@name' => $object->name,
+ ));
+}
+
+/**
+ * Helper function to provide a sample jump menu form.
+ */
+function ctools_ajax_sample_jump_menu_form() {
+ $url = url('ctools_ajax_sample/jumped');
+ $form_state = array();
+ $form = ctools_jump_menu(array(), $form_state, array($url => t('Jump!')), array());
+ return $form;
+}
+
+/**
+ * Provide a message to the user that the jump menu worked.
+ */
+function ctools_ajax_sample_jump_menu_page() {
+ $return_link = l(t('Return to the examples page.'), 'ctools_ajax_sample');
+ $output = t('You successfully jumped! !return_link', array('!return_link' => $return_link));
+ return $output;
+}
+
+/**
+ * Provide a form for an example ajax modal button.
+ */
+function ctools_ajax_sample_ajax_button_form() {
+ $form = array();
+
+ $form['url'] = array(
+ '#type' => 'hidden',
+ // The name of the class is the #id of $form['ajax_button'] with "-url"
+ // suffix.
+ '#attributes' => array('class' => array('ctools-ajax-sample-button-url')),
+ '#value' => url('ctools_ajax_sample/nojs/animal'),
+ );
+
+ $form['ajax_button'] = array(
+ '#type' => 'button',
+ '#value' => 'Wizard (button modal)',
+ '#attributes' => array('class' => array('ctools-use-modal')),
+ '#id' => 'ctools-ajax-sample-button',
+ );
+
+ return $form;
+}
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/images/ajax-loader.gif b/sites/all/modules/ctools/ctools_ajax_sample/images/ajax-loader.gif
new file mode 100644
index 0000000..d84f653
Binary files /dev/null and b/sites/all/modules/ctools/ctools_ajax_sample/images/ajax-loader.gif differ
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/images/loading-large.gif b/sites/all/modules/ctools/ctools_ajax_sample/images/loading-large.gif
new file mode 100644
index 0000000..1c72ebb
Binary files /dev/null and b/sites/all/modules/ctools/ctools_ajax_sample/images/loading-large.gif differ
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/images/loading.gif b/sites/all/modules/ctools/ctools_ajax_sample/images/loading.gif
new file mode 100644
index 0000000..dc21df1
Binary files /dev/null and b/sites/all/modules/ctools/ctools_ajax_sample/images/loading.gif differ
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/images/popups-border.png b/sites/all/modules/ctools/ctools_ajax_sample/images/popups-border.png
new file mode 100644
index 0000000..ba939f8
Binary files /dev/null and b/sites/all/modules/ctools/ctools_ajax_sample/images/popups-border.png differ
diff --git a/sites/all/modules/ctools/ctools_ajax_sample/js/ctools-ajax-sample.js b/sites/all/modules/ctools/ctools_ajax_sample/js/ctools-ajax-sample.js
new file mode 100644
index 0000000..0cbfc87
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_ajax_sample/js/ctools-ajax-sample.js
@@ -0,0 +1,42 @@
+/**
+* Provide the HTML to create the modal dialog.
+*/
+Drupal.theme.prototype.CToolsSampleModal = function () {
+ var html = '';
+
+ html += '';
+
+ return html;
+
+}
diff --git a/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.info b/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.info
new file mode 100644
index 0000000..637bf25
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.info
@@ -0,0 +1,13 @@
+name = Custom content panes
+description = Create custom, exportable, reusable content panes for applications like Panels.
+core = 7.x
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+dependencies[] = ctools
+
+; Information added by Drupal.org packaging script on 2018-02-24
+version = "7.x-1.14"
+core = "7.x"
+project = "ctools"
+datestamp = "1519455788"
+
diff --git a/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.install b/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.install
new file mode 100644
index 0000000..dcf87e7
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.install
@@ -0,0 +1,71 @@
+ 'Contains exportable customized content for this site.',
+ 'export' => array(
+ 'identifier' => 'content',
+ 'bulk export' => TRUE,
+ 'primary key' => 'cid',
+ 'api' => array(
+ 'owner' => 'ctools_custom_content',
+ 'api' => 'ctools_content',
+ 'minimum_version' => 1,
+ 'current_version' => 1,
+ ),
+ 'create callback' => 'ctools_content_type_new',
+ ),
+ 'fields' => array(
+ 'cid' => array(
+ 'type' => 'serial',
+ 'description' => 'A database primary key to ensure uniqueness',
+ 'not null' => TRUE,
+ 'no export' => TRUE,
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Unique ID for this content. Used to identify it programmatically.',
+ ),
+ 'admin_title' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Administrative title for this content.',
+ ),
+ 'admin_description' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Administrative description for this content.',
+ 'object default' => '',
+ ),
+ 'category' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Administrative category for this content.',
+ ),
+ 'settings' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Serialized settings for the actual content to be used',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ ),
+ 'primary key' => array('cid'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.module b/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.module
new file mode 100644
index 0000000..3060fa6
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_custom_content/ctools_custom_content.module
@@ -0,0 +1,118 @@
+ array(
+ 'title' => t('Administer custom content'),
+ 'description' => t('Add, edit and delete CTools custom stored custom content'),
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_ctools_plugin_directory() to let the system know
+ * we implement task and task_handler plugins.
+ */
+function ctools_custom_content_ctools_plugin_directory($module, $plugin) {
+ // Most of this module is implemented as an export ui plugin, and the
+ // rest is in ctools/includes/ctools_custom_content.inc.
+ if ($module == 'ctools' && $plugin == 'export_ui') {
+ return 'plugins/' . $plugin;
+ }
+}
+
+/**
+ * Implements hook_get_pane_links_alter().
+ */
+function ctools_custom_content_get_pane_links_alter(&$links, $pane, $content_type) {
+ if ($pane->type == 'custom') {
+ if (!isset($pane->configuration['name'])) {
+ $name_of_pane = $pane->subtype;
+ }
+ else {
+ $name_of_pane = $pane->configuration['name'];
+ }
+
+ $links['top']['edit_custom_content'] = array(
+ 'title' => t('Edit custom content pane'),
+ 'href' => url('admin/structure/ctools-content/list/' . $name_of_pane . '/edit', array('absolute' => TRUE)),
+ 'attributes' => array('target' => array('_blank')),
+ );
+ }
+}
+
+/**
+ * Create callback for creating a new CTools custom content type.
+ *
+ * This ensures we get proper defaults from the plugin for its settings.
+ */
+function ctools_content_type_new($set_defaults) {
+ $item = ctools_export_new_object('ctools_custom_content', $set_defaults);
+ ctools_include('content');
+ $plugin = ctools_get_content_type('custom');
+ $item->settings = ctools_content_get_defaults($plugin, array());
+ return $item;
+}
+
+/**
+ * Implementation of hook_panels_dashboard_blocks().
+ *
+ * Adds page information to the Panels dashboard.
+ */
+function ctools_custom_content_panels_dashboard_blocks(&$vars) {
+ $vars['links']['ctools_custom_content'] = array(
+ 'title' => l(t('Custom content'), 'admin/structure/ctools-content/add'),
+ 'description' => t('Custom content panes are basic HTML you enter that can be reused in all of your panels.'),
+ );
+
+ // Load all mini panels and their displays.
+ ctools_include('export');
+ $items = ctools_export_crud_load_all('ctools_custom_content');
+ $count = 0;
+ $rows = array();
+
+ foreach ($items as $item) {
+ $rows[] = array(
+ check_plain($item->admin_title),
+ array(
+ 'data' => l(t('Edit'), "admin/structure/ctools-content/list/$item->name/edit"),
+ 'class' => 'links',
+ ),
+ );
+
+ // Only show 10.
+ if (++$count >= 10) {
+ break;
+ }
+ }
+
+ if ($rows) {
+ $content = theme('table', array('rows' => $rows, 'attributes' => array('class' => 'panels-manage')));
+ }
+ else {
+ $content = '' . t('There are no custom content panes.') . '
';
+ }
+
+ $vars['blocks']['ctools_custom_content'] = array(
+ 'title' => t('Manage custom content'),
+ 'link' => l(t('Go to list'), 'admin/structure/ctools-content'),
+ 'content' => $content,
+ 'class' => 'dashboard-content',
+ 'section' => 'right',
+ );
+}
diff --git a/sites/all/modules/ctools/ctools_custom_content/plugins/export_ui/ctools_custom_content.inc b/sites/all/modules/ctools/ctools_custom_content/plugins/export_ui/ctools_custom_content.inc
new file mode 100644
index 0000000..c7933bc
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_custom_content/plugins/export_ui/ctools_custom_content.inc
@@ -0,0 +1,23 @@
+ 'ctools_custom_content',
+ 'access' => 'administer custom content',
+
+ 'menu' => array(
+ 'menu item' => 'ctools-content',
+ 'menu title' => 'Custom content panes',
+ 'menu description' => 'Add, edit or delete custom content panes.',
+ ),
+
+ 'title singular' => t('content pane'),
+ 'title singular proper' => t('Content pane'),
+ 'title plural' => t('content panes'),
+ 'title plural proper' => t('Content panes'),
+
+ 'handler' => 'ctools_custom_content_ui',
+);
diff --git a/sites/all/modules/ctools/ctools_custom_content/plugins/export_ui/ctools_custom_content_ui.class.php b/sites/all/modules/ctools/ctools_custom_content/plugins/export_ui/ctools_custom_content_ui.class.php
new file mode 100644
index 0000000..e56f7a7
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_custom_content/plugins/export_ui/ctools_custom_content_ui.class.php
@@ -0,0 +1,150 @@
+settings['body'])) {
+ $form_state['item']->settings['format'] = $form_state['item']->settings['body']['format'];
+ $form_state['item']->settings['body'] = $form_state['item']->settings['body']['value'];
+ }
+ parent::edit_form($form, $form_state);
+
+ $form['category'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Category'),
+ '#description' => t('What category this content should appear in. If left blank the category will be "Miscellaneous".'),
+ '#default_value' => $form_state['item']->category,
+ );
+
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $form_state['item']->settings['title'],
+ '#title' => t('Title'),
+ );
+
+ $form['title_heading'] = array(
+ '#title' => t('Title heading'),
+ '#type' => 'select',
+ '#default_value' => isset($form_state['item']->settings['title_heading']) ? $form_state['item']->settings['title_heading'] : 'h2',
+ '#options' => array(
+ 'h1' => t('h1'),
+ 'h2' => t('h2'),
+ 'h3' => t('h3'),
+ 'h4' => t('h4'),
+ 'h5' => t('h5'),
+ 'h6' => t('h6'),
+ 'div' => t('div'),
+ 'span' => t('span'),
+ ),
+ );
+
+ $form['body'] = array(
+ '#type' => 'text_format',
+ '#title' => t('Body'),
+ '#default_value' => $form_state['item']->settings['body'],
+ '#format' => $form_state['item']->settings['format'],
+ );
+
+ $form['substitute'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Use context keywords'),
+ '#description' => t('If checked, context keywords will be substituted in this content.'),
+ '#default_value' => !empty($form_state['item']->settings['substitute']),
+ );
+ }
+
+ public function edit_form_submit(&$form, &$form_state) {
+ parent::edit_form_submit($form, $form_state);
+
+ // Since items in our settings are not in the schema, we have to do these manually:
+ $form_state['item']->settings['title'] = $form_state['values']['title'];
+ $form_state['item']->settings['title_heading'] = $form_state['values']['title_heading'];
+ $form_state['item']->settings['body'] = $form_state['values']['body']['value'];
+ $form_state['item']->settings['format'] = $form_state['values']['body']['format'];
+ $form_state['item']->settings['substitute'] = $form_state['values']['substitute'];
+ }
+
+ public function list_form(&$form, &$form_state) {
+ parent::list_form($form, $form_state);
+
+ $options = array('all' => t('- All -'));
+ foreach ($this->items as $item) {
+ $options[$item->category] = $item->category;
+ }
+
+ $form['top row']['category'] = array(
+ '#type' => 'select',
+ '#title' => t('Category'),
+ '#options' => $options,
+ '#default_value' => 'all',
+ '#weight' => -10,
+ );
+ }
+
+ public function list_filter($form_state, $item) {
+ if ($form_state['values']['category'] != 'all' && $form_state['values']['category'] != $item->category) {
+ return TRUE;
+ }
+
+ return parent::list_filter($form_state, $item);
+ }
+
+ public function list_sort_options() {
+ return array(
+ 'disabled' => t('Enabled, title'),
+ 'title' => t('Title'),
+ 'name' => t('Name'),
+ 'category' => t('Category'),
+ 'storage' => t('Storage'),
+ );
+ }
+
+ public function list_build_row($item, &$form_state, $operations) {
+ // Set up sorting.
+ switch ($form_state['values']['order']) {
+ case 'disabled':
+ $this->sorts[$item->name] = empty($item->disabled) . $item->admin_title;
+ break;
+
+ case 'title':
+ $this->sorts[$item->name] = $item->admin_title;
+ break;
+
+ case 'name':
+ $this->sorts[$item->name] = $item->name;
+ break;
+
+ case 'category':
+ $this->sorts[$item->name] = $item->category;
+ break;
+
+ case 'storage':
+ $this->sorts[$item->name] = $item->type . $item->admin_title;
+ break;
+ }
+
+ $ops = theme('links__ctools_dropbutton', array('links' => $operations, 'attributes' => array('class' => array('links', 'inline'))));
+
+ $this->rows[$item->name] = array(
+ 'data' => array(
+ array('data' => check_plain($item->name), 'class' => array('ctools-export-ui-name')),
+ array('data' => check_plain($item->admin_title), 'class' => array('ctools-export-ui-title')),
+ array('data' => check_plain($item->category), 'class' => array('ctools-export-ui-category')),
+ array('data' => $ops, 'class' => array('ctools-export-ui-operations')),
+ ),
+ 'title' => check_plain($item->admin_description),
+ 'class' => array(!empty($item->disabled) ? 'ctools-export-ui-disabled' : 'ctools-export-ui-enabled'),
+ );
+ }
+
+ public function list_table_header() {
+ return array(
+ array('data' => t('Name'), 'class' => array('ctools-export-ui-name')),
+ array('data' => t('Title'), 'class' => array('ctools-export-ui-title')),
+ array('data' => t('Category'), 'class' => array('ctools-export-ui-category')),
+ array('data' => t('Operations'), 'class' => array('ctools-export-ui-operations')),
+ );
+ }
+
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/README.txt b/sites/all/modules/ctools/ctools_plugin_example/README.txt
new file mode 100644
index 0000000..2f9b0ff
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/README.txt
@@ -0,0 +1,14 @@
+
+The CTools Plugin Example is an example for developers of how to CTools
+access, argument, content type, context, and relationship plugins.
+
+There are a number of ways to profit from this:
+
+1. The code itself intends to be as simple and self-explanatory as possible.
+ Nothing fancy is attempted: It's just trying to use the plugin API to show
+ how it can be used.
+
+2. There is a sample panel. You can access it at /ctools_plugin_example/xxxx
+ to see how it works.
+
+3. There is Advanced Help at admin/advanced_help/ctools_plugin_example.
diff --git a/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.info b/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.info
new file mode 100644
index 0000000..40f133b
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.info
@@ -0,0 +1,16 @@
+name = Chaos Tools (CTools) Plugin Example
+description = Shows how an external module can provide ctools plugins (for Panels, etc.).
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+dependencies[] = ctools
+dependencies[] = panels
+dependencies[] = page_manager
+dependencies[] = advanced_help
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2018-02-24
+version = "7.x-1.14"
+core = "7.x"
+project = "ctools"
+datestamp = "1519455788"
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.module b/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.module
new file mode 100644
index 0000000..a9a6080
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.module
@@ -0,0 +1,94 @@
+ 'CTools plugin example',
+ 'description' => t("Demonstration code, advanced help, and a demo panel to show how to build ctools plugins."),
+ 'page callback' => 'ctools_plugin_example_explanation_page',
+ 'access arguments' => array('administer site configuration'),
+ 'type' => MENU_NORMAL_ITEM,
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ *
+ * It simply tells panels where to find the .inc files that define various
+ * args, contexts, content_types. In this case the subdirectories of
+ * ctools_plugin_example/panels are used.
+ */
+function ctools_plugin_example_ctools_plugin_directory($module, $plugin) {
+ if ($module == 'ctools' && !empty($plugin)) {
+ return "plugins/$plugin";
+ }
+}
+
+/**
+ * Implement hook_ctools_plugin_api().
+ *
+ * If you do this, CTools will pick up default panels pages in
+ * .pages_default.inc.
+ */
+function ctools_plugin_example_ctools_plugin_api($module, $api) {
+ // @todo -- this example should explain how to put it in a different file.
+ if ($module == 'panels_mini' && $api == 'panels_default') {
+ return array('version' => 1);
+ }
+ if ($module == 'page_manager' && $api == 'pages_default') {
+ return array('version' => 1);
+ }
+}
+
+/**
+ * Just provide an explanation page for the admin section.
+ *
+ * @return unknown_type
+ */
+function ctools_plugin_example_explanation_page() {
+ $content = '' . t("The CTools Plugin Example is simply a developer's demo of how to create plugins for CTools. It provides no useful functionality for an ordinary user.") . '
';
+
+ $content .= '' . t(
+ 'There is a demo panel demonstrating much of the functionality provided at
+ CTools demo panel , and you can find documentation on the examples at
+ !ctools_plugin_example_help.
+ CTools itself provides documentation at !ctools_help. Mostly, though, the code itself is intended to be the teacher.
+ You can find it in %path.',
+ array(
+ '@demo_url' => url('ctools_plugin_example/xxxxx'),
+ '!ctools_plugin_example_help' => theme('advanced_help_topic', array('module' => 'ctools_plugin_example', 'topic' => 'Chaos-Tools--CTools--Plugin-Examples', 'type' => 'title')),
+ '!ctools_help' => theme('advanced_help_topic', array('module' => 'ctools', 'topic' => 'plugins', 'type' => 'title')),
+ '%path' => drupal_get_path('module', 'ctools_plugin_example'),
+ )) . '
';
+
+ return $content;
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.pages_default.inc b/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.pages_default.inc
new file mode 100644
index 0000000..bbabf22
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/ctools_plugin_example.pages_default.inc
@@ -0,0 +1,447 @@
+.pages_default.inc
+ * With this naming, no additional code needs to be provided. CTools will just find the file.
+ * The name of the hook is _default_page_manager_pages()
+ *
+ * This example provides two pages, but the returned array could
+ * have several pages.
+ *
+ * @return
+ * Array of pages, normally exported from Panels.
+ */
+function ctools_plugin_example_default_page_manager_pages() {
+
+ // Begin exported panel.
+ $page = new stdClass();
+ $page->disabled = FALSE; /* Edit this to true to make a default page disabled initially */
+ $page->api_version = 1;
+ $page->name = 'ctools_plugin_example';
+ $page->task = 'page';
+ $page->admin_title = 'CTools plugin example';
+ $page->admin_description = 'This panel provides no functionality to a working Drupal system. It\'s intended to display the various sample plugins provided by the CTools Plugin Example module. ';
+ $page->path = 'ctools_plugin_example/%sc';
+ $page->access = array(
+ 'logic' => 'and',
+ );
+ $page->menu = array(
+ 'type' => 'normal',
+ 'title' => 'CTools plugin example',
+ 'name' => 'navigation',
+ 'weight' => '0',
+ 'parent' => array(
+ 'type' => 'none',
+ 'title' => '',
+ 'name' => 'navigation',
+ 'weight' => '0',
+ ),
+ );
+ $page->arguments = array(
+ 'sc' => array(
+ 'id' => 2,
+ 'identifier' => 'simplecontext-arg',
+ 'name' => 'simplecontext_arg',
+ 'settings' => array(),
+ ),
+ );
+ $page->conf = array();
+ $page->default_handlers = array();
+ $handler = new stdClass();
+ $handler->disabled = FALSE; /* Edit this to true to make a default handler disabled initially */
+ $handler->api_version = 1;
+ $handler->name = 'page_ctools_panel_context';
+ $handler->task = 'page';
+ $handler->subtask = 'ctools_plugin_example';
+ $handler->handler = 'panel_context';
+ $handler->weight = 0;
+ $handler->conf = array(
+ 'title' => 'Panel',
+ 'no_blocks' => FALSE,
+ 'css_id' => '',
+ 'css' => '',
+ 'contexts' => array(
+ '0' => array(
+ 'name' => 'simplecontext',
+ 'id' => 1,
+ 'identifier' => 'Configured simplecontext (not from argument)',
+ 'keyword' => 'configured_simplecontext',
+ 'context_settings' => array(
+ 'sample_simplecontext_setting' => 'default simplecontext setting',
+ ),
+ ),
+ ),
+ 'relationships' => array(
+ '0' => array(
+ 'context' => 'argument_simplecontext_arg_2',
+ 'name' => 'relcontext_from_simplecontext',
+ 'id' => 1,
+ 'identifier' => 'Relcontext from simplecontext (from relationship)',
+ 'keyword' => 'relcontext',
+ ),
+ ),
+ 'access' => array(
+ 'logic' => 'and',
+ ),
+ );
+ $display = new panels_display();
+ $display->layout = 'threecol_33_34_33_stacked';
+ $display->layout_settings = array();
+ $display->panel_settings = array(
+ 'style' => 'rounded_corners',
+ 'style_settings' => array(
+ 'default' => array(
+ 'corner_location' => 'pane',
+ ),
+ ),
+ );
+ $display->cache = array();
+ $display->title = 'CTools plugin example panel';
+ $display->hide_title = FALSE;
+ $display->title_pane = 1;
+ $display->content = array();
+ $display->panels = array();
+ $pane = new stdClass();
+ $pane->pid = 'new-1';
+ $pane->panel = 'left';
+ $pane->type = 'no_context_content_type';
+ $pane->subtype = 'no_context_content_type';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'item1' => 'contents of config item 1',
+ 'item2' => 'contents of config item 2',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 0;
+ $display->content['new-1'] = $pane;
+ $display->panels['left'][0] = 'new-1';
+ $pane = new stdClass();
+ $pane->pid = 'new-2';
+ $pane->panel = 'left';
+ $pane->type = 'custom';
+ $pane->subtype = 'custom';
+ $pane->shown = TRUE;
+ $pane->access = array(
+ 'plugins' => array(
+ '0' => array(
+ 'name' => 'arg_length',
+ 'settings' => array(
+ 'greater_than' => '1',
+ 'arg_length' => '4',
+ ),
+ 'context' => 'argument_simplecontext_arg_2',
+ ),
+ ),
+ );
+ $pane->configuration = array(
+ 'title' => 'Long Arg Visibility Block',
+ 'body' => 'This block will be here when the argument is longer than configured arg length. It uses the \'arg_length\' access plugin to test against the length of the argument used for Simplecontext.',
+ 'format' => '1',
+ 'substitute' => 1,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 1;
+ $display->content['new-2'] = $pane;
+ $display->panels['left'][1] = 'new-2';
+ $pane = new stdClass();
+ $pane->pid = 'new-3';
+ $pane->panel = 'left';
+ $pane->type = 'custom';
+ $pane->subtype = 'custom';
+ $pane->shown = TRUE;
+ $pane->access = array(
+ 'plugins' => array(
+ '0' => array(
+ 'name' => 'arg_length',
+ 'settings' => array(
+ 'greater_than' => '0',
+ 'arg_length' => '4',
+ ),
+ 'context' => 'argument_simplecontext_arg_2',
+ ),
+ ),
+ );
+ $pane->configuration = array(
+ 'title' => 'Short Arg Visibility',
+ 'body' => 'This block appears when the simplecontext argument is less than the configured length.',
+ 'format' => '1',
+ 'substitute' => 1,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 2;
+ $display->content['new-3'] = $pane;
+ $display->panels['left'][2] = 'new-3';
+ $pane = new stdClass();
+ $pane->pid = 'new-4';
+ $pane->panel = 'middle';
+ $pane->type = 'simplecontext_content_type';
+ $pane->subtype = 'simplecontext_content_type';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'buttons' => NULL,
+ '#validate' => NULL,
+ '#submit' => NULL,
+ '#action' => NULL,
+ 'context' => 'argument_simplecontext_arg_2',
+ 'aligner_start' => NULL,
+ 'override_title' => 1,
+ 'override_title_text' => 'Simplecontext (with an arg)',
+ 'aligner_stop' => NULL,
+ 'override_title_markup' => NULL,
+ 'config_item_1' => 'Config item 1 contents',
+ '#build_id' => NULL,
+ '#type' => NULL,
+ '#programmed' => NULL,
+ 'form_build_id' => 'form-19c4ae6cb54fad8f096da46e95694e5a',
+ '#token' => NULL,
+ 'form_token' => '17141d3531eaa7b609da78afa6f3b560',
+ 'form_id' => 'simplecontext_content_type_edit_form',
+ '#id' => NULL,
+ '#description' => NULL,
+ '#attributes' => NULL,
+ '#required' => NULL,
+ '#tree' => NULL,
+ '#parents' => NULL,
+ '#method' => NULL,
+ '#post' => NULL,
+ '#processed' => NULL,
+ '#defaults_loaded' => NULL,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 0;
+ $display->content['new-4'] = $pane;
+ $display->panels['middle'][0] = 'new-4';
+ $pane = new stdClass();
+ $pane->pid = 'new-5';
+ $pane->panel = 'middle';
+ $pane->type = 'simplecontext_content_type';
+ $pane->subtype = 'simplecontext_content_type';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'buttons' => NULL,
+ '#validate' => NULL,
+ '#submit' => NULL,
+ '#action' => NULL,
+ 'context' => 'context_simplecontext_1',
+ 'aligner_start' => NULL,
+ 'override_title' => 1,
+ 'override_title_text' => 'Configured simplecontext content type (not from arg)',
+ 'aligner_stop' => NULL,
+ 'override_title_markup' => NULL,
+ 'config_item_1' => '(configuration for simplecontext)',
+ '#build_id' => NULL,
+ '#type' => NULL,
+ '#programmed' => NULL,
+ 'form_build_id' => 'form-d016200490abd015dc5b8a7e366d76ea',
+ '#token' => NULL,
+ 'form_token' => '17141d3531eaa7b609da78afa6f3b560',
+ 'form_id' => 'simplecontext_content_type_edit_form',
+ '#id' => NULL,
+ '#description' => NULL,
+ '#attributes' => NULL,
+ '#required' => NULL,
+ '#tree' => NULL,
+ '#parents' => NULL,
+ '#method' => NULL,
+ '#post' => NULL,
+ '#processed' => NULL,
+ '#defaults_loaded' => NULL,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 1;
+ $display->content['new-5'] = $pane;
+ $display->panels['middle'][1] = 'new-5';
+ $pane = new stdClass();
+ $pane->pid = 'new-6';
+ $pane->panel = 'middle';
+ $pane->type = 'custom';
+ $pane->subtype = 'custom';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'admin_title' => 'Simplecontext keyword usage',
+ 'title' => 'Simplecontext keyword usage',
+ 'body' => 'Demonstrating context keyword usage:
+ item1 is %sc:item1
+ item2 is %sc:item2
+ description is %sc:description',
+ 'format' => '1',
+ 'substitute' => 1,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 2;
+ $display->content['new-6'] = $pane;
+ $display->panels['middle'][2] = 'new-6';
+ $pane = new stdClass();
+ $pane->pid = 'new-7';
+ $pane->panel = 'right';
+ $pane->type = 'relcontext_content_type';
+ $pane->subtype = 'relcontext_content_type';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'buttons' => NULL,
+ '#validate' => NULL,
+ '#submit' => NULL,
+ '#action' => NULL,
+ 'context' => 'relationship_relcontext_from_simplecontext_1',
+ 'aligner_start' => NULL,
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'aligner_stop' => NULL,
+ 'override_title_markup' => NULL,
+ 'config_item_1' => 'some stuff in this one',
+ '#build_id' => NULL,
+ '#type' => NULL,
+ '#programmed' => NULL,
+ 'form_build_id' => 'form-8485f84511bd06e51b4a48e998448054',
+ '#token' => NULL,
+ 'form_token' => '1c3356396374d51d7d2531a10fd25310',
+ 'form_id' => 'relcontext_edit_form',
+ '#id' => NULL,
+ '#description' => NULL,
+ '#attributes' => NULL,
+ '#required' => NULL,
+ '#tree' => NULL,
+ '#parents' => NULL,
+ '#method' => NULL,
+ '#post' => NULL,
+ '#processed' => NULL,
+ '#defaults_loaded' => NULL,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 0;
+ $display->content['new-7'] = $pane;
+ $display->panels['right'][0] = 'new-7';
+ $pane = new stdClass();
+ $pane->pid = 'new-8';
+ $pane->panel = 'top';
+ $pane->type = 'custom';
+ $pane->subtype = 'custom';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'title' => 'Demonstrating ctools plugins',
+ 'body' => 'The CTools Plugin Example module (and this panel page) are just here to demonstrate how to build CTools plugins.
+
+ ',
+ 'format' => '2',
+ 'substitute' => 1,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 0;
+ $display->content['new-8'] = $pane;
+ $display->panels['top'][0] = 'new-8';
+ $handler->conf['display'] = $display;
+ $page->default_handlers[$handler->name] = $handler;
+
+ // End of exported panel.
+ $pages['ctools_plugin_example_demo_page'] = $page;
+
+ // Begin exported panel.
+ $page = new stdClass();
+ $page->disabled = FALSE; /* Edit this to true to make a default page disabled initially */
+ $page->api_version = 1;
+ $page->name = 'ctools_plugin_example_base';
+ $page->task = 'page';
+ $page->admin_title = 'CTools Plugin Example base page';
+ $page->admin_description = 'This panel is for when people hit /ctools_plugin_example without an argument. We can use it to tell people to move on.';
+ $page->path = 'ctools_plugin_example';
+ $page->access = array();
+ $page->menu = array();
+ $page->arguments = array();
+ $page->conf = array();
+ $page->default_handlers = array();
+ $handler = new stdClass();
+ $handler->disabled = FALSE; /* Edit this to true to make a default handler disabled initially */
+ $handler->api_version = 1;
+ $handler->name = 'page_ctools_plugin_example_base_panel_context';
+ $handler->task = 'page';
+ $handler->subtask = 'ctools_plugin_example_base';
+ $handler->handler = 'panel_context';
+ $handler->weight = 0;
+ $handler->conf = array(
+ 'title' => 'Panel',
+ 'no_blocks' => FALSE,
+ 'css_id' => '',
+ 'css' => '',
+ 'contexts' => array(),
+ 'relationships' => array(),
+ );
+ $display = new panels_display();
+ $display->layout = 'onecol';
+ $display->layout_settings = array();
+ $display->panel_settings = array();
+ $display->cache = array();
+ $display->title = '';
+ $display->hide_title = FALSE;
+ $display->content = array();
+ $display->panels = array();
+ $pane = new stdClass();
+ $pane->pid = 'new-1';
+ $pane->panel = 'middle';
+ $pane->type = 'custom';
+ $pane->subtype = 'custom';
+ $pane->shown = TRUE;
+ $pane->access = array();
+ $pane->configuration = array(
+ 'title' => 'Use this page with an argument',
+ 'body' => 'This demo page works if you use an argument, like ctools_plugin_example/xxxxx .',
+ 'format' => '1',
+ 'substitute' => NULL,
+ );
+ $pane->cache = array();
+ $pane->style = array();
+ $pane->css = array();
+ $pane->extras = array();
+ $pane->position = 0;
+ $display->content['new-1'] = $pane;
+ $display->panels['middle'][0] = 'new-1';
+ $handler->conf['display'] = $display;
+ $page->default_handlers[$handler->name] = $handler;
+ // End exported panel.
+ $pages['base_page'] = $page;
+
+ return $pages;
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Access-Plugins--Determining-access-and-visibility.html b/sites/all/modules/ctools/ctools_plugin_example/help/Access-Plugins--Determining-access-and-visibility.html
new file mode 100644
index 0000000..781260e
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Access-Plugins--Determining-access-and-visibility.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
We can use access plugins to determine access to a page or visibility of the panes in a page. Basically, we just determine access based on configuration settings and the various contexts that are available to us.
+
The arbitrary example in plugins/access/arg_length.inc determines access based on the length of the simplecontext argument. You can configure whether access should be granted if the simplecontext argument is greater or less than some number.
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Argument-Plugins--Starting-at-the-beginning.html b/sites/all/modules/ctools/ctools_plugin_example/help/Argument-Plugins--Starting-at-the-beginning.html
new file mode 100644
index 0000000..4dd569d
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Argument-Plugins--Starting-at-the-beginning.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
Contexts are fundamental to CTools, and they almost always start with an argument to a panels page, so we'll start there too.
+
We first need to process an argument.
+
We're going to work with a "Simplecontext" context type and argument, and then with a content type that displays it. So we'll start by with the Simplecontext argument plugin in plugins/arguments/simplecontext_arg.inc.
+
Note that the name of the file (simplecontext_arg.inc) is built from the machine name of our plugin (simplecontext_arg). And note also that the primary function that we use to provide our argument (ctools_plugin_example_simplecontext_arg_ctools_arguments()) is also built from the machine name. This magic is most of the naming magic that you have to know.
+
You can browse plugins/arguments/simplecontext_arg.inc and see the little that it does.
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Chaos-Tools--CTools--Plugin-Examples.html b/sites/all/modules/ctools/ctools_plugin_example/help/Chaos-Tools--CTools--Plugin-Examples.html
new file mode 100644
index 0000000..7576c80
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Chaos-Tools--CTools--Plugin-Examples.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
This demonstration module is intended for developers to look at and play with. CTools plugins are not terribly difficult to do, but it can be hard to sort through the various arguments and required functions. The idea here is that you should have a starting point for most anything you want to do. Just work through the example, and then start experimenting with changing it.
+
There are two parts to this demo:
+
First, there is a sample panel provided that uses all the various plugins. It's at ctools_example/12345 . You can edit the panel and configure all the panes on it.
+
Second, the code is there for you to experiment with and change as you see fit. Sometimes starting with simple code and working with it can take you places that it's hard to go when you're looking at more complex examples.
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Content-Type-Plugins--Displaying-content-using-a-context.html b/sites/all/modules/ctools/ctools_plugin_example/help/Content-Type-Plugins--Displaying-content-using-a-context.html
new file mode 100644
index 0000000..918a13c
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Content-Type-Plugins--Displaying-content-using-a-context.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
Now we get to the heart of the matter: Building a content type plugin. A content type plugin uses the contexts available to it to display something. plugins/content_types/simplecontext_content_type.inc does this work for us.
+
Note that our content type also has an edit form which can be used to configure its behavior. This settings form is accessed through the panels interface, and it's up to you what the settings mean to the code and the generation of content in the display rendering.
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Context-plugins--Creating-a--context--from-an-argument.html b/sites/all/modules/ctools/ctools_plugin_example/help/Context-plugins--Creating-a--context--from-an-argument.html
new file mode 100644
index 0000000..e8efbb3
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Context-plugins--Creating-a--context--from-an-argument.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
Now that we have a plugin for a simplecontext argument, we can create a plugin for a simplecontext context.
+
Normally, a context would take an argument which is a key like a node ID (nid) and retrieve a more complex object from a database or whatever. In our example, we'll artificially transform the argument into an arbitrary "context" data object.
+
plugins/contexts/simplecontext.inc implements our context.
+
Note that there are actually two ways to create a context. The normal one, which we've been referring to, is to create a context from an argument. However, it is also possible to configure a context in a panel using the panels interface. This is quite inflexible, but might be useful for configuring single page. However, it means that we have a settings form for exactly that purpose. Our context would have to know how to create itself from a settings form as well as from an argument. Simplecontext can do that.
+
A context plugin can also provide keywords that expose parts of its context using keywords like masterkeyword:dataitem. The node plugin for ctools has node:nid and node:title, for example. The simplecontext plugin here provides the simplest of keywords.
+
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Module-setup-and-hooks.html b/sites/all/modules/ctools/ctools_plugin_example/help/Module-setup-and-hooks.html
new file mode 100644
index 0000000..f816917
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Module-setup-and-hooks.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
Your module must provide a few things so that your plugins can be found.
+
First, you need to implement hook_ctools_plugin_directory(). Here we're telling CTools that our plugins will be found in the module's directory in the plugins/<plugintype> directory. Context plugins will be in ctools_plugin_example/plugins/contexts, Content-type plugins will be in ctools_plugin_example/plugins/content_types.
+
<?php function ctools_plugin_example_ctools_plugin_directory ( $module , $plugin ) { if ( $module == 'ctools' && !empty( $plugin )) { return "plugins/$plugin" ; } } ?>
+
Second, if you module wants to provide default panels pages, you can implement hook_ctools_plugin_api(). CTools will then pick up your panels pages in the file named <modulename>.pages_default.inc.
+
<?php function ctools_plugin_example_ctools_plugin_api ( $module , $api ) { if ( $module == 'panels_mini' && $api == 'panels_default' ) { return array( 'version' => 1 ); } if ( $module == 'page_manager' && $api == 'pages_default' ) { return array( 'version' => 1 ); } } ?>
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/Relationships--Letting-one-context-take-us-to-another.html b/sites/all/modules/ctools/ctools_plugin_example/help/Relationships--Letting-one-context-take-us-to-another.html
new file mode 100644
index 0000000..7691245
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/Relationships--Letting-one-context-take-us-to-another.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
Often a single data type can lead us to other data types. For example, a node has a user (the author) and the user has data associated with it.
+
A relationship plugin allows this kind of data to be accessed.
+
An example relationship plugin is provided in plugins/relationships/relcontext_from_simplecontext.inc. It looks at a simplecontext (which we got from an argument) and builds an (artificial) "relcontext" from that.
+
+
+
+
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/help/ctools_plugin_example.help.ini b/sites/all/modules/ctools/ctools_plugin_example/help/ctools_plugin_example.help.ini
new file mode 100644
index 0000000..6fb3d4c
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/help/ctools_plugin_example.help.ini
@@ -0,0 +1,42 @@
+[Chaos-Tools--CTools--Plugin-Examples]
+title = CTools Plugin Examples
+file = Chaos-Tools--CTools--Plugin-Examples
+weight = 0
+parent =
+
+[Module-setup-and-hooks]
+title = Module setup and hooks
+file = Module-setup-and-hooks
+weight = -15
+parent = Chaos-Tools--CTools--Plugin-Examples
+
+[Argument-Plugins--Starting-at-the-beginning]
+title = Argument Plugins: Starting at the beginning
+file = Argument-Plugins--Starting-at-the-beginning
+weight = -14
+parent = Chaos-Tools--CTools--Plugin-Examples
+
+[Context-plugins--Creating-a--context--from-an-argument]
+title = Context plugins: Creating a context from an argument
+file = Context-plugins--Creating-a--context--from-an-argument
+weight = -13
+parent = Chaos-Tools--CTools--Plugin-Examples
+
+[Content-Type-Plugins--Displaying-content-using-a-context]
+title = Content Type Plugins: Displaying content using a context
+file = Content-Type-Plugins--Displaying-content-using-a-context
+weight = -12
+parent = Chaos-Tools--CTools--Plugin-Examples
+
+[Access-Plugins--Determining-access-and-visibility]
+title = Access Plugins: Determining access and visibility
+file = Access-Plugins--Determining-access-and-visibility
+weight = -11
+parent = Chaos-Tools--CTools--Plugin-Examples
+
+[Relationships--Letting-one-context-take-us-to-another]
+title = Relationships: Letting one context take us to another
+file = Relationships--Letting-one-context-take-us-to-another
+weight = -10
+parent = Chaos-Tools--CTools--Plugin-Examples
+
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/access/arg_length.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/access/arg_length.inc
new file mode 100644
index 0000000..3b5d863
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/access/arg_length.inc
@@ -0,0 +1,66 @@
+ t("Arg length"),
+ 'description' => t('Control access by length of simplecontext argument.'),
+ 'callback' => 'ctools_plugin_example_arg_length_ctools_access_check',
+ 'settings form' => 'ctools_plugin_example_arg_length_ctools_access_settings',
+ 'summary' => 'ctools_plugin_example_arg_length_ctools_access_summary',
+ 'required context' => new ctools_context_required(t('Simplecontext'), 'simplecontext'),
+);
+
+/**
+ * Settings form for the 'by role' access plugin.
+ */
+function ctools_plugin_example_arg_length_ctools_access_settings(&$form, &$form_state, $conf) {
+ $form['settings']['greater_than'] = array(
+ '#type' => 'radios',
+ '#title' => t('Grant access if simplecontext argument length is'),
+ '#options' => array(1 => t('Greater than'), 0 => t('Less than or equal to')),
+ '#default_value' => $conf['greater_than'],
+ );
+ $form['settings']['arg_length'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Length of simplecontext argument'),
+ '#size' => 3,
+ '#default_value' => $conf['arg_length'],
+ '#description' => t('Access/visibility will be granted based on arg length.'),
+ );
+}
+
+/**
+ * Check for access.
+ */
+function ctools_plugin_example_arg_length_ctools_access_check($conf, $context) {
+ // As far as I know there should always be a context at this point, but this
+ // is safe.
+ if (empty($context) || empty($context->data)) {
+ return FALSE;
+ }
+ $compare = ($context->arg_length > $conf['arg_length']);
+ if (($compare && $conf['greater_than']) || (!$compare && !$conf['greater_than'])) {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * Provide a summary description based upon the checked roles.
+ */
+function ctools_plugin_example_arg_length_ctools_access_summary($conf, $context) {
+ return t('Simpletext argument must be !comp @length characters',
+ array(
+ '!comp' => $conf['greater_than'] ? 'greater than' : 'less than or equal to',
+ '@length' => $conf['arg_length'],
+ ));
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/access/example_role.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/access/example_role.inc
new file mode 100644
index 0000000..75721e8
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/access/example_role.inc
@@ -0,0 +1,75 @@
+ t("CTools example: role"),
+ 'description' => t('Control access by role.'),
+ 'callback' => 'ctools_plugin_example_example_role_ctools_access_check',
+ 'default' => array('rids' => array()),
+ 'settings form' => 'ctools_plugin_example_example_role_ctools_access_settings',
+ 'summary' => 'ctools_plugin_example_example_role_ctools_access_summary',
+ 'required context' => new ctools_context_required(t('User'), 'user'),
+);
+
+/**
+ * Settings form for the 'by role' access plugin.
+ */
+function ctools_plugin_example_example_role_ctools_access_settings(&$form, &$form_state, $conf) {
+ $form['settings']['rids'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Role'),
+ '#default_value' => $conf['rids'],
+ '#options' => ctools_get_roles(),
+ '#description' => t('Only the checked roles will be granted access.'),
+ );
+}
+
+/**
+ * Compress the roles allowed to the minimum.
+ */
+function ctools_plugin_example_example_role_ctools_access_settings_submit(&$form, &$form_state) {
+ $form_state['values']['settings']['rids'] = array_keys(array_filter($form_state['values']['settings']['rids']));
+}
+
+/**
+ * Check for access.
+ */
+function ctools_plugin_example_example_role_ctools_access_check($conf, $context) {
+ // As far as I know there should always be a context at this point, but this
+ // is safe.
+ if (empty($context) || empty($context->data) || !isset($context->data->roles)) {
+ return FALSE;
+ }
+
+ $roles = array_keys($context->data->roles);
+ $roles[] = $context->data->uid ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
+ return (bool) array_intersect($conf['rids'], $roles);
+}
+
+/**
+ * Provide a summary description based upon the checked roles.
+ */
+function ctools_plugin_example_example_role_ctools_access_summary($conf, $context) {
+ if (!isset($conf['rids'])) {
+ $conf['rids'] = array();
+ }
+ $roles = ctools_get_roles();
+ $names = array();
+ foreach (array_filter($conf['rids']) as $rid) {
+ $names[] = check_plain($roles[$rid]);
+ }
+ if (empty($names)) {
+ return t('@identifier can have any role', array('@identifier' => $context->identifier));
+ }
+ return format_plural(count($names), '@identifier must have role "@roles"', '@identifier can be one of "@roles"', array('@roles' => implode(', ', $names), '@identifier' => $context->identifier));
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/arguments/simplecontext_arg.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/arguments/simplecontext_arg.inc
new file mode 100644
index 0000000..7fb732c
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/arguments/simplecontext_arg.inc
@@ -0,0 +1,49 @@
+ t("Simplecontext arg"),
+ // Keyword to use for %substitution.
+ 'keyword' => 'simplecontext',
+ 'description' => t('Creates a "simplecontext" from the arg.'),
+ 'context' => 'simplecontext_arg_context',
+ // placeholder_form is used in panels preview, for example, so we can
+ // preview without getting the arg from a URL.
+ 'placeholder form' => array(
+ '#type' => 'textfield',
+ '#description' => t('Enter the simplecontext arg'),
+ ),
+);
+
+/**
+ * Get the simplecontext context using the arg. In this case we're just going
+ * to manufacture the context from the data in the arg, but normally it would
+ * be an API call, db lookup, etc.
+ */
+function simplecontext_arg_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+ // If $empty == TRUE it wants a generic, unfilled context.
+ if ($empty) {
+ return ctools_context_create_empty('simplecontext');
+ }
+ // Do whatever error checking is required, returning FALSE if it fails the test
+ // Normally you'd check
+ // for a missing object, one you couldn't create, etc.
+ if (empty($arg)) {
+ return FALSE;
+ }
+ return ctools_context_create('simplecontext', $arg);
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/icon_example.png b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/icon_example.png
new file mode 100644
index 0000000..58c6bee
Binary files /dev/null and b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/icon_example.png differ
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/no_context_content_type.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/no_context_content_type.inc
new file mode 100644
index 0000000..48ce0f5
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/no_context_content_type.inc
@@ -0,0 +1,114 @@
+ t('CTools example no context content type'),
+ 'description' => t('No context content type - requires and uses no context.'),
+
+ // 'single' => TRUE means has no subtypes.
+ 'single' => TRUE,
+ // Constructor.
+ 'content_types' => array('no_context_content_type'),
+ // Name of a function which will render the block.
+ 'render callback' => 'no_context_content_type_render',
+ // The default context.
+ 'defaults' => array(),
+
+ // This explicitly declares the config form. Without this line, the func would be
+ // ctools_plugin_example_no_context_content_type_edit_form.
+ 'edit form' => 'no_context_content_type_edit_form',
+
+ // Icon goes in the directory with the content type.
+ 'icon' => 'icon_example.png',
+ 'category' => array(t('CTools Examples'), -9),
+
+ // This example does not provide 'admin info', which would populate the
+ // panels builder page preview.
+);
+
+/**
+ * Run-time rendering of the body of the block.
+ *
+ * @param $subtype
+ * @param $conf
+ * Configuration as done at admin time.
+ * @param $args
+ * @param $context
+ * Context - in this case we don't have any.
+ *
+ * @return
+ * An object with at least title and content members.
+ */
+function no_context_content_type_render($subtype, $conf, $args, $context) {
+ $block = new stdClass();
+
+ $ctools_help = theme('advanced_help_topic', array('module' => 'ctools', 'topic' => 'plugins', 'type' => 'title'));
+ $ctools_plugin_example_help = theme('advanced_help_topic', array('module' => 'ctools_plugin_example', 'topic' => 'Chaos-Tools--CTools--Plugin-Examples', 'type' => 'title'));
+
+ // The title actually used in rendering.
+ $block->title = check_plain("No-context content type");
+ $block->content = t("
+ Welcome to the CTools Plugin Example demonstration content type.
+
+ This block is a content type which requires no context at all. It's like a custom block,
+ but not even that sophisticated.
+
+ For more information on the example plugins, please see the advanced help for
+
+ {$ctools_help} and {$ctools_plugin_example_help}
+
+ ");
+ if (!empty($conf)) {
+ $block->content .= 'The only information that can be displayed in this block comes from the code and its settings form:
';
+ $block->content .= '' . var_export($conf, TRUE) . '
';
+ }
+
+ return $block;
+
+}
+
+/**
+ * 'Edit form' callback for the content type.
+ * This example just returns a form; validation and submission are standard drupal
+ * Note that if we had not provided an entry for this in hook_content_types,
+ * this could have had the default name
+ * ctools_plugin_example_no_context_content_type_edit_form.
+ */
+function no_context_content_type_edit_form($form, &$form_state) {
+ $conf = $form_state['conf'];
+ $form['item1'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Item1'),
+ '#size' => 50,
+ '#description' => t('The setting for item 1.'),
+ '#default_value' => !empty($conf['item1']) ? $conf['item1'] : '',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ $form['item2'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Item2'),
+ '#size' => 50,
+ '#description' => t('The setting for item 2'),
+ '#default_value' => !empty($conf['item2']) ? $conf['item2'] : '',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ return $form;
+}
+
+function no_context_content_type_edit_form_submit($form, &$form_state) {
+ foreach (array('item1', 'item2') as $key) {
+ $form_state['conf'][$key] = $form_state['values'][$key];
+ }
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/relcontext_content_type.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/relcontext_content_type.inc
new file mode 100644
index 0000000..ced6411
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/relcontext_content_type.inc
@@ -0,0 +1,100 @@
+ t('CTools example relcontext content type'),
+ 'admin info' => 'ctools_plugin_example_relcontext_content_type_admin_info',
+ 'content_types' => 'relcontext_content_type',
+ 'single' => TRUE,
+ 'render callback' => 'relcontext_content_type_render',
+ // Icon goes in the directory with the content type. Here, in plugins/content_types.
+ 'icon' => 'icon_example.png',
+ 'description' => t('Relcontext content type - works with relcontext context.'),
+ 'required context' => new ctools_context_required(t('Relcontext'), 'relcontext'),
+ 'category' => array(t('CTools Examples'), -9),
+ 'edit form' => 'relcontext_edit_form',
+
+ // This example does not provide 'admin info', which would populate the
+ // panels builder page preview.
+);
+
+/**
+ * Run-time rendering of the body of the block.
+ *
+ * @param $subtype
+ * @param $conf
+ * Configuration as done at admin time
+ * @param $args
+ * @param $context
+ * Context - in this case we don't have any
+ *
+ * @return
+ * An object with at least title and content members
+ */
+function relcontext_content_type_render($subtype, $conf, $args, $context) {
+ $data = $context->data;
+ $block = new stdClass();
+
+ // Don't forget to check this data if it's untrusted.
+ // The title actually used in rendering.
+ $block->title = "Relcontext content type";
+ $block->content = t("
+ This is a block of data created by the Relcontent content type.
+ Data in the block may be assembled from static text (like this) or from the
+ content type settings form (\$conf) for the content type, or from the context
+ that is passed in.
+ In our case, the configuration form (\$conf) has just one field, 'config_item_1;
+ and it's configured with:
+ ");
+ if (!empty($conf)) {
+ $block->content .= '' . var_export($conf['config_item_1'], TRUE) . '
';
+ }
+ if (!empty($context)) {
+ $block->content .= ' The args ($args) were ' .
+ var_export($args, TRUE) . '
';
+ }
+ $block->content .= ' And the relcontext context ($context->data->description)
+ (which was created from a
+ simplecontext context) was ' .
+ print_r($context->data->description, TRUE) . '
';
+ return $block;
+}
+
+/**
+ * 'Edit' callback for the content type.
+ * This example just returns a form.
+ */
+function relcontext_edit_form($form, &$form_state) {
+ $conf = $form_state['conf'];
+
+ $form['config_item_1'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Config Item 1 (relcontext)'),
+ '#size' => 50,
+ '#description' => t('Setting for relcontext.'),
+ '#default_value' => !empty($conf['config_item_1']) ? $conf['config_item_1'] : '',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ return $form;
+}
+
+function relcontext_edit_form_submit($form, &$form_state) {
+ foreach (element_children($form) as $key) {
+ if (!empty($form_state['values'][$key])) {
+ $form_state['conf'][$key] = $form_state['values'][$key];
+ }
+ }
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/simplecontext_content_type.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/simplecontext_content_type.inc
new file mode 100644
index 0000000..e34fa18
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/content_types/simplecontext_content_type.inc
@@ -0,0 +1,126 @@
+ t('Simplecontext content type'),
+ 'content_types' => 'simplecontext_content_type',
+ // 'single' means not to be subtyped.
+ 'single' => TRUE,
+ // Name of a function which will render the block.
+ 'render callback' => 'simplecontext_content_type_render',
+
+ // Icon goes in the directory with the content type.
+ 'icon' => 'icon_example.png',
+ 'description' => t('Simplecontext content type - works with a simplecontext context.'),
+ 'required context' => new ctools_context_required(t('Simplecontext'), 'simplecontext'),
+ 'edit form' => 'simplecontext_content_type_edit_form',
+ 'admin title' => 'ctools_plugin_example_simplecontext_content_type_admin_title',
+
+ // Presents a block which is used in the preview of the data.
+ // Pn Panels this is the preview pane shown on the panels building page.
+ 'admin info' => 'ctools_plugin_example_simplecontext_content_type_admin_info',
+ 'category' => array(t('CTools Examples'), -9),
+);
+
+function ctools_plugin_example_simplecontext_content_type_admin_title($subtype, $conf, $context = NULL) {
+ $output = t('Simplecontext');
+ if ($conf['override_title'] && !empty($conf['override_title_text'])) {
+ $output = filter_xss_admin($conf['override_title_text']);
+ }
+ return $output;
+}
+
+/**
+ * Callback to provide administrative info (the preview in panels when building
+ * a panel).
+ *
+ * In this case we'll render the content with a dummy argument and
+ * a dummy context.
+ */
+function ctools_plugin_example_simplecontext_content_type_admin_info($subtype, $conf, $context = NULL) {
+ $context = new stdClass();
+ $context->data = new stdClass();
+ $context->data->description = t("no real context");
+ $block = simplecontext_content_type_render($subtype, $conf, array("Example"), $context);
+ return $block;
+}
+
+/**
+ * Run-time rendering of the body of the block (content type)
+ *
+ * @param $subtype
+ * @param $conf
+ * Configuration as done at admin time
+ * @param $args
+ * @param $context
+ * Context - in this case we don't have any
+ *
+ * @return
+ * An object with at least title and content members
+ */
+function simplecontext_content_type_render($subtype, $conf, $args, $context) {
+ $data = $context->data;
+ $block = new stdClass();
+
+ // Don't forget to check this data if it's untrusted.
+ // The title actually used in rendering.
+ $block->title = "Simplecontext content type";
+ $block->content = t("
+ This is a block of data created by the Simplecontext content type.
+ Data in the block may be assembled from static text (like this) or from the
+ content type settings form (\$conf) for the content type, or from the context
+ that is passed in.
+ In our case, the configuration form (\$conf) has just one field, 'config_item_1;
+ and it's configured with:
+ ");
+ if (!empty($conf)) {
+ $block->content .= '' . print_r(filter_xss_admin($conf['config_item_1']), TRUE) . '
';
+ }
+ if (!empty($context)) {
+ $block->content .= ' The args ($args) were ' .
+ var_export($args, TRUE) . '
';
+ }
+ $block->content .= ' And the simplecontext context ($context->data->description) was ' .
+ print_r($context->data->description, TRUE) . '
';
+ return $block;
+}
+
+/**
+ * 'Edit' callback for the content type.
+ * This example just returns a form.
+ */
+function simplecontext_content_type_edit_form($form, &$form_state) {
+ $conf = $form_state['conf'];
+ $form['config_item_1'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Config Item 1 for simplecontext content type'),
+ '#size' => 50,
+ '#description' => t('The stuff for item 1.'),
+ '#default_value' => !empty($conf['config_item_1']) ? $conf['config_item_1'] : '',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ return $form;
+}
+
+function simplecontext_content_type_edit_form_submit($form, &$form_state) {
+ foreach (element_children($form) as $key) {
+ if (!empty($form_state['values'][$key])) {
+ $form_state['conf'][$key] = $form_state['values'][$key];
+ }
+ }
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/contexts/relcontext.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/contexts/relcontext.inc
new file mode 100644
index 0000000..61e25db
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/contexts/relcontext.inc
@@ -0,0 +1,82 @@
+ t("Relcontext"),
+ 'description' => t('A relcontext object.'),
+ // Function to create the relcontext.
+ 'context' => 'ctools_plugin_example_context_create_relcontext',
+ // Function that does the settings.
+ 'settings form' => 'relcontext_settings_form',
+ 'keyword' => 'relcontext',
+ 'context name' => 'relcontext',
+);
+
+/**
+ * Create a context, either from manual configuration (form) or from an argument on the URL.
+ *
+ * @param $empty
+ * If true, just return an empty context.
+ * @param $data
+ * If from settings form, an array as from a form. If from argument, a string.
+ * @param $conf
+ * TRUE if the $data is coming from admin configuration, FALSE if it's from a URL arg.
+ *
+ * @return
+ * a Context object.
+ */
+function ctools_plugin_example_context_create_relcontext($empty, $data = NULL, $conf = FALSE) {
+ $context = new ctools_context('relcontext');
+ $context->plugin = 'relcontext';
+ if ($empty) {
+ return $context;
+ }
+ if ($conf) {
+ if (!empty($data)) {
+ $context->data = new stdClass();
+ // For this simple item we'll just create our data by stripping non-alpha and
+ // adding 'sample_relcontext_setting' to it.
+ $context->data->description = 'relcontext_from__' . preg_replace('/[^a-z]/i', '', $data['sample_relcontext_setting']);
+ $context->data->description .= '_from_configuration_sample_simplecontext_setting';
+ $context->title = t("Relcontext context from simplecontext");
+ return $context;
+ }
+ }
+ else {
+ // $data is coming from an arg - it's just a string.
+ // This is used for keyword.
+ $context->title = "relcontext_" . $data->data->description;
+ $context->argument = $data->argument;
+ // Make up a bogus context.
+ $context->data = new stdClass();
+ // For this simple item we'll just create our data by stripping non-alpha and
+ // prepend 'relcontext_' and adding '_created_from_from_simplecontext' to it.
+ $context->data->description = 'relcontext_' . preg_replace('/[^a-z]/i', '', $data->data->description);
+ $context->data->description .= '_created_from_simplecontext';
+ return $context;
+ }
+}
+
+function relcontext_settings_form($conf, $external = FALSE) {
+ $form = array();
+
+ $form['sample_relcontext_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Relcontext setting'),
+ '#size' => 50,
+ '#description' => t('Just an example setting.'),
+ '#default_value' => !empty($conf['sample_relcontext_setting']) ? $conf['sample_relcontext_setting'] : '',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ return $form;
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/contexts/simplecontext.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/contexts/simplecontext.inc
new file mode 100644
index 0000000..0ee4658
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/contexts/simplecontext.inc
@@ -0,0 +1,132 @@
+ t("Simplecontext"),
+ 'description' => t('A single "simplecontext" context, or data element.'),
+// Func to create context.
+ 'context' => 'ctools_plugin_example_context_create_simplecontext',
+ 'context name' => 'simplecontext',
+ 'settings form' => 'simplecontext_settings_form',
+ 'keyword' => 'simplecontext',
+
+ // Provides a list of items which are exposed as keywords.
+ 'convert list' => 'simplecontext_convert_list',
+ // Convert keywords into data.
+ 'convert' => 'simplecontext_convert',
+
+ 'placeholder form' => array(
+ '#type' => 'textfield',
+ '#description' => t('Enter some data to represent this "simplecontext".'),
+ ),
+);
+
+/**
+ * Create a context, either from manual configuration or from an argument on the URL.
+ *
+ * @param $empty
+ * If true, just return an empty context.
+ * @param $data
+ * If from settings form, an array as from a form. If from argument, a string.
+ * @param $conf
+ * TRUE if the $data is coming from admin configuration, FALSE if it's from a URL arg.
+ *
+ * @return
+ * a Context object/
+ */
+function ctools_plugin_example_context_create_simplecontext($empty, $data = NULL, $conf = FALSE) {
+ $context = new ctools_context('simplecontext');
+ $context->plugin = 'simplecontext';
+
+ if ($empty) {
+ return $context;
+ }
+
+ if ($conf) {
+ if (!empty($data)) {
+ $context->data = new stdClass();
+ // For this simple item we'll just create our data by stripping non-alpha and
+ // adding '_from_configuration_item_1' to it.
+ $context->data->item1 = t("Item1");
+ $context->data->item2 = t("Item2");
+ $context->data->description = preg_replace('/[^a-z]/i', '', $data['sample_simplecontext_setting']);
+ $context->data->description .= '_from_configuration_sample_simplecontext_setting';
+ $context->title = t("Simplecontext context from config");
+ return $context;
+ }
+ }
+ else {
+ // $data is coming from an arg - it's just a string.
+ // This is used for keyword.
+ $context->title = $data;
+ $context->argument = $data;
+ // Make up a bogus context.
+ $context->data = new stdClass();
+ $context->data->item1 = t("Item1");
+ $context->data->item2 = t("Item2");
+
+ // For this simple item we'll just create our data by stripping non-alpha and
+ // adding '_from_simplecontext_argument' to it.
+ $context->data->description = preg_replace('/[^a-z]/i', '', $data);
+ $context->data->description .= '_from_simplecontext_argument';
+ $context->arg_length = strlen($context->argument);
+ return $context;
+ }
+}
+
+function simplecontext_settings_form($conf, $external = FALSE) {
+ if (empty($conf)) {
+ $conf = array(
+ 'sample_simplecontext_setting' => 'default simplecontext setting',
+ );
+ }
+ $form = array();
+ $form['sample_simplecontext_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Setting for simplecontext'),
+ '#size' => 50,
+ '#description' => t('An example setting that could be used to configure a context'),
+ '#default_value' => $conf['sample_simplecontext_setting'],
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ return $form;
+}
+
+/**
+ * Provide a list of sub-keywords.
+ *
+ * This is used to provide keywords from the context for use in a content type,
+ * pane, etc.
+ */
+function simplecontext_convert_list() {
+ return array(
+ 'item1' => t('Item1'),
+ 'item2' => t('Item2'),
+ 'description' => t('Description'),
+ );
+}
+
+/**
+ * Convert a context into a string to be used as a keyword by content types, etc.
+ */
+function simplecontext_convert($context, $type) {
+ switch ($type) {
+ case 'item1':
+ return $context->data->item1;
+
+ case 'item2':
+ return $context->data->item2;
+
+ case 'description':
+ return $context->data->description;
+ }
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/panels.pages.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/panels.pages.inc
new file mode 100644
index 0000000..25422cf
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/panels.pages.inc
@@ -0,0 +1,213 @@
+pid = 'new';
+ $page->did = 'new';
+ $page->name = 'ctools_plugin_example_demo_panel';
+ $page->title = 'Panels Plugin Example Demo Panel';
+ $page->access = array();
+ $page->path = 'demo_panel';
+ $page->load_flags = 1;
+ $page->css_id = '';
+ $page->arguments = array(
+ 0 =>
+ array(
+ 'name' => 'simplecontext_arg',
+ 'id' => 1,
+ 'default' => '404',
+ 'title' => '',
+ 'identifier' => 'Simplecontext arg',
+ 'keyword' => 'simplecontext',
+ ),
+ );
+ $page->relationships = array(
+ 0 =>
+ array(
+ 'context' => 'argument_simplecontext_arg_1',
+ 'name' => 'relcontext_from_simplecontext',
+ 'id' => 1,
+ 'identifier' => 'Relcontext from Simplecontext',
+ 'keyword' => 'relcontext',
+ ),
+ );
+ $page->no_blocks = '0';
+ $page->switcher_options = array();
+ $page->menu = '0';
+ $page->contexts = array();
+ $display = new ctools_display();
+ $display->did = 'new';
+ $display->layout = 'threecol_33_34_33_stacked';
+ $display->layout_settings = array();
+ $display->panel_settings = array();
+ $display->content = array();
+ $display->panels = array();
+ $pane = new stdClass();
+ $pane->pid = 'new-1';
+ $pane->panel = 'left';
+ $pane->type = 'custom';
+ $pane->shown = '1';
+ $pane->subtype = 'custom';
+ $pane->access = array();
+ $pane->configuration = array(
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'title' => '"No Context Item"',
+ 'body' => 'The "no context item" content type is here to demonstrate that you can create a content_type that does not require a context. This is probably the same as just creating a custom php block on the fly, and might serve the same purpose.',
+ 'format' => '1',
+ );
+ $pane->cache = array();
+ $display->content['new-1'] = $pane;
+ $display->panels['left'][0] = 'new-1';
+ $pane = new stdClass();
+ $pane->pid = 'new-2';
+ $pane->panel = 'left';
+ $pane->type = 'no_context_item';
+ $pane->shown = '1';
+ $pane->subtype = 'description';
+ $pane->access = array();
+ $pane->configuration = array(
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'item1' => 'one',
+ 'item2' => 'two',
+ 'item3' => 'three',
+ );
+ $pane->cache = array();
+ $display->content['new-2'] = $pane;
+ $display->panels['left'][1] = 'new-2';
+ $pane = new stdClass();
+ $pane->pid = 'new-3';
+ $pane->panel = 'middle';
+ $pane->type = 'custom';
+ $pane->shown = '1';
+ $pane->subtype = 'custom';
+ $pane->access = array();
+ $pane->configuration = array(
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'title' => 'Simplecontext',
+ 'body' => 'The "Simplecontext" content and content type demonstrate a very basic context and how to display it.
+
+ Simplecontext includes configuration, so it can get info from the config. It can also get its information to run from a simplecontext context, generated either from an arg to the panels page or via explicitly adding a context to the page.',
+ 'format' => '1',
+ );
+ $pane->cache = array();
+ $display->content['new-3'] = $pane;
+ $display->panels['middle'][0] = 'new-3';
+ $pane = new stdClass();
+ $pane->pid = 'new-4';
+ $pane->panel = 'middle';
+ $pane->type = 'simplecontext_item';
+ $pane->shown = '1';
+ $pane->subtype = 'description';
+ $pane->access = array(
+ 0 => '2',
+ 1 => '4',
+ );
+ $pane->configuration = array(
+ 'context' => 'argument_simplecontext_arg_1',
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'config_item_1' => 'simplecontext called from arg',
+ );
+ $pane->cache = array();
+ $display->content['new-4'] = $pane;
+ $display->panels['middle'][1] = 'new-4';
+ $pane = new stdClass();
+ $pane->pid = 'new-5';
+ $pane->panel = 'right';
+ $pane->type = 'custom';
+ $pane->shown = '1';
+ $pane->subtype = 'custom';
+ $pane->access = array();
+ $pane->configuration = array(
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'title' => 'Relcontext',
+ 'body' => 'The relcontext content_type gets its data from a relcontext, which is an example of a relationship. This panel should be run with an argument like "/xxx", which allows the simplecontext to get its context, and then the relcontext is configured in this panel to get (create) its data from the simplecontext.',
+ 'format' => '1',
+ );
+ $pane->cache = array();
+ $display->content['new-5'] = $pane;
+ $display->panels['right'][0] = 'new-5';
+ $pane = new stdClass();
+ $pane->pid = 'new-6';
+ $pane->panel = 'right';
+ $pane->type = 'relcontext_item';
+ $pane->shown = '1';
+ $pane->subtype = 'description';
+ $pane->access = array();
+ $pane->configuration = array(
+ 'context' => 'relationship_relcontext_from_simplecontext_1',
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'config_item_1' => 'default1',
+ );
+ $pane->cache = array();
+ $display->content['new-6'] = $pane;
+ $display->panels['right'][1] = 'new-6';
+ $pane = new stdClass();
+ $pane->pid = 'new-7';
+ $pane->panel = 'top';
+ $pane->type = 'custom_php';
+ $pane->shown = '1';
+ $pane->subtype = 'custom_php';
+ $pane->access = array();
+ $pane->configuration = array(
+ 'style' => 'default',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ 'css_id' => '',
+ 'css_class' => '',
+ 'title' => '',
+ 'body' => '$arg = arg(1);
+ $arg0 = arg(0);
+ if (!$arg) {
+ $block->content = <<This page is intended to run with an arg and you don\'t have one.
+
+ Without an arg, the page doesn\'t have any context.
+ Please try something like "/$arg0/xxx"
+END;
+
+ $block->title = "This is intended to run with an argument";
+ } else {
+ $block->content = "The arg for this page is \'$arg\'";
+ }',
+ );
+ $pane->cache = array();
+ $display->content['new-7'] = $pane;
+ $display->panels['top'][0] = 'new-7';
+ $page->display = $display;
+ $page->displays = array();
+ $pages['ctools_plugin_example'] = $page;
+
+ return $pages;
+}
diff --git a/sites/all/modules/ctools/ctools_plugin_example/plugins/relationships/relcontext_from_simplecontext.inc b/sites/all/modules/ctools/ctools_plugin_example/plugins/relationships/relcontext_from_simplecontext.inc
new file mode 100644
index 0000000..087d5ec
--- /dev/null
+++ b/sites/all/modules/ctools/ctools_plugin_example/plugins/relationships/relcontext_from_simplecontext.inc
@@ -0,0 +1,46 @@
+ t("Relcontext from simplecontext"),
+ 'keyword' => 'relcontext',
+ 'description' => t('Adds a relcontext from existing simplecontext.'),
+ 'required context' => new ctools_context_required(t('Simplecontext'), 'simplecontext'),
+ 'context' => 'ctools_relcontext_from_simplecontext_context',
+ 'settings form' => 'ctools_relcontext_from_simplecontext_settings_form',
+);
+
+/**
+ * Return a new context based on an existing context.
+ */
+function ctools_relcontext_from_simplecontext_context($context = NULL, $conf) {
+ // If unset it wants a generic, unfilled context, which is just NULL.
+ if (empty($context->data)) {
+ return ctools_context_create_empty('relcontext', NULL);
+ }
+
+ // You should do error-checking here.
+ // Create the new context from some element of the parent context.
+ // In this case, we'll pass in the whole context so it can be used to
+ // create the relcontext.
+ return ctools_context_create('relcontext', $context);
+}
+
+/**
+ * Settings form for the relationship.
+ */
+function ctools_relcontext_from_simplecontext_settings_form($conf) {
+ // We won't configure it in this case.
+ return array();
+}
diff --git a/sites/all/modules/ctools/drush/ctools.drush.inc b/sites/all/modules/ctools/drush/ctools.drush.inc
new file mode 100644
index 0000000..3677b17
--- /dev/null
+++ b/sites/all/modules/ctools/drush/ctools.drush.inc
@@ -0,0 +1,1036 @@
+ array('ctex'),
+ 'callback' => 'ctools_drush_export',
+ 'description' => 'Export multiple CTools exportable objects directly to code.',
+ 'arguments' => array(
+ 'module' => 'Name of your module.',
+ ),
+ 'options' => array(
+ 'subdir' => 'The name of the sub directory to create the module in. Defaults to ctools_export which will be placed into sites/all/modules.',
+ 'remove' => 'Remove existing files before writing, except the .module file.',
+ 'filter' => 'Filter the list of exportables by status. Available options are enabled, disabled, overridden, database, code and all. Defaults to enabled.',
+ 'tables' => 'Comma separated list of exportable table names to filter by.',
+ ),
+ 'examples' => array(
+ 'drush ctex export_module' => 'Export CTools exportables to a module called "export_module".',
+ 'drush ctex export_module --subdir=exports' => 'Same as above, but into the sites/all/modules/exports directory.',
+ 'drush ctex export_module --subdir=exports --remove' => 'Same as above, but automatically removing all files, except for the .module file.',
+ 'drush ctex --filter="views_view"' => 'Filter export selection to the views_view table only.',
+ ),
+ );
+
+ $items['ctools-export-info'] = array(
+ 'aliases' => array('ctei'),
+ 'callback' => 'ctools_drush_export_info',
+ 'description' => 'Show available CTools exportable objects.',
+ 'arguments' => array(),
+ 'options' => array(
+ 'format' => 'Display exportables info in a different format such as print_r, json, export. The default is to show in a tabular format.',
+ 'tables-only' => 'Only show list of exportable types/table names and not available objects.',
+ 'filter' => 'Filter the list of exportables by status. Available options are enabled, disabled, overridden, database, and code.',
+ 'module' => $module_text,
+ ),
+ 'examples' => array(
+ 'drush ctools-export-info' => 'View export info on all exportables.',
+ 'drush ctools-export-info views_view variable' => 'View export info for views_view and variable exportable types only.',
+ 'drush ctei --filter=enabled' => 'Show all enabled exportables.',
+ 'drush ctei views_view --filter=disabled' => 'Show all enabled exportables.',
+ 'drush ctei views_view --module=node' => 'Show all exportables provided by/on behalf of the node module.',
+ ),
+ );
+
+ $items['ctools-export-view'] = array(
+ 'aliases' => array('ctev'),
+ 'callback' => 'ctools_drush_export_op_command',
+ 'description' => 'View CTools exportable object code output.',
+ 'arguments' => array(
+ 'table name' => 'Base table of the exportable you want to view.',
+ 'machine names' => 'Space separated list of exportables you want to view.',
+ ),
+ 'options' => array(
+ 'indent' => 'The string to use for indentation when displaying the exportable export code. Defaults to \'\'.',
+ 'no-colour' => 'Remove any colour formatting from export string output. Ideal if you are sending the output of this command to a file.',
+ 'module' => $module_text,
+ 'all' => $all_text,
+ ),
+ 'examples' => array(
+ 'drush ctools-export-view views_view' => 'View all views exportable objects.',
+ 'drush ctools-export-view views_view archive' => 'View default views archive view.',
+ ),
+ );
+
+ $items['ctools-export-revert'] = array(
+ 'aliases' => array('cter'),
+ 'callback' => 'ctools_drush_export_op_command',
+ 'description' => 'Revert CTools exportables from changes overridden in the database.',
+ 'arguments' => array(
+ 'table name' => 'Base table of the exportable you want to revert.',
+ 'machine names' => 'Space separated list of exportables you want to revert.',
+ ),
+ 'options' => array(
+ 'module' => $module_text,
+ 'all' => $all_text,
+ ),
+ 'examples' => array(
+ 'drush ctools-export-revert views_view' => 'Revert all overridden views exportable objects.',
+ 'drush ctools-export-revert views_view archive' => 'Revert overridden default views archive view.',
+ 'drush ctools-export-revert --all' => 'Revert all exportables on the system.',
+ ),
+ );
+
+ $items['ctools-export-enable'] = array(
+ 'aliases' => array('ctee'),
+ 'callback' => 'ctools_drush_export_op_command',
+ 'description' => 'Enable CTools exportables.',
+ 'arguments' => array(
+ 'table name' => 'Base table of the exportable you want to enable.',
+ 'machine names' => 'Space separated list of exportables you want to enable.',
+ ),
+ 'options' => array(
+ 'module' => $module_text,
+ 'all' => $all_text,
+ ),
+ 'examples' => array(
+ 'drush ctools-export-enable views_view' => 'Enable all overridden views exportable objects.',
+ 'drush ctools-export-enable views_view archive' => 'Enable overridden default views archive view.',
+ ),
+ );
+
+ $items['ctools-export-disable'] = array(
+ 'aliases' => array('cted'),
+ 'callback' => 'ctools_drush_export_op_command',
+ 'description' => 'Disable CTools exportables.',
+ 'arguments' => array(
+ 'table name' => 'Base table of the exportable you want to disable.',
+ 'machine names' => 'Space separated list of exportables you want to disable.',
+ ),
+ 'options' => array(
+ 'module' => $module_text,
+ 'all' => $all_text,
+ ),
+ 'examples' => array(
+ 'drush ctools-export-disable views_view' => 'Disable all overridden views exportable objects.',
+ 'drush ctools-export-disable views_view archive' => 'Disable overridden default views archive view.',
+ ),
+ );
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_drush_help().
+ */
+function ctools_drush_help($section) {
+ switch ($section) {
+ case 'meta:ctools:title':
+ return dt('CTools commands');
+
+ case 'meta:entity:summary':
+ return dt('CTools drush commands.');
+ }
+}
+
+/**
+ * Drush callback: export.
+ */
+function ctools_drush_export($module = 'foo') {
+ $error = FALSE;
+ if (preg_match('@[^a-z_]+@', $module)) {
+ $error = dt('The name of the module must contain only lowercase letters and underscores') . '.';
+ drush_log($error, 'error');
+ return;
+ }
+
+ // Filter by tables.
+ $tables = _ctools_drush_explode_options('tables');
+
+ // Check status.
+ $filter = drush_get_option('filter', FALSE);
+ if (empty($filter)) {
+ drush_set_option('filter', 'enabled');
+ }
+
+ // Selection.
+ $options = array('all' => dt('Export everything'), 'select' => dt('Make selection'));
+ $selection = drush_choice($options, dt('Select to proceed'));
+
+ if (!$selection) {
+ return;
+ }
+
+ // Present the selection screens.
+ if ($selection == 'select') {
+ $selections = _ctools_drush_selection_screen($tables);
+ }
+ else {
+ $info = _ctools_drush_export_info($tables, TRUE);
+ $selections = $info['exportables'];
+ }
+
+ // Subdirectory.
+ $dest_exists = FALSE;
+ $subdir = drush_get_option('subdir', 'ctools_export');
+ $dest = 'sites/all/modules/' . $subdir . '/' . $module;
+
+ // Overwriting files can be set with 'remove' argument.
+ $remove = drush_get_option('remove', FALSE);
+
+ // Check if folder exists.
+ if (file_exists($dest)) {
+ $dest_exists = TRUE;
+ if ($remove) {
+ if (drush_confirm(dt('All files except for the .info and .module files in !module will be removed. You can choose later if you want to overwrite these files as well. Are you sure you want to proceed ?', array('!module' => $module)))) {
+ $remove = TRUE;
+ drush_log(dt('Files will be removed'), 'success');
+ }
+ else {
+ drush_log(dt('Export aborted.'), 'success');
+ return;
+ }
+ }
+ }
+
+ // Remove files (except for the .module file) if the destination folder exists.
+ if ($remove && $dest_exists) {
+ _ctools_drush_file_delete($dest);
+ }
+
+ // Create new dir if needed.
+ if (!$dest_exists) {
+ if (!file_exists('sites/all/modules/' . $subdir)) {
+ drush_mkdir('sites/all/modules/' . $subdir);
+ }
+ }
+
+ // Create destination directory.
+ drush_mkdir($dest);
+
+ // Load bulk export module.
+ module_load_include('module', 'bulk_export');
+
+ // Create options and call Bulk export function.
+ // We create an array, because maybe in the future we can pass in more
+ // options to the export function (pre-selected modules and/or exportables).
+ $options = array(
+ 'name' => $module,
+ 'selections' => $selections,
+ );
+ $files = bulk_export_export(TRUE, $options);
+
+ $alter = array(
+ 'module' => $module,
+ 'files' => $files,
+ );
+ // Let other drush commands alter the files.
+ drush_command_invoke_all_ref('drush_ctex_files_alter', $alter);
+ $files = $alter['files'];
+
+ // Start writing.
+ if (is_array($files)) {
+ foreach ($files as $base_file => $data) {
+ $filename = $dest . '/' . $base_file;
+ // Extra check for .module file.
+ if ($base_file == ($module . '.module' || $module . '.info') && file_exists($filename)) {
+ if (!drush_confirm(dt('Do you want to overwrite !module_file', array('!module_file' => $base_file)))) {
+ drush_log(dt('Writing of !filename skipped. This is the code that was supposed to be written:', array('!filename' => $filename)), 'success');
+ drush_print('---------');
+ drush_print(shellColours::getColouredOutput("\n$data", 'light_green'));
+ drush_print('---------');
+ continue;
+ }
+ }
+ if (file_put_contents($filename, $data)) {
+ drush_log(dt('Succesfully written !filename', array('!filename' => $filename)), 'success');
+ }
+ else {
+ drush_log(dt('Error writing !filename', array('!filename' => $filename)), 'error');
+ }
+ }
+ }
+ else {
+ drush_log(dt('No files were found to be written.'), 'error');
+ }
+}
+
+/**
+ * Helper function to select the exportables. By default, all exportables
+ * will be selected, so it will be easier to deselect them.
+ *
+ * @param $tables
+ */
+function _ctools_drush_selection_screen(array $tables = array()) {
+ $selections = $build = array();
+ $files = system_rebuild_module_data();
+
+ $selection_number = 0;
+
+ $info = _ctools_drush_export_info($tables, TRUE);
+ $exportables = $info['exportables'];
+ $schemas = $info['schemas'];
+
+ $export_tables = array();
+
+ foreach (array_keys($exportables) as $table) {
+ natcasesort($exportables[$table]);
+ $export_tables[$table] = $files[$schemas[$table]['module']]->info['name'] . ' (' . $table . ')';
+ }
+
+ foreach ($export_tables as $table => $table_title) {
+ if (!empty($exportables[$table])) {
+ $table_count = count($exportables[$table]);
+ $selection_number += $table_count;
+ foreach ($exportables[$table] as $key => $title) {
+ $build[$table]['title'] = $table_title;
+ $build[$table]['items'][$key] = $title;
+ $build[$table]['count'] = $table_count;
+ $selections[$table][$key] = $key;
+ }
+ }
+ }
+
+ drush_print(dt('Number of exportables selected: !number', array('!number' => $selection_number)));
+ drush_print(dt('By default all exportables are selected. Select a table to deselect exportables. Select "cancel" to start writing the files.'));
+
+ // Let's go into a loop.
+ $return = FALSE;
+ while (!$return) {
+
+ // Present the tables choice.
+ $table_rows = array();
+ foreach ($build as $table => $info) {
+ $table_rows[$table] = $info['title'] . ' (' . $info['count'] . ')';
+ }
+ $table_choice = drush_choice($table_rows, dt('Select a table. Select cancel to start writing files.'));
+
+ // Bail out.
+ if (!$table_choice) {
+ drush_log(dt('Selection mode done, starting to write the files.'), 'notice');
+ $return = TRUE;
+ return $selections;
+ }
+
+ // Present the exportables choice, using the drush_choice_multiple.
+ $max = count($build[$table_choice]['items']);
+ $exportable_rows = array();
+ foreach ($build[$table_choice]['items'] as $key => $title) {
+ $exportable_rows[$key] = $title;
+ }
+ drush_print(dt('Exportables from !table', array('!table' => $build[$table_choice]['title'])));
+ $multi_select = drush_choice_multiple($exportable_rows, $selections[$table_choice], dt('Select exportables.'), '!value', '!value (selected)', 0, $max);
+
+ // Update selections.
+ if (is_array($multi_select)) {
+ $build[$table_choice]['count'] = count($multi_select);
+ $selections[$table_choice] = array();
+ foreach ($multi_select as $key) {
+ $selections[$table_choice][$key] = $key;
+ }
+ }
+ }
+}
+
+/**
+ * Delete files in a directory, keeping the .module and .info files.
+ *
+ * @param $path
+ * Path to directory in which to remove files.
+ */
+function _ctools_drush_file_delete($path) {
+ if (is_dir($path)) {
+ $files = new DirectoryIterator($path);
+ foreach ($files as $fileInfo) {
+ if (!$fileInfo->isDot() && !in_array($fileInfo->getExtension(), array('module', 'info'))) {
+ unlink($fileInfo->getPathname());
+ }
+ }
+ }
+}
+
+/**
+ * Drush callback: Export info.
+ *
+ * @params $table_names
+ * Each argument will be taken as a CTools exportable table name.
+ */
+function ctools_drush_export_info() {
+ // Collect array of table names from args.
+ $table_names = func_get_args();
+
+ // Get format option to allow for alternative output.
+ $format = drush_get_option('format', FALSE);
+ $tables_only = drush_get_option('tables-only', FALSE);
+ $filter = drush_get_option('filter', FALSE);
+ $export_module = drush_get_option('module', FALSE);
+
+ $load = (bool) $filter || $export_module;
+
+ // Get info on these tables, or all tables if none specified.
+ $info = _ctools_drush_export_info($table_names, $load);
+ $schemas = $info['schemas'];
+ $exportables = $info['exportables'];
+
+ if (empty($exportables)) {
+ drush_log(dt('There are no exportables available.'), 'warning');
+ return;
+ }
+
+ // Filter by export module.
+ if (is_string($export_module)) {
+ $exportables = _ctools_drush_export_module_filter($exportables, $export_module);
+ }
+
+ if (empty($exportables)) {
+ drush_log(dt('There are no exportables matching this criteria.'), 'notice');
+ return;
+ }
+
+ $exportable_counts = _ctools_drush_count_exportables($exportables);
+
+ // Only use array keys if --tables-only option is set.
+ if ($tables_only) {
+ $exportables = array_keys($exportables);
+ }
+
+ // Use format from --format option if it's present, and send to drush_format.
+ if ($format) {
+ // Create array with all exportable info and counts in one.
+ $output = array(
+ 'exportables' => $exportables,
+ 'count' => $exportable_counts,
+ );
+ drush_print(drush_format($output, NULL, $format));
+ }
+ // Build a tabular output as default.
+ else {
+ $header = $tables_only ? array() : array(dt('Module'), dt('Base table'), dt('Exportables'));
+ $rows = array();
+ foreach ($exportables as $table => $info) {
+ if (is_array($info)) {
+ $row = array(
+ $schemas[$table]['module'],
+ $table,
+ // Machine name is better for this?
+ shellColours::getColouredOutput(implode("\n", array_keys($info)), 'light_green') . "\n",
+ );
+ $rows[] = $row;
+ }
+ else {
+ $rows[] = array($info);
+ }
+ }
+ if (!empty($rows)) {
+ drush_print("\n");
+ array_unshift($rows, $header);
+ drush_print_table($rows, TRUE, array(20, 20));
+ drush_print(dt('Total exportables found: !total', array('!total' => $exportable_counts['total'])));
+ foreach ($exportable_counts['exportables'] as $table_name => $count) {
+ drush_print(dt('!table_name (!count)', array('!table_name' => $table_name, '!count' => $count)), 2);
+ }
+ drush_print("\n");
+ }
+ }
+}
+
+/**
+ * Drush callback: Acts as the hub for all op commands to keep
+ * all arg handling etc in one place.
+ */
+function ctools_drush_export_op_command() {
+ // Get all info for the current drush command.
+ $command = drush_get_command();
+ $op = '';
+
+ switch ($command['command']) {
+ case 'ctools-export-view':
+ $op = 'view';
+ break;
+
+ case 'ctools-export-revert':
+ // Revert is same as deleting. As any objects in the db are deleted.
+ $op = 'delete';
+ break;
+
+ case 'ctools-export-enable':
+ $op = 'enable';
+ break;
+
+ case 'ctools-export-disable':
+ $op = 'disable';
+ break;
+ }
+
+ if (!$op) {
+ return;
+ }
+
+ if (drush_get_option('all', FALSE)) {
+ $info = _ctools_drush_export_info(array(), TRUE);
+ $exportable_info = $info['exportables'];
+
+ $all = drush_confirm(dt('Are you sure you would like to !op all exportables on the system?',
+ array('!op' => _ctools_drush_export_op_alias($op))));
+
+ if ($all && $exportable_info) {
+ foreach ($exportable_info as $table => $exportables) {
+ if (!empty($exportables)) {
+ ctools_drush_export_op($op, $table, $exportables);
+ }
+ }
+ }
+ }
+ else {
+ $args = func_get_args();
+ // Table name should always be first arg...
+ $table_name = array_shift($args);
+ // Any additional args are assumed to be exportable names.
+ $object_names = $args;
+
+ // Return any exportables based on table name, object names, options.
+ $exportables = _ctools_drush_export_op_command_logic($op, $table_name, $object_names);
+
+ if ($exportables) {
+ ctools_drush_export_op($op, $table_name, $exportables);
+ }
+ }
+}
+
+/**
+ * Iterate through exportable object names, load them, and pass each
+ * object to the correct op function.
+ *
+ * @param $op
+ * @param $table_name
+ * @param $exportables
+ */
+function ctools_drush_export_op($op = '', $table_name = '', $exportables = NULL) {
+ $objects = ctools_export_crud_load_multiple($table_name, array_keys($exportables));
+
+ $function = '_ctools_drush_export_' . $op;
+ if (function_exists($function)) {
+ foreach ($objects as $object) {
+ $function($table_name, $object);
+ }
+ }
+ else {
+ drush_log(dt('CTools CRUD function !function doesn\'t exist.',
+ array('!function' => $function)), 'error');
+ }
+}
+
+/**
+ * Helper function to abstract logic for selecting exportable types/objects
+ * from individual commands as they will all share this same error
+ * handling/arguments for returning list of exportables.
+ *
+ * @param $table_name
+ * @param $object_names
+ *
+ * @return
+ * Array of exportable objects (filtered if necessary, by name etc..) or FALSE if not.
+ */
+function _ctools_drush_export_op_command_logic($op = '', $table_name = NULL, array $object_names = array()) {
+ if (!$table_name) {
+ drush_log(dt('Exportable table name empty. Use the --all command if you want to perform this operation on all tables.'), 'error');
+ return FALSE;
+ }
+
+ // Get export info based on table name.
+ $info = _ctools_drush_export_info(array($table_name), TRUE);
+
+ if (!isset($info['exportables'][$table_name])) {
+ drush_log(dt('Exportable table name not found.'), 'error');
+ return FALSE;
+ }
+
+ $exportables = &$info['exportables'];
+
+ if (empty($object_names)) {
+ $all = drush_confirm(dt('No object names entered. Would you like to try and !op all exportables of type !type',
+ array('!op' => _ctools_drush_export_op_alias($op), '!type' => $table_name)));
+ if (!$all) {
+ drush_log(dt('Command cancelled'), 'success');
+ return FALSE;
+ }
+ }
+ else {
+ // Iterate through object names and check they exist in exportables array.
+ // Log error and unset them if they don't.
+ foreach ($object_names as $object_name) {
+ if (!isset($exportables[$table_name][$object_name])) {
+ drush_log(dt('Invalid exportable: !exportable', array('!exportable' => $object_name)), 'error');
+ unset($object_names[$table_name][$object_name]);
+ }
+ }
+ // Iterate through exportables to get just a list of selected ones.
+ foreach (array_keys($exportables[$table_name]) as $exportable) {
+ if (!in_array($exportable, $object_names)) {
+ unset($exportables[$table_name][$exportable]);
+ }
+ }
+ }
+
+ $export_module = drush_get_option('module', FALSE);
+
+ if (is_string($export_module)) {
+ $exportables = _ctools_drush_export_module_filter($exportables, $export_module);
+ }
+
+ return $exportables[$table_name];
+}
+
+/**
+ * Return array of CTools exportable info based on available tables returned from
+ * ctools_export_get_schemas().
+ *
+ * @param $table_names
+ * Array of table names to return.
+ * @param $load
+ * (bool) should ctools exportable objects be loaded for each type.
+ * The default behaviour will load just a list of exportable names.
+ *
+ * @return
+ * Nested arrays of available exportables, keyed by table name.
+ */
+function _ctools_drush_export_info(array $table_names = array(), $load = FALSE) {
+ ctools_include('export');
+ // Get available schemas that declare exports.
+ $schemas = ctools_export_get_schemas(TRUE);
+ $exportables = array();
+
+ if (!empty($schemas)) {
+ // Remove types we don't want, if any.
+ if (!empty($table_names)) {
+ foreach (array_keys($schemas) as $table_name) {
+ if (!in_array($table_name, $table_names)) {
+ unset($schemas[$table_name]);
+ }
+ }
+ }
+ // Load array of available exportables for each schema.
+ foreach ($schemas as $table_name => $schema) {
+ // Load all objects.
+ if ($load) {
+ $exportables[$table_name] = ctools_export_crud_load_all($table_name);
+ }
+ // Get a list of exportable names.
+ else {
+ if (!empty($schema['export']['list callback']) && function_exists($schema['export']['list callback'])) {
+ $exportables[$table_name] = $schema['export']['list callback']();
+ }
+ else {
+ $exportables[$table_name] = ctools_export_default_list($table_name, $schema);
+ }
+ }
+ }
+ }
+
+ if ($load) {
+ $filter = drush_get_option('filter', FALSE);
+ $exportables = _ctools_drush_filter_exportables($exportables, $filter);
+ }
+
+ return array('exportables' => $exportables, 'schemas' => $schemas);
+}
+
+/**
+ * View a single object.
+ *
+ * @param $table_name
+ * @param $object
+ */
+function _ctools_drush_export_view($table_name, $object) {
+ $indent = drush_get_option('indent', '');
+ $no_colour = drush_get_option('no-colour', FALSE);
+ $export = ctools_export_crud_export($table_name, $object, $indent);
+ if ($no_colour) {
+ drush_print("\n$export");
+ }
+ else {
+ drush_print(shellColours::getColouredOutput("\n$export", 'light_green'));
+ }
+}
+
+/**
+ * Revert a single object.
+ *
+ * @param $table_name
+ * @param $object
+ */
+function _ctools_drush_export_delete($table_name, $object) {
+ $name = _ctools_drush_get_export_name($table_name, $object);
+
+ if (_ctools_drush_object_is_overridden($object)) {
+ // Remove from db.
+ ctools_export_crud_delete($table_name, $object);
+ drush_log("Reverted object: $name", 'success');
+ }
+ else {
+ drush_log("Nothing to revert for: $name", 'notice');
+ }
+}
+
+/**
+ * Enable a single object.
+ *
+ * @param $table_name
+ * @param $object
+ */
+function _ctools_drush_export_enable($table_name, $object) {
+ $name = _ctools_drush_get_export_name($table_name, $object);
+
+ if (_ctools_drush_object_is_disabled($object)) {
+
+ // Enable object.
+ ctools_export_crud_enable($table_name, $object);
+ drush_log("Enabled object: $name", 'success');
+ }
+ else {
+ drush_log("$name is already Enabled", 'notice');
+ }
+}
+
+/**
+ * Disable a single object.
+ *
+ * @param $table_name
+ * @param $object
+ */
+function _ctools_drush_export_disable($table_name, $object) {
+ $name = _ctools_drush_get_export_name($table_name, $object);
+
+ if (!_ctools_drush_object_is_disabled($object)) {
+ // Disable object.
+ ctools_export_crud_disable($table_name, $object);
+ drush_log("Disabled object: $name", 'success');
+ }
+ else {
+ drush_log("$name is already disabled", 'notice');
+ }
+}
+
+/**
+ * Filter a nested array of exportables by export module.
+ *
+ * @param array $exportables
+ * Passed by reference. A nested array of exportables, keyed by table name.
+ * @param string $export_module
+ * The name of the export module providing the exportable.
+ */
+function _ctools_drush_export_module_filter($exportables, $export_module) {
+ $module_list = module_list();
+
+ if (!isset($module_list[$export_module])) {
+ drush_log(dt('Invalid export module: !export_module', array('!export_module' => $export_module)), 'error');
+ }
+
+ foreach ($exportables as $table => $objects) {
+ foreach ($objects as $key => $object) {
+ if (empty($object->export_module) || ($object->export_module !== $export_module)) {
+ unset($exportables[$table][$key]);
+ }
+ }
+ }
+
+ return array_filter($exportables);
+}
+
+/**
+ * Gets the key for an exportable type.
+ *
+ * @param $table_name
+ * The exportable table name.
+ * @param $object
+ * The exportable object.
+ *
+ * @return string
+ * The key defined in the export schema data.
+ */
+function _ctools_drush_get_export_name($table_name, $object) {
+ $info = _ctools_drush_export_info(array($table_name));
+ $key = $info['schemas'][$table_name]['export']['key'];
+ return $object->{$key};
+}
+
+/**
+ * Determine if an object is disabled.
+ *
+ * @param $object
+ * Loaded CTools exportable object.
+ *
+ * @return TRUE or FALSE
+ */
+function _ctools_drush_object_is_disabled($object) {
+ return (isset($object->disabled) && ($object->disabled == TRUE)) ? TRUE : FALSE;
+}
+
+/**
+ * Determine if an object is enabled.
+ *
+ * @see _ctools_drush_object_is_disabled()
+ */
+function _ctools_drush_object_is_enabled($object) {
+ return (empty($object->disabled)) ? TRUE : FALSE;
+}
+
+/**
+ * Determine if an object is overridden.
+ */
+function _ctools_drush_object_is_overridden($object) {
+ $status = EXPORT_IN_CODE + EXPORT_IN_DATABASE;
+ return ($object->export_type == $status) ? TRUE : FALSE;
+}
+
+/**
+ * Determine if an object is not overridden.
+ */
+function _ctools_drush_object_is_not_overridden($object) {
+ $status = EXPORT_IN_CODE + EXPORT_IN_DATABASE;
+ return ($object->export_type == $status) ? FALSE : TRUE;
+}
+
+/**
+ * Determine if an object is only in the db.
+ */
+function _ctools_drush_object_is_db_only($object) {
+ return ($object->export_type == EXPORT_IN_DATABASE) ? TRUE : FALSE;
+}
+
+/**
+ * Determine if an object is not in the db.
+ */
+function _ctools_drush_object_is_not_db_only($object) {
+ return ($object->export_type == EXPORT_IN_DATABASE) ? FALSE : TRUE;
+}
+
+/**
+ * Determine if an object is a code only default.
+ */
+function _ctools_drush_object_is_code_only($object) {
+ return ($object->export_type == EXPORT_IN_CODE) ? TRUE : FALSE;
+}
+
+/**
+ * Determine if an object is not a code only default.
+ */
+function _ctools_drush_object_is_not_code_only($object) {
+ return ($object->export_type == EXPORT_IN_CODE) ? FALSE : TRUE;
+}
+
+/**
+ * Return an array of count information based on exportables array.
+ *
+ * @param $exportables
+ * Array of exportables to count.
+ *
+ * @return
+ * Array of count data containing the following:
+ * 'total' - A total count of all exportables.
+ * 'exportables' - An array of exportable counts per table.
+ */
+function _ctools_drush_count_exportables($exportables) {
+ $count = array('exportables' => array());
+
+ foreach ($exportables as $table => $objects) {
+ // Add the object count for each table.
+ $count['exportables'][$table] = count($objects);
+ }
+
+ // Once all tables have been counted, total these up.
+ $count['total'] = array_sum($count['exportables']);
+
+ return $count;
+}
+
+/**
+ * Filters a collection of exportables based on filters.
+ *
+ * @param $exportables
+ * @param $filter
+ */
+function _ctools_drush_filter_exportables($exportables, $filter) {
+ $eval = FALSE;
+
+ if (is_string($filter)) {
+ switch ($filter) {
+ // Show enabled exportables only.
+ case 'enabled':
+ $eval = '_ctools_drush_object_is_disabled';
+ break;
+
+ // Show disabled exportables only.
+ case 'disabled':
+ $eval = '_ctools_drush_object_is_enabled';
+ break;
+
+ // Show overridden exportables only.
+ case 'overridden':
+ $eval = '_ctools_drush_object_is_not_overridden';
+ break;
+
+ // Show database only exportables.
+ case 'database':
+ $eval = '_ctools_drush_object_is_not_db_only';
+ break;
+
+ // Show code only exportables.
+ case 'code':
+ $eval = '_ctools_drush_object_is_not_code_only';
+ break;
+
+ // Do nothing.
+ case 'all':
+ break;
+
+ default:
+ drush_log(dt('Invalid filter option. Available options are: enabled, disabled, overridden, database, and code.'), 'error');
+ return;
+ }
+
+ if ($eval) {
+ foreach ($exportables as $table => $objects) {
+ foreach ($objects as $key => $object) {
+ if ($eval($object)) {
+ unset($exportables[$table][$key]);
+ }
+ }
+ }
+ }
+ }
+
+ return array_filter($exportables);
+}
+
+/**
+ * Return an alias for an op, that will be used to show as output.
+ * For now, this is mainly necessary for delete => revert alias.
+ *
+ * @param $op
+ * The op name. Such as 'enable', 'disable', or 'delete'.
+ *
+ * @return
+ * The matched alias value or the original $op passed in if not found.
+ */
+function _ctools_drush_export_op_alias($op) {
+ $aliases = array(
+ 'delete' => 'revert',
+ );
+
+ if (isset($aliases[$op])) {
+ return $aliases[$op];
+ }
+
+ return $op;
+}
+
+/**
+ * Convert the drush options from a csv list into an array.
+ *
+ * @param $drush_option
+ * The drush option name to invoke.
+ *
+ * @return
+ * Exploded array of options.
+ */
+function _ctools_drush_explode_options($drush_option) {
+ $options = drush_get_option($drush_option, array());
+ if (!empty($options)) {
+ $options = explode(',', $options);
+ return array_map('trim', $options);
+ }
+
+ return $options;
+}
+
+/**
+ * Class to deal with wrapping output strings with
+ * colour formatting for the shell.
+ */
+class shellColours {
+
+ private static $foreground_colours = array(
+ 'black' => '0;30',
+ 'dark_gray' => '1;30',
+ 'blue' => '0;34',
+ 'light_blue' => '1;34',
+ 'green' => '0;32',
+ 'light_green' => '1;32',
+ 'cyan' => '0;36',
+ 'light_cyan' => '1;36',
+ 'red' => '0;31',
+ 'light_red' => '1;31',
+ 'purple' => '0;35',
+ 'light_purple' => '1;35',
+ 'brown' => '0;33',
+ 'yellow' => '1;33',
+ 'light_gray' => '0;37',
+ 'white' => '1;37',
+ );
+
+ private static $background_colours = array(
+ 'black' => '40',
+ 'red' => '41',
+ 'green' => '42',
+ 'yellow' => '43',
+ 'blue' => '44',
+ 'magenta' => '45',
+ 'cyan' => '46',
+ 'light_gray' => '47',
+ );
+
+ /**
+ * shellColours constructor.
+ */
+ private function __construct() {}
+
+ /**
+ * Returns coloured string.
+ */
+ public static function getColouredOutput($string, $foreground_colour = NULL, $background_colour = NULL) {
+ $coloured_string = "";
+
+ // Check if given foreground colour found.
+ if ($foreground_colour) {
+ $coloured_string .= "\033[" . self::$foreground_colours[$foreground_colour] . "m";
+ }
+ // Check if given background colour found.
+ if ($background_colour) {
+ $coloured_string .= "\033[" . self::$background_colours[$background_colour] . "m";
+ }
+
+ // Add string and end colouring.
+ $coloured_string .= $string . "\033[0m";
+
+ return $coloured_string;
+ }
+
+ /**
+ * Returns all foreground colour names.
+ */
+ public static function getForegroundColours() {
+ return array_keys(self::$foreground_colours);
+ }
+
+ /**
+ * Returns all background colour names.
+ */
+ public static function getBackgroundColours() {
+ return array_keys(self::$background_colours);
+ }
+
+} // shellColours
diff --git a/sites/all/modules/ctools/help/about.html b/sites/all/modules/ctools/help/about.html
new file mode 100644
index 0000000..30b64c2
--- /dev/null
+++ b/sites/all/modules/ctools/help/about.html
@@ -0,0 +1,29 @@
+The Chaos Tool Suite is a series of tools for developers to make code that I've found to be very useful to Views and Panels more readily available. Certain methods of doing things, particularly with AJAX, exportable objects and a plugin system, are proving to be ideas that are useful outside of just Views and Panels. This module does not offer much directly to the end user, but instead, creates a library for other modules to use. If you are an end user and some module asked you to install the CTools suite, then this is far as you really need to go. If you're a developer and are interested in these tools, read on!
+
+Tools provided by CTools
+
+
+Plugins
+The plugins tool allows a module to allow other modules (and themes!) to provide plugins which provide some kind of functionality or some kind of task. For example, in Panels there are several types of plugins: Content types (which are like blocks), layouts (which are page layouts) and styles (which can be used to style a panel). Each plugin is represented by a .inc file, and the functionality they offer can differ wildly.
+
+Context
+Context is the idea that the objects that are used in page generation have more value than simply creating a single piece of output. Instead, contexts can be used to create multiple pieces of content that can all be put onto the page. Additionally, contexts can be used to derive other contexts via relationships, such as determining the node author and displaying data about the new context.
+
+AJAX Tools
+AJAX (also known as AHAH) is a method of allowing the browser and the server to communicate without requiring a page refresh. It can be used to create complicated interactive forms, but it is somewhat difficult to integrate into Drupal's Form API. These tools make it easier to accomplish this goal. In addition, CTools provides a few other javascript helpers, such as a modal dialog, a collapsible div, a simple dropdown and dependent checkboxes.
+
+CSS scrubbing and caching
+Drupal comes with a fantastic array of tools to ensure HTML is safe to output but does not contain any similar tools for CSS. CTools provides a small tool to sanitize CSS, so user-input CSS code can still be safely used. It also provides a method for caching CSS for better performance.
+
+Exportable objects
+Views and Panels both use objects that can either be in code or in the database, and the objects can be exported into a piece of PHP code, so they can be moved from site to site or out of the database entirely. This library abstracts that functionality, so other modules can use this same concept for their data.
+
+Form tools
+Drupal 6's FAPI really improved over Drupal 5, and made a lot of things possible. Still, it missed a few items that were needed to make form wizards and truly dynamic AJAX forms possible. CTools includes a replacement for drupal_get_form() that has a few more options and allows the caller to examine the $form_state once the form has completed.
+
+Form wizards
+Finally! An easy way to have form wizards, which is any 'form' that is actually a string of forms that build up to a final conclusion. The form wizard supports a single entry point, the ability to choose whether or not the user can go forward/back/up on the form and easy callbacks to handle the difficult job of dealing with data in between forms.
+
+Temporary object cache
+For normal forms, all of the data needed for an object is stored in the form so that the browser handles a lot of the work. For multi-step and ajax forms, however, this is impractical, and letting the browser store data can be insecure. The object cache provides a non-volatile location to store temporary data while the form is being worked on. This is much safer than the standard Drupal caching mechanism, which is volatile, meaning it can be cleared at any time and any system using it must be capable of recreating the data that was there. This system also allows for object locking, since any object which has an item in the cache from another person can be assumed to be 'locked for editing'.
+
diff --git a/sites/all/modules/ctools/help/ajax.html b/sites/all/modules/ctools/help/ajax.html
new file mode 100644
index 0000000..e69de29
diff --git a/sites/all/modules/ctools/help/collapsible-div.html b/sites/all/modules/ctools/help/collapsible-div.html
new file mode 100644
index 0000000..b9b6d9c
--- /dev/null
+++ b/sites/all/modules/ctools/help/collapsible-div.html
@@ -0,0 +1 @@
+To be written.
diff --git a/sites/all/modules/ctools/help/context-access.html b/sites/all/modules/ctools/help/context-access.html
new file mode 100644
index 0000000..95a8d7f
--- /dev/null
+++ b/sites/all/modules/ctools/help/context-access.html
@@ -0,0 +1,12 @@
+Access plugins allow context based access control to pages.
+ 'title' => Title of the plugin
+ 'description' => Description of the plugin
+ 'callback' => callback to see if there is access is available. params: $conf, $contexts, $account
+ 'required context' => zero or more required contexts for this access plugin
+ 'default' => an array of defaults or a callback giving defaults
+ 'settings form' => settings form. params: &$form, &$form_state, $conf
+ settings form validate
+ settings form submit
+
+
+Warning: your settings array will be stored in the meny system to reduce loads, so be trim .
\ No newline at end of file
diff --git a/sites/all/modules/ctools/help/context-arguments.html b/sites/all/modules/ctools/help/context-arguments.html
new file mode 100644
index 0000000..5c479ae
--- /dev/null
+++ b/sites/all/modules/ctools/help/context-arguments.html
@@ -0,0 +1,14 @@
+Arguments create a context from external input, which is assumed to be a string as though it came from a URL element.
+
+'title' => title
+ 'description' => Description
+ 'keyword' => Default keyword for the context
+ 'context' => Callback to create the context. Params: $arg = NULL, $conf = NULL, $empty = FALSE
+ 'default' => either an array of default settings or a string which is a callback or null to not use.
+ 'settings form' => params: $form, $form_state, $conf -- gets the whole form. Should put anything it wants to keep automatically in $form['settings']
+ 'settings form validate' => params: $form, $form_state
+ 'settings form submit' => params: $form, $form_state
+ 'criteria form' => params: $form, &$form_state, $conf, $argument, $id -- gets the whole argument. It should only put form widgets in $form[$id]. $conf may not be properly initialized so always guard against this due to arguments being changed and handlers not being updated to match.
+ + submit + validate
+ 'criteria select' => returns true if the selected criteria matches the context. params: $context, $conf
+
diff --git a/sites/all/modules/ctools/help/context-content.html b/sites/all/modules/ctools/help/context-content.html
new file mode 100644
index 0000000..c1c6a35
--- /dev/null
+++ b/sites/all/modules/ctools/help/context-content.html
@@ -0,0 +1,157 @@
+The CTools pluggable content system provides various pieces of content as discrete bits of data that can be added to other applications, such as Panels or Dashboard via the UI. Whatever the content is added to stores the configuration for that individual piece of content, and provides this to the content.
+
+Each content type plugin will be contained in a .inc file, with subsidiary files, if necessary, in or near the same directory. Each content type consists of some information and one or more subtypes, which all use the same renderer. Subtypes are considered to be instances of the type. For example, the 'views' content type would have each view in the system as a subtype. Many content types will have exactly one subtype.
+
+Because the content and forms can be provided via ajax, the plugin also provides a list of CSS and JavaScript information that should be available on whatever page the content or forms may be AJAXed onto.
+
+For the purposes of selecting content from the UI, each content subtype will have the following information:
+
+
+ A title
+ A short description
+ A category [Do we want to add hierarchy categories? Do we want category to be more than just a string?]
+ An icon [do we want multiple icons? This becomes a hefty requirement]
+
+
+Each piece of content provides one or more configuration forms, if necessary, and the system that includes the content will handle the data storage. These forms can be provided in sequence as wizards or as extra forms that can be accessed through advanced administration.
+
+The plugin for a content type should contain:
+
+
+ title
+ For use on the content permissions screen.
+ content types
+ Either an array of content type definitions, or a callback that will return content type definitions. This callback will get the plugin definition as an argument.
+
+ content type
+ [Optional] Provide a single content type definition. This is only necessary if content types might be intensive.
+
+ render callback
+ The callback to render the content. Parameters:
+
+ $subtype
+ The name of the subtype being rendered. NOT the loaded subtype data.
+
+ $conf
+ The stored configuration for the content.
+
+ $args
+ Any arguments passed.
+
+ $context
+ An array of contexts requested by the required contexts and assigned by the configuration step.
+
+ $incoming_content
+ Any 'incoming content' if this is a wrapper.
+
+
+
+ admin title
+ A callback to provide the administrative title. If it is not a function, then it will be counted as a string to use as the admin title.
+
+ admin info
+ A callback to provide administrative information about the content, to be displayed when manipulating the content. It should contain a summary of configuration.
+
+ edit form
+ Either a single form ID or an array of forms *keyed* by form ID with the value to be used as the title of the form. %title me be used as a placeholder for the administrative title if necessary.
+ Example:
+array(
+ 'ctools_example_content_form_second' => t('Configure first form'),
+ 'ctools_example_content_form_first' => t('Configure second form'),
+),
+
+The first form will always have required configuration added to it. These forms are normal FAPI forms, but you do not need to provide buttons, these will be added by the form wizard.
+
+
+ add form
+ [Optional] If different from the edit forms, provide them here in the same manner. Also may be set to FALSE to not have an add form.
+
+ css
+ A file or array of CSS files that are necessary for the content.
+
+ js
+ A file or array of javascript files that are necessary for the content to be displayed.
+
+ admin css
+ A file or array of CSS files that are necessary for the forms.
+
+ admin js
+ A file or array of JavaScript files that are necessary for the forms.
+
+ extra forms
+ An array of form information to handle extra administrative forms.
+
+ no title override
+ Set to TRUE if the title cannot be overridden.
+
+ single
+ Set to TRUE if this content provides exactly one subtype.
+
+ render last
+ Set to true if for some reason this content needs to render after other content. This is primarily used for forms to ensure that render order is correct.
+
+
+TODO: many of the above callbacks can be assumed based upon patterns: modulename + '_' + name + '_' + function. i.e, render, admin_title, admin_info, etc.
+
+TODO: Some kind of simple access control to easily filter out content.
+
+The subtype definition should contain:
+
+
+ title
+ The title of the subtype.
+
+ icon
+ The icon to display for the subtype.
+
+ path
+ The path for the icon if it is not in the same directory as the plugin.
+
+ description
+ The short description of the subtype, to be used when selecting it in the UI.
+
+ category
+ Either a text string for the category, or an array of the text string followed by the category weight.
+
+ required context [Optional]
+
+ Either a ctools_context_required or ctools_context_optional or array of contexts for this content. If omitted, no contexts are used.
+
+ create content access [Optional]
+
+ An optional callback to determine if a user can access this subtype. The callback will receive two arguments, the type and subtype.
+
+
+Rendered content
+
+Rendered content is a little more than just HTML.
+
+
+ title
+ The safe to render title of the content.
+
+ content
+ The safe to render HTML content.
+
+ links
+ An array of links associated with the content suitable for theme('links').
+
+ more
+ An optional 'more' link (destination only)
+
+ admin_links
+ Administrative links associated with the content, suitable for theme('links').
+
+ feeds
+ An array of feed icons or links associated with the content. Each member of the array is rendered HTML.
+
+ type
+ The content type.
+
+ subtype
+ The content subtype. These two may be used together as module-delta for block style rendering.
+
+
+Todo: example
+
+Todo after implementations are updated to new version.
diff --git a/sites/all/modules/ctools/help/context-context.html b/sites/all/modules/ctools/help/context-context.html
new file mode 100644
index 0000000..2314bd5
--- /dev/null
+++ b/sites/all/modules/ctools/help/context-context.html
@@ -0,0 +1,13 @@
+Context plugin data:
+
+
+ 'title' => Visible title
+ 'description' => Description of context
+ 'context' => Callback to create a context. Params: $empty, $data = NULL, $conf = FALSE
+ 'settings form' => Callback to show a context setting form. Params: ($conf, $external = FALSE)
+ 'settings form validate' => params: ($form, &$form_values, &$form_state)
+ 'settings form submit' => params: 'ctools_context_node_settings_form_submit',
+ 'keyword' => The default keyword to use.
+ 'context name' => The unique identifier for this context for use by required context checks.
+ 'no ui' => if TRUE this context cannot be selected.
+
\ No newline at end of file
diff --git a/sites/all/modules/ctools/help/context-relationships.html b/sites/all/modules/ctools/help/context-relationships.html
new file mode 100644
index 0000000..cc9969e
--- /dev/null
+++ b/sites/all/modules/ctools/help/context-relationships.html
@@ -0,0 +1,13 @@
+Relationship plugin data:
+
+
+ 'title' => The title to display.
+ 'description' => Description to display.
+ 'keyword' => Default keyword for the context created by this relationship.
+ 'required context' => One or more ctools_context_required/optional objects
+ describing the context input.
+ new panels_required_context(t('Node'), 'node'),
+ 'context' => The callback to create the context. Params: ($context = NULL, $conf)
+ 'settings form' => Settings form. Params: $conf
+ 'settings form validate' => Validate.
+
diff --git a/sites/all/modules/ctools/help/context.html b/sites/all/modules/ctools/help/context.html
new file mode 100644
index 0000000..e69de29
diff --git a/sites/all/modules/ctools/help/css.html b/sites/all/modules/ctools/help/css.html
new file mode 100644
index 0000000..b9b6d9c
--- /dev/null
+++ b/sites/all/modules/ctools/help/css.html
@@ -0,0 +1 @@
+To be written.
diff --git a/sites/all/modules/ctools/help/ctools.help.ini b/sites/all/modules/ctools/help/ctools.help.ini
new file mode 100644
index 0000000..fcb121b
--- /dev/null
+++ b/sites/all/modules/ctools/help/ctools.help.ini
@@ -0,0 +1,97 @@
+[advanced help settings]
+line break = TRUE
+
+[about]
+title = About Chaos Tool Suite
+weight = -100
+
+[context]
+title = Context tool
+weight = -40
+
+[context-access]
+title = Context based access control plugins
+parent = context
+
+[context-context]
+title = Context plugins
+parent = context
+
+[context-arguments]
+title = Argument plugins
+parent = context
+
+[context-relationships]
+title = Relationship plugins
+parent = context
+
+[context-content]
+title = Content plugins
+parent = context
+
+[css]
+title = CSS scrubbing and caching tool
+
+[menu]
+title = Miscellaneous menu helper tool
+
+[plugins]
+title = Plugins and APIs tool
+weight = -50
+
+[plugins-api]
+title = Implementing APIs
+parent = plugins
+
+[plugins-creating]
+title = Creating plugins
+parent = plugins
+
+[plugins-implementing]
+title = Implementing plugins
+parent = plugins
+
+[export]
+title = Exportable objects tool
+
+[export-ui]
+title = Exportable objects UI creator
+
+[form]
+title = Form tools
+
+[wizard]
+title = Form wizard tool
+
+[ajax]
+title = AJAX and Javascript helper tools
+weight = -30
+
+[modal]
+title = Javascript modal tool
+parent = ajax
+
+[collapsible-div]
+title = Javascript collapsible DIV
+parent = ajax
+
+[dropdown]
+title = Javascript dropdown
+parent = ajax
+
+[dropbutton]
+title = Javascript dropbutton
+parent = ajax
+
+[dependent]
+title = Dependent checkboxes and radio buttons
+parent = ajax
+
+[object-cache]
+title = Temporary object caching
+
+; A bunch of this stuff we'll put in panels.
+
+[plugins-content]
+title = Creating content type plugins
+parent = panels%api
diff --git a/sites/all/modules/ctools/help/dependent.html b/sites/all/modules/ctools/help/dependent.html
new file mode 100644
index 0000000..b9b6d9c
--- /dev/null
+++ b/sites/all/modules/ctools/help/dependent.html
@@ -0,0 +1 @@
+To be written.
diff --git a/sites/all/modules/ctools/help/dropbutton.html b/sites/all/modules/ctools/help/dropbutton.html
new file mode 100644
index 0000000..b9b6d9c
--- /dev/null
+++ b/sites/all/modules/ctools/help/dropbutton.html
@@ -0,0 +1 @@
+To be written.
diff --git a/sites/all/modules/ctools/help/dropdown.html b/sites/all/modules/ctools/help/dropdown.html
new file mode 100644
index 0000000..b9b6d9c
--- /dev/null
+++ b/sites/all/modules/ctools/help/dropdown.html
@@ -0,0 +1 @@
+To be written.
diff --git a/sites/all/modules/ctools/help/export-ui.html b/sites/all/modules/ctools/help/export-ui.html
new file mode 100644
index 0000000..e6b1086
--- /dev/null
+++ b/sites/all/modules/ctools/help/export-ui.html
@@ -0,0 +1,85 @@
+Most user interfaces for exportables are very similar, so CTools includes a tool to provide the framework for the most common UI. This tool is a plugin of the 'export_ui' type. In order to create a UI for your exportable object with this tool, you first need to ensure that your module supports the plugin:
+
+
+function HOOK_ctools_plugin_directory($module, $plugin) {
+ if ($module == 'ctools' && $plugin == 'export_ui') {
+ return 'plugins/' . $plugin;
+ }
+}
+
+
+Then, you need to create a plugin .inc file describing your UI. Most of the UI runs with sane but simple defaults, so for the very simplest UI you don't need to do very much. This is a very simple example plugin for the 'example' export type:
+
+
+$plugin = array(
+ // The name of the table as found in the schema in hook_install. This
+ // must be an exportable type with the 'export' section defined.
+ 'schema' => 'example',
+
+ // The access permission to use. If not provided it will default to
+ // 'administer site configuration'
+ 'access' => 'administer example',
+
+ // You can actually define large chunks of the menu system here. Nothing
+ // is required here. If you leave out the values, the prefix will default
+ // to admin/structure and the item will default to the plugin name.
+ 'menu' => array(
+ 'menu prefix' => 'admin/structure',
+ 'menu item' => 'example',
+ // Title of the top level menu. Note this should not be translated,
+ // as the menu system will translate it.
+ 'menu title' => 'Example',
+ // Description of the top level menu, which is usually needed for
+ // menu items in an administration list. Will be translated
+ // by the menu system.
+ 'menu description' => 'Administer site example objects.',
+ ),
+
+ // These are required to provide proper strings for referring to the
+ // actual type of exportable. "proper" means it will appear at the
+ // beginning of a sentence.
+ 'title singular' => t('example'),
+ 'title singular proper' => t('Example'),
+ 'title plural' => t('examples'),
+ 'title plural proper' => t('Examples'),
+
+ // This will provide you with a form for editing the properties on your
+ // exportable, with validate and submit handler.
+ //
+ // The item being edited will be in $form_state['item'].
+ //
+ // The submit handler is only responsible for moving data from
+ // $form_state['values'] to $form_state['item'].
+ //
+ // All callbacks will accept &$form and &$form_state as arguments.
+ 'form' => array(
+ 'settings' => 'example_ctools_export_ui_form',
+ 'validate' => 'example_ctools_export_ui_form_validate',
+ 'submit' => 'example_ctools_export_ui_form_submit',
+ ),
+
+);
+
+
+For a more complete list of what you can set in your plugin, please see ctools_export_ui_defaults() in includes/export-ui.inc to see what the defaults are.
+
+More advanced UIs
+
+The bulk of this UI is built on an class called ctools_export_ui, which is itself stored in ctools/plugins/export_ui as the default plugin. Many UIs will have more complex needs than the defaults provide. Using OO and overriding methods can allow an implementation to use the basics and still provide more where it is needed. To utilize this, first add a 'handler' directive to your plugin .inc file:
+
+
+ 'handler' => array(
+ 'class' => 'ctools_export_ui_example',
+ 'parent' => 'ctools_export_ui',
+ ),
+
+
+Then create your class in ctools_export_ui_example.class.php in your plugins directory:
+
+
+class ctools_export_ui_example extends ctools_export_ui {
+
+}
+
+
+You can override any method found in the class (see the source file for details). In particular, there are several list methods that are good candidates for overriding if you need to provide richer data for listing, sorting or filtering. If you need multi-step add/edit forms, you can override edit_page(), add_page(), clone_page(), and import_page() to put your wizard in place of the basic editing system. For an example of how to use multi-step wizards, see the import_page() method.
diff --git a/sites/all/modules/ctools/help/export.html b/sites/all/modules/ctools/help/export.html
new file mode 100644
index 0000000..573c98a
--- /dev/null
+++ b/sites/all/modules/ctools/help/export.html
@@ -0,0 +1,294 @@
+Exportable objects are objects that can live either in the database or in code, or in both. If they live in both, then the object in code is considered to be "overridden", meaning that the version in code is ignored in favor of the version in the database.
+
+The main benefit to this is that you can move objects that are intended to be structure or feature-related into code, thus removing them entirely from the database. This is a very important part of the deployment path, since in an ideal world, the database is primarily user generated content, whereas site structure and site features should be in code. However, many many features in Drupal rely on objects being in the database and provide UIs to create them.
+
+Using this system, you can give your objects dual life. They can be created in the UI, exported into code and put in revision control. Views and Panels both use this system heavily. Plus, any object that properly implements this system can be utilized by the Features module to be used as part of a bundle of objects that can be turned into feature modules.
+
+Typically, exportable objects have two identifiers. One identifier is a simple serial used for database identification. It is a primary key in the database and can be used locally. It also has a name which is an easy way to uniquely identify it. This makes it much less likely that importing and exporting these objects across systems will have collisions. They still can, of course, but with good name selection, these problems can be worked around.
+
+Making your objects exportable
+
+To make your objects exportable, you do have to do a medium amount of work.
+
+
+ Create a chunk of code in your object's schema definition in the .install file to introduce the object to CTools' export system.
+ Create a load function for your object that utilizes ctools_export_load_object().
+ Create a save function for your object that utilizes drupal_write_record() or any method you desire.
+ Create an import and export mechanism from the UI.
+
+
+The export section of the schema file
+
+Exportable objects are created by adding definition to the schema in an 'export' section. For example:
+
+
+function mymodule_schema() {
+ $schema['mymodule_myobj'] = array(
+ 'description' => t('Table storing myobj definitions.'),
+ 'export' => array(
+ 'key' => 'name',
+ 'key name' => 'Name',
+ 'primary key' => 'oid',
+ 'identifier' => 'myobj', // Exports will be as $myobj
+ 'default hook' => 'default_mymodule_myobj', // Function hook name.
+ 'api' => array(
+ 'owner' => 'mymodule',
+ 'api' => 'default_mymodule_myobjs', // Base name for api include files.
+ 'minimum_version' => 1,
+ 'current_version' => 1,
+ ),
+ // If the key is stored in a table that is joined in, specify it:
+ 'key in table' => 'my_join_table',
+
+ ),
+
+ // If your object's data is split up across multiple tables, you can
+ // specify additional tables to join. This is very useful when working
+ // with modules like exportables.module that has a special table for
+ // translating keys to local database IDs.
+ //
+ // The joined table must have its own schema definition.
+ //
+ // If using joins, you should implement a 'delete callback' (see below)
+ // to ensure that deletes happen properly. export.inc does not do this
+ // automatically!
+ 'join' => array(
+ 'exportables' => array(
+ // The following parameters will be used in this way:
+ // SELECT ... FROM {mymodule_myobj} t__0 INNER JOIN {my_join_table} t__1 ON t__0.id = t__1.id AND extras
+ 'table' => 'my_join_table',
+ 'left_key' => 'format',
+ 'right_key' => 'id',
+ // Optionally you can define a callback to add custom conditions or
+ // alter the query as necessary. The callback function takes 3 args:
+ //
+ // myjoincallback(&$query, $schema, $join_schema);
+ //
+ // where $query is the database query object, $schema is the schema for
+ // the export base table and $join_schema is the schema for the current
+ // join table.
+ 'callback' => 'myjoincallback',
+
+ // You must specify which fields will be loaded. These fields must
+ // exist in the schema definition of the joined table.
+ 'load' => array(
+ 'machine',
+ ),
+
+ // And finally you can define other tables to perform INNER JOINS
+ //'other_joins' => array(
+ // 'table' => ...
+ //),
+ ),
+ )
+ 'fields' => array(
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Unique ID for this object. Used to identify it programmatically.',
+ ),
+ 'oid' => array(
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
+ 'no export' => TRUE, // Do not export database-only keys.
+ ),
+ // ......
+ 'primary key' => array('oid'),
+ 'unique keys' => array(
+ 'name' => array('name'),
+ ),
+ );
+ return $schema;
+}
+
+
+
+key
+This is the primary key of the exportable object and should be a string as names are more portable across systems. It is possible to use numbers here, but be aware that export collisions are very likely. Defaults to 'name'.
+
+key name
+Human readable title of the export key. Defaults to 'Name'. Because the schema is cached, do not translate this. It must instead be translated when used.
+
+primary key
+A single field within the table that is to be used as the main identifier to discern whether or not the object has been written. As the schema definition's primary key value will be used by default, it is not usually necessary to define this.
+
+object
+The class the object should be created as, if 'object factory' is not set. If this is not set either, defaults as stdClass.
+
+object factory
+Function used to create the object. The function receives the schema and the loaded data as a parameters: your_factory_function($schema, $data). If this is set, 'object' has no effect since you can use your function to create whatever class you wish.
+
+admin_title
+A convenience field that may contain the field which represents the human readable administrative title for use in export UI. If a field "admin_title" exists, it will automatically be used.
+
+admin_description
+A convenience field that may contain the field which represents the human readable administrative title for use in export UI. If a field "admin_title" exists, it will automatically be used.
+
+can disable
+Control whether or not the exportable objects can be disabled. All this does is cause the 'disabled' field on the object to always be set appropriately, and a variable is kept to record the state. Changes made to this state must be handled by the owner of the object. Defaults to TRUE.
+
+status
+Exportable objects can be enabled or disabled, and this status is stored in a variable. This defines what variable that is. Defaults to: 'default_' . $table.
+
+default hook
+What hook to invoke to find exportable objects that are currently defined. These will all be gathered into a giant array. Defaults to 'default_' . $table.
+
+cache defaults
+If true, default objects will be cached so that the processing of the hook does not need to be called often. Defaults to FALSE. Recommended if you will potentially have a lot of objects in code. Not recommended if code will be the exception.
+
+default cache bin
+If default object caching is enabled, what cache bin to use. This defaults to the basic "cache". It is highly recommended that you use a different cache bin if possible.
+
+identifier
+When exporting the object, the identifier is the variable that the exported object will be placed in. Defaults to $table.
+
+bulk export
+Declares whether or not the exportable will be available for bulk exporting.
+
+export type string
+The export type string (Local, Overridden, Database) is normally stored as $item->type. Since type is a very common keyword, it's possible to specify what key to actually use.
+
+list callback
+Bulk export callback to provide a list of exportable objects to be chosen for bulk exporting. Defaults to $module . '_' . $table . '_list' if the function exists. If it is not, a default listing function will be provided that will make a best effort to list the titles. See ctools_export_default_list().
+
+to hook code callback
+Function used to generate an export for the bulk export process. This is only necessary if the export is more complicated than simply listing the fields. Defaults to $module . '_' . $table . '_to_hook_code'.
+
+boolean
+Explicitly indicate if a table field contains a boolean or not. The Schema API does not model the
+difference between a tinyint and a boolean type. Boolean values are stored in tinyint fields. This may cause mismatch errors when exporting a non-boolean value from a tinyint field. Add this to a tinyint field if it contains boolean data and can be exported. Defaults to TRUE.
+
+ create callback
+CRUD callback to use to create a new exportable item in memory. If not provided, the default function will be used. The single argument is a boolean used to determine if defaults should be set on the object. This object will not be written to the database by this callback.
+
+load callback
+CRUD callback to use to load a single item. If not provided, the default load function will be used. The callback will accept a single argument which should be an identifier of the export key.
+
+load multiple callback
+CRUD callback to use to load multiple items. If not provided, the default multiple load function will be used. The callback will accept an array which should be the identifiers of the export key.
+
+load all callback
+CRUD callback to use to load all items, usually for administrative purposes. If not provided, the default load function will be used. The callback will accept a single argument to determine if the load cache should be reset or not.
+
+save callback
+CRUD callback to use to save a single item. If not provided, the default save function will be used. The callback will accept a single argument which should be the complete exportable object to save.
+
+delete callback
+CRUD callback to use to delete a single item. If not provided, the default delete function will be used. The callback will accept a single argument which can be *either* the object or just the export key to delete. The callback MUST be able to accept either.
+
+export callback
+CRUD callback to use for exporting. If not provided, the default export function will be used. The callback will accept two arguments, the first is the item to export, the second is the indent to place on the export, if any.
+
+import callback
+CRUD callback to use for importing. If not provided, the default export function will be used. This function will accept the code as a single argument and, if the code evaluates, return an object represented by that code. In the case of failure, this will return a string with human readable errors.
+
+status callback
+CRUD callback to use for updating the status of an object. If the status is TRUE the object will be disabled. If the status is FALSE the object will be enabled.
+
+api
+The 'api' key can optionally contain some information for the plugin API definition. This means that the imports can be tied to an API name which is used to have automatic inclusion of files, and can be used to prevent dangerous objects from older versions from being loaded, causing a loss of functionality rather than site crashes or security loopholes.
+
+If not present, no additional files will be loaded and the default hook will always be a simple hook that must be either part of the .module file or loaded during normal operations.
+
+api supports these subkeys:
+
+
+owner
+The module that owns the API. Typically this is the name of the module that owns the schema. This will be one of the two keys used by hook_ctools_plugin_api() to determine version compatibility. Note that the name of this hook can be tailored via the use of hook_ctools_plugin_api_hook_name(). See ctools_plugin_api_get_hook() for details.
+api
+This is the name of the API, and will be the second parameter to the above mentioned hook. It will also be used as part of the name of the file that the hook containing default objects will be in, which comes in the form of MODULENAME.API.inc.
+minimum_version
+The minimum version supported. Any module reporting an API less than this will not have its default objects used. This should be updated only when API changes can cause older objects to crash or otherwise break badly.
+current_version
+The current version of the API. Any module reporting a required API higher than this will not have its default objects used.
+
+
+
+
+In addition, each field can contain the following:
+
+no export
+Set to TRUE to prevent that field from being exported.
+
+export callback
+A function to override the export behavior. It will receive ($myobject, $field, $value, $indent) as arguments. By default, fields are exported through ctools_var_export().
+
+
+Reserved keys on exportable objects
+
+Exportable objects have several reserved keys that are used by the CTools export API. Each key can be found at $myobj->{$key} on an object loaded through ctools_export_load_object(). Implementing modules should not use these keys as they will be overwritten by the CTools export API.
+
+api_version
+The API version that this object implements.
+
+disabled
+A boolean for whether the object is disabled.
+
+export_module
+For objects that live in code, the module which provides the default object.
+
+export_type
+A bitmask representation of an object current storage. You can use this bitmask in combination with the EXPORT_IN_CODE and EXPORT_IN_DATABASE constants to test for an object's storage in your code.
+
+
+in_code_only
+A boolean for whether the object lives only in code.
+
+table
+The schema API table that this object belongs to.
+
+type
+A string representing the storage type of this object. Can be one of the following:
+
+Normal is an object that lives only in the database.
+Overridden is an object that lives in the database and is overriding the exported configuration of a corresponding object in code.
+Default is an object that lives only in code.
+
+Note: This key can be changed by setting 'export type string' to something else, to try and prevent "type" from conflicting.
+
+
+
+The load callback
+Calling ctools_export_crud_load($table, $name) will invoke your load callback, calling ctools_export_crud_load_multiple($table, $names) will invoke your load multiple callback, and calling ctools_export_crud_load_all($table, $reset) will invoke your load all callback. The default handlers should be sufficient for most uses.
+
+Typically, there will be three load functions. A 'single' load, to load just one object, a 'multiple' load to load multiple objects, and an 'all' load, to load all of the objects for use in administrating the objects or utilizing the objects when you need all of them. Using ctools_export_load_object() you can easily do both, as well as quite a bit in between. This example duplicates the default functionality for loading one myobj.
+
+
+/**
+ * Implements 'load callback' for myobj exportables.
+ */
+function mymodule_myobj_load($name) {
+ ctools_include('export');
+ $result = ctools_export_load_object('mymodule_myobjs', 'names', array($name));
+ if (isset($result[$name])) {
+ return $result[$name];
+ }
+}
+
+/**
+ * Implements 'load multiple callback' for myobj exportables.
+ */
+function mymodule_myobj_load_multiple(array $names) {
+ ctools_include('export')
+ $results = ctools_export_load_object('mymodule_myobjs', 'names', $names);
+ return array_filter($results);
+}
+
+
+The save callback
+Calling ctools_export_crud_save($table, $object) will invoke your save callback. The default handlers should be sufficient for most uses. For the default save mechanism to work, you must define a 'primary key' in the 'export' section of your schema. The following example duplicates the default functionality for the myobj.
+
+
+/**
+* Save a single myobj.
+*/
+function mymodule_myobj_save(&$myobj) {
+ $update = (isset($myobj->oid) && is_numeric($myobj->oid)) ? array('oid') : array();
+ return drupal_write_record('myobj', $myobj, $update);
+}
+
+
+Default hooks for your exports
+All exportables come with a 'default' hook, which can be used to put your exportable into code. The easiest way to actually use this hook is to set up your exportable for bulk exporting, enable the bulk export module and export an object.
diff --git a/sites/all/modules/ctools/help/form.html b/sites/all/modules/ctools/help/form.html
new file mode 100644
index 0000000..b9b6d9c
--- /dev/null
+++ b/sites/all/modules/ctools/help/form.html
@@ -0,0 +1 @@
+To be written.
diff --git a/sites/all/modules/ctools/help/modal.html b/sites/all/modules/ctools/help/modal.html
new file mode 100644
index 0000000..761fd2e
--- /dev/null
+++ b/sites/all/modules/ctools/help/modal.html
@@ -0,0 +1,215 @@
+CTools provides a simple modal that can be used as a popup to place forms. It differs from the normal modal frameworks in that it does not do its work via an iframe. This is both an advantage and a disadvantage. The iframe simply renders normal pages in a sub-browser and they can do their thing. That makes it much easier to put arbitrary pages and forms in a modal. However, the iframe is not very good at actually communicating changes to the main page, so you cannot open the modal, have it do some work, and then modify the page.
+
+Invoking the modal
+
+The basic form of the modal can be set up just by including the javascript and adding the proper class to a link or form that will open the modal. To include the proper javascript, simply include the library and call the add_js function:
+
+ctools_include('modal');
+ctools_modal_add_js();
+
+
+You can have links and buttons bound to use the modal by adding the class ctools-use-modal. For convenience, there is a helper function to try and do this, though it's not great at doing all links so using this is optional:
+
+/**
+ * Render an image as a button link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * @param $image
+ * The path to an image to use that will be sent to theme('image') for rendering.
+ * @param $dest
+ * The destination of the link.
+ * @param $alt
+ * The alt text of the link.
+ * @param $class
+ * Any class to apply to the link. @todo this should be a options array.
+ */
+function ctools_modal_image_button($image, $dest, $alt, $class = '') {
+ return ctools_ajax_text_button(theme('image', array('path' => $image), $dest, $alt, $class, 'ctools-use-modal');
+}
+
+/**
+ * Render text as a link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
+ * not use user input so this is a very minor concern.
+ *
+ * @param $text
+ * The text to display as the link.
+ * @param $dest
+ * The destination of the link.
+ * @param $alt
+ * The alt text of the link.
+ * @param $class
+ * Any class to apply to the link. @todo this should be a options array.
+ */
+function ctools_modal_text_button($text, $dest, $alt, $class = '') {
+ return ctools_ajax_text_button($text, $dest, $alt, $class, 'ctools-use-modal');
+}
+
+
+Like with all CTools' AJAX functionality, the href of the link will be the destination, with any appearance of /nojs/ converted to /ajax/.
+
+For submit buttons, however, the URL may be found a different, slightly more complex way. If you do not wish to simply submit the form to the modal, you can create a URL using hidden form fields. The ID of the item is taken and -url is appended to it to derive a class name. Then, all form elements that contain that class name are founded and their values put together to form a URL.
+
+For example, let's say you have an 'add' button, and it has a select form item that tells your system what widget it is adding. If the id of the add button is edit-add, you would place a hidden input with the base of your URL in the form and give it a class of 'edit-add-url'. You would then add 'edit-add-url' as a class to the select widget allowing you to convert this value to the form without posting. If no URL is found, the action of the form will be used and the entire form posted to it.
+
+Customizing the modal
+
+If you do not wish to use the default modal, the modal can be customized by creating an array of data to define a customized modal. To do this, you add an array of info to the javascript settings to define the customizations for the modal and add an additional class to your modal link or button to tell it which modal to use.
+
+First, you need to create a settings array. You can do this most easily with a bit of PHP:
+
+drupal_add_js(array(
+ 'my-modal-style' => array(
+ 'modalSize' => array(
+ 'type' => 'fixed',
+ 'width' => 250,
+ 'height' => 250,
+ ),
+ ),
+ ), 'setting');
+
+
+The key to the array above (in this case, my-modal-style) is the identifier to your modal theme. You can have multiple modal themes on a page, so be sure to use an ID that will not collide with some other module's use. Using your module or theme as a prefix is a good idea.
+
+Then, when adding the ctools-use-modal class to your link or button, also add the following class: ctools-modal-ID (in the example case, that would be ctools-modal-my-modal-style).
+
+modalSize can be 'fixed' or 'scale'. If fixed it will be a raw pixel value; if 'scale' it will be a percentage of the screen.
+
+You can set:
+
+ modalSize : an array of data to control the sizing of the modal. It can contain:
+
+ type : Either fixed or scale . If fixed, the modal will always be a fixed size. If scale the modal will scale to a percentage of the browser window. Default: scale .
+ width : If fixed the width in pixels. If scale the percentage of the screen expressed as a number less than zero. (For 80 percent, use .8, for example). Default: .8
+ height : If fixed the height in pixels. If scale the percentage of the screen expressed as a number less than zero. (For 80 percent, use .8, for example). Default: .8
+ addWidth : Any additional width to add to the modal in pixels. Only useful if the type is scale. Default: 0
+ addHeight : Any additional height to add to the modal in pixels. Only useful if the type is scale. Default: 0
+ contentRight : The number of pixels to remove from the content inside the modal to make room for scroll bar and decorations. Default: 25
+ contentBottom : The number of pixels to remove from the content inside the modal to make room for scroll bar and decorations. Default: 45
+
+
+ modalTheme : The Drupal javascript themable function which controls how the modal will be rendered. This function must be in the Drupal.theme.prototype namespace. If you set this value, you must include a corresponding function in a javascript file and use drupal_add_js() to add that file. Default: CToolsModalDialog
+
+ Drupal.theme.prototype.CToolsModalDialog = function () {
+ var html = ''
+ html += ' <div id="ctools-modal">'
+ html += ' <div class="ctools-modal-content">' // panels-modal-content
+ html += ' <div class="modal-header">';
+ html += ' <a class="close" href="#">';
+ html += Drupal.CTools.Modal.currentSettings.closeText + Drupal.CTools.Modal.currentSettings.closeImage;
+ html += ' </a>';
+ html += ' <span id="modal-title" class="modal-title"> </span>';
+ html += ' </div>';
+ html += ' <div id="modal-content" class="modal-content">';
+ html += ' </div>';
+ html += ' </div>';
+ html += ' </div>';
+
+ return html;
+ }
+
+ throbberTheme : The Drupal javascript themable function which controls how the modal throbber will be rendered. This function must be in the Drupal.theme.prototype namespace. If you set this value, you must include a corresponding function in a javascript file and use drupal_add_js() to add that file. Default: CToolsModalThrobber
+
+ Drupal.theme.prototype.CToolsModalThrobber = function () {
+ var html = '';
+ html += ' <div id="modal-throbber">';
+ html += ' <div class="modal-throbber-wrapper">';
+ html += Drupal.CTools.Modal.currentSettings.throbber;
+ html += ' </div>';
+ html += ' </div>';
+
+ return html;
+ };
+
+
+ modalOptions : The options object that's sent to Drupal.CTools.Modal.modalContent. Can contain any CSS settings that will be applied to the modal backdrop, in particular settings such as opacity and background . Default: array('opacity' => .55, 'background' => '#fff');
+ animation : Controls how the modal is animated when it is first drawn. Either show , fadeIn or slideDown . Default: show
+ animationSpeed : The speed of the animation, expressed either as a word jQuery understands (slow, medium or fast) or a number of milliseconds for the animation to run. Defaults: fast
+closeText : The text to display for the close button. Be sure to wrap this in t() for translatability. Default: t('Close window')
+closeImage : The image to use for the close button of the modal. Default: theme('image', array('path' => ctools_image_path('icon-close-window.png'), 'alt' => t('Close window'), 'title' => t('Close window')))
+loadingText : The text to display for the modal title during loading, along with the throbber. Be sure to wrap this in t() for translatability. Default: t('Close window')
+throbber : The HTML to display for the throbber image. You can use this instead of CToolsModalThrobber theme if you just want to change the image but not the throbber HTML. Default: theme('image', array('path' => ctools_image_path('throbber.gif'), 'alt' => t('Loading...'), 'title' => t('Loading')))
+
+
+Rendering within the modal
+To render your data inside the modal, you need to provide a page callback in your module that responds more or less like a normal page.
+
+In order to handle degradability, it's nice to allow your page to work both inside and outside of the modal so that users whose javascript is turned off can still use your page. There are two ways to accomplish this. First, you can embed 'nojs' as a portion of the URL and then watch to see if that turns into 'ajax' when the link is clicked. Second, if you do not wish to modify the URLs, you can check $_POST['js'] or $_POST['ctools_js'] to see if that flag was set. The URL method is the most flexible because it is easy to send the two links along completely different paths if necessary, and it is also much easier to manually test your module's output by manually visiting the nojs URL. It's actually quite difficult to do this if you have to set $_POST['js'] to test.
+
+In your hook_menu, you can use the a CTools' AJAX convenience loader to help:
+
+
+ $items['ctools_ajax_sample/%ctools_js/login'] = array(
+ 'title' => 'Login',
+ 'page callback' => 'ctools_ajax_sample_login',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+
+
+The first argument to the page callback will be the result of ctools_js_load() which will return TRUE if 'ajax' was the string, and FALSE if anything else (i.e, nojs) is the string. Which means you can then declare your function like this:
+
+
+function ctools_ajax_sample_login($js) {
+ if ($js) {
+ // react with the modal
+ }
+ else {
+ // react without the modal
+ }
+}
+
+
+If your modal does not include a form, rendering the output you wish to give the user is just a matter of calling the modal renderer with your data:
+
+
+function ctools_ajax_hello_world($js) {
+ $title = t('Greetings');
+ $output = '<p>' . t('Hello world') . '</p>';
+ if ($js) {
+ ctools_modal_render($title, $output);
+ }
+ else {
+ drupal_set_title($title);
+ return $output;
+ }
+}
+
+
+If you need to do more than just render your modal, you can use a CTools $commands array. See the function ctools_modal_render() for more information on what you need to do here.
+
+If you are displaying a form -- and the vast majority of modals display forms -- then you need to do just slightly more. Fortunately there is the ctools_modal_form_wrapper() function:
+
+
+ ctools_include('modal');
+ ctools_include('ajax');
+ $form_state = array(
+ 'title' => t('Title of my form'),
+ 'ajax' => $js,
+ );
+ $output = ctools_modal_form_wrapper('my_form', $form_state);
+ // There are some possible states after calling the form wrapper:
+ // 1) We are not using $js and the form has been executed.
+ // 2) We are using $js and the form was successfully submitted and
+ // we need to dismiss the modal.
+ // Most other states are handled automatically unless you set flags in
+ // $form_state to avoid handling them, so we only deal with those two
+ // states.
+ if ($form_state['executed'] && $js) {
+ $commands = array();
+ $commands[] = ctools_modal_command_dismiss(t('Login Success'));
+ // In typical usage you will do something else here, such as update a
+ // div with HTML or redirect the page based upon the results of the modal
+ // form.
+ print ajax_render($commands);
+ exit;
+ }
+
+ // Otherwise, just return the output.
+ return $output;
+
+
+You can also use CTools' form wizard inside the modal. You do not need to do much special beyond setting $form_state['modal'] = TRUE in the wizard form; it already knows how to handle the rest.
diff --git a/sites/all/modules/ctools/help/object-cache.html b/sites/all/modules/ctools/help/object-cache.html
new file mode 100644
index 0000000..801a836
--- /dev/null
+++ b/sites/all/modules/ctools/help/object-cache.html
@@ -0,0 +1,132 @@
+The CTools Object Cache is a specialized cache for storing data that is non-volatile. This differs from the standard Drupal cache mechanism, which is volatile, meaning that the data can be cleared at any time and it is expected that any cached data can easily be reconstructed. In contrast, data stored in this cache is not expected to be reconstructable. It is primarily used for storing user input which is retrieved in stages, allowing for more complex user interface interactions.
+
+The object cache consists of 3 normal functions for cache maintenance, and 2 additional functions to facilitate locking.
+
+To use any of these functions, you must first use ctools_include:
+
+
+ctools_include('object-cache');
+
+
+
+/**
+ * Get an object from the non-volatile ctools cache.
+ *
+ * This function caches in memory as well, so that multiple calls to this
+ * will not result in multiple database reads.
+ *
+ * @param $obj
+ * A 128 character or less string to define what kind of object is being
+ * stored; primarily this is used to prevent collisions.
+ * @param $name
+ * The name of the object being stored.
+ * @param $skip_cache
+ * Skip the memory cache, meaning this must be read from the db again.
+ *
+ * @return
+ * The data that was cached.
+ */
+function ctools_object_cache_get($obj, $name, $skip_cache = FALSE) {
+
+
+
+/**
+ * Store an object in the non-volatile ctools cache.
+ *
+ * @param $obj
+ * A 128 character or less string to define what kind of object is being
+ * stored; primarily this is used to prevent collisions.
+ * @param $name
+ * The name of the object being stored.
+ * @param $cache
+ * The object to be cached. This will be serialized prior to writing.
+ */
+function ctools_object_cache_set($obj, $name, $cache) {
+
+
+
+/**
+ * Remove an object from the non-volatile ctools cache
+ *
+ * @param $obj
+ * A 128 character or less string to define what kind of object is being
+ * stored; primarily this is used to prevent collisions.
+ * @param $name
+ * The name of the object being removed.
+ */
+function ctools_object_cache_clear($obj, $name) {
+
+
+To facilitate locking, which is the ability to prohibit updates by other users while one user has an object cached, this system provides two functions:
+
+
+/**
+ * Determine if another user has a given object cached.
+ *
+ * This is very useful for 'locking' objects so that only one user can
+ * modify them.
+ *
+ * @param $obj
+ * A 128 character or less string to define what kind of object is being
+ * stored; primarily this is used to prevent collisions.
+ * @param $name
+ * The name of the object being removed.
+ *
+ * @return
+ * An object containing the UID and updated date if found; NULL if not.
+ */
+function ctools_object_cache_test($obj, $name) {
+
+
+The object returned by ctools_object_cache_test can be directly used to determine whether a user should be allowed to cache their own version of an object.
+
+To allow the concept of breaking a lock, that is, clearing another users changes:
+
+
+/**
+ * Remove an object from the non-volatile ctools cache for all session IDs.
+ *
+ * This is useful for clearing a lock.
+ *
+ * @param $obj
+ * A 128 character or less string to define what kind of object is being
+ * stored; primarily this is used to prevent collisions.
+ * @param $name
+ * The name of the object being removed.
+ */
+function ctools_object_cache_clear_all($obj, $name) {
+
+
+Typical best practice is to use wrapper functions such as these:
+
+
+/**
+ * Get the cached changes to a given task handler.
+ */
+function delegator_page_get_page_cache($name) {
+ ctools_include('object-cache');
+ $cache = ctools_object_cache_get('delegator_page', $name);
+ if (!$cache) {
+ $cache = delegator_page_load($name);
+ $cache->locked = ctools_object_cache_test('delegator_page', $name);
+ }
+
+ return $cache;
+}
+
+/**
+ * Store changes to a task handler in the object cache.
+ */
+function delegator_page_set_page_cache($name, $page) {
+ ctools_include('object-cache');
+ $cache = ctools_object_cache_set('delegator_page', $name, $page);
+}
+
+/**
+ * Remove an item from the object cache.
+ */
+function delegator_page_clear_page_cache($name) {
+ ctools_include('object-cache');
+ ctools_object_cache_clear('delegator_page', $name);
+}
+
diff --git a/sites/all/modules/ctools/help/plugins-api.html b/sites/all/modules/ctools/help/plugins-api.html
new file mode 100644
index 0000000..548f17b
--- /dev/null
+++ b/sites/all/modules/ctools/help/plugins-api.html
@@ -0,0 +1,55 @@
+APIs are a form of plugins that are tightly associated with a module. Instead of a module providing any number of plugins, each module provides only one file for an API and this file can contain hooks that the module should invoke.
+
+Modules support this API by implementing hook_ctools_plugin_api($module, $api). If they support the API, they return a packet of data:
+
+
+function mymodule_ctools_plugin_api($module, $api) {
+ if ($module == 'some module' && $api = 'some api') {
+ return array(
+ 'version' => The minimum API version this system supports. If this API version is incompatible then the .inc file will not be loaded.
+ 'path' => Where to find the file. Optional; if not specified it will be the module's directory.
+ 'file' => an alternative version of the filename. If not specified it will be $module.$api.inc
+ );
+ }
+}
+
+
+This implementation must be in the .module file.
+
+Modules utilizing this can invole ctools_plugin_api_include() in order to ensure all modules that support the API will have their files loaded as necessary. It's usually easiest to create a small helper function like this:
+
+
+define('MYMODULE_MINIMUM_VERSION', 1);
+define('MYMODULE_VERSION', 1);
+
+function mymodule_include_api() {
+ ctools_include('plugins');
+ return ctools_plugin_api_include('mymodule', 'myapi', MYMODULE_MINIMUM_VERSION, MYMODULE_VERSION);
+}
+
+
+Using a define will ensure your use of version numbers is consistent and easy to update when you make API changes. You can then use the usual module_invoke type commands:
+
+
+mymodule_include_api();
+module_invoke('myhook', $data);
+
+
+If you need to pass references, this construct is standard:
+
+
+foreach (mymodule_include_api() as $module => $info) {
+ $function = $module . '_hookname';
+ // Just because they implement the API and include a file does not guarantee they implemented
+ // a hook function!
+ if (!function_exists($function)) {
+ continue;
+ }
+
+ // Typically array_merge() is used below if data is returned.
+ $result = $function($data1, $data2, $data3);
+}
+
+
+TODO: There needs to be a way to check API version without including anything, as a module may simply
+provide normal plugins and versioning could still matter.
diff --git a/sites/all/modules/ctools/help/plugins-creating.html b/sites/all/modules/ctools/help/plugins-creating.html
new file mode 100644
index 0000000..6d5b35f
--- /dev/null
+++ b/sites/all/modules/ctools/help/plugins-creating.html
@@ -0,0 +1,204 @@
+There are two primary pieces to using plugins. The first is getting the data, and the second is using the data.
+
+Defining a plugin
+To define that you offer a plugin that modules can implement, you first must implement hook_ctools_plugin_type() to tell the plugin system about your plugin.
+
+
+/**
+ * Implements hook_ctools_plugin_type() to inform CTools about the layout plugin.
+ */
+function panels_ctools_plugin_type() {
+ $plugins['layouts'] = array(
+ 'load themes' => TRUE,
+ );
+
+ return $plugins;
+}
+
+
+The following information can be specified for each plugin type:
+
+cache
+Defaults to: FALSE
+If set to TRUE, the results of ctools_get_plugins will be cached in the 'cache' table (by default), thus preventing .inc files from being loaded. ctools_get_plugins looking for a specific plugin will always load the appropriate .inc file.
+cache table
+Defaults to: 'cache'
+If 'cache' is TRUE, then this value specifies the cache table where the cached plugin information will be stored.
+classes
+Defaults to: array()
+An array of class identifiers (i.e. plugin array keys) which a plugin of this type uses to provide classes to the CTools autoloader. For example, if classes is set to array('class'), then CTools will search each $plugin['class'] for a class to autoload. Depending of the plugin structure, a class identifier may be either:
+
+- a file name:
+the file which holds the class with the name structure as: [filename].[class].php
+in this case the class name can be different than the class identifier
+- the class name:
+if the class is in the same file as the $plugin
+the plugin .inc file can have a different name than the class identifier
+
+
+ defaults
+Defaults to: array()
+An array of defaults that should be added to each plugin; this can be used to ensure that every plugin has the basic data necessary. These defaults will not ovewrite data supplied by the plugin. This could also be a function name, in which case the callback will be used to provide defaults. NOTE, however, that the callback-based approach is deprecated as it is redundant with the 'process' callback, and as such will be removed in later versions. Consequently, you should only use the array form for maximum cross-version compatibility.
+load themes
+Defaults to: FALSE
+If set to TRUE, then plugins of this type can be supplied by themes as well as modules. If this is the case, all themes that are currently enabled will provide a plugin: NOTE: Due to a slight UI bug in Drupal, it is possible for the default theme to be active but not enabled. If this is the case, that theme will NOT provide plugins, so if you are using this feature, be sure to document that issue. Also, themes set via $custom_theme do not necessarily need to be enabled, but the system has no way of knowing what those themes are, so the enabled flag is the only true method of identifying which themes can provide layouts.
+hook
+Defaults to: (dynamic value)
+The name of the hook used to collect data for this plugin. Normally this is $module . '_' . $type -- but this can be changed here. If you change this, you MUST be sure to document this for your plugin implementors as it will change the format of the specially named hook.
+ process
+Defaults to: ''
+An optional function callback to use for processing a plugin. This can be used to provide automated settings that must be calculated per-plugin instance (i.e., it is not enough to simply append an array via 'defaults'). The parameters on this callback are: callback(&$plugin, $info) where $plugin is a reference to the plugin as processed and $info is the fully processed result of hook_ctools_plugin_api_info().
+ extension
+Defaults to: 'inc'
+Can be used to change the extension on files containing plugins of this type. By default the extension will be "inc", though it will default to "info" if "info files" is set to true. Do not include the dot in the extension if changing it, that will be added automatically.
+info file
+Defaults to: FALSE
+If set to TRUE, then the plugin will look for a .info file instead of a .inc. Internally, this will look exactly the same, though obviously a .info file cannot contain functions. This can be good for styles that may not need to contain code.
+use hooks
+Defaults to: TRUE *
+Use to enable support for plugin definition hooks instead of plugin definition files. NOTE: using a central plugin definition hook is less optimal for the plugins system, and as such this will default to FALSE in later versions.
+child plugins
+Defaults to: FALSE
+If set to TRUE, the plugin type can automatically have 'child plugins' meaning each plugin can actually provide multiple plugins. This is mostly used for plugins that store some of their information in the database, such as views, blocks or exportable custom versions of plugins.
+To implement, each plugin can have a 'get child' and 'get children' callback. Both of these should be implemented for performance reasons, since it is best to avoid getting all children if necessary, but if 'get child' is not implemented, it will fall back to 'get children' if it has to.
+Child plugins should be named parent:child, with the : being the separator, so that it knows which parent plugin to ask for the child. The 'get children' method should at least return the parent plugin as part of the list, unless it wants the parent plugin itself to not be a choosable option, which is not unheard of.
+'get children' arguments are ($plugin, $parent) and 'get child' arguments are ($plugin, $parent, $child).
+
+
+In addition, there is a 'module' and 'type' settings; these are for internal use of the plugin system and you should not change these.
+Getting the data
+To create a plugin, a module only has to execute ctools_get_plugins with the right data:
+
+
+ ctools_include('plugins');
+ ctools_get_plugins($module, $type, [$id])
+
+
+In the above example, $module should be your module's name and $type is the type of the plugin. It is typically best practice to provide some kind of wrapper function to make this easier. For example, Panels provides the following functions to implement the 'content_types' plugin:
+
+
+/**
+ * Fetch metadata on a specific content_type plugin.
+ *
+ * @param $content type
+ * Name of a panel content type.
+ *
+ * @return
+ * An array with information about the requested panel content type.
+ */
+function panels_get_content_type($content_type) {
+ ctools_include('context');
+ ctools_include('plugins');
+ return ctools_get_plugins('panels', 'content_types', $content_type);
+}
+
+/**
+ * Fetch metadata for all content_type plugins.
+ *
+ * @return
+ * An array of arrays with information about all available panel content types.
+ */
+function panels_get_content_types() {
+ ctools_include('context');
+ ctools_include('plugins');
+ return ctools_get_plugins('panels', 'content_types');
+}
+
+
+Using the data
+
+Each plugin returns a packet of data, which is added to with a few defaults. Each plugin is guaranteed to always have the following data:
+
+name
+The name of the plugin. This is also the key in the array, of the full list of plugins, and is placed here since that is not always available.
+module
+The module that supplied the plugin.
+file
+The actual file containing the plugin.
+path
+The path to the file containing the plugin. This is useful for using secondary files, such as templates, css files, images, etc, that may come with a plugin.
+
+
+Any of the above items can be overridden by the plugin itself, though the most likely one to be modified is the 'path'.
+
+The most likely data (beyond simple printable data) for a plugin to provide is a callback. The plugin system provides a pair of functions to make it easy and consistent for these callbacks to be used. The first is ctools_plugin_get_function, which requires the full $plugin object.
+
+
+/**
+ * Get a function from a plugin, if it exists. If the plugin is not already
+ * loaded, try ctools_plugin_load_function() instead.
+ *
+ * @param $plugin
+ * The loaded plugin type.
+ * @param $callback_name
+ * The identifier of the function. For example, 'settings form'.
+ *
+ * @return
+ * The actual name of the function to call, or NULL if the function
+ * does not exist.
+ */
+function ctools_plugin_get_function($plugin, $callback_name)
+
+
+The second does not require the full $plugin object, and will load it:
+
+
+/**
+ * Load a plugin and get a function name from it, returning success only
+ * if the function exists.
+ *
+ * @param $module
+ * The module that owns the plugin type.
+ * @param $type
+ * The type of plugin.
+ * @param $id
+ * The id of the specific plugin to load.
+ * @param $callback_name
+ * The identifier of the function. For example, 'settings form'.
+ *
+ * @return
+ * The actual name of the function to call, or NULL if the function
+ * does not exist.
+ */
+function ctools_plugin_load_function($module, $type, $id, $callback_name) {
+
+
+Both of these functions will ensure any needed files are included. In fact, it allows each callback to specify alternative include files. The plugin implementation could include code like this:
+
+
+ 'callback_name' => 'actual_name_of_function_here',
+
+
+Or like this:
+
+ 'callback_name' => array(
+ 'file' => 'filename',
+ 'path' => 'filepath', // optional, will use plugin path if absent
+ 'function' => 'actual_name_of_function_here',
+ ),
+
+
+An example, for 'plugin_example' type
+
+
+$plugin = array(
+ 'name' => 'my_plugin',
+ 'module' => 'my_module',
+ 'example_callback' => array(
+ 'file' => 'my_plugin.extrafile.inc',
+ 'function' => 'my_module_my_plugin_example_callback',
+ ),
+);
+
+
+To utilize this callback on this plugin:
+
+
+if ($function = ctools_plugin_get_function($plugin, 'example_callback')) {
+ $function($arg1, $arg2, $etc);
+}
+
+
+Document your plugins!
+
+Since the data provided by your plugin tends to be specific to your plugin type, you really need to document what the data returned in the hook in the .inc file will be or nobody will figure it out. Use advanced help and document it there. If every system that utilizes plugins does this, then plugin implementors will quickly learn to expect the documentation to be in the advanced help.
diff --git a/sites/all/modules/ctools/help/plugins-implementing.html b/sites/all/modules/ctools/help/plugins-implementing.html
new file mode 100644
index 0000000..c95e72d
--- /dev/null
+++ b/sites/all/modules/ctools/help/plugins-implementing.html
@@ -0,0 +1,62 @@
+There are two parts to implementing a plugin: telling the system where it is, and implementing one or more .inc files that contain the plugin data.
+
+Telling the system where your plugins live
+How a module implements plugins
+To implement any plugins at all, you must implement a single function for all plugins: hook_ctools_plugin_directory . Every time a module loads plugins, this hook will be called to see which modules implement those plugins and in what directory those plugins will live.
+
+
+function hook_ctools_plugin_directory($module, $plugin) {
+ if ($module == 'panels' && $plugin == 'content_types') {
+ return 'plugins/content_types';
+ }
+}
+
+
+The directory returned should be relative to your module. Another common usage is to simply return that you implement all plugins owned by a given module (or modules):
+
+
+function hook_ctools_plugin_directory($module, $plugin) {
+ if ($module == 'panels') {
+ return 'plugins/' . $plugin;
+ }
+}
+
+
+Typically, it is recommended that all plugins be placed into the 'plugins' directory for clarity and maintainability. Inside the directory, any number of subdirectories can be used. For plugins that require extra files, such as templates, css, javascript or image files, this is highly recommended:
+
+mymodule.module
+mymodule.info
+plugins/
+ content_types/
+ my_content_type.inc
+ layouts/
+ my_layout.inc
+ my_layout.css
+ my_layout.tpl.php
+ my_layout_image.png
+
+
+How a theme implements plugins
+Themes can implement plugins if the plugin owner specified that it's possible in its hook_ctools_plugin_type() call. If so, it is generally exactly the same as modules, except for one important difference: themes don't get hook_ctools_plugin_directory(). Instead, themes add a line to their .info file:
+
+
+plugins[module][type] = directory
+
+
+How to structure the .inc file
+
+The top of the .inc file should contain an array that defines the plugin. This array is simply defined in the global namespace of the file and does not need a function. Note that previous versions of this plugin system required a specially named function. While this function will still work, its use is now discouraged, as it is annoying to name properly.
+
+This array should look something like this:
+
+
+$plugin = array(
+ 'key' => 'value',
+);
+
+
+Several values will be filled in for you automatically, but you can override them if necessary. They include 'name', 'path', 'file' and 'module'. Additionally, the plugin owner can provide other defaults as well.
+
+There are no required keys by the plugin system itself. The only requirements in the $plugin array will be defined by the plugin type.
+
+After this array, if your plugin needs functions, they can be declared. Different plugin types have different needs here, so exactly what else will be needed will change from type to type.
diff --git a/sites/all/modules/ctools/help/plugins.html b/sites/all/modules/ctools/help/plugins.html
new file mode 100644
index 0000000..1e64da0
--- /dev/null
+++ b/sites/all/modules/ctools/help/plugins.html
@@ -0,0 +1,5 @@
+The plugins tool allows a module to allow other modules (and themes!) to provide plugins which provide some kind of functionality or some kind of task. For example, in Panels there are several types of plugins: Content types (which are like blocks), layouts (which are page layouts) and styles (which can be used to style a panel). Each plugin is represented by a .inc file, and the functionality they offer can differ wildly.
+
+A module which uses plugins can implement a hook describing the plugin (which is not necessary, as defaults will be filled in) and then calls a ctools function which loads either all the known plugins (used for listing/choosing) or a specific plugin (used when it's known which plugin is needed). From the perspective of the plugin system, a plugin is a packet of data, usually some printable info and a list of callbacks. It is up to the module implementing plugins to determine what that info means and what the callbacks do.
+
+A module which implements plugins must first implement the hook_ctools_plugin_directory function, which simply tells the system which plugins are supported and what directory to look in. Each plugin will then be in a separate .inc file in the directory supplied. The .inc file will contain a specially named hook which returns the data necessary to implement the plugin.
diff --git a/sites/all/modules/ctools/help/wizard.html b/sites/all/modules/ctools/help/wizard.html
new file mode 100644
index 0000000..33fc456
--- /dev/null
+++ b/sites/all/modules/ctools/help/wizard.html
@@ -0,0 +1,311 @@
+Form wizards, or multi-step forms, are a process by which the user goes through or can use an arbitrary number of different forms to create a single object or perform a single task. Traditionally the multi-step form is difficult in Drupal because there is no easy place to put data in between forms. No longer! The form wizard tool allows a single entry point to easily set up a wizard of multiple forms, provide callbacks to handle data storage and updates between forms and when forms are completed. This tool pairs well with the object cache tool for storage.
+
+The form info array
+The wizard starts with an array of data that describes all of the forms available to the wizard and sets options for how the wizard will present and control the flow. Here is an example of the $form_info array as used in the delegator module:
+
+
+ $form_info = array(
+ 'id' => 'delegator_page',
+ 'path' => "admin/structure/pages/edit/$page_name/%step",
+ 'show trail' => TRUE,
+ 'show back' => TRUE,
+ 'show return' => FALSE,
+ 'next callback' => 'delegator_page_add_subtask_next',
+ 'finish callback' => 'delegator_page_add_subtask_finish',
+ 'return callback' => 'delegator_page_add_subtask_finish',
+ 'cancel callback' => 'delegator_page_add_subtask_cancel',
+ 'order' => array(
+ 'basic' => t('Basic settings'),
+ 'argument' => t('Argument settings'),
+ 'access' => t('Access control'),
+ 'menu' => t('Menu settings'),
+ 'multiple' => t('Task handlers'),
+ ),
+ 'forms' => array(
+ 'basic' => array(
+ 'form id' => 'delegator_page_form_basic'
+ ),
+ 'access' => array(
+ 'form id' => 'delegator_page_form_access'
+ ),
+ 'menu' => array(
+ 'form id' => 'delegator_page_form_menu'
+ ),
+ 'argument' => array(
+ 'form id' => 'delegator_page_form_argument'
+ ),
+ 'multiple' => array(
+ 'form id' => 'delegator_page_argument_form_multiple'
+ ),
+ ),
+ );
+
+
+The above array starts with an id which is used to identify the wizard in various places and a path which is needed to redirect to the next step between forms. It then creates some settings which control which pieces are displayed. In this case, it displays a form trail and a 'back' button, but not the 'return' button. Then there are the wizard callbacks which allow the wizard to act appropriately when forms are submitted. Finally it contains a list of forms and their order so that it knows which forms to use and what order to use them by default. Note that the keys in the order and list of forms match; that key is called the step and is used to identify each individual step of the wizard.
+
+Here is a full list of every item that can be in the form info array:
+
+
+id
+An id for wizard. This is used like a hook to automatically name callbacks , as well as a form step's form building function. It is also used in trail theming.
+
+path
+The path to use when redirecting between forms. %step will be replaced with the key for the form.
+
+return path
+When a form is complete, this is the path to go to. This is required if the 'return' button is shown and not using AJAX. It is also used for the 'Finish' button. If it is not present and needed, the cancel path will also be checked.
+
+cancel path
+When a form is canceled, this is the path to go to. This is required if the 'cancel' is shown and not using AJAX.
+
+show trail
+If set to TRUE, the form trail will be shown like a breadcrumb at the top of each form. Defaults to FALSE.
+
+show back
+If set to TRUE, show a back button on each form. Defaults to FALSE.
+
+show return
+If set to TRUE, show a return button. Defaults to FALSE.
+
+show cancel
+If set to TRUE, show a cancel button. Defaults to FALSE.
+
+back text
+Set the text of the 'back' button. Defaults to t('Back').
+
+next text
+Set the text of the 'next' button. Defaults to t('Continue').
+
+return text
+Set the text of the 'return' button. Defaults to t('Update and return').
+
+finish text
+Set the text of the 'finish' button. Defaults to t('Finish').
+
+cancel text
+Set the text of the 'cancel' button. Defaults to t('Cancel').
+
+ajax
+Turn on AJAX capabilities, using CTools' ajax.inc. Defaults to FALSE.
+
+modal
+Put the wizard in the modal tool. The modal must already be open and called from an ajax button for this to work, which is easily accomplished using functions provided by the modal tool.
+
+ajax render
+A callback to display the rendered form via ajax. This is not required if using the modal tool, but is required otherwise since ajax by itself does not know how to render the results. Params: &$form_state, $output.
+
+finish callback
+The function to call when a form is complete and the finish button has been clicked. This function should finalize all data. Params: &$form_state.
+Defaults to $form_info['id']._finish if function exists.
+
+
+cancel callback
+The function to call when a form is canceled by the user. This function should clean up any data that is cached. Params: &$form_state.
+Defaults to $form_info['id']._cancel if function exists.
+
+return callback
+The function to call when a form is complete and the return button has been clicked. This is often the same as the finish callback. Params: &$form_state.
+Defaults to $form_info['id']._return if function exists.
+
+next callback
+The function to call when the next button has been clicked. This function should take the submitted data and cache it for later use by the finish callback. Params: &$form_state.
+Defaults to $form_info['id']._next if function exists.
+
+order
+An optional array of forms, keyed by the step, which represents the default order the forms will be displayed in. If not set, the forms array will control the order. Note that submit callbacks can override the order so that branching logic can be used.
+
+forms
+An array of form info arrays, keyed by step, describing every form available to the wizard. If order array isn't set, the wizard will use this to set the default order. Each array contains:
+
+ form id
+
+ The id of the form, as used in the Drupal form system. This is also the name of the function that represents the form builder.
+ Defaults to $form_info['id']._.$step._form.
+
+
+ include
+ The name of a file to include which contains the code for this form. This makes it easy to include the form wizard in another file or set of files. This must be the full path of the file, so be sure to use drupal_get_path() when setting this. This can also be an array of files if multiple files need to be included.
+
+ title
+ The title of the form, to be optionally set via drupal_get_title. This is required when using the modal if $form_state['title'] is not set.
+
+
+
+
+Invoking the form wizard
+Your module should create a page callback via hook_menu, and this callback should contain an argument for the step. The path that leads to this page callback should be the same as the 'path' set in the $form_info array.
+
+The page callback should set up the $form_info, and figure out what the default step should be if no step is provided (note that the wizard does not do this for you; you MUST specify a step). Then invoke the form wizard:
+
+
+ $form_state = array();
+ ctools_include('wizard');
+ $output = ctools_wizard_multistep_form($form_info, $step, $form_state);
+
+
+If using AJAX or the modal, This part is actually done! If not, you have one more small step:
+
+
+ return $output;
+
+
+Forms and their callbacks
+Each form within the wizard is a complete, independent form using Drupal's Form API system. It has a form builder callback as well as submit and validate callbacks and can be form altered. The primary difference between these forms and a normal Drupal form is that the submit handler should not save any data. Instead, it should make any changes to a cached object (usually placed on the $form_state) and only the _finish or _return handler should actually save any real data.
+
+How you handle this is completely up to you. The recommended best practice is to use the CTools Object cache, and a good way to do this is to write a couple of wrapper functions around the cache that look like these example functions:
+
+
+/**
+ * Get the cached changes to a given task handler.
+ */
+function delegator_page_get_page_cache($name) {
+ ctools_include('object-cache');
+ $cache = ctools_object_cache_get('delegator_page', $name);
+ if (!$cache) {
+ $cache = delegator_page_load($name);
+ $cache->locked = ctools_object_cache_test('delegator_page', $name);
+ }
+
+ return $cache;
+}
+
+/**
+ * Store changes to a task handler in the object cache.
+ */
+function delegator_page_set_page_cache($name, $page) {
+ ctools_include('object-cache');
+ $cache = ctools_object_cache_set('delegator_page', $name, $page);
+}
+
+/**
+ * Remove an item from the object cache.
+ */
+function delegator_page_clear_page_cache($name) {
+ ctools_include('object-cache');
+ ctools_object_cache_clear('delegator_page', $name);
+}
+
+
+Using these wrappers, when performing a get_cache operation, it defaults to loading the real object. It then checks to see if another user has this object cached using the ctools_object_cache_test() function, which automatically sets a lock (which can be used to prevent writes later on).
+
+With this set up, the _next, _finish and _cancel callbacks are quite simple:
+
+
+/**
+ * Callback generated when the add page process is finished.
+ */
+function delegator_page_add_subtask_finish(&$form_state) {
+ $page = &$form_state['page'];
+
+ // Create a real object from the cache
+ delegator_page_save($page);
+
+ // Clear the cache
+ delegator_page_clear_page_cache($form_state['cache name']);
+}
+
+/**
+ * Callback generated when the 'next' button is clicked.
+ *
+ * All we do here is store the cache.
+ */
+function delegator_page_add_subtask_next(&$form_state) {
+ // Update the cache with changes.
+ delegator_page_set_page_cache($form_state['cache name'], $form_state['page']);
+}
+
+/**
+ * Callback generated when the 'cancel' button is clicked.
+ *
+ * All we do here is clear the cache.
+ */
+function delegator_page_add_subtask_cancel(&$form_state) {
+ // Update the cache with changes.
+ delegator_page_clear_page_cache($form_state['cache name']);
+}
+
+
+All that's needed to tie this together is to understand how the changes made it into the cache in the first place. This happened in the various form _submit handlers, which made changes to $form_state['page'] based upon the values set in the form:
+
+
+/**
+ * Store the values from the basic settings form.
+ */
+function delegator_page_form_basic_submit($form, &$form_state) {
+ if (!isset($form_state['page']->pid) && empty($form_state['page']->import)) {
+ $form_state['page']->name = $form_state['values']['name'];
+ }
+
+ $form_state['page']->admin_title = $form_state['values']['admin_title'];
+ $form_state['page']->path = $form_state['values']['path'];
+
+ return $form;
+}
+
+
+No database operations were made during this _submit, and that's a very important distinction about this system.
+
+Proper handling of back button
+When using 'show back' => TRUE the cached data should be assigned to the #default_value form property. Otherwise when the user goes back to the previous step the forms default values instead of his (cached) input is used.
+
+
+/**
+ * Form builder function for wizard.
+ */
+function wizardid_step2_form($form, &$form_state) {
+ $form_state['my data'] = my_module_get_cache($form_state['cache name']);
+ $form['example'] = array(
+ '#type' => 'radios',
+ '#title' => t('Title'),
+ '#default_value' => $form_state['my data']->example ? $form_state['my data']->example : default,
+ '#options' => array(
+ 'default' => t('Default'),
+ 'setting1' => t('Setting1'),
+ ),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler to prepare needed values for storing in cache.
+ */
+function wizardid_step2_form_submit($form, &$form_state) {
+ $form_state['my data']->example = $form_state['values']['example'];
+}
+
+
+The data is stored in the my data object on submitting. If the user goes back to this step the cached my data is used as the default form value. The function my_module_get_cache() is like the cache functions explained above.
+
+Required fields, cancel and back buttons
+If you have required fields in your forms, the back and cancel buttons will not work as expected since validation of the form will fail. You can add the following code to the top of your form validation to avoid this problem:
+
+/**
+ * Validation handler for step2 form
+ */
+function wizardid_step2_form_validate(&$form, &$form_state) {
+ // if the clicked button is anything but the normal flow
+ if ($form_state['clicked_button']['#next'] != $form_state['next']) {
+ drupal_get_messages('error');
+ form_set_error(NULL, '', TRUE);
+ return;
+ }
+ // you form validation goes here
+ // ...
+}
+
+
+Wizard for anonymous users
+If you are creating a wizard which is be used by anonymous users, you might run into some issues with drupal's caching for anonymous users. You can circumvent this by using hook_init and telling drupal to not cache your wizard pages:
+
+/**
+ * Implementation of hook init
+ */
+function mymodule_init() {
+ // if the path leads to the wizard
+ if (drupal_match_path($_GET['q'], 'path/to/your/wizard/*')) {
+ // set cache to false
+ $GLOBALS['conf']['cache'] = FALSE;
+ }
+}
+
diff --git a/sites/all/modules/ctools/images/arrow-active.png b/sites/all/modules/ctools/images/arrow-active.png
new file mode 100644
index 0000000..3bbd3c2
Binary files /dev/null and b/sites/all/modules/ctools/images/arrow-active.png differ
diff --git a/sites/all/modules/ctools/images/collapsible-collapsed.png b/sites/all/modules/ctools/images/collapsible-collapsed.png
new file mode 100644
index 0000000..95a214a
Binary files /dev/null and b/sites/all/modules/ctools/images/collapsible-collapsed.png differ
diff --git a/sites/all/modules/ctools/images/collapsible-expanded.png b/sites/all/modules/ctools/images/collapsible-expanded.png
new file mode 100644
index 0000000..46f39ec
Binary files /dev/null and b/sites/all/modules/ctools/images/collapsible-expanded.png differ
diff --git a/sites/all/modules/ctools/images/expanded-options.png b/sites/all/modules/ctools/images/expanded-options.png
new file mode 100644
index 0000000..b7b755c
Binary files /dev/null and b/sites/all/modules/ctools/images/expanded-options.png differ
diff --git a/sites/all/modules/ctools/images/icon-close-window.png b/sites/all/modules/ctools/images/icon-close-window.png
new file mode 100644
index 0000000..5f0cf69
Binary files /dev/null and b/sites/all/modules/ctools/images/icon-close-window.png differ
diff --git a/sites/all/modules/ctools/images/icon-configure.png b/sites/all/modules/ctools/images/icon-configure.png
new file mode 100644
index 0000000..e23d67c
Binary files /dev/null and b/sites/all/modules/ctools/images/icon-configure.png differ
diff --git a/sites/all/modules/ctools/images/icon-delete.png b/sites/all/modules/ctools/images/icon-delete.png
new file mode 100644
index 0000000..5f0cf69
Binary files /dev/null and b/sites/all/modules/ctools/images/icon-delete.png differ
diff --git a/sites/all/modules/ctools/images/no-icon.png b/sites/all/modules/ctools/images/no-icon.png
new file mode 100644
index 0000000..fa78ec1
Binary files /dev/null and b/sites/all/modules/ctools/images/no-icon.png differ
diff --git a/sites/all/modules/ctools/images/status-active.gif b/sites/all/modules/ctools/images/status-active.gif
new file mode 100644
index 0000000..207e95c
Binary files /dev/null and b/sites/all/modules/ctools/images/status-active.gif differ
diff --git a/sites/all/modules/ctools/images/throbber.gif b/sites/all/modules/ctools/images/throbber.gif
new file mode 100644
index 0000000..8a084b8
Binary files /dev/null and b/sites/all/modules/ctools/images/throbber.gif differ
diff --git a/sites/all/modules/ctools/includes/action-links.theme.inc b/sites/all/modules/ctools/includes/action-links.theme.inc
new file mode 100644
index 0000000..f53f59c
--- /dev/null
+++ b/sites/all/modules/ctools/includes/action-links.theme.inc
@@ -0,0 +1,34 @@
+ 'links',
+ 'file' => 'includes/action-links.theme.inc',
+ );
+}
+
+/**
+ * Render a menu local actions wrapper.
+ *
+ * @param $links
+ * Local actions links.
+ * @param $attributes
+ * An array of attributes to append to the wrapper.
+ */
+function theme_ctools_menu_local_actions_wrapper($variables) {
+ $links = drupal_render($variables['links']);
+
+ if (empty($links)) {
+ return;
+ }
+
+ return '';
+}
diff --git a/sites/all/modules/ctools/includes/ajax.inc b/sites/all/modules/ctools/includes/ajax.inc
new file mode 100644
index 0000000..4d72d0c
--- /dev/null
+++ b/sites/all/modules/ctools/includes/ajax.inc
@@ -0,0 +1,160 @@
+ $image)), $dest, $alt, $class);
+}
+
+/**
+ * Render text as a link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
+ * not use user input so this is a very minor concern.
+ *
+ * @param $text
+ * The text that will be displayed as the link.
+ * @param $dest
+ * The destination of the link.
+ * @param $alt
+ * The alt text of the link.
+ * @param $class
+ * Any class to apply to the link. @todo this should be a options array.
+ * @param $type
+ * A type to use, in case a different behavior should be attached. Defaults
+ * to ctools-use-ajax.
+ */
+function ctools_ajax_text_button($text, $dest, $alt, $class = '', $type = 'use-ajax') {
+ drupal_add_library('system', 'drupal.ajax');
+ return l($text, $dest, array('html' => TRUE, 'attributes' => array('class' => array($type, $class), 'title' => $alt)));
+}
+
+/**
+ * Render an icon and related text as a link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
+ * not use user input so this is a very minor concern.
+ *
+ * @param $text
+ * The text that will be displayed as the link.
+ * @param $image
+ * The icon image to include in the link.
+ * @param $dest
+ * The destination of the link.
+ * @param $alt
+ * The title text of the link.
+ * @param $class
+ * Any class to apply to the link. @todo this should be a options array.
+ * @param $type
+ * A type to use, in case a different behavior should be attached. Defaults
+ * to ctools-use-ajax.
+ */
+function ctools_ajax_icon_text_button($text, $image, $dest, $alt, $class = '', $type = 'use-ajax') {
+ drupal_add_library('system', 'drupal.ajax');
+ $rendered_image = theme('image', array('path' => $image));
+ $link_content = $rendered_image . "" . $text . " ";
+ return l($link_content, $dest, array('html' => TRUE, 'attributes' => array('class' => array($type, $class), 'title' => $alt)));
+}
+
+/**
+ * Set a single property to a value, on all matched elements.
+ *
+ * @param $selector
+ * The CSS selector. This can be any selector jquery uses in $().
+ * @param $name
+ * The name or key: of the data attached to this selector.
+ * @param $value
+ * The value of the data.
+ */
+function ctools_ajax_command_attr($selector, $name, $value) {
+ ctools_add_js('ajax-responder');
+ return array(
+ 'command' => 'attr',
+ 'selector' => $selector,
+ 'name' => $name,
+ 'value' => $value,
+ );
+}
+
+/**
+ * Force a client-side redirect.
+ *
+ * @param $url
+ * The url to be redirected to. This can be an absolute URL or a
+ * Drupal path.
+ * @param $delay
+ * A delay before applying the redirection, in milliseconds.
+ * @param $options
+ * An array of options to pass to the url() function.
+ */
+function ctools_ajax_command_redirect($url, $delay = 0, $options = array()) {
+ ctools_add_js('ajax-responder');
+ return array(
+ 'command' => 'redirect',
+ 'url' => url($url, $options),
+ 'delay' => $delay,
+ );
+}
+
+/**
+ * Force a reload of the current page.
+ */
+function ctools_ajax_command_reload() {
+ ctools_add_js('ajax-responder');
+ return array(
+ 'command' => 'reload',
+ );
+}
+
+/**
+ * Submit a form.
+ *
+ * This is useful for submitting a parent form after a child form has finished
+ * processing in a modal overlay.
+ *
+ * @param $selector
+ * The CSS selector to identify the form for submission. This can be any
+ * selector jquery uses in $().
+ */
+function ctools_ajax_command_submit($selector) {
+ ctools_add_js('ajax-responder');
+ return array(
+ 'command' => 'submit',
+ 'selector' => $selector,
+ );
+}
+
+/**
+ * Send an error response back via AJAX and immediately exit.
+ */
+function ctools_ajax_render_error($error = '') {
+ $commands = array();
+ $commands[] = ajax_command_alert($error);
+ print ajax_render($commands);
+ exit;
+}
diff --git a/sites/all/modules/ctools/includes/cache.inc b/sites/all/modules/ctools/includes/cache.inc
new file mode 100644
index 0000000..cace800
--- /dev/null
+++ b/sites/all/modules/ctools/includes/cache.inc
@@ -0,0 +1,162 @@
+ TRUE,
+ 'ignore words' => array(),
+ 'separator' => '-',
+ 'replacements' => array(),
+ 'transliterate' => FALSE,
+ 'reduce ascii' => TRUE,
+ 'max length' => FALSE,
+ 'lower case' => FALSE,
+ );
+
+ // Allow modules to make other changes to the settings.
+ if (isset($settings['clean id'])) {
+ drupal_alter('ctools_cleanstring_' . $settings['clean id'], $settings);
+ }
+
+ drupal_alter('ctools_cleanstring', $settings);
+
+ $output = $string;
+
+ // Do any replacements the user selected up front.
+ if (!empty($settings['replacements'])) {
+ $output = strtr($output, $settings['replacements']);
+ }
+
+ // Remove slashes if instructed to do so.
+ if ($settings['clean slash']) {
+ $output = str_replace('/', '', $output);
+ }
+
+ if (!empty($settings['transliterate']) && module_exists('transliteration')) {
+ $output = transliteration_get($output);
+ }
+
+ // Reduce to the subset of ASCII96 letters and numbers.
+ if ($settings['reduce ascii']) {
+ $pattern = '/[^a-zA-Z0-9\/]+/';
+ $output = preg_replace($pattern, $settings['separator'], $output);
+ }
+
+ // Get rid of words that are on the ignore list.
+ if (!empty($settings['ignore words'])) {
+ $ignore_re = '\b' . preg_replace('/,/', '\b|\b', $settings['ignore words']) . '\b';
+
+ if (function_exists('mb_eregi_replace')) {
+ $output = mb_eregi_replace($ignore_re, '', $output);
+ }
+ else {
+ $output = preg_replace("/$ignore_re/i", '', $output);
+ }
+ }
+
+ // Always replace whitespace with the separator.
+ $output = preg_replace('/\s+/', $settings['separator'], $output);
+
+ // In preparation for pattern matching,
+ // escape the separator if and only if it is not alphanumeric.
+ if (isset($settings['separator'])) {
+ if (preg_match('/^[^' . CTOOLS_PREG_CLASS_ALNUM . ']+$/uD', $settings['separator'])) {
+ $seppattern = $settings['separator'];
+ }
+ else {
+ $seppattern = '\\' . $settings['separator'];
+ }
+ // Trim any leading or trailing separators (note the need to.
+ $output = preg_replace("/^$seppattern+|$seppattern+$/", '', $output);
+
+ // Replace multiple separators with a single one.
+ $output = preg_replace("/$seppattern+/", $settings['separator'], $output);
+ }
+
+ // Enforce the maximum component length.
+ if (!empty($settings['max length'])) {
+ $output = ctools_cleanstring_truncate($output, $settings['max length'], $settings['separator']);
+ }
+
+ if (!empty($settings['lower case'])) {
+ $output = drupal_strtolower($output);
+ }
+ return $output;
+}
+
+/**
+ * A friendly version of truncate_utf8.
+ *
+ * @param $string
+ * The string to be truncated.
+ * @param $length
+ * An integer for the maximum desired length.
+ * @param $separator
+ * A string which contains the word boundary such as - or _.
+ *
+ * @return
+ * The string truncated below the maxlength.
+ */
+function ctools_cleanstring_truncate($string, $length, $separator) {
+ if (drupal_strlen($string) > $length) {
+ // Leave one more character.
+ $string = drupal_substr($string, 0, $length + 1);
+ // Space exists AND is not on position 0.
+ if ($last_break = strrpos($string, $separator)) {
+ $string = substr($string, 0, $last_break);
+ }
+ else {
+ $string = drupal_substr($string, 0, $length);
+ }
+ }
+ return $string;
+}
diff --git a/sites/all/modules/ctools/includes/collapsible.theme.inc b/sites/all/modules/ctools/includes/collapsible.theme.inc
new file mode 100644
index 0000000..81df4bc
--- /dev/null
+++ b/sites/all/modules/ctools/includes/collapsible.theme.inc
@@ -0,0 +1,78 @@
+ array('handle' => NULL, 'content' => NULL, 'collapsed' => FALSE),
+ 'file' => 'includes/collapsible.theme.inc',
+ );
+ $items['ctools_collapsible_remembered'] = array(
+ 'variables' => array('id' => NULL, 'handle' => NULL, 'content' => NULL, 'collapsed' => FALSE),
+ 'file' => 'includes/collapsible.theme.inc',
+ );
+}
+
+/**
+ * Render a collapsible div.
+ *
+ * @param $handle
+ * Text to put in the handle/title area of the div.
+ * @param $content
+ * Text to put in the content area of the div, this is what will get
+ * collapsed.
+ * @param $collapsed
+ * If true, this div will start out collapsed.
+ */
+function theme_ctools_collapsible($vars) {
+ ctools_add_js('collapsible-div');
+ ctools_add_css('collapsible-div');
+
+ $class = $vars['collapsed'] ? ' ctools-collapsed' : '';
+ $output = '';
+
+ return $output;
+}
+
+/**
+ * Render a collapsible div whose state will be remembered.
+ *
+ * @param $id
+ * The CSS id of the container. This is required.
+ * @param $handle
+ * Text to put in the handle/title area of the div.
+ * @param $content
+ * Text to put in the content area of the div, this is what will get
+ * collapsed.
+ * @param $collapsed
+ * If true, this div will start out collapsed.
+ */
+function theme_ctools_collapsible_remembered($vars) {
+ $id = $vars['id'];
+ $handle = $vars['handle'];
+ $content = $vars['content'];
+ $collapsed = $vars['collapsed'];
+ ctools_add_js('collapsible-div');
+ ctools_add_css('collapsible-div');
+
+ $class = $collapsed ? ' ctools-collapsed' : '';
+ $output = '';
+
+ return $output;
+}
diff --git a/sites/all/modules/ctools/includes/content.inc b/sites/all/modules/ctools/includes/content.inc
new file mode 100644
index 0000000..49c3565
--- /dev/null
+++ b/sites/all/modules/ctools/includes/content.inc
@@ -0,0 +1,865 @@
+ $plugin['title'],
+ 'description' => $plugin['description'],
+ 'icon' => ctools_content_admin_icon($plugin),
+ 'category' => $plugin['category'],
+ );
+
+ if (isset($plugin['required context'])) {
+ $type['required context'] = $plugin['required context'];
+ }
+ if (isset($plugin['top level'])) {
+ $type['top level'] = $plugin['top level'];
+ }
+ $plugin['content types'] = array($plugin['name'] => $type);
+ if (!isset($plugin['single'])) {
+ $plugin['single'] = TRUE;
+ }
+ }
+ }
+}
+
+/**
+ * Fetch metadata on a specific content_type plugin.
+ *
+ * @param mixed $content
+ * Name of a panel content type.
+ *
+ * @return
+ * An array with information about the requested panel content type.
+ */
+function ctools_get_content_type($content_type) {
+ ctools_include('context');
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'content_types', $content_type);
+}
+
+/**
+ * Fetch metadata for all content_type plugins.
+ *
+ * @return
+ * An array of arrays with information about all available panel content types.
+ */
+function ctools_get_content_types() {
+ ctools_include('context');
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'content_types');
+}
+
+/**
+ * Get all of the individual subtypes provided by a given content type. This
+ * would be all of the blocks for the block type, or all of the views for
+ * the view type.
+ *
+ * @param $type
+ * The content type to load.
+ *
+ * @return
+ * An array of all subtypes available.
+ */
+function ctools_content_get_subtypes($type) {
+ static $cache = array();
+
+ $subtypes = array();
+
+ if (is_array($type)) {
+ $plugin = $type;
+ }
+ else {
+ $plugin = ctools_get_content_type($type);
+ }
+
+ if (empty($plugin) || empty($plugin['name'])) {
+ return;
+ }
+
+ if (isset($cache[$plugin['name']])) {
+ return $cache[$plugin['name']];
+ }
+
+ if (isset($plugin['content types'])) {
+ $function = $plugin['content types'];
+ if (is_array($function)) {
+ $subtypes = $function;
+ }
+ elseif (function_exists($function)) {
+ // Cast to array to prevent errors from non-array returns.
+ $subtypes = (array) $function($plugin);
+ }
+ }
+
+ // Walk through the subtypes and ensure minimal settings are
+ // retained.
+ foreach ($subtypes as $id => $subtype) {
+ // Ensure that the 'subtype_id' value exists.
+ if (!isset($subtype['subtype_id'])) {
+ $subtypes[$id]['subtype_id'] = $id;
+ }
+
+ // Use exact name since this is a modify by reference.
+ ctools_content_prepare_subtype($subtypes[$id], $plugin);
+ }
+
+ $cache[$plugin['name']] = $subtypes;
+
+ return $subtypes;
+}
+
+/**
+ * Given a content type and a subtype id, return the information about that
+ * content subtype.
+ *
+ * @param $type
+ * The content type being fetched.
+ * @param $subtype_id
+ * The id of the subtype being fetched.
+ *
+ * @return
+ * An array of information describing the content subtype.
+ */
+function ctools_content_get_subtype($type, $subtype_id) {
+ $subtype = array();
+ if (is_array($type)) {
+ $plugin = $type;
+ }
+ else {
+ $plugin = ctools_get_content_type($type);
+ }
+
+ $function = ctools_plugin_get_function($plugin, 'content type');
+ if ($function) {
+ $subtype = $function($subtype_id, $plugin);
+ }
+ else {
+ $subtypes = ctools_content_get_subtypes($type);
+ if (isset($subtypes[$subtype_id])) {
+ $subtype = $subtypes[$subtype_id];
+ }
+ // If there's only 1 and we somehow have the wrong subtype ID, do not
+ // care. Return the proper subtype anyway.
+ if (empty($subtype) && !empty($plugin['single'])) {
+ $subtype = current($subtypes);
+ }
+ }
+
+ if ($subtype) {
+ // Ensure that the 'subtype_id' value exists. This is also done in
+ // ctools_content_get_subtypes(), but it wouldn't be called if the plugin
+ // provides the subtype through its own function.
+ if (!isset($subtype['subtype_id'])) {
+ $subtype['subtype_id'] = $subtype_id;
+ }
+
+ ctools_content_prepare_subtype($subtype, $plugin);
+ }
+ return $subtype;
+}
+
+/**
+ * Ensure minimal required settings on a content subtype exist.
+ */
+function ctools_content_prepare_subtype(&$subtype, $plugin) {
+ foreach (array('path', 'js', 'css') as $key) {
+ if (!isset($subtype[$key]) && isset($plugin[$key])) {
+ $subtype[$key] = $plugin[$key];
+ }
+ }
+
+ // Trigger hook_ctools_content_subtype_alter().
+ drupal_alter('ctools_content_subtype', $subtype, $plugin);
+}
+
+/**
+ * Get the content from a given content type.
+ *
+ * @param $type
+ * The content type. May be the name or an already loaded content type plugin.
+ * @param $subtype
+ * The name of the subtype being rendered.
+ * @param $conf
+ * The configuration for the content type.
+ * @param $keywords
+ * An array of replacement keywords that come from outside contexts.
+ * @param $args
+ * The arguments provided to the owner of the content type. Some content may
+ * wish to configure itself based on the arguments the panel or dashboard
+ * received.
+ * @param $context
+ * An array of context objects available for use.
+ * @param $incoming_content
+ * Any incoming content, if this display is a wrapper.
+ *
+ * @return
+ * The content as rendered by the plugin, or NULL.
+ * This content should be an object with the following possible properties:
+ * - title: The safe to render title of the content.
+ * - title_heading: The title heading.
+ * - content: The safe to render HTML content.
+ * - links: An array of links associated with the content suitable for
+ * theme('links').
+ * - more: An optional 'more' link (destination only)
+ * - admin_links: Administrative links associated with the content, suitable
+ * for theme('links').
+ * - feeds: An array of feed icons or links associated with the content.
+ * Each member of the array is rendered HTML.
+ * - type: The content type.
+ * - subtype: The content subtype. These two may be used together as
+ * module-delta for block style rendering.
+ */
+function ctools_content_render($type, $subtype, $conf, $keywords = array(), $args = array(), $context = array(), $incoming_content = '') {
+ if (is_array($type)) {
+ $plugin = $type;
+ }
+ else {
+ $plugin = ctools_get_content_type($type);
+ }
+
+ $subtype_info = ctools_content_get_subtype($plugin, $subtype);
+
+ $function = ctools_plugin_get_function($subtype_info, 'render callback');
+ if (!$function) {
+ $function = ctools_plugin_get_function($plugin, 'render callback');
+ }
+
+ if ($function) {
+ $pane_context = ctools_content_select_context($plugin, $subtype, $conf, $context);
+ if ($pane_context === FALSE) {
+ return;
+ }
+
+ $content = $function($subtype, $conf, $args, $pane_context, $incoming_content);
+
+ if (empty($content)) {
+ return;
+ }
+
+ // Set up some defaults and other massaging on the content before we hand
+ // it back to the caller.
+ if (!isset($content->type)) {
+ $content->type = $plugin['name'];
+ }
+
+ if (!isset($content->subtype)) {
+ $content->subtype = $subtype;
+ }
+
+ // Override the title if configured to.
+ if (!empty($conf['override_title'])) {
+ // Give previous title as an available substitution here.
+ $keywords['%title'] = empty($content->title) ? '' : $content->title;
+ $content->original_title = $keywords['%title'];
+ $content->title = $conf['override_title_text'];
+ $content->title_heading = isset($conf['override_title_heading']) ? $conf['override_title_heading'] : 'h2';
+ }
+
+ if (!empty($content->title)) {
+ // Perform substitutions.
+ if (!empty($keywords) || !empty($context)) {
+ $content->title = ctools_context_keyword_substitute($content->title, $keywords, $context);
+ }
+
+ // Sterilize the title.
+ $content->title = filter_xss_admin($content->title);
+
+ // If a link is specified, populate.
+ if (!empty($content->title_link)) {
+ if (!is_array($content->title_link)) {
+ $url = array('href' => $content->title_link);
+ }
+ else {
+ $url = $content->title_link;
+ }
+ // Set defaults so we don't bring up notices.
+ $url += array('href' => '', 'attributes' => array(), 'query' => array(), 'fragment' => '', 'absolute' => NULL, 'html' => TRUE);
+ $content->title = l($content->title, $url['href'], $url);
+ }
+ }
+
+ return $content;
+ }
+}
+
+/**
+ * Determine if a content type can be edited or not.
+ *
+ * Some content types simply have their content and no options. This function
+ * lets a UI determine if it should display an edit link or not.
+ */
+function ctools_content_editable($type, $subtype, $conf) {
+ if (empty($type['edit form']) && empty($subtype['edit form'])) {
+ return FALSE;
+ }
+
+ $function = FALSE;
+
+ if (!empty($subtype['check editable'])) {
+ $function = ctools_plugin_get_function($subtype, 'check editable');
+ }
+ elseif (!empty($type['check editable'])) {
+ $function = ctools_plugin_get_function($type, 'check editable');
+ }
+
+ if ($function) {
+ return $function($type, $subtype, $conf);
+ }
+
+ return TRUE;
+}
+
+/**
+ * Get the administrative title from a given content type.
+ *
+ * @param $type
+ * The content type. May be the name or an already loaded content type object.
+ * @param $subtype
+ * The subtype being rendered.
+ * @param $conf
+ * The configuration for the content type.
+ * @param $context
+ * An array of context objects available for use. These may be placeholders.
+ */
+function ctools_content_admin_title($type, $subtype, $conf, $context = NULL) {
+ if (is_array($type)) {
+ $plugin = $type;
+ }
+ elseif (is_string($type)) {
+ $plugin = ctools_get_content_type($type);
+ }
+ else {
+ return;
+ }
+
+ if ($function = ctools_plugin_get_function($plugin, 'admin title')) {
+ $pane_context = ctools_content_select_context($plugin, $subtype, $conf, $context);
+ if ($pane_context === FALSE) {
+ if ($plugin['name'] == $subtype) {
+ return t('@type will not display due to missing context', array('@type' => $plugin['name']));
+ }
+ return t('@type:@subtype will not display due to missing context', array('@type' => $plugin['name'], '@subtype' => $subtype));
+ }
+
+ return $function($subtype, $conf, $pane_context);
+ }
+ elseif (isset($plugin['admin title'])) {
+ return $plugin['admin title'];
+ }
+ elseif (isset($plugin['title'])) {
+ return $plugin['title'];
+ }
+}
+
+/**
+ * Get the proper icon path to use, falling back to default icons if no icon exists.
+ *
+ * $subtype
+ * The loaded subtype info.
+ */
+function ctools_content_admin_icon($subtype) {
+ $icon = '';
+
+ if (isset($subtype['icon'])) {
+ $icon = $subtype['icon'];
+ if (!file_exists($icon)) {
+ $icon = $subtype['path'] . '/' . $icon;
+ }
+ }
+
+ if (empty($icon) || !file_exists($icon)) {
+ $icon = ctools_image_path('no-icon.png');
+ }
+
+ return $icon;
+}
+
+/**
+ * Set up the default $conf for a new instance of a content type.
+ */
+function ctools_content_get_defaults($plugin, $subtype) {
+ if (isset($plugin['defaults'])) {
+ $defaults = $plugin['defaults'];
+ }
+ elseif (isset($subtype['defaults'])) {
+ $defaults = $subtype['defaults'];
+ }
+ if (isset($defaults)) {
+ if (is_string($defaults) && function_exists($defaults)) {
+ if ($return = $defaults($pane)) {
+ return $return;
+ }
+ }
+ elseif (is_array($defaults)) {
+ return $defaults;
+ }
+ }
+
+ return array();
+}
+
+/**
+ * Get the administrative title from a given content type.
+ *
+ * @param $type
+ * The content type. May be the name or an already loaded content type object.
+ * @param $subtype
+ * The subtype being rendered.
+ * @param $conf
+ * The configuration for the content type.
+ * @param $context
+ * An array of context objects available for use. These may be placeholders.
+ */
+function ctools_content_admin_info($type, $subtype, $conf, $context = NULL) {
+ if (is_array($type)) {
+ $plugin = $type;
+ }
+ else {
+ $plugin = ctools_get_content_type($type);
+ }
+
+ if ($function = ctools_plugin_get_function($plugin, 'admin info')) {
+ $output = $function($subtype, $conf, $context);
+ }
+
+ if (empty($output) || !is_object($output)) {
+ $output = new stdClass();
+ // Replace the _ with " " for a better output.
+ $subtype = check_plain(str_replace("_", " ", $subtype));
+ $output->title = $subtype;
+ $output->content = t('No info available.');
+ }
+ return $output;
+}
+
+/**
+ * Add the default FAPI elements to the content type configuration form.
+ */
+function ctools_content_configure_form_defaults($form, &$form_state) {
+ $plugin = $form_state['plugin'];
+ $subtype = $form_state['subtype'];
+ $contexts = isset($form_state['contexts']) ? $form_state['contexts'] : NULL;
+ $conf = $form_state['conf'];
+
+ $add_submit = FALSE;
+ if (!empty($subtype['required context']) && is_array($contexts)) {
+ $form['context'] = ctools_context_selector($contexts, $subtype['required context'], isset($conf['context']) ? $conf['context'] : array());
+ $add_submit = TRUE;
+ }
+
+ ctools_include('dependent');
+
+ // Unless we're not allowed to override the title on this content type, add this
+ // gadget to all panes.
+ if (empty($plugin['no title override']) && empty($subtype['no title override'])) {
+ $form['aligner_start'] = array(
+ '#markup' => '',
+ );
+ $form['override_title'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => isset($conf['override_title']) ? $conf['override_title'] : '',
+ '#title' => t('Override title'),
+ '#id' => 'override-title-checkbox',
+ );
+ $form['override_title_text'] = array(
+ '#type' => 'textfield',
+ '#default_value' => isset($conf['override_title_text']) ? $conf['override_title_text'] : '',
+ '#size' => 35,
+ '#id' => 'override-title-textfield',
+ '#dependency' => array('override-title-checkbox' => array(1)),
+ '#dependency_type' => 'hidden',
+ );
+ $form['override_title_heading'] = array(
+ '#type' => 'select',
+ '#default_value' => isset($conf['override_title_heading']) ? $conf['override_title_heading'] : 'h2',
+ '#options' => array(
+ 'h1' => t('h1'),
+ 'h2' => t('h2'),
+ 'h3' => t('h3'),
+ 'h4' => t('h4'),
+ 'h5' => t('h5'),
+ 'h6' => t('h6'),
+ 'div' => t('div'),
+ 'span' => t('span'),
+ ),
+ '#id' => 'override-title-heading',
+ '#dependency' => array('override-title-checkbox' => array(1)),
+ '#dependency_type' => 'hidden',
+ );
+
+ $form['aligner_stop'] = array(
+ '#markup' => '
',
+ );
+ if (is_array($contexts)) {
+ $form['override_title_markup'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#markup' => t('You may use %keywords from contexts, as well as %title to contain the original title.'),
+ );
+ }
+ $add_submit = TRUE;
+ }
+
+ if ($add_submit) {
+ // '#submit' is already set up due to the wizard.
+ $form['#submit'][] = 'ctools_content_configure_form_defaults_submit';
+ }
+ return $form;
+}
+
+/**
+ * Submit handler to store context/title override info.
+ */
+function ctools_content_configure_form_defaults_submit(&$form, &$form_state) {
+ if (isset($form_state['values']['context'])) {
+ $form_state['conf']['context'] = $form_state['values']['context'];
+ }
+ if (isset($form_state['values']['override_title'])) {
+ $form_state['conf']['override_title'] = $form_state['values']['override_title'];
+ $form_state['conf']['override_title_text'] = $form_state['values']['override_title_text'];
+ $form_state['conf']['override_title_heading'] = $form_state['values']['override_title_heading'];
+ }
+}
+
+/**
+ * Get the config form.
+ *
+ * The $form_info and $form_state need to be preconfigured with data you'll need
+ * such as whether or not you're using ajax, or the modal. $form_info will need
+ * your next/submit callbacks so that you can cache your data appropriately.
+ *
+ * @return
+ * If this function returns false, no form exists.
+ */
+function ctools_content_form($op, $form_info, &$form_state, $plugin, $subtype_name, $subtype, &$conf, $step = NULL) {
+ $form_state += array(
+ 'plugin' => $plugin,
+ 'subtype' => $subtype,
+ 'subtype_name' => $subtype_name,
+ 'conf' => &$conf,
+ 'op' => $op,
+ );
+
+ $form_info += array(
+ 'id' => 'ctools_content_form',
+ 'show back' => TRUE,
+ );
+
+ // Turn the forms defined in the plugin into the format the wizard needs.
+ if ($op == 'add') {
+ if (!empty($subtype['add form'])) {
+ _ctools_content_create_form_info($form_info, $subtype['add form'], $subtype, $subtype, $op);
+ }
+ elseif (!empty($plugin['add form'])) {
+ _ctools_content_create_form_info($form_info, $plugin['add form'], $plugin, $subtype, $op);
+ }
+ }
+
+ if (empty($form_info['order'])) {
+ // Use the edit form for the add form if add form was completely left off.
+ if (!empty($subtype['edit form'])) {
+ _ctools_content_create_form_info($form_info, $subtype['edit form'], $subtype, $subtype, $op);
+ }
+ elseif (!empty($plugin['edit form'])) {
+ _ctools_content_create_form_info($form_info, $plugin['edit form'], $plugin, $subtype, $op);
+ }
+ }
+
+ if (empty($form_info['order'])) {
+ return FALSE;
+ }
+
+ ctools_include('wizard');
+ return ctools_wizard_multistep_form($form_info, $step, $form_state);
+
+}
+
+function _ctools_content_create_form_info(&$form_info, $info, $plugin, $subtype, $op) {
+ if (is_string($info)) {
+ if (empty($subtype['title'])) {
+ $title = t('Configure');
+ }
+ elseif ($op == 'add') {
+ $title = t('Configure new !subtype_title', array('!subtype_title' => $subtype['title']));
+ }
+ else {
+ $title = t('Configure !subtype_title', array('!subtype_title' => $subtype['title']));
+ }
+ $form_info['order'] = array('form' => $title);
+ $form_info['forms'] = array(
+ 'form' => array(
+ 'title' => $title,
+ 'form id' => $info,
+ 'wrapper' => 'ctools_content_configure_form_defaults',
+ ),
+ );
+ }
+ elseif (is_array($info)) {
+ $form_info['order'] = array();
+ $form_info['forms'] = array();
+ $count = 0;
+ $base = 'step';
+ $wrapper = NULL;
+ foreach ($info as $form_id => $title) {
+ // @todo -- docs say %title can be used to sub for the admin title.
+ $step = $base . ++$count;
+ if (empty($wrapper)) {
+ $wrapper = $step;
+ }
+
+ if (is_array($title)) {
+ if (!empty($title['default'])) {
+ $wrapper = $step;
+ }
+ $title = $title['title'];
+ }
+
+ $form_info['order'][$step] = $title;
+ $form_info['forms'][$step] = array(
+ 'title' => $title,
+ 'form id' => $form_id,
+ );
+ }
+ if ($wrapper) {
+ $form_info['forms'][$wrapper]['wrapper'] = 'ctools_content_configure_form_defaults';
+ }
+ }
+}
+
+/**
+ * Get an array of all available content types that can be fed into the
+ * display editor for the add content list.
+ *
+ * @param $context
+ * If a context is provided, content that requires that context can apepar.
+ * @param $has_content
+ * Whether or not the display will have incoming content
+ * @param $allowed_types
+ * An array of allowed content types (pane types) keyed by content_type . '-' . sub_type
+ * @param $default_types
+ * A default allowed/denied status for content that isn't known about
+ */
+function ctools_content_get_available_types($contexts = NULL, $has_content = FALSE, $allowed_types = NULL, $default_types = NULL) {
+ $plugins = ctools_get_content_types();
+ $available = array();
+
+ foreach ($plugins as $id => $plugin) {
+ foreach (ctools_content_get_subtypes($plugin) as $subtype_id => $subtype) {
+ // Exclude items that require content if we're saying we don't
+ // provide it.
+ if (!empty($subtype['requires content']) && !$has_content) {
+ continue;
+ }
+
+ // Check to see if the content type can be used in this context.
+ if (!empty($subtype['required context'])) {
+ if (!ctools_context_match_requirements($contexts, $subtype['required context'])) {
+ continue;
+ }
+ }
+
+ // Check to see if the passed-in allowed types allows this content.
+ if ($allowed_types) {
+ $key = $id . '-' . $subtype_id;
+ if (!isset($allowed_types[$key])) {
+ $allowed_types[$key] = isset($default_types[$id]) ? $default_types[$id] : $default_types['other'];
+ }
+ if (!$allowed_types[$key]) {
+ continue;
+ }
+ }
+
+ // Check if the content type provides an access callback.
+ if (isset($subtype['create content access']) && function_exists($subtype['create content access']) && !$subtype['create content access']($plugin, $subtype)) {
+ continue;
+ }
+
+ // If we made it through all the tests, then we can use this content.
+ $available[$id][$subtype_id] = $subtype;
+ }
+ }
+ return $available;
+}
+
+/**
+ * Get an array of all content types that can be fed into the
+ * display editor for the add content list, regardless of
+ * availability.
+ */
+function ctools_content_get_all_types() {
+ $plugins = ctools_get_content_types();
+ $available = array();
+
+ foreach ($plugins as $id => $plugin) {
+ foreach (ctools_content_get_subtypes($plugin) as $subtype_id => $subtype) {
+ // If we made it through all the tests, then we can use this content.
+ $available[$id][$subtype_id] = $subtype;
+ }
+ }
+ return $available;
+}
+
+/**
+ * Select the context to be used for a piece of content, based upon config.
+ *
+ * @param $plugin
+ * The content plugin
+ * @param $subtype
+ * The subtype of the content.
+ * @param $conf
+ * The configuration array that should contain the context.
+ * @param $contexts
+ * A keyed array of available contexts.
+ *
+ * @return
+ * The matching contexts or NULL if none or necessary, or FALSE if
+ * requirements can't be met.
+ */
+function ctools_content_select_context($plugin, $subtype, $conf, $contexts) {
+ // Identify which of our possible contexts apply.
+ if (empty($subtype)) {
+ return;
+ }
+
+ $subtype_info = ctools_content_get_subtype($plugin, $subtype);
+ if (empty($subtype_info)) {
+ return;
+ }
+
+ if (!empty($subtype_info['all contexts']) || !empty($plugin['all contexts'])) {
+ return $contexts;
+ }
+
+ // If the content requires a context, fetch it; if no context is returned,
+ // do not display the pane.
+ if (empty($subtype_info['required context'])) {
+ return;
+ }
+
+ // Deal with dynamic required contexts not getting updated in the panes.
+ // For example, Views let you dynamically change context info. While
+ // we cannot be perfect, one thing we can do is if no context at all
+ // was asked for, and then was later added but none is selected, make
+ // a best guess as to what context should be used. THis is right more
+ // than it's wrong.
+ if (is_array($subtype_info['required context'])) {
+ if (empty($conf['context']) || count($subtype_info['required context']) != count($conf['context'])) {
+ foreach ($subtype_info['required context'] as $index => $required) {
+ if (!isset($conf['context'][$index])) {
+ $filtered = ctools_context_filter($contexts, $required);
+ if ($filtered) {
+ $keys = array_keys($filtered);
+ $conf['context'][$index] = array_shift($keys);
+ }
+ }
+ }
+ }
+ }
+ else {
+ if (empty($conf['context'])) {
+ $filtered = ctools_context_filter($contexts, $subtype_info['required context']);
+ if ($filtered) {
+ $keys = array_keys($filtered);
+ $conf['context'] = array_shift($keys);
+ }
+ }
+ }
+
+ if (empty($conf['context'])) {
+ return;
+ }
+
+ $context = ctools_context_select($contexts, $subtype_info['required context'], $conf['context']);
+
+ return $context;
+}
+
+/**
+ * Fetch a piece of content from the addressable content system.
+ *
+ * @param $address
+ * A string or an array representing the address of the content.
+ * @param $type
+ * The type of content to return. The type is dependent on what
+ * the content actually is. The default, 'content' means a simple
+ * string representing the content. However, richer systems may
+ * offer more options. For example, Panels might allow the
+ * fetching of 'display' and 'pane' objects. Page Manager
+ * might allow for the fetching of 'task_handler' objects
+ * (AKA variants).
+ */
+function ctools_get_addressable_content($address, $type = 'content') {
+ if (!is_array($address)) {
+ $address = explode('::', $address);
+ }
+
+ if (!$address) {
+ return;
+ }
+
+ // This removes the module from the address so the
+ // implementor is not responsible for that part.
+ $module = array_shift($address);
+ return module_invoke($module, 'addressable_content', $address, $type);
+}
diff --git a/sites/all/modules/ctools/includes/content.menu.inc b/sites/all/modules/ctools/includes/content.menu.inc
new file mode 100644
index 0000000..64d44b0
--- /dev/null
+++ b/sites/all/modules/ctools/includes/content.menu.inc
@@ -0,0 +1,178 @@
+ array('access content'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'includes/content.menu.inc',
+ );
+ $items['ctools/autocomplete/%'] = array(
+ 'page callback' => 'ctools_content_autocomplete_entity',
+ 'page arguments' => array(2),
+ ) + $base;
+}
+
+/**
+ * Helper function for autocompletion of entity titles.
+ */
+function ctools_content_autocomplete_entity($entity_type, $string = '') {
+ if ($string != '') {
+ $entity_info = entity_get_info($entity_type);
+
+ if (!module_exists('entity')) {
+ module_load_include('inc', 'ctools', 'includes/entity-access');
+ _ctools_entity_access($entity_info, $entity_type);
+ }
+
+ // We must query all ids, because if every one of the 10 don't have access
+ // the user may never be able to autocomplete a node title.
+ $preg_matches = array();
+ $matches = array();
+ $match = preg_match('/\[id: (\d+)\]/', $string, $preg_matches);
+ if (!$match) {
+ $match = preg_match('/^id: (\d+)/', $string, $preg_matches);
+ }
+ // If an ID match was found, use that ID rather than the whole string.
+ if ($match) {
+ $entity_id = $preg_matches[1];
+ $results = _ctools_getReferencableEntities($entity_type, $entity_info, $entity_id, '=', 1);
+ }
+ else {
+ // We cannot find results if the entity doesn't have a label to search.
+ if (!isset($entity_info['entity keys']['label'])) {
+ drupal_json_output(array("[id: NULL]" => '' . t('Entity Type !entity_type does not support autocomplete search.', array('!entity_type' => $entity_type)) . ' '));
+ return;
+ }
+ $results = _ctools_getReferencableEntities($entity_type, $entity_info, $string, 'LIKE', 10);
+ }
+ foreach ($results as $entity_id => $result) {
+ $matches[$result['label'] . " [id: $entity_id]"] = '' . check_plain($result['label']) . ' ';
+ $matches[$result['label'] . " [id: $entity_id]"] .= isset($result['bundle']) ? ' (' . check_plain($result['bundle']) . ') ' : '';
+ }
+
+ drupal_json_output($matches);
+ }
+}
+
+/**
+ * Use EntityReference_SelectionHandler_Generic class to build our search query.
+ */
+function _ctools_buildQuery($entity_type, $entity_info, $match = NULL, $match_operator = 'CONTAINS') {
+ $base_table = $entity_info['base table'];
+ $label_key = $entity_info['entity keys']['label'];
+ $query = db_select($base_table)
+ ->fields($base_table, array($entity_info['entity keys']['id']));
+
+ if (isset($match)) {
+ if (isset($label_key)) {
+ $query->condition($base_table . '.' . $label_key, '%' . $match . '%', $match_operator);
+ }
+ // This should never happen, but double check just in case.
+ else {
+ return array();
+ }
+ }
+ // Add a generic entity access tag to the query.
+ $query->addTag('ctools');
+
+ // We have to perform two checks. First check is a query alter (with tags)
+ // in an attempt to only return results that have access. However, this is
+ // not full-proof since entities many not implement hook_access query tag.
+ // This is why we have a second check after entity load, before we display
+ // the label of an entity.
+ if ($entity_type == 'comment') {
+ // Adding the 'comment_access' tag is sadly insufficient for comments: core
+ // requires us to also know about the concept of 'published' and
+ // 'unpublished'.
+ if (!user_access('administer comments')) {
+ $query->condition('comment.status', COMMENT_PUBLISHED);
+ }
+
+ // Join to a node if the user does not have node access bypass permissions
+ // to obey node published permissions.
+ if (!user_access('bypass node access')) {
+ $node_alias = $query->innerJoin('node', 'n', '%alias.nid = comment.nid');
+ $query->condition($node_alias . '.status', NODE_PUBLISHED);
+ }
+ $query->addTag('node_access');
+ }
+ else {
+ $query->addTag($entity_type . '_access');
+ }
+
+ // Add the sort option.
+ if (isset($label_key)) {
+ $query->orderBy($base_table . '.' . $label_key, 'ASC');
+ }
+
+ return $query;
+}
+
+/**
+ * Private function to get referencable entities. Based on code from the
+ * Entity Reference module.
+ */
+function _ctools_getReferencableEntities($entity_type, $entity_info, $match = NULL, $match_operator = 'LIKE', $limit = 0) {
+ global $user;
+ $account = $user;
+ $options = array();
+ // We're an entity ID, return the id.
+ if (is_numeric($match) && $match_operator == '=') {
+ if ($entity = array_shift(entity_load($entity_type, array($match)))) {
+ if (isset($entity_info['access callback']) && function_exists($entity_info['access callback'])) {
+ if ($entity_info['access callback']('view', $entity, $account, $entity_type)) {
+ $label = entity_label($entity_type, $entity);
+ return array(
+ $match => array(
+ 'label' => !empty($label) ? $label : $entity->{$entity_info['entity keys']['id']},
+ 'bundle' => !empty($entity_info['entity keys']['bundle']) ? check_plain($entity->{$entity_info['entity keys']['bundle']}) : NULL,
+ ),
+ );
+ }
+ }
+ }
+ // If you don't have access, or an access callback or a valid entity, just
+ // Return back the Entity ID.
+ return array(
+ $match => array(
+ 'label' => $match,
+ 'bundle' => NULL,
+ ),
+ );
+ }
+
+ // We have matches, build a query to fetch the result.
+ if ($query = _ctools_buildQuery($entity_type, $entity_info, $match, $match_operator)) {
+ if ($limit > 0) {
+ $query->range(0, $limit);
+ }
+
+ $results = $query->execute();
+
+ if (!empty($results)) {
+ foreach ($results as $record) {
+ $entities = entity_load($entity_type, array($record->{$entity_info['entity keys']['id']}));
+ $entity = array_shift($entities);
+ if (isset($entity_info['access callback']) && function_exists($entity_info['access callback'])) {
+ if ($entity_info['access callback']('view', $entity, $account, $entity_type)) {
+ $label = entity_label($entity_type, $entity);
+ $options[$record->{$entity_info['entity keys']['id']}] = array(
+ 'label' => !empty($label) ? $label : $entity->{$entity_info['entity keys']['id']},
+ 'bundle' => !empty($entity_info['entity keys']['bundle']) ? check_plain($entity->{$entity_info['entity keys']['bundle']}) : NULL,
+ );
+ }
+ }
+ }
+ }
+ return $options;
+ }
+ return array();
+}
diff --git a/sites/all/modules/ctools/includes/content.plugin-type.inc b/sites/all/modules/ctools/includes/content.plugin-type.inc
new file mode 100644
index 0000000..0364a92
--- /dev/null
+++ b/sites/all/modules/ctools/includes/content.plugin-type.inc
@@ -0,0 +1,17 @@
+ FALSE,
+ 'process' => array(
+ 'function' => 'ctools_content_process',
+ 'file' => 'content.inc',
+ 'path' => drupal_get_path('module', 'ctools') . '/includes',
+ ),
+ );
+}
diff --git a/sites/all/modules/ctools/includes/content.theme.inc b/sites/all/modules/ctools/includes/content.theme.inc
new file mode 100644
index 0000000..ae4456a
--- /dev/null
+++ b/sites/all/modules/ctools/includes/content.theme.inc
@@ -0,0 +1,21 @@
+ array(
+ * 0 => array(
+ * 'name' => 'name of access plugin',
+ * 'settings' => array(), // These will be set by the form
+ * ),
+ * // ... as many as needed
+ * ),
+ * 'logic' => 'AND', // or 'OR',
+ * ),
+ * @endcode
+ *
+ * To add this widget to your UI, you need to do a little bit of setup.
+ *
+ * The form will utilize two callbacks, one to get the cached version
+ * of the access settings, and one to store the cached version of the
+ * access settings. These will be used from AJAX forms, so they will
+ * be completely out of the context of this page load and will not have
+ * knowledge of anything sent to this form (the 'module' and 'argument'
+ * will be preserved through the URL only).
+ *
+ * The 'module' is used to determine the location of the callback. It
+ * does not strictly need to be a module, so that if your module defines
+ * multiple systems that use this callback, it can use anything within the
+ * module's namespace it likes.
+ *
+ * When retrieving the cache, the cache may not have already been set up;
+ * In order to efficiently use cache space, we want to cache the stored
+ * settings *only* when they have changed. Therefore, the get access cache
+ * callback should first look for cache, and if it finds nothing, return
+ * the original settings.
+ *
+ * The callbacks:
+ * - $module . _ctools_access_get($argument) -- get the 'access' settings
+ * from cache. Must return array($access, $contexts); This callback can
+ * perform access checking to make sure this URL is not being gamed.
+ * - $module . _ctools_access_set($argument, $access) -- set the 'access'
+ * settings in cache.
+ * - $module . _ctools_access_clear($argument) -- clear the cache.
+ *
+ * The ctools_object_cache is recommended for this purpose, but you can use
+ * any caching mechanism you like. An example:
+ *
+ * @code{
+ * ctools_include('object-cache');
+ * ctools_object_cache_set("$module:argument", $access);
+ * }
+ *
+ * To utilize this form:
+ * @code
+ * ctools_include('context-access-admin');
+ * $form_state = array(
+ * 'access' => $access,
+ * 'module' => 'module name',
+ * 'callback argument' => 'some string',
+ * 'contexts' => $contexts, // an array of contexts. Optional if no contexts.
+ * // 'logged-in-user' will be added if not present as the access system
+ * // requires this context.
+ * ),
+ * $output = drupal_build_form('ctools_access_admin_form', $form_state);
+ * if (!empty($form_state['executed'])) {
+ * // save $form_state['access'] however you like.
+ * }
+ * @endcode
+ *
+ * Additionally, you may add 'no buttons' => TRUE if you wish to embed this
+ * form into your own, and instead call
+ *
+ * @code{
+ * $form = ctools_access_admin_form($form, $form_state);
+ * }
+ *
+ * You'll be responsible for adding a submit button.
+ *
+ * You may use ctools_access($access, $contexts) which will return
+ * TRUE if access is passed or FALSE if access is not passed.
+ */
+
+/**
+ * Administrative form for access control.
+ */
+function ctools_access_admin_form($form, &$form_state) {
+ ctools_include('context');
+ $argument = isset($form_state['callback argument']) ? $form_state['callback argument'] : '';
+ $fragment = $form_state['module'];
+ if ($argument) {
+ $fragment .= '-' . $argument;
+ }
+
+ $contexts = isset($form_state['contexts']) ? $form_state['contexts'] : array();
+
+ $form['access_table'] = array(
+ '#markup' => ctools_access_admin_render_table($form_state['access'], $fragment, $contexts),
+ );
+
+ $form['add-button'] = array(
+ '#theme' => 'ctools_access_admin_add',
+ );
+ // This sets up the URL for the add access modal.
+ $form['add-button']['add-url'] = array(
+ '#attributes' => array('class' => array("ctools-access-add-url")),
+ '#type' => 'hidden',
+ '#value' => url("ctools/context/ajax/access/add/$fragment", array('absolute' => TRUE)),
+ );
+
+ $plugins = ctools_get_relevant_access_plugins($contexts);
+ $options = array();
+ foreach ($plugins as $id => $plugin) {
+ $options[$id] = $plugin['title'];
+ }
+
+ asort($options);
+
+ $form['add-button']['type'] = array(
+ // This ensures that the form item is added to the URL.
+ '#attributes' => array('class' => array("ctools-access-add-url")),
+ '#type' => 'select',
+ '#options' => $options,
+ '#required' => FALSE,
+ );
+
+ $form['add-button']['add'] = array(
+ '#type' => 'submit',
+ '#attributes' => array('class' => array('ctools-use-modal')),
+ '#id' => "ctools-access-add",
+ '#value' => t('Add'),
+ );
+
+ $form['logic'] = array(
+ '#type' => 'radios',
+ '#options' => array(
+ 'and' => t('All criteria must pass.'),
+ 'or' => t('Only one criteria must pass.'),
+ ),
+ '#default_value' => isset($form_state['access']['logic']) ? $form_state['access']['logic'] : 'and',
+ );
+
+ if (empty($form_state['no buttons'])) {
+ $form['buttons']['save'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#submit' => array('ctools_access_admin_form_submit'),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Render the table. This is used both to render it initially and to rerender
+ * it upon ajax response.
+ */
+function ctools_access_admin_render_table($access, $fragment, $contexts) {
+ ctools_include('ajax');
+ ctools_include('modal');
+ $rows = array();
+
+ if (empty($access['plugins'])) {
+ $access['plugins'] = array();
+ }
+
+ foreach ($access['plugins'] as $id => $test) {
+ $row = array();
+ $plugin = ctools_get_access_plugin($test['name']);
+ $title = isset($plugin['title']) ? $plugin['title'] : t('Broken/missing access plugin %plugin', array('%plugin' => $test['name']));
+
+ $row[] = array('data' => $title, 'class' => array('ctools-access-title'));
+
+ $description = ctools_access_summary($plugin, $contexts, $test);
+ $row[] = array('data' => $description, 'class' => array('ctools-access-description'));
+
+ $operations = ctools_modal_image_button(ctools_image_path('icon-configure.png'), "ctools/context/ajax/access/configure/$fragment/$id", t('Configure settings for this item.'));
+ $operations .= ctools_ajax_image_button(ctools_image_path('icon-delete.png'), "ctools/context/ajax/access/delete/$fragment/$id", t('Remove this item.'));
+
+ $row[] = array('data' => $operations, 'class' => array('ctools-access-operations'), 'align' => 'right');
+
+ $rows[] = $row;
+ }
+
+ $header = array(
+ array('data' => t('Title'), 'class' => array('ctools-access-title')),
+ array('data' => t('Description'), 'class' => array('ctools-access-description')),
+ array('data' => '', 'class' => array('ctools-access-operations'), 'align' => 'right'),
+ );
+
+ if (empty($rows)) {
+ $rows[] = array(array('data' => t('No criteria selected, this test will pass.'), 'colspan' => count($header)));
+ }
+
+ ctools_modal_add_js();
+ return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'ctools-access-table')));
+}
+
+/**
+ * Theme the 'add' portion of the access form into a table.
+ */
+function theme_ctools_access_admin_add($vars) {
+ $rows = array(array(drupal_render_children($vars['form'])));
+ $output = '';
+ $output .= theme('table', array('rows' => $rows));
+ $output .= '
';
+ return $output;
+}
+
+function ctools_access_admin_form_submit($form, &$form_state) {
+ $form_state['access']['logic'] = $form_state['values']['logic'];
+
+ $function = $form_state['module'] . '_ctools_access_clear';
+ if (function_exists($function)) {
+ $function($form_state['callback argument']);
+ }
+}
+
+// --------------------------------------------------------------------------
+// AJAX menu entry points.
+/**
+ * AJAX callback to add a new access test to the list.
+ */
+
+function ctools_access_ajax_add($fragment = NULL, $name = NULL) {
+ ctools_include('ajax');
+ ctools_include('modal');
+ ctools_include('context');
+
+ if (empty($fragment) || empty($name)) {
+ ctools_ajax_render_error();
+ }
+
+ $plugin = ctools_get_access_plugin($name);
+ if (empty($plugin)) {
+ ctools_ajax_render_error();
+ }
+
+ // Separate the fragment into 'module' and 'argument'.
+ if (strpos($fragment, '-') === FALSE) {
+ $module = $fragment;
+ $argument = NULL;
+ }
+ else {
+ list($module, $argument) = explode('-', $fragment, 2);
+ }
+
+ $function = $module . '_ctools_access_get';
+ if (!function_exists($function)) {
+ ctools_ajax_render_error(t('Missing callback hooks.'));
+ }
+
+ list($access, $contexts) = $function($argument);
+
+ // Make sure we have the logged in user context.
+ if (!isset($contexts['logged-in-user'])) {
+ $contexts['logged-in-user'] = ctools_access_get_loggedin_context();
+ }
+
+ if (empty($access['plugins'])) {
+ $access['plugins'] = array();
+ }
+
+ $test = ctools_access_new_test($plugin);
+
+ $id = $access['plugins'] ? max(array_keys($access['plugins'])) + 1 : 0;
+ $access['plugins'][$id] = $test;
+
+ $form_state = array(
+ 'plugin' => $plugin,
+ 'id' => $id,
+ 'test' => &$access['plugins'][$id],
+ 'access' => &$access,
+ 'contexts' => $contexts,
+ 'title' => t('Add criteria'),
+ 'ajax' => TRUE,
+ 'modal' => TRUE,
+ 'modal return' => TRUE,
+ );
+
+ $output = ctools_modal_form_wrapper('ctools_access_ajax_edit_item', $form_state);
+ $access = $form_state['access'];
+ $access['plugins'][$id] = $form_state['test'];
+
+ if (!isset($output[0])) {
+ $function = $module . '_ctools_access_set';
+ if (function_exists($function)) {
+ $function($argument, $access);
+ }
+
+ $table = ctools_access_admin_render_table($access, $fragment, $contexts);
+ $output = array();
+ $output[] = ajax_command_replace('table#ctools-access-table', $table);
+ $output[] = ctools_modal_command_dismiss();
+ }
+
+ print ajax_render($output);
+}
+
+/**
+ * AJAX callback to edit an access test in the list.
+ */
+function ctools_access_ajax_edit($fragment = NULL, $id = NULL) {
+ ctools_include('ajax');
+ ctools_include('modal');
+ ctools_include('context');
+
+ if (empty($fragment) || !isset($id)) {
+ ctools_ajax_render_error();
+ }
+
+ // Separate the fragment into 'module' and 'argument'.
+ if (strpos($fragment, '-') === FALSE) {
+ $module = $fragment;
+ $argument = NULL;
+ }
+ else {
+ list($module, $argument) = explode('-', $fragment, 2);
+ }
+
+ $function = $module . '_ctools_access_get';
+ if (!function_exists($function)) {
+ ctools_ajax_render_error(t('Missing callback hooks.'));
+ }
+
+ list($access, $contexts) = $function($argument);
+
+ if (empty($access['plugins'][$id])) {
+ ctools_ajax_render_error();
+ }
+
+ // Make sure we have the logged in user context.
+ if (!isset($contexts['logged-in-user'])) {
+ $contexts['logged-in-user'] = ctools_access_get_loggedin_context();
+ }
+
+ $plugin = ctools_get_access_plugin($access['plugins'][$id]['name']);
+ $form_state = array(
+ 'plugin' => $plugin,
+ 'id' => $id,
+ 'test' => &$access['plugins'][$id],
+ 'access' => &$access,
+ 'contexts' => $contexts,
+ 'title' => t('Edit criteria'),
+ 'ajax' => TRUE,
+ 'modal' => TRUE,
+ 'modal return' => TRUE,
+ );
+
+ $output = ctools_modal_form_wrapper('ctools_access_ajax_edit_item', $form_state);
+ $access = $form_state['access'];
+ $access['plugins'][$id] = $form_state['test'];
+
+ if (!isset($output[0])) {
+ $function = $module . '_ctools_access_set';
+ if (function_exists($function)) {
+ $function($argument, $access);
+ }
+
+ $table = ctools_access_admin_render_table($access, $fragment, $contexts);
+ $output = array();
+ $output[] = ajax_command_replace('table#ctools-access-table', $table);
+ $output[] = ctools_modal_command_dismiss();
+ }
+
+ print ajax_render($output);
+}
+
+/**
+ * Form to edit the settings of an access test.
+ */
+function ctools_access_ajax_edit_item($form, &$form_state) {
+ $test = &$form_state['test'];
+ $plugin = &$form_state['plugin'];
+ if (isset($plugin['required context'])) {
+ $form['context'] = ctools_context_selector($form_state['contexts'], $plugin['required context'], $test['context']);
+ }
+ $form['settings'] = array('#tree' => TRUE);
+ if ($function = ctools_plugin_get_function($plugin, 'settings form')) {
+ $form = $function($form, $form_state, $test['settings']);
+ }
+
+ $form['not'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Reverse (NOT)'),
+ '#default_value' => !empty($test['not']),
+ );
+
+ $form['save'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for argument settings.
+ */
+function ctools_access_ajax_edit_item_validate($form, &$form_state) {
+ if ($function = ctools_plugin_get_function($form_state['plugin'], 'settings form validate')) {
+ $function($form, $form_state);
+ }
+}
+
+/**
+ * Submit handler for argument settings.
+ */
+function ctools_access_ajax_edit_item_submit($form, &$form_state) {
+ if ($function = ctools_plugin_get_function($form_state['plugin'], 'settings form submit')) {
+ $function($form, $form_state);
+ }
+
+ $form_state['test']['settings'] = $form_state['values']['settings'];
+ if (isset($form_state['values']['context'])) {
+ $form_state['test']['context'] = $form_state['values']['context'];
+ }
+ $form_state['test']['not'] = !empty($form_state['values']['not']);
+}
+
+/**
+ * AJAX command to remove an access control item.
+ */
+function ctools_access_ajax_delete($fragment = NULL, $id = NULL) {
+ ctools_include('ajax');
+ ctools_include('modal');
+ ctools_include('context');
+
+ if (empty($fragment) || !isset($id)) {
+ ajax_render_error();
+ }
+
+ // Separate the fragment into 'module' and 'argument'.
+ if (strpos($fragment, '-') === FALSE) {
+ $module = $fragment;
+ $argument = NULL;
+ }
+ else {
+ list($module, $argument) = explode('-', $fragment, 2);
+ }
+
+ $function = $module . '_ctools_access_get';
+ if (!function_exists($function)) {
+ ajax_render_error(t('Missing callback hooks.'));
+ }
+
+ list($access, $contexts) = $function($argument);
+
+ if (isset($access['plugins'][$id])) {
+ unset($access['plugins'][$id]);
+ }
+
+ // re-cache.
+ $function = $module . '_ctools_access_set';
+ if (function_exists($function)) {
+ $function($argument, $access);
+ }
+
+ $table = ctools_access_admin_render_table($access, $fragment, $contexts);
+ $output = array();
+ $output[] = ajax_command_replace('table#ctools-access-table', $table);
+
+ print ajax_render($output);
+}
diff --git a/sites/all/modules/ctools/includes/context-admin.inc b/sites/all/modules/ctools/includes/context-admin.inc
new file mode 100644
index 0000000..533c89f
--- /dev/null
+++ b/sites/all/modules/ctools/includes/context-admin.inc
@@ -0,0 +1,849 @@
+ array(
+ 'title' => t('Arguments'),
+ 'singular title' => t('argument'),
+ 'description' => '', // t("Arguments are parsed from the URL and translated into contexts that may be added to the display via the 'content' tab. These arguments are parsed in the order received, and you may use % in your URL to hold the place of an object; the rest of the arguments will come after the URL. For example, if the URL is node/%/panel and your user visits node/1/panel/foo, the first argument will be 1, and the second argument will be foo."),
+ 'add button' => t('Add argument'),
+ 'context function' => 'ctools_get_argument',
+ 'key' => 'arguments', // the key that data will be stored on an object, eg $panel_page
+ 'sortable' => TRUE,
+ 'settings' => 'argument_settings',
+ ),
+ 'relationship' => array(
+ 'title' => t('Relationships'),
+ 'singular title' => t('relationship'),
+ 'description' => '', // t('Relationships are contexts that are created from already existing contexts; the add relationship button will only appear once there is another context available. Relationships can load objects based upon how they are related to each other; for example, the author of a node, or a taxonomy term attached to a node, or the vocabulary of a taxonomy term.'),
+ 'add button' => t('Add relationship'),
+ 'context function' => 'ctools_get_relationship',
+ 'key' => 'relationships',
+ 'sortable' => FALSE,
+ 'settings' => 'relationship_settings',
+ ),
+ 'context' => array(
+ 'title' => t('Contexts'),
+ 'singular title' => t('context'),
+ 'description' => '', // t('Contexts are embedded directly into the panel; you generally must select an object in the panel. For example, you could select node 5, or the term "animals" or the user "administrator"'),
+ 'add button' => t('Add context'),
+ 'context function' => 'ctools_get_context',
+ 'key' => 'contexts',
+ 'sortable' => FALSE,
+ 'settings' => 'context_settings',
+ ),
+ 'requiredcontext' => array(
+ 'title' => t('Required contexts'),
+ 'singular title' => t('required context'),
+ 'description' => '', // t('Required contexts are passed in from some external source, such as a containing panel. If a mini panel has required contexts, it can only appear when that context is available, and therefore will not show up as a standard Drupal block.'),
+ 'add button' => t('Add required context'),
+ 'context function' => 'ctools_get_context',
+ 'key' => 'requiredcontexts',
+ 'sortable' => FALSE,
+ ),
+ );
+ }
+
+ if ($type === NULL) {
+ return $info;
+ }
+
+ return $info[$type];
+}
+
+
+/**
+ * Get the data belonging to a particular context.
+ */
+function ctools_context_get_plugin($type, $name) {
+ $info = ctools_context_info($type);
+ if (function_exists($info['context function'])) {
+ return $info['context function']($name);
+ }
+}
+
+/**
+ * Add the argument table plus gadget plus javascript to the form.
+ */
+function ctools_context_add_argument_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
+ if (empty($cache_key)) {
+ $cache_key = $object->name;
+ }
+
+ $form_location = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#theme' => 'ctools_context_item_form',
+ '#cache_key' => $cache_key,
+ '#ctools_context_type' => 'argument',
+ '#ctools_context_module' => $module,
+ );
+
+ $args = ctools_get_arguments();
+ $choices = array();
+ foreach ($args as $name => $arg) {
+ if (empty($arg['no ui'])) {
+ $choices[$name] = $arg['title'];
+ }
+ }
+
+ asort($choices);
+
+ if (!empty($choices) || !empty($object->arguments)) {
+ ctools_context_add_item_table('argument', $form_location, $choices, $object->arguments);
+ }
+}
+
+function ctools_context_add_context_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
+ if (empty($cache_key)) {
+ $cache_key = $object->name;
+ }
+
+ $form_location = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#theme' => 'ctools_context_item_form',
+ '#cache_key' => $cache_key,
+ '#ctools_context_type' => 'context',
+ '#ctools_context_module' => $module,
+ );
+
+ // Store the order the choices are in so javascript can manipulate it.
+ $form_location['markup'] = array(
+ '#markup' => ' ',
+ );
+
+ $choices = array();
+ foreach (ctools_get_contexts() as $name => $arg) {
+ if (empty($arg['no ui'])) {
+ $choices[$name] = $arg['title'];
+ }
+ }
+
+ asort($choices);
+
+ if (!empty($choices) || !empty($object->contexts)) {
+ ctools_context_add_item_table('context', $form_location, $choices, $object->contexts);
+ }
+
+}
+
+function ctools_context_add_required_context_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
+ if (empty($cache_key)) {
+ $cache_key = $object->name;
+ }
+
+ $form_location = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#theme' => 'ctools_context_item_form',
+ '#cache_key' => $cache_key,
+ '#ctools_context_type' => 'requiredcontext',
+ '#ctools_context_module' => $module,
+ );
+
+ // Store the order the choices are in so javascript can manipulate it.
+ $form_location['markup'] = array(
+ '#value' => ' ',
+ );
+
+ $choices = array();
+ foreach (ctools_get_contexts() as $name => $arg) {
+ if (empty($arg['no required context ui'])) {
+ $choices[$name] = $arg['title'];
+ }
+ }
+
+ asort($choices);
+
+ if (!empty($choices) || !empty($object->contexts)) {
+ ctools_context_add_item_table('requiredcontext', $form_location, $choices, $object->requiredcontexts);
+ }
+}
+
+function ctools_context_add_relationship_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
+ if (empty($cache_key)) {
+ $cache_key = $object->name;
+ }
+
+ $form_location = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#theme' => 'ctools_context_item_form',
+ '#cache_key' => $cache_key,
+ '#ctools_context_type' => 'relationship',
+ '#ctools_context_module' => $module,
+ );
+
+ // Store the order the choices are in so javascript can manipulate it.
+ $form_location['markup'] = array(
+ '#value' => ' ',
+ );
+
+ $base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+ $available_relationships = ctools_context_get_relevant_relationships(ctools_context_load_contexts($object, TRUE, $base_contexts));
+
+ ctools_context_add_item_table('relationship', $form_location, $available_relationships, $object->relationships);
+}
+
+/**
+ * Include all context administrative include files, css, javascript.
+ */
+function ctools_context_admin_includes() {
+ ctools_include('context');
+ ctools_include('modal');
+ ctools_include('ajax');
+ ctools_include('object-cache');
+ ctools_modal_add_js();
+ ctools_modal_add_plugin_js(ctools_get_contexts());
+ ctools_modal_add_plugin_js(ctools_get_relationships());
+}
+
+/**
+ * Add the context table to the page.
+ */
+function ctools_context_add_item_table($type, &$form, $available_contexts, $items) {
+ $form[$type] = array(
+ '#tree' => TRUE,
+ );
+
+ $module = $form['#ctools_context_module'];
+ $cache_key = $form['#cache_key'];
+
+ if (isset($items) && is_array($items)) {
+ foreach ($items as $position => $context) {
+ ctools_context_add_item_to_form($module, $type, $cache_key, $form[$type][$position], $position, $context);
+ }
+ }
+
+ $type_info = ctools_context_info($type);
+ $form['description'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#markup' => $type_info['description'],
+ );
+
+ ctools_context_add_item_table_buttons($type, $module, $form, $available_contexts);
+}
+
+function ctools_context_add_item_table_buttons($type, $module, &$form, $available_contexts) {
+ drupal_add_library('system', 'drupal.ajax');
+ $form['buttons'] = array(
+ '#tree' => TRUE,
+ );
+
+ if (!empty($available_contexts)) {
+ $type_info = ctools_context_info($type);
+
+ $module = $form['#ctools_context_module'];
+ $cache_key = $form['#cache_key'];
+
+ // The URL for this ajax button
+ $form['buttons'][$type]['add-url'] = array(
+ '#attributes' => array('class' => array("ctools-$type-add-url")),
+ '#type' => 'hidden',
+ '#value' => url("ctools/context/ajax/add/$module/$type/$cache_key", array('absolute' => TRUE)),
+ );
+
+ asort($available_contexts);
+ // This also will be in the URL.
+ $form['buttons'][$type]['item'] = array(
+ '#attributes' => array('class' => array("ctools-$type-add-url")),
+ '#type' => 'select',
+ '#options' => $available_contexts,
+ '#required' => FALSE,
+ );
+
+ $form['buttons'][$type]['add'] = array(
+ '#type' => 'submit',
+ '#attributes' => array('class' => array('ctools-use-modal')),
+ '#id' => "ctools-$type-add",
+ '#value' => $type_info['add button'],
+ );
+ }
+}
+
+/**
+ * Add a row to the form. Used both in the main form and by
+ * the ajax to add an item.
+ */
+function ctools_context_add_item_to_form($module, $type, $cache_key, &$form, $position, $item) {
+ // This is the single function way to load any plugin by variable type.
+ $info = ctools_context_get_plugin($type, $item['name']);
+ $form['title'] = array(
+ '#markup' => check_plain($item['identifier']),
+ );
+
+ // Relationships not sortable.
+ $type_info = ctools_context_info($type);
+
+ if (!empty($type_info['sortable'])) {
+ $form['position'] = array(
+ '#type' => 'weight',
+ '#default_value' => $position,
+ '#attributes' => array('class' => array('drag-position')),
+ );
+ }
+
+ $form['remove'] = array(
+ '#markup' => ctools_ajax_image_button(ctools_image_path('icon-delete.png'), "ctools/context/ajax/delete/$module/$type/$cache_key/$position", t('Remove this item.')),
+ );
+
+ $form['settings'] = array(
+ '#markup' => ctools_modal_image_button(ctools_image_path('icon-configure.png'), "ctools/context/ajax/configure/$module/$type/$cache_key/$position", t('Configure settings for this item.')),
+ );
+}
+
+
+// ---------------------------------------------------------------------------
+// AJAX forms and stuff.
+
+/**
+ * Ajax entry point to add an context
+ */
+function ctools_context_ajax_item_add($mechanism = NULL, $type = NULL, $cache_key = NULL, $name = NULL, $step = NULL) {
+ ctools_include('ajax');
+ ctools_include('modal');
+ ctools_include('context');
+ ctools_include('cache');
+ ctools_include('plugins-admin');
+
+ if (!$name) {
+ return ctools_ajax_render_error();
+ }
+
+ // Load stored object from cache.
+ if (!($object = ctools_cache_get($mechanism, $cache_key))) {
+ ctools_ajax_render_error(t('Invalid object name.'));
+ }
+
+ // Get info about what we're adding, i.e, relationship, context, argument, etc.
+ $plugin_definition = ctools_context_get_plugin($type, $name);
+ if (empty($plugin_definition)) {
+ ctools_ajax_render_error(t('Invalid context type'));
+ }
+
+ // Set up the $conf array for this plugin
+ if (empty($step) || empty($object->temporary)) {
+ // Create the basis for our new context.
+ $conf = ctools_context_get_defaults($plugin_definition, $object, $type);
+ $object->temporary = &$conf;
+ }
+ else {
+ $conf = &$object->temporary;
+ }
+
+ // Load the contexts that may be used.
+ $base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+ $contexts = ctools_context_load_contexts($object, TRUE, $base_contexts);
+
+ $type_info = ctools_context_info($type);
+ $form_state = array(
+ 'ajax' => TRUE,
+ 'modal' => TRUE,
+ 'modal return' => TRUE,
+ 'object' => &$object,
+ 'conf' => &$conf,
+ 'plugin' => $plugin_definition,
+ 'type' => $type,
+ 'contexts' => $contexts,
+ 'title' => t('Add @type "@context"', array('@type' => $type_info['singular title'], '@context' => $plugin_definition['title'])),
+ 'type info' => $type_info,
+ 'op' => 'add',
+ 'step' => $step,
+ );
+
+ $form_info = array(
+ 'path' => "ctools/context/ajax/add/$mechanism/$type/$cache_key/$name/%step",
+ 'show cancel' => TRUE,
+ 'default form' => 'ctools_edit_context_form_defaults',
+ 'auto cache' => TRUE,
+ 'cache mechanism' => $mechanism,
+ 'cache key' => $cache_key,
+ // This is stating what the cache will be referred to in $form_state
+ 'cache location' => 'object',
+ );
+
+ if ($type == 'requiredcontext') {
+ $form_info += array(
+ 'add form name' => 'required context add form',
+ 'edit form name' => 'required context edit form',
+ );
+ }
+
+ $output = ctools_plugin_configure_form($form_info, $form_state);
+
+ if (!empty($form_state['cancel'])) {
+ $output = array(ctools_modal_command_dismiss());
+ }
+ elseif (!empty($form_state['complete'])) {
+ // Successful submit -- move temporary data to location.
+
+ // Create a reference to the place our context lives. Since this is fairly
+ // generic, this is the easiest way to get right to the place of the
+ // object without knowing precisely what data we're poking at.
+ $ref = &$object->{$type_info['key']};
+
+ // Figure out the position for our new context.
+ $position = empty($ref) ? 0 : max(array_keys($ref)) + 1;
+
+ $conf['id'] = ctools_context_next_id($ref, $name);
+ $ref[$position] = $conf;
+
+ if (isset($object->temporary)) {
+ unset($object->temporary);
+ }
+
+ ctools_cache_operation($mechanism, $cache_key, 'finalize', $object);
+
+ // Very irritating way to update the form for our contexts.
+ $arg_form_state = form_state_defaults() + array(
+ 'values' => array(),
+ 'process_input' => FALSE,
+ 'complete form' => array(),
+ );
+
+ $rel_form_state = $arg_form_state;
+
+ $arg_form = array(
+ '#post' => array(),
+ '#programmed' => FALSE,
+ '#tree' => FALSE,
+ '#parents' => array(),
+ '#array_parents' => array(),
+ );
+
+ // Build a chunk of the form to merge into the displayed form
+ $arg_form[$type] = array(
+ '#tree' => TRUE,
+ );
+ $arg_form[$type][$position] = array(
+ '#tree' => TRUE,
+ );
+
+ ctools_context_add_item_to_form($mechanism, $type, $cache_key, $arg_form[$type][$position], $position, $ref[$position]);
+ $arg_form = form_builder('ctools_context_form', $arg_form, $arg_form_state);
+
+ // Build the relationships table so we can ajax it in.
+ // This is an additional thing that goes in here.
+ $rel_form = array(
+ '#theme' => 'ctools_context_item_form',
+ '#cache_key' => $cache_key,
+ '#ctools_context_type' => 'relationship',
+ '#ctools_context_module' => $mechanism,
+ '#only_buttons' => TRUE,
+ '#post' => array(),
+ '#programmed' => FALSE,
+ '#tree' => FALSE,
+ '#parents' => array(),
+ '#array_parents' => array(),
+ );
+
+ $rel_form['relationship'] = array(
+ '#tree' => TRUE,
+ );
+
+ // Allow an object to set some 'base' contexts that come from elsewhere.
+ $rel_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+ $all_contexts = ctools_context_load_contexts($object, TRUE, $rel_contexts);
+ $available_relationships = ctools_context_get_relevant_relationships($all_contexts);
+
+ $output = array();
+ if (!empty($available_relationships)) {
+ ctools_context_add_item_table_buttons('relationship', $mechanism, $rel_form, $available_relationships);
+ $rel_form = form_builder('dummy_form_id', $rel_form, $rel_form_state);
+ $output[] = ajax_command_replace('div#ctools-relationships-table div.buttons', drupal_render($rel_form));
+ }
+
+ $theme_vars = array();
+ $theme_vars['type'] = $type;
+ $theme_vars['form'] = $arg_form[$type][$position];
+ $theme_vars['position'] = $position;
+ $theme_vars['count'] = $position;
+ $text = theme('ctools_context_item_row', $theme_vars);
+ $output[] = ajax_command_append('#' . $type . '-table tbody', $text);
+ $output[] = ajax_command_changed('#' . $type . '-row-' . $position, '.title');
+ $output[] = ctools_modal_command_dismiss();
+ }
+ else {
+ $output = ctools_modal_form_render($form_state, $output);
+ }
+ print ajax_render($output);
+ exit;
+}
+
+/**
+ * Ajax entry point to edit an item
+ */
+function ctools_context_ajax_item_edit($mechanism = NULL, $type = NULL, $cache_key = NULL, $position = NULL, $step = NULL) {
+ ctools_include('ajax');
+ ctools_include('modal');
+ ctools_include('context');
+ ctools_include('cache');
+ ctools_include('plugins-admin');
+
+ if (!isset($position)) {
+ return ctools_ajax_render_error();
+ }
+
+ // Load stored object from cache.
+ if (!($object = ctools_cache_get($mechanism, $cache_key))) {
+ ctools_ajax_render_error(t('Invalid object name.'));
+ }
+
+ $type_info = ctools_context_info($type);
+
+ // Create a reference to the place our context lives. Since this is fairly
+ // generic, this is the easiest way to get right to the place of the
+ // object without knowing precisely what data we're poking at.
+ $ref = &$object->{$type_info['key']};
+
+ if (empty($step) || empty($object->temporary)) {
+ // Create the basis for our new context.
+ $conf = $object->{$type_info['key']}[$position];
+ $object->temporary = &$conf;
+ }
+ else {
+ $conf = &$object->temporary;
+ }
+
+ $name = $ref[$position]['name'];
+ if (empty($name)) {
+ ctools_ajax_render_error();
+ }
+
+ // load the plugin definition
+ $plugin_definition = ctools_context_get_plugin($type, $name);
+ if (empty($plugin_definition)) {
+ ctools_ajax_render_error(t('Invalid context type'));
+ }
+
+ // Load the contexts
+ $base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+ $contexts = ctools_context_load_contexts($object, TRUE, $base_contexts);
+
+ $form_state = array(
+ 'ajax' => TRUE,
+ 'modal' => TRUE,
+ 'modal return' => TRUE,
+ 'object' => &$object,
+ 'conf' => &$conf,
+ 'position' => $position,
+ 'plugin' => $plugin_definition,
+ 'type' => $type,
+ 'contexts' => $contexts,
+ 'title' => t('Edit @type "@context"', array('@type' => $type_info['singular title'], '@context' => $plugin_definition['title'])),
+ 'type info' => $type_info,
+ 'op' => 'add',
+ 'step' => $step,
+ );
+
+ $form_info = array(
+ 'path' => "ctools/context/ajax/configure/$mechanism/$type/$cache_key/$position/%step",
+ 'show cancel' => TRUE,
+ 'default form' => 'ctools_edit_context_form_defaults',
+ 'auto cache' => TRUE,
+ 'cache mechanism' => $mechanism,
+ 'cache key' => $cache_key,
+ // This is stating what the cache will be referred to in $form_state
+ 'cache location' => 'object',
+ );
+
+ if ($type == 'requiredcontext') {
+ $form_info += array(
+ 'add form name' => 'required context add form',
+ 'edit form name' => 'required context edit form',
+ );
+ }
+
+ $output = ctools_plugin_configure_form($form_info, $form_state);
+
+ if (!empty($form_state['cancel'])) {
+ $output = array(ctools_modal_command_dismiss());
+ }
+ elseif (!empty($form_state['complete'])) {
+ // successful submit
+ $ref[$position] = $conf;
+ if (isset($object->temporary)) {
+ unset($object->temporary);
+ }
+
+ ctools_cache_operation($mechanism, $cache_key, 'finalize', $object);
+
+ $output = array();
+ $output[] = ctools_modal_command_dismiss();
+
+ $arg_form_state = form_state_defaults() + array(
+ 'values' => array(),
+ 'process_input' => FALSE,
+ 'complete form' => array(),
+ );
+
+ $arg_form = array(
+ '#post' => array(),
+ '#parents' => array(),
+ '#array_parents' => array(),
+ '#programmed' => FALSE,
+ '#tree' => FALSE,
+ );
+
+ // Build a chunk of the form to merge into the displayed form
+ $arg_form[$type] = array(
+ '#tree' => TRUE,
+ );
+ $arg_form[$type][$position] = array(
+ '#tree' => TRUE,
+ );
+
+ ctools_context_add_item_to_form($mechanism, $type, $cache_key, $arg_form[$type][$position], $position, $ref[$position]);
+ $arg_form = form_builder('ctools_context_form', $arg_form, $arg_form_state);
+
+ $theme_vars = array();
+ $theme_vars['type'] = $type;
+ $theme_vars['form'] = $arg_form[$type][$position];
+ $theme_vars['position'] = $position;
+ $theme_vars['count'] = $position;
+ $output[] = ajax_command_replace('#' . $type . '-row-' . $position, theme('ctools_context_item_row', $theme_vars));
+ $output[] = ajax_command_changed('#' . $type . '-row-' . $position, '.title');
+ }
+ else {
+ $output = ctools_modal_form_render($form_state, $output);
+ }
+ print ajax_render($output);
+ exit;
+}
+
+/**
+ * Get the defaults for a new instance of a context plugin.
+ *
+ * @param $plugin_definition
+ * The metadata definition of the plugin from ctools_get_plugins().
+ * @param $object
+ * The object the context plugin will be added to.
+ * @param $type
+ * The type of context plugin. i.e, context, requiredcontext, relationship
+ */
+function ctools_context_get_defaults($plugin_definition, $object, $type) {
+ // Fetch the potential id of the plugin so we can append
+ // title and keyword information for new ones.
+ $type_info = ctools_context_info($type);
+ $id = ctools_context_next_id($object->{$type_info['key']}, $plugin_definition['name']);
+
+ $conf = array(
+ 'identifier' => $plugin_definition['title'] . ($id > 1 ? ' ' . $id : ''),
+ 'keyword' => ctools_get_keyword($object, $plugin_definition['keyword']),
+ 'name' => $plugin_definition['name'],
+ );
+
+ if (isset($plugin_definition['defaults'])) {
+ $defaults = $plugin_definition['defaults'];
+ }
+ elseif (isset($subtype['defaults'])) {
+ $defaults = $subtype['defaults'];
+ }
+
+ if (isset($defaults)) {
+ if (is_string($defaults) && function_exists($defaults)) {
+ if ($settings = $defaults($plugin_definition)) {
+ $conf += $settings;
+ }
+ }
+ elseif (is_array($defaults)) {
+ $conf += $defaults;
+ }
+ }
+
+ return $conf;
+}
+
+/**
+ * Form wrapper for the edit context form.
+ *
+ * @todo: We should uncombine these.
+ */
+function ctools_edit_context_form_defaults($form, &$form_state) {
+ // Basic values required to orient ourselves
+ $object = $form_state['object'];
+ $plugin_definition = $form_state['plugin'];
+ $type_info = $form_state['type info'];
+ $contexts = $form_state['contexts'];
+ $conf = $form_state['conf'];
+
+ if ($type_info['key'] == 'arguments' && !isset($conf['default'])) {
+ $conf['default'] = 'ignore';
+ $conf['title'] = '';
+ }
+
+ $form['description'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#markup' => check_plain($plugin_definition['description']),
+ );
+
+ if ($type_info['key'] == 'relationships') {
+ $form['context'] = ctools_context_selector($contexts, $plugin_definition['required context'], isset($conf['context']) ? $conf['context'] : '');
+ }
+ if ($type_info['key'] == 'arguments') {
+ $form['default'] = array(
+ '#type' => 'select',
+ '#title' => t('Default'),
+ '#options' => array(
+ 'ignore' => t('Ignore it; content that requires this context will not be available.'),
+ '404' => t('Display page not found or display nothing at all.'),
+ ),
+ '#default_value' => $conf['default'],
+ '#description' => t('If the argument is missing or is not valid, select how this should behave.'),
+ );
+
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $conf['title'],
+ '#description' => t('Enter a title to use when this argument is present. You may use %KEYWORD substitution, where the keyword is specified below.'),
+ );
+ }
+
+ $form['identifier'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Identifier'),
+ '#description' => t('Enter a name to identify this !type on administrative screens.', array('!type' => t('context'))),
+ '#default_value' => $conf['identifier'],
+ );
+
+ $form['keyword'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Keyword'),
+ '#description' => t('Enter a keyword to use for substitution in titles.'),
+ '#default_value' => $conf['keyword'],
+ );
+
+ if ($type_info['key'] == 'requiredcontexts') {
+ $form['optional'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Context is optional'),
+ '#default_value' => !empty($form_state['conf']['optional']),
+ '#description' => t('This context need not be present for the component to function.'),
+ );
+ }
+
+ $form['#submit'][] = 'ctools_edit_context_form_defaults_submit';
+
+ return $form;
+}
+
+/**
+ * Submit handler to store context identifier and keyword info.
+ */
+function ctools_edit_context_form_defaults_submit(&$form, &$form_state) {
+ if ($form_state['type info']['key'] == 'relationships') {
+ $form_state['conf']['context'] = $form_state['values']['context'];
+ }
+ if ($form_state['type info']['key'] == 'arguments') {
+ $form_state['conf']['default'] = $form_state['values']['default'];
+ $form_state['conf']['title'] = $form_state['values']['title'];
+ }
+ if ($form_state['type info']['key'] == 'requiredcontexts') {
+ $form_state['conf']['optional'] = $form_state['values']['optional'];
+ }
+
+ $form_state['conf']['identifier'] = $form_state['values']['identifier'];
+ $form_state['conf']['keyword'] = $form_state['values']['keyword'];
+}
+
+/**
+ * Ajax entry point to edit an item
+ */
+function ctools_context_ajax_item_delete($mechanism = NULL, $type = NULL, $cache_key = NULL, $position = NULL) {
+ ctools_include('ajax');
+ ctools_include('context');
+ ctools_include('cache');
+
+ if (!isset($position)) {
+ return ctools_ajax_render_error();
+ }
+
+ // Load stored object from cache.
+ if (!($object = ctools_cache_get($mechanism, $cache_key))) {
+ ctools_ajax_render_error(t('Invalid object name.'));
+ }
+
+ $type_info = ctools_context_info($type);
+
+ // Create a reference to the place our context lives. Since this is fairly
+ // generic, this is the easiest way to get right to the place of the
+ // object without knowing precisely what data we're poking at.
+ $ref = &$object->{$type_info['key']};
+
+ if (!array_key_exists($position, $ref)) {
+ ctools_ajax_render_error(t('Unable to delete missing item!'));
+ }
+
+ unset($ref[$position]);
+ ctools_cache_operation($mechanism, $cache_key, 'finalize', $object);
+
+ $output = array();
+ $output[] = ajax_command_replace('#' . $type . '-row-' . $position, '');
+ $output[] = ajax_command_restripe("#$type-table");
+ print ajax_render($output);
+ exit;
+}
+
+// --- End of contexts
+
+function ctools_save_context($type, &$ref, $form_values) {
+ $type_info = ctools_context_info($type);
+
+ // Organize arguments
+ $new = array();
+ $order = array();
+
+ foreach ($ref as $id => $context) {
+ $position = $form_values[$type][$id]['position'];
+ $order[$position] = $id;
+ }
+
+ ksort($order);
+ foreach ($order as $id) {
+ $new[] = $ref[$id];
+ }
+ $ref = $new;
+}
+
+function ctools_get_keyword($page, $word) {
+ // Create a complete set of keywords
+ $keywords = array();
+ foreach (array('arguments', 'relationships', 'contexts', 'requiredcontexts') as $type) {
+ if (!empty($page->$type) && is_array($page->$type)) {
+ foreach ($page->$type as $info) {
+ $keywords[$info['keyword']] = TRUE;
+ }
+ }
+ }
+
+ $keyword = $word;
+ $count = 1;
+ while (!empty($keywords[$keyword])) {
+ $keyword = $word . '_' . ++$count;
+ }
+ return $keyword;
+}
+
diff --git a/sites/all/modules/ctools/includes/context-task-handler.inc b/sites/all/modules/ctools/includes/context-task-handler.inc
new file mode 100644
index 0000000..ed4acad
--- /dev/null
+++ b/sites/all/modules/ctools/includes/context-task-handler.inc
@@ -0,0 +1,551 @@
+ $handler) {
+ $plugin = page_manager_get_task_handler($handler->handler);
+ // First, see if the handler has a tester.
+ $function = ctools_plugin_get_function($plugin, 'test');
+ if ($function) {
+ $test = $function($handler, $contexts, $args);
+ if ($test) {
+ return $id;
+ }
+ }
+ else {
+ // If not, if it's a 'context' type handler, use the default tester.
+ if ($plugin['handler type'] == 'context') {
+ $test = ctools_context_handler_default_test($handler, $contexts, $args);
+ if ($test) {
+ return $id;
+ }
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Default test function to see if a task handler should be rendered.
+ *
+ * This tests against the standard selection criteria that most task
+ * handlers should be implementing.
+ */
+function ctools_context_handler_default_test($handler, $base_contexts, $args) {
+ ctools_include('context');
+ // Add my contexts.
+ $contexts = ctools_context_handler_get_handler_contexts($base_contexts, $handler);
+
+ // Test.
+ return ctools_context_handler_select($handler, $contexts);
+}
+
+/**
+ * Render a task handler.
+ */
+function ctools_context_handler_render_handler($task, $subtask, $handler, $contexts, $args, $page = TRUE) {
+ $function = page_manager_get_renderer($handler);
+ if (!$function) {
+ return NULL;
+ }
+
+ if ($page) {
+ if ($subtask) {
+ $task_name = page_manager_make_task_name($task['name'], $subtask['name']);
+ }
+ else {
+ $task_name = $task['name'];
+ }
+
+ page_manager_get_current_page(array(
+ 'name' => $task_name,
+ 'task' => $task,
+ 'subtask' => $subtask,
+ 'contexts' => $contexts,
+ 'arguments' => $args,
+ 'handler' => $handler,
+ ));
+ }
+
+ $info = $function($handler, $contexts, $args);
+ if (!$info) {
+ return NULL;
+ }
+
+ $context = array(
+ 'args' => $args,
+ 'contexts' => $contexts,
+ 'task' => $task,
+ 'subtask' => $subtask,
+ 'handler' => $handler,
+ );
+ drupal_alter('ctools_render', $info, $page, $context);
+
+ // If we don't own the page, let the caller deal with rendering.
+ if (!$page) {
+ return $info;
+ }
+
+ if (!empty($info['response code']) && $info['response code'] != 200) {
+ switch ($info['response code']) {
+ case 403:
+ return MENU_ACCESS_DENIED;
+
+ case 404:
+ return MENU_NOT_FOUND;
+
+ case 410:
+ drupal_add_http_header('Status', '410 Gone');
+ drupal_exit();
+ break;
+
+ case 301:
+ case 302:
+ case 303:
+ case 304:
+ case 305:
+ case 307:
+ $info += array(
+ 'query' => array(),
+ 'fragment' => '',
+ );
+ $options = array(
+ 'query' => $info['query'],
+ 'fragment' => $info['fragment'],
+ );
+ drupal_goto($info['destination'], $options, $info['response code']);
+ // @todo -- should other response codes be supported here?
+ }
+ }
+
+ $plugin = page_manager_get_task_handler($handler->handler);
+
+ if (module_exists('contextual') && user_access('access contextual links') && isset($handler->task)) {
+ // Provide a contextual link to edit this, if we can:
+ $callback = isset($plugin['contextual link']) ? $plugin['contextual link'] : 'ctools_task_handler_default_contextual_link';
+ if ($callback && function_exists($callback)) {
+ $links = $callback($handler, $plugin, $contexts, $args);
+ }
+
+ if (!empty($links) && is_array($links)) {
+ $build = array(
+ '#theme_wrappers' => array('container'),
+ '#attributes' => array('class' => array('contextual-links-region')),
+ );
+
+ if (!is_array($info['content'])) {
+ $build['content']['#markup'] = $info['content'];
+ }
+ else {
+ $build['content'] = $info['content'];
+ }
+
+ $build['contextual_links'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#theme' => 'links__contextual',
+ '#links' => $links,
+ '#attributes' => array('class' => array('contextual-links')),
+ '#attached' => array(
+ 'library' => array(array('contextual', 'contextual-links')),
+ ),
+ );
+ $info['content'] = $build;
+ }
+ }
+
+ foreach (ctools_context_handler_get_task_arguments($task, $subtask) as $id => $argument) {
+ $plugin = ctools_get_argument($argument['name']);
+ $cid = ctools_context_id($argument, 'argument');
+ if (!empty($contexts[$cid]) && ($function = ctools_plugin_get_function($plugin, 'breadcrumb'))) {
+ $function($argument['settings'], $contexts[$cid]);
+ }
+ }
+
+ if (isset($info['title'])) {
+ drupal_set_title($info['title'], PASS_THROUGH);
+ }
+
+ // Only directly output if $page was set to true.
+ if (!empty($info['no_blocks'])) {
+ ctools_set_no_blocks(FALSE);
+ }
+ return $info['content'];
+}
+
+/**
+ * Provides contextual link for a task as defined by the handler.
+ *
+ * This provides a simple link to th main content operation and is suitable
+ * for most normal handlers. Setting 'contextual link' to a function overrides
+ * this and setting it to FALSE will prevent a contextual link from appearing.
+ */
+function ctools_task_handler_default_contextual_link($handler, $plugin, $contexts, $args) {
+ if (!user_access('administer page manager')) {
+ return;
+ }
+
+ $task = page_manager_get_task($handler->task);
+
+ $title = !empty($task['tab title']) ? $task['tab title'] : t('Edit @type', array('@type' => $plugin['title']));
+ $trail = array();
+ if (!empty($plugin['tab operation'])) {
+ if (is_array($plugin['tab operation'])) {
+ $trail = $plugin['tab operation'];
+ }
+ elseif (function_exists($plugin['tab operation'])) {
+ $trail = $plugin['tab operation']($handler, $contexts, $args);
+ }
+ }
+ $path = page_manager_edit_url(page_manager_make_task_name($handler->task, $handler->subtask), $trail);
+
+ $links = array(array(
+ 'href' => $path,
+ 'title' => $title,
+ 'query' => drupal_get_destination(),
+ ),
+ );
+
+ return $links;
+}
+
+/**
+ * Called to execute actions that should happen before a handler is rendered.
+ */
+function ctools_context_handler_pre_render($handler, $contexts, $args) {
+ foreach (module_implements('ctools_context_handler_pre_render') as $module) {
+ $function = $module . '_ctools_context_handler_pre_render';
+ $function($handler, $contexts, $args);
+ }
+}
+
+/**
+ * Compare arguments to contexts for selection purposes.
+ *
+ * @param object $handler
+ * The handler in question.
+ * @param object $contexts
+ * The context objects provided by the task.
+ *
+ * @return bool
+ * TRUE if these contexts match the selection rules. NULL or FALSE
+ * otherwise.
+ */
+function ctools_context_handler_select($handler, $contexts) {
+ if (empty($handler->conf['access'])) {
+ return TRUE;
+ }
+
+ ctools_include('context');
+ return ctools_access($handler->conf['access'], $contexts);
+}
+
+/**
+ * Get the array of summary strings for the arguments.
+ *
+ * These summary strings are used to communicate to the user what
+ * arguments the task handlers are selecting.
+ *
+ * @param object $task
+ * The loaded task plugin.
+ * @param object $subtask
+ * The subtask id.
+ * @param object $handler
+ * The handler to be checked.
+ *
+ * @return array
+ * Returns array of summary strings for arguments selected by task handlers.
+ */
+function ctools_context_handler_summary($task, $subtask, $handler) {
+ if (empty($handler->conf['access']['plugins'])) {
+ return array();
+ }
+
+ ctools_include('context');
+ $strings = array();
+ $contexts = ctools_context_handler_get_all_contexts($task, $subtask, $handler);
+
+ foreach ($handler->conf['access']['plugins'] as $test) {
+ $plugin = ctools_get_access_plugin($test['name']);
+ if ($string = ctools_access_summary($plugin, $contexts, $test)) {
+ $strings[] = $string;
+ }
+ }
+
+ return $strings;
+}
+
+// --------------------------------------------------------------------------
+// Tasks and Task handlers can both have their own sources of contexts.
+// Sometimes we need all of these contexts at once (when editing
+// the task handler, for example) but sometimes we need them separately
+// (when a task has contexts loaded and is trying out the task handlers,
+// for example). Therefore there are two paths we can take to getting contexts.
+/**
+ * Load the contexts for a task, using arguments.
+ *
+ * This creates the base array of contexts, loaded from arguments, suitable
+ * for use in rendering.
+ */
+function ctools_context_handler_get_task_contexts($task, $subtask, $args) {
+ $contexts = ctools_context_handler_get_base_contexts($task, $subtask);
+ $arguments = ctools_context_handler_get_task_arguments($task, $subtask);
+ ctools_context_get_context_from_arguments($arguments, $contexts, $args);
+
+ return $contexts;
+}
+
+/**
+ * Load the contexts for a task handler.
+ *
+ * This expands a base set of contexts passed in from a task with the
+ * contexts defined on the task handler. The contexts from the task
+ * must already have been loaded.
+ */
+function ctools_context_handler_get_handler_contexts($contexts, $handler) {
+ $object = ctools_context_handler_get_handler_object($handler);
+ return ctools_context_load_contexts($object, FALSE, $contexts);
+}
+
+/**
+ * Load the contexts for a task and task handler together.
+ *
+ * This pulls the arguments from a task and everything else from a task
+ * handler and loads them as a group. Since there is no data, this loads
+ * the contexts as placeholders.
+ */
+function ctools_context_handler_get_all_contexts($task, $subtask, $handler) {
+ $contexts = array();
+
+ $object = ctools_context_handler_get_task_object($task, $subtask, $handler);
+ $contexts = ctools_context_load_contexts($object, TRUE, $contexts);
+ ctools_context_handler_set_access_restrictions($task, $subtask, $handler, $contexts);
+ return $contexts;
+}
+
+/**
+ * Create an object suitable for use with the context system that kind of
+ * expects things in a certain, kind of clunky format.
+ */
+function ctools_context_handler_get_handler_object($handler) {
+ $object = new stdClass();
+ $object->name = $handler->name;
+ $object->contexts = isset($handler->conf['contexts']) ? $handler->conf['contexts'] : array();
+ $object->relationships = isset($handler->conf['relationships']) ? $handler->conf['relationships'] : array();
+
+ return $object;
+}
+
+/**
+ * Create an object suitable for use with the context system that kind of
+ * expects things in a certain, kind of clunky format. This one adds in
+ * arguments from the task.
+ */
+function ctools_context_handler_get_task_object($task, $subtask, $handler) {
+ $object = new stdClass();
+ $object->name = !empty($handler->name) ? $handler->name : 'temp';
+ $object->base_contexts = ctools_context_handler_get_base_contexts($task, $subtask, TRUE);
+ $object->arguments = ctools_context_handler_get_task_arguments($task, $subtask);
+ $object->contexts = isset($handler->conf['contexts']) ? $handler->conf['contexts'] : array();
+ $object->relationships = isset($handler->conf['relationships']) ? $handler->conf['relationships'] : array();
+
+ return $object;
+}
+
+/**
+ * Get base contexts from a task, if it has any.
+ *
+ * Tasks can get their contexts either from base contexts or arguments; base
+ * contexts extract their information from the environment.
+ */
+function ctools_context_handler_get_base_contexts($task, $subtask, $placeholders = FALSE) {
+ if ($function = ctools_plugin_get_function($task, 'get base contexts')) {
+ return $function($task, $subtask, $placeholders);
+ }
+
+ return array();
+}
+
+/**
+ * Get the arguments from a task that are used to load contexts.
+ */
+function ctools_context_handler_get_task_arguments($task, $subtask) {
+ if ($function = ctools_plugin_get_function($task, 'get arguments')) {
+ return $function($task, $subtask);
+ }
+
+ return array();
+}
+
+/**
+ * Set any access restrictions on the contexts for a handler.
+ *
+ * Both the task and the handler could add restrictions to the contexts
+ * based upon the access control. These restrictions might be useful
+ * to limit what kind of content appears in the add content dialog;
+ * for example, if we have an access item that limits a node context
+ * to only 'story' and 'page' types, there is no need for content that
+ * only applies to the 'poll' type to appear.
+ */
+function ctools_context_handler_set_access_restrictions($task, $subtask, $handler, &$contexts) {
+ // First, for the task:
+ if ($function = ctools_plugin_get_function($task, 'access restrictions')) {
+ $function($task, $subtask, $contexts);
+ }
+
+ // Then for the handler:
+ if (isset($handler->conf['access'])) {
+ ctools_access_add_restrictions($handler->conf['access'], $contexts);
+ }
+}
+
+/**
+ * Form to choose context based selection rules for a task handler.
+ *
+ * The configuration will be assumed to go simply in $handler->conf and
+ * will be keyed by the argument ID.
+ */
+function ctools_context_handler_edit_criteria($form, &$form_state) {
+ if (!isset($form_state['handler']->conf['access'])) {
+ $form_state['handler']->conf['access'] = array();
+ }
+
+ ctools_include('context');
+ ctools_include('modal');
+ ctools_include('ajax');
+ ctools_modal_add_plugin_js(ctools_get_access_plugins());
+ ctools_include('context-access-admin');
+ $form_state['module'] = (isset($form_state['module'])) ? $form_state['module'] : 'page_manager_task_handler';
+ // Encode a bunch of info into the argument so we can get our cache later.
+ $form_state['callback argument'] = $form_state['task_name'] . '*' . $form_state['handler']->name;
+ $form_state['access'] = $form_state['handler']->conf['access'];
+ $form_state['no buttons'] = TRUE;
+ $form_state['contexts'] = ctools_context_handler_get_all_contexts($form_state['task'], $form_state['subtask'], $form_state['handler']);
+
+ $form['markup'] = array(
+ '#markup' => '' .
+ t('If there is more than one variant on a page, when the page is visited each variant is given an opportunity to be displayed. Starting from the first variant and working to the last, each one tests to see if its selection rules will pass. The first variant that meets its criteria (as specified below) will be used.') .
+ '
',
+ );
+ $form = ctools_access_admin_form($form, $form_state);
+ return $form;
+}
+
+/**
+ * Submit handler for rules selection.
+ */
+function ctools_context_handler_edit_criteria_submit(&$form, &$form_state) {
+ $form_state['handler']->conf['access']['logic'] = $form_state['values']['logic'];
+}
+
+/**
+ * Edit contexts that go with this panel.
+ */
+function ctools_context_handler_edit_context($form, &$form_state) {
+ ctools_include('context-admin');
+ ctools_context_admin_includes();
+
+ $handler = $form_state['handler'];
+ $page = $form_state['page'];
+ $cache_name = $handler->name ? $handler->name : 'temp';
+ if (isset($page->context_cache[$cache_name])) {
+ $cache = $page->context_cache[$cache_name];
+ }
+ else {
+ $cache = ctools_context_handler_get_task_object($form_state['task'], $form_state['subtask'], $form_state['handler']);
+ $form_state['page']->context_cache[$cache_name] = $cache;
+ }
+
+ $form['right'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ $form['left'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ $module = 'page_manager_context::' . $page->task_name;
+ ctools_context_add_context_form($module, $form, $form_state, $form['right']['contexts_table'], $cache);
+ ctools_context_add_relationship_form($module, $form, $form_state, $form['right']['relationships_table'], $cache);
+
+ $theme_vars = array();
+ $theme_vars['object'] = $cache;
+ $theme_vars['header'] = t('Summary of contexts');
+ $form['left']['summary'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#markup' => theme('ctools_context_list', $theme_vars),
+ );
+
+ $form_state['context_object'] = &$cache;
+ return $form;
+}
+
+/**
+ * Process submission of the context edit form.
+ */
+function ctools_context_handler_edit_context_submit(&$form, &$form_state) {
+ $handler = &$form_state['handler'];
+
+ $cache_name = $handler->name ? $handler->name : 'temp';
+
+ $handler->conf['contexts'] = $form_state['context_object']->contexts;
+ $handler->conf['relationships'] = $form_state['context_object']->relationships;
+ if (isset($form_state['page']->context_cache[$cache_name])) {
+ unset($form_state['page']->context_cache[$cache_name]);
+ }
+}
diff --git a/sites/all/modules/ctools/includes/context.inc b/sites/all/modules/ctools/includes/context.inc
new file mode 100644
index 0000000..0fc6208
--- /dev/null
+++ b/sites/all/modules/ctools/includes/context.inc
@@ -0,0 +1,2095 @@
+type = $type;
+ $this->data = $data;
+ $this->title = t('Unknown context');
+ $this->page_title = '';
+ $this->identifier = '';
+ $this->keyword = '';
+ $this->restrictions = array();
+ $this->empty = FALSE;
+ // Other vars are NULL.
+ }
+
+ /**
+ * Determine whether this object is of type @var $type .
+ *
+ * Both the internal value ($this->type) and the supplied value ($type) can
+ * be a string or an array of strings, and if one or both are arrays the match
+ * succeeds if at least one common element is found.
+ *
+ * Type names
+ *
+ * @param string|array $type
+ * 'type' can be:
+ * - 'any' to match all types (this is true of the internal value too).
+ * - an array of type name strings, when more than one type is acceptable.
+ *
+ * @return bool
+ * True if the type matches, False otherwise.
+ */
+ public function is_type($type) {
+ if ($type === 'any' || $this->type === 'any') {
+ return TRUE;
+ }
+
+ $a = is_array($type) ? $type : array($type);
+ $b = is_array($this->type) ? $this->type : array($this->type);
+ return (bool) array_intersect($a, $b);
+ }
+
+ /**
+ * Return the argument.
+ *
+ * @return mixed
+ * The value of $argument.
+ */
+ public function get_argument() {
+ return $this->argument;
+ }
+
+ /**
+ * Return the value of argument (or arg) variable as it was passed in.
+ *
+ * For example see ctools_plugin_load_function() and ctools_terms_context().
+ *
+ * @return mixed
+ * The original arg value.
+ */
+ public function get_original_argument() {
+ if (!is_null($this->original_argument)) {
+ return $this->original_argument;
+ }
+ return $this->argument;
+ }
+
+ /**
+ * Return the keyword.
+ *
+ * @return mixed
+ * The value of $keyword.
+ */
+ public function get_keyword() {
+ return $this->keyword;
+ }
+
+ /**
+ * Return the identifier.
+ *
+ * @return mixed
+ * The value of $identifier.
+ */
+ public function get_identifier() {
+ return $this->identifier;
+ }
+
+ /**
+ * Return the title.
+ *
+ * @return mixed
+ * The value of $title.
+ */
+ public function get_title() {
+ return $this->title;
+ }
+
+ /**
+ * Return the page title.
+ *
+ * @return mixed
+ * The value of $page_title.
+ */
+ public function get_page_title() {
+ return $this->page_title;
+ }
+
+}
+
+/**
+ * Used to create a method of comparing if a list of contexts
+ * match a required context type.
+ */
+class ctools_context_required {
+ /**
+ * @var array
+ * Keyword strings associated with the context.
+ */
+ public $keywords;
+
+ /**
+ * If set, the title will be used in the selector to identify
+ * the context. This is very useful when multiple contexts
+ * are required to inform the user will be used for what.
+ */
+ public $title;
+
+ /**
+ * Test to see if this context is required.
+ */
+ public $required = TRUE;
+
+ /**
+ * If TRUE, skip the check in ctools_context_required::select()
+ * for contexts whose names may have changed.
+ */
+ public $skip_name_check = FALSE;
+
+ /**
+ * The ctools_context_required constructor.
+ *
+ * Note: Constructor accepts a variable number of arguments, with optional
+ * type-dependent args at the end of the list and one required argument,
+ * the title. Note in particular that skip_name_check MUST be passed in as
+ * a boolean (and not, for example, as an integer).
+ *
+ * @param string $title
+ * The title of the context for use in UI selectors when multiple contexts
+ * qualify.
+ * @param string $keywords
+ * One or more keywords to use for matching which contexts are allowed.
+ * @param array $restrictions
+ * Array of context restrictions.
+ * @param bool $skip_name_check
+ * If True, skip the check in select() for contexts whose names may have
+ * changed.
+ */
+ public function __construct($title) {
+ // If it was possible, using variadic syntax this should be:
+ // __construct($title, string ...$keywords, array $restrictions = NULL, bool $skip = NULL)
+ // but that form isn't allowed.
+ $args = func_get_args();
+ $this->title = array_shift($args);
+
+ // If we have a boolean value at the end for $skip_name_check, store it.
+ if (is_bool(end($args))) {
+ $this->skip_name_check = array_pop($args);
+ }
+
+ // If we were given restrictions at the end, store them.
+ if (count($args) > 1 && is_array(end($args))) {
+ $this->restrictions = array_pop($args);
+ }
+
+ if (count($args) === 1) {
+ $args = array_shift($args);
+ }
+ $this->keywords = $args;
+ }
+
+ /**
+ * Filter the contexts to determine which apply in the current environment.
+ *
+ * A context passes the filter if:
+ * - the context matches 'type' of the required keywords (uses
+ * ctools_context::is_type(), so includes 'any' matches, etc).
+ * - AND if restrictions are present, there are some common elements between
+ * the requirement and the context.
+ *
+ * @param array $contexts
+ * An array of ctools_context objects (or something which will cast to an
+ * array of them). The contexts to apply the filter on.
+ *
+ * @return array
+ * An array of context objects, keyed with the same keys used for $contexts,
+ * which pass the filter.
+ *
+ * @see ctools_context::is_type()
+ */
+ public function filter($contexts) {
+ $result = array();
+
+ /**
+ * See which of these contexts are valid.
+ * @var ctools_context $context
+ */
+ foreach ((array) $contexts as $cid => $context) {
+ if ($context->is_type($this->keywords)) {
+
+ // Compare to see if our contexts were met.
+ if (!empty($this->restrictions) && !empty($context->restrictions)) {
+
+ foreach ($this->restrictions as $key => $values) {
+ // If we have a restriction, the context must either not have that
+ // restriction listed, which means we simply don't know what it is,
+ // or there must be an intersection of the restricted values on
+ // both sides.
+ if (!is_array($values)) {
+ $values = array($values);
+ }
+
+ if (!empty($context->restrictions[$key])
+ && !array_intersect($values, $context->restrictions[$key])
+ ) {
+ // Break out to check next context; this one fails the filter.
+ continue 2;
+ }
+ }
+ }
+ // This context passes the filter.
+ $result[$cid] = $context;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Select one context from the list of contexts, accounting for changed IDs.
+ *
+ * Fundamentally, this returns $contexts[$context] or FALSE if that does not
+ * exist. Additional logic accounts for changes in context names and dealing
+ * with a $contexts parameter that is not an array.
+ *
+ * If we had requested a $context but that $context doesn't exist in our
+ * context list, there is a good chance that what happened is the context
+ * IDs changed. Look for another context that satisfies our requirements,
+ * unless $skip_name_check is set.
+ *
+ * @param ctools_context|array $contexts
+ * A context, or an array of ctools_context.
+ * @param string $context
+ * A context ID.
+ *
+ * @return bool|ctools_context
+ * The matching ctools_context, or False if no such context was found.
+ */
+ public function select($contexts, $context) {
+ // Easier to deal with a standalone object as a 1-element array of objects.
+ if (!is_array($contexts)) {
+ if (is_object($contexts) && $contexts instanceof ctools_context) {
+ $contexts = array($contexts->id => $contexts);
+ }
+ else {
+ $contexts = array($contexts);
+ }
+ }
+
+ // If we had requested a $context but that $context doesn't exist in our
+ // context list, there is a good chance that what happened is the context
+ // IDs changed. Check for another context that satisfies our requirements.
+ if (!$this->skip_name_check
+ && !empty($context) && !isset($contexts[$context])
+ ) {
+ $choices = $this->filter($contexts);
+
+ // If we got a hit, take the first one that matches.
+ if ($choices) {
+ $keys = array_keys($choices);
+ $context = reset($keys);
+ }
+ }
+
+ if (empty($context) || empty($contexts[$context])) {
+ return FALSE;
+ }
+ return $contexts[$context];
+ }
+
+}
+
+/**
+ * Used to compare to see if a list of contexts match an optional context. This
+ * can produce empty contexts to use as placeholders.
+ */
+class ctools_context_optional extends ctools_context_required {
+
+ /**
+ * {@inheritdoc}
+ */
+ public $required = FALSE;
+
+ /**
+ * Add the 'empty' context to the existing set.
+ *
+ * @param array &$contexts
+ * An array of ctools_context objects.
+ */
+ public function add_empty(&$contexts) {
+ $context = new ctools_context('any');
+ $context->title = t('No context');
+ $context->identifier = t('No context');
+ $contexts['empty'] = $context;
+ }
+
+ /**
+ * Filter the contexts to determine which apply in the current environment.
+ *
+ * As for ctools_context_required, but we add the empty context to those
+ * passed in so the check is optional (i.e. if nothing else matches, the
+ * empty context will, and so there will always be at least one matched).
+ *
+ * @param array $contexts
+ * An array of ctools_context objects (or something which will cast to an
+ * array of them). The contexts to apply the filter on.
+ *
+ * @return array
+ * An array of context objects, keyed with the same keys used for $contexts,
+ * which pass the filter.
+ *
+ * @see ctools_context::is_type()
+ */
+ public function filter($contexts) {
+ /**
+ * @todo We are assuming here that $contexts is actually an array, whereas
+ * ctools_context_required::filter only requires $contexts is convertible
+ * to an array.
+ */
+ $this->add_empty($contexts);
+ return parent::filter($contexts);
+ }
+
+ /**
+ * Select and return one context from the list of applicable contexts.
+ *
+ * Fundamentally, this returns $contexts[$context] or the empty context if
+ * that does not exist.
+ *
+ * @param array $contexts
+ * The applicable contexts to check.
+ * @param string $context
+ * The context id to check for.
+ *
+ * @return bool|ctools_context
+ * The matching ctools_context, or False if no such context was found.
+ *
+ * @see ctools_context_required::select()
+ */
+ public function select($contexts, $context) {
+ /**
+ * @todo We are assuming here that $contexts is actually an array, whereas
+ * ctools_context_required::select permits ctools_context objects as well.
+ */
+ $this->add_empty($contexts);
+ if (empty($context)) {
+ return $contexts['empty'];
+ }
+
+ $result = parent::select($contexts, $context);
+
+ // Don't flip out if it can't find the context; this is optional, put
+ // in an empty.
+ if ($result === FALSE) {
+ $result = $contexts['empty'];
+ }
+ return $result;
+ }
+
+}
+
+/**
+ * Return a keyed array of context that match the given 'required context'
+ * filters.
+ *
+ * Functions or systems that require contexts of a particular type provide a
+ * ctools_context_required or ctools_context_optional object. This function
+ * examines that object and an array of contexts to determine which contexts
+ * match the filter.
+ *
+ * Since multiple contexts can be required, this function will accept either
+ * an array of all required contexts, or just a single required context object.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param array|ctools_context_required|ctools_context_optional $required
+ * A *_required or *_optional object, or an array of such objects, which
+ * define the selection condition.
+ *
+ * @return array
+ * A keyed array of contexts that match the filter.
+ */
+function ctools_context_filter($contexts, $required) {
+ if (is_array($required)) {
+ $result = array();
+ foreach ($required as $item) {
+ $result = array_merge($result, _ctools_context_filter($contexts, $item));
+ }
+ return $result;
+ }
+
+ return _ctools_context_filter($contexts, $required);
+}
+
+/**
+ * Helper function for ctools_context_filter().
+ *
+ * Used to transform the required context during the merge into the final array.
+ *
+ * @internal This function DOES NOT form part of the CTools API.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param ctools_context_required|ctools_context_optional $required
+ * A ctools_context_required or ctools_context_optional object, although if
+ * given something else will return an empty array.
+ *
+ * @return array
+ */
+function _ctools_context_filter($contexts, $required) {
+ $result = array();
+
+ if (is_object($required)) {
+ $result = $required->filter($contexts);
+ }
+
+ return $result;
+}
+
+/**
+ * Create a select box to choose possible contexts.
+ *
+ * This only creates a selector if there is actually a choice; if there
+ * is only one possible context, that one is silently assigned.
+ *
+ * If an array of required contexts is provided, one selector will be
+ * provided for each context.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param array|ctools_context_required|ctools_context_optional $required
+ * The required context object or array of objects.
+ * @param array|string $default
+ * The default value for the select object, suitable for a #default_value
+ * render key. Where $required is an array, this is an array keyed by the
+ * same key values as $required for all keys where an empty string is not a
+ * suitable default. Otherwise it is just the default value.
+ *
+ * @return array
+ * A form element, or NULL if there are no contexts that satisfy the
+ * requirements.
+ */
+function ctools_context_selector($contexts, $required, $default) {
+ if (is_array($required)) {
+ $result = array('#tree' => TRUE);
+ $count = 1;
+ foreach ($required as $id => $item) {
+ $result[] = _ctools_context_selector(
+ $contexts, $item, isset($default[$id]) ? $default[$id] : '', $count++
+ );
+ }
+ return $result;
+ }
+
+ return _ctools_context_selector($contexts, $required, $default);
+}
+
+/**
+ * Helper function for ctools_context_selector().
+ *
+ * @internal This function DOES NOT form part of the CTools API. Use the API
+ * function ctools_context_selector() instead.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param ctools_context_required|ctools_context_optional $required
+ * The required context object.
+ * @param $default
+ * The default value for the select object, suitable for a #default_value
+ * render key.
+ * @param int $num
+ * If supplied and non-zero, the title of the select form element will be
+ * "Context $num", otherwise it will be "Context".
+ *
+ * @return array
+ * A form element, or NULL if there are no contexts that satisfy the
+ * requirements.
+ */
+function _ctools_context_selector($contexts, $required, $default, $num = 0) {
+ $filtered = ctools_context_filter($contexts, $required);
+ $count = count($filtered);
+
+ $form = array();
+
+ if ($count >= 1) {
+ // If there's more than one to choose from, create a select widget.
+ foreach ($filtered as $cid => $context) {
+ $options[$cid] = $context->get_identifier();
+ }
+ if (!empty($required->title)) {
+ $title = $required->title;
+ }
+ else {
+ $title = $num ? t('Context %count', array('%count' => $num)) : t('Context');
+ }
+
+ $form = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => $title,
+ '#default_value' => $default,
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Are there enough contexts for a plugin?
+ *
+ * Some plugins can have a 'required contexts' item which can either
+ * be a context requirement object or an array of them. When contexts
+ * are required, items that do not have enough contexts should not
+ * appear. This tests an item to see if it has enough contexts
+ * to actually appear.
+ *
+ * @param $contexts
+ * A keyed array of all available contexts.
+ * @param $required
+ * The required context object or array of objects.
+ *
+ * @return bool
+ * True if there are enough contexts, otherwise False.
+ */
+function ctools_context_match_requirements($contexts, $required) {
+ if (!is_array($required)) {
+ $required = array($required);
+ }
+
+ // Get the keys to avoid bugs in PHP 5.0.8 with keys and loops.
+ // And use it to remove optional contexts.
+ $keys = array_keys($required);
+ foreach ($keys as $key) {
+ if (empty($required[$key]->required)) {
+ unset($required[$key]);
+ }
+ }
+
+ $count = count($required);
+ return (count(ctools_context_filter($contexts, $required)) >= $count);
+}
+
+/**
+ * Create a select box to choose possible contexts.
+ *
+ * This only creates a selector if there is actually a choice; if there
+ * is only one possible context, that one is silently assigned.
+ *
+ * If an array of required contexts is provided, one selector will be
+ * provided for each context.
+ *
+ * @param $contexts
+ * A keyed array of all available contexts.
+ * @param $required
+ * The required context object or array of objects.
+ * @param array|string $default
+ * The default value for the select object, suitable for a #default_value
+ * render key. Where $required is an array, this is an array keyed by the
+ * same key values as $required for all keys where an empty string is not a
+ * suitable default. Otherwise it is just the default value.
+ *
+ * @return array
+ * A form element, or NULL if there are no contexts that satisfy the
+ * requirements.
+ */
+function ctools_context_converter_selector($contexts, $required, $default) {
+ if (is_array($required)) {
+ $result = array('#tree' => TRUE);
+ $count = 1;
+ foreach ($required as $id => $dependency) {
+ $default_id = isset($default[$id]) ? $default[$id] : '';
+ $result[] = _ctools_context_converter_selector(
+ $contexts, $dependency, $default_id, $count++
+ );
+ }
+ return $result;
+ }
+
+ return _ctools_context_converter_selector($contexts, $required, $default);
+}
+
+/**
+ * Helper function for ctools_context_converter_selector().
+ *
+ * @internal This function DOES NOT form part of the CTools API. Use the API
+ * function ctools_context_converter_selector() instead.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param ctools_context $required
+ * The required context object.
+ * @param $default
+ * The default value for the select object, suitable for a #default_value
+ * render key.
+ * @param int $num
+ * If supplied and non-zero, the title of the select form element will be
+ * "Context $num", otherwise it will be "Context".
+ *
+ * @return array|null
+ * A form element, or NULL if there are no contexts that satisfy the
+ * requirements.
+ */
+function _ctools_context_converter_selector($contexts, $required, $default, $num = 0) {
+ $filtered = ctools_context_filter($contexts, $required);
+ $count = count($filtered);
+
+ if ($count > 1) {
+ // If there's more than one to choose from, create a select widget.
+ $options = array();
+ foreach ($filtered as $cid => $context) {
+ if ($context->type === 'any') {
+ $options[''] = t('No context');
+ continue;
+ }
+ $key = $context->get_identifier();
+ if ($converters = ctools_context_get_converters($cid . '.', $context)) {
+ $options[$key] = $converters;
+ }
+ }
+ if (empty($options)) {
+ return array(
+ '#type' => 'value',
+ '#value' => 'any',
+ );
+ }
+ if (!empty($required->title)) {
+ $title = $required->title;
+ }
+ else {
+ $title = $num ? t('Context %count', array('%count' => $num)) : t('Context');
+ }
+
+ return array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => $title,
+ '#description' => t('Please choose which context and how you would like it converted.'),
+ '#default_value' => $default,
+ );
+ }
+ else {
+ // Not enough choices to need a selector, so don't make one.
+ return NULL;
+ }
+}
+
+/**
+ * Get a list of converters available for a given context.
+ *
+ * @param string $cid
+ * A context ID.
+ * @param ctools_context $context
+ * The context for which converters are needed.
+ *
+ * @return array
+ * A list of context converters.
+ */
+function ctools_context_get_converters($cid, $context) {
+ if (empty($context->plugin)) {
+ return array();
+ }
+
+ return _ctools_context_get_converters($cid, $context->plugin);
+}
+
+/**
+ * Get a list of converters available for a given context.
+ *
+ * @internal This function DOES NOT form part of the CTools API. Use the API
+ * function ctools_context_get_converters() instead.
+ *
+ * @param string $id
+ * A context ID.
+ * @param string $plugin_name
+ * The name of the context plugin.
+ *
+ * @return array
+ * A list of context converters.
+ */
+function _ctools_context_get_converters($id, $plugin_name) {
+ $plugin = ctools_get_context($plugin_name);
+ if (empty($plugin['convert list'])) {
+ return array();
+ }
+
+ $converters = array();
+ if (is_array($plugin['convert list'])) {
+ $converters = $plugin['convert list'];
+ }
+ elseif ($function = ctools_plugin_get_function($plugin, 'convert list')) {
+ $converters = (array) $function($plugin);
+ }
+
+ foreach (module_implements('ctools_context_convert_list_alter') as $module) {
+ $function = $module . '_ctools_context_convert_list_alter';
+ $function($plugin, $converters);
+ }
+
+ // Now, change them all to include the plugin:
+ $return = array();
+ foreach ($converters as $key => $title) {
+ $return[$id . $key] = $title;
+ }
+
+ natcasesort($return);
+ return $return;
+}
+
+/**
+ * Get a list of all contexts converters available.
+ *
+ * For all contexts returned by ctools_get_contexts(), return the converter
+ * for all contexts that have one.
+ *
+ * @return array
+ * A list of context converters, keyed by the title of the converter.
+ */
+function ctools_context_get_all_converters() {
+ $contexts = ctools_get_contexts();
+ $converters = array();
+ foreach ($contexts as $name => $context) {
+ if (empty($context['no required context ui'])) {
+ $context_converters = _ctools_context_get_converters($name . '.', $name);
+ if ($context_converters) {
+ $converters[$context['title']] = $context_converters;
+ }
+ }
+ }
+
+ return $converters;
+}
+
+/**
+ * Let the context convert an argument based upon the converter that was given.
+ *
+ * @param ctools_context $context
+ * The context object.
+ * @param string $converter
+ * The type of converter to use, which should be a string provided by the
+ * converter list function.
+ * @param array $converter_options
+ * An array of options to pass on to the generation function. For contexts
+ * that use token module, of particular use is 'sanitize' => FALSE which can
+ * get raw tokens. This should ONLY be used in values that will later be
+ * treated as unsafe user input since these values are by themselves unsafe.
+ * It is particularly useful to get raw values from Field API.
+ *
+ * @return string|null
+ */
+function ctools_context_convert_context($context, $converter, $converter_options = array()) {
+ // Contexts without plugins might be optional placeholders.
+ if (empty($context->plugin)) {
+ return NULL;
+ }
+
+ $value = $context->argument;
+ $plugin = ctools_get_context($context->plugin);
+ if ($function = ctools_plugin_get_function($plugin, 'convert')) {
+ $value = $function($context, $converter, $converter_options);
+ }
+
+ foreach (module_implements('ctools_context_converter_alter') as $module) {
+ $function = $module . '_ctools_context_converter_alter';
+ $function($context, $converter, $value, $converter_options);
+ }
+
+ return $value;
+}
+
+/**
+ * Choose a context or contexts based upon the selection made via
+ * ctools_context_filter.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param array|ctools_context_required $required
+ * The required context object(s) provided by the plugin.
+ * @param $context
+ * The selection made using ctools_context_selector().
+ *
+ * @return ctools_context|array|false
+ * Returns FALSE if $required is not an object, or array of objects, or
+ * the value of $required->select() for the context, or an array of those (if
+ * passed an array in $required).
+ */
+function ctools_context_select($contexts, $required, $context) {
+ if (is_array($required)) {
+
+ /**
+ * @var array $required
+ * Array of required context objects.
+ * @var ctools_context_required $item
+ * A required context object.
+ */
+ $result = array();
+ foreach ($required as $id => $item) {
+ // @todo What's the difference between the following and "empty($item)" ?
+ if (empty($required[$id])) {
+ continue;
+ }
+
+ if (($result[] = _ctools_context_select($contexts, $item, $context[$id])) === FALSE) {
+ return FALSE;
+ }
+ }
+ return $result;
+ }
+
+ return _ctools_context_select($contexts, $required, $context);
+}
+
+/**
+ * Helper function for calling the required context object's selection function.
+ *
+ * This function DOES NOT form part of the CTools API.
+ *
+ * @param array $contexts
+ * A keyed array of all available contexts.
+ * @param ctools_context_required $required
+ * The required context object provided by the plugin.
+ * @param $context
+ * The selection made using ctools_context_selector().
+ *
+ * @return ctools_context|bool
+ * FALSE if the $required is not an object. A ctools_context object if one
+ * matched.
+ */
+function _ctools_context_select($contexts, $required, $context) {
+ if (!is_object($required)) {
+ return FALSE;
+ }
+
+ return $required->select($contexts, $context);
+}
+
+/**
+ * Create a new context object.
+ *
+ * @param string $type
+ * The type of context to create; this loads a plugin.
+ * @param mixed $data
+ * The data to put into the context.
+ * @param $conf
+ * A configuration structure if this context was created via UI.
+ *
+ * @return ctools_context
+ * A $context or NULL if one could not be created.
+ */
+function ctools_context_create($type, $data = NULL, $conf = FALSE) {
+ ctools_include('plugins');
+ $plugin = ctools_get_context($type);
+
+ if ($function = ctools_plugin_get_function($plugin, 'context')) {
+ return $function(FALSE, $data, $conf, $plugin);
+ }
+}
+
+/**
+ * Create an empty context object.
+ *
+ * Empty context objects are primarily used as placeholders in the UI where
+ * the actual contents of a context object may not be known. It may have
+ * additional text embedded to give the user clues as to how the context
+ * is used.
+ *
+ * @param $type
+ * The type of context to create; this loads a plugin.
+ *
+ * @return ctools_context
+ * A $context or NULL if one could not be created.
+ */
+function ctools_context_create_empty($type) {
+ $plugin = ctools_get_context($type);
+ if ($function = ctools_plugin_get_function($plugin, 'context')) {
+ $context = $function(TRUE, NULL, FALSE, $plugin);
+ if (is_object($context)) {
+ $context->empty = TRUE;
+ }
+
+ return $context;
+ }
+}
+
+/**
+ * Perform keyword and context substitutions.
+ *
+ * @param string $string
+ * The string in which to replace keywords.
+ * @param array $keywords
+ * Array of keyword-replacement pairs.
+ * @param array $contexts
+ *
+ * @param array $converter_options
+ * Options to pass on to ctools_context_convert_context(), defaults to an
+ * empty array.
+ *
+ * @return string
+ * The returned string, with substitutions performed.
+ */
+function ctools_context_keyword_substitute($string, $keywords, $contexts, array $converter_options = array()) {
+ // Ensure a default keyword exists:
+ $keywords['%%'] = '%';
+
+ // Match contexts to the base keywords:
+ $context_keywords = array();
+ foreach ($contexts as $context) {
+ if (isset($context->keyword)) {
+ $context_keywords[$context->keyword] = $context;
+ }
+ }
+
+ // Look for context matches we we only have to convert known matches.
+ $matches = array();
+ if (preg_match_all('/%(%|[a-zA-Z0-9_-]+(?:\:[a-zA-Z0-9_-]+)*)/us', $string, $matches)) {
+ foreach ($matches[1] as $keyword) {
+ // Ignore anything it finds with %%.
+ if ($keyword[0] == '%') {
+ continue;
+ }
+
+ // If the keyword is already set by something passed in, don't try to
+ // overwrite it.
+ if (array_key_exists('%' . $keyword, $keywords)) {
+ continue;
+ }
+
+ // Figure out our keyword and converter, if specified.
+ if (strpos($keyword, ':')) {
+ list($context, $converter) = explode(':', $keyword, 2);
+ }
+ else {
+ $context = $keyword;
+ if (isset($context_keywords[$keyword])) {
+ $plugin = ctools_get_context($context_keywords[$context]->plugin);
+
+ // Fall back to a default converter, if specified.
+ if ($plugin && !empty($plugin['convert default'])) {
+ $converter = $plugin['convert default'];
+ }
+ }
+ }
+
+ if (!isset($context_keywords[$context])) {
+ $keywords['%' . $keyword] = '%' . $keyword;
+ }
+ else {
+ if (empty($context_keywords[$context]) || !empty($context_keywords[$context]->empty)) {
+ $keywords['%' . $keyword] = '';
+ }
+ else {
+ if (!empty($converter)) {
+ $keywords['%' . $keyword] = ctools_context_convert_context($context_keywords[$context], $converter, $converter_options);
+ }
+ else {
+ $keywords['%' . $keyword] = $context_keywords[$keyword]->title;
+ }
+ }
+ }
+ }
+ }
+ return strtr($string, $keywords);
+}
+
+/**
+ * Determine a unique context ID for a context.
+ *
+ * Often contexts of many different types will be placed into a list. This
+ * ensures that even though contexts of multiple types may share IDs, they
+ * are unique in the final list.
+ */
+function ctools_context_id($context, $type = 'context') {
+ // If not set, FALSE or empty.
+ if (!isset($context['id']) || !$context['id']) {
+ $context['id'] = 1;
+ }
+
+ // @todo is '' the appropriate default value?
+ $name = isset($context['name']) ? $context['name'] : '';
+
+ return $type . '_' . $name . '_' . $context['id'];
+}
+
+/**
+ * Get the next id available given a list of already existing objects.
+ *
+ * This finds the next id available for the named object.
+ *
+ * @param array $objects
+ * A list of context descriptor objects, i.e, arguments, relationships,
+ * contexts, etc.
+ * @param string $name
+ * The name being used.
+ *
+ * @return int
+ * The next integer id available.
+ */
+function ctools_context_next_id($objects, $name) {
+ $id = 0;
+ // Figure out which instance of this argument we're creating.
+ if (!$objects) {
+ return $id + 1;
+ }
+
+ foreach ($objects as $object) {
+ if (isset($object['name']) && $object['name'] === $name) {
+ if (isset($object['id']) && $object['id'] > $id) {
+ $id = $object['id'];
+ }
+ // @todo If obj has no 'id', should we increment local id? $id = $id + 1;
+ }
+ }
+
+ return $id + 1;
+}
+
+// ---------------------------------------------------------------------------
+// Functions related to contexts from arguments.
+/**
+ * Fetch metadata for a specific argument plugin.
+ *
+ * @param $argument
+ * Name of an argument plugin.
+ *
+ * @return array
+ * An array with information about the requested argument plugin.
+ */
+
+function ctools_get_argument($argument) {
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'arguments', $argument);
+}
+
+/**
+ * Fetch metadata for all argument plugins.
+ *
+ * @return array
+ * An array of arrays with information about all available argument plugins.
+ */
+function ctools_get_arguments() {
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'arguments');
+}
+
+/**
+ * Get a context from an argument.
+ *
+ * @param $argument
+ * The configuration of an argument. It must contain the following data:
+ * - name: The name of the argument plugin being used.
+ * - argument_settings: The configuration based upon the plugin forms.
+ * - identifier: The human readable identifier for this argument, usually
+ * defined by the UI.
+ * - keyword: The keyword used for this argument for substitutions.
+ *
+ * @param $arg
+ * The actual argument received. This is expected to be a string from a URL
+ * but this does not have to be the only source of arguments.
+ * @param $empty
+ * If true, the $arg will not be used to load the context. Instead, an empty
+ * placeholder context will be loaded.
+ *
+ * @return ctools_context
+ * A context object if one can be loaded.
+ */
+function ctools_context_get_context_from_argument($argument, $arg, $empty = FALSE) {
+ ctools_include('plugins');
+ if (empty($argument['name'])) {
+ return NULL;
+ }
+
+ $function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'context');
+ if ($function) {
+ // Backward compatibility: Merge old style settings into new style:
+ if (!empty($argument['settings'])) {
+ $argument += $argument['settings'];
+ unset($argument['settings']);
+ }
+
+ $context = $function($arg, $argument, $empty);
+
+ if (is_object($context)) {
+ $context->identifier = $argument['identifier'];
+ $context->page_title = isset($argument['title']) ? $argument['title'] : '';
+ $context->keyword = $argument['keyword'];
+ $context->id = ctools_context_id($argument, 'argument');
+ $context->original_argument = $arg;
+
+ if (!empty($context->empty)) {
+ $context->placeholder = array(
+ 'type' => 'argument',
+ 'conf' => $argument,
+ );
+ }
+ }
+ return $context;
+ }
+}
+
+/**
+ * Retrieve a list of empty contexts for all arguments.
+ *
+ * @param array $arguments
+ *
+ * @return array
+ *
+ * @see ctools_context_get_context_from_arguments()
+ */
+function ctools_context_get_placeholders_from_argument($arguments) {
+ $contexts = array();
+ foreach ($arguments as $argument) {
+ $context = ctools_context_get_context_from_argument($argument, NULL, TRUE);
+ if ($context) {
+ $contexts[ctools_context_id($argument, 'argument')] = $context;
+ }
+ }
+ return $contexts;
+}
+
+/**
+ * Load the contexts for a given list of arguments.
+ *
+ * @param array $arguments
+ * The array of argument definitions.
+ * @param array &$contexts
+ * The array of existing contexts. New contexts will be added to this array.
+ * @param array $args
+ * The arguments to load.
+ *
+ * @return bool
+ * TRUE if all is well, FALSE if an argument wants to 404.
+ *
+ * @see ctools_context_get_context_from_argument()
+ */
+function ctools_context_get_context_from_arguments($arguments, &$contexts, $args) {
+ foreach ($arguments as $argument) {
+ // Pull the argument off the list.
+ $arg = array_shift($args);
+ $id = ctools_context_id($argument, 'argument');
+
+ // For % arguments embedded in the URL, our context is already loaded.
+ // There is no need to go and load it again.
+ if (empty($contexts[$id])) {
+ if ($context = ctools_context_get_context_from_argument($argument, $arg)) {
+ $contexts[$id] = $context;
+ }
+ }
+ else {
+ $context = $contexts[$id];
+ }
+
+ if ((empty($context) || empty($context->data))
+ && !empty($argument['default'])
+ && $argument['default'] === '404'
+ ) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+// ---------------------------------------------------------------------------
+// Functions related to contexts from relationships.
+/**
+ * Fetch plugin metadata for a specific relationship plugin.
+ *
+ * @param $relationship
+ * Name of a panel content type.
+ *
+ * @return array
+ * An array with information about the requested relationship.
+ *
+ * @see ctools_get_relationships()
+ */
+
+function ctools_get_relationship($relationship) {
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'relationships', $relationship);
+}
+
+/**
+ * Fetch metadata for all relationship plugins.
+ *
+ * @return array
+ * An array of arrays with information about all available relationships.
+ *
+ * @see ctools_get_relationship()
+ */
+function ctools_get_relationships() {
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'relationships');
+}
+
+/**
+ * Return a context from a relationship.
+ *
+ * @param array $relationship
+ * The configuration of a relationship. It must contain the following data:
+ * - name: The name of the relationship plugin being used.
+ * - relationship_settings: The configuration based upon the plugin forms.
+ * - identifier: The human readable identifier for this relationship, usually
+ * defined by the UI.
+ * - keyword: The keyword used for this relationship for substitutions.
+ *
+ * @param ctools_context $source_context
+ * The context this relationship is based upon.
+ * @param bool $placeholders
+ * If TRUE, placeholders are acceptable.
+ *
+ * @return ctools_context|null
+ * A context object if one can be loaded, otherwise NULL.
+ *
+ * @see ctools_context_get_relevant_relationships()
+ * @see ctools_context_get_context_from_relationships()
+ */
+function ctools_context_get_context_from_relationship($relationship, $source_context, $placeholders = FALSE) {
+ ctools_include('plugins');
+ $function = ctools_plugin_load_function('ctools', 'relationships', $relationship['name'], 'context');
+ if ($function) {
+ // Backward compatibility: Merge old style settings into new style:
+ if (!empty($relationship['relationship_settings'])) {
+ $relationship += $relationship['relationship_settings'];
+ unset($relationship['relationship_settings']);
+ }
+
+ $context = $function($source_context, $relationship, $placeholders);
+ if ($context) {
+ $context->identifier = $relationship['identifier'];
+ $context->page_title = isset($relationship['title']) ? $relationship['title'] : '';
+ $context->keyword = $relationship['keyword'];
+ if (!empty($context->empty)) {
+ $context->placeholder = array(
+ 'type' => 'relationship',
+ 'conf' => $relationship,
+ );
+ }
+ return $context;
+ }
+ }
+ return NULL;
+}
+
+/**
+ * Fetch all relevant relationships.
+ *
+ * Relevant relationships are any relationship that can be created based upon
+ * the list of existing contexts. For example, the 'node author' relationship
+ * is relevant if there is a 'node' context, but makes no sense if there is
+ * not one.
+ *
+ * @param $contexts
+ * An array of contexts used to figure out which relationships are relevant.
+ *
+ * @return array
+ * An array of relationship keys that are relevant for the given set of
+ * contexts.
+ *
+ * @see ctools_context_filter()
+ * @see ctools_context_get_context_from_relationship()
+ * @see ctools_context_get_context_from_relationships()
+ */
+function ctools_context_get_relevant_relationships($contexts) {
+ $relevant = array();
+ $relationships = ctools_get_relationships();
+
+ // Go through each relationship.
+ foreach ($relationships as $rid => $relationship) {
+ // For each relationship, see if there is a context that satisfies it.
+ if (empty($relationship['no ui'])
+ && ctools_context_filter($contexts, $relationship['required context'])
+ ) {
+ $relevant[$rid] = $relationship['title'];
+ }
+ }
+
+ return $relevant;
+}
+
+/**
+ * Fetch all active relationships.
+ *
+ * @param $relationships
+ * An keyed array of relationship data including:
+ * - name: name of relationship
+ * - context: context id relationship belongs to. This will be used to
+ * identify which context in the $contexts array to use to create the
+ * relationship context.
+ *
+ * @param $contexts
+ * A keyed array of contexts used to figure out which relationships
+ * are relevant. New contexts will be added to this.
+ *
+ * @param $placeholders
+ * If TRUE, placeholders are acceptable.
+ *
+ * @see ctools_context_get_context_from_relationship()
+ * @see ctools_context_get_relevant_relationships()
+ */
+function ctools_context_get_context_from_relationships($relationships, &$contexts, $placeholders = FALSE) {
+ foreach ($relationships as $rdata) {
+ if (!isset($rdata['context'])) {
+ continue;
+ }
+
+ if (is_array($rdata['context'])) {
+ $rcontexts = array();
+ foreach ($rdata['context'] as $cid) {
+ if (empty($contexts[$cid])) {
+ continue 2;
+ }
+ $rcontexts[] = $contexts[$cid];
+ }
+ }
+ else {
+ if (empty($contexts[$rdata['context']])) {
+ continue;
+ }
+ $rcontexts = $contexts[$rdata['context']];
+ }
+
+ $cid = ctools_context_id($rdata, 'relationship');
+ if ($context = ctools_context_get_context_from_relationship($rdata, $rcontexts)) {
+ $contexts[$cid] = $context;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Functions related to loading contexts from simple context definitions.
+/**
+ * Fetch metadata on a specific context plugin.
+ *
+ * @param string $context
+ * Name of a context.
+ *
+ * @return array
+ * An array with information about the requested panel context.
+ */
+
+function ctools_get_context($context) {
+ static $gate = array();
+ ctools_include('plugins');
+ $plugin = ctools_get_plugins('ctools', 'contexts', $context);
+ if (empty($gate['context']) && !empty($plugin['superceded by'])) {
+ // This gate prevents infinite loops.
+ $gate[$context] = TRUE;
+ $new_plugin = ctools_get_plugins('ctools', 'contexts', $plugin['superceded by']);
+ $gate[$context] = FALSE;
+
+ // If a new plugin was returned, return it. Otherwise fall through and
+ // return the original we fetched.
+ if ($new_plugin) {
+ return $new_plugin;
+ }
+ }
+
+ return $plugin;
+}
+
+/**
+ * Fetch metadata for all context plugins.
+ *
+ * @return array
+ * An array of arrays with information about all available panel contexts.
+ */
+function ctools_get_contexts() {
+ ctools_include('plugins');
+ return ctools_get_plugins('ctools', 'contexts');
+}
+
+/**
+ * Return a context object from a context definition array.
+ *
+ * The input $context contains the information needed to identify and invoke
+ * the context plugin and create the plugin context from that.
+ *
+ * @param array $context
+ * The configuration of a context. It must contain the following data:
+ * - name: The name of the context plugin being used.
+ * - context_settings: The configuration based upon the plugin forms.
+ * - identifier: The human readable identifier for this context, usually
+ * defined by the UI.
+ * - keyword: The keyword used for this context for substitutions.
+ * @param string $type
+ * This is either 'context' which indicates the context will be loaded
+ * from data in the settings, or 'requiredcontext' which means the
+ * context must be acquired from an external source. This is the method
+ * used to pass pure contexts from one system to another.
+ * @param mixed $argument
+ * Optional information passed to the plugin context via the arg defined in
+ * the plugin's "placeholder name" field.
+ *
+ * @return ctools_context|null
+ * A context object if one can be loaded.
+ *
+ * @see ctools_get_context()
+ * @see ctools_plugin_get_function()
+ */
+function ctools_context_get_context_from_context($context, $type = 'context', $argument = NULL) {
+ ctools_include('plugins');
+ $plugin = ctools_get_context($context['name']);
+ $function = ctools_plugin_get_function($plugin, 'context');
+ if ($function) {
+ // Backward compatibility: Merge old style settings into new style:
+ if (!empty($context['context_settings'])) {
+ $context += $context['context_settings'];
+ unset($context['context_settings']);
+ }
+
+ if (isset($argument) && isset($plugin['placeholder name'])) {
+ $context[$plugin['placeholder name']] = $argument;
+ }
+
+ $return = $function($type == 'requiredcontext', $context, TRUE, $plugin);
+ if ($return) {
+ $return->identifier = $context['identifier'];
+ $return->page_title = isset($context['title']) ? $context['title'] : '';
+ $return->keyword = $context['keyword'];
+
+ if (!empty($context->empty)) {
+ $context->placeholder = array(
+ 'type' => 'context',
+ 'conf' => $context,
+ );
+ }
+
+ return $return;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Retrieve a list of base contexts based upon a simple 'contexts' definition.
+ *
+ * For required contexts this will always retrieve placeholders.
+ *
+ * @param $contexts
+ * The list of contexts defined in the UI.
+ * @param $type
+ * Either 'context' or 'requiredcontext', which indicates whether the contexts
+ * are loaded from internal data or copied from an external source.
+ * @param $placeholders
+ * If True, placeholders are acceptable.
+ *
+ * @return array
+ * Array of contexts, keyed by context ID.
+ */
+function ctools_context_get_context_from_contexts($contexts, $type = 'context', $placeholders = FALSE) {
+ $return = array();
+ foreach ($contexts as $context) {
+ $ctext = ctools_context_get_context_from_context($context, $type);
+ if ($ctext) {
+ if ($placeholders) {
+ $ctext->placeholder = TRUE;
+ }
+ $return[ctools_context_id($context, $type)] = $ctext;
+ }
+ }
+ return $return;
+}
+
+/**
+ * Match up external contexts to our required contexts.
+ *
+ * This function is used to create a list of contexts with proper IDs based
+ * upon a list of required contexts.
+ *
+ * These contexts passed in should match the numeric positions of the required
+ * contexts. The caller must ensure this has already happened correctly as this
+ * function will not detect errors here.
+ *
+ * @param $required
+ * A list of required contexts as defined by the UI.
+ * @param $contexts
+ * A list of matching contexts as passed in from the calling system.
+ *
+ * @return array
+ * Array of contexts, keyed by context ID.
+ */
+function ctools_context_match_required_contexts($required, $contexts) {
+ $return = array();
+ if (!is_array($required)) {
+ return $return;
+ }
+
+ foreach ($required as $r) {
+ $context = clone array_shift($contexts);
+ $context->identifier = $r['identifier'];
+ $context->page_title = isset($r['title']) ? $r['title'] : '';
+ $context->keyword = $r['keyword'];
+ $return[ctools_context_id($r, 'requiredcontext')] = $context;
+ }
+
+ return $return;
+}
+
+/**
+ * Load a full array of contexts for an object.
+ *
+ * Not all of the types need to be supported by this object.
+ *
+ * This function is not used to load contexts from external data, but may be
+ * used to load internal contexts and relationships. Otherwise it can also be
+ * used to generate a full set of placeholders for UI purposes.
+ *
+ * @param object $object
+ * An object that contains some or all of the following variables:
+ *
+ * - requiredcontexts: A list of UI configured contexts that are required
+ * from an external source. Since these require external data, they will
+ * only be added if $placeholders is set to TRUE, and empty contexts will
+ * be created.
+ * - arguments: A list of UI configured arguments that will create contexts.
+ * As these require external data, they will only be added if $placeholders
+ * is set to TRUE.
+ * - contexts: A list of UI configured contexts that have no external source,
+ * and are essentially hardcoded. For example, these might configure a
+ * particular node or a particular taxonomy term.
+ * - relationships: A list of UI configured contexts to be derived from other
+ * contexts that already exist from other sources. For example, these might
+ * be used to get a user object from a node via the node author
+ * relationship.
+ * @param bool $placeholders
+ * If True, this will generate placeholder objects for any types this function
+ * cannot load.
+ * @param array $contexts
+ * An array of pre-existing contexts that will be part of the return value.
+ *
+ * @return array
+ * Merged output of all results of ctools_context_get_context_from_contexts().
+ */
+function ctools_context_load_contexts($object, $placeholders = TRUE, $contexts = array()) {
+ if (!empty($object->base_contexts)) {
+ $contexts += $object->base_contexts;
+ }
+
+ if ($placeholders) {
+ // This will load empty contexts as placeholders for arguments that come
+ // from external sources. If this isn't set, it's assumed these context
+ // will already have been matched up and loaded.
+ if (!empty($object->requiredcontexts) && is_array($object->requiredcontexts)) {
+ $contexts += ctools_context_get_context_from_contexts($object->requiredcontexts, 'requiredcontext', $placeholders);
+ }
+
+ if (!empty($object->arguments) && is_array($object->arguments)) {
+ $contexts += ctools_context_get_placeholders_from_argument($object->arguments);
+ }
+ }
+
+ if (!empty($object->contexts) && is_array($object->contexts)) {
+ $contexts += ctools_context_get_context_from_contexts($object->contexts, 'context', $placeholders);
+ }
+
+ // Add contexts from relationships.
+ if (!empty($object->relationships) && is_array($object->relationships)) {
+ ctools_context_get_context_from_relationships($object->relationships, $contexts, $placeholders);
+ }
+
+ return $contexts;
+}
+
+/**
+ * Return the first context with a form id from a list of contexts.
+ *
+ * This function is used to figure out which contexts represents 'the form'
+ * from a list of contexts. Only one contexts can actually be 'the form' for
+ * a given page, since the @code{