From 9adc940a67800d8768d14e4758f81709cd1b1713 Mon Sep 17 00:00:00 2001 From: Bachir Soussi Chiadmi Date: Mon, 13 May 2019 18:47:27 +0200 Subject: [PATCH] updated webform, webform_localization, profile2, term_merge, search_api_saved_pages, rules, redirect, overide_node_options --- 1 | 0 .../admin/override_node_options/README.txt | 45 + .../override_node_options.info | 7 +- .../override_node_options.install | 35 +- .../override_node_options.module | 240 +- .../override_node_options.test | 201 +- .../contrib/admin/redirect/redirect.admin.inc | 42 +- .../admin/redirect/redirect.controller.inc | 20 + .../contrib/admin/redirect/redirect.info | 10 +- .../contrib/admin/redirect/redirect.install | 68 +- .../contrib/admin/redirect/redirect.js | 17 +- .../contrib/admin/redirect/redirect.module | 232 +- .../contrib/admin/redirect/redirect.test | 61 +- ...direct_handler_field_redirect_redirect.inc | 5 - ...redirect_handler_field_redirect_source.inc | 5 - .../modules/contrib/admin/rules/DEVELOPER.txt | 2 +- .../modules/contrib/admin/rules/README.txt | 20 +- .../fix_errors_on_update-2090511-214_2.patch | 27 - .../contrib/admin/rules/includes/faces.inc | 70 +- .../admin/rules/includes/rules.core.inc | 666 ++- .../admin/rules/includes/rules.event.inc | 58 +- .../admin/rules/includes/rules.plugins.inc | 247 +- .../admin/rules/includes/rules.processor.inc | 89 +- .../admin/rules/includes/rules.state.inc | 126 +- .../admin/rules/includes/rules.upgrade.inc | 141 +- .../admin/rules/modules/comment.rules.inc | 47 +- .../contrib/admin/rules/modules/data.eval.inc | 86 +- .../admin/rules/modules/data.rules.inc | 88 +- .../admin/rules/modules/entity.eval.inc | 22 +- .../admin/rules/modules/entity.rules.inc | 39 +- .../contrib/admin/rules/modules/events.inc | 173 +- .../contrib/admin/rules/modules/node.eval.inc | 126 +- .../admin/rules/modules/node.rules.inc | 96 +- .../contrib/admin/rules/modules/path.eval.inc | 8 +- .../admin/rules/modules/path.rules.inc | 14 +- .../contrib/admin/rules/modules/php.eval.inc | 67 +- .../contrib/admin/rules/modules/php.rules.inc | 13 +- .../admin/rules/modules/rules_core.eval.inc | 54 +- .../admin/rules/modules/rules_core.rules.inc | 41 +- .../admin/rules/modules/system.eval.inc | 37 +- .../admin/rules/modules/system.rules.inc | 27 +- .../admin/rules/modules/taxonomy.rules.inc | 68 +- .../contrib/admin/rules/modules/user.eval.inc | 37 +- .../admin/rules/modules/user.rules.inc | 51 +- .../modules/contrib/admin/rules/rules.api.php | 265 +- .../contrib/admin/rules/rules.drush.inc | 160 +- .../contrib/admin/rules/rules.features.inc | 41 +- .../modules/contrib/admin/rules/rules.info | 18 +- .../modules/contrib/admin/rules/rules.install | 133 +- .../modules/contrib/admin/rules/rules.module | 532 ++- .../contrib/admin/rules/rules.rules.inc | 50 +- .../admin/rules/rules_admin/rules_admin.inc | 40 +- .../admin/rules/rules_admin/rules_admin.info | 13 +- .../rules/rules_admin/rules_admin.module | 3 +- .../rules/rules_admin/tests/rules_admin.test | 126 + .../rules/rules_i18n/rules_i18n.i18n.inc | 8 +- .../admin/rules/rules_i18n/rules_i18n.info | 8 +- .../admin/rules/rules_i18n/rules_i18n.install | 19 + .../admin/rules/rules_i18n/rules_i18n.module | 6 +- .../rules/rules_i18n/rules_i18n.rules.inc | 11 +- .../admin/rules/rules_i18n/rules_i18n.test | 21 +- .../includes/rules_scheduler.handler.inc | 5 + .../includes/rules_scheduler.views.inc | 7 +- .../rules_scheduler.views_default.inc | 6 +- .../includes/rules_scheduler_views_filter.inc | 7 +- .../rules_scheduler/rules_scheduler.admin.inc | 11 +- .../rules_scheduler/rules_scheduler.drush.inc | 8 +- .../rules_scheduler/rules_scheduler.info | 21 +- .../rules_scheduler/rules_scheduler.install | 76 +- .../rules_scheduler/rules_scheduler.module | 129 +- .../rules_scheduler/rules_scheduler.rules.inc | 13 +- .../{ => tests}/rules_scheduler.test | 77 +- .../tests/rules_scheduler_test.info | 7 +- .../contrib/admin/rules/tests/rules.test | 511 ++- .../contrib/admin/rules/tests/rules_test.info | 8 +- .../admin/rules/tests/rules_test.module | 6 +- .../admin/rules/tests/rules_test.rules.inc | 142 +- .../rules/tests/rules_test.rules_defaults.inc | 9 +- .../admin/rules/tests/rules_test.test.inc | 14 +- .../rules/tests/rules_test_invocation.info | 7 +- .../admin/rules/ui/rules.autocomplete.js | 17 +- .../contrib/admin/rules/ui/rules.ui.css | 16 +- .../contrib/admin/rules/ui/ui.controller.inc | 42 +- .../contrib/admin/rules/ui/ui.core.inc | 300 +- .../contrib/admin/rules/ui/ui.data.inc | 231 +- .../contrib/admin/rules/ui/ui.forms.inc | 190 +- .../contrib/admin/rules/ui/ui.plugins.inc | 56 +- .../contrib/admin/rules/ui/ui.theme.inc | 27 +- .../contrib/admin/token/tests/token_test.info | 6 +- .../modules/contrib/admin/token/token.info | 6 +- .../modules/contrib/admin/token/token.install | 40 +- .../modules/contrib/admin/token/token.module | 16 +- .../contrib/admin/token/token.pages.inc | 4 +- .../contrib/admin/token/token.tokens.inc | 3 +- .../contrib/editor/wysiwyg_filter/README.txt | 29 +- .../wysiwyg_filter/wysiwyg_filter.admin.inc | 69 +- .../editor/wysiwyg_filter/wysiwyg_filter.inc | 67 +- .../editor/wysiwyg_filter/wysiwyg_filter.info | 6 +- .../wysiwyg_filter/wysiwyg_filter.module | 15 +- .../wysiwyg_filter/wysiwyg_filter.pages.inc | 74 +- .../contrib/form/webform/CHANGELOG.txt | 37 - .../modules/contrib/form/webform/README.txt | 31 +- .../modules/contrib/form/webform/THEMING.txt | 17 +- .../contrib/form/webform/components/date.inc | 257 +- .../contrib/form/webform/components/email.inc | 151 +- .../form/webform/components/fieldset.inc | 14 +- .../contrib/form/webform/components/file.inc | 454 ++- .../contrib/form/webform/components/grid.inc | 595 ++- .../form/webform/components/hidden.inc | 64 +- .../form/webform/components/markup.inc | 111 +- .../form/webform/components/number.inc | 294 +- .../form/webform/components/pagebreak.inc | 14 +- .../form/webform/components/select.inc | 352 +- .../form/webform/components/textarea.inc | 77 +- .../form/webform/components/textfield.inc | 103 +- .../contrib/form/webform/components/time.inc | 255 +- .../form/webform/css/webform-admin.css | 154 +- .../contrib/form/webform/css/webform.css | 50 + .../includes/exporters/webform_exporter.inc | 103 + .../exporters/webform_exporter_delimited.inc | 60 + .../webform_exporter_excel_delimited.inc | 60 + .../exporters/webform_exporter_excel_xlsx.inc | 267 ++ .../form/webform/includes/webform.admin.inc | 199 +- .../webform/includes/webform.components.inc | 612 +-- .../webform/includes/webform.conditionals.inc | 1768 ++++++++ .../form/webform/includes/webform.emails.inc | 354 +- .../form/webform/includes/webform.export.inc | 150 +- .../form/webform/includes/webform.options.inc | 14 +- .../form/webform/includes/webform.pages.inc | 333 +- .../form/webform/includes/webform.report.inc | 1578 ++++++-- .../webform/includes/webform.submissions.inc | 785 ++-- .../includes/webform.webformconditionals.inc | 637 +++ .../contrib/form/webform/js/node-type-form.js | 27 + .../contrib/form/webform/js/select-admin.js | 83 +- .../contrib/form/webform/js/webform-admin.js | 421 +- .../contrib/form/webform/js/webform.js | 758 +++- .../webform-analysis-component.tpl.php | 22 + .../templates/webform-analysis.tpl.php | 31 + .../templates/webform-calendar.tpl.php | 2 +- .../templates/webform-confirmation.tpl.php | 9 +- .../webform/templates/webform-form.tpl.php | 16 +- .../webform/templates/webform-mail.tpl.php | 22 +- .../templates/webform-progressbar.tpl.php | 65 + .../webform-results-submissions.tpl.php | 4 +- .../webform-submission-information.tpl.php | 4 +- .../tests/WebformComponentsTestCase.test | 194 + .../tests/WebformConditionalsTestCase.test | 224 ++ .../webform/tests/WebformGeneralTestCase.test | 95 + ...s.test => WebformPermissionsTestCase.test} | 38 +- ...on.test => WebformSubmissionTestCase.test} | 116 +- .../{webform.test => WebformTestCase.test} | 632 ++- .../form/webform/tests/components.test | 37 - .../views/default_views/webform_analysis.inc | 59 + .../views/default_views/webform_results.inc | 169 + .../default_views/webform_submissions.inc | 211 + .../views/default_views/webform_webforms.inc | 217 + .../form/webform/views/webform.views.inc | 569 ++- .../webform_handler_area_result_pager.inc | 101 + .../views/webform_handler_field_form_body.inc | 27 +- .../views/webform_handler_field_is_draft.inc | 18 +- .../webform_handler_field_node_link_edit.inc | 37 +- ...ebform_handler_field_node_link_results.inc | 43 +- ...webform_handler_field_submission_count.inc | 34 +- .../webform_handler_field_submission_data.inc | 243 ++ .../webform_handler_field_submission_link.inc | 105 +- .../webform_handler_field_webform_status.inc | 33 +- .../views/webform_handler_filter_is_draft.inc | 20 +- ...webform_handler_filter_submission_data.inc | 102 + .../webform_handler_filter_webform_status.inc | 8 +- .../views/webform_handler_numeric_data.inc | 173 + ...m_handler_relationship_submission_data.inc | 141 + .../webform_plugin_row_submission_view.inc | 118 + .../contrib/form/webform/webform.api.php | 675 +++- .../contrib/form/webform/webform.drush.inc | 225 ++ .../modules/contrib/form/webform/webform.info | 36 +- .../contrib/form/webform/webform.install | 1642 +++++++- .../contrib/form/webform/webform.module | 3578 ++++++++++++----- .../contrib/form/webform/webform.tokens.inc | 299 ++ .../webform_localization/components/grid.inc | 59 + .../components/select.inc | 102 + .../webform_localization.component.sync.inc | 90 +- .../includes/webform_localization.i18n.inc | 303 +- .../includes/webform_localization.sync.inc | 41 +- .../webform_localization.submission.test | 250 ++ .../tests/webform_localization.test | 129 +- .../webform_localization.api.php | 72 + .../webform_localization.info | 20 +- .../webform_localization.install | 11 +- .../webform_localization.module | 818 +++- .../localisation/l10n_update/README.txt | 70 +- .../l10n_update/css/l10n_update.admin-rtl.css | 2 +- .../l10n_update/css/l10n_update.admin.css | 17 +- .../l10n_update/includes/gettext/PoHeader.php | 23 +- .../l10n_update/includes/gettext/PoItem.php | 131 +- .../includes/gettext/PoMemoryWriter.php | 39 +- .../includes/gettext/PoStreamInterface.php | 4 +- .../includes/gettext/PoStreamReader.php | 85 +- .../includes/gettext/PoWriterInterface.php | 2 +- .../l10n_update/includes/locale/Gettext.php | 14 +- .../includes/locale/PoDatabaseReader.php | 32 +- .../includes/locale/PoDatabaseWriter.php | 61 +- .../includes/locale/SourceString.php | 9 + .../includes/locale/StringBase.php | 74 +- .../includes/locale/StringDatabaseStorage.php | 57 +- .../includes/locale/StringInterface.php | 78 +- .../locale/StringStorageException.php | 2 +- .../locale/StringStorageInterface.php | 9 +- .../includes/locale/TranslationString.php | 5 +- .../locale/TranslationsStreamWrapper.php | 5 +- .../l10n_update/js/l10n_update.admin.js | 60 +- ...l10n_update-translation-last-check.tpl.php | 23 +- ...10n_update-translation-update-info.tpl.php | 35 +- .../l10n_update/l10n_update.admin.inc | 92 +- .../l10n_update/l10n_update.api.php | 62 +- .../l10n_update/l10n_update.batch.inc | 44 +- .../l10n_update/l10n_update.bulk.inc | 100 +- .../l10n_update/l10n_update.compare.inc | 135 +- .../l10n_update/l10n_update.drush.inc | 39 +- .../l10n_update/l10n_update.fetch.inc | 34 +- .../l10n_update/l10n_update.http.inc | 63 +- .../localisation/l10n_update/l10n_update.info | 9 +- .../l10n_update/l10n_update.install | 347 +- .../l10n_update/l10n_update.module | 121 +- .../l10n_update/l10n_update.translation.inc | 60 +- .../l10n_update/tests/L10nUpdateCronTest.test | 11 +- .../tests/L10nUpdateInterfaceTest.test | 32 +- .../l10n_update/tests/L10nUpdateTest.test | 109 +- .../l10n_update/tests/L10nUpdateTestBase.test | 23 +- .../l10n_update_test/l10n_update_test.info | 6 +- .../l10n_update_test/l10n_update_test.module | 37 +- .../l10n_update_test_translate.info | 6 +- .../search_api_saved_searches/CHANGELOG.txt | 29 + .../search_api_saved_searches/README.txt | 11 +- .../search_api_saved_searches.admin.inc | 26 +- .../search_api_saved_searches.info | 7 +- .../search_api_saved_searches.install | 16 +- .../search_api_saved_searches.module | 293 +- .../search_api_saved_searches.pages.inc | 50 +- .../search_api_saved_searches.rules.inc | 112 + ...earch_api_saved_searches.search_entity.inc | 15 +- .../search_api_saved_searches.tokens.inc | 22 +- .../search_api_saved_searches_i18n.info | 7 +- .../views/handler_field_saved_search_name.inc | 40 +- .../contrib/taxonomy/term_merge/README.txt | 4 +- .../taxonomy/term_merge/help/term_merge.html | 2 +- .../help/term_merge_duplicate_suggestion.html | 6 +- ...ferenceTermMergeSynonymsBehavior.class.inc | 50 + ...ferenceTermMergeSynonymsBehavior.class.inc | 72 + .../TermMergeSynonymsBehavior.interface.inc | 35 + .../TextTermMergeSynonymsBehavior.class.inc | 56 + .../plugins/behavior/term_merge.inc | 12 + .../taxonomy/term_merge/term_merge.api.php | 2 +- .../taxonomy/term_merge/term_merge.batch.inc | 6 +- .../taxonomy/term_merge/term_merge.info | 16 +- .../taxonomy/term_merge/term_merge.module | 323 +- .../taxonomy/term_merge/term_merge.pages.inc | 360 +- .../taxonomy/term_merge/term_merge.test | 107 +- .../modules/contrib/users/profile2/README.txt | 60 +- .../users/profile2/contrib/profile2_i18n.info | 7 +- .../profile2/contrib/profile2_og_access.info | 7 +- .../users/profile2/contrib/profile2_page.inc | 30 +- .../users/profile2/contrib/profile2_page.info | 7 +- .../profile2/contrib/profile2_page.module | 168 +- .../plugins/relationships/profile2.inc | 61 + .../contrib/users/profile2/profile2.admin.inc | 12 + .../contrib/users/profile2/profile2.api.php | 2 +- .../users/profile2/profile2.delete.inc | 38 + .../contrib/users/profile2/profile2.devel.inc | 145 + .../contrib/users/profile2/profile2.info | 7 +- .../contrib/users/profile2/profile2.install | 64 +- .../contrib/users/profile2/profile2.module | 398 +- .../contrib/users/profile2/profile2.test | 32 +- .../actions/book.action.inc | 13 +- .../actions/modify.action.inc | 18 +- .../actions_permissions.info | 7 +- .../js/views_bulk_operations.js | 35 +- .../plugins/operation_types/action.class.php | 7 +- ...lk_operations_handler_field_operations.inc | 1 + .../views_bulk_operations.info | 7 +- .../views_bulk_operations.module | 30 +- .../views_bulk_operations.rules.inc | 6 +- 281 files changed, 28658 insertions(+), 7138 deletions(-) create mode 100644 1 create mode 100644 sites/all/modules/contrib/admin/override_node_options/README.txt create mode 100755 sites/all/modules/contrib/admin/redirect/redirect.controller.inc delete mode 100644 sites/all/modules/contrib/admin/rules/fix_errors_on_update-2090511-214_2.patch create mode 100644 sites/all/modules/contrib/admin/rules/rules_admin/tests/rules_admin.test create mode 100644 sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.install rename sites/all/modules/contrib/admin/rules/rules_scheduler/{ => tests}/rules_scheduler.test (54%) delete mode 100644 sites/all/modules/contrib/form/webform/CHANGELOG.txt create mode 100644 sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter.inc create mode 100644 sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter_delimited.inc create mode 100644 sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter_excel_delimited.inc create mode 100644 sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter_excel_xlsx.inc create mode 100644 sites/all/modules/contrib/form/webform/includes/webform.conditionals.inc create mode 100644 sites/all/modules/contrib/form/webform/includes/webform.webformconditionals.inc create mode 100644 sites/all/modules/contrib/form/webform/js/node-type-form.js create mode 100644 sites/all/modules/contrib/form/webform/templates/webform-analysis-component.tpl.php create mode 100644 sites/all/modules/contrib/form/webform/templates/webform-analysis.tpl.php create mode 100644 sites/all/modules/contrib/form/webform/templates/webform-progressbar.tpl.php create mode 100644 sites/all/modules/contrib/form/webform/tests/WebformComponentsTestCase.test create mode 100644 sites/all/modules/contrib/form/webform/tests/WebformConditionalsTestCase.test create mode 100644 sites/all/modules/contrib/form/webform/tests/WebformGeneralTestCase.test rename sites/all/modules/contrib/form/webform/tests/{permissions.test => WebformPermissionsTestCase.test} (62%) rename sites/all/modules/contrib/form/webform/tests/{submission.test => WebformSubmissionTestCase.test} (58%) rename sites/all/modules/contrib/form/webform/tests/{webform.test => WebformTestCase.test} (55%) delete mode 100644 sites/all/modules/contrib/form/webform/tests/components.test create mode 100644 sites/all/modules/contrib/form/webform/views/default_views/webform_analysis.inc create mode 100644 sites/all/modules/contrib/form/webform/views/default_views/webform_results.inc create mode 100644 sites/all/modules/contrib/form/webform/views/default_views/webform_submissions.inc create mode 100644 sites/all/modules/contrib/form/webform/views/default_views/webform_webforms.inc create mode 100644 sites/all/modules/contrib/form/webform/views/webform_handler_area_result_pager.inc create mode 100644 sites/all/modules/contrib/form/webform/views/webform_handler_field_submission_data.inc create mode 100644 sites/all/modules/contrib/form/webform/views/webform_handler_filter_submission_data.inc create mode 100644 sites/all/modules/contrib/form/webform/views/webform_handler_numeric_data.inc create mode 100644 sites/all/modules/contrib/form/webform/views/webform_handler_relationship_submission_data.inc create mode 100644 sites/all/modules/contrib/form/webform/views/webform_plugin_row_submission_view.inc create mode 100644 sites/all/modules/contrib/form/webform/webform.drush.inc create mode 100644 sites/all/modules/contrib/form/webform/webform.tokens.inc create mode 100644 sites/all/modules/contrib/form/webform_localization/components/grid.inc create mode 100644 sites/all/modules/contrib/form/webform_localization/components/select.inc create mode 100644 sites/all/modules/contrib/form/webform_localization/tests/webform_localization.submission.test create mode 100644 sites/all/modules/contrib/form/webform_localization/webform_localization.api.php create mode 100644 sites/all/modules/contrib/search/search_api_saved_searches/search_api_saved_searches.rules.inc create mode 100644 sites/all/modules/contrib/taxonomy/term_merge/includes/EntityReferenceTermMergeSynonymsBehavior.class.inc create mode 100644 sites/all/modules/contrib/taxonomy/term_merge/includes/TaxonomyTermReferenceTermMergeSynonymsBehavior.class.inc create mode 100644 sites/all/modules/contrib/taxonomy/term_merge/includes/TermMergeSynonymsBehavior.interface.inc create mode 100644 sites/all/modules/contrib/taxonomy/term_merge/includes/TextTermMergeSynonymsBehavior.class.inc create mode 100644 sites/all/modules/contrib/taxonomy/term_merge/plugins/behavior/term_merge.inc create mode 100644 sites/all/modules/contrib/users/profile2/plugins/relationships/profile2.inc create mode 100644 sites/all/modules/contrib/users/profile2/profile2.delete.inc create mode 100644 sites/all/modules/contrib/users/profile2/profile2.devel.inc diff --git a/1 b/1 new file mode 100644 index 00000000..e69de29b diff --git a/sites/all/modules/contrib/admin/override_node_options/README.txt b/sites/all/modules/contrib/admin/override_node_options/README.txt new file mode 100644 index 00000000..7da954fd --- /dev/null +++ b/sites/all/modules/contrib/admin/override_node_options/README.txt @@ -0,0 +1,45 @@ +CONTENTS OF THIS FILE +--------------------- + + * Introduction + * Requirements + * Installation + * Configuration + * Maintainers + + +INTRODUCTION +------------ + +The Override Node Options module allows permissions to be set to each field +within the Authoring information and Publishing options fieldsets on the node +add/edit form so that they can be configued by non-admin users. + + +REQUIREMENTS +------------ + +No special requirements. + + +INSTALLATION +------------ + +Install as you would normally install a contributed Drupal module. Visit +https://www.drupal.org/docs/7/extending-drupal-7/installing-drupal-7-contributed-modules +for further information. + + +CONFIGURATION +------------- + + * Adjust module settings in admin/config/content/override-node-options to + enable general and/or content type specific permissions. + * Assign permissions in admin/people/permissions#module-override_node_options. + +MAINTAINERS +----------- + +Current maintainers: + + * Oliver Davies (opdavies) - https://www.drupal.org/u/opdavies diff --git a/sites/all/modules/contrib/admin/override_node_options/override_node_options.info b/sites/all/modules/contrib/admin/override_node_options/override_node_options.info index b2f8a14d..4133e471 100644 --- a/sites/all/modules/contrib/admin/override_node_options/override_node_options.info +++ b/sites/all/modules/contrib/admin/override_node_options/override_node_options.info @@ -2,11 +2,12 @@ name = Override node options description = "Allow non-admins to override the default publishing options for nodes they can edit." core = 7.x package = Permissions +configure = admin/config/content/override-node-options files[] = override_node_options.test -; Information added by Drupal.org packaging script on 2014-09-19 -version = "7.x-1.13" +; Information added by Drupal.org packaging script on 2018-03-31 +version = "7.x-1.14" core = "7.x" project = "override_node_options" -datestamp = "1411157931" +datestamp = "1522482188" diff --git a/sites/all/modules/contrib/admin/override_node_options/override_node_options.install b/sites/all/modules/contrib/admin/override_node_options/override_node_options.install index 9138b48c..de6c8b1a 100644 --- a/sites/all/modules/contrib/admin/override_node_options/override_node_options.install +++ b/sites/all/modules/contrib/admin/override_node_options/override_node_options.install @@ -9,27 +9,28 @@ * Implements hook_install(). */ function override_node_options_install() { - db_update('system')->fields(array( - 'weight' => -1 - )) - ->condition('name', 'override_node_options', '=') - ->execute(); -} - -/** - * Implements hook_uninstall(). - */ -function override_node_options_uninstall() { - db_query("DELETE FROM {variable} WHERE name LIKE 'override_node_options_%'"); + db_update('system') + ->fields(array('weight' => 1)) + ->condition('name', 'override_node_options', '=') + ->execute(); } /** * Implements hook_update_N(). */ function override_node_options_update_7113() { - db_update('system')->fields(array( - 'weight' => -1 - )) - ->condition('name', 'override_node_options', '=') - ->execute(); + db_update('system') + ->fields(array('weight' => -1)) + ->condition('name', 'override_node_options', '=') + ->execute(); +} + +/** + * Update the module weight. + */ +function override_node_options_update_7114() { + db_update('system') + ->fields(array('weight' => 1)) + ->condition('name', 'override_node_options', '=') + ->execute(); } diff --git a/sites/all/modules/contrib/admin/override_node_options/override_node_options.module b/sites/all/modules/contrib/admin/override_node_options/override_node_options.module index 5bf6252b..7acfacf7 100644 --- a/sites/all/modules/contrib/admin/override_node_options/override_node_options.module +++ b/sites/all/modules/contrib/admin/override_node_options/override_node_options.module @@ -2,19 +2,62 @@ /** * @file + * Main module file for override_node_options. + * * Allow users to override the default publishing options for nodes they can * edit without giving them the 'administer nodes' permission. */ /** - * Implements hook_permisson(). + * Implements hook_permission(). */ function override_node_options_permission() { - $permissions = array(); + // Global permissions which apply to all node types. + $permissions = array( + 'administer override node options' => array( + 'title' => t('Administer override node options.'), + ), + ); - // Generate override node permissions for all applicable node types. - foreach (node_permissions_get_configured_types() as $type) { - $permissions += override_node_options_list_permissions($type); + $show_perms = variable_get('override_node_options_permissions', array('general', 'specific')); + if (in_array('general', $show_perms, TRUE)) { + $permissions += array( + 'override all published option' => array( + 'title' => t('Override published option for all node types.'), + ), + 'override all promote to front page option' => array( + 'title' => t('Override promote to front page option for all node types.'), + ), + 'override all sticky option' => array( + 'title' => t('Override sticky option for all node types.'), + ), + 'override all revision option' => array( + 'title' => t('Override revision option for all node types.'), + ), + 'enter all revision log entry' => array( + 'title' => t('Enter revision log entry for all node types.'), + ), + 'override all authored on option' => array( + 'title' => t('Override authored on option for all node types.'), + ), + 'override all authored by option' => array( + 'title' => t('Override authored by option for all node types.'), + ), + ); + if (module_exists('comment')) { + $permissions += array( + 'override all comment setting option' => array( + 'title' => t('Override comment setting option for all node types.'), + ), + ); + } + } + + if (in_array('specific', $show_perms, TRUE)) { + // Generate override node permissions for all applicable node types. + foreach (node_permissions_get_configured_types() as $type) { + $permissions += override_node_options_list_permissions($type); + } } return $permissions; @@ -23,13 +66,16 @@ function override_node_options_permission() { /** * Helper function to generate override node permission list for a given type. * - * @param $type + * @param string $type * The machine-readable name of the node type. - * @return + * + * @return array * An array of permission names and description. */ -function override_node_options_list_permissions($type) { - $name = node_type_get_name($type); +function override_node_options_list_permissions($type, $name = NULL) { + if (!$name) { + $name = node_type_get_name($type); + } $type = check_plain($type); $permissions = array( @@ -56,63 +102,177 @@ function override_node_options_list_permissions($type) { ), ); + if (module_exists('comment')) { + $permissions += array( + "override $type comment setting option" => array( + 'title' => t('Override %type_name comment setting option.', array('%type_name' => $name)), + ), + ); + } + return $permissions; } +/** + * Implements hook_menu(). + */ +function override_node_options_menu() { + $items['admin/config/content/override-node-options'] = array( + 'title' => 'Override Node Options Settings', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('override_node_options_settings_form'), + 'access arguments' => array('administer override node options'), + 'type' => MENU_NORMAL_ITEM, + ); + + return $items; +} + +/** + * Settings form. + */ +function override_node_options_settings_form($form, &$form_values) { + $form = array(); + + $form['override_node_options_permissions'] = array( + '#type' => 'checkboxes', + '#title' => t('Provide the following permissions:'), + '#options' => array( + 'general' => t('General permissions, accross all node types'), + 'specific' => t('Specific permissions, for each individual node type'), + ), + '#default_value' => variable_get('override_node_options_permissions', array('general', 'specific')), + ); + + $form['#submit'][] = 'override_node_options_settings_form_submit'; + + return system_settings_form($form); +} + +/** + * Submit handler for settings form. + */ +function override_node_options_settings_form_submit(&$form, &$form_state) { + // Get old perms to compare. + $old_perms = variable_get('override_node_options_permissions', array('general', 'specific')); + $new_perms = $form_state['values']['override_node_options_permissions']; + + // Clean up saved permissions. + $role_names = user_roles(); + $revoke = array(); + + if (!in_array('specific', $new_perms, TRUE) && in_array('specific', $old_perms, TRUE)) { + $alert = TRUE; + $permissions = array(); + foreach (node_permissions_get_configured_types() as $type) { + $permissions += override_node_options_list_permissions($type); + } + + foreach ($permissions as $permission => $description) { + $revoke[$permission] = FALSE; + } + + // Be sure to clear the cache. + cache_clear_all(); + } + + if (!in_array('general', $new_perms, TRUE) && in_array('general', $old_perms, TRUE)) { + $alert = TRUE; + $revoke = array( + 'override all published option' => FALSE, + 'override all promote to front page option' => FALSE, + 'override all sticky option' => FALSE, + 'override all revision option' => FALSE, + 'enter all revision log entry' => FALSE, + 'override all authored on option' => FALSE, + 'override all authored by option' => FALSE, + ); + if (module_exists('comment')) { + $revoke['override all comment setting option'] = FALSE; + } + } + + // Any specific grants not used anymore need to be deleted. + if (!empty($revoke)) { + foreach ($role_names as $rid => $name) { + user_role_change_permissions($rid, $revoke); + } + } + + // Set a helpful message. + $message = 'Configuration saved.'; + $arguments = array(); + $status = 'status'; + + if ($alert) { + if (user_access('administer permissions')) { + $arguments = array('!permissions' => l(t('the permissions page'), 'admin/config/people/permissions', array('fragment' => 'module-override_node_options'))); + $message .= t(' Please visit !permissions and double check access.'); + $status = 'warning'; + } + else { + $message .= t(' Please visit the permissions page and double check access.'); + } + } + + drupal_set_message(t($message, $arguments), $status); +} + + /** * Implements hook_form_alter(). */ -function override_node_options_form_alter(&$form, $form_state, $form_id) { +function override_node_options_form_alter(&$form, &$form_state, $form_id) { if (!empty($form['#node_edit_form']) && !user_access('administer nodes')) { // Get a copy of the current node object. $node = $form['#node']; - - // Add access to the 'Revision information: log message' field - $form['revision_information']['log']['#access'] = user_access('enter ' . $node->type . ' revision log entry'); + + // Add access to the 'Revision information: log message' field. + $form['revision_information']['log']['#access'] = user_access('enter ' . $node->type . ' revision log entry') || user_access('enter all revision log entry'); // Add access to the 'Revision information' fieldset. - $form['revision_information']['revision']['#access'] = user_access('override ' . $node->type . ' revision option'); + $form['revision_information']['revision']['#access'] = user_access('override ' . $node->type . ' revision option') || user_access('override all revision option'); $form['revision_information']['#access'] = element_get_visible_children($form['revision_information']); // Add access to the 'Authoring information' fieldset. - $form['author']['name']['#access'] = user_access('override ' . $node->type . ' authored by option'); - $form['author']['date']['#access'] = user_access('override ' . $node->type . ' authored on option'); - if (key_exists('#access', $form['author'])) { + $form['author']['name']['#access'] = user_access('override ' . $node->type . ' authored by option') || user_access('override all authored by option'); + $form['author']['date']['#access'] = user_access('override ' . $node->type . ' authored on option') || user_access('override all authored on option'); + if (array_key_exists('#access', $form['author'])) { $form['author']['#access'] |= element_get_visible_children($form['author']); } else { $form['author']['#access'] = element_get_visible_children($form['author']); } - + // Add access to the 'Publishing options' fieldset. - $form['options']['status']['#access'] = user_access('override ' . $node->type . ' published option'); - $form['options']['promote']['#access'] = user_access('override ' . $node->type . ' promote to front page option'); - $form['options']['sticky']['#access'] = user_access('override ' . $node->type . ' sticky option'); - if (key_exists('#access', $form['options'])) { + $form['options']['status']['#access'] = user_access(sprintf('override %s published option', $node->type)); + $form['options']['promote']['#access'] = user_access(sprintf('override %s promote to front page option', $node->type)); + $form['options']['sticky']['#access'] = user_access(sprintf('override %s sticky option', $node->type)); + + // If access is granted for promote or sticky, show (but disable) status. + // This keeps core's JS working, and correctly populates the vertical tab. + if ($form['options']['status']['#access'] == FALSE && ($form['options']['promote']['#access'] || $form['options']['sticky']['#access'])) { + $form['options']['status']['#access'] = TRUE; + $form['options']['status']['#disabled'] = TRUE; + } + + $form['options']['status']['#access'] = user_access('override ' . $node->type . ' published option') || user_access('override all published option'); + $form['options']['promote']['#access'] = user_access('override ' . $node->type . ' promote to front page option') || user_access('override all promote to front page option'); + $form['options']['sticky']['#access'] = user_access('override ' . $node->type . ' sticky option') || user_access('override all sticky option'); + if (array_key_exists('#access', $form['options'])) { $form['options']['#access'] |= element_get_visible_children($form['options']); } else { $form['options']['#access'] = element_get_visible_children($form['options']); } - // @todo Remove when http://drupal.org/node/683630 is fixed. - if ($form['author']['name']['#access']) { - $form['#submit'][] = 'override_node_options_submit_node'; + // Add access to the 'Comment settings' fieldset. + if (module_exists('comment') && isset($form['comment_settings'])) { + $form['comment_settings']['#access'] |= user_access('override ' . $node->type . ' comment setting option') || user_access('override all comment setting option'); + } + + if (!empty($form['#node_edit_form']) && !user_access('administer nodes')) { + $form['author']['#attached']['js'][1]['data']['anonymous'] = $form['author']['name']['#default_value']; } } } - -/** - * Perform additional node form submission processing normally skipped by core. - * - * @todo Remove when http://drupal.org/node/683630 is fixed. - */ -function override_node_options_submit_node($form, &$form_state) { - // Populate the "authored by" field. - if ($account = user_load_by_name($form_state['values']['name'])) { - $form_state['values']['uid'] = $account->uid; - } - else { - $form_state['values']['uid'] = 0; - } -} diff --git a/sites/all/modules/contrib/admin/override_node_options/override_node_options.test b/sites/all/modules/contrib/admin/override_node_options/override_node_options.test index 923e9f8c..e9f8927a 100644 --- a/sites/all/modules/contrib/admin/override_node_options/override_node_options.test +++ b/sites/all/modules/contrib/admin/override_node_options/override_node_options.test @@ -5,57 +5,86 @@ * Unit tests for the override_node_options module. */ +/** + * Defines a base class for testing the Override Node Options module. + */ class OverrideNodeOptionsTestCase extends DrupalWebTestCase { - protected $normal_user; - protected $admin_user; + + /** + * A standard user with basic permissions. + * + * @var stdClass + */ + protected $normalUser; + + /** + * A page node to test against. + * + * @var stdClass + */ protected $node; + /** + * {@inheritdoc} + */ public static function getInfo() { return array( 'name' => 'Override node options', - 'description' => 'Functional tests for overridding options on node forms.', + 'description' => 'Functional tests for overriding options on node forms.', 'group' => 'Override node options', ); } + /** + * {@inheritdoc} + */ public function setUp() { parent::setUp('override_node_options'); - $this->normal_user = $this->drupalCreateUser(array('create page content', 'edit any page content')); + + $this->normalUser = $this->drupalCreateUser(array('create page content', 'edit any page content')); $this->node = $this->drupalCreateNode(); } /** - * Assert that fields in a node were updated to certail values. + * Assert that fields in a node were updated to certain values. * - * @param $node + * @param \stdClass $node * The node object to check (will be reloaded from the database). - * @param $fields + * @param array $fields * An array of values to check equality, keyed by node object property. */ - function assertNodeFieldsUpdated(stdClass $node, array $fields) { + private function assertNodeFieldsUpdated(stdClass $node, array $fields) { // Re-load the node from the database to make sure we have the current // values. $node = node_load($node->nid, NULL, TRUE); foreach ($fields as $field => $value) { - $this->assertEqual($node->$field, $value, t('Node @field was updated to !value, expected !expected.', array('@field' => $field, '!value' => var_export($node->$field, TRUE), '!expected' => var_export($value, TRUE)))); + $this->assertEqual( + $node->$field, + $value, + t('Node @field was updated to !value, expected !expected.', array( + '@field' => $field, + '!value' => var_export($node->$field, TRUE), + '!expected' => var_export($value, TRUE), + )) + ); } } /** * Assert that the user cannot access fields on node add and edit forms. * - * @param $node + * @param \stdClass $node * The node object, will be used on the node edit form. - * @param $fields + * @param array $fields * An array of form fields to check. */ - function assertNodeFieldsNoAccess(stdClass $node, array $fields) { - $this->drupalGet('node/add/' . $node->type); + private function assertNodeFieldsNoAccess(stdClass $node, array $fields) { + $this->drupalGet("node/add/{$node->type}"); foreach ($fields as $field) { $this->assertNoFieldByName($field); } - $this->drupalGet('node/' . $this->node->nid . '/edit'); + $this->drupalGet("node/{$this->node->nid}/edit"); foreach ($fields as $field) { $this->assertNoFieldByName($field); } @@ -64,61 +93,135 @@ class OverrideNodeOptionsTestCase extends DrupalWebTestCase { /** * Test the 'Authoring information' fieldset. */ - function testNodeOptions() { - $this->admin_user = $this->drupalCreateUser(array('create page content', 'edit any page content', 'override page published option', 'override page promote to front page option', 'override page sticky option')); - $this->drupalLogin($this->admin_user); + protected function testNodeOptions() { + $specific_user = $this->drupalCreateUser(array( + 'create page content', + 'edit any page content', + 'override page published option', + 'override page promote to front page option', + 'override page sticky option', + 'override page comment setting option', + )); - $fields = array( - 'status' => (bool) !$this->node->status, - 'promote' => (bool) !$this->node->promote, - 'sticky' => (bool) !$this->node->sticky, - ); - $this->drupalPost('node/' . $this->node->nid . '/edit', $fields, t('Save')); - $this->assertNodeFieldsUpdated($this->node, $fields); + $general_user = $this->drupalCreateUser(array( + 'create page content', + 'edit any page content', + 'override all published option', + 'override all promote to front page option', + 'override all sticky option', + 'override all comment setting option', + )); - $this->drupalLogin($this->normal_user); + foreach (array($specific_user, $general_user) as $account) { + $this->drupalLogin($account); + + $fields = array( + 'status' => (bool) !$this->node->status, + 'promote' => (bool) !$this->node->promote, + 'sticky' => (bool) !$this->node->sticky, + 'comment' => COMMENT_NODE_OPEN, + ); + $this->drupalPost("node/{$this->node->nid}/edit", $fields, t('Save')); + $this->assertNodeFieldsUpdated($this->node, $fields); + } + + $this->drupalLogin($this->normalUser); $this->assertNodeFieldsNoAccess($this->node, array_keys($fields)); } /** * Test the 'Revision information' fieldset. */ - function testNodeRevisions() { - $this->admin_user = $this->drupalCreateUser(array('create page content', 'edit any page content', 'override page revision option')); - $this->drupalLogin($this->admin_user); + protected function testNodeRevisions() { + $specific_user = $this->drupalCreateUser(array( + 'create page content', + 'edit any page content', + 'override page revision option', + )); - $fields = array( - 'revision' => TRUE, - ); - $this->drupalPost('node/' . $this->node->nid . '/edit', $fields, t('Save')); - $this->assertNodeFieldsUpdated($this->node, array('vid' => $this->node->vid + 1)); + $general_user = $this->drupalCreateUser(array( + 'create page content', + 'edit any page content', + 'override all revision option', + )); - $this->drupalLogin($this->normal_user); + foreach (array($specific_user, $general_user) as $account) { + $this->drupalLogin($account); + + // Ensure that we have the latest node data. + $node = node_load($this->node->nid, NULL, TRUE); + + $fields = array( + 'revision' => TRUE, + ); + $this->drupalPost("node/{$node->nid}/edit", $fields, t('Save')); + $this->assertNodeFieldsUpdated($node, array('vid' => $node->vid + 1)); + } + + $this->drupalLogin($this->normalUser); $this->assertNodeFieldsNoAccess($this->node, array_keys($fields)); } /** * Test the 'Authoring information' fieldset. */ - function testNodeAuthor() { - $this->admin_user = $this->drupalCreateUser(array('create page content', 'edit any page content', 'override page authored on option', 'override page authored by option')); - $this->drupalLogin($this->admin_user); + protected function testNodeAuthor() { + $specific_user = $this->drupalCreateUser(array( + 'create page content', + 'edit any page content', + 'override page authored on option', + 'override page authored by option', + )); - $this->drupalPost('node/' . $this->node->nid . '/edit', array('name' => 'invalid-user'), t('Save')); - $this->assertText('The username invalid-user does not exist.'); + $general_user = $this->drupalCreateUser(array( + 'create page content', + 'edit any page content', + 'override all authored on option', + 'override all authored by option', + )); - $this->drupalPost('node/' . $this->node->nid . '/edit', array('date' => 'invalid-date'), t('Save')); - $this->assertText('You have to specify a valid date.'); + foreach (array($specific_user, $general_user) as $account) { + $this->drupalLogin($account); - $time = time() + 500; - $fields = array( - 'name' => '', - 'date' => format_date($time, 'custom', 'Y-m-d H:i:s O'), - ); - $this->drupalPost('node/' . $this->node->nid . '/edit', $fields, t('Save')); - $this->assertNodeFieldsUpdated($this->node, array('uid' => 0, 'created' => $time)); + $this->drupalPost("node/{$this->node->nid}/edit", array('name' => 'invalid-user'), t('Save')); + $this->assertText('The username invalid-user does not exist.'); - $this->drupalLogin($this->normal_user); + $this->drupalPost("node/{$this->node->nid}/edit", array('date' => 'invalid-date'), t('Save')); + $this->assertText('You have to specify a valid date.'); + + $time = time() + 500; + $fields = array( + 'name' => '', + 'date' => format_date($time, 'custom', 'Y-m-d H:i:s O'), + ); + $this->drupalPost("node/{$this->node->nid}/edit", $fields, t('Save')); + $this->assertNodeFieldsUpdated($this->node, array('uid' => 0, 'created' => $time)); + } + + $this->drupalLogin($this->normalUser); $this->assertNodeFieldsNoAccess($this->node, array_keys($fields)); } + + /** + * Ensure that the node created date does not change when the node is edited. + */ + public function testNodeCreatedDateDoesNotChange() { + $this->drupalLogin( + $this->drupalCreateUser(array('edit any page content')) + ); + + $node = $this->drupalCreateNode(); + + // Update the node. + $this->drupalPost("node/{$node->nid}/edit", array(), t('Save')); + + // Load a new instance of the node. + $node2 = node_load($node->nid); + + // Ensure that the node was updated by comparing the changed dates, but the + // created dates still match. + $this->assertNotEqual($node->changed, $node2->changed, t('Changed values do not match.')); + $this->assertEqual($node->created, $node2->created, t('Created values do match.')); + } + } diff --git a/sites/all/modules/contrib/admin/redirect/redirect.admin.inc b/sites/all/modules/contrib/admin/redirect/redirect.admin.inc index 3d70ebf4..1e804383 100644 --- a/sites/all/modules/contrib/admin/redirect/redirect.admin.inc +++ b/sites/all/modules/contrib/admin/redirect/redirect.admin.inc @@ -18,6 +18,7 @@ function redirect_list_form($form, &$form_state) { $header = array( 'source' => array('data' => t('From'), 'field' => 'source', 'sort' => 'asc'), 'redirect' => array('data' => t('To'), 'field' => 'redirect'), + 'status' => array('data' => t('Status'), 'field' => 'status'), 'status_code' => array('data' => t('Type'), 'field' => 'status_code'), 'language' => array('data' => t('Language'), 'field' => 'language'), 'count' => array('data' => t('Count'), 'field' => 'count'), @@ -84,6 +85,7 @@ function redirect_list_form($form, &$form_state) { drupal_alter('redirect_url', $redirect->redirect, $redirect->redirect_options); $row['source'] = l($source_url, $redirect->source, $redirect->source_options); $row['redirect'] = l($redirect_url, $redirect->redirect, $redirect->redirect_options); + $row['status'] = $redirect->status ? t('Enabled') : t('Disabled'); $row['status_code'] = $redirect->status_code ? $redirect->status_code : t('Default (@default)', array('@default' => $default_status_code)); $row['language'] = module_invoke('locale', 'language_name', $redirect->language); $row['count'] = $redirect->count; @@ -262,6 +264,10 @@ function redirect_list_form_operations_submit($form, &$form_state) { } call_user_func_array($function, $args); + // We display the number of redirects the user selected, regardless of + // how many redirects actually changed status. Eg. if 1 enabled and 1 + // disabled redirects are checked for being enabled, we'll still display + // "Enabled 1 redirect." $count = count($form_state['values']['rids']); watchdog('redirect', '@action @count redirects.', array('@action' => $operation['action_past'], '@count' => $count)); drupal_set_message(format_plural(count($rids), '@action @count redirect.', '@action @count redirects.', array('@action' => $operation['action_past'], '@count' => $count))); @@ -336,6 +342,10 @@ function redirect_edit_form($form, &$form_state, $redirect = NULL) { '#type' => 'value', '#value' => $redirect->hash, ); + $form['uid'] = array( + '#type' => 'value', + '#value' => $redirect->uid, + ); $form['source'] = array( '#type' => 'textfield', @@ -361,6 +371,7 @@ function redirect_edit_form($form, &$form_state, $redirect = NULL) { '#description' => t('Enter an internal Drupal path, path alias, or complete external URL (like http://example.com/) to redirect to. Use %front to redirect to the front page.', array('%front' => '')), '#element_validate' => array('redirect_element_validate_redirect'), ); + $form['redirect_options'] = array( '#type' => 'value', '#value' => $redirect->redirect_options, @@ -373,6 +384,14 @@ function redirect_edit_form($form, &$form_state, $redirect = NULL) { '#value' => $redirect->language, ); + $form['status'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled'), + '#description' => t('If this box is checked, this redirect will be enabled.'), + '#default_value' => $redirect->status, + '#required' => FALSE, + ); + $form['advanced'] = array( '#type' => 'fieldset', '#title' => t('Advanced options'), @@ -465,7 +484,7 @@ function redirect_element_validate_redirect($element, &$form_state) { // Normalize the path. $value = drupal_get_normal_path($value, $form_state['values']['language']); - if (!valid_url($value) && !valid_url($value, TRUE) && $value != '' && $value != '') { + if (!valid_url($value) && !valid_url($value, TRUE) && $value != '' && $value != '' && !file_exists($value)) { form_error($element, t('The redirect path %value is not valid.', array('%value' => $value))); } @@ -520,10 +539,13 @@ function redirect_edit_form_validate($form, &$form_state) { $redirect = (object) $form_state['values']; if (empty($form_state['values']['override'])) { - if ($existing = redirect_load_by_source($redirect->source, $redirect->language)) { + // Find out if any (disabled or enabled) redirect with this source already + // exists. + if ($existing = redirect_load_by_source($redirect->source, $redirect->language, array(), FALSE)) { if ($redirect->rid != $existing->rid && $redirect->language == $existing->language) { - // The "from" path should not conflict with another redirect - $form_state['storage']['override_messages']['redirect-conflict'] = t('The base source path %source is already being redirected. Do you want to edit the existing redirect?', array('%source' => $redirect->source, '@edit-page' => url('admin/config/search/redirect/edit/'. $existing->rid))); + // The "from" path should not conflict with another (disabled or + // enabled) redirect. + $form_state['storage']['override_messages']['redirect-conflict'] = t('A redirect already exists for the source path %source. Do you want to edit the existing redirect?', array('%source' => $redirect->source, '@edit-page' => url('admin/config/search/redirect/edit/'. $existing->rid))); $form_state['rebuild'] = TRUE; } } @@ -621,7 +643,7 @@ function redirect_settings_form($form, &$form_state) { '#type' => 'checkbox', '#title' => t('Allow redirects to be saved into the page cache.'), '#default_value' => variable_get('redirect_page_cache', 0), - '#description' => t('This feature requires Cache pages for anonymous users to be enabled and the %variable variable to be TRUE.', array('@performance' => url('admin/config/development/performance'), '%variable' => "\$conf['page_cache_invoke_hooks']")), + '#description' => t('This feature requires Cache pages for anonymous users to be enabled and the %variable variable to be true (currently set to @value).', array('@performance' => url('admin/config/development/performance'), '%variable' => "\$conf['page_cache_invoke_hooks']", '@value' => var_export(variable_get('page_cache_invoke_hooks', TRUE), TRUE))), '#disabled' => !variable_get('cache', 0) || !variable_get('page_cache_invoke_hooks', TRUE), ); $form['redirect_purge_inactive'] = array( @@ -629,7 +651,7 @@ function redirect_settings_form($form, &$form_state) { '#title' => t('Delete redirects that have not been accessed for'), '#default_value' => variable_get('redirect_purge_inactive', 0), '#options' => array(0 => t('Never (do not discard)')) + drupal_map_assoc(array(604800, 1209600, 1814400, 2592000, 5184000, 7776000, 10368000, 15552000, 31536000), 'format_interval'), - '#description' => t('Only redirects managaged by the redirect module itself will be deleted. Redirects managed by other modules will be left alone.'), + '#description' => t('Only redirects managed by the redirect module itself will be deleted. Redirects managed by other modules will be left alone.'), '#disabled' => variable_get('redirect_page_cache', 0) && !variable_get('page_cache_invoke_hooks', TRUE), ); @@ -814,6 +836,7 @@ function redirect_list_table($redirects, $header) { $header = array_intersect_key(array( 'source' => array('data' => t('From'), 'field' => 'source', 'sort' => 'asc'), 'redirect' => array('data' => t('To'), 'field' => 'redirect'), + 'status' => array('data' => t('Status'), 'field' => 'status'), 'status_code' => array('data' => t('Type'), 'field' => 'status_code'), 'language' => array('data' => t('Language'), 'field' => 'language'), 'count' => array('data' => t('Count'), 'field' => 'count'), @@ -834,6 +857,7 @@ function redirect_list_table($redirects, $header) { $redirect_url = redirect_url($redirect->redirect, array_merge($redirect->redirect_options, array('alias' => TRUE))); $row['data']['source'] = l($source_url, $redirect->source, $redirect->source_options); $row['data']['redirect'] = l($redirect_url, $redirect->redirect, $redirect->redirect_options); + $row['data']['status'] = $redirect->status ? t('Enabled') : t('Disabled'); $row['data']['status_code'] = $redirect->status_code ? $redirect->status_code : t('Default (@default)', array('@default' => $default_status_code)); $row['data']['language'] = module_invoke('locale', 'language_name', $redirect->language); $row['data']['count'] = $redirect->count; @@ -852,6 +876,12 @@ function redirect_list_table($redirects, $header) { $row['class'][] = 'warning'; $row['title'] = t('This redirect overrides an existing internal path.'); } + if ($redirect->status) { + $row['class'][] = 'redirect-enabled'; + } + else { + $row['class'][] = 'redirect-disabled'; + } $operations = array(); if (redirect_access('update', $redirect)) { diff --git a/sites/all/modules/contrib/admin/redirect/redirect.controller.inc b/sites/all/modules/contrib/admin/redirect/redirect.controller.inc new file mode 100755 index 00000000..2d289b1e --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.controller.inc @@ -0,0 +1,20 @@ + $redirect) { + $redirects[$key]->source_options = unserialize($redirect->source_options); + $redirects[$key]->redirect_options = unserialize($redirect->redirect_options); + } + parent::attachLoad($redirects, $revision_id); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.info b/sites/all/modules/contrib/admin/redirect/redirect.info index b87b4e8f..b04e0a38 100644 --- a/sites/all/modules/contrib/admin/redirect/redirect.info +++ b/sites/all/modules/contrib/admin/redirect/redirect.info @@ -1,9 +1,7 @@ name = Redirect description = Allows users to redirect from old URLs to new URLs. core = 7.x -files[] = redirect.module -files[] = redirect.admin.inc -files[] = redirect.install +files[] = redirect.controller.inc files[] = redirect.test files[] = views/redirect.views.inc ;files[] = views/redirect_handler_field_redirect_type.inc @@ -15,9 +13,9 @@ files[] = views/redirect_handler_field_redirect_link_edit.inc files[] = views/redirect_handler_field_redirect_link_delete.inc configure = admin/config/search/redirect/settings -; Information added by drupal.org packaging script on 2012-09-18 -version = "7.x-1.0-rc1" +; Information added by Drupal.org packaging script on 2015-07-08 +version = "7.x-1.0-rc3" core = "7.x" project = "redirect" -datestamp = "1347989995" +datestamp = "1436393342" diff --git a/sites/all/modules/contrib/admin/redirect/redirect.install b/sites/all/modules/contrib/admin/redirect/redirect.install index fa67aaa5..a8fea019 100644 --- a/sites/all/modules/contrib/admin/redirect/redirect.install +++ b/sites/all/modules/contrib/admin/redirect/redirect.install @@ -86,7 +86,14 @@ function redirect_schema() { 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, - 'description' => 'The timestamp of when the redirect was last accessed.' + 'description' => 'The timestamp of when the redirect was last accessed.', + ), + 'status' => array( + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 1, + 'description' => 'Boolean indicating whether the redirect is enabled (visible to non-administrators).', ), ), 'primary key' => array('rid'), @@ -95,7 +102,11 @@ function redirect_schema() { ), 'indexes' => array( 'expires' => array('type', 'access'), - 'source_language' => array('source', 'language'), + 'status_source_language' => array( + 'status', + 'source', + 'language', + ), ), ); @@ -263,6 +274,58 @@ function redirect_update_7000(&$sandbox) { } } +/** + * Rebuild the registry and clear the entity info cache. + */ +function redirect_update_7100() { + if (!class_exists('RedirectController')) { + registry_rebuild(); + entity_info_cache_clear(); + } +} + +/** + * Add status field. + */ +function redirect_update_7101() { + db_add_field('redirect', 'status', array( + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 1, + 'description' => 'Boolean indicating whether the redirect is enabled (visible to non-administrators).', + )); + db_drop_index('redirect', 'source_language'); + db_add_index('redirect', 'status_source_language', array( + 'status', + 'source', + 'language', + )); +} + +/** + * Disable redirects that could cause infinite loops. + */ +function redirect_update_7102() { + $rids = db_query("SELECT r.rid FROM {redirect} r INNER JOIN {url_alias} u ON r.source = u.alias AND r.redirect = u.source AND r.language = u.language")->fetchCol(); + if ($rids) { + // Disable redirects + $count = db_update('redirect') + ->fields(array('status' => 0)) + ->condition('rid', $rids) + ->execute(); + + $disabled_redirects_message = format_plural($count, + '1 circular redirect causing infinite loop was disabled.', + '@count circular redirects causing infinite loop were disabled.'); + + return $disabled_redirects_message; + } + else { + return t('No circular redirects were found that could cause infinite loops.'); + } +} + /** * Migrate a path redirect redirect to a redirect redirect. */ @@ -315,6 +378,7 @@ function _redirect_migrate_path_redirect_redirect($old_redirect) { 'redirect_options' => serialize($redirect->redirect_options), 'language' => $redirect->language, 'status_code' => $redirect->status_code, + 'status' => 1, 'count' => 0, 'access' => $old_redirect->last_used, )) diff --git a/sites/all/modules/contrib/admin/redirect/redirect.js b/sites/all/modules/contrib/admin/redirect/redirect.js index c59785d6..fedbaede 100644 --- a/sites/all/modules/contrib/admin/redirect/redirect.js +++ b/sites/all/modules/contrib/admin/redirect/redirect.js @@ -4,12 +4,23 @@ Drupal.behaviors.redirectFieldsetSummaries = { attach: function (context) { $('fieldset.redirect-list', context).drupalSetSummary(function (context) { - if ($('table.redirect-list tbody td.empty', context).size()) { + if ($('table.redirect-list tbody td.empty', context).length) { return Drupal.t('No redirects'); } else { - var redirects = $('table.redirect-list tbody tr').size(); - return Drupal.formatPlural(redirects, '1 redirect', '@count redirects'); + var enabled_redirects = $('table.redirect-list tbody tr.redirect-enabled', context).length; + var disabled_redirects = $('table.redirect-list tbody tr.redirect-disabled', context).length; + var text = ''; + if (enabled_redirects > 0) { + var text = Drupal.formatPlural(enabled_redirects, '1 enabled redirect', '@count enabled redirects'); + } + if (disabled_redirects > 0) { + if (text.length > 0) { + text = text + '
'; + } + text = text + Drupal.formatPlural(disabled_redirects, '1 disabled redirect', '@count disabled redirects'); + } + return text; } }); } diff --git a/sites/all/modules/contrib/admin/redirect/redirect.module b/sites/all/modules/contrib/admin/redirect/redirect.module index 9f7a256f..993eeed0 100644 --- a/sites/all/modules/contrib/admin/redirect/redirect.module +++ b/sites/all/modules/contrib/admin/redirect/redirect.module @@ -46,24 +46,6 @@ function redirect_entity_info() { return $info; } -/** - * Controller class for redirects. - * - * This extends the DrupalDefaultEntityController class, adding required - * special handling for redirect objects. - */ -class RedirectController extends DrupalDefaultEntityController { - - protected function attachLoad(&$redirects, $revision_id = FALSE) { - // Unserialize the URL option fields. - foreach ($redirects as $key => $redirect) { - $redirects[$key]->source_options = unserialize($redirect->source_options); - $redirects[$key]->redirect_options = unserialize($redirect->redirect_options); - } - parent::attachLoad($redirects, $revision_id); - } -} - /** * Implements hook_hook_info(). */ @@ -245,11 +227,12 @@ function redirect_url_inbound_alter(&$path, $original_path, $path_language) { } // Redirect to canonical URLs. - if ($path && variable_get('redirect_canonical', 1)) { - $alias = drupal_get_path_alias($path, $path_language); - if ($alias != $path && $alias != $original_path) { + // Commented out per https://www.drupal.org/node/2048137. + //if ($path && variable_get('redirect_canonical', 1)) { + //$alias = drupal_get_path_alias($path, $path_language); + //if ($alias != $path && $alias != $original_path) { //return redirect_redirect(array('redirect' => $alias, 'type' => 'global')); - } + //} // Redirect from default entity paths to the proper entity path. //if ($path_entity = redirect_load_entity_from_path($path)) { @@ -259,7 +242,7 @@ function redirect_url_inbound_alter(&$path, $original_path, $path_language) { // } // } //} - } + //} } /** @@ -394,6 +377,8 @@ function redirect_path_update(array $path) { } if (!empty($path['original']['pid']) && $path['original']['pid'] == $path['pid'] && $path['original']['alias'] != $path['alias']) { + // Disable all redirects having the same source as this alias. + redirect_disable_by_path($path['alias'], $path['language']); $redirect = new stdClass(); redirect_object_prepare($redirect); $redirect->source = $path['original']['alias']; @@ -401,9 +386,25 @@ function redirect_path_update(array $path) { $redirect->language = $path['original']['language']; // Check if the redirect exists before saving. $hash = redirect_hash($redirect); - if (!redirect_load_by_hash($hash)) { + $existing = redirect_load_by_hash($hash); + if (!$existing) { redirect_save($redirect); } + // If the existing redirect is disabled, re-enable it. + elseif (isset($existing->status) && $existing->status == 0) { + $existing->status = 1; + redirect_save($existing); + } + } +} + +/** + * Implements hook_path_insert(). + */ +function redirect_path_insert(array $path) { + if (!empty($path['alias'])) { + // Disable all redirects having the same source as this alias. + redirect_disable_by_path($path['alias'], $path['language']); } } @@ -542,24 +543,26 @@ function redirect_load_by_hash($hash, $reset = FALSE) { } /** - * Load multiple URL redirects from the database by {redirect}.source. + * Fetches multiple URL redirect IDs from the database by {redirect}.source. * * @param $source * The source of the URL redirect. + * @param $language + * Language of the source URL. + * @param $enabled_only + * Boolean that indicates whether to only load enabled redirects. * - * @return - * An array of URL redirect objects indexed by redirect IDs. - * - * @see redirect_load_multiple() - * @see _redirect_uasort() - * @see redirect_compare_array_recursive() - * - * @ingroup redirect_api + * @return array + * An indexed array of IDs, or an empty array if there is no result set. */ -function redirect_load_by_source($source, $language = LANGUAGE_NONE, array $query = array()) { +function redirect_fetch_rids_by_path($source, $language, $enabled_only = FALSE) { // Run a case-insensitive query for matching RIDs first. $rid_query = db_select('redirect'); $rid_query->addField('redirect', 'rid'); + // Prevent errors if redirect_update_7101() has not yet been run. + if ($enabled_only && db_field_exists('redirect', 'status')) { + $rid_query->condition('status', 1); + } if ($source != variable_get('site_frontpage', 'node')) { $rid_query->condition('source', db_like($source), 'LIKE'); } @@ -571,7 +574,33 @@ function redirect_load_by_source($source, $language = LANGUAGE_NONE, array $quer } $rid_query->condition('language', array($language, LANGUAGE_NONE)); $rids = $rid_query->execute()->fetchCol(); + return $rids; +} + +/** + * Load multiple URL redirects from the database by {redirect}.source. + * + * @param $source + * The source of the URL redirect. + * @param $language + * Language of the source URL. + * @param $query + * Array of URL query parameters. + * @param $enabled_only + * Boolean that indicates whether to only load enabled redirects. + * + * @return + * The first matched URL redirect object, or FALSE if there aren't any. + * + * @see redirect_load_multiple() + * @see _redirect_uasort() + * @see redirect_compare_array_recursive() + * + * @ingroup redirect_api + */ +function redirect_load_by_source($source, $language = LANGUAGE_NONE, array $query = array(), $enabled_only = TRUE) { + $rids = redirect_fetch_rids_by_path($source, $language, $enabled_only); if ($rids && $redirects = redirect_load_multiple($rids)) { // Narrow down the list of candidates. foreach ($redirects as $rid => $redirect) { @@ -700,11 +729,11 @@ function redirect_validate($redirect, $form, &$form_state) { redirect_hash($redirect); if ($existing = redirect_load_by_hash($redirect->hash)) { if ($redirect->rid != $existing->rid) { - form_set_error('source', t('The source path %source is already being redirected. Do you want to edit the existing redirect?', array('%source' => redirect_url($redirect->source, $redirect->source_options), '@edit-page' => url('admin/config/search/redirect/edit/'. $existing->rid)))); + form_set_error('source', t('A redirect already exists for the source path %source. Do you want to edit the existing redirect?', array('%source' => redirect_url($redirect->source, $redirect->source_options), '@edit-page' => url('admin/config/search/redirect/edit/'. $existing->rid)))); } } - // Allow other modules to validate the SSH public key. + // Allow other modules to validate the redirect. foreach (module_implements('redirect_validate') as $module) { $function = $module . '_redirect_validate'; $function($redirect, $form, $form_state); @@ -723,6 +752,7 @@ function redirect_object_prepare($redirect, $defaults = array()) { 'count' => 0, 'access' => 0, 'hash' => '', + 'status' => 1, ); foreach ($defaults as $key => $default) { @@ -865,6 +895,28 @@ function redirect_delete_by_path($path) { } } +/** + * Disable any redirects associated with a path. + * + * Given a source like 'node/1' this function will delete any redirects that + * have that specific source. + * + * @param $path + * An string with an internal Drupal path. + * + * @param @langauge + * The langcode of the path. + * + * @ingroup redirect_api + */ +function redirect_disable_by_path($path, $language) { + $rids = redirect_fetch_rids_by_path($path, $language, FALSE); + + if ($rids) { + return redirect_change_status_multiple($rids, 0); + } +} + /** * Delete an entity URL alias and any of its sub-paths. * @@ -879,7 +931,8 @@ function redirect_delete_by_path($path) { * @ingroup redirect_api */ function redirect_delete_by_entity_path($entity_type, $entity) { - if ($uri = entity_uri($entity_type, $entity)) { + $uri = entity_uri($entity_type, $entity); + if (!empty($uri['path'])) { redirect_delete_by_path($uri['path']); } @@ -893,6 +946,48 @@ function redirect_delete_by_entity_path($entity_type, $entity) { //} } +/** + * Change the status of multiple URL redirects. + * + * @param array $rids + * An array of redirect IDs to disable. + * @param int|string $status + * The status to set the redirect to: either disabled (0) or enabled (1). + * + * @ingroup redirect_api + */ +function redirect_change_status_multiple(array $rids, $status) { + if ($status !== 0 && $status !== 1 && $status !== '0' && $status !== '1') { + watchdog('Cannot change redirect status to %status', array('%status' => $status)); + drupal_set_message(t('Cannot change redirect status to %status', array('%status' => $status))); + return; + } + if (!empty($rids)) { + $redirects = redirect_load_multiple($rids, array(), TRUE); + + foreach ($redirects as $rid => $redirect) { + if (isset($redirect->status) && $redirect->status == $status) { + // We no-op if the redirect is not actually changing status. + // So if a disabled redirect is disabled, neither redirect_save() is + // triggered, nor do we log any message. + continue; + } + + $redirect->status = $status; + redirect_save($redirect); + + if ($status) { + $redirect_link = l($redirect->redirect, $redirect->redirect) . '=> ' . l($redirect->source, $redirect->source); + watchdog('redirect', 'Enabled redirect: !redirect_source_link', array('!redirect_link' => $redirect_link)); + } + else { + $redirect_link = l($redirect->redirect, $redirect->redirect) . '=> ' . l($redirect->source, $redirect->source); + watchdog('redirect', 'Disabled redirect: !redirect_source_link', array('!redirect_link' => $redirect_link)); + } + } + } +} + /** * Delete multiple URL redirects. * @@ -947,10 +1042,15 @@ function redirect_purge_inactive_redirects(array $types = array('redirect'), $in $interval = variable_get('redirect_purge_inactive', 0); } - if (!$interval || !variable_get('redirect_page_cache', 0) || !variable_get('page_cache_invoke_hooks', TRUE)) { + if (!$interval) { + return FALSE; + } + + if (variable_get('redirect_page_cache', 0) && !variable_get('page_cache_invoke_hooks', TRUE)) { // If serving redirects from the page cache is enabled and hooks are not // executed during page caching, then we cannot track when a redirect is // used. Therefore, we cannot remove unused redirects. + watchdog('redirect', 'Due to existing settings, could not track when a redirect is used, so could not remove unused redirects.'); return FALSE; } @@ -979,17 +1079,6 @@ function redirect_purge_inactive_redirects(array $types = array('redirect'), $in * @ingroup redirect_api */ function redirect_redirect($redirect = NULL) { - // First check if we're in an infinite loop. - $session_id = session_id(); - if (flood_is_allowed('redirection', 5, 15, $session_id ? $session_id : NULL)) { - flood_register_event('redirection', 60, $session_id ? $session_id : NULL); - } - else { - watchdog('redirect', 'Infinite loop stopped.'); - drupal_set_message('Oops, looks like this request tried to create an infinite loop. We do not allow such things here. We are a professional website!'); - return FALSE; - } - if (!isset($redirect)) { $redirect = new stdClass(); } @@ -1017,6 +1106,10 @@ function redirect_redirect($redirect = NULL) { // Continue if the redirect has not been disabled by hook_redirect_alter(). if (isset($redirect->redirect) && isset($redirect->callback) && $redirect->redirect !== FALSE && function_exists($redirect->callback)) { + // Prevent circular redirects. + if ($GLOBALS['base_root'] . request_uri() == url($redirect->redirect, array('absolute' => TRUE) + $redirect->redirect_options)) { + return FALSE; + } // Perform the actual redirect. $callback = $redirect->callback; $callback($redirect); @@ -1150,7 +1243,7 @@ function redirect_can_redirect() { $path = current_path(); $can_redirect = TRUE; - if ($_SERVER['SCRIPT_NAME'] != $GLOBALS['base_path'] . 'index.php') { + if (!preg_match('/index\.php$/', $_SERVER['SCRIPT_NAME'])) { // Do not redirect if the root script is not /index.php. $can_redirect = FALSE; } @@ -1162,7 +1255,7 @@ function redirect_can_redirect() { // If this is a command line request (Drush, etc), skip processing. $can_redirect = FALSE; } - elseif (variable_get('maintenance_mode', 0) || defined('MAINTENANCE_MODE')) { + elseif ((variable_get('maintenance_mode', 0) || defined('MAINTENANCE_MODE')) && !user_access('access site in maintenance mode')) { // Do not redirect in offline or maintenance mode. $can_redirect = FALSE; } @@ -1176,7 +1269,7 @@ function redirect_can_redirect() { } /** - * Compare tha all values and associations in one array match another array. + * Compare that all values and associations in one array match another array. * * We cannot use array_diff_assoc() here because we need to be recursive. * @@ -1457,7 +1550,7 @@ function redirect_field_attach_form($entity_type, $entity, &$form, &$form_state, } $uri = entity_uri($entity_type, $entity); - if (empty($uri)) { + if (empty($uri['path'])) { // If the entity has no source path, then we cannot lookup the existing // redirects. return; @@ -1467,7 +1560,6 @@ function redirect_field_attach_form($entity_type, $entity, &$form, &$form_state, $form['redirect'] = array( '#type' => 'fieldset', '#title' => t('URL redirects'), - '#description' => t('The following are a list of URL redirects that point to this @entitytype.', array('@entitytype' => drupal_strtolower($info['label']))), '#collapsible' => TRUE, '#collapsed' => TRUE, '#access' => user_access('administer redirects'), @@ -1483,13 +1575,6 @@ function redirect_field_attach_form($entity_type, $entity, &$form, &$form_state, } } - // We don't have to put our include in $form_state['build_info']['files'] - // since the build array will already be cached. - module_load_include('inc', 'redirect', 'redirect.admin'); - $redirects = redirect_load_multiple(FALSE, array('redirect' => $uri['path'])); - $header = array('source', 'status_code', 'language', 'count', 'access', 'operations'); - $form['redirect'] += redirect_list_table($redirects, $header); - $redirect = array( 'redirect' => $uri['path'], 'redirect_options' => array_diff_key($uri['options'], array('entity_type' => '', 'entity' => '')), @@ -1508,6 +1593,13 @@ function redirect_field_attach_form($entity_type, $entity, &$form, &$form_state, 'query' => array_filter($redirect) + drupal_get_destination(), ); } + + // We don't have to put our include in $form_state['build_info']['files'] + // since the build array will already be cached. + module_load_include('inc', 'redirect', 'redirect.admin'); + $redirects = redirect_load_multiple(FALSE, array('redirect' => $uri['path'])); + $header = array('source', 'status', 'status_code', 'language', 'count', 'access', 'operations'); + $form['redirect'] += redirect_list_table($redirects, $header); } /** @@ -1516,8 +1608,8 @@ function redirect_field_attach_form($entity_type, $entity, &$form, &$form_state, function redirect_field_extra_fields() { $entity_info = entity_get_info(); foreach (array_keys($entity_info) as $entity_type) { - if ($entity_type == 'comment') { - // The comment entity type supports URIs, but they're not real. + if (!redirect_entity_type_supports_redirects($entity_type)) { + // Redirect is explicitly disabled for this entity type. continue; } foreach (array_keys($entity_info[$entity_type]['bundles']) as $bundle) { @@ -1564,5 +1656,19 @@ function redirect_redirect_operations() { 'callback' => 'redirect_delete_multiple', 'confirm' => TRUE, ); + $operations['disable'] = array( + 'action' => t('Disable'), + 'action_past' => t('Disabled'), + 'callback' => 'redirect_change_status_multiple', + 'callback arguments' => array(0), + 'confirm' => TRUE, + ); + $operations['enable'] = array( + 'action' => t('Enable'), + 'action_past' => t('Enabled'), + 'callback' => 'redirect_change_status_multiple', + 'callback arguments' => array(1), + 'confirm' => TRUE, + ); return $operations; } diff --git a/sites/all/modules/contrib/admin/redirect/redirect.test b/sites/all/modules/contrib/admin/redirect/redirect.test index 1c237f83..33c6d23d 100644 --- a/sites/all/modules/contrib/admin/redirect/redirect.test +++ b/sites/all/modules/contrib/admin/redirect/redirect.test @@ -229,16 +229,59 @@ class RedirectFunctionalTest extends RedirectTestHelper { $node = $this->drupalCreateNode(array('type' => 'article', 'path' => array('alias' => 'first-alias'))); // Change the node's alias will create an automatic redirect from 'first-alias' to the node. - $this->drupalPost("node/{$node->nid}/edit", array('path[alias]' => 'second-alias'), 'Save'); - //$redirect = redirect_load_by_source('first-alias'); - //$this->assertRedirect($redirect); + $this->drupalPost("node/{$node->nid}/edit", array('path[alias]' => 'second-alias'), t('Save')); + $this->drupalGet('first-alias'); + $this->assertText($node->title); - $this->drupalPost("node/{$node->nid}/edit", array('path[alias]' => 'first-alias'), 'Save'); - //$redirect = redirect_load_by_source('second-alias'); - //$this->assertRedirect($redirect); + $this->drupalPost("node/{$node->nid}/edit", array('path[alias]' => 'first-alias'), t('Save')); + $this->assertResponse(200, "Changing node's alias back to 'first-alias' does not break page load with a circular redirect."); + $this->assertNoText('Infinite redirect loop prevented.'); + $this->drupalGet('second-alias'); + $this->assertText($node->title); - $this->drupalPost("node/{$node->nid}/edit", array('path[alias]' => 'second-alias'), 'Save'); - //$redirect = redirect_load_by_source('first-alias'); - //$this->assertRedirect($redirect); + $this->drupalPost("node/{$node->nid}/edit", array('path[alias]' => 'second-alias'), t('Save')); + $this->assertResponse(200, "Changing node's alias back to 'second-alias' does not break page load with a circular redirect."); + $this->assertNoText('Infinite redirect loop prevented.'); + // Check that first-alias redirect has been re-enabled. + $this->drupalGet('first-alias'); + $this->assertText($node->title); + } + + function testPathAddOverwriteRedirects() { + // Create an initial article node with a path alias. + $first_node = $this->drupalCreateNode(array('type' => 'article', 'path' => array('alias' => 'first-alias'))); + // Change the node's alias will create an automatic redirect from 'first-alias' to the node. + $this->drupalPost("node/{$first_node->nid}/edit", array('path[alias]' => 'second-alias'), t('Save')); + + // Now create a second article node with the same alias as the redirect + // created above. + $second_node = $this->drupalCreateNode(array('type' => 'article', 'path' => array('alias' => 'first-alias'))); + + // Visit the path 'first-alias' which should be an alias for $second_node. + $this->drupalGet('first-alias'); + $this->assertNoText($first_node->title, 'Adding a new path alias that matches an existing redirect disables the redirect.'); + $this->assertText($second_node->title, 'Adding a new path alias that matches an existing redirect disables the redirect.'); + } + + function testDisableEnableRedirect() { + // Add a new redirect. + $redirect = $this->addRedirect('redirect', 'node'); + // Check that it is enabled. + $this->assertEqual($redirect->status, 1); + + // Disable the redirect. + $edit = array('status' => FALSE); + $this->drupalPost("admin/config/search/redirect/edit/{$redirect->rid}", $edit, t('Save')); + $redirect = redirect_load($redirect->rid); + // Check that it has been disabled. + $this->assertEqual($redirect->status, 0); + $this->drupalGet("admin/config/search/redirect/edit/{$redirect->rid}"); + $this->assertNoFieldChecked('edit-status', 'status is unchecked'); + $this->assertNoRedirect($redirect); + + // Re-enable the redirect. + $edit = array('status' => 1); + $this->drupalPost("admin/config/search/redirect/edit/{$redirect->rid}", $edit, t('Save')); + $this->assertRedirect($redirect); } } diff --git a/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_redirect.inc b/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_redirect.inc index a5adb9eb..09930c80 100644 --- a/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_redirect.inc +++ b/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_redirect.inc @@ -45,11 +45,6 @@ class redirect_handler_field_redirect_redirect extends views_handler_field { ); } - function query() { - $this->ensure_my_table(); - $this->add_additional_fields(); - } - function render($values) { $redirect = $values->{$this->aliases['redirect']}; $redirect_options = unserialize($values->{$this->aliases['redirect_options']}); diff --git a/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_source.inc b/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_source.inc index 318f1bdb..b462d9a9 100644 --- a/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_source.inc +++ b/sites/all/modules/contrib/admin/redirect/views/redirect_handler_field_redirect_source.inc @@ -45,11 +45,6 @@ class redirect_handler_field_redirect_source extends views_handler_field { ); } - function query() { - $this->ensure_my_table(); - $this->add_additional_fields(); - } - function render($values) { $source = $values->{$this->aliases['source']}; $source_options = unserialize($values->{$this->aliases['source_options']}); diff --git a/sites/all/modules/contrib/admin/rules/DEVELOPER.txt b/sites/all/modules/contrib/admin/rules/DEVELOPER.txt index dee65569..d5d0f478 100644 --- a/sites/all/modules/contrib/admin/rules/DEVELOPER.txt +++ b/sites/all/modules/contrib/admin/rules/DEVELOPER.txt @@ -23,4 +23,4 @@ Terminology & Overview outside of the rule admin module too. In fact the rules admin module is pretty small, as it just relies on the provided UI of the components. * The UI is incorporated using the faces object extension mechanism, see - rules_rules_plugin_info() for an overview of the used UI extenders. \ No newline at end of file + rules_rules_plugin_info() for an overview of the used UI extenders. diff --git a/sites/all/modules/contrib/admin/rules/README.txt b/sites/all/modules/contrib/admin/rules/README.txt index a4a56319..ee5360e6 100644 --- a/sites/all/modules/contrib/admin/rules/README.txt +++ b/sites/all/modules/contrib/admin/rules/README.txt @@ -9,7 +9,7 @@ Maintainers: The Rules module allows site administrators to define conditionally executed actions based on occurring events (ECA-rules). -Project homepage: http://drupal.org/project/rules +Project homepage: https://www.drupal.org/project/rules Installation @@ -18,10 +18,10 @@ Installation *Before* starting, make sure that you have read at least the introduction - so you know at least the basic concepts. You can find it here: - http://drupal.org/node/298480 + https://www.drupal.org/node/298480 * Rules depends on the Entity API module, download and install it from - http://drupal.org/project/entity + https://www.drupal.org/project/entity * Copy the whole rules directory to your modules directory (e.g. DRUPAL_ROOT/sites/all/modules) and activate the Rules and Rules UI modules. @@ -30,8 +30,8 @@ you know at least the basic concepts. You can find it here: Documentation ------------- -* Check out the general docs at http://drupal.org/node/298476 -* Check out the developer targeted docs at http://drupal.org/node/878718 +* Check out the general docs at https://www.drupal.org/node/298476 +* Check out the developer targeted docs at https://www.drupal.org/node/878718 Rules Scheduler @@ -41,9 +41,9 @@ Rules Scheduler to schedule the execution of Rules components. * Make sure that you have configured cron for your drupal installation as cron is used for scheduling the Rules components. For help see - http://drupal.org/cron - * If the Views module (http://drupal.org/project/views) is installed, the module - displays the list of scheduled tasks in the UI. + https://www.drupal.org/cron + * If the Views module (https://www.drupal.org/project/views) is installed, the + module displays the list of scheduled tasks in the UI. Upgrade from Rules 6.x-1.x to Rules 7.x-2.x @@ -60,7 +60,7 @@ Upgrade from Rules 6.x-1.x to Rules 7.x-2.x * Note that for importing an export the export needs to pass the configuration integrity check, what might be troublesome if the conversion was not 100% successful. In that case, try choosing the - immediate saving method and correct the configuration after conversion. + immediate saving method and correct the configuration after conversion. * A rule configuration might require multiple modules to be in place and upgraded to work properly. E.g. if you used an action provided by a third party module, make sure the module is in place and upgraded @@ -85,7 +85,7 @@ Upgrade from Rules 6.x-1.x to Rules 7.x-2.x for Drupal 7. The Drupal 6 tasks are preserved in the database as long as you do not clear your Rules 1.x configuration though. * The Rules Forms module has not been updated to Drupal 7 and there are no - plans to do so, as unfortuntely the module's design does not allow for + plans to do so, as unfortunately the module's design does not allow for automatic configuration updates. Thus, a possible future Rules 2.x Forms module is likely to work different, e.g. by working only for entity forms on the field level. diff --git a/sites/all/modules/contrib/admin/rules/fix_errors_on_update-2090511-214_2.patch b/sites/all/modules/contrib/admin/rules/fix_errors_on_update-2090511-214_2.patch deleted file mode 100644 index c1fe32ba..00000000 --- a/sites/all/modules/contrib/admin/rules/fix_errors_on_update-2090511-214_2.patch +++ /dev/null @@ -1,27 +0,0 @@ -diff --git a/rules.module b/rules.module -index 719852c..9e4ec8f 100644 ---- a/rules.module -+++ b/rules.module -@@ -8,6 +8,22 @@ - // hook_init(). - require_once dirname(__FILE__) . '/modules/events.inc'; - -+/** Rules >=2.4 introduces a class called 'RulesEventHandlerEntityBundle' found in -+ * 'includes/rules.event.inc', so we include this file for versions older than 2.4 -+ * in order to prevent a bug at node/2090511. -+ */ -+$result = db_query("SELECT schema_version FROM {system} WHERE name = :name", array( -+ ':name' => 'rules', -+)); -+ -+if ($result) { -+ while ($row = $result->fetchAssoc()) { -+ if ($row['schema_version'] <7210) { -+ require_once dirname(__FILE__) . '/includes/rules.event.inc'; -+ } -+ } -+} -+ - /** - * Implements hook_module_implements_alter(). - */ diff --git a/sites/all/modules/contrib/admin/rules/includes/faces.inc b/sites/all/modules/contrib/admin/rules/includes/faces.inc index 213cd818..d0fe8ce1 100644 --- a/sites/all/modules/contrib/admin/rules/includes/faces.inc +++ b/sites/all/modules/contrib/admin/rules/includes/faces.inc @@ -1,7 +1,8 @@ object = $object; } @@ -63,17 +65,17 @@ if (!class_exists('FacesExtender', FALSE)) { } /** - * Invokes any method on the extended object. May be used to invoke - * protected methods. + * Invokes any method on the extended object, including protected methods. * - * @param $name + * @param string $name * The method name. - * @param $arguments + * @param array $args * An array of arguments to pass to the method. */ protected function call($name, array $args = array()) { return $this->object->call($name, $args); } + } } @@ -108,7 +110,7 @@ if (!class_exists('FacesExtendable', FALSE)) { /** * Magic method: Invoke the dynamically implemented methods. */ - function __call($name, $arguments = array()) { + public function __call($name, $arguments = array()) { if (isset($this->facesMethods[$name])) { $method = $this->facesMethods[$name]; // Include code, if necessary. @@ -134,9 +136,11 @@ if (!class_exists('FacesExtendable', FALSE)) { } /** - * Returns the extender object for the given class. May be used to - * explicitly invoke a specific extender, e.g. a function overriding a - * method may use that to explicitly invoke the original extender. + * Returns the extender object for the given class. + * + * May be used to explicitly invoke a specific extender, e.g. a function + * overriding a method may use that to explicitly invoke the original + * extender. */ public function extender($class) { if (!isset($this->facesClassInstances[$class])) { @@ -146,14 +150,17 @@ if (!class_exists('FacesExtendable', FALSE)) { } /** + * Returns whether the object can face as the given interface. + * * Returns whether the object can face as the given interface, thus it - * returns TRUE if this oject has been extended by an appropriate + * returns TRUE if this object has been extended by an appropriate * implementation. * * @param $interface - * Optional. A interface to test for. If it's omitted, all interfaces that - * the object can be faced as are returned. - * @return + * Optional. An interface to test for. If it's omitted, all interfaces + * that the object can be faced as are returned. + * + * @return bool * Whether the object can face as the interface or an array of interface * names. */ @@ -169,9 +176,9 @@ if (!class_exists('FacesExtendable', FALSE)) { * * @param $interface * The interface name or an array of interface names. - * @param $class + * @param $className * The extender class, which has to implement the FacesExtenderInterface. - * @param $include + * @param array $includes * An optional array describing the file to include before invoking the * class. The array entries known are 'type', 'module', and 'name' * matching the parameters of module_load_include(). Only 'module' is @@ -206,10 +213,10 @@ if (!class_exists('FacesExtendable', FALSE)) { * @param $interface * The interface name or FALSE to extend the object without a given * interface. - * @param $methods + * @param array $callbacks * An array, where the keys are methods of the given interface and the * values the callback functions to use. - * @param $includes + * @param array $includes * An optional array to describe files to include before invoking the * callbacks. You may pass a single array describing one include for all * callbacks or an array of arrays, keyed by the method names. Look at the @@ -234,11 +241,11 @@ if (!class_exists('FacesExtendable', FALSE)) { /** * Override the implementation of an extended method. * - * @param $methods - * An array of methods of the interface, that should be overriden, where + * @param array $callbacks + * An array of methods of the interface, that should be overridden, where * the keys are methods to override and the values the callback functions * to use. - * @param $includes + * @param array $includes * An optional array to describe files to include before invoking the * callbacks. You may pass a single array describing one include for all * callbacks or an array of arrays, keyed by the method names. Look at the @@ -257,6 +264,7 @@ if (!class_exists('FacesExtendable', FALSE)) { /** * Adds in include files for the given methods while removing any old files. + * * If a single include file is described, it's added for all methods. */ protected function addIncludes($methods, $includes) { @@ -272,6 +280,8 @@ if (!class_exists('FacesExtendable', FALSE)) { } /** + * Destroys all references to created instances. + * * Destroys all references to created instances so that PHP's garbage * collection can do its work. This is needed as PHP's gc has troubles with * circular references until PHP < 5.3. @@ -296,9 +306,9 @@ if (!class_exists('FacesExtendable', FALSE)) { * This also allows to pass arguments by reference, so it may be used to * pass arguments by reference to dynamically extended methods. * - * @param $name + * @param string $name * The method name. - * @param $arguments + * @param array $args * An array of arguments to pass to the method. */ public function call($name, array $args = array()) { @@ -307,5 +317,7 @@ if (!class_exists('FacesExtendable', FALSE)) { } return $this->__call($name, $args); } + } -} \ No newline at end of file + +} diff --git a/sites/all/modules/contrib/admin/rules/includes/rules.core.inc b/sites/all/modules/contrib/admin/rules/includes/rules.core.inc index f2690fed..b00ed410 100644 --- a/sites/all/modules/contrib/admin/rules/includes/rules.core.inc +++ b/sites/all/modules/contrib/admin/rules/includes/rules.core.inc @@ -7,8 +7,7 @@ // This is not necessary as the classes are autoloaded via the registry. However // it saves some possible update headaches until the registry is rebuilt. -// @todo -// Remove for a future release. +// @todo Remove for a future release. require_once dirname(__FILE__) . '/faces.inc'; /** @@ -17,11 +16,23 @@ require_once dirname(__FILE__) . '/faces.inc'; class RulesEntityController extends EntityAPIControllerExportable { /** - * Overriden. + * Overridden. + * + * @see EntityAPIController::create() + */ + public function create(array $values = array()) { + // Default to rules as owning module. + $values += array('owner' => 'rules'); + return parent::create($values); + } + + /** + * Overridden. + * * @see DrupalDefaultEntityController::attachLoad() */ protected function attachLoad(&$queried_entities, $revision_id = FALSE) { - // Retrieve stdClass records and turn them in rules objects stored in 'data' + // Retrieve stdClass records and store them as rules objects in 'data'. $ids = array_keys($queried_entities); $result = db_select('rules_tags') ->fields('rules_tags', array('id', 'tag')) @@ -57,7 +68,8 @@ class RulesEntityController extends EntityAPIControllerExportable { /** * Override to support having events and tags as conditions. - * @see EntityAPIController::applyConditions($entities, $conditions) + * + * @see EntityAPIController::applyConditions() * @see rules_query_rules_config_load_multiple_alter() */ protected function applyConditions($entities, $conditions = array()) { @@ -95,6 +107,8 @@ class RulesEntityController extends EntityAPIControllerExportable { * @param $export * A serialized string in JSON format as produced by the * RulesPlugin::export() method, or the PHP export as usual PHP array. + * @param string $error_msg + * The error message. */ public function import($export, &$error_msg = '') { $export = is_array($export) ? $export : drupal_json_decode($export); @@ -102,8 +116,9 @@ class RulesEntityController extends EntityAPIControllerExportable { $error_msg = t('Unable to parse the pasted export.'); return FALSE; } - // The key ist the configuration name and the value the actual export. - list($name, $export) = each($export); + // The key is the configuration name and the value the actual export. + $name = key($export); + $export = current($export); if (!isset($export['PLUGIN'])) { $error_msg = t('Export misses plugin information.'); return FALSE; @@ -111,7 +126,7 @@ class RulesEntityController extends EntityAPIControllerExportable { // Create an empty configuration, re-set basic keys and import. $config = rules_plugin_factory($export['PLUGIN']); $config->name = $name; - foreach (array('label', 'active', 'weight', 'tags', 'access_exposed') as $key) { + foreach (array('label', 'active', 'weight', 'tags', 'access_exposed', 'owner') as $key) { if (isset($export[strtoupper($key)])) { $config->$key = $export[strtoupper($key)]; } @@ -132,12 +147,35 @@ class RulesEntityController extends EntityAPIControllerExportable { public function save($rules_config, DatabaseTransaction $transaction = NULL) { $transaction = isset($transaction) ? $transaction : db_transaction(); + // Load the stored entity, if any. + if (!isset($rules_config->original) && $rules_config->{$this->idKey}) { + $rules_config->original = entity_load_unchanged($this->entityType, $rules_config->{$this->idKey}); + } + $original = isset($rules_config->original) ? $rules_config->original : NULL; + $return = parent::save($rules_config, $transaction); $this->storeTags($rules_config); if ($rules_config instanceof RulesTriggerableInterface) { $this->storeEvents($rules_config); } $this->storeDependencies($rules_config); + + // See if there are any events that have been removed. + if ($original && $rules_config->plugin == 'reaction rule') { + foreach (array_diff($original->events(), $rules_config->events()) as $event_name) { + // Check if the event handler implements the event dispatcher interface. + $handler = rules_get_event_handler($event_name, $rules_config->getEventSettings($event_name)); + if (!$handler instanceof RulesEventDispatcherInterface) { + continue; + } + + // Only stop an event dispatcher if there are no rules for it left. + if (!rules_config_load_multiple(FALSE, array('event' => $event_name, 'plugin' => 'reaction rule', 'active' => TRUE)) && $handler->isWatching()) { + $handler->stopWatching(); + } + } + } + return $return; } @@ -167,10 +205,10 @@ class RulesEntityController extends EntityAPIControllerExportable { foreach ($rules_config->events() as $event) { db_insert('rules_trigger') ->fields(array( - 'id' => $rules_config->id, - 'event' => $event, - )) - ->execute(); + 'id' => $rules_config->id, + 'event' => $event, + )) + ->execute(); } } @@ -182,16 +220,17 @@ class RulesEntityController extends EntityAPIControllerExportable { foreach ($rules_config->dependencies as $dependency) { db_insert('rules_dependencies') ->fields(array( - 'id' => $rules_config->id, - 'module' => $dependency, - )) - ->execute(); + 'id' => $rules_config->id, + 'module' => $dependency, + )) + ->execute(); } } } /** * Overridden to support tags and events in $conditions. + * * @see EntityAPIControllerExportable::buildQuery() */ protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { @@ -199,7 +238,7 @@ class RulesEntityController extends EntityAPIControllerExportable { $query_conditions =& $query->conditions(); foreach ($query_conditions as &$condition) { // One entry in $query_conditions is a string with key '#conjunction'. - // @see QueryConditionInterface::conditions(). + // @see QueryConditionInterface::conditions() if (is_array($condition)) { // Support using 'tags' => array('tag1', 'tag2') as condition. if ($condition['field'] == 'base.tags') { @@ -210,6 +249,8 @@ class RulesEntityController extends EntityAPIControllerExportable { if ($condition['field'] == 'base.event') { $query->join('rules_trigger', 'tr', "base.id = tr.id"); $condition['field'] = 'tr.event'; + // Use like operator to support % wildcards also. + $condition['operator'] = 'LIKE'; } } } @@ -218,6 +259,7 @@ class RulesEntityController extends EntityAPIControllerExportable { /** * Overridden to also delete tags and events. + * * @see EntityAPIControllerExportable::delete() */ public function delete($ids, DatabaseTransaction $transaction = NULL) { @@ -237,11 +279,43 @@ class RulesEntityController extends EntityAPIControllerExportable { ->execute(); } } - return parent::delete($ids, $transaction); + $return = parent::delete($ids, $transaction); + + // Stop event dispatchers when deleting the last rule of an event set. + $processed = array(); + foreach ($configs as $config) { + if ($config->getPluginName() != 'reaction rule') { + continue; + } + + foreach ($config->events() as $event_name) { + // Only process each event once. + if (!empty($processed[$event_name])) { + continue; + } + $processed[$event_name] = TRUE; + + // Check if the event handler implements the event dispatcher interface. + $handler = rules_get_event_handler($event_name, $config->getEventSettings($event_name)); + if (!$handler instanceof RulesEventDispatcherInterface) { + continue; + } + + // Only stop an event dispatcher if there are no rules for it left. + if ($handler->isWatching() && !rules_config_load_multiple(FALSE, array('event' => $event_name, 'plugin' => 'reaction rule', 'active' => TRUE))) { + $handler->stopWatching(); + } + } + } + + return $return; } + } /** + * Base class for RulesExtendables. + * * The RulesExtendable uses the rules cache to setup the defined extenders * and overrides automatically. * As soon faces is used the faces information is autoloaded using setUp(). @@ -250,6 +324,7 @@ abstract class RulesExtendable extends FacesExtendable { /** * The name of the info definitions associated with info about this class. + * * This would be defined abstract, if possible. Common rules hooks with class * info are e.g. plugin_info and data_info. */ @@ -260,8 +335,8 @@ abstract class RulesExtendable extends FacesExtendable { */ protected $itemName; - protected $cache, $itemInfo = array(); - + protected $cache; + protected $itemInfo = array(); public function __construct() { $this->setUp(); @@ -274,14 +349,14 @@ abstract class RulesExtendable extends FacesExtendable { if (isset($this->cache[$this->hook][$this->itemName])) { $this->itemInfo = &$this->cache[$this->hook][$this->itemName]; } - // Set up the Faces Extenders + // Set up the Faces Extenders. if (!empty($this->itemInfo['faces_cache'])) { list($this->facesMethods, $this->facesIncludes, $this->faces) = $this->itemInfo['faces_cache']; } } /** - * Force the object to be setUp, this executes setUp() if not done yet. + * Forces the object to be setUp, this executes setUp() if not done yet. */ public function forceSetUp() { if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) { @@ -336,12 +411,14 @@ abstract class RulesExtendable extends FacesExtendable { * The info about the item as specified in the hook. * @param $interface * The interface to check for. - * @return + * + * @return bool * Whether it supports the given interface. */ public static function itemFacesAs(&$itemInfo, $interface) { return in_array($interface, class_implements($itemInfo['class'])) || isset($itemInfo['faces_cache'][2][$interface]); } + } /** @@ -373,11 +450,13 @@ abstract class RulesPlugin extends RulesExtendable { /** * The parent element, if any. + * * @var RulesContainerPlugin */ protected $parent = NULL; - protected $cache = NULL, $hook = 'plugin_info'; + protected $cache = NULL; + protected $hook = 'plugin_info'; /** * Identifies an element inside a configuration. @@ -389,7 +468,6 @@ abstract class RulesPlugin extends RulesExtendable { */ protected $availableVariables; - /** * Sets a new parent element. */ @@ -467,7 +545,7 @@ abstract class RulesPlugin extends RulesExtendable { * Iterate over all elements nested below the current element. * * This helper can be used to recursively iterate over all elements of a - * configuration. To iterate over the children only, just regulary iterate + * configuration. To iterate over the children only, just regularly iterate * over the object. * * @param $mode @@ -520,7 +598,7 @@ abstract class RulesPlugin extends RulesExtendable { /** * Evaluate the element on a given rules evaluation state. */ - abstract function evaluate(RulesState $state); + abstract public function evaluate(RulesState $state); protected static function compare(RulesPlugin $a, RulesPlugin $b) { if ($a->weight == $b->weight) { @@ -545,7 +623,7 @@ abstract class RulesPlugin extends RulesExtendable { /** * Returns info about parameters needed for executing the configured plugin. * - * @param $optional + * @param bool $optional * Whether optional parameters should be included. * * @see self::pluginParameterInfo() @@ -678,19 +756,21 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Returns asserted additions to the available variable info. Any returned - * info is merged into the variable info, in case the execution flow passes - * the element. + * Returns asserted additions to the available variable info. + * + * Any returned info is merged into the variable info, in case the execution + * flow passes the element. * E.g. this is used to assert the content type of a node if the condition - * is met, such that the per node type properties are available. + * is met, such that the per-node type properties are available. */ protected function variableInfoAssertions() { return array(); } /** - * Get the name of this plugin instance. The returned name should identify - * the code which drives this plugin. + * Gets the name of this plugin instance. + * + * The returned name should identify the code which drives this plugin. */ public function getPluginName() { return $this->itemName; @@ -747,7 +827,6 @@ abstract class RulesPlugin extends RulesExtendable { * Usually settings get processed automatically, however if $this->settings * has been altered manually after element construction, it needs to be * invoked explicitly with $force set to TRUE. - * */ public function processSettings($force = FALSE) { // Process if not done yet. @@ -773,14 +852,15 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Makes sure the plugin is configured right, e.g. all needed variables - * are available in the element's scope and dependent modules are enabled. + * Makes sure the plugin is configured right. * - * @return RulesPlugin - * Returns $this to support chained usage. + * "Configured right" means all needed variables are available in the + * element's scope and dependent modules are enabled. + * + * @return $this * * @throws RulesIntegrityException - * In case of a failed integraty check, a RulesIntegrityException exception + * In case of a failed integrity check, a RulesIntegrityException exception * is thrown. */ public function integrityCheck() { @@ -835,21 +915,24 @@ abstract class RulesPlugin extends RulesExtendable { elseif (!$this->isRoot() && !isset($this->settings[$name]) && empty($info['optional']) && $info['type'] != 'hidden') { throw new RulesIntegrityException(t('Missing configuration for parameter %name.', array('%name' => $name)), array($this, 'parameter', $name)); } - //TODO: Make sure used values are allowed. (key/value pairs + allowed values) + // @todo Make sure used values are allowed. + // (key/value pairs + allowed values). } } /** + * Returns the argument for the parameter $name described with $info. + * * Returns the argument as configured in the element settings for the * parameter $name described with $info. * - * @param $name + * @param string $name * The name of the parameter for which to get the argument. * @param $info * Info about the parameter. * @param RulesState $state * The current evaluation state. - * @param $langcode + * @param string $langcode * (optional) The language code used to get the argument value if the * argument value should be translated. By default (NULL) the current * interface language will be used. @@ -902,7 +985,7 @@ abstract class RulesPlugin extends RulesExtendable { if (!empty($this->settings[$name . ':process'])) { // For processing, make sure the data is unwrapped now. $return = rules_unwrap_data(array($arg), array($info)); - // @todo for Drupal 8: Refactor to add the name and language code as + // @todo For Drupal 8: Refactor to add the name and language code as // separate parameter to process(). $info['#name'] = $name; $info['#langcode'] = $langcode; @@ -939,10 +1022,12 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Apply the given data selector by using the info about available variables. - * Thus it doesn't require an actual evaluation state. + * Applies the given data selector. * - * @param $selector + * Applies the given data selector by using the info about available + * variables. Thus it doesn't require an actual evaluation state. + * + * @param string $selector * The selector string, e.g. "node:author:mail". * * @return EntityMetadataWrapper @@ -964,7 +1049,7 @@ abstract class RulesPlugin extends RulesExtendable { } } } - // In case of an exception or we were unable to get a wrapper, return FALSE. + // Return FALSE if there is no wrappper or we get an exception. catch (EntityMetadataWrapperException $e) { return FALSE; } @@ -999,8 +1084,10 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Saves the configuration to the database, regardless whether this is invoked - * on the rules configuration or a contained rule element. + * Saves the configuration to the database. + * + * The configuration is saved regardless whether this method is invoked on + * the rules configuration or a contained rule element. */ public function save($name = NULL, $module = 'rules') { if (isset($this->parent)) { @@ -1025,7 +1112,17 @@ abstract class RulesPlugin extends RulesExtendable { $this->plugin = $this->itemName; $this->name = isset($name) ? $name : $this->name; + // Module stores the module via which the rule is configured and is used + // for generating machine names with the right prefix. However, for + // default configurations 'module' points to the module providing the + // default configuration, so the module via which the rules is configured + // is stored in the "owner" property. + // @todo For Drupal 8 use "owner" for generating machine names also and + // module only for the modules providing default configurations. $this->module = !isset($this->module) || $module != 'rules' ? $module : $this->module; + if (!isset($this->owner)) { + $this->owner = 'rules'; + } $this->ensureNameExists(); $this->data = $this; $return = entity_get_controller('rules_config')->save($this); @@ -1049,7 +1146,8 @@ abstract class RulesPlugin extends RulesExtendable { elseif ($this->plugin == 'reaction rule') { // Clear event sets cached for evaluation. cache_clear_all('event_', 'cache_rules', TRUE); - variable_del('rules_empty_sets'); + // Clear event whitelist for rebuild. + cache_clear_all('rules_event_whitelist', 'cache_rules', TRUE); } drupal_static_reset('rules_get_cache'); drupal_static_reset('rules_config_update_dirty_flag'); @@ -1081,7 +1179,9 @@ abstract class RulesPlugin extends RulesExtendable { // Keep the id always as we need it for the recursion prevention. $array = drupal_map_assoc(array('parent', 'id', 'elementId', 'weight', 'settings')); // Keep properties related to configurations if they are there. - foreach (array('name', 'module', 'status', 'label', 'recursion', 'tags') as $key) { + $info = entity_get_info('rules_config'); + $fields = array_merge($info['schema_fields_sql']['base table'], array('recursion', 'tags')); + foreach ($fields as $key) { if (isset($this->$key)) { $array[$key] = $key; } @@ -1112,6 +1212,8 @@ abstract class RulesPlugin extends RulesExtendable { } /** + * Deletes configuration from database. + * * If invoked on a rules configuration it is deleted from database. If * invoked on a contained rule element, it's removed from the configuration. */ @@ -1156,7 +1258,7 @@ abstract class RulesPlugin extends RulesExtendable { * A status constant, i.e. one of ENTITY_CUSTOM, ENTITY_IN_CODE, * ENTITY_OVERRIDDEN or ENTITY_FIXED. * - * @return + * @return bool * TRUE if the configuration has the status, else FALSE. * * @see entity_has_status() @@ -1166,8 +1268,7 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Remove circular object references so the PHP garbage collector does its - * work. + * Removes circular object references so PHP garbage collector can work. */ public function destroy() { parent::destroy(); @@ -1175,8 +1276,9 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Seamlessy invokes the method implemented via faces without having to think - * about references. + * Seamlessly invokes the method implemented via faces. + * + * Frees the caller from having to think about references. */ public function form(&$form, &$form_state, array $options = array()) { $this->__call('form', array(&$form, &$form_state, $options)); @@ -1260,13 +1362,14 @@ abstract class RulesPlugin extends RulesExtendable { /** * Exports a rule configuration. * - * @param $prefix + * @param string $prefix * An optional prefix for each line. - * @param $php + * @param bool $php * Optional. Set to TRUE to format the export using PHP arrays. By default * JSON is used. + * * @return - * The exported confiugration. + * The exported configuration. * * @see rules_import() */ @@ -1316,8 +1419,17 @@ abstract class RulesPlugin extends RulesExtendable { } /** - * Finalizies the configuration export by adding general attributes regarding - * the configuration and returns it in the right format. + * Finalizes the configuration export. + * + * Adds general attributes regarding the configuration and returns it in the + * right format for export. + * + * @param $export + * @param string $prefix + * An optional prefix for each line. + * @param bool $php + * Optional. Set to TRUE to format the export using PHP arrays. By default + * JSON is used. */ protected function returnExport($export, $prefix = '', $php = FALSE) { $this->ensureNameExists(); @@ -1331,6 +1443,9 @@ abstract class RulesPlugin extends RulesExtendable { if (isset($this->active) && !$this->active) { $export_cfg[$this->name]['ACTIVE'] = FALSE; } + if (!empty($this->owner)) { + $export_cfg[$this->name]['OWNER'] = $this->owner; + } if (!empty($this->tags)) { $export_cfg[$this->name]['TAGS'] = $this->tags; } @@ -1360,11 +1475,13 @@ abstract class RulesPlugin extends RulesExtendable { public function resetInternalCache() { $this->availableVariables = NULL; } + } /** - * Defines a common base class for so called "Abstract Plugins" like actions. - * Thus modules have to provide the concrete plugin implementation. + * Defines a common base class for so-called "Abstract Plugins" like actions. + * + * Modules have to provide the concrete plugin implementation. */ abstract class RulesAbstractPlugin extends RulesPlugin { @@ -1373,14 +1490,14 @@ abstract class RulesAbstractPlugin extends RulesPlugin { protected $infoLoaded = FALSE; /** - * @param $name + * @param string $name * The plugin implementation's name. - * @param $info + * @param $settings * Further information provided about the plugin. Optional. * @throws RulesException * If validation of the passed settings fails RulesExceptions are thrown. */ - function __construct($name = NULL, $settings = array()) { + public function __construct($name = NULL, $settings = array()) { $this->elementName = $name; $this->settings = (array) $settings + array('#_needs_processing' => TRUE); $this->setUp(); @@ -1391,9 +1508,18 @@ abstract class RulesAbstractPlugin extends RulesPlugin { if (isset($this->cache[$this->itemName . '_info'][$this->elementName])) { $this->info = $this->cache[$this->itemName . '_info'][$this->elementName]; // Remember that the info has been correctly setup. - // @see self::forceSetup(). + // @see self::forceSetup() $this->infoLoaded = TRUE; + // Register the defined class, if any. + if (isset($this->info['class'])) { + $this->faces['RulesPluginImplInterface'] = 'RulesPluginImplInterface'; + $face_methods = get_class_methods('RulesPluginImplInterface'); + $class_info = array(1 => $this->info['class']); + foreach ($face_methods as $method) { + $this->facesMethods[$method] = $class_info; + } + } // Add in per-plugin implementation callbacks if any. if (!empty($this->info['faces_cache'])) { foreach ($this->info['faces_cache'] as $face => $data) { @@ -1533,7 +1659,7 @@ abstract class RulesAbstractPlugin extends RulesPlugin { public function dependencies() { $modules = array_flip(parent::dependencies()); $modules += array_flip((array) $this->__call('dependencies')); - return array_keys($modules + (isset($this->info['module']) ? array($this->info['module'] => 1) : array())); + return array_keys($modules + (!empty($this->info['module']) ? array($this->info['module'] => 1) : array())); } public function executeByArgs($args = array()) { @@ -1562,7 +1688,6 @@ abstract class RulesAbstractPlugin extends RulesPlugin { */ abstract protected function executeCallback(array $args, RulesState $state = NULL); - public function evaluate(RulesState $state) { $this->processSettings(); try { @@ -1586,7 +1711,7 @@ abstract class RulesAbstractPlugin extends RulesPlugin { } public function getPluginName() { - return $this->itemName ." ". $this->elementName; + return $this->itemName . " " . $this->elementName; } /** @@ -1606,8 +1731,8 @@ abstract class RulesAbstractPlugin extends RulesPlugin { self::includeFiles(); // Get the plugin's own info data. - $cache[$this->itemName .'_info'] = rules_fetch_data($this->itemName .'_info'); - foreach ($cache[$this->itemName .'_info'] as $name => &$info) { + $cache[$this->itemName . '_info'] = rules_fetch_data($this->itemName . '_info'); + foreach ($cache[$this->itemName . '_info'] as $name => &$info) { $info += array( 'parameter' => isset($info['arguments']) ? $info['arguments'] : array(), 'provides' => isset($info['new variables']) ? $info['new variables'] : array(), @@ -1615,9 +1740,17 @@ abstract class RulesAbstractPlugin extends RulesPlugin { 'callbacks' => array(), ); unset($info['arguments'], $info['new variables']); - $info['callbacks'] += array('execute' => $info['base']); - // Build up the per plugin implementation faces cache. + if (function_exists($info['base'])) { + $info['callbacks'] += array('execute' => $info['base']); + } + + // We do not need to build a faces cache for RulesPluginHandlerInterface, + // which gets added in automatically as its a parent of + // RulesPluginImplInterface. + unset($this->faces['RulesPluginHandlerInterface']); + + // Build up the per-plugin implementation faces cache. foreach ($this->faces as $interface) { $methods = $file_names = array(); $includes = self::getIncludeFiles($info['module']); @@ -1627,6 +1760,11 @@ abstract class RulesAbstractPlugin extends RulesPlugin { $methods[$method][0] = $function; $file_names[$method] = $this->getFileName($function, $includes); } + // Note that this skips RulesPluginImplInterface, which is not + // implemented by plugin handlers. + elseif (isset($info['class']) && is_subclass_of($info['class'], $interface)) { + $methods[$method][1] = $info['class']; + } elseif (function_exists($function = $info['base'] . '_' . $method)) { $methods[$method][0] = $function; $file_names[$method] = $this->getFileName($function, $includes); @@ -1635,7 +1773,7 @@ abstract class RulesAbstractPlugin extends RulesPlugin { // Cache only the plugin implementation specific callbacks. $info['faces_cache'][$interface] = array($methods, array_filter($file_names)); } - // Filter out interfaces with no overriden methods. + // Filter out interfaces with no overridden methods. $info['faces_cache'] = rules_filter_array($info['faces_cache'], 0, TRUE); // We don't need that any more. unset($info['callbacks'], $info['base']); @@ -1643,7 +1781,9 @@ abstract class RulesAbstractPlugin extends RulesPlugin { } /** - * Makes sure the providing modules' .rules.inc file is included as diverse + * Loads this module's .rules.inc file. + * + * Makes sure the providing modules' .rules.inc file is included, as diverse * callbacks may reside in that file. */ protected function loadBasicInclude() { @@ -1657,9 +1797,9 @@ abstract class RulesAbstractPlugin extends RulesPlugin { } /** - * Make sure all supported destinations are included. + * Makes sure all supported destinations are included. */ - protected static function includeFiles() { + public static function includeFiles() { static $included; if (!isset($included)) { @@ -1669,16 +1809,40 @@ abstract class RulesAbstractPlugin extends RulesPlugin { module_load_include('inc', $module, $name); } } + $dirs = array(); + foreach (module_implements('rules_directory') as $module) { + // Include all files once, so the discovery can find them. + $result = module_invoke($module, 'rules_directory'); + if (!is_array($result)) { + $result = array($module => $result); + } + $dirs += $result; + } + foreach ($dirs as $module => $directory) { + $module_path = drupal_get_path('module', $module); + foreach (array('inc', 'php') as $extension) { + foreach (glob("$module_path/$directory/*.$extension") as $filename) { + include_once $filename; + } + } + } $included = TRUE; } } /** - * Returns all include files for a module. If $all is set to FALSE the - * $module.rules.inc file isn't added. + * Returns all include files for a module. + * + * @param string $module + * The module name. + * @param bool $all + * If FALSE, the $module.rules.inc file isn't added. + * + * @return string[] + * An array containing the names of all the include files for a module. */ protected static function getIncludeFiles($module, $all = TRUE) { - $files = (array)module_invoke($module, 'rules_file_info'); + $files = (array) module_invoke($module, 'rules_file_info'); // Automatically add "$module.rules_forms.inc" and "$module.rules.inc". $files[] = $module . '.rules_forms'; if ($all) { @@ -1688,40 +1852,49 @@ abstract class RulesAbstractPlugin extends RulesPlugin { } protected function getFileName($function, $includes) { - $reflector = new ReflectionFunction($function); - // On windows the path contains backslashes instead of slashes, fix that. - $file = str_replace('\\', '/', $reflector->getFileName()); - foreach ($includes as $include) { - $pos = strpos($file, $include . '.inc'); - // Test whether the file ends with the given filename.inc. - if ($pos !== FALSE && strlen($file) - $pos == strlen($include) + 4) { - return $include; + static $filenames; + if (!isset($filenames) || !array_key_exists($function, $filenames)) { + $filenames[$function] = NULL; + $reflector = new ReflectionFunction($function); + // On windows the path contains backslashes instead of slashes, fix that. + $file = str_replace('\\', '/', $reflector->getFileName()); + foreach ($includes as $include) { + $pos = strpos($file, $include . '.inc'); + // Test whether the file ends with the given filename.inc. + if ($pos !== FALSE && strlen($file) - $pos == strlen($include) + 4) { + $filenames[$function] = $include; + return $include; + } } } + return $filenames[$function]; } + } /** - * Interface for objects that can be used as action. + * Interface for objects that can be used as actions. */ interface RulesActionInterface { /** - * @return As specified. + * @return + * As specified. * * @throws RulesEvaluationException * Throws an exception if not all necessary arguments have been provided. */ public function execute(); + } /** - * Interface for objects that can be used as condition. + * Interface for objects that can be used as conditions. */ interface RulesConditionInterface { /** - * @return Boolean. + * @return bool * * @throws RulesEvaluationException * Throws an exception if not all necessary arguments have been provided. @@ -1737,34 +1910,60 @@ interface RulesConditionInterface { * Returns whether the element is configured to negate the result. */ public function isNegated(); + } +/** + * Interface for objects that are triggerable. + */ interface RulesTriggerableInterface { /** - * Returns a reference on the array of event names associated with this - * object. + * Returns the array of (configured) event names associated with this object. */ - public function &events(); + public function events(); + + /** + * Removes an event from the rule configuration. + * + * @param string $event_name + * The name of the (configured) event to remove. + * + * @return RulesTriggerableInterface + * The object instance itself, to allow chaining. + */ + public function removeEvent($event_name); /** * Adds the specified event. * + * @param string $event_name + * The base name of the event to add. + * @param array $settings + * (optional) The event settings. If there are no event settings, pass an + * empty array (default). + * * @return RulesTriggerableInterface */ - public function event($event); + public function event($event_name, array $settings = array()); + + /** + * Gets the event settings associated with the given (configured) event. + * + * @param string $event_name + * The (configured) event's name. + * + * @return array|null + * The array of event settings, or NULL if there are no settings. + */ + public function getEventSettings($event_name); + } /** - * Provides the interface used for implementing an abstract plugin by using - * the Faces extension mechanism. + * Provides the base interface for implementing abstract plugins via classes. */ -interface RulesPluginImplInterface { - - /** - * Execute the action or condition making use of the parameters as specified. - */ - public function execute(); +interface RulesPluginHandlerInterface { /** * Validates $settings independent from a form submission. @@ -1777,7 +1976,8 @@ interface RulesPluginImplInterface { /** * Processes settings independent from a form submission. * - * Processing results may be stored and accessed on execution time in $settings. + * Processing results may be stored and accessed on execution time + * in $settings. */ public function process(); @@ -1809,7 +2009,7 @@ interface RulesPluginImplInterface { public function dependencies(); /** - * Alter the generated configuration form of the element. + * Alters the generated configuration form of the element. * * Validation and processing of the settings should be untied from the form * and implemented in validate() and process() wherever it makes sense. @@ -1818,11 +2018,11 @@ interface RulesPluginImplInterface { */ public function form_alter(&$form, $form_state, $options); - /** - * Optionally returns an array of info assertions for the specified - * parameters. This allows conditions to assert additional metadata, such as - * info about the fields of a bundle. + * Returns an array of info assertions for the specified parameters. + * + * This allows conditions to assert additional metadata, such as info about + * the fields of a bundle. * * @see RulesPlugin::variableInfoAssertions() */ @@ -1830,31 +2030,81 @@ interface RulesPluginImplInterface { } +/** + * Interface for implementing conditions via classes. + * + * In addition to the interface an execute() and a static getInfo() method must + * be implemented. The static getInfo() method has to return the info as + * returned by hook_rules_condition_info() but including an additional 'name' + * key, specifying the plugin name. + * The execute method is the equivalent to the usual execution callback and + * gets the parameters passed as specified in the info array. + * + * See RulesNodeConditionType for an example and rules_discover_plugins() + * for information about class discovery. + */ +interface RulesConditionHandlerInterface extends RulesPluginHandlerInterface {} + +/** + * Interface for implementing actions via classes. + * + * In addition to the interface an execute() and a static getInfo() method must + * be implemented. The static getInfo() method has to return the info as + * returned by hook_rules_action_info() but including an additional 'name' key, + * specifying the plugin name. + * The execute method is the equivalent to the usual execution callback and + * gets the parameters passed as specified in the info array. + * + * See RulesNodeConditionType for an example and rules_discover_plugins() + * for information about class discovery. + */ +interface RulesActionHandlerInterface extends RulesPluginHandlerInterface {} + +/** + * Interface used for implementing an abstract plugin via Faces. + * + * Provides the interface used for implementing an abstract plugin by using + * the Faces extension mechanism. + */ +interface RulesPluginImplInterface extends RulesPluginHandlerInterface { + + /** + * Executes the action or condition making use of the parameters as specified. + */ + public function execute(); + +} + /** * Interface for optimizing evaluation. * * @see RulesContainerPlugin::optimize() */ interface RulesOptimizationInterface { + /** * Optimizes a rule configuration in order to speed up evaluation. */ public function optimize(); + } /** - * Class providing default implementations of the methods of the RulesPluginImplInterface. - * - * If a plugin implementation does not provide a function for a method, the - * default method of this class will be invoked. - * - * @see RulesPluginImplInterface - * @see RulesAbstractPlugin + * Base class for implementing abstract plugins via classes. */ -class RulesAbstractPluginDefaults extends FacesExtender implements RulesPluginImplInterface { +abstract class RulesPluginHandlerBase extends FacesExtender implements RulesPluginHandlerInterface { - public function execute() { - throw new RulesEvaluationException($this->object->getPluginName() .": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR); + /** + * @var RulesAbstractPlugin + */ + protected $element; + + /** + * Overridden to provide $this->element to make the code more meaningful. + */ + public function __construct(FacesExtendable $object) { + $this->object = $object; + $this->element = $object; } /** @@ -1865,11 +2115,44 @@ class RulesAbstractPluginDefaults extends FacesExtender implements RulesPluginIm } public function validate() {} + public function process() {} + public function info_alter(&$element_info) {} + public function dependencies() {} + public function form_alter(&$form, $form_state, $options) {} + public function assertions() {} + +} + +/** + * Base class for implementing conditions via classes. + */ +abstract class RulesConditionHandlerBase extends RulesPluginHandlerBase implements RulesConditionHandlerInterface {} + +/** + * Base class for implementing actions via classes. + */ +abstract class RulesActionHandlerBase extends RulesPluginHandlerBase implements RulesActionHandlerInterface {} + +/** + * Provides default implementations of all RulesPluginImplInterface methods. + * + * If a plugin implementation does not provide a function for a method, the + * default method of this class will be invoked. + * + * @see RulesPluginImplInterface + * @see RulesAbstractPlugin + */ +class RulesAbstractPluginDefaults extends RulesPluginHandlerBase implements RulesPluginImplInterface { + + public function execute() { + throw new RulesEvaluationException($this->object->getPluginName() . ": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR); + } + } /** @@ -1877,13 +2160,14 @@ class RulesAbstractPluginDefaults extends FacesExtender implements RulesPluginIm */ class RulesRecursiveElementIterator extends ArrayIterator implements RecursiveIterator { - public function getChildren() { - return $this->current()->getIterator(); - } + public function getChildren() { + return $this->current()->getIterator(); + } + + public function hasChildren() { + return $this->current() instanceof IteratorAggregate; + } - public function hasChildren() { - return $this->current() instanceof IteratorAggregate; - } } /** @@ -1914,7 +2198,7 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre } /** - * Allow access to the children through the iterator. + * Allows access to the children through the iterator. * * @return RulesRecursiveElementIterator */ @@ -1937,7 +2221,7 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre } public function dependencies() { - $modules = array(); + $modules = array_flip(parent::dependencies()); foreach ($this->children as $child) { $modules += array_flip($child->dependencies()); } @@ -1976,6 +2260,8 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre } /** + * Returns available state variables for an element. + * * Returns info about variables available in the evaluation state for any * children elements or if given for a special child element. * @@ -2018,6 +2304,8 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre } /** + * Executes container with the given arguments. + * * Condition containers just return a boolean while action containers return * the configured provided variables as an array of variables. */ @@ -2072,7 +2360,7 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre } /** - * Override delete to keep the children alive, if possible. + * Overrides delete to keep the children alive, if possible. */ public function delete($keep_children = TRUE) { if (isset($this->parent) && $keep_children) { @@ -2090,13 +2378,13 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre /** * Sorts all child elements by their weight. * - * @param $deep + * @param bool $deep * If enabled a deep sort is performed, thus the whole element tree below * this element is sorted. */ public function sortChildren($deep = FALSE) { // Make sure the array order is kept in case two children have the same - // weight by ensuring later childrens would have higher weights. + // weight by ensuring later children would have higher weights. foreach (array_values($this->children) as $i => $child) { $child->weight += $i / 1000; } @@ -2104,7 +2392,7 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre // Fix up the weights afterwards to be unique integers. foreach (array_values($this->children) as $i => $child) { - $child->weight = $i * 2; + $child->weight = $i; } if ($deep) { @@ -2125,9 +2413,10 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre } /** - * Determines whether the element should be exported in flat style. Flat style - * means that the export keys are written directly into the export array, - * whereas else the export is written into a sub-array. + * Determines whether the element should be exported in flat style. + * + * Flat style means that the export keys are written directly into the export + * array, whereas else the export is written into a sub-array. */ protected function exportFlat() { // By default we always use flat style for plugins without any parameters @@ -2178,6 +2467,18 @@ abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggre $child->resetInternalCache(); } } + + /** + * Overrides optimize(). + */ + public function optimize() { + parent::optimize(); + // Now let the children optimize itself. + foreach ($this as $element) { + $element->optimize(); + } + } + } /** @@ -2195,11 +2496,12 @@ abstract class RulesActionContainer extends RulesContainerPlugin implements Rule } /** - * Add an action. Pass either an instance of the RulesActionInterface - * or the arguments as needed by rules_action(). + * Adds an action to the container. * - * @return RulesActionContainer - * Returns $this to support chained usage. + * Pass in either an instance of the RulesActionInterface or the arguments + * as needed by rules_action(). + * + * @return $this */ public function action($name, $settings = array()) { $action = (is_object($name) && $name instanceof RulesActionInterface) ? $name : rules_action($name, $settings); @@ -2231,6 +2533,8 @@ abstract class RulesActionContainer extends RulesContainerPlugin implements Rule } /** + * Returns an array of provided variable names. + * * Returns an array of variable names, which are provided by passing through * the provided variables of the children. */ @@ -2253,6 +2557,7 @@ abstract class RulesActionContainer extends RulesContainerPlugin implements Rule $this->info['provides'] = $export['PROVIDES VARIABLES']; } } + } /** @@ -2263,11 +2568,12 @@ abstract class RulesConditionContainer extends RulesContainerPlugin implements R protected $negate = FALSE; /** - * Add a condition. Pass either an instance of the RulesConditionInterface - * or the arguments as needed by rules_condition(). + * Adds a condition to the container. * - * @return RulesConditionContainer - * Returns $this to support chained usage. + * Pass in either an instance of the RulesConditionInterface or the arguments + * as needed by rules_condition(). + * + * @return $this */ public function condition($name, $settings = array()) { $condition = (is_object($name) && $name instanceof RulesConditionInterface) ? $name : rules_condition($name, $settings); @@ -2335,6 +2641,7 @@ abstract class RulesConditionContainer extends RulesContainerPlugin implements R } return $vars; } + } /** @@ -2352,7 +2659,7 @@ class RulesLog { * @return RulesLog * Returns the rules logger instance. */ - static function logger() { + public static function logger() { if (!isset(self::$logger)) { $class = __CLASS__; self::$logger = new $class(variable_get('rules_log_level', self::INFO)); @@ -2361,7 +2668,8 @@ class RulesLog { } protected $log = array(); - protected $logLevel, $line = 0; + protected $logLevel; + protected $line = 0; /** * This is a singleton. @@ -2388,7 +2696,7 @@ class RulesLog { /** * Checks the log and throws an exception if there were any problems. */ - function checkLog($logLevel = self::WARN) { + public function checkLog($logLevel = self::WARN) { foreach ($this->log as $entry) { if ($entry[2] >= $logLevel) { throw new Exception($this->render()); @@ -2397,9 +2705,13 @@ class RulesLog { } /** - * Checks the log for (error) messages with a log level equal or higher than the given one. + * Checks the log for error messages. * - * @return + * @param int $logLevel + * Lowest log level to return. Values lower than $logLevel will not be + * returned. + * + * @return bool * Whether the an error has been logged. */ public function hasErrors($logLevel = self::WARN) { @@ -2445,20 +2757,20 @@ class RulesLog { // messages here. $vars['head'] = t($this->log[$line][0], $this->log[$line][1]); if (isset($this->log[$line][5])) { - $vars['link'] = '[' . l('edit', $this->log[$line][5]) . ']'; + $vars['link'] = '[' . l(t('edit'), $this->log[$line][5]) . ']'; } $vars['log'] = $this->renderHelper($line); $output[] = theme('rules_debug_element', $vars); } else { - $formatted_diff = round(($this->log[$line][3] - $startTime) * 1000, 3) .' ms'; - $msg = $formatted_diff .' '. t($this->log[$line][0], $this->log[$line][1]); + $formatted_diff = round(($this->log[$line][3] - $startTime) * 1000, 3) . ' ms'; + $msg = $formatted_diff . ' ' . t($this->log[$line][0], $this->log[$line][1]); if ($this->log[$line][2] >= RulesLog::WARN) { $level = $this->log[$line][2] == RulesLog::WARN ? 'warn' : 'error'; - $msg = ''. $msg .''; + $msg = '' . $msg . ''; } if (isset($this->log[$line][5]) && !isset($this->log[$line][4])) { - $msg .= ' [' . l('edit', $this->log[$line][5]) . ']'; + $msg .= ' [' . l(t('edit'), $this->log[$line][5]) . ']'; } $output[] = $msg; @@ -2478,12 +2790,14 @@ class RulesLog { public function clear() { $this->log = array(); } + } /** - * A common exception for Rules. + * A base exception class for Rules. * - * This class can be used to catch all exceptions thrown by Rules. + * This class can be used to catch all exceptions thrown by Rules, and it + * may be subclassed to describe more specific exceptions. */ abstract class RulesException extends Exception {} @@ -2496,21 +2810,27 @@ abstract class RulesException extends Exception {} */ class RulesEvaluationException extends RulesException { - public $msg, $args, $severity, $element, $keys = array(); + public $msg; + public $args; + public $severity; + public $element; + public $keys = array(); /** - * @param $msg + * Constructor. + * + * @param string $msg * The exception message containing placeholder as t(). - * @param $args + * @param array $args * Replacement arguments such as for t(). * @param $element * The element of a configuration causing the exception or an array * consisting of the element and keys specifying a setting value causing * the exception. - * @param $severity + * @param int $severity * The RulesLog severity. Defaults to RulesLog::WARN. */ - function __construct($msg, array $args = array(), $element = NULL, $severity = RulesLog::WARN) { + public function __construct($msg, array $args = array(), $element = NULL, $severity = RulesLog::WARN) { $this->element = is_array($element) ? array_shift($element) : $element; $this->keys = is_array($element) ? $element : array(); $this->msg = $msg; @@ -2526,34 +2846,40 @@ class RulesEvaluationException extends RulesException { rules_clear_cache(); } } - // @todo fix _drupal_decode_exception() to use __toString() and override it. + // @todo Fix _drupal_decode_exception() to use __toString() and override it. $this->message = t($this->msg, $this->args); } + } /** - * An exception that is thrown for Rules configurations that fail the integrity check. + * Indicates the Rules configuration failed the integrity check. * * @see RulesPlugin::integrityCheck() */ class RulesIntegrityException extends RulesException { - public $msg, $element, $keys = array(); + public $msg; + public $element; + public $keys = array(); /** + * Constructs a RulesIntegrityException object. + * * @param string $msg * The exception message, already translated. * @param $element * The element of a configuration causing the exception or an array * consisting of the element and keys specifying a parameter or provided * variable causing the exception, e.g. - * @code array($element, 'parameter', 'node') @endcode. + * @code array($element, 'parameter', 'node') @endcode */ - function __construct($msg, $element = NULL) { + public function __construct($msg, $element = NULL) { $this->element = is_array($element) ? array_shift($element) : $element; $this->keys = is_array($element) ? $element : array(); parent::__construct($msg); } + } /** @@ -2564,9 +2890,9 @@ class RulesDependencyException extends RulesIntegrityException {} /** * Determines the plugin to be used for importing a child element. * - * @param $key + * @param string $key * The key to look for, e.g. 'OR' or 'DO'. - * @param $default + * @param string $default * The default to return if no special plugin can be found. */ function _rules_import_get_plugin($key, $default = 'action') { @@ -2582,7 +2908,7 @@ function _rules_import_get_plugin($key, $default = 'action') { } } } - // Cut of any leading NOT from the key. + // Cut off any leading NOT from the key. if (strpos($key, 'NOT ') === 0) { $key = substr($key, 4); } diff --git a/sites/all/modules/contrib/admin/rules/includes/rules.event.inc b/sites/all/modules/contrib/admin/rules/includes/rules.event.inc index 27f6a336..f97dec1e 100644 --- a/sites/all/modules/contrib/admin/rules/includes/rules.event.inc +++ b/sites/all/modules/contrib/admin/rules/includes/rules.event.inc @@ -70,6 +70,7 @@ interface RulesEventHandlerInterface { * Returns an array of default settings. * * @return array + * The array of default settings. */ public function getDefaults(); @@ -145,6 +146,7 @@ interface RulesEventHandlerInterface { * The info array of the event the event handler belongs to. */ public function getEventInfo(); + } /** @@ -169,6 +171,7 @@ interface RulesEventDispatcherInterface extends RulesEventHandlerInterface { * TRUE if the event dispatcher is currently active, FALSE otherwise. */ public function isWatching(); + } /** @@ -198,7 +201,7 @@ abstract class RulesEventHandlerBase implements RulesEventHandlerInterface { protected $settings = array(); /** - * Implements RulesEventHandlerInterface::__construct() + * Implements RulesEventHandlerInterface::__construct(). */ public function __construct($event_name, $info) { $this->eventName = $event_name; @@ -207,14 +210,14 @@ abstract class RulesEventHandlerBase implements RulesEventHandlerInterface { } /** - * Implements RulesEventHandlerInterface::getSettings() + * Implements RulesEventHandlerInterface::getSettings(). */ public function getSettings() { return $this->settings; } /** - * Implements RulesEventHandlerInterface::setSettings() + * Implements RulesEventHandlerInterface::setSettings(). */ public function setSettings(array $settings) { $this->settings = $settings + $this->getDefaults(); @@ -222,14 +225,14 @@ abstract class RulesEventHandlerBase implements RulesEventHandlerInterface { } /** - * Implements RulesEventHandlerInterface::validate() + * Implements RulesEventHandlerInterface::validate(). */ public function validate() { // Nothing to check by default. } /** - * Implements RulesEventHandlerInterface::extractFormValues() + * Implements RulesEventHandlerInterface::extractFormValues(). */ public function extractFormValues(array &$form, array &$form_state) { foreach ($this->getDefaults() as $key => $setting) { @@ -238,66 +241,68 @@ abstract class RulesEventHandlerBase implements RulesEventHandlerInterface { } /** - * Implements RulesEventHandlerInterface::availableVariables() + * Implements RulesEventHandlerInterface::availableVariables(). */ public function availableVariables() { return isset($this->eventInfo['variables']) ? $this->eventInfo['variables'] : array(); } /** - * Implements RulesEventHandlerInterface::getEventName() + * Implements RulesEventHandlerInterface::getEventName(). */ public function getEventName() { return $this->eventName; } /** - * Implements RulesEventHandlerInterface::getEventInfo() + * Implements RulesEventHandlerInterface::getEventInfo(). */ public function getEventInfo() { return $this->eventInfo; } + } /** * A handler for events having no settings. This is the default handler. */ -class RulesEventDefaultHandler extends RulesEventHandlerBase { +class RulesEventDefaultHandler extends RulesEventHandlerBase { /** - * Implements RulesEventHandlerInterface::buildForm() + * Implements RulesEventHandlerInterface::buildForm(). */ public function buildForm(array &$form_state) { return array(); } /** - * Implements RulesEventHandlerInterface::getConfiguredEventName() + * Implements RulesEventHandlerInterface::getConfiguredEventName(). */ public function getEventNameSuffix() { return ''; } /** - * Implements RulesEventHandlerInterface::summary() + * Implements RulesEventHandlerInterface::summary(). */ public function summary() { return check_plain($this->eventInfo['label']); } /** - * Implements RulesEventHandlerInterface::getDefaults() + * Implements RulesEventHandlerInterface::getDefaults(). */ public function getDefaults() { return array(); } /** - * Implements RulesEventHandlerInterface::getSettings() + * Implements RulesEventHandlerInterface::getSettings(). */ public function getSettings() { return NULL; } + } /** @@ -305,10 +310,12 @@ class RulesEventDefaultHandler extends RulesEventHandlerBase { */ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { - protected $entityType, $entityInfo, $bundleKey; + protected $entityType; + protected $entityInfo; + protected $bundleKey; /** - * Implements RulesEventHandlerInterface::__construct() + * Implements RulesEventHandlerInterface::__construct(). */ public function __construct($event_name, $info) { parent::__construct($event_name, $info); @@ -321,7 +328,7 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { } /** - * Implements RulesEventHandlerInterface::summary() + * Implements RulesEventHandlerInterface::summary(). */ public function summary() { $bundle = &$this->settings['bundle']; @@ -331,7 +338,7 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { } /** - * Implements RulesEventHandlerInterface::buildForm() + * Implements RulesEventHandlerInterface::buildForm(). */ public function buildForm(array &$form_state) { $form['bundle'] = array( @@ -340,6 +347,7 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { '#description' => t('If you need to filter for multiple values, either add multiple events or use the "Entity is of bundle" condition instead.'), '#default_value' => $this->settings['bundle'], '#empty_value' => '', + '#options' => array(), ); foreach ($this->entityInfo['bundles'] as $name => $bundle_info) { $form['bundle']['#options'][$name] = $bundle_info['label']; @@ -351,20 +359,21 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { * Returns the label to use for the bundle property. * * @return string + * The label to use for the bundle property. */ protected function getBundlePropertyLabel() { return $this->entityInfo['entity keys']['bundle']; } /** - * Implements RulesEventHandlerInterface::extractFormValues() + * Implements RulesEventHandlerInterface::extractFormValues(). */ public function extractFormValues(array &$form, array &$form_state) { $this->settings['bundle'] = !empty($form_state['values']['bundle']) ? $form_state['values']['bundle'] : NULL; } /** - * Implements RulesEventHandlerInterface::validate() + * Implements RulesEventHandlerInterface::validate(). */ public function validate() { if ($this->settings['bundle'] && empty($this->entityInfo['bundles'][$this->settings['bundle']])) { @@ -373,19 +382,19 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { '%bundle' => $this->settings['bundle'], '%entity_type' => $this->entityInfo['label'], '@bundle' => $this->getBundlePropertyLabel(), - )), array(NULL, 'bundle')); + )), array(NULL, 'bundle')); } } /** - * Implements RulesEventHandlerInterface::getConfiguredEventName() + * Implements RulesEventHandlerInterface::getConfiguredEventName(). */ public function getEventNameSuffix() { return $this->settings['bundle']; } /** - * Implements RulesEventHandlerInterface::getDefaults() + * Implements RulesEventHandlerInterface::getDefaults(). */ public function getDefaults() { return array( @@ -394,7 +403,7 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { } /** - * Implements RulesEventHandlerInterface::availableVariables() + * Implements RulesEventHandlerInterface::availableVariables(). */ public function availableVariables() { $variables = $this->eventInfo['variables']; @@ -408,4 +417,5 @@ class RulesEventHandlerEntityBundle extends RulesEventHandlerBase { } return $variables; } + } diff --git a/sites/all/modules/contrib/admin/rules/includes/rules.plugins.inc b/sites/all/modules/contrib/admin/rules/includes/rules.plugins.inc index 2a4fa517..62fda469 100644 --- a/sites/all/modules/contrib/admin/rules/includes/rules.plugins.inc +++ b/sites/all/modules/contrib/admin/rules/includes/rules.plugins.inc @@ -1,15 +1,18 @@ negate ? t('NOT @condition', array('@condition' => $label)) : $label; + return $this->negate ? t('NOT !condition', array('!condition' => $label)) : $label; } + } /** * An actual rule. + * * Note: A rule also implements the RulesActionInterface (inherited). */ class Rule extends RulesActionContainer { protected $conditions = NULL; + + /** + * @var string + */ protected $itemName = 'rule'; + /** + * @var string + */ public $label = 'unlabeled'; public function __construct($variables = array(), $providesVars = array()) { @@ -159,8 +179,9 @@ class Rule extends RulesActionContainer { } /** - * Get an iterator over all contained conditions. Note that this iterator also - * implements the ArrayAcces interface. + * Gets an iterator over all contained conditions. + * + * Note that this iterator also implements the ArrayAccess interface. * * @return RulesRecursiveElementIterator */ @@ -183,8 +204,9 @@ class Rule extends RulesActionContainer { } /** - * Get an iterator over all contained actions. Note that this iterator also - * implements the ArrayAcces interface. + * Gets an iterator over all contained actions. + * + * Note that this iterator also implements the ArrayAccess interface. * * @return RulesRecursiveElementIterator */ @@ -193,11 +215,12 @@ class Rule extends RulesActionContainer { } /** - * Add a condition. Pass either an instance of the RulesConditionInterface - * or the arguments as needed by rules_condition(). + * Adds a condition. * - * @return Rule - * Returns $this to support chained usage. + * Pass either an instance of the RulesConditionInterface or the arguments as + * needed by rules_condition(). + * + * @return $this */ public function condition($name, $settings = array()) { $this->conditions->condition($name, $settings); @@ -309,7 +332,9 @@ class Rule extends RulesActionContainer { } /** - * Rules may not provided any variable info assertions, as Rules are only + * Overrides RulesPlugin::variableInfoAssertions(). + * + * Rules may not provide any variable info assertions, as Rules are only * conditionally executed. */ protected function variableInfoAssertions() { @@ -324,7 +349,7 @@ class Rule extends RulesActionContainer { } /** - * Overriden to expose the variables of all actions for embedded rules. + * Overridden to expose the variables of all actions for embedded rules. */ public function providesVariables() { $provides = parent::providesVariables(); @@ -340,6 +365,7 @@ class Rule extends RulesActionContainer { parent::resetInternalCache(); $this->conditions->resetInternalCache(); } + } /** @@ -347,22 +373,30 @@ class Rule extends RulesActionContainer { */ class RulesReactionRule extends Rule implements RulesTriggerableInterface { + /** + * @var string + */ protected $itemName = 'reaction rule'; + + /** + * @var array + */ protected $events = array(); /** - * Returns the array of events associated with that Rule. + * @var array */ - public function &events() { + protected $eventSettings = array(); + + /** + * Implements RulesTriggerableInterface::events(). + */ + public function events() { return $this->events; } /** - * Removes an event from the rule configuration. - * - * @param $event - * The name of the event to remove. - * @return RulesReactionRule + * Implements RulesTriggerableInterface::removeEvent(). */ public function removeEvent($event) { if (($id = array_search($event, $this->events)) !== FALSE) { @@ -372,10 +406,43 @@ class RulesReactionRule extends Rule implements RulesTriggerableInterface { } /** - * @return RulesReactionRule + * Implements RulesTriggerableInterface::event(). */ - public function event($event) { - $this->events[] = $event; + public function event($event_name, array $settings = NULL) { + // Process any settings and determine the configured event's name. + if ($settings) { + $handler = rules_get_event_handler($event_name, $settings); + if ($suffix = $handler->getEventNameSuffix()) { + $event_name .= '--' . $suffix; + $this->eventSettings[$event_name] = $settings; + } + else { + // Do not store settings if there is no suffix. + unset($this->eventSettings[$event_name]); + } + } + if (array_search($event_name, $this->events) === FALSE) { + $this->events[] = $event_name; + } + return $this; + } + + /** + * Implements RulesTriggerableInterface::getEventSettings(). + */ + public function getEventSettings($event_name) { + if (isset($this->eventSettings[$event_name])) { + return $this->eventSettings[$event_name]; + } + } + + public function integrityCheck() { + parent::integrityCheck(); + // Check integrity of the configured events. + foreach ($this->events as $event_name) { + $handler = rules_get_event_handler($event_name, $this->getEventSettings($event_name)); + $handler->validate(); + } return $this; } @@ -394,9 +461,9 @@ class RulesReactionRule extends Rule implements RulesTriggerableInterface { } public function access() { - $event_info = rules_fetch_data('event_info'); - foreach ($this->events as $event) { - if (!empty($event_info[$event]['access callback']) && !call_user_func($event_info[$event]['access callback'], 'event', $event)) { + foreach ($this->events as $event_name) { + $event_info = rules_get_event_info($event_name); + if (!empty($event_info['access callback']) && !call_user_func($event_info['access callback'], 'event', $event_info['name'])) { return FALSE; } } @@ -405,10 +472,10 @@ class RulesReactionRule extends Rule implements RulesTriggerableInterface { public function dependencies() { $modules = array_flip(parent::dependencies()); - $event_info = rules_fetch_data('event_info'); - foreach ($this->events as $event) { - if (isset($event_info[$event]['module'])) { - $modules[$event_info[$event]['module']] = TRUE; + foreach ($this->events as $event_name) { + $event_info = rules_get_event_info($event_name); + if (isset($event_info['module'])) { + $modules[$event_info['module']] = TRUE; } } return array_keys($modules); @@ -433,15 +500,20 @@ class RulesReactionRule extends Rule implements RulesTriggerableInterface { else { // The intersection of the variables provided by the events are // available. - $event_info = rules_fetch_data('event_info'); - $events = array_intersect($this->events, array_keys($event_info)); - foreach ($events as $event) { - $event_info[$event] += array('variables' => array()); + foreach ($this->events as $event_name) { + $handler = rules_get_event_handler($event_name, $this->getEventSettings($event_name)); + if (isset($this->availableVariables)) { - $this->availableVariables = array_intersect_key($this->availableVariables, $event_info[$event]['variables']); + $event_vars = $handler->availableVariables(); + // Merge variable info by intersecting the variable-info keys also, + // so we have only metadata available that is valid for all of the + // provided variables. + foreach (array_intersect_key($this->availableVariables, $event_vars) as $name => $variable_info) { + $this->availableVariables[$name] = array_intersect_key($variable_info, $event_vars[$name]); + } } else { - $this->availableVariables = $event_info[$event]['variables']; + $this->availableVariables = $handler->availableVariables(); } } $this->availableVariables = isset($this->availableVariables) ? RulesState::defaultVariables() + $this->availableVariables : RulesState::defaultVariables(); @@ -451,18 +523,38 @@ class RulesReactionRule extends Rule implements RulesTriggerableInterface { } public function __sleep() { - return parent::__sleep() + drupal_map_assoc(array('events')); + return parent::__sleep() + drupal_map_assoc(array('events', 'eventSettings')); } protected function exportChildren($key = 'ON') { - $export[$key] = array_values($this->events); + foreach ($this->events as $event_name) { + $export[$key][$event_name] = (array) $this->getEventSettings($event_name); + } return $export + parent::exportChildren(); } protected function importChildren($export, $key = 'ON') { - $this->events = $export[$key]; + // Detect and support old-style exports: a numerically indexed array of + // event names. + if (is_string(reset($export[$key])) && is_numeric(key($export[$key]))) { + $this->events = $export[$key]; + } + else { + $this->events = array_keys($export[$key]); + $this->eventSettings = array_filter($export[$key]); + } parent::importChildren($export); } + + /** + * Overrides optimize(). + */ + public function optimize() { + parent::optimize(); + // No need to keep event settings for evaluation. + $this->eventSettings = array(); + } + } /** @@ -470,6 +562,9 @@ class RulesReactionRule extends Rule implements RulesTriggerableInterface { */ class RulesAnd extends RulesConditionContainer { + /** + * @var string + */ protected $itemName = 'and'; public function evaluate(RulesState $state) { @@ -486,6 +581,7 @@ class RulesAnd extends RulesConditionContainer { public function label() { return !empty($this->label) ? $this->label : ($this->negate ? t('NOT AND') : t('AND')); } + } /** @@ -493,6 +589,9 @@ class RulesAnd extends RulesConditionContainer { */ class RulesOr extends RulesConditionContainer { + /** + * @var string + */ protected $itemName = 'or'; public function evaluate(RulesState $state) { @@ -511,6 +610,8 @@ class RulesOr extends RulesConditionContainer { } /** + * Overrides RulesContainerPlugin::stateVariables(). + * * Overridden to exclude all variable assertions as in an OR we cannot assert * the children are successfully evaluated. */ @@ -527,6 +628,7 @@ class RulesOr extends RulesConditionContainer { } return $vars; } + } /** @@ -534,6 +636,9 @@ class RulesOr extends RulesConditionContainer { */ class RulesLoop extends RulesActionContainer { + /** + * @var string + */ protected $itemName = 'loop'; protected $listItemInfo; @@ -640,6 +745,7 @@ class RulesLoop extends RulesActionContainer { $this->settings['item:label'] = reset($export['ITEM']); } } + } /** @@ -647,6 +753,9 @@ class RulesLoop extends RulesActionContainer { */ class RulesActionSet extends RulesActionContainer { + /** + * @var string + */ protected $itemName = 'action set'; } @@ -656,6 +765,9 @@ class RulesActionSet extends RulesActionContainer { */ class RulesRuleSet extends RulesActionContainer { + /** + * @var string + */ protected $itemName = 'rule set'; /** @@ -672,6 +784,7 @@ class RulesRuleSet extends RulesActionContainer { protected function importChildren($export, $key = 'RULES') { parent::importChildren($export, $key); } + } /** @@ -679,8 +792,16 @@ class RulesRuleSet extends RulesActionContainer { */ class RulesEventSet extends RulesRuleSet { + /** + * @var string + */ protected $itemName = 'event set'; - // Event sets may recurse as we block recursions on rule-level. + + /** + * Event sets may recurse as we block recursions on rule-level. + * + * @var bool + */ public $recursion = TRUE; public function __construct($info = array()) { @@ -698,7 +819,10 @@ class RulesEventSet extends RulesRuleSet { } /** - * Cache event-sets per event to allow efficient usage via rules_invoke_event(). + * Rebuilds the event cache. + * + * We cache event-sets per event in order to allow efficient usage via + * rules_invoke_event(). * * @see rules_get_cache() * @see rules_invoke_event() @@ -711,18 +835,27 @@ class RulesEventSet extends RulesRuleSet { $rules = rules_config_load_multiple(FALSE, array('plugin' => 'reaction rule', 'active' => TRUE)); foreach ($rules as $name => $rule) { - foreach ($rule->events() as $event) { + foreach ($rule->events() as $event_name) { + $event_base_name = rules_get_event_base_name($event_name); // Skip not defined events. - if (empty($events[$event])) { + if (empty($events[$event_base_name])) { continue; } // Create an event set if not yet done. - if (!isset($sets[$event])) { - $event_info = $events[$event] + array( - 'variables' => isset($events[$event]['arguments']) ? $events[$event]['arguments'] : array(), - ); - $sets[$event] = new RulesEventSet($event_info); - $sets[$event]->name = $event; + if (!isset($sets[$event_name])) { + $handler = rules_get_event_handler($event_name, $rule->getEventSettings($event_name)); + + // Start the event dispatcher for this event, if any. + if ($handler instanceof RulesEventDispatcherInterface && !$handler->isWatching()) { + $handler->startWatching(); + } + + // Update the event info with the variables available based on the + // event settings. + $event_info = $events[$event_base_name]; + $event_info['variables'] = $handler->availableVariables(); + $sets[$event_name] = new RulesEventSet($event_info); + $sets[$event_name]->name = $event_name; } // If a rule is marked as dirty, check if this still applies. @@ -732,23 +865,22 @@ class RulesEventSet extends RulesRuleSet { if (!$rule->dirty) { // Clone the rule to avoid modules getting the changed version from // the static cache. - $sets[$event]->rule(clone $rule); + $sets[$event_name]->rule(clone $rule); } } } // Create cache items for all created sets. - foreach ($sets as $event => $set) { + foreach ($sets as $event_name => $set) { $set->sortChildren(); $set->optimize(); // Allow modules to alter the cached event set. - drupal_alter('rules_event_set', $event, $set); - rules_set_cache('event_' . $event, $set); + drupal_alter('rules_event_set', $event_name, $set); + rules_set_cache('event_' . $event_name, $set); } - // Cache a list of empty sets so we can use it to speed up later calls. - // See rules_get_event_set(). - $empty_events = array_keys(array_diff_key($events, $sets)); - variable_set('rules_empty_sets', array_flip($empty_events)); + // Cache a whitelist of configured events so we can use it to speed up later + // calls. See rules_invoke_event(). + rules_set_cache('rules_event_whitelist', array_flip(array_keys($sets))); } protected function stateVariables($element = NULL) { @@ -763,4 +895,5 @@ class RulesEventSet extends RulesRuleSet { public function save($name = NULL, $module = 'rules') { return FALSE; } + } diff --git a/sites/all/modules/contrib/admin/rules/includes/rules.processor.inc b/sites/all/modules/contrib/admin/rules/includes/rules.processor.inc index 468bea0b..42ca45d8 100644 --- a/sites/all/modules/contrib/admin/rules/includes/rules.processor.inc +++ b/sites/all/modules/contrib/admin/rules/includes/rules.processor.inc @@ -1,7 +1,8 @@ access() && (!isset($this->processor) || $this->processor->editAccess()); } - /** * Prepares the processor for parameters. * - * It turns the settings into a suiting processor object, which gets invoked + * It turns the settings into a suitable processor object, which gets invoked * on evaluation time. * * @param $setting * The processor settings which are to be prepared. * @param $param_info * The info about the parameter to prepare the processor for. - * @param $var_info + * @param array $var_info * An array of info about the available variables. */ public static function prepareSetting(&$setting, $param_info, $var_info = array()) { @@ -88,17 +92,22 @@ abstract class RulesDataProcessor { /** * Returns defined data processors applicable for the given parameter. - * Optionally also access to the processors is checked. + * + * Optionally also checks access to the processors. * * @param $param_info * If given, only processors valid for this parameter are returned. + * @param bool $access_check + * @param string $hook */ public static function processors($param_info = NULL, $access_check = TRUE, $hook = 'data_processor_info') { static $items = array(); if (!isset($items[$hook]['all'])) { $items[$hook]['all'] = rules_fetch_data($hook); - uasort($items[$hook]['all'], array(__CLASS__, '_item_sort')); + if (isset($items[$hook]['all'])) { + uasort($items[$hook]['all'], array(__CLASS__, '_item_sort')); + } } // Data processing isn't supported for multiple types. if (isset($param_info) && is_array($param_info['type'])) { @@ -182,17 +191,20 @@ abstract class RulesDataProcessor { } /** - * Processes the value. If $this->processor is set, invoke this processor - * first so chaining multiple processors is working. + * Processes the value. + * + * If $this->processor is set, invoke this processor first so chaining + * multiple processors is working. * * @param $value * The value to process. * @param $info * Info about the parameter for which we process the value. - * @param $state RulesState + * @param RulesState $state * The rules evaluation state. - * @param $element RulesPlugin + * @param RulesPlugin $element * The element for which we process the value. + * * @return * The processed value. */ @@ -200,6 +212,9 @@ abstract class RulesDataProcessor { /** * Return whether the current user has permission to use the processor. + * + * @return bool + * Whether the current user has permission to use the processor. */ public static function access() { return TRUE; @@ -210,7 +225,7 @@ abstract class RulesDataProcessor { * * @param $settings * The settings of the processor. - * @param $var_info + * @param array $var_info * An array of info about the available variables. * * @return @@ -219,13 +234,15 @@ abstract class RulesDataProcessor { protected static function form($settings, $var_info) { return array(); } + } /** - * A base processor for use as input evaluators. Input evaluators are not listed - * in hook_rules_data_processor_info(). Instead they use - * hook_rules_evaluator_info() and get attached to input forms. + * A base processor for use by input evaluators. + * + * Input evaluators are not listed in hook_rules_data_processor_info(). Instead + * they use hook_rules_evaluator_info() and get attached to input forms. */ abstract class RulesDataInputEvaluator extends RulesDataProcessor { @@ -266,9 +283,10 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { } /** - * Overriden to prepare input evaluator processors. The setting is expected - * to be the input value to be evaluated later on and is replaced by the - * suiting processor. + * Overridden to prepare input evaluator processors. + * + * The setting is expected to be the input value to be evaluated later on + * and is replaced by the suitable processor. */ public static function prepareSetting(&$setting, $param_info, $var_info = array()) { $processor = NULL; @@ -284,7 +302,9 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { } /** - * Overriden to just attach the help() of evaluators. + * Overrides RulesDataProcessor::attachForm(). + * + * Overridden to just attach the help() of evaluators. */ public static function attachForm(&$form, $settings, $param_info, $var_info, $access_check = TRUE) { foreach (self::evaluators($param_info, $access_check) as $name => $info) { @@ -294,14 +314,15 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { } /** - * Returns all input evaluators that can be applied to the parameters needed - * type. + * Returns all input evaluators that can be applied to the parameters type. */ public static function evaluators($param_info = NULL, $access_check = TRUE) { return parent::processors($param_info, $access_check, 'evaluator_info'); } /** + * Overrides RulesDataProcessor::processors(). + * * Overridden to default to our hook, thus being equivalent to * self::evaluators(). */ @@ -310,14 +331,16 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { } /** - * Prepares the evalution, e.g. to determine whether the input evaluator has - * been used. If this evaluator should be skipped just unset $this->setting. + * Prepares the evaluation. * - * @param $text + * For example, to determine whether the input evaluator has been used. + * If this evaluator should be skipped just unset $this->setting. + * + * @param string $text * The text to evaluate later on. - * @param $variables + * @param array $variables * An array of info about available variables. - * @param $param_info + * @param array $param_info * (optional) An array of information about the handled parameter value. * For backward compatibility, this parameter is not required. */ @@ -326,9 +349,9 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { /** * Apply the input evaluator. * - * @param $text + * @param string $text * The text to evaluate. - * @param $options + * @param array $options * A keyed array of settings and flags to control the processing. * Supported options are: * - language: A language object to be used when processing. @@ -337,7 +360,7 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { * certain way. * - sanitize: A boolean flag indicating whether incorporated replacements * should be sanitized. - * @param RulesState + * @param RulesState $state * The rules evaluation state. * * @return @@ -348,13 +371,13 @@ abstract class RulesDataInputEvaluator extends RulesDataProcessor { /** * Provide some usage help for the evaluator. * - * @param $variables + * @param array $variables * An array of info about available variables. - * @param $param_info + * @param array $param_info * (optional) An array of information about the handled parameter value. * For backward compatibility, this parameter is not required. * - * @return + * @return array * A renderable array. */ public static function help($variables) { diff --git a/sites/all/modules/contrib/admin/rules/includes/rules.state.inc b/sites/all/modules/contrib/admin/rules/includes/rules.state.inc index a4150e76..2e9213e3 100644 --- a/sites/all/modules/contrib/admin/rules/includes/rules.state.inc +++ b/sites/all/modules/contrib/admin/rules/includes/rules.state.inc @@ -1,7 +1,8 @@ language($langcode); @@ -312,30 +325,37 @@ class RulesState { /** * Magic method. Only serialize variables and their info. + * * Additionally we remember currently blocked configs, so we can restore them * upon deserialization using restoreBlocks(). */ - public function __sleep () { + public function __sleep() { $this->currentlyBlocked = self::$blocked; return array('info', 'variables', 'currentlyBlocked'); } + /** + * Magic method. Unserialize variables and their info. + */ public function __wakeup() { $this->save = new ArrayObject(); } /** - * Restore the before serialization blocked configurations. + * Restores the before-serialization blocked configurations. * * Warning: This overwrites any possible currently blocked configs. Thus - * do not invoke this method, if there might be evaluations active. + * do not invoke this method if there might be evaluations active. */ public function restoreBlocks() { self::$blocked = $this->currentlyBlocked; } /** - * Defines always available variables. + * Defines always-available variables. + * + * @param $key + * (optional) */ public static function defaultVariables($key = NULL) { // Add a variable for accessing site-wide data properties. @@ -350,12 +370,13 @@ class RulesState { ); return isset($key) ? $vars[$key] : $vars; } + } /** * A class holding static methods related to data. */ -class RulesData { +class RulesData { /** * Returns whether the type match. They match if type1 is compatible to type2. @@ -364,11 +385,11 @@ class RulesData { * The name of the type to check for whether it is compatible to type2. * @param $param_info * The type expression to check for. - * @param $ancestors - * Whether sub-type relationships for checking type compatibility should be - * taken into account. Defaults to TRUE. + * @param bool $ancestors + * (optional) Whether sub-type relationships for checking type compatibility + * should be taken into account. Defaults to TRUE. * - * @return + * @return bool * Whether the types match. */ public static function typesMatch($var_info, $param_info, $ancestors = TRUE) { @@ -395,7 +416,7 @@ class RulesData { $cache = &rules_get_cache(); self::typeCalcAncestors($cache, $var_type); // If one of the types is an ancestor return TRUE. - return (bool)array_intersect_key($cache['data_info'][$var_type]['ancestors'], array_flip($valid_types)); + return (bool) array_intersect_key($cache['data_info'][$var_type]['ancestors'], array_flip($valid_types)); } return FALSE; } @@ -417,25 +438,27 @@ class RulesData { } /** - * Returns matching data variables or properties for the given info and the to - * be configured parameter. + * Returns data for the given info and the to-be-configured parameter. + * + * Returns matching data variables or properties for the given info and the + * to-be-configured parameter. * * @param $source * Either an array of info about available variables or a entity metadata * wrapper. * @param $param_info * The information array about the to be configured parameter. - * @param $prefix + * @param string $prefix * An optional prefix for the data selectors. - * @param $recursions + * @param int $recursions * The number of recursions used to go down the tree. Defaults to 2. - * @param $suggestions + * @param bool $suggestions * Whether possibilities to recurse are suggested as soon as the deepest * level of recursions is reached. Defaults to TRUE. * - * @return - * An array of info about matching variables or properties that match, keyed - * with the data selector. + * @return array + * An array of info about matching variables or properties that match, keyed + * with the data selector. */ public static function matchingDataSelector($source, $param_info, $prefix = '', $recursions = 2, $suggestions = TRUE) { // If an array of info is given, get entity metadata wrappers first. @@ -467,7 +490,8 @@ class RulesData { $matches += self::matchingDataSelector($wrapper, $param_info, $prefix . $name . ':', $recursions - 1, $suggestions); } elseif ($suggestions) { - // We may not recurse any more, but indicate the possibility to recurse. + // We may not recurse any more, + // but indicate the possibility to recurse. $matches[$prefix . $name . ':'] = $wrapper->info(); if (!is_array($source) && $source instanceof EntityListWrapper) { // Add some more possible list items. @@ -482,8 +506,10 @@ class RulesData { } /** - * Adds asserted metadata to the variable info. In case there are already - * assertions for a variable, the assertions are merged such that both apply. + * Adds asserted metadata to the variable info. + * + * In case there are already assertions for a variable, the assertions are + * merged such that both apply. * * @see RulesData::applyMetadataAssertions() */ @@ -508,10 +534,9 @@ class RulesData { // before the child-wrapper is created. if (count($parts) == 1) { // Support asserting a type in case of generic entity references only. - if (isset($assertion['type']) && $var_info[$parts[0]]['type'] == 'entity') { - if (entity_get_info($assertion['type'])) { - $var_info[$parts[0]]['type'] = $assertion['type']; - } + $var_type = &$var_info[$parts[0]]['type']; + if (isset($assertion['type']) && ($var_type == 'entity' || $var_type == 'list')) { + $var_type = $assertion['type']; unset($assertion['type']); } // Add any single bundle directly to the variable info, so the @@ -547,8 +572,9 @@ class RulesData { } /** - * Property info alter callback for the entity metadata wrapper for applying - * the rules metadata assertions. + * Property info alter callback for the entity metadata wrapper. + * + * Used for applying the rules metadata assertions. * * @see RulesData::addMetadataAssertions() */ @@ -586,7 +612,8 @@ class RulesData { $property_info['properties'][$key]['rules assertion'] = $assertion[$key]; $property_info['properties'][$key]['property info alter'] = array('RulesData', 'applyMetadataAssertions'); - // Apply any 'type' and 'bundle' assertion directly to the propertyinfo. + // Apply any 'type' and 'bundle' assertion directly to the property + // info. if (isset($assertion[$key]['#info']['type'])) { $type = $assertion[$key]['#info']['type']; // Support asserting a type in case of generic entity references only. @@ -608,9 +635,10 @@ class RulesData { } /** - * Property info alter callback for the entity metadata wrapper to inject - * metadata for the 'site' variable. In contrast to doing this via - * hook_rules_data_info() this callback makes use of the already existing + * Property info alter callback for the entity metadata wrapper. + * + * Used to inject metadata for the 'site' variable. In contrast to doing this + * via hook_rules_data_info() this callback makes use of the already existing * property info cache for site information of entity metadata. * * @see RulesPlugin::availableVariables() @@ -622,6 +650,7 @@ class RulesData { // have specified further metadata. return RulesData::applyMetadataAssertions($wrapper, $property_info); } + } /** @@ -653,7 +682,7 @@ abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper { * The type of the passed data. * @param $data * Optional. The data to wrap or its identifier. - * @param $info + * @param array $info * Optional. Used internally to pass info about properties down the tree. */ public function __construct($type, $data = NULL, $info = array()) { @@ -722,7 +751,7 @@ abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper { } /** - * Prepare for serializiation. + * Prepare for serialization. */ public function __sleep() { $vars = parent::__sleep(); @@ -734,6 +763,9 @@ abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper { return $vars; } + /** + * Prepare for unserialization. + */ public function __wakeup() { if ($this->id !== FALSE) { // Make sure data is set, so the data will be loaded when needed. @@ -756,10 +788,11 @@ abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper { * The loaded data object, or FALSE if loading failed. */ abstract protected function load($id); + } /** - * Interface that allows custom wrapper classes to declare that they are savable. + * Used to declare custom wrapper classes as savable. */ interface RulesDataWrapperSavableInterface { @@ -767,4 +800,5 @@ interface RulesDataWrapperSavableInterface { * Save the currently wrapped data. */ public function save(); + } diff --git a/sites/all/modules/contrib/admin/rules/includes/rules.upgrade.inc b/sites/all/modules/contrib/admin/rules/includes/rules.upgrade.inc index e70d3ec9..2eb2cade 100644 --- a/sites/all/modules/contrib/admin/rules/includes/rules.upgrade.inc +++ b/sites/all/modules/contrib/admin/rules/includes/rules.upgrade.inc @@ -28,8 +28,8 @@ function rules_upgrade_form($form, &$form_state) { '#prefix' => '

', '#suffix' => '

', '#markup' => t('This form allows you to convert rules or rule sets from Rules 1.x to Rules 2.x.') . ' ' . - t('In order to convert a rule or rule set make sure you have all dependend modules installed and upgraded, i.e. modules which provide Rules integration that has been used in your rules or rule sets. In addition those modules may need to implement some Rules specific update hooks for the conversion to properly work.') . ' ' . - t('After conversion, the old rules and rule sets will stay in the database until you manually delete them. That way you can make sure the conversion has gone right before you delete the old rules and rule sets.') + t('In order to convert a rule or rule set make sure you have all dependent modules installed and upgraded, i.e. modules which provide Rules integration that has been used in your rules or rule sets. In addition those modules may need to implement some Rules specific update hooks for the conversion to properly work.') . ' ' . + t('After conversion, the old rules and rule sets will stay in the database until you manually delete them. That way you can make sure the conversion has gone right before you delete the old rules and rule sets.'), ); $option_rules = $option_sets = array(); @@ -51,7 +51,7 @@ function rules_upgrade_form($form, &$form_state) { $form['clear'] = array( '#prefix' => '

', '#suffix' => '

', - '#markup' => t('Once you have successfully converted your configuration, you can clean up your database and delete all Rules 1.x configurations.', array('!url' => url('admin/config/workflow/rules/upgrade/clear'))) + '#markup' => t('Once you have successfully converted your configuration, you can clean up your database and delete all Rules 1.x configurations.', array('!url' => url('admin/config/workflow/rules/upgrade/clear'))), ); } @@ -72,8 +72,8 @@ function rules_upgrade_form($form, &$form_state) { '#type' => 'radios', '#title' => t('Method'), '#options' => array( - 'export' => t('Convert configuration and export it.'), - 'save' => t('Convert configuration and save it.'), + 'export' => t('Convert configuration and export it.'), + 'save' => t('Convert configuration and save it.'), ), '#default_value' => 'export', ); @@ -81,7 +81,7 @@ function rules_upgrade_form($form, &$form_state) { $form['actions']['convert'] = array( '#type' => 'submit', '#value' => t('Convert'), - '#disabled' => !db_table_exists('rules_rules') + '#disabled' => !db_table_exists('rules_rules'), ); return $form; } @@ -137,6 +137,9 @@ function rules_upgrade_confirm_clear_form($form, $form_state) { return confirm_form($form, $confirm_question, 'admin/config/workflow/rules/upgrade', $confirm_question_long, t('Delete data'), t('Cancel')); } +/** + * Submit handler for deleting data. + */ function rules_upgrade_confirm_clear_form_submit($form, &$form_state) { db_drop_table('rules_rules'); db_drop_table('rules_sets'); @@ -206,7 +209,7 @@ function rules_upgrade_convert_rule_set($name, $cfg_old) { } // Add in all rules of the set. - foreach(_rules_upgrade_fetch_all_rules() as $rule_name => $rule) { + foreach (_rules_upgrade_fetch_all_rules() as $rule_name => $rule) { if ($rule['#set'] == $name) { drupal_set_message(' >> ' . t('Converting %plugin %name...', array('%plugin' => t('rule'), '%name' => $rule_name . ': ' . $rule['#label']))); $new_rule = rules_upgrade_plugin_factory($rule); @@ -220,9 +223,9 @@ function rules_upgrade_convert_rule_set($name, $cfg_old) { /** * Convert a single element. * - * @param $element + * @param array $element * The element to convert. - * @param $target + * @param RulesPlugin $target * The converted element to write to. */ function rules_upgrade_convert_element(array $element, RulesPlugin $target) { @@ -235,7 +238,7 @@ function rules_upgrade_convert_element(array $element, RulesPlugin $target) { foreach ($target->pluginParameterInfo() as $name => $info) { rules_upgrade_element_parameter_settings($element, $target, $name); } - // @todo: Care about php input evaluator for non-text parameters. + // @todo Care about php input evaluator for non-text parameters. // Take care of variable names and labels. foreach ($target->pluginProvidesVariables() as $name => $info) { @@ -268,10 +271,10 @@ function rules_upgrade_convert_element(array $element, RulesPlugin $target) { // Invoke action/condition specific hooks and a general one. if (($element['#type'] == 'action' || $element['#type'] == 'condition')) { - if (function_exists($function = $element['#name'] .'_upgrade')) { + if (function_exists($function = $element['#name'] . '_upgrade')) { $element_name = $function($element, $target); } - elseif (isset($element['#info']['base']) && function_exists($function = $element['#info']['base'] .'_upgrade')) { + elseif (isset($element['#info']['base']) && function_exists($function = $element['#info']['base'] . '_upgrade')) { $element_name = $function($element, $target); } } @@ -299,8 +302,10 @@ function rules_upgrade_plugin_factory($element) { switch ($element['#type']) { case 'OR': return rules_plugin_factory('or'); + case 'AND': return rules_plugin_factory('and'); + default: return rules_plugin_factory($element['#type']); @@ -329,7 +334,7 @@ function rules_upgrade_plugin_factory($element) { } // Call the upgrade callback if one has been defined. - if (function_exists($function = $element['#name'] .'_upgrade_map_name') || (isset($element['#info']['base']) && function_exists($function = $element['#info']['base'] .'_upgrade_map_name'))) { + if (function_exists($function = $element['#name'] . '_upgrade_map_name') || (isset($element['#info']['base']) && function_exists($function = $element['#info']['base'] . '_upgrade_map_name'))) { $element_name = $function($element); } if (!isset($element_name)) { @@ -380,59 +385,77 @@ function rules_upgrade_element_variable_settings($element, $target, $name, $new_ * Upgrade callbacks for upgrading the provided Rules 1.x integration. */ -// Comment.module integration. +/** + * Comment.module integration. + */ function rules_action_load_comment_upgrade_map_name($element) { return 'entity_fetch'; } + function rules_action_load_comment_upgrade($element, $target) { $target->settings['type'] = 'comment'; rules_upgrade_element_parameter_settings($element, $target, 'cid', 'id'); rules_upgrade_element_variable_settings($element, $target, 'comment_loaded', 'entity_fetched'); } -// Node.module integration. +/** + * Node.module integration. + */ function rules_condition_content_is_type_upgrade_map_name($element) { return 'node_is_of_type'; } + function rules_condition_content_is_published_upgrade_map_name($element) { return 'node_is_published'; } + function rules_condition_content_is_sticky_upgrade_map_name($element) { return 'node_is_sticky'; } + function rules_condition_content_is_promoted_upgrade_map_name($element) { return 'node_is_promoted'; } + function rules_condition_content_is_new_upgrade_map_name($element) { return 'entity_is_new'; } + function rules_condition_content_is_new_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'node', 'entity'); } + function rules_action_node_set_author_upgrade_map_name($element) { return 'data_set'; } + function rules_action_node_set_author_upgrade($element, $target) { $target->settings['data:select'] = $element['#settings']['#argument map']['node'] . ':author'; $target->settings['value:select'] = $element['#settings']['#argument map']['author']; } + function rules_action_node_load_author_upgrade_map_name($element) { return 'entity_fetch'; } + function rules_action_node_load_author_upgrade($element, $target) { $target->settings['type'] = 'user'; $target->settings['id'] = $element['#settings']['#argument map']['node'] . ':author:uid'; } + function rules_action_set_node_title_upgrade_map_name($element) { return 'data_set'; } + function rules_action_set_node_title_upgrade($element, $target) { $target->settings['data:select'] = $element['#settings']['#argument map']['node'] . ':title'; $target->settings['value'] = $element['#settings']['title']; } + function rules_action_add_node_upgrade_map_name($element) { return 'entity_create'; } + function rules_action_add_node_upgrade($element, $target) { $target->settings['type'] = 'node'; rules_upgrade_element_parameter_settings($element, $target, 'title', 'param_title'); @@ -443,105 +466,135 @@ function rules_action_add_node_upgrade($element, $target) { drupal_set_message(t('Warning: The node-access check option for the node creation action is not supported any more.')); } } + function rules_action_load_node_upgrade_map_name($element) { return 'entity_fetch'; } + function rules_action_load_node_upgrade($element, $target) { $target->settings['type'] = 'node'; rules_upgrade_element_parameter_settings($element, $target, 'nid', 'id'); rules_upgrade_element_parameter_settings($element, $target, 'vid', 'revision_id'); rules_upgrade_element_variable_settings($element, $target, 'node_loaded', 'entity_fetched'); } + function rules_action_delete_node_upgrade_map_name($element) { return 'entity_delete'; } + function rules_action_delete_node_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'node', 'entity'); } + function rules_core_node_publish_action_upgrade_map_name($element) { return 'node_publish'; } + function rules_core_node_unpublish_action_upgrade_map_name($element) { return 'node_unpublish'; } + function rules_core_node_make_sticky_action_upgrade_map_name($element) { return 'node_make_sticky_action'; } + function rules_core_node_make_unsticky_action_upgrade_map_name($element) { return 'node_make_unsticky_action'; } + function rules_core_node_promote_action_upgrade_map_name($element) { return 'node_promote_action'; } + function rules_core_node_unpromote_action_upgrade_map_name($element) { return 'node_unpromote_action'; } - -// Path.module integration. +/** + * Path.module integration. + */ function rules_condition_url_has_alias_upgrade_map_name($element) { return 'path_has_alias'; } + function rules_condition_url_has_alias_upgrade($element, $target) { $target->settings['source'] = $element['#settings']['src']; $target->settings['alias'] = $element['#settings']['dst']; } + function rules_condition_alias_exists_upgrade_map_name($element) { return 'path_alias_exists'; } + function rules_condition_alias_exists_upgrade($element, $target) { $target->settings['alias'] = $element['#settings']['dst']; } + function rules_action_path_alias_upgrade($element, $target) { $target->settings['source'] = $element['#settings']['src']; $target->settings['alias'] = $element['#settings']['dst']; } + function rules_action_node_path_alias_upgrade($element, $target) { $target->settings['alias'] = $element['#settings']['dst']; } -// PHP.module integration. +/** + * PHP.module integration. + */ function rules_condition_custom_php_upgrade_map_name($element) { return 'php_eval'; } + function rules_action_custom_php_upgrade_map_name($element) { return 'php_eval'; } -// General Rules integration. +/** + * General Rules integration. + */ function rules_condition_text_compare_upgrade_map_name($element) { - // @todo: Support regex. + // @todo Support regex. return 'data_is'; } + function rules_condition_text_compare_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'text1', 'data'); rules_upgrade_element_parameter_settings($element, $target, 'text2', 'value'); } + function rules_condition_number_compare_upgrade_map_name($element) { return 'data_is'; } + function rules_condition_number_compare_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'number1', 'data'); rules_upgrade_element_parameter_settings($element, $target, 'number2', 'value'); } + function rules_condition_check_boolean_upgrade_map_name($element) { return 'data_is'; } + function rules_condition_check_boolean_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'boolean', 'data'); $target->settings['value'] = TRUE; } + function rules_action_invoke_set_upgrade_map_name($element) { return 'component_' . $element['#info']['set']; } + function rules_action_invoke_set_upgrade($element, $target) { foreach ($element['#info']['arguments'] as $name => $info) { rules_upgrade_element_parameter_settings($element, $target, $name); } } + function rules_action_save_variable_upgrade_map_name($element) { return isset($element['#info']['new variables']) ? 'variable_add' : 'entity_save'; } + function rules_action_save_variable_upgrade($element, $target) { $type = $element['#info']['arguments']['var_name']['default value']; if (isset($element['#info']['new variables'])) { @@ -554,20 +607,25 @@ function rules_action_save_variable_upgrade($element, $target) { } } - -// System.module integration. +/** + * System.module integration. + */ function rules_action_set_breadcrumb_upgrade_map_name($element) { - return 'breadcumb_set'; + return 'breadcrumb_set'; } + function rules_action_mail_to_user_upgrade_map_name($element) { return 'mail'; } + function rules_action_mail_to_user_upgrade($element, $target) { $target->settings['to:select'] = $element['#settings']['#argument map']['user'] . ':mail'; } + function rules_action_drupal_goto_upgrade_map_name($element) { return 'redirect'; } + function rules_action_drupal_goto_upgrade($element, $target) { $settings = $element['#settings']; $target->settings['url'] = $settings['path']; @@ -579,86 +637,109 @@ function rules_action_drupal_goto_upgrade($element, $target) { } function rules_action_watchdog_upgrade_map_name($element) { - // @todo: Support action in Rules 2.x! + // @todo Support action in Rules 2.x! return NULL; } -// Taxonomy.module integration. -// @todo: Finish. +/** + * Taxonomy.module integration. + * + * @todo Finish. + */ function rules_action_taxonomy_load_term_upgrade_map_name($element) { return 'entity_fetch'; } + function rules_action_taxonomy_add_term_upgrade_map_name($element) { return 'entity_create'; } + function rules_action_taxonomy_delete_term_upgrade_map_name($element) { return 'entity_delete'; } + function rules_action_taxonomy_term_assign_to_content_upgrade_map_name($element) { - // @todo : list. + // @todo List. return NULL; } + function rules_action_taxonomy_term_remove_from_content_upgrade_map_name($element) { - // @todo : list. + // @todo List. return NULL; } + function rules_action_taxonomy_load_vocab_upgrade_map_name($element) { return 'entity_fetch'; } + function rules_action_taxonomy_add_vocab_upgrade_map_name($element) { return 'data_set'; } -// User.module integration. +/** + * User.module integration. + */ function rules_condition_user_hasrole_upgrade_map_name($element) { return 'user_has_role'; } + function rules_condition_user_hasrole_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'user', 'account'); } + function rules_condition_user_comparison_upgrade_map_name($element) { return 'data_is'; } + function rules_condition_user_comparison_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'user1', 'data'); rules_upgrade_element_parameter_settings($element, $target, 'user2', 'value'); } + function rules_action_user_addrole_upgrade_map_name($element) { return 'user_add_role'; } + function rules_action_user_addrole_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'user', 'account'); } + function rules_action_user_removerole_upgrade_map_name($element) { return 'user_remove_role'; } + function rules_action_user_removerole_upgrade($element, $target) { rules_upgrade_element_parameter_settings($element, $target, 'user', 'account'); } + function rules_action_load_user_upgrade_map_name($element) { if (!empty($element['#settings']['username'])) { drupal_set_message(t('Warning: Directly upgrading the load user by name action is not supported.')); } return 'entity_fetch'; } + function rules_action_load_user_upgrade($element, $target) { $target->settings['type'] = 'user'; rules_upgrade_element_parameter_settings($element, $target, 'userid', 'id'); rules_upgrade_element_variable_settings($element, $target, 'user_loaded', 'entity_fetched'); } + function rules_action_user_create_upgrade_map_name($element) { return 'entity_create'; } + function rules_action_user_create_upgrade($element, $target) { $target->settings['type'] = 'user'; rules_upgrade_element_parameter_settings($element, $target, 'username', 'param_name'); rules_upgrade_element_parameter_settings($element, $target, 'email', 'param_mail'); rules_upgrade_element_variable_settings($element, $target, 'user_added', 'entity_created'); - } + function rules_core_user_block_user_action_upgrade_map_name($element) { return 'user_block'; } + function rules_core_user_block_user_action_upgrade($element, $target) { $target->settings['account:select'] = $element['#settings']['#argument map']['user']; } diff --git a/sites/all/modules/contrib/admin/rules/modules/comment.rules.inc b/sites/all/modules/contrib/admin/rules/modules/comment.rules.inc index f9846fda..2052e6f9 100644 --- a/sites/all/modules/contrib/admin/rules/modules/comment.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/comment.rules.inc @@ -1,20 +1,23 @@ t('comment'), 'module' => 'comment', 'access callback' => 'rules_comment_integration_access', + 'class' => 'RulesCommentEventHandler', ); return array( 'comment_insert' => $defaults + array( @@ -26,15 +29,30 @@ function rules_comment_event_info() { 'comment_update' => $defaults + array( 'label' => t('After updating an existing comment'), 'variables' => array( - 'comment' => array('type' => 'comment', 'label' => t('updated comment')), - 'comment_unchanged' => array('type' => 'comment', 'label' => t('unchanged comment'), 'handler' => 'rules_events_entity_unchanged'), + 'comment' => array( + 'type' => 'comment', + 'label' => t('updated comment'), + ), + 'comment_unchanged' => array( + 'type' => 'comment', + 'label' => t('unchanged comment'), + 'handler' => 'rules_events_entity_unchanged', + ), ), ), 'comment_presave' => $defaults + array( 'label' => t('Before saving a comment'), 'variables' => array( - 'comment' => array('type' => 'comment', 'label' => t('saved comment'), 'skip save' => TRUE), - 'comment_unchanged' => array('type' => 'comment', 'label' => t('unchanged comment'), 'handler' => 'rules_events_entity_unchanged'), + 'comment' => array( + 'type' => 'comment', + 'label' => t('saved comment'), + 'skip save' => TRUE, + ), + 'comment_unchanged' => array( + 'type' => 'comment', + 'label' => t('unchanged comment'), + 'handler' => 'rules_events_entity_unchanged', + ), ), ), 'comment_view' => $defaults + array( @@ -62,6 +80,23 @@ function rules_comment_integration_access($type, $name) { } } +/** + * Event handler support comment bundle event settings. + */ +class RulesCommentEventHandler extends RulesEventHandlerEntityBundle { + + /** + * Returns the label to use for the bundle property. + * + * @return string + * Returns the label to use for the bundle property. + */ + protected function getBundlePropertyLabel() { + return t('type'); + } + +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/modules/data.eval.inc b/sites/all/modules/contrib/admin/rules/modules/data.eval.inc index 5370e70b..b3b67592 100644 --- a/sites/all/modules/contrib/admin/rules/modules/data.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/data.eval.inc @@ -5,6 +5,7 @@ * Contains rules integration for the data module needed during evaluation. * * @addtogroup rules + * * @{ */ @@ -43,7 +44,7 @@ function rules_action_data_set_info_alter(&$element_info, $element) { if ($wrapper = $element->applyDataSelector($element->settings['data:select'])) { $info = $wrapper->info(); $element_info['parameter']['value']['type'] = $wrapper->type(); - $element_info['parameter']['value']['options list'] = !empty($info['options list']) ? 'rules_data_selector_options_list' : FALSE; + $element_info['parameter']['value']['options list'] = !empty($info['options list']) ? 'rules_data_selector_options_list' : FALSE; } } @@ -62,15 +63,26 @@ function rules_action_data_calc($input1, $op, $input2, $settings, $state, $eleme case '+': $result = $input1 + $input2; break; + case '-': $result = $input1 - $input2; break; + case '*': $result = $input1 * $input2; break; + case '/': $result = $input1 / $input2; break; + + case 'min': + $result = min($input1, $input2); + break; + + case 'max': + $result = max($input1, $input2); + break; } if (isset($result)) { // Ensure results are valid integer values if necessary. @@ -136,7 +148,7 @@ function rules_data_list_info_alter(&$element_info, RulesAbstractPlugin $element if ($type = entity_property_list_extract_type($wrapper->type())) { $info = $wrapper->info(); $element_info['parameter']['item']['type'] = $type; - $element_info['parameter']['item']['options list'] = !empty($info['options list']) ? 'rules_data_selector_options_list' : FALSE; + $element_info['parameter']['item']['options list'] = !empty($info['options list']) ? 'rules_data_selector_options_list' : FALSE; } } } @@ -191,9 +203,11 @@ function rules_action_data_convert($arguments, RulesPlugin $element, $state) { case 'up': $arguments['value'] = ceil($arguments['value']); break; + case 'down': $arguments['value'] = floor($arguments['value']); break; + default: case 'round': $arguments['value'] = round($arguments['value']); @@ -205,12 +219,18 @@ function rules_action_data_convert($arguments, RulesPlugin $element, $state) { case 'decimal': $result = floatval($arguments['value']); break; + case 'integer': $result = intval($arguments['value']); break; + case 'text': $result = strval($arguments['value']); break; + + case 'token': + $result = strval($arguments['value']); + break; } return array('conversion_result' => $result); @@ -224,8 +244,19 @@ function rules_action_data_convert_info_alter(&$element_info, RulesAbstractPlugi if (isset($element->settings['type']) && $type = $element->settings['type']) { $element_info['provides']['conversion_result']['type'] = $type; - if ($type != 'integer') { - // Only support the rounding behavior option for integers. + // Only support the rounding behavior option for integers. + if ($type == 'integer') { + $element_info['parameter']['rounding_behavior'] = array( + 'type' => 'token', + 'label' => t('Rounding behavior'), + 'description' => t('The rounding behavior the conversion should use.'), + 'options list' => 'rules_action_data_convert_rounding_behavior_options', + 'restriction' => 'input', + 'default value' => 'round', + 'optional' => TRUE, + ); + } + else { unset($element_info['parameter']['rounding_behavior']); } @@ -234,12 +265,18 @@ function rules_action_data_convert_info_alter(&$element_info, RulesAbstractPlugi case 'integer': $sources = array('decimal', 'text', 'token', 'uri', 'date', 'duration', 'boolean'); break; + case 'decimal': $sources = array('integer', 'text', 'token', 'uri', 'date', 'duration', 'boolean'); break; + case 'text': $sources = array('integer', 'decimal', 'token', 'uri', 'date', 'duration', 'boolean'); break; + + case 'token': + $sources = array('integer', 'decimal', 'text', 'uri', 'date', 'duration', 'boolean'); + break; } $element_info['parameter']['value']['type'] = $sources; } @@ -284,10 +321,12 @@ function rules_action_data_create_info_alter(&$element_info, RulesAbstractPlugin if (isset($type_info['property info'])) { // Add the data type's properties as parameters. foreach ($type_info['property info'] as $property => $property_info) { - // Prefix parameter names to avoid name clashes with existing parameters. - $element_info['parameter']['param_' . $property] = array_intersect_key($property_info, array_flip(array('type', 'label'))); + // Prefix parameter names to avoid name clashes with + // existing parameters. + $element_info['parameter']['param_' . $property] = array_intersect_key($property_info, array_flip(array('type', 'label', 'allow null'))); if (empty($property_info['required'])) { $element_info['parameter']['param_' . $property]['optional'] = TRUE; + $element_info['parameter']['param_' . $property]['allow null'] = TRUE; } } } @@ -316,13 +355,17 @@ function rules_condition_data_is($data, $op, $value) { return (isset($data) && isset($value)) || (!isset($data) && !isset($value)); } return $data == $value; + case '<': return $data < $value; + case '>': return $data > $value; - // Note: This is deprecated by the text comparison condition and IN below. + + // Note: This is deprecated by the text comparison condition and IN below. case 'contains': return is_string($data) && strpos($data, $value) !== FALSE || is_array($data) && in_array($value, $data); + case 'IN': return is_array($value) && in_array($data, $value); } @@ -339,7 +382,7 @@ function rules_condition_data_is_info_alter(&$element_info, RulesAbstractPlugin if ($wrapper = $element->applyDataSelector($element->settings['data:select'])) { $info = $wrapper->info(); $element_info['parameter']['value']['type'] = $element->settings['op'] == 'IN' ? 'list<' . $wrapper->type() . '>' : $wrapper->type(); - $element_info['parameter']['value']['options list'] = !empty($info['options list']) ? 'rules_data_selector_options_list' : FALSE; + $element_info['parameter']['value']['options list'] = !empty($info['options list']) ? 'rules_data_selector_options_list' : FALSE; } } @@ -360,6 +403,22 @@ function rules_condition_data_list_contains($list, $item, $settings, $state) { return in_array($item, $list); } +/** + * Condition: List count comparison. + */ +function rules_condition_data_list_count_is($list, $op = '==', $value) { + switch ($op) { + case '==': + return count($list) == $value; + + case '<': + return count($list) < $value; + + case '>': + return count($list) > $value; + } +} + /** * Condition: Data value is empty. */ @@ -388,11 +447,18 @@ function rules_data_text_comparison($text, $text2, $op = 'contains') { switch ($op) { case 'contains': return strpos($text, $text2) !== FALSE; + case 'starts': return strpos($text, $text2) === 0; + case 'ends': - return strrpos($text, $text2) === (strlen($text) - strlen($text2)); + return strrpos($text, $text2) === (strlen($text) - strlen($text2)); + case 'regex': - return (bool) preg_match('/'. str_replace('/', '\\/', $text2) .'/', $text); + return (bool) preg_match('/' . str_replace('/', '\\/', $text2) . '/', $text); } } + +/** + * @} + */ diff --git a/sites/all/modules/contrib/admin/rules/modules/data.rules.inc b/sites/all/modules/contrib/admin/rules/modules/data.rules.inc index ebeb3380..8143e779 100644 --- a/sites/all/modules/contrib/admin/rules/modules/data.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/data.rules.inc @@ -1,14 +1,30 @@ array( + 'label' => t('Data'), + 'equals group' => t('Data'), + 'weight' => -50, + ), + ); +} + /** * Implements hook_rules_file_info() on behalf of the pseudo data module. + * * @see rules_core_modules() */ function rules_data_file_info() { @@ -17,6 +33,7 @@ function rules_data_file_info() { /** * Implements hook_rules_action_info() on behalf of the pseudo data module. + * * @see rules_core_modules() */ function rules_data_action_info() { @@ -147,7 +164,7 @@ function rules_data_action_info() { 'type' => 'unknown', 'label' => t('Value'), 'optional' => TRUE, - 'description' => t('Optionally, specify the initial value of the variable.') + 'description' => t('Optionally, specify the initial value of the variable.'), ), ), 'provides' => array( @@ -203,20 +220,10 @@ function rules_data_action_info() { 'restriction' => 'input', ), 'value' => array( - 'type' => array('decimal', 'integer', 'text'), + 'type' => array('decimal', 'integer', 'text', 'token'), 'label' => t('Value to convert'), 'default mode' => 'selector', ), - // For to-integer conversion only. - 'rounding_behavior' => array( - 'type' => 'token', - 'label' => t('Rounding behavior'), - 'description' => t('The rounding behavior the conversion should use.'), - 'options list' => 'rules_action_data_convert_rounding_behavior_options', - 'restriction' => 'input', - 'default value' => 'round', - 'optional' => TRUE, - ), ), 'provides' => array( 'conversion_result' => array( @@ -242,6 +249,7 @@ function rules_action_data_convert_types_options(RulesPlugin $element, $param_na 'decimal' => t('Decimal'), 'integer' => t('Integer'), 'text' => t('Text'), + 'token' => t('Token'), ); } @@ -298,7 +306,7 @@ function rules_action_data_set_form_alter(&$form, &$form_state, $options, RulesA else { // Change the data parameter to be not editable. $form['parameter']['data']['settings']['#access'] = FALSE; - // TODO: improve display + // @todo Improve display. $form['parameter']['data']['info'] = array( '#prefix' => '

', '#markup' => t('Selected data: %selector', array('%selector' => $element->settings['data:select'])), @@ -324,8 +332,7 @@ function rules_action_data_calc_form_alter(&$form, &$form_state, $options, Rules } /** - * Custom validate callback for entity create, add variable and data create - * action. + * Validate callback for entity create, add variable and data create actions. */ function rules_action_create_type_validate($element) { if (!isset($element->settings['type'])) { @@ -365,7 +372,6 @@ function rules_data_list_form_alter(&$form, &$form_state, $options, RulesAbstrac } } - /** * Form alter callback for actions relying on the entity type or the data type. */ @@ -396,7 +402,7 @@ function rules_action_type_form_alter(&$form, &$form_state, $options, RulesAbstr unset($form['submit']); unset($form['provides']); // Disable #ajax for the first step as it has troubles with lazy-loaded JS. - // @todo: Re-enable once JS lazy-loading is fixed in core. + // @todo Re-enable once JS lazy-loading is fixed in core. unset($form['parameter']['type']['settings']['type']['#ajax']); unset($form['reload']['#ajax']); } @@ -442,6 +448,8 @@ function rules_action_data_calc_operator_options(RulesPlugin $element, $param_na '-' => '( - )', '*' => '( * )', '/' => '( / )', + 'min' => 'min', + 'max' => 'max', ); // Only show +/- in case a date has been selected. if (($info = $element->getArgumentInfo('input_1')) && $info['type'] == 'date') { @@ -473,6 +481,7 @@ function rules_data_action_data_create_options() { /** * Implements hook_rules_condition_info() on behalf of the pseudo data module. + * * @see rules_core_modules() */ function rules_data_condition_info() { @@ -540,6 +549,32 @@ function rules_data_condition_info() { 'form_alter' => 'rules_data_list_form_alter', ), ), + 'list_count_is' => array( + 'label' => t('List count comparison'), + 'parameter' => array( + 'list' => array( + 'type' => 'list', + 'label' => t('List to check'), + 'description' => t('A multi value data element to have its count compared, specified by using a data selector, eg node:author:roles.'), + ), + 'op' => array( + 'type' => 'text', + 'label' => t('Operator'), + 'description' => t('The comparison operator.'), + 'optional' => TRUE, + 'default value' => '==', + 'options list' => 'rules_condition_data_list_count_is_operator_options', + 'restriction' => 'input', + ), + 'value' => array( + 'type' => 'integer', + 'label' => t('Count'), + 'description' => t('The count to compare the data count with.'), + ), + ), + 'group' => t('Data'), + 'base' => 'rules_condition_data_list_count_is', + ), 'text_matches' => array( 'label' => t('Text comparison'), 'parameter' => array( @@ -569,11 +604,13 @@ function rules_data_condition_info() { } /** + * Asserts the bundle of entities, if it's compared. + * * If the bundle is compared, add the metadata assertion so other elements * can make use of properties specific to the bundle. */ function rules_condition_data_is_assertions($element) { - // Assert the bundle of entities, if its compared. + // Assert the bundle of entities, if it's compared. if ($wrapper = $element->applyDataSelector($element->settings['data:select'])) { $info = $wrapper->info(); if (isset($info['parent']) && $info['parent'] instanceof EntityDrupalWrapper) { @@ -613,7 +650,7 @@ function rules_condition_data_is_form_alter(&$form, &$form_state, $options, Rule else { // Change the data parameter to be not editable. $form['parameter']['data']['settings']['#access'] = FALSE; - // TODO: improve display + // @todo Improve display. $form['parameter']['data']['info'] = array( '#prefix' => '

', '#markup' => t('Selected data: %selector', array('%selector' => $element->settings['data:select'])), @@ -704,6 +741,17 @@ function rules_data_selector_options_list(RulesAbstractPlugin $element) { } } +/** + * Options list callback for condition list_count_is. + */ +function rules_condition_data_list_count_is_operator_options() { + return array( + '==' => t('equals'), + '<' => t('is lower than'), + '>' => t('is greater than'), + ); +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/modules/entity.eval.inc b/sites/all/modules/contrib/admin/rules/modules/entity.eval.inc index 0f4c36af..edd18a40 100644 --- a/sites/all/modules/contrib/admin/rules/modules/entity.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/entity.eval.inc @@ -5,6 +5,7 @@ * Contains rules integration for entities needed during evaluation. * * @addtogroup rules + * * @{ */ @@ -65,10 +66,16 @@ function rules_action_entity_query_info_alter(&$element_info, RulesAbstractPlugi $element_info['parameter']['property']['options list'] = 'rules_action_entity_query_property_options_list'; if ($element->settings['property']) { - $wrapper = entity_metadata_wrapper($element->settings['type']); + $wrapper = rules_get_entity_metadata_wrapper_all_properties($element); if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) { - $element_info['parameter']['value']['type'] = $property->type(); - $element_info['parameter']['value']['options list'] = $property->optionsList() ? 'rules_action_entity_query_value_options_list' : FALSE; + $property_type = $property->type(); + // If the cardinality of the property > 1, i.e. of type 'list<{type}>', + // we will also accept a parameter of type {type}. + if (substr($property_type, 0, strlen('list<')) === 'list<' && substr($property_type, -strlen('>')) === '>') { + $property_type = array($property_type, substr($property_type, strlen('list<'), strlen($property_type) - strlen('list<>'))); + } + $element_info['parameter']['value']['type'] = $property_type; + $element_info['parameter']['value']['options list'] = $property->optionsList() ? 'rules_action_entity_query_value_options_list' : FALSE; } } } @@ -106,9 +113,10 @@ function rules_action_entity_create_info_alter(&$element_info, RulesAbstractPlug $info = $child->info(); if (!empty($info['required'])) { $info += array('type' => 'text'); - // Prefix parameter names to avoid name clashes with existing parameters. + // Prefix parameter names to avoid name clashes + // with existing parameters. $element_info['parameter']['param_' . $name] = array_intersect_key($info, array_flip(array('type', 'label', 'description'))); - $element_info['parameter']['param_' . $name]['options list'] = $child->optionsList() ? 'rules_action_entity_parameter_options_list' : FALSE; + $element_info['parameter']['param_' . $name]['options list'] = $child->optionsList() ? 'rules_action_entity_parameter_options_list' : FALSE; } } $element_info['provides']['entity_created']['type'] = $element->settings['type']; @@ -172,3 +180,7 @@ function rules_condition_entity_field_access(EntityDrupalWrapper $wrapper, $fiel $field = field_info_field($field_name); return !empty($field) && field_access($op, $field, $wrapper->type(), $wrapper->value(), $account = NULL); } + +/** + * @} + */ diff --git a/sites/all/modules/contrib/admin/rules/modules/entity.rules.inc b/sites/all/modules/contrib/admin/rules/modules/entity.rules.inc index 76ec4ee4..97b3f697 100644 --- a/sites/all/modules/contrib/admin/rules/modules/entity.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/entity.rules.inc @@ -1,22 +1,39 @@ array( + 'label' => t('Entities'), + 'equals group' => t('Entities'), + 'weight' => -50, + ), + ); +} + /** * Implements hook_rules_action_info() on behalf of the entity module. + * * @see rules_core_modules() */ function rules_entity_action_info() { @@ -227,7 +244,7 @@ function rules_action_entity_query_value_options_list(RulesAbstractPlugin $eleme // Get the possible values for the selected property. $element->settings += array('type' => NULL, 'property' => NULL); if ($element->settings['type'] && $element->settings['property']) { - $wrapper = entity_metadata_wrapper($element->settings['type']); + $wrapper = rules_get_entity_metadata_wrapper_all_properties($element); if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) { return $property->optionsList('view'); @@ -309,6 +326,7 @@ function rules_entity_action_access($type, $name) { /** * Implements hook_rules_condition_info() on behalf of the entity module. + * * @see rules_core_modules() */ function rules_entity_condition_info() { @@ -442,11 +460,17 @@ function rules_condition_entity_is_new_help() { * Returns options for choosing a field for the selected entity. */ function rules_condition_entity_has_field_options(RulesAbstractPlugin $element) { - $options = array(); - foreach (field_info_fields() as $field_name => $field) { - $options[$field_name] = $field_name; + // The field_info_field_map() function was introduced in Drupal 7.22. See + // https://www.drupal.org/node/1915646. + if (function_exists('field_info_field_map')) { + $fields = field_info_field_map(); } - return $options; + else { + $fields = field_info_fields(); + } + $field_list = drupal_map_assoc(array_keys($fields)); + ksort($field_list); + return $field_list; } /** @@ -546,12 +570,13 @@ function rules_condition_entity_is_of_bundle_form_alter(&$form, &$form_state, $o $form['reload']['#limit_validation_errors'] = array(array('parameter', 'entity')); unset($form['parameter']['type']); unset($form['reload']['#attributes']['class']); - // NO break; + // NO break. case 2: $form['negate']['#access'] = FALSE; unset($form['parameter']['bundle']); unset($form['submit']); break; + case 3: if (($info = $element->getArgumentInfo('entity')) && $info['type'] != 'entity') { // Hide the entity type parameter if not needed. diff --git a/sites/all/modules/contrib/admin/rules/modules/events.inc b/sites/all/modules/contrib/admin/rules/modules/events.inc index a0e3e234..01d8aedb 100644 --- a/sites/all/modules/contrib/admin/rules/modules/events.inc +++ b/sites/all/modules/contrib/admin/rules/modules/events.inc @@ -1,19 +1,24 @@ TRUE, - 'node' => TRUE, - 'user' => TRUE, - ); - if (isset($entity_types[$type])) { - rules_invoke_event($type . '_view', $entity, $view_mode); + switch ($type) { + case 'comment': + rules_invoke_event('comment_view--' . $entity->node_type, $entity, $view_mode); + rules_invoke_event('comment_view', $entity, $view_mode); + break; + + case 'node': + rules_invoke_event('node_view--' . $entity->type, $entity, $view_mode); + rules_invoke_event('node_view', $entity, $view_mode); + break; + + case 'user': + rules_invoke_event('user_view', $entity, $view_mode); + break; } } @@ -50,15 +61,26 @@ function rules_entity_view($entity, $type, $view_mode, $langcode) { * Implements hook_entity_presave(). */ function rules_entity_presave($entity, $type) { - $entity_types = array( - 'comment' => TRUE, - 'node' => TRUE, - 'taxonomy_term' => TRUE, - 'taxonomy_vocabulary' => TRUE, - 'user' => TRUE, - ); - if (isset($entity_types[$type])) { - rules_invoke_event($type . '_presave', $entity); + switch ($type) { + case 'comment': + rules_invoke_event('comment_presave--' . $entity->node_type, $entity); + rules_invoke_event('comment_presave', $entity); + break; + + case 'node': + rules_invoke_event('node_presave--' . $entity->type, $entity); + rules_invoke_event('node_presave', $entity); + break; + + case 'taxonomy_term': + rules_invoke_event('taxonomy_term_presave--' . $entity->vocabulary_machine_name, $entity); + rules_invoke_event('taxonomy_term_presave', $entity); + break; + + case 'taxonomy_vocabulary': + case 'user': + rules_invoke_event($type . '_presave', $entity); + break; } } @@ -66,15 +88,26 @@ function rules_entity_presave($entity, $type) { * Implements hook_entity_update(). */ function rules_entity_update($entity, $type) { - $entity_types = array( - 'comment' => TRUE, - 'node' => TRUE, - 'taxonomy_term' => TRUE, - 'taxonomy_vocabulary' => TRUE, - 'user' => TRUE, - ); - if (isset($entity_types[$type])) { - rules_invoke_event($type . '_update', $entity); + switch ($type) { + case 'comment': + rules_invoke_event('comment_update--' . $entity->node_type, $entity); + rules_invoke_event('comment_update', $entity); + break; + + case 'node': + rules_invoke_event('node_update--' . $entity->type, $entity); + rules_invoke_event('node_update', $entity); + break; + + case 'taxonomy_term': + rules_invoke_event('taxonomy_term_update--' . $entity->vocabulary_machine_name, $entity); + rules_invoke_event('taxonomy_term_update', $entity); + break; + + case 'taxonomy_vocabulary': + case 'user': + rules_invoke_event($type . '_update', $entity); + break; } } @@ -82,15 +115,26 @@ function rules_entity_update($entity, $type) { * Implements hook_entity_insert(). */ function rules_entity_insert($entity, $type) { - $entity_types = array( - 'comment' => TRUE, - 'node' => TRUE, - 'taxonomy_term' => TRUE, - 'taxonomy_vocabulary' => TRUE, - 'user' => TRUE, - ); - if (isset($entity_types[$type])) { - rules_invoke_event($type . '_insert', $entity); + switch ($type) { + case 'comment': + rules_invoke_event('comment_insert--' . $entity->node_type, $entity); + rules_invoke_event('comment_insert', $entity); + break; + + case 'node': + rules_invoke_event('node_insert--' . $entity->type, $entity); + rules_invoke_event('node_insert', $entity); + break; + + case 'taxonomy_term': + rules_invoke_event('taxonomy_term_insert--' . $entity->vocabulary_machine_name, $entity); + rules_invoke_event('taxonomy_term_insert', $entity); + break; + + case 'taxonomy_vocabulary': + case 'user': + rules_invoke_event($type . '_insert', $entity); + break; } } @@ -98,15 +142,26 @@ function rules_entity_insert($entity, $type) { * Implements hook_entity_delete(). */ function rules_entity_delete($entity, $type) { - $entity_types = array( - 'comment' => TRUE, - 'node' => TRUE, - 'taxonomy_term' => TRUE, - 'taxonomy_vocabulary' => TRUE, - 'user' => TRUE, - ); - if (isset($entity_types[$type])) { - rules_invoke_event($type . '_delete', $entity); + switch ($type) { + case 'comment': + rules_invoke_event('comment_delete--' . $entity->node_type, $entity); + rules_invoke_event('comment_delete', $entity); + break; + + case 'node': + rules_invoke_event('node_delete--' . $entity->type, $entity); + rules_invoke_event('node_delete', $entity); + break; + + case 'taxonomy_term': + rules_invoke_event('taxonomy_term_delete--' . $entity->vocabulary_machine_name, $entity); + rules_invoke_event('taxonomy_term_delete', $entity); + break; + + case 'taxonomy_vocabulary': + case 'user': + rules_invoke_event($type . '_delete', $entity); + break; } } @@ -124,8 +179,10 @@ function rules_user_logout($account) { rules_invoke_event('user_logout', $account); } -/** - * System events. Note that rules_init() is the main module file is used to +/* + * System events. + * + * Note that rules_init() is the main module file is used to * invoke the init event. */ @@ -147,7 +204,7 @@ function rules_watchdog($log_entry) { * Getter callback for the log entry message property. */ function rules_system_log_get_message($log_entry) { - return t($log_entry['message'], (array)$log_entry['variables']); + return t($log_entry['message'], (array) $log_entry['variables']); } /** diff --git a/sites/all/modules/contrib/admin/rules/modules/node.eval.inc b/sites/all/modules/contrib/admin/rules/modules/node.eval.inc index ee96e15e..49450dc9 100644 --- a/sites/all/modules/contrib/admin/rules/modules/node.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/node.eval.inc @@ -5,35 +5,137 @@ * Contains rules integration for the node module needed during evaluation. * * @addtogroup rules + * * @{ */ /** - * Condition: Check for selected content types + * Base class providing node condition defaults. */ -function rules_condition_node_is_of_type($node, $types) { - return in_array($node->type, $types); +abstract class RulesNodeConditionBase extends RulesConditionHandlerBase { + + public static function defaults() { + return array( + 'parameter' => array( + 'node' => array('type' => 'node', 'label' => t('Content')), + ), + 'category' => 'node', + 'access callback' => 'rules_node_integration_access', + ); + } + } /** - * Condition: Check if the node is published + * Condition: Check for selected content types. */ -function rules_condition_node_is_published($node) { - return $node->status == 1; +class RulesNodeConditionType extends RulesNodeConditionBase { + + /** + * Defines the condition. + */ + public static function getInfo() { + $info = self::defaults() + array( + 'name' => 'node_is_of_type', + 'label' => t('Content is of type'), + 'help' => t('Evaluates to TRUE if the given content is of one of the selected content types.'), + ); + $info['parameter']['type'] = array( + 'type' => 'list', + 'label' => t('Content types'), + 'options list' => 'node_type_get_names', + 'description' => t('The content type(s) to check for.'), + 'restriction' => 'input', + ); + return $info; + } + + /** + * Executes the condition. + */ + public function execute($node, $types) { + return in_array($node->type, $types); + } + + /** + * Provides the content type of a node as asserted metadata. + */ + public function assertions() { + return array('node' => array('bundle' => $this->element->settings['type'])); + } + } /** - * Condition: Check if the node is sticky + * Condition: Check if the node is published. */ -function rules_condition_node_is_sticky($node) { - return $node->sticky == 1; +class RulesNodeConditionPublished extends RulesNodeConditionBase { + + /** + * Defines the condition. + */ + public static function getInfo() { + return self::defaults() + array( + 'name' => 'node_is_published', + 'label' => t('Content is published'), + ); + } + + /** + * Executes the condition. + */ + public function execute($node) { + return $node->status == 1; + } + } /** - * Condition: Check if the node is promoted to the frontpage + * Condition: Check if the node is sticky. */ -function rules_condition_node_is_promoted($node) { - return $node->promote == 1; +class RulesNodeConditionSticky extends RulesNodeConditionBase { + + /** + * Defines the condition. + */ + public static function getInfo() { + return self::defaults() + array( + 'name' => 'node_is_sticky', + 'label' => t('Content is sticky'), + ); + } + + /** + * Executes the condition. + */ + public function execute($node) { + return $node->sticky == 1; + } + +} + +/** + * Condition: Check if the node is promoted to the frontpage. + */ +class RulesNodeConditionPromoted extends RulesNodeConditionBase { + + /** + * Defines the condition. + */ + public static function getInfo() { + return self::defaults() + array( + 'name' => 'node_is_promoted', + 'label' => t('Content is promoted to frontpage'), + ); + } + + /** + * Executes the condition. + */ + public function execute($node) { + return $node->promote == 1; + } + } /** diff --git a/sites/all/modules/contrib/admin/rules/modules/node.rules.inc b/sites/all/modules/contrib/admin/rules/modules/node.rules.inc index 87f8821e..97aa00ba 100644 --- a/sites/all/modules/contrib/admin/rules/modules/node.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/node.rules.inc @@ -1,12 +1,26 @@ array( + 'label' => t('Node'), + 'equals group' => t('Node'), + ), + ); +} + /** * Implements hook_rules_file_info() on behalf of the node module. */ @@ -21,25 +35,28 @@ function rules_node_event_info() { $items = array( 'node_insert' => array( 'label' => t('After saving new content'), - 'group' => t('Node'), + 'category' => 'node', 'variables' => rules_events_node_variables(t('created content')), 'access callback' => 'rules_node_integration_access', + 'class' => 'RulesNodeEventHandler', ), 'node_update' => array( 'label' => t('After updating existing content'), - 'group' => t('Node'), + 'category' => 'node', 'variables' => rules_events_node_variables(t('updated content'), TRUE), 'access callback' => 'rules_node_integration_access', + 'class' => 'RulesNodeEventHandler', ), 'node_presave' => array( 'label' => t('Before saving content'), - 'group' => t('Node'), + 'category' => 'node', 'variables' => rules_events_node_variables(t('saved content'), TRUE), 'access callback' => 'rules_node_integration_access', + 'class' => 'RulesNodeEventHandler', ), 'node_view' => array( 'label' => t('Content is viewed'), - 'group' => t('Node'), + 'category' => 'node', 'help' => t("Note that if drupal's page cache is enabled, this event won't be generated for pages served from cache."), 'variables' => rules_events_node_variables(t('viewed content')) + array( 'view_mode' => array( @@ -51,12 +68,14 @@ function rules_node_event_info() { ), ), 'access callback' => 'rules_node_integration_access', + 'class' => 'RulesNodeEventHandler', ), 'node_delete' => array( 'label' => t('After deleting content'), - 'group' => t('Node'), + 'category' => 'node', 'variables' => rules_events_node_variables(t('deleted content')), 'access callback' => 'rules_node_integration_access', + 'class' => 'RulesNodeEventHandler', ), ); // Specify that on presave the node is saved anyway. @@ -65,7 +84,7 @@ function rules_node_event_info() { } /** - * Returns some parameter suitable for using it with a node + * Returns some parameter suitable for using it with a node. */ function rules_events_node_variables($node_label, $update = FALSE) { $args = array( @@ -83,51 +102,6 @@ function rules_events_node_variables($node_label, $update = FALSE) { return $args; } -/** - * Implements hook_rules_condition_info() on behalf of the node module. - */ -function rules_node_condition_info() { - $defaults = array( - 'parameter' => array( - 'node' => array('type' => 'node', 'label' => t('Content')), - ), - 'group' => t('Node'), - 'access callback' => 'rules_node_integration_access', - ); - $items['node_is_of_type'] = $defaults + array( - 'label' => t('Content is of type'), - 'help' => t('Evaluates to TRUE if the given content is of one of the selected content types.'), - 'base' => 'rules_condition_node_is_of_type', - ); - $items['node_is_of_type']['parameter']['type'] = array( - 'type' => 'list', - 'label' => t('Content types'), - 'options list' => 'node_type_get_names', - 'description' => t('The content type(s) to check for.'), - 'restriction' => 'input', - ); - $items['node_is_published'] = $defaults + array( - 'label' => t('Content is published'), - 'base' => 'rules_condition_node_is_published', - ); - $items['node_is_sticky'] = $defaults + array( - 'label' => t('Content is sticky'), - 'base' => 'rules_condition_node_is_sticky', - ); - $items['node_is_promoted'] = $defaults + array( - 'label' => t('Content is promoted to frontpage'), - 'base' => 'rules_condition_node_is_promoted', - ); - return $items; -} - -/** - * Provides the content type of a node as asserted metadata. - */ -function rules_condition_node_is_of_type_assertions($element) { - return array('node' => array('bundle' => $element->settings['type'])); -} - /** * Implements hook_rules_action_info() on behalf of the node module. */ @@ -136,7 +110,7 @@ function rules_node_action_info() { 'parameter' => array( 'node' => array('type' => 'node', 'label' => t('Content'), 'save' => TRUE), ), - 'group' => t('Node'), + 'category' => 'node', 'access callback' => 'rules_node_admin_access', ); // Add support for hand-picked core actions. @@ -168,6 +142,22 @@ function rules_node_admin_access() { return user_access('administer nodes'); } +/** + * Event handler support node bundle event settings. + */ +class RulesNodeEventHandler extends RulesEventHandlerEntityBundle { + + /** + * Returns the label to use for the bundle property. + * + * @return string + */ + protected function getBundlePropertyLabel() { + return t('type'); + } + +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/modules/path.eval.inc b/sites/all/modules/contrib/admin/rules/modules/path.eval.inc index 9f74acd7..8c6e13d0 100644 --- a/sites/all/modules/contrib/admin/rules/modules/path.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/path.eval.inc @@ -5,6 +5,7 @@ * Contains rules integration for the path module needed during evaluation. * * @addtogroup rules + * * @{ */ @@ -74,9 +75,12 @@ function rules_condition_path_alias_exists($alias, $langcode = LANGUAGE_NONE) { } /** - * Cleans the given path by replacing non ASCII characters with the replacment character. + * Cleans the given path. * - * Path cleaning may be adapted by overriding the configuration variables + * A path is cleaned by replacing non ASCII characters in the path with the + * replacement character. + * + * Path cleaning may be customized by overriding the configuration variables: * @code rules_clean_path @endcode, * @code rules_path_replacement_char @endcode and * @code rules_path_transliteration @endcode diff --git a/sites/all/modules/contrib/admin/rules/modules/path.rules.inc b/sites/all/modules/contrib/admin/rules/modules/path.rules.inc index 2d62ec41..8f0eef6f 100644 --- a/sites/all/modules/contrib/admin/rules/modules/path.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/path.rules.inc @@ -1,9 +1,11 @@ array( 'type' => 'text', 'label' => t('Existing system path'), - 'description' => t('Specifies the existing path you wish to alias. For example: node/28, forum/1, taxonomy/term/1+2.') .' '. t('Leave it empty to delete URL aliases pointing to the given path alias.'), + 'description' => t('Specifies the existing path you wish to alias. For example: node/28, forum/1, taxonomy/term/1+2.') . ' ' . t('Leave it empty to delete URL aliases pointing to the given path alias.'), 'optional' => TRUE, ), 'alias' => array( 'type' => 'text', 'label' => t('URL alias'), - 'description' => t('Specify an alternative path by which this data can be accessed. For example, "about" for an about page. Use a relative path and do not add a trailing slash.') .' '. t('Leave it empty to delete URL aliases pointing to the given system path.'), + 'description' => t('Specify an alternative path by which this data can be accessed. For example, "about" for an about page. Use a relative path and do not add a trailing slash.') . ' ' . t('Leave it empty to delete URL aliases pointing to the given system path.'), 'optional' => TRUE, 'cleaning callback' => 'rules_path_clean_replacement_values', ), @@ -61,7 +63,7 @@ function rules_path_action_info() { 'alias' => array( 'type' => 'text', 'label' => t('URL alias'), - 'description' => t('Specify an alternative path by which the content can be accessed. For example, "about" for an about page. Use a relative path and do not add a trailing slash.') .' '. t('Leave it empty to delete the URL alias.'), + 'description' => t('Specify an alternative path by which the content can be accessed. For example, "about" for an about page. Use a relative path and do not add a trailing slash.') . ' ' . t('Leave it empty to delete the URL alias.'), 'optional' => TRUE, 'cleaning callback' => 'rules_path_clean_replacement_values', ), @@ -82,7 +84,7 @@ function rules_path_action_info() { 'alias' => array( 'type' => 'text', 'label' => t('URL alias'), - 'description' => t('Specify an alternative path by which the term can be accessed. For example, "content/drupal" for a Drupal term. Use a relative path and do not add a trailing slash.') .' '. t('Leave it empty to delete the URL alias.'), + 'description' => t('Specify an alternative path by which the term can be accessed. For example, "content/drupal" for a Drupal term. Use a relative path and do not add a trailing slash.') . ' ' . t('Leave it empty to delete the URL alias.'), 'optional' => TRUE, 'cleaning callback' => 'rules_path_clean_replacement_values', ), @@ -168,4 +170,4 @@ function rules_path_condition_info() { /** * @} - */ \ No newline at end of file + */ diff --git a/sites/all/modules/contrib/admin/rules/modules/php.eval.inc b/sites/all/modules/contrib/admin/rules/modules/php.eval.inc index 9e705465..ac400b08 100644 --- a/sites/all/modules/contrib/admin/rules/modules/php.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/php.eval.inc @@ -5,6 +5,7 @@ * Contains rules integration for the php module needed during evaluation. * * @addtogroup rules + * * @{ */ @@ -13,10 +14,21 @@ */ class RulesPHPEvaluator extends RulesDataInputEvaluator { - public static function access() { - return user_access('use PHP for settings'); + /** + * Overrides RulesDataProcessor::editAccess(). + */ + public function editAccess() { + return parent::editAccess() && (user_access('use PHP for settings') || drupal_is_cli()); } + /** + * Helper function to find variables in PHP code. + * + * @param string $text + * The PHP code. + * @param array $var_info + * Array with variable names as keys. + */ public static function getUsedVars($text, $var_info) { if (strpos($text, 'setting = self::getUsedVars($text, $var_info); } /** - * Evaluates PHP code contained in $text. This doesn't apply $options, thus - * the PHP code is responsible for behaving appropriately. + * Evaluates PHP code contained in $text. + * + * This method doesn't apply $options, thus the PHP code is responsible for + * behaving appropriately. */ public function evaluate($text, $options, RulesState $state) { $vars['eval_options'] = $options; @@ -46,6 +63,9 @@ class RulesPHPEvaluator extends RulesDataInputEvaluator { return rules_php_eval($text, rules_unwrap_data($vars)); } + /** + * Overrides RulesDataInputEvaluator::help(). + */ public static function help($var_info) { module_load_include('inc', 'rules', 'rules/modules/php.rules'); @@ -58,6 +78,7 @@ class RulesPHPEvaluator extends RulesDataInputEvaluator { return $render; } + } /** @@ -65,6 +86,9 @@ class RulesPHPEvaluator extends RulesDataInputEvaluator { */ class RulesPHPDataProcessor extends RulesDataProcessor { + /** + * Overrides RulesDataProcessor::form(). + */ protected static function form($settings, $var_info) { $settings += array('code' => ''); $form = array( @@ -84,14 +108,21 @@ class RulesPHPDataProcessor extends RulesDataProcessor { return $form; } - public static function access() { - return user_access('use PHP for settings'); + /** + * Overrides RulesDataProcessor::editAccess(). + */ + public function editAccess() { + return parent::editAccess() && (user_access('use PHP for settings') || drupal_is_cli()); } + /** + * Overrides RulesDataProcessor::process(). + */ public function process($value, $info, RulesState $state, RulesPlugin $element) { $value = isset($this->processor) ? $this->processor->process($value, $info, $state, $element) : $value; return rules_php_eval_return($this->setting['code'], array('value' => $value)); } + } /** @@ -108,11 +139,11 @@ function rules_execute_php_eval($code, $settings, $state, $element) { } /** - * Evalutes the given PHP code, with the given variables defined. + * Evaluates the given PHP code, with the given variables defined. * - * @param $code - * The PHP code to run, with - * @param $arguments + * @param string $code + * The PHP code to run, including + * @param array $arguments * Array containing variables to be extracted to the code. * * @return @@ -122,7 +153,7 @@ function rules_php_eval($code, $arguments = array()) { extract($arguments); ob_start(); - print eval('?>'. $code); + print eval('?>' . $code); $output = ob_get_contents(); ob_end_clean(); @@ -130,13 +161,15 @@ function rules_php_eval($code, $arguments = array()) { } /** - * Evalutes the given PHP code, with the given variables defined. This is like - * rules_php_eval() but does return the returned data from the PHP code. + * Evaluates the given PHP code, with the given variables defined. * - * @param $code - * The PHP code to run, without - * @param $arguments - * Array containing variables to be extracted to the code. + * This is like rules_php_eval(), but does return the returned data from + * the PHP code. + * + * @param string $code + * The PHP code to run, without + * @param array $arguments + * Array containing variables to be extracted to the code. * * @return * The return value of the evaled code. diff --git a/sites/all/modules/contrib/admin/rules/modules/php.rules.inc b/sites/all/modules/contrib/admin/rules/modules/php.rules.inc index 742b8ce8..d00c00ec 100644 --- a/sites/all/modules/contrib/admin/rules/modules/php.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/php.rules.inc @@ -1,9 +1,11 @@ array( 'class' => 'RulesPHPDataProcessor', - 'type' => array('text', 'token', 'decimal', 'integer', 'date', 'duration', 'boolean', 'uri'), + 'type' => array('text', 'token', 'decimal', 'integer', 'date', 'duration', 'boolean', 'uri'), 'weight' => 10, 'module' => 'php', ), @@ -124,8 +126,7 @@ function rules_php_evaluator_help($var_info, $action_help = FALSE) { $render['top'] = array( '#prefix' => '

', '#suffix' => '

', - '#markup' => t('PHP code inside of <?php ?> delimiters will be evaluated and replaced by its output. E.g. <? echo 1+1?> will be replaced by 2.') - . ' ' . t('Furthermore you can make use of the following variables:'), + '#markup' => t('PHP code inside of <?php ?> delimiters will be evaluated and replaced by its output. E.g. <? echo 1+1?> will be replaced by 2.') . ' ' . t('Furthermore you can make use of the following variables:'), ); $render['vars'] = array( '#theme' => 'table', @@ -136,7 +137,7 @@ function rules_php_evaluator_help($var_info, $action_help = FALSE) { $cache = rules_get_cache(); foreach ($var_info as $name => $info) { $row = array(); - $row[] = '$'. check_plain($name); + $row[] = '$' . check_plain($name); $label = isset($cache['data_info'][$info['type']]['label']) ? $cache['data_info'][$info['type']]['label'] : $info['type']; $row[] = check_plain(drupal_ucfirst($label)); $row[] = check_plain($info['label']); @@ -155,4 +156,4 @@ function rules_php_evaluator_help($var_info, $action_help = FALSE) { /** * @} - */ \ No newline at end of file + */ diff --git a/sites/all/modules/contrib/admin/rules/modules/rules_core.eval.inc b/sites/all/modules/contrib/admin/rules/modules/rules_core.eval.inc index 2738c9f4..c0e9f246 100644 --- a/sites/all/modules/contrib/admin/rules/modules/rules_core.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/rules_core.eval.inc @@ -5,6 +5,7 @@ * Contains rules core integration needed during evaluation. * * @addtogroup rules + * * @{ */ @@ -46,7 +47,7 @@ function rules_element_invoke_component($arguments, RulesPlugin $element) { $state->mergeSaveVariables($new_state, $component, $element->settings); $state->unblock($component); - // Cleanup the state, what saves not mergable variables now. + // Cleanup the state, what saves not mergeable variables now. $new_state->cleanup(); rules_log('Finished evaluation of @plugin %label.', $replacements, RulesLog::INFO, $component, FALSE); return $return; @@ -57,13 +58,18 @@ function rules_element_invoke_component($arguments, RulesPlugin $element) { } /** - * A class implementing a rules input evaluator processing date input. This is - * needed to treat relative date inputs for strtotime right, consider "now". + * A class implementing a rules input evaluator processing date input. + * + * This is needed to treat relative date inputs for strtotime() correctly. + * Consider for example "now". */ class RulesDateInputEvaluator extends RulesDataInputEvaluator { const DATE_REGEX_LOOSE = '/^(\d{4})-?(\d{2})-?(\d{2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})?)?$/'; + /** + * Overrides RulesDataInputEvaluator::prepare(). + */ public function prepare($text, $var_info) { if (is_numeric($text)) { // Let rules skip this input evaluators in case it's already a timestamp. @@ -71,6 +77,9 @@ class RulesDateInputEvaluator extends RulesDataInputEvaluator { } } + /** + * Overrides RulesDataInputEvaluator::evaluate(). + */ public function evaluate($text, $options, RulesState $state) { return self::gmstrtotime($text); } @@ -89,14 +98,19 @@ class RulesDateInputEvaluator extends RulesDataInputEvaluator { public static function isFixedDateString($date) { return is_string($date) && preg_match(self::DATE_REGEX_LOOSE, $date); } + } /** - * A class implementing a rules input evaluator processing URI inputs to make - * sure URIs are absolute and path aliases get applied. + * A class implementing a rules input evaluator processing URI inputs. + * + * Makes sure URIs are absolute and path aliases get applied. */ class RulesURIInputEvaluator extends RulesDataInputEvaluator { + /** + * Overrides RulesDataInputEvaluator::prepare(). + */ public function prepare($uri, $var_info) { if (!isset($this->processor) && valid_url($uri, TRUE)) { // Only process if another evaluator is used or the url is not absolute. @@ -104,6 +118,9 @@ class RulesURIInputEvaluator extends RulesDataInputEvaluator { } } + /** + * Overrides RulesDataInputEvaluator::evaluate(). + */ public function evaluate($uri, $options, RulesState $state) { if (!url_is_external($uri)) { // Extract the path and build the URL using the url() function, so URL @@ -119,6 +136,7 @@ class RulesURIInputEvaluator extends RulesDataInputEvaluator { } throw new RulesEvaluationException('Input evaluation generated an invalid URI.', array(), NULL, RulesLog::WARN); } + } /** @@ -126,6 +144,9 @@ class RulesURIInputEvaluator extends RulesDataInputEvaluator { */ class RulesDateOffsetProcessor extends RulesDataProcessor { + /** + * Overrides RulesDataProcessor::form(). + */ protected static function form($settings, $var_info) { $settings += array('value' => ''); $form = array( @@ -145,6 +166,9 @@ class RulesDateOffsetProcessor extends RulesDataProcessor { return $form; } + /** + * Overrides RulesDataProcessor::process(). + */ public function process($value, $info, RulesState $state, RulesPlugin $element) { $value = isset($this->processor) ? $this->processor->process($value, $info, $state, $element) : $value; return RulesDateOffsetProcessor::applyOffset($value, $this->setting['value']); @@ -178,6 +202,7 @@ class RulesDateOffsetProcessor extends RulesDataProcessor { return $timestamp + $offset; } } + } /** @@ -185,6 +210,9 @@ class RulesDateOffsetProcessor extends RulesDataProcessor { */ class RulesNumericOffsetProcessor extends RulesDataProcessor { + /** + * Overrides RulesDataProcessor::form(). + */ protected static function form($settings, $var_info) { $settings += array('value' => ''); $form = array( @@ -205,15 +233,20 @@ class RulesNumericOffsetProcessor extends RulesDataProcessor { return $form; } + /** + * Overrides RulesDataProcessor::process(). + */ public function process($value, $info, RulesState $state, RulesPlugin $element) { $value = isset($this->processor) ? $this->processor->process($value, $info, $state, $element) : $value; return $value + $this->setting['value']; } + } - /** - * A custom wrapper class for vocabularies that is capable of loading vocabularies by machine name. + * A custom wrapper class for vocabularies. + * + * This class is capable of loading vocabularies by machine name. */ class RulesTaxonomyVocabularyWrapper extends EntityDrupalWrapper { @@ -231,7 +264,7 @@ class RulesTaxonomyVocabularyWrapper extends EntityDrupalWrapper { } /** - * Overriden to permit machine names as values. + * Overridden to permit machine names as values. */ public function validate($value) { if (isset($value) && is_string($value)) { @@ -239,4 +272,9 @@ class RulesTaxonomyVocabularyWrapper extends EntityDrupalWrapper { } return parent::validate($value); } + } + +/** + * @} + */ diff --git a/sites/all/modules/contrib/admin/rules/modules/rules_core.rules.inc b/sites/all/modules/contrib/admin/rules/modules/rules_core.rules.inc index 9edf4467..01215d56 100644 --- a/sites/all/modules/contrib/admin/rules/modules/rules_core.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/rules_core.rules.inc @@ -1,13 +1,29 @@ array( + 'label' => t('Components'), + 'equals group' => t('Components'), + 'weight' => 50, + ), + ); +} + /** * Implements hook_rules_file_info() on behalf of the pseudo rules_core module. * @@ -107,6 +123,12 @@ function rules_rules_core_data_info() { 'group' => t('Entity'), 'is wrapped' => TRUE, ), + 'ip_address' => array( + 'label' => t('IP Address'), + 'parent' => 'text', + 'ui class' => 'RulesDataUIIPAddress', + 'token type' => 'rules_text', + ), ); foreach (entity_get_info() as $type => $info) { if (!empty($info['label'])) { @@ -117,6 +139,11 @@ function rules_rules_core_data_info() { 'group' => t('Entity'), 'ui class' => empty($info['exportable']) ? 'RulesDataUIEntity' : 'RulesDataUIEntityExportable', ); + // If this entity type serves as bundle for another one, provide an + // options list for selecting a bundle entity. + if (!empty($info['bundle of'])) { + $return[$type]['ui class'] = 'RulesDataUIBundleEntity'; + } } } @@ -167,13 +194,13 @@ function rules_rules_core_evaluator_info() { 'class' => 'RulesDateInputEvaluator', 'type' => 'date', 'weight' => -10, - ), + ), // Post-process any input value to absolute URIs. 'uri' => array( 'class' => 'RulesURIInputEvaluator', 'type' => 'uri', 'weight' => 50, - ), + ), ); } @@ -189,12 +216,12 @@ function rules_rules_core_data_processor_info() { 'class' => 'RulesDateOffsetProcessor', 'type' => 'date', 'weight' => -2, - ), + ), 'num_offset' => array( 'class' => 'RulesNumericOffsetProcessor', 'type' => array('integer', 'decimal'), 'weight' => -2, - ), + ), ); } @@ -282,7 +309,7 @@ function rules_element_invoke_component_validate(RulesPlugin $element) { } /** - * Implements the features export callback of the RulesPluginFeaturesIntegrationInterace. + * Implements the features export callback of the RulesPluginFeaturesIntegrationInterface. */ function rules_element_invoke_component_features_export(&$export, &$pipe, $module_name = '', $element) { // Add the used component to the pipe. diff --git a/sites/all/modules/contrib/admin/rules/modules/system.eval.inc b/sites/all/modules/contrib/admin/rules/modules/system.eval.inc index a15f217d..d784db80 100644 --- a/sites/all/modules/contrib/admin/rules/modules/system.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/system.eval.inc @@ -5,6 +5,7 @@ * Contains rules integration for the system module needed during evaluation. * * @addtogroup rules + * * @{ */ @@ -42,8 +43,8 @@ function rules_action_drupal_goto($url, $force = FALSE, $destination = FALSE) { if ($force && isset($_GET['destination'])) { unset($_GET['destination']); } - // We don't invoke drupal_goto() right now, as this would end the the current - // page execution unpredictly for modules. So we'll take over drupal_goto() + // We don't invoke drupal_goto() right now, as this would end the current + // page execution unpredictably for modules. So we'll take over drupal_goto() // calls from somewhere else via hook_drupal_goto_alter() and make sure // a drupal_goto() is invoked before the page is output with // rules_page_build(). @@ -106,9 +107,8 @@ function rules_action_mail_to_users_of_role($roles, $subject, $message, $from = $result = db_query('SELECT mail FROM {users} WHERE uid > 0'); } else { - $rids = implode(',', $roles); // Avoid sending emails to members of two or more target role groups. - $result = db_query('SELECT DISTINCT u.mail FROM {users} u INNER JOIN {users_roles} r ON u.uid = r.uid WHERE r.rid IN ('. $rids .')'); + $result = db_query('SELECT DISTINCT u.mail FROM {users} u INNER JOIN {users_roles} r ON u.uid = r.uid WHERE r.rid IN (:rids)', array(':rids' => $roles)); } // Now, actually send the mails. @@ -123,11 +123,17 @@ function rules_action_mail_to_users_of_role($roles, $subject, $message, $from = $message = array('result' => TRUE); foreach ($result as $row) { $message = drupal_mail('rules', $key, $row->mail, language_default(), $params, $from); - if (!$message['result']) { + // If $message['result'] is FALSE, then it's likely that email sending is + // failing at the moment, and we should just abort sending any more. If + // however, $message['result'] is NULL, then it's likely that a module has + // aborted sending this particular email to this particular user, and we + // should just keep on sending emails to the other users. + // For more information on the result value, see drupal_mail(). + if ($message['result'] === FALSE) { break; } } - if ($message['result']) { + if ($message['result'] !== FALSE) { $role_names = array_intersect_key(user_roles(TRUE), array_flip($roles)); watchdog('rules', 'Successfully sent email to the role(s) %roles.', array('%roles' => implode(', ', $role_names))); } @@ -136,7 +142,7 @@ function rules_action_mail_to_users_of_role($roles, $subject, $message, $from = /** * Implements hook_mail(). * - * Set's the message subject and body as configured. + * Sets the message subject and body as configured. */ function rules_mail($key, &$message, $params) { @@ -144,11 +150,25 @@ function rules_mail($key, &$message, $params) { $message['body'][] = $params['message']; } +/** + * Action: Block an IP address. + */ +function rules_action_block_ip($ip_address = NULL) { + if (empty($ip_address)) { + $ip_address = ip_address(); + } + db_insert('blocked_ips')->fields(array('ip' => $ip_address))->execute(); + watchdog('rules', 'Banned IP address %ip', array('%ip' => $ip_address)); +} + /** * A class implementing a rules input evaluator processing tokens. */ class RulesTokenEvaluator extends RulesDataInputEvaluator { + /** + * Overrides RulesDataInputEvaluator::prepare(). + */ public function prepare($text, $var_info) { $text = is_array($text) ? implode('', $text) : $text; // Skip this evaluator if there are no tokens. @@ -156,6 +176,8 @@ class RulesTokenEvaluator extends RulesDataInputEvaluator { } /** + * Evaluate tokens. + * * We replace the tokens on our own as we cannot use token_replace(), because * token usually assumes that $data['node'] is a of type node, which doesn't * hold in general in our case. @@ -245,6 +267,7 @@ class RulesTokenEvaluator extends RulesDataInputEvaluator { } return $render; } + } /** diff --git a/sites/all/modules/contrib/admin/rules/modules/system.rules.inc b/sites/all/modules/contrib/admin/rules/modules/system.rules.inc index fcd4f808..3f41dd4e 100644 --- a/sites/all/modules/contrib/admin/rules/modules/system.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/system.rules.inc @@ -1,9 +1,11 @@ 'rules_action_mail_to_users_of_role', 'access callback' => 'rules_system_integration_access', ), + 'block_ip' => array( + 'label' => t('Block IP address'), + 'group' => t('System'), + 'parameter' => array( + 'ip_address' => array( + 'type' => 'ip_address', + 'label' => t('IP address'), + 'description' => t('If not provided, the IP address of the current user will be used.'), + 'optional' => TRUE, + 'default value' => NULL, + ), + ), + 'base' => 'rules_action_block_ip', + 'access callback' => 'rules_system_integration_access', + ), ); } @@ -279,7 +298,7 @@ function rules_system_evaluator_info() { 'class' => 'RulesTokenEvaluator', 'type' => array('text', 'uri', 'list', 'list'), 'weight' => 0, - ), + ), ); } diff --git a/sites/all/modules/contrib/admin/rules/modules/taxonomy.rules.inc b/sites/all/modules/contrib/admin/rules/modules/taxonomy.rules.inc index 7644aea3..69c3c3ec 100644 --- a/sites/all/modules/contrib/admin/rules/modules/taxonomy.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/taxonomy.rules.inc @@ -1,9 +1,11 @@ t('Taxonomy'), 'access callback' => 'rules_taxonomy_term_integration_access', 'module' => 'taxonomy', + 'class' => 'RulesTaxonomyEventHandler', ); $defaults_vocab = array( 'group' => t('Taxonomy'), @@ -32,14 +35,26 @@ function rules_taxonomy_event_info() { 'label' => t('After updating an existing term'), 'variables' => array( 'term' => array('type' => 'taxonomy_term', 'label' => t('updated term')), - 'term_unchanged' => array('type' => 'taxonomy_term', 'label' => t('unchanged term'), 'handler' => 'rules_events_entity_unchanged'), + 'term_unchanged' => array( + 'type' => 'taxonomy_term', + 'label' => t('unchanged term'), + 'handler' => 'rules_events_entity_unchanged', + ), ), ), 'taxonomy_term_presave' => $defaults_term + array( 'label' => t('Before saving a taxonomy term'), 'variables' => array( - 'term' => array('type' => 'taxonomy_term', 'label' => t('saved term'), 'skip save' => TRUE), - 'term_unchanged' => array('type' => 'taxonomy_term', 'label' => t('unchanged term'), 'handler' => 'rules_events_entity_unchanged'), + 'term' => array( + 'type' => 'taxonomy_term', + 'label' => t('saved term'), + 'skip save' => TRUE, + ), + 'term_unchanged' => array( + 'type' => 'taxonomy_term', + 'label' => t('unchanged term'), + 'handler' => 'rules_events_entity_unchanged', + ), ), ), 'taxonomy_term_delete' => $defaults_term + array( @@ -57,21 +72,39 @@ function rules_taxonomy_event_info() { 'taxonomy_vocabulary_update' => $defaults_vocab + array( 'label' => t('After updating an existing vocabulary'), 'variables' => array( - 'vocabulary' => array('type' => 'taxonomy_vocabulary', 'label' => t('updated vocabulary')), - 'vocabulary_unchanged' => array('type' => 'taxonomy_vocabulary', 'label' => t('unchanged vocabulary'), 'handler' => 'rules_events_entity_unchanged'), + 'vocabulary' => array( + 'type' => 'taxonomy_vocabulary', + 'label' => t('updated vocabulary'), + ), + 'vocabulary_unchanged' => array( + 'type' => 'taxonomy_vocabulary', + 'label' => t('unchanged vocabulary'), + 'handler' => 'rules_events_entity_unchanged', + ), ), ), 'taxonomy_vocabulary_presave' => $defaults_vocab + array( 'label' => t('Before saving a vocabulary'), 'variables' => array( - 'vocabulary' => array('type' => 'taxonomy_vocabulary', 'label' => t('saved vocabulary'), 'skip save' => TRUE), - 'vocabulary_unchanged' => array('type' => 'taxonomy_vocabulary', 'label' => t('unchanged vocabulary'), 'handler' => 'rules_events_entity_unchanged'), + 'vocabulary' => array( + 'type' => 'taxonomy_vocabulary', + 'label' => t('saved vocabulary'), + 'skip save' => TRUE, + ), + 'vocabulary_unchanged' => array( + 'type' => 'taxonomy_vocabulary', + 'label' => t('unchanged vocabulary'), + 'handler' => 'rules_events_entity_unchanged', + ), ), ), 'taxonomy_vocabulary_delete' => $defaults_vocab + array( 'label' => t('After deleting a vocabulary'), 'variables' => array( - 'vocabulary' => array('type' => 'taxonomy_vocabulary', 'label' => t('deleted vocabulary')), + 'vocabulary' => array( + 'type' => 'taxonomy_vocabulary', + 'label' => t('deleted vocabulary'), + ), ), ), ); @@ -95,6 +128,23 @@ function rules_taxonomy_vocabulary_integration_access($type, $name) { } } +/** + * Event handler support taxonomy bundle event settings. + */ +class RulesTaxonomyEventHandler extends RulesEventHandlerEntityBundle { + + /** + * Returns the label to use for the bundle property. + * + * @return string + * The label to use for the bundle property. + */ + protected function getBundlePropertyLabel() { + return t('vocabulary'); + } + +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/modules/user.eval.inc b/sites/all/modules/contrib/admin/rules/modules/user.eval.inc index 57e255c1..395c454c 100644 --- a/sites/all/modules/contrib/admin/rules/modules/user.eval.inc +++ b/sites/all/modules/contrib/admin/rules/modules/user.eval.inc @@ -5,11 +5,12 @@ * Contains rules integration for the user module needed during evaluation. * * @addtogroup rules + * * @{ */ /** - * Condition user: condition to check whether user has particular roles + * Condition user: condition to check whether user has particular roles. */ function rules_condition_user_has_role($account, $roles, $operation = 'AND') { switch ($operation) { @@ -43,7 +44,7 @@ function rules_condition_user_is_blocked($account) { */ function rules_action_user_add_role($account, $roles) { if ($account->uid || !empty($account->is_new)) { - // Get role list (minus the anonymous) + // Get role list (minus the anonymous). $role_list = user_roles(TRUE); foreach ($roles as $rid) { @@ -97,6 +98,38 @@ function rules_action_user_unblock($account) { $account->status = 1; } +/** + * Action: Send a user account e-mail. + */ +function rules_action_user_send_account_email($account, $email_type) { + // If we received an authenticated user account... + if (!empty($account->uid)) { + module_load_include('inc', 'rules', 'modules/user.rules'); + $types = rules_user_account_email_options_list(); + + // Attempt to send the account e-mail. + // This code is adapted from _user_mail_notify(). + $params = array('account' => $account); + $language = user_preferred_language($account); + $mail = drupal_mail('user', $email_type, $account->mail, $language, $params); + if ($email_type == '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); + } + + $result = empty($mail) ? NULL : $mail['result']; + + // Log the success or failure. + if ($result) { + watchdog('rules', '%type e-mail sent to %recipient.', array('%type' => $types[$email_type], '%recipient' => $account->mail)); + } + else { + watchdog('rules', 'Failed to send %type e-mail to %recipient.', array('%type' => $types[$email_type], '%recipient' => $account->mail)); + } + } +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/modules/user.rules.inc b/sites/all/modules/contrib/admin/rules/modules/user.rules.inc index f5148e41..9feb1ce6 100644 --- a/sites/all/modules/contrib/admin/rules/modules/user.rules.inc +++ b/sites/all/modules/contrib/admin/rules/modules/user.rules.inc @@ -1,9 +1,11 @@ 'text', 'label' => t('view mode'), 'options list' => 'rules_get_entity_view_modes', + // Add the entity-type for the options list callback. + 'options list entity type' => 'user', ), ), 'access callback' => 'rules_user_integration_access', @@ -88,7 +92,8 @@ function rules_user_event_info() { /** * Options list for user cancel methods. - * @todo: Use for providing a user_cancel action. + * + * @todo Use for providing a user_cancel action. */ function rules_user_cancel_methods() { module_load_include('inc', 'user', 'user.pages'); @@ -171,7 +176,7 @@ function rules_user_condition_operations() { */ function rules_user_action_info() { $defaults = array( - 'parameter' => array( + 'parameter' => array( 'account' => array( 'type' => 'user', 'label' => t('User'), @@ -196,7 +201,7 @@ function rules_user_action_info() { 'base' => 'rules_action_user_remove_role', ); $defaults = array( - 'parameter' => array( + 'parameter' => array( 'account' => array( 'type' => 'user', 'label' => t('User'), @@ -214,6 +219,24 @@ function rules_user_action_info() { 'label' => t('Unblock a user'), 'base' => 'rules_action_user_unblock', ); + $items['user_send_account_email'] = array( + 'label' => t('Send account e-mail'), + 'parameter' => array( + 'account' => array( + 'type' => 'user', + 'label' => t('Account'), + ), + 'email_type' => array( + 'type' => 'text', + 'label' => t('E-mail type'), + 'description' => t("Select the e-mail based on your site's account settings to send to the user."), + 'options list' => 'rules_user_account_email_options_list', + ), + ), + 'group' => t('User'), + 'base' => 'rules_action_user_send_account_email', + 'access callback' => 'rules_user_integration_access', + ); return $items; } @@ -231,6 +254,24 @@ function rules_user_roles_options_list($element) { return entity_metadata_user_roles('roles', array(), $element instanceof RulesConditionInterface ? 'view' : 'edit'); } +/** + * Options list callback for user account e-mail types. + * + * @see _user_mail_notify() + */ +function rules_user_account_email_options_list() { + return array( + 'register_admin_created' => t('Welcome (new user created by administrator)'), + 'register_no_approval_required' => t('Welcome (no approval required)'), + 'register_pending_approval' => t('Welcome (awaiting approval)'), + 'password_reset' => t('Password recovery'), + 'status_activated' => t('Account activation'), + 'status_blocked' => t('Account blocked'), + 'cancel_confirm' => t('Account cancellation confirmation'), + 'status_canceled' => t('Account canceled'), + ); +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/rules.api.php b/sites/all/modules/contrib/admin/rules/rules.api.php index 86b670c2..f8af7bd4 100644 --- a/sites/all/modules/contrib/admin/rules/rules.api.php +++ b/sites/all/modules/contrib/admin/rules/rules.api.php @@ -2,19 +2,20 @@ /** * @file + * Documentation for hooks provided by the Rules API. + * * This file contains no working PHP code; it exists to provide additional * documentation for doxygen as well as to document hooks in the standard * Drupal manner. */ - /** * @defgroup rules Rules module integrations. * * Module integrations with the rules module. * * The Rules developer documentation describes how modules can integrate with - * rules: http://drupal.org/node/298486. + * rules: https://www.drupal.org/node/298486. */ /** @@ -30,7 +31,11 @@ * placed into the file MODULENAME.rules.inc, which gets automatically included * when the hook is invoked. * - * @return + * However, as an alternative to implementing this hook, class based plugin + * handlers may be provided by implementing RulesActionHandlerInterface. See + * the interface for details. + * + * @return array * An array of information about the module's provided rules actions. * The array contains a sub-array for each action, with the action name as * the key. Actions names may only contain lowercase alpha-numeric characters @@ -62,7 +67,7 @@ * - 'access callback': (optional) A callback which has to return whether the * currently logged in user is allowed to configure this action. See * rules_node_integration_access() for an example callback. - * Each 'parameter' array may contain the following properties: + * Each 'parameter' array may contain the following properties: * - label: The label of the parameter. Start capitalized. Required. * - type: The rules data type of the parameter, which is to be passed to the * action. All types declared in hook_rules_data_info() may be specified, as @@ -110,7 +115,7 @@ * to clean inserted replacements; e.g. this is used by the token evaluator. * - wrapped: (optional) Set this to TRUE in case the data should be passed * wrapped. This only applies to wrapped data types, e.g. entities. - * Each 'provides' array may contain the following properties: + * Each 'provides' array may contain the following properties: * - label: The label of the variable. Start capitalized. Required. * - type: The rules data type of the variable. All types declared in * hook_rules_data_info() may be specified. Types may be parametrized e.g. @@ -118,21 +123,20 @@ * - save: (optional) If this is set to TRUE, the provided variable is saved * by rules when the rules evaluation ends. Only possible for savable data * types. Defaults to FALSE. + * The module has to provide an implementation for each action, being a + * function named as specified in the 'base' key or for the execution callback. + * All other possible callbacks are optional. + * Supported action callbacks by rules are defined and documented in the + * RulesPluginImplInterface. However any module may extend the action plugin + * based upon a defined interface using hook_rules_plugin_info(). All methods + * defined in those interfaces can be overridden by the action implementation. + * The callback implementations for those interfaces may reside in any file + * specified in hook_rules_file_info(). * - * The module has to provide an implementation for each action, being a - * function named as specified in the 'base' key or for the execution callback. - * All other possible callbacks are optional. - * Supported action callbacks by rules are defined and documented in the - * RulesPluginImplInterface. However any module may extend the action plugin - * based upon a defined interface using hook_rules_plugin_info(). All methods - * defined in those interfaces can be overridden by the action implementation. - * The callback implementations for those interfaces may reside in any file - * specified in hook_rules_file_info(). - * - * @see hook_rules_file_info() - * @see rules_action_execution_callback() - * @see hook_rules_plugin_info() - * @see RulesPluginImplInterface + * @see hook_rules_file_info() + * @see rules_action_execution_callback() + * @see hook_rules_plugin_info() + * @see RulesPluginImplInterface */ function hook_rules_action_info() { return array( @@ -151,6 +155,60 @@ function hook_rules_action_info() { ); } +/** + * Define categories for Rules items, e.g. actions, conditions or events. + * + * Categories are similar to the previously used 'group' key in e.g. + * hook_rules_action_info(), but have a machine name and some more optional + * keys like a weight, or an icon. + * + * For best compatibility, modules may keep using the 'group' key for referring + * to categories. However, if a 'group' key and a 'category' is given the group + * will be treated as grouping in the given category (e.g. group "paypal" in + * category "commerce payment"). + * + * @return array + * An array of information about the module's provided categories. + * The array contains a sub-array for each category, with the category name as + * the key. Names may only contain lowercase alpha-numeric characters + * and underscores and should be prefixed with the providing module name. + * Possible attributes for each sub-array are: + * - label: The label of the category. Start capitalized. Required. + * - weight: (optional) A weight for sorting the category. Defaults to 0. + * - equals group: (optional) For BC, categories may be defined that equal + * a previously used 'group'. + * - icon: (optional) The file path of an icon to use, relative to the module + * or specified icon path. The icon should be a transparent SVG containing + * no colors (only #fff). See https://www.drupal.org/node/2090265 for + * instructions on how to create a suitable icon. + * Note that the icon is currently not used by Rules, however other UIs + * building upon Rules (like fluxkraft) do, and future releases of Rules + * might do as well. Consequently, the definition of an icon is optional. + * However, if both an icon font and icon is given, the icon is preferred. + * - icon path: (optional) The base path for the icon. Defaults to the + * providing module's directory. + * - icon font class: (optional) An icon font class referring to a suitable + * icon. Icon font class names should map to the ones as defined by Font + * Awesome, while themes might want to choose to provide another icon font. + * See http://fortawesome.github.io/Font-Awesome/cheatsheet/. + * - icon background color: (optional) The color used as icon background. + * Should have a high contrast to white. Defaults to #ddd. + */ +function hook_rules_category_info() { + return array( + 'rules_data' => array( + 'label' => t('Data'), + 'equals group' => t('Data'), + 'weight' => -50, + ), + 'fluxtwitter' => array( + 'label' => t('Twitter'), + 'icon font class' => 'icon-twitter', + 'icon font background color' => '#30a9fd', + ), + ); +} + /** * Specify files containing rules integration code. * @@ -163,13 +221,34 @@ function hook_rules_action_info() { * plugin method callbacks in any file without having to care about file * inclusion. * - * @return + * @return array * An array of file names without the file ending which defaults to '.inc'. */ function hook_rules_file_info() { return array('yourmodule.rules-eval'); } +/** + * Specifies directories for class-based plugin handler discovery. + * + * Implementing this hook is not a requirement, it is just one option to load + * the files containing the classes during discovery - see + * rules_discover_plugins(). + * + * @return string|array + * A directory relative to the module directory, which holds the files + * containing rules plugin handlers, or multiple directories keyed by the + * module the directory is contained in. + * All files in those directories having a 'php' or 'inc' file extension will + * be loaded during discovery. Optionally, wildcards ('*') may be used to + * match multiple directories. + * + * @see rules_discover_plugins() + */ +function hook_rules_directory() { + return 'lib/Drupal/fluxtwitter/Rules/*'; +} + /** * The execution callback for an action. * @@ -180,10 +259,11 @@ function hook_rules_file_info() { * The callback gets arguments passed as described as parameter in * hook_rules_action_info() as well as an array containing the action's * configuration settings. - * @return - * The action may return an array containg parameter or provided variables + * + * @return array + * The action may return an array containing parameter or provided variables * with their names as key. This is used update the value of a parameter or to - * provdide the value for a provided variable. + * provide the value for a provided variable. * Apart from that any parameters which have the key 'save' set to TRUE will * be remembered to be saved by rules unless the action returns FALSE. * Conditions have to return a boolean value in any case. @@ -203,6 +283,10 @@ function rules_action_execution_callback($node, $title, $settings) { * placed into the file MODULENAME.rules.inc, which gets automatically included * when the hook is invoked. * + * However, as an alternative to implementing this hook, class based plugin + * handlers may be provided by implementing RulesConditionHandlerInterface. See + * the interface for details. + * * Adding conditions works exactly the same way as adding actions, with the * exception that conditions can't provide variables and cannot save parameters. * Thus the 'provides' attribute is not supported. Furthermore the condition @@ -234,7 +318,11 @@ function hook_rules_condition_info() { * usually it's invoked directly from the providing module but wrapped by a * module_exists('rules') check. * - * @return + * However, as an alternative to implementing this hook, class based event + * handlers may be provided by implementing RulesEventHandlerInterface. See + * the interface for details. + * + * @return array * An array of information about the module's provided rules events. The array * contains a sub-array for each event, with the event name as the key. The * name may only contain lower case alpha-numeric characters and underscores @@ -244,13 +332,18 @@ function hook_rules_condition_info() { * - group: A group for this element, used for grouping the events in the * interface. Should start with a capital letter and be translated. * Required. - * - 'access callback': An callback, which has to return whether the + * - class: (optional) An event handler class implementing the + * RulesEventHandlerInterface. If none is specified the + * RulesEventDefaultHandler class will be used. While the default event + * handler has no settings, custom event handlers may be implemented to + * to make an event configurable. See RulesEventHandlerInterface. + * - access callback: (optional) An callback, which has to return whether the * currently logged in user is allowed to configure rules for this event. * Access should be only granted, if the user at least may access all the - * variables provided by the event. Optional. - * - help: A help text for rules reaction on this event. - * - variables: An array describing all variables that are available for - * elements reaction on this event. Optional. Each variable has to be + * variables provided by the event. + * - help: (optional) A help text for rules reaction on this event. + * - variables: (optional) An array describing all variables that are + * available for elements reacting on this event. Each variable has to be * described by a sub-array with the possible attributes: * - label: The label of the variable. Start capitalized. Required. * - type: The rules data type of the variable. All types declared in @@ -262,17 +355,17 @@ function hook_rules_condition_info() { * - 'options list': (optional) A callback that returns an array of possible * values for this variable as specified for entity properties at * hook_entity_property_info(). - * - 'skip save': If the variable is saved after the event has occurred - * anyway, set this to TRUE. So rules won't save the variable a second - * time. Optional, defaults to FALSE. - * - handler: A handler to load the actual variable value. This is useful - * for lazy loading variables. The handler gets all so far available - * variables passed in the order as defined. Optional. Also see - * http://drupal.org/node/884554. + * - 'skip save': (optional) If the variable is saved after the event has + * occurred anyway, set this to TRUE. So rules won't save the variable a + * second time. Defaults to FALSE. + * - handler: (optional) A handler to load the actual variable value. This + * is useful for lazy loading variables. The handler gets all so far + * available variables passed in the order as defined. Also see + * https://www.drupal.org/node/884554. * Note that for lazy-loading entities just the entity id may be passed * as variable value, so a handler is not necessary in that case. * - * @see rules_invoke_event() + * @see rules_invoke_event() */ function hook_rules_event_info() { $items = array( @@ -321,8 +414,7 @@ function hook_rules_event_info() { * module. * For a list of data types defined by rules see rules_rules_core_data_info(). * - * - * @return + * @return array * An array of information about the module's provided data types. The array * contains a sub-array for each data type, with the data type name as the * key. The name may only contain lower case alpha-numeric characters and @@ -338,7 +430,9 @@ function hook_rules_event_info() { * configuration UI to configure parameters of this type. The given class * must extend RulesDataUI and may implement the * RulesDataDirectInputFormInterface in order to allow the direct data input - * configuration mode. Defaults to RulesDataUI. + * configuration mode. For supporting selecting values from options lists, + * the UI class may implement RulesDataInputOptionsListInterface also. + * Defaults to RulesDataUI. * - wrap: (optional) If set to TRUE, the data is wrapped internally using * wrappers provided by the entity API module. This is required for entities * and data structures to support selecting a property via the data selector @@ -352,7 +446,7 @@ function hook_rules_event_info() { * makes use of the class for wrapping the data of the given type. However * note that if data is already wrapped when it is passed to Rules, the * existing wrappers will be kept. - * For modules implementing identifiable data types being non-entites the + * For modules implementing identifiable data types being non-entities the * class RulesIdentifiableDataWrapper is provided, which can be used as base * for a custom wrapper class. See RulesIdentifiableDataWrapper for details. * - property info: (optional) May be used for non-entity data structures to @@ -376,9 +470,9 @@ function hook_rules_event_info() { * - cleaning callback: (optional) A callback that input evaluators may use * to clean inserted replacements; e.g. this is used by the token evaluator. * - * @see entity_metadata_wrapper() - * @see hook_rules_data_info_alter() - * @see rules_rules_core_data_info() + * @see entity_metadata_wrapper() + * @see hook_rules_data_info_alter() + * @see rules_rules_core_data_info() */ function hook_rules_data_info() { return array( @@ -403,7 +497,7 @@ function hook_rules_data_info() { * A rules configuration may consist of elements being instances of any rules * plugin. This hook can be used to define new or to extend rules plugins. * - * @return + * @return array * An array of information about the module's provided rules plugins. The * array contains a sub-array for each plugin, with the plugin name as the * key. The name may only contain lower case alpha-numeric characters, @@ -449,8 +543,8 @@ function hook_rules_data_info() { * of the 'or' plugin. Note that only uppercase values are allowed, as * lower case values are treated as action or condition exports. * - * @see class RulesPlugin - * @see hook_rules_plugin_info_alter() + * @see RulesPlugin + * @see hook_rules_plugin_info_alter() */ function hook_rules_plugin_info() { return array( @@ -489,7 +583,7 @@ function hook_rules_plugin_info() { * and help() could be overridden in order to control access permissions or to * provide some usage help. * - * @return + * @return array * An array of information about the module's provided input evaluators. The * array contains a sub-array for each evaluator, with the evaluator name as * the key. The name may only contain lower case alpha-numeric characters and @@ -503,8 +597,8 @@ function hook_rules_plugin_info() { * used. Defaults to 'text'. Multiple data types may be specified using an * array. * - * @see class RulesDataInputEvaluator - * @see hook_rules_evaluator_info_alter() + * @see RulesDataInputEvaluator + * @see hook_rules_evaluator_info_alter() */ function hook_rules_evaluator_info() { return array( @@ -512,7 +606,7 @@ function hook_rules_evaluator_info() { 'class' => 'RulesTokenEvaluator', 'type' => array('text', 'uri'), 'weight' => 0, - ), + ), ); } @@ -527,7 +621,7 @@ function hook_rules_evaluator_info() { * access() could be overridden in order to provide a configuration form or * to control access permissions. * - * @return + * @return array * An array of information about the module's provided data processors. The * array contains a sub-array for each processor, with the processor name as * the key. The name may only contain lower case alpha-numeric characters and @@ -542,8 +636,8 @@ function hook_rules_evaluator_info() { * used. Defaults to 'text'. Multiple data types may be specified using an * array. * - * @see class RulesDataProcessor - * @see hook_rules_data_processor_info_alter() + * @see RulesDataProcessor + * @see hook_rules_data_processor_info_alter() */ function hook_rules_data_processor_info() { return array( @@ -551,7 +645,7 @@ function hook_rules_data_processor_info() { 'class' => 'RulesDateOffsetProcessor', 'type' => 'date', 'weight' => -2, - ), + ), ); } @@ -564,10 +658,10 @@ function hook_rules_data_processor_info() { * @param $actions * The items of all modules as returned from hook_rules_action_info(). * - * @see hook_rules_action_info(). + * @see hook_rules_action_info() */ function hook_rules_action_info_alter(&$actions) { - // The rules action is more powerful, so hide the core action + // The rules action is more powerful, so hide the core action. unset($actions['rules_core_node_assign_owner_action']); // We prefer handling saving by rules - not by the user. unset($actions['rules_core_node_save_action']); @@ -597,7 +691,7 @@ function hook_rules_condition_info_alter(&$conditions) { * @param $events * The items of all modules as returned from hook_rules_event_info(). * - * @see hook_rules_event_info(). + * @see hook_rules_event_info() */ function hook_rules_event_info_alter(&$events) { // Change events. @@ -669,7 +763,7 @@ function hook_rules_data_processor_info_alter(&$processor_info) { * This hook is invoked during rules configuration loading, which is handled * by entity_load(), via classes RulesEntityController and EntityCRUDController. * - * @param $configs + * @param array $configs * An array of rules configurations being loaded, keyed by id. */ function hook_rules_config_load($configs) { @@ -707,7 +801,7 @@ function hook_rules_config_insert($config) { * The rules configuration that is being inserted or updated. */ function hook_rules_config_presave($config) { - if ($config->id && $config->module == 'yours') { + if ($config->id && $config->owner == 'your_module') { // Add custom condition. $config->conditon(/* Your condition */); } @@ -763,7 +857,7 @@ function hook_rules_config_execute($config) { * should be placed into the file MODULENAME.rules_defaults.inc, which gets * automatically included when the hook is invoked. * - * @return + * @return array * An array of rules configurations with the configuration names as keys. * * @see hook_default_rules_configuration_alter() @@ -772,6 +866,8 @@ function hook_rules_config_execute($config) { function hook_default_rules_configuration() { $rule = rules_reaction_rule(); $rule->label = 'example default rule'; + // Add rules tags. + $rule->tags = array('Admin', 'Tag2'); $rule->active = FALSE; $rule->event('node_update') ->condition(rules_condition('data_is', array('data:select' => 'node:status', 'value' => TRUE))->negate()) @@ -779,6 +875,7 @@ function hook_default_rules_configuration() { ->action('drupal_message', array('message' => 'A node has been updated.')); $configs['rules_test_default_1'] = $rule; + return $configs; } @@ -806,10 +903,10 @@ function hook_default_rules_configuration_alter(&$configs) { * This hook is invoked by the entity module after default rules configurations * have been rebuilt; i.e. defaults have been saved to the database. * - * @param $rules_configs + * @param array $rules_configs * The array of default rules configurations which have been inserted or * updated, keyed by name. - * @param $originals + * @param array $originals * An array of original rules configurations keyed by name; i.e. the rules * configurations before the current defaults have been applied. For inserted * rules configurations no original is available. @@ -888,7 +985,8 @@ function hook_rules_event_set_alter($event_name, RulesEventSet $event_set) { * @param $element * The element array of a configured condition or action which is to be * upgraded. - * @return + * + * @return string * The name of the action or condition which should be used. */ function hook_rules_action_base_upgrade_map_name($element) { @@ -896,13 +994,13 @@ function hook_rules_action_base_upgrade_map_name($element) { } /** - * D6 to D7 upgrade procedure hook for mapping action or condition configuration. + * D6 to D7 upgrade process hook for mapping action or condition configuration. * * During upgrading Drupal 6 rule configurations to Drupal 7 Rules is taking * care of upgrading the configuration of all known parameters, which only works * if the parameter name has not changed. * If something changed, this callback can be used to properly apply the - * configruation of the Drupal 6 action ($element) to the Drupal 7 version + * configuration of the Drupal 6 action ($element) to the Drupal 7 version * ($target). * * This is no real hook, but a callback that is invoked for each Drupal 6 @@ -924,7 +1022,7 @@ function hook_rules_action_base_upgrade($element, RulesPlugin $target) { } /** - * D6 to D7 upgrade procedure hook for mapping action or condition configuration. + * D6 to D7 upgrade process hook for mapping action or condition configuration. * * A alter hook that is called after the action/condition specific callback for * each element of a configuration that is upgraded. @@ -967,6 +1065,39 @@ function hook_rules_ui_menu_alter(&$items, $base_path, $base_count) { ); } +/** + * Control access to Rules configurations. + * + * Modules may implement this hook if they want to have a say in whether or not + * a given user has access to perform a given operation on a Rules + * configuration. + * + * @param string $op + * The operation being performed. One of 'view', 'create', 'update' or + * 'delete'. + * @param $rules_config + * (optional) A Rules configuration to check access for. If nothing is given, + * access for all Rules configurations is determined. + * @param $account + * (optional) The user to check for. If no account is passed, access is + * determined for the current user. + * + * @return bool|null + * Return TRUE to grant access, FALSE to explicitly deny access. Return NULL + * or nothing to not affect the operation. + * Access is granted as soon as a module grants access and no one denies + * access. Thus if no module explicitly grants access, access will be denied. + * + * @see rules_config_access() + */ +function hook_rules_config_access($op, $rules_config = NULL, $account = NULL) { + // Instead of returning FALSE return nothing, so others still can grant + // access. + if (isset($rules_config) && $rules_config->owner == 'mymodule' && user_access('my modules permission')) { + return TRUE; + } +} + /** * @} */ diff --git a/sites/all/modules/contrib/admin/rules/rules.drush.inc b/sites/all/modules/contrib/admin/rules/rules.drush.inc index dc30de4c..9d4c18dc 100644 --- a/sites/all/modules/contrib/admin/rules/rules.drush.inc +++ b/sites/all/modules/contrib/admin/rules/rules.drush.inc @@ -7,22 +7,29 @@ /** * Implements hook_drush_command(). - * - * @return - * An associative array describing your command(s). - * - * @see drush_parse_command() */ function rules_drush_command() { $items = array(); $items['rules-list'] = array( - 'description' => "List all the active and inactive rules for your site.", + 'description' => 'List all the active and inactive rules for your site.', 'drupal dependencies' => array('rules'), 'aliases' => array('rules'), + 'outputformat' => array( + 'default' => 'table', + 'pipe-format' => 'list', + 'field-labels' => array( + 'rule' => dt('Rule'), + 'label' => dt('Label'), + 'event' => dt('Event'), + 'active' => dt('Active'), + 'status' => dt('Status'), + ), + 'output-data-type' => 'format-table', + ), ); $items['rules-enable'] = array( - 'description' => "Enable a rule on your site.", + 'description' => 'Enable a rule on your site.', 'arguments' => array( 'rule' => 'Rule name to enable.', ), @@ -30,13 +37,34 @@ function rules_drush_command() { 'aliases' => array('re'), ); $items['rules-disable'] = array( - 'description' => "Disable a rule on your site.", + 'description' => 'Disable a rule on your site.', 'arguments' => array( 'rule' => 'Rule name to export.', ), 'drupal dependencies' => array('rules'), 'aliases' => array('rd'), ); + $items['rules-revert'] = array( + 'description' => 'Revert a rule to its original state on your site.', + 'arguments' => array( + 'rule' => 'Rule name to revert.', + ), + 'drupal dependencies' => array('rules'), + ); + $items['rules-delete'] = array( + 'description' => 'Delete a rule on your site.', + 'arguments' => array( + 'rule' => 'Rules name to delete.', + ), + 'drupal dependencies' => array('rules'), + ); + $items['rules-export'] = array( + 'description' => 'Export a rule.', + 'arguments' => array( + 'rule' => 'Rules name to export.', + ), + 'drupal dependencies' => array('rules'), + ); return $items; } @@ -46,12 +74,23 @@ function rules_drush_command() { */ function rules_drush_help($section) { switch ($section) { - case 'drush:rules': - return dt("List all the rules on your site."); + case 'drush:rules-list': + return dt('List all the rules on your site.'); + case 'drush:rules-enable': - return dt("Enable/activate a rule on your site."); + return dt('Enable/activate a rule on your site.'); + case 'drush:rules-disable': - return dt("Disable/deactivate a rule on your site."); + return dt('Disable/deactivate a rule on your site.'); + + case 'drush:rules-revert': + return dt('Revert a module-provided rule to its original state on your site.'); + + case 'drush:rules-delete': + return dt('Delete a rule on your site.'); + + case 'drush:rules-export': + return dt('Export a rule.'); } } @@ -60,27 +99,34 @@ function rules_drush_help($section) { */ function drush_rules_list() { $rules = rules_config_load_multiple(FALSE); - $rows = array(array(dt('Rule'), dt('Label'), dt('Event'), dt('Active'), dt('Status'))); + $rows = array(); foreach ($rules as $rule) { if (!empty($rule->name) && !empty($rule->label)) { $events = array(); $event_info = rules_fetch_data('event_info'); if ($rule instanceof RulesTriggerableInterface) { foreach ($rule->events() as $event_name) { - $event_info += array($event_name => array('label' => dt('Unknown event "!event_name"', array('!event_name' => $event_name)))); + $event_info += array( + $event_name => array( + 'label' => dt('Unknown event "!event_name"', array('!event_name' => $event_name)), + ), + ); $events[] = check_plain($event_info[$event_name]['label']); } } - $rows[] = array( - $rule->name, - $rule->label, - implode(', ', $events), - $rule->active ? dt('Enabled') : dt('Disabled'), - $rule->status ? theme('entity_status', array('status' => $rule->status, 'html' => FALSE)) : '', + $rows[$rule->name] = array( + 'rule' => $rule->name, + 'label' => $rule->label, + 'event' => implode(', ', $events), + 'active' => $rule->active ? dt('Enabled') : dt('Disabled'), + 'status' => $rule->status ? theme('entity_status', array('status' => $rule->status, 'html' => FALSE)) : '', ); } } - drush_print_table($rows, TRUE); + if (version_compare(DRUSH_VERSION, '6.0', '<')) { + drush_print_table($rows, TRUE); + } + return $rows; } /** @@ -132,3 +178,75 @@ function drush_rules_disable() { drush_log(dt('The rule "!name" is already disabled.', array('!name' => $rule_name)), 'warning'); } } + +/** + * Reverts a rule on the site. + */ +function drush_rules_revert() { + $args = func_get_args(); + $rule_name = (!empty($args) && is_array($args)) ? array_shift($args) : ''; + if (empty($rule_name)) { + return drush_set_error('', 'No rule name given.'); + } + + $rule = rules_config_load($rule_name); + if (empty($rule)) { + return drush_set_error('', dt('Could not load rule named "!rule-name".', array('!rule-name' => $rule_name))); + } + + if (($rule->status & ENTITY_OVERRIDDEN) == ENTITY_OVERRIDDEN) { + if (drush_confirm(dt('Are you sure you want to revert the rule named "!rule-name"? This action cannot be undone.', array('!rule-name' => $rule_name)))) { + $rule->delete(); + drush_log(dt('The rule "!name" has been reverted to its default state.', array('!name' => $rule_name)), 'success'); + } + else { + drush_user_abort(); + } + } + else { + drush_log(dt('The rule "!name" has not been overridden and can\'t be reverted.', array('!name' => $rule_name)), 'warning'); + } +} + +/** + * Deletes a rule on the site. + */ +function drush_rules_delete() { + $args = func_get_args(); + $rule_name = (!empty($args) && is_array($args)) ? array_shift($args) : ''; + if (empty($rule_name)) { + return drush_set_error('', 'No rule name given.'); + } + + $rule = rules_config_load($rule_name); + if (empty($rule)) { + return drush_set_error('', dt('Could not load rule named "!rule-name".', array('!rule-name' => $rule_name))); + } + + if (drush_confirm(dt('Are you sure you want to delete the rule named "!rule-name"? This action cannot be undone.', array('!rule-name' => $rule_name)))) { + $rule->delete(); + drush_log(dt('The rule "!name" has been deleted.', array('!name' => $rule_name)), 'success'); + } + else { + drush_user_abort(); + } +} + +/** + * Exports a single rule. + */ +function drush_rules_export() { + $args = func_get_args(); + $rule_name = (!empty($args) && is_array($args)) ? array_shift($args) : ''; + if (empty($rule_name)) { + return drush_set_error('', dt('No rule name given.')); + } + + $rule = rules_config_load($rule_name); + if (empty($rule)) { + return drush_set_error('', dt('Could not load rule named "!rule-name".', array('!rule-name' => $rule_name))); + } + + drush_print($rule->export()); + drush_log(dt('The rule "!name" has been exported.', array('!name' => $rule_name)), 'success'); +} diff --git a/sites/all/modules/contrib/admin/rules/rules.features.inc b/sites/all/modules/contrib/admin/rules/rules.features.inc index d34c8d60..e3224b56 100644 --- a/sites/all/modules/contrib/admin/rules/rules.features.inc +++ b/sites/all/modules/contrib/admin/rules/rules.features.inc @@ -2,11 +2,11 @@ /** * @file - * Provides Features integration for the Rules module, based upon the features - * integration provided by the Entity API. + * Provides Features integration for the Rules module. + * + * This code is based upon the features integration provided by the Entity API. */ - /** * Controller handling the features integration. */ @@ -24,7 +24,8 @@ class RulesFeaturesController extends EntityDefaultFeaturesController { /** * Generates the result for hook_features_export(). - * Overridden to add in rules specific stuff. + * + * Overridden to add in rules-specific stuff. */ public function export($data, &$export, $module_name = '') { $pipe = parent::export($data, $export, $module_name); @@ -34,28 +35,32 @@ class RulesFeaturesController extends EntityDefaultFeaturesController { // Add in plugin / element specific additions. $iterator = new RecursiveIteratorIterator($rules_config, RecursiveIteratorIterator::SELF_FIRST); foreach ($iterator as $element) { - if ($element->facesAs('RulesPluginFeaturesIntegrationInterace')) { - // Directly use __call() so we cann pass $export by reference. + if ($element->facesAs('RulesPluginFeaturesIntegrationInterface')) { + // Directly use __call() so we can pass $export by reference. $element->__call('features_export', array(&$export, &$pipe, $module_name)); } } } return $pipe; } + } /** * Default extension callback used as default for the abstract plugin class. - * Actions / conditions may override this with their own implementation, which + * + * Actions and conditions may override this with an implementation which * actually does something. * - * @see RulesPluginFeaturesIntegrationInterace + * @see RulesPluginFeaturesIntegrationInterface */ function rules_features_abstract_default_features_export(&$export, &$pipe, $module_name = '', $element) { - + // Do nothing. } /** + * Interface to give features access to the faces extensions mechanism. + * * Interface that allows rules plugins or actions/conditions to customize the * features export by implementing the interface using the faces extensions * mechanism. @@ -63,10 +68,24 @@ function rules_features_abstract_default_features_export(&$export, &$pipe, $modu * @see hook_rules_plugin_info() * @see hook_rules_action_info() */ -interface RulesPluginFeaturesIntegrationInterace { +interface RulesPluginFeaturesIntegrationInterface { /** * Allows customizing the features export for a given rule element. */ - function features_export(&$export, &$pipe, $module_name = ''); + public function features_export(&$export, &$pipe, $module_name = ''); + +} + +/** + * Interface for backwards compatibility with older versions of Rules. + * + * Mis-spelled interface provided so that contributed modules which were + * implementing the wrong spelling (corrected in Rules 7.x-2.12) will not stop + * working now that the interface is spelled correctly. + * + * @todo Remove this when we can be sure that no contributed modules are + * still using the wrong spelling. + */ +interface RulesPluginFeaturesIntegrationInterace extends RulesPluginFeaturesIntegrationInterface { } diff --git a/sites/all/modules/contrib/admin/rules/rules.info b/sites/all/modules/contrib/admin/rules/rules.info index e8e3a593..123805da 100644 --- a/sites/all/modules/contrib/admin/rules/rules.info +++ b/sites/all/modules/contrib/admin/rules/rules.info @@ -3,25 +3,33 @@ description = React on events and conditionally evaluate actions. package = Rules core = 7.x files[] = rules.features.inc -files[] = tests/rules.test files[] = includes/faces.inc files[] = includes/rules.core.inc +files[] = includes/rules.event.inc files[] = includes/rules.processor.inc files[] = includes/rules.plugins.inc files[] = includes/rules.state.inc +files[] = modules/comment.rules.inc +files[] = modules/node.eval.inc +files[] = modules/node.rules.inc files[] = modules/php.eval.inc files[] = modules/rules_core.eval.inc files[] = modules/system.eval.inc +files[] = modules/taxonomy.rules.inc files[] = ui/ui.controller.inc files[] = ui/ui.core.inc files[] = ui/ui.data.inc files[] = ui/ui.plugins.inc + +; Test cases +files[] = tests/rules.test +files[] = tests/rules_test.rules.inc + dependencies[] = entity_token dependencies[] = entity -; Information added by drupal.org packaging script on 2013-03-27 -version = "7.x-2.3" +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1364401818" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/rules.install b/sites/all/modules/contrib/admin/rules/rules.install index f49c78e6..b852bae1 100644 --- a/sites/all/modules/contrib/admin/rules/rules.install +++ b/sites/all/modules/contrib/admin/rules/rules.install @@ -1,16 +1,25 @@ condition('name', 'rules_debug_region_%', 'LIKE') + ->execute(); + cache_clear_all('variables', 'cache_bootstrap'); } /** @@ -46,7 +69,7 @@ function rules_schema() { 'description' => 'The label of the configuration.', 'default' => 'unlabeled', ), - 'plugin' => array( + 'plugin' => array( 'type' => 'varchar', 'length' => 127, 'not null' => TRUE, @@ -87,6 +110,13 @@ function rules_schema() { 'length' => 255, 'not null' => FALSE, ), + 'owner' => array( + 'description' => 'The name of the module via which the rule has been configured.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => 'rules', + ), 'access_exposed' => array( 'type' => 'int', 'not null' => TRUE, @@ -107,7 +137,7 @@ function rules_schema() { 'name' => array('name'), ), 'indexes' => array( - 'plugin' => array('plugin'), + 'plugin' => array('plugin', 'active'), ), ); $schema['rules_trigger'] = array( @@ -207,7 +237,7 @@ function rules_update_7200() { 'description' => 'The label of the configuration.', 'default' => 'unlabeled', ), - 'plugin' => array( + 'plugin' => array( 'type' => 'varchar', 'length' => 127, 'not null' => TRUE, @@ -440,3 +470,94 @@ function rules_update_7209() { 'description' => 'Whether to use a permission to control access for using components.', )); } + +/** + * Deletes the unused rules_empty_sets variable. + */ +function rules_update_7210() { + variable_del('rules_empty_sets'); +} + +/** + * Creates the "owner" column. + */ +function rules_update_7211() { + // Create a owner column. + if (!db_field_exists('rules_config', 'owner')) { + db_add_field('rules_config', 'owner', array( + 'description' => 'The name of the module via which the rule has been configured.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => 'rules', + )); + } +} + +/** + * Make sure registry gets rebuilt to avoid upgrade troubles. + */ +function rules_update_7212() { + // Make sure module information gets refreshed and registry is rebuilt. + drupal_static_reset('system_rebuild_module_data'); + registry_rebuild(); +} + +/** + * Recover the "owner" property for broken configurations. + */ +function rules_update_7213() { + $rows = db_select('rules_config', 'c') + ->fields('c') + ->condition('status', ENTITY_OVERRIDDEN) + ->condition('owner', 'rules', '<>') + ->execute() + ->fetchAllAssoc('id'); + + foreach ($rows as $id => $row) { + if ($row->module == $row->owner) { + db_update('rules_config') + ->condition('id', $id) + ->fields(array('owner' => 'rules')) + ->execute(); + } + } +} + +/** + * Switch out the rules_event_whitelist variable for a cache equivalent. + */ +function rules_update_7214() { + // Enable Rules if currently disabled so that this update won't fail. + $disable_rules = FALSE; + if (!module_exists('rules')) { + module_enable(array('rules')); + $disable_rules = TRUE; + } + // Set new event_whitelist cache cid. + rules_set_cache('rules_event_whitelist', variable_get('rules_event_whitelist', array())); + // Delete old conf variable. + variable_del('rules_event_whitelist'); + // Avoid any missing class errors. + registry_rebuild(); + // Clear and rebuild Rules caches. + // See: rules_admin_settings_cache_rebuild_submit. + rules_clear_cache(); + rules_get_cache(); + _rules_rebuild_component_cache(); + RulesEventSet::rebuildEventCache(); + // Disable Rules again if it was disabled before this update started. + if ($disable_rules) { + module_disable(array('rules')); + } +} + +/** + * Add an index for retrieving active config of a certain plugin. + */ +function rules_update_7215() { + if (db_index_exists('rules_config', 'plugin')) { + db_drop_index('rules_config', 'plugin'); + } + db_add_index('rules_config', 'plugin', array('plugin', 'active')); +} diff --git a/sites/all/modules/contrib/admin/rules/rules.module b/sites/all/modules/contrib/admin/rules/rules.module index daa0faa5..e713cd95 100644 --- a/sites/all/modules/contrib/admin/rules/rules.module +++ b/sites/all/modules/contrib/admin/rules/rules.module @@ -1,20 +1,66 @@ $group), $implementations); + } +} + +/** + * Implements hook_menu_get_item_alter(). + */ +function rules_menu_get_item_alter() { + // Make sure that event invocation is enabled before menu items are loaded. + // But make sure later calls to menu_get_item() won't automatically re-enabled + // the rules invocation. + // Example: modules that implement hook_entity_ENTITY_TYPE_load() might want + // to invoke Rules events in that load hook, which is also invoked for menu + // item loading. Since this can happen even before hook_init() we need to make + // sure that firing Rules events is enabled at that point. A typical use case + // for this is Drupal Commerce with commerce_cart_commerce_order_load(). + if (!drupal_static('rules_init', FALSE)) { + rules_event_invocation_enabled(TRUE); + } +} + /** * Implements hook_init(). */ function rules_init() { - module_load_include('inc', 'rules', 'modules/events'); + // See rules_menu_get_item_alter(). + $rules_init = &drupal_static(__FUNCTION__, FALSE); + $rules_init = TRUE; + // Enable event invocation once hook_init() was invoked for Rules. + rules_event_invocation_enabled(TRUE); rules_invoke_event('init'); } /** - * Returns an instance of the rules UI controller, which eases re-using the Rules UI. + * Returns an instance of the rules UI controller. * + * This function is for convenience, to ease re-using the Rules UI. * See the rules_admin.module for example usage. * * @return RulesUIController @@ -32,8 +78,9 @@ function rules_ui() { * * @param $name * The action's name. - * @param $settings + * @param array $settings * The action's settings array. + * * @return RulesAction */ function rules_action($name, $settings = array()) { @@ -45,8 +92,9 @@ function rules_action($name, $settings = array()) { * * @param $name * The condition's name. - * @param $settings + * @param array $settings * The condition's settings array. + * * @return RulesCondition */ function rules_condition($name, $settings = array()) { @@ -56,9 +104,9 @@ function rules_condition($name, $settings = array()) { /** * Creates a new rule. * - * @param $variables + * @param array $variables * The array of variables to setup in the evaluation state, making them - * available for the configuraion elements. Values for the variables need to + * available for the configuration elements. Values for the variables need to * be passed as argument when the rule is executed. Only Rule instances with * no variables can be embedded in other configurations, e.g. rule sets. * The array has to be keyed by variable name and contain a sub-array for each @@ -71,9 +119,10 @@ function rules_condition($name, $settings = array()) { * initially, but the "Set data value" action may be used to do so. This is * in particular useful for defining variables which can be provided to the * caller (see $provides argument) but need not be passed in as parameter. - * @param $provides + * @param array $provides * The names of variables which should be provided to the caller. Only * variables contained in $variables may be specified. + * * @return Rule */ function rule($variables = NULL, $provides = array()) { @@ -92,8 +141,9 @@ function rules_reaction_rule() { /** * Creates a logical OR condition container. * - * @param $variables + * @param array $variables * An optional array as for rule(). + * * @return RulesOr */ function rules_or($variables = NULL) { @@ -103,8 +153,9 @@ function rules_or($variables = NULL) { /** * Creates a logical AND condition container. * - * @param $variables + * @param array $variables * An optional array as for rule(). + * * @return RulesAnd */ function rules_and($variables = NULL) { @@ -114,13 +165,14 @@ function rules_and($variables = NULL) { /** * Creates a loop. * - * @param $settings + * @param array $settings * The loop settings, containing * 'list:select': The data selector for the list to loop over. * 'item:var': Optionally a name for the list item variable. - * 'item:label': Optionally a lebel for the list item variable. - * @param $variables + * 'item:label': Optionally a label for the list item variable. + * @param array $variables * An optional array as for rule(). + * * @return RulesLoop */ function rules_loop($settings = array(), $variables = NULL) { @@ -130,10 +182,11 @@ function rules_loop($settings = array(), $variables = NULL) { /** * Creates a rule set. * - * @param $variables + * @param array $variables * An array as for rule(). - * @param $provides + * @param array $provides * The names of variables which should be provided to the caller. See rule(). + * * @return RulesRuleSet */ function rules_rule_set($variables = array(), $provides = array()) { @@ -143,10 +196,11 @@ function rules_rule_set($variables = array(), $provides = array()) { /** * Creates an action set. * - * @param $variables + * @param array $variables * An array as for rule(). - * @param $provides + * @param array $provides * The names of variables which should be provided to the caller. See rule(). + * * @return RulesActionSet */ function rules_action_set($variables = array(), $provides = array()) { @@ -158,15 +212,15 @@ function rules_action_set($variables = array(), $provides = array()) { * * @param $msg * The message to log. - * @param $args + * @param array $args * An array of placeholder arguments as used by t(). * @param $priority * A priority as defined by the RulesLog class. * @param RulesPlugin $element - * (optional) The RulesElement causing the log entry. - * @param boolean $scope - * (optional) This may be used to denote the beginning (TRUE) or the end - * (FALSE) of a new execution scope. + * (optional) The RulesElement causing the log entry. + * @param bool $scope + * (optional) This may be used to denote the beginning (TRUE) or the end + * (FALSE) of a new execution scope. */ function rules_log($msg, $args = array(), $priority = RulesLog::INFO, RulesPlugin $element = NULL, $scope = NULL) { static $logger, $settings; @@ -183,7 +237,11 @@ function rules_log($msg, $args = array(), $priority = RulesLog::INFO, RulesPlugi if (isset($element) && isset($element->root()->name)) { $link = l(t('edit configuration'), RulesPluginUI::path($element->root()->name, 'edit', $element)); } + // Disabled rules invocation to avoid an endless loop when using + // watchdog - which would trigger a rules event. + rules_event_invocation_enabled(FALSE); watchdog('rules', $msg, $args, $priority == RulesLog::WARN ? WATCHDOG_WARNING : WATCHDOG_ERROR, $link); + rules_event_invocation_enabled(TRUE); } // Do nothing in case debugging is totally disabled. if (!$settings['rules_debug_log'] && !$settings['rules_debug']) { @@ -199,15 +257,21 @@ function rules_log($msg, $args = array(), $priority = RulesLog::INFO, RulesPlugi /** * Fetches module definitions for the given hook name. * - * Used for collecting events, rules, actions and condtions from other modules. + * Used for collecting events, rules, actions and condition from other modules. * * @param $hook * The hook of the definitions to get from invoking hook_rules_{$hook}. */ function rules_fetch_data($hook) { $data = &drupal_static(__FUNCTION__, array()); + static $discover = array( + 'action_info' => 'RulesActionHandlerInterface', + 'condition_info' => 'RulesConditionHandlerInterface', + 'event_info' => 'RulesEventHandlerInterface', + ); if (!isset($data[$hook])) { + $data[$hook] = array(); foreach (module_implements('rules_' . $hook) as $module) { $result = call_user_func($module . '_rules_' . $hook); if (isset($result) && is_array($result)) { @@ -217,11 +281,90 @@ function rules_fetch_data($hook) { } } } - drupal_alter('rules_'. $hook, $data[$hook]); + // Support class discovery. + if (isset($discover[$hook])) { + $data[$hook] += rules_discover_plugins($discover[$hook]); + } + drupal_alter('rules_' . $hook, $data[$hook]); } return $data[$hook]; } +/** + * Discover plugin implementations. + * + * Class based plugin handlers must be loaded when rules caches are rebuilt, + * such that they get discovered properly. You have the following options: + * - Put it into a regular module file (discouraged) + * - Put it into your module.rules.inc file + * - Put it in any file and declare it using hook_rules_file_info() + * - Put it in any file and declare it using hook_rules_directory() + * + * In addition to that, the class must be loadable via regular class + * auto-loading, thus put the file holding the class in your info file or use + * another class-loader. + * + * @param string $class + * The class or interface the plugins must implement. For a plugin to be + * discovered it must have a static getInfo() method also. + * + * @return array + * An info-hook style array containing info about discovered plugins. + * + * @see RulesActionHandlerInterface + * @see RulesConditionHandlerInterface + * @see RulesEventHandlerInterface + */ +function rules_discover_plugins($class) { + // Make sure all files possibly holding plugins are included. + RulesAbstractPlugin::includeFiles(); + + $items = array(); + foreach (get_declared_classes() as $plugin_class) { + if (is_subclass_of($plugin_class, $class) && method_exists($plugin_class, 'getInfo')) { + $info = call_user_func(array($plugin_class, 'getInfo')); + $info['class'] = $plugin_class; + $info['module'] = _rules_discover_module($plugin_class); + $items[$info['name']] = $info; + } + } + return $items; +} + +/** + * Determines the module providing the given class. + * + * @param string $class + * The name of the class or interface plugins to discover. + * + * @return string|false + * The path of the class, relative to the Drupal installation root, + * or FALSE if not discovered. + */ +function _rules_discover_module($class) { + $paths = &drupal_static(__FUNCTION__); + + if (!isset($paths)) { + // Build up a map of modules keyed by their directory. + foreach (system_list('module_enabled') as $name => $module_info) { + $paths[dirname($module_info->filename)] = $name; + } + } + + // Retrieve the class file and convert its absolute path to a regular Drupal + // path relative to the installation root. + $reflection = new ReflectionClass($class); + $path = str_replace(realpath(DRUPAL_ROOT) . DIRECTORY_SEPARATOR, '', realpath(dirname($reflection->getFileName()))); + $path = DIRECTORY_SEPARATOR != '/' ? str_replace(DIRECTORY_SEPARATOR, '/', $path) : $path; + + // Go up the path until we match a module. + $parts = explode('/', $path); + while (!isset($paths[$path]) && array_pop($parts)) { + $path = dirname($path); + } + return isset($paths[$path]) ? $paths[$path] : FALSE; +} + /** * Gets a rules cache entry. */ @@ -241,24 +384,39 @@ function &rules_get_cache($cid = 'data') { if ($get = cache_get($cid . $cid_suffix, 'cache_rules')) { $cache[$cid] = $get->data; } - elseif ($cid === 'data') { - // There is no 'data' cache so we need to rebuild it. Make sure subsequent - // cache gets of the main 'data' cache during rebuild get the interim - // cache by passing in the reference of the static cache variable. - _rules_rebuild_cache($cache['data']); - } - elseif (strpos($cid, 'comp_') === 0) { - $cache[$cid] = FALSE; - _rules_rebuild_component_cache(); - return $cache[$cid]; - } - elseif (strpos($cid, 'event_') === 0) { - $cache[$cid] = FALSE; - RulesEventSet::rebuildEventCache(); - return $cache[$cid]; - } else { - $cache[$cid] = FALSE; + // Prevent stampeding by ensuring the cache is rebuilt just once at the + // same time. + while (!lock_acquire(__FUNCTION__ . $cid . $cid_suffix, 60)) { + // Now wait until the lock is released. + lock_wait(__FUNCTION__ . $cid . $cid_suffix, 30); + // If the lock is released it's likely the cache was rebuild. Thus check + // again if we can fetch it from the persistent cache. + if ($get = cache_get($cid . $cid_suffix, 'cache_rules')) { + $cache[$cid] = $get->data; + return $cache[$cid]; + } + } + if ($cid === 'data') { + // There is no 'data' cache so we need to rebuild it. Make sure + // subsequent cache gets of the main 'data' cache during rebuild get + // the interim cache by passing in the reference of the static cache + // variable. + _rules_rebuild_cache($cache['data']); + } + elseif (strpos($cid, 'comp_') === 0) { + $cache[$cid] = FALSE; + _rules_rebuild_component_cache(); + } + elseif (strpos($cid, 'event_') === 0 || $cid == 'rules_event_whitelist') { + $cache[$cid] = FALSE; + RulesEventSet::rebuildEventCache(); + } + else { + $cache[$cid] = FALSE; + } + // Ensure a set lock is released. + lock_release(__FUNCTION__ . $cid . $cid_suffix); } } return $cache[$cid]; @@ -279,7 +437,7 @@ function &rules_get_cache($cid = 'data') { * @see entity_defaults_rebuild() */ function _rules_rebuild_cache(&$cache) { - foreach(array('data_info', 'plugin_info') as $hook) { + foreach (array('data_info', 'plugin_info') as $hook) { $cache[$hook] = rules_fetch_data($hook); } foreach ($cache['plugin_info'] as $name => &$info) { @@ -322,7 +480,7 @@ function _rules_rebuild_component_cache() { * * In addition to calling cache_set(), this function makes sure the cache item * is immediately available via rules_get_cache() by keeping all cache items - * in memory. That way we can garantuee rules_get_cache() is able to retrieve + * in memory. That way we can guarantee rules_get_cache() is able to retrieve * any cache item, even if all cache gets fail. * * @see rules_get_cache() @@ -337,16 +495,14 @@ function rules_set_cache($cid, $data) { * Implements hook_flush_caches(). */ function rules_flush_caches() { - variable_del('rules_empty_sets'); return array('cache_rules'); } /** - * Clears the rule set cache + * Clears the rule set cache. */ function rules_clear_cache() { cache_clear_all('*', 'cache_rules', TRUE); - variable_del('rules_empty_sets'); drupal_static_reset('rules_get_cache'); drupal_static_reset('rules_fetch_data'); drupal_static_reset('rules_config_update_dirty_flag'); @@ -356,16 +512,17 @@ function rules_clear_cache() { /** * Imports the given export and returns the imported configuration. * - * @param $export + * @param string $export * A serialized string in JSON format as produced by the RulesPlugin::export() * method, or the PHP export as usual PHP array. + * @param string $error_msg + * * @return RulesPlugin */ function rules_import($export, &$error_msg = '') { return entity_get_controller('rules_config')->import($export, $error_msg); } - /** * Wraps the given data. * @@ -373,9 +530,10 @@ function rules_import($export, &$error_msg = '') { * If available, the actual data, else NULL. * @param $info * An array of info about this data. - * @param $force + * @param bool $force * Usually data is only wrapped if really needed. If set to TRUE, wrapping the * data is forced, so primitive data types are also wrapped. + * * @return EntityMetadataWrapper * An EntityMetadataWrapper or the unwrapped data. * @@ -416,11 +574,12 @@ function &rules_wrap_data($data = NULL, $info, $force = FALSE) { /** * Unwraps the given data, if it's wrapped. * - * @param $data + * @param array $data * An array of wrapped data. - * @param $info + * @param array $info * Optionally an array of info about how to unwrap the data. Keyed as $data. - * @return + * + * @return array * An array containing unwrapped or passed through data. */ function rules_unwrap_data(array $data, $info = array()) { @@ -460,6 +619,70 @@ function rules_unwrap_data(array $data, $info = array()) { return $data; } +/** + * Gets event info for a given event. + * + * @param string $event_name + * A (configured) event name. + * + * @return array + * An array of event info. If the event is unknown, a suiting info array is + * generated and returned + */ +function rules_get_event_info($event_name) { + $base_event_name = rules_get_event_base_name($event_name); + $events = rules_fetch_data('event_info'); + if (isset($events[$base_event_name])) { + return $events[$base_event_name] + array('name' => $base_event_name); + } + return array( + 'label' => t('Unknown event "!event_name"', array('!event_name' => $base_event_name)), + 'name' => $base_event_name, + ); +} + +/** + * Returns the base name of a configured event name. + * + * For a configured event name like node_view--article the base event name + * node_view is returned. + * + * @param string $event_name + * A (configured) event name. + * + * @return string + * The event base name. + */ +function rules_get_event_base_name($event_name) { + // Cut off any suffix from a configured event name. + if (strpos($event_name, '--') !== FALSE) { + $parts = explode('--', $event_name, 2); + return $parts[0]; + } + return $event_name; +} + +/** + * Returns the rule event handler for the given event. + * + * Events having no settings are handled via the class RulesEventSettingsNone. + * + * @param string $event_name + * The event name (base or configured). + * @param array $settings + * (optional) An array of event settings to set on the handler. + * + * @return RulesEventHandlerInterface + * The event handler. + */ +function rules_get_event_handler($event_name, array $settings = NULL) { + $event_name = rules_get_event_base_name($event_name); + $event_info = rules_get_event_info($event_name); + $class = !empty($event_info['class']) ? $event_info['class'] : 'RulesEventDefaultHandler'; + $handler = new $class($event_name, $event_info); + return isset($settings) ? $handler->setSettings($settings) : $handler; +} + /** * Creates a new instance of a the given rules plugin. * @@ -473,7 +696,7 @@ function rules_plugin_factory($plugin_name, $arg1 = NULL, $arg2 = NULL) { } /** - * Implementation of hook_rules_plugin_info(). + * Implements hook_rules_plugin_info(). * * Note that the cache is rebuilt in the order of the plugins. Therefore the * condition and action plugins must be at the top, so that any components @@ -485,11 +708,11 @@ function rules_rules_plugin_info() { 'condition' => array( 'class' => 'RulesCondition', 'embeddable' => 'RulesConditionContainer', - 'extenders' => array ( + 'extenders' => array( 'RulesPluginImplInterface' => array( 'class' => 'RulesAbstractPluginDefaults', ), - 'RulesPluginFeaturesIntegrationInterace' => array( + 'RulesPluginFeaturesIntegrationInterface' => array( 'methods' => array( 'features_export' => 'rules_features_abstract_default_features_export', ), @@ -502,11 +725,11 @@ function rules_rules_plugin_info() { 'action' => array( 'class' => 'RulesAction', 'embeddable' => 'RulesActionContainer', - 'extenders' => array ( + 'extenders' => array( 'RulesPluginImplInterface' => array( 'class' => 'RulesAbstractPluginDefaults', ), - 'RulesPluginFeaturesIntegrationInterace' => array( + 'RulesPluginFeaturesIntegrationInterface' => array( 'methods' => array( 'features_export' => 'rules_features_abstract_default_features_export', ), @@ -598,7 +821,7 @@ function rules_rules_plugin_info() { } /** - * Implementation of hook_entity_info(). + * Implements hook_entity_info(). */ function rules_entity_info() { return array( @@ -627,10 +850,10 @@ function rules_entity_info() { } /** - * Implementation of hook_hook_info(). + * Implements hook_hook_info(). */ function rules_hook_info() { - foreach(array('plugin_info', 'data_info', 'condition_info', 'action_info', 'event_info', 'file_info', 'evaluator_info', 'data_processor_info') as $hook) { + foreach (array('plugin_info', 'rules_directory', 'data_info', 'condition_info', 'action_info', 'event_info', 'file_info', 'evaluator_info', 'data_processor_info') as $hook) { $hooks['rules_' . $hook] = array( 'group' => 'rules', ); @@ -657,12 +880,12 @@ function rules_hook_info() { * @see hook_entity_info() * @see RulesEntityController * - * @param $names + * @param array|false $names * An array of rules configuration names or FALSE to load all. - * @param $conditions + * @param array $conditions * An array of conditions in the form 'field' => $value. * - * @return + * @return array * An array of rule configurations indexed by their ids. */ function rules_config_load_multiple($names = array(), $conditions = array()) { @@ -690,10 +913,10 @@ function rules_config_load($name) { * Whether to return only the label or the whole component object. * @param $type * Optionally filter for 'action' or 'condition' components. - * @param $conditions + * @param array $conditions * An array of additional conditions as required by rules_config_load(). * - * @return + * @return array * An array keyed by component name containing either the label or the config. */ function rules_get_components($label = FALSE, $type = NULL, $conditions = array()) { @@ -716,7 +939,7 @@ function rules_get_components($label = FALSE, $type = NULL, $conditions = array( /** * Delete rule configurations from database. * - * @param $ids + * @param array $ids * An array of entity IDs. */ function rules_config_delete(array $ids) { @@ -726,13 +949,13 @@ function rules_config_delete(array $ids) { /** * Ensures the configuration's 'dirty' flag is up to date by running an integrity check. * - * @param $update + * @param bool $update * (optional) Whether the dirty flag is also updated in the database if * necessary. Defaults to TRUE. */ function rules_config_update_dirty_flag($rules_config, $update = TRUE) { // Keep a log of already check configurations to avoid repetitive checks on - // oftent used components. + // often used components. // @see rules_element_invoke_component_validate() $checked = &drupal_static(__FUNCTION__, array()); if (!empty($checked[$rules_config->name])) { @@ -779,7 +1002,7 @@ function rules_config_update_dirty_flag($rules_config, $update = TRUE) { * @param ... * Arguments to pass to the hook / event. * - * @return + * @return array * An array of return values of the hook implementations. If modules return * arrays from their implementations, those are merged into one array. */ @@ -822,15 +1045,16 @@ function rules_invoke_all() { * @see rules_invoke_event_by_args() */ function rules_invoke_event() { - global $conf; - $args = func_get_args(); $event_name = $args[0]; unset($args[0]); - // For invoking the rules event we directly acccess the global $conf. This is - // fast without having to introduce another static cache. - if (!defined('MAINTENANCE_MODE') && !isset($conf['rules_empty_sets'][$event_name]) && $event = rules_get_cache('event_' . $event_name)) { - $event->executeByArgs($args); + // We maintain a whitelist of configured events to reduces the number of cache + // reads. If the whitelist is not in the cache we proceed and it is rebuilt. + if (rules_event_invocation_enabled()) { + $whitelist = rules_get_cache('rules_event_whitelist'); + if ((($whitelist === FALSE) || isset($whitelist[$event_name])) && $event = rules_get_cache('event_' . $event_name)) { + $event->executeByArgs($args); + } } } @@ -839,7 +1063,7 @@ function rules_invoke_event() { * * @param $event_name * The event's name. - * @param $args + * @param array $args * An array of parameters for the variables provided by the event, as defined * in hook_rules_event_info(). Either pass an array keyed by the variable * names or a numerically indexed array, in which case the ordering of the @@ -852,12 +1076,13 @@ function rules_invoke_event() { * @see rules_invoke_event() */ function rules_invoke_event_by_args($event_name, $args = array()) { - global $conf; - - // For invoking the rules event we directly acccess the global $conf. This is - // fast without having to introduce another static cache. - if (!defined('MAINTENANCE_MODE') && !isset($conf['rules_empty_sets'][$event_name]) && $event = rules_get_cache('event_' . $event_name)) { - $event->executeByArgs($args); + // We maintain a whitelist of configured events to reduces the number of cache + // reads. If the whitelist is empty we proceed and it is rebuilt. + if (rules_event_invocation_enabled()) { + $whitelist = rules_get_cache('rules_event_whitelist'); + if ((empty($whitelist) || isset($whitelist[$event_name])) && $event = rules_get_cache('event_' . $event_name)) { + $event->executeByArgs($args); + } } } @@ -869,7 +1094,7 @@ function rules_invoke_event_by_args($event_name, $args = array()) { * @param $args * Pass further parameters as required for the invoked component. * - * @return + * @return array * An array of variables as provided by the component, or FALSE in case the * component could not be executed. */ @@ -883,10 +1108,12 @@ function rules_invoke_component() { } /** - * Filters the given array of arrays by keeping only entries which have $key set - * to the value of $value. + * Filters the given array of arrays. * - * @param $array + * This filter operates by keeping only entries which have $key set to the + * value of $value. + * + * @param array $array * The array of arrays to filter. * @param $key * The key used for the comparison. @@ -908,10 +1135,11 @@ function rules_filter_array($array, $key, $value) { } /** - * Merges the $update array into $array making sure no values of $array not - * appearing in $update are lost. + * Merges the $update array into $array. * - * @return + * Makes sure no values of $array not appearing in $update are lost. + * + * @return array * The updated array. */ function rules_update_array(array $array, array $update) { @@ -929,12 +1157,13 @@ function rules_update_array(array $array, array $update) { /** * Extracts the property with the given name. * - * @param $arrays + * @param array $arrays * An array of arrays from which a property is to be extracted. * @param $key * The name of the property to extract. * - * @return An array of extracted properties, keyed as in $arrays- + * @return array + * An array of extracted properties, keyed as in $arrays. */ function rules_extract_property($arrays, $key) { $data = array(); @@ -959,9 +1188,9 @@ function rules_array_key($array) { * * @param $replacements * An array of token replacements that need to be "cleaned" for use in the URL. - * @param $data + * @param array $data * An array of objects used to generate the replacements. - * @param $options + * @param array $options * An array of options used to generate the replacements. * * @see rules_path_action_info() @@ -1068,6 +1297,7 @@ function rules_permissions_by_component(array $components = array()) { /** * Menu callback for loading rules configuration elements. + * * @see RulesUIController::config_menu() */ function rules_element_load($element_id, $config_name) { @@ -1077,6 +1307,7 @@ function rules_element_load($element_id, $config_name) { /** * Menu callback for getting the title as configured. + * * @see RulesUIController::config_menu() */ function rules_get_title($text, $element) { @@ -1095,6 +1326,7 @@ function rules_get_title($text, $element) { * Menu callback for getting the title for the add element page. * * Uses a work-a-round for accessing the plugin name. + * * @see RulesUIController::config_menu() */ function rules_menu_add_element_title($array) { @@ -1187,17 +1419,24 @@ function rules_drupal_goto_alter(&$path, &$options, &$http_response_code) { * Returns whether the debug log should be shown. */ function rules_show_debug_output() { - if (variable_get('rules_debug', FALSE) == RulesLog::INFO && user_access('access rules debug')) { + // For performance avoid unnecessary auto-loading of the RulesLog class. + if (!class_exists('RulesLog', FALSE)) { + return FALSE; + } + if (variable_get('rules_debug', 0) == RulesLog::INFO && user_access('access rules debug')) { return TRUE; } - // For performance avoid unnecessary auto-loading of the RulesLog class. - return variable_get('rules_debug', FALSE) == RulesLog::WARN && user_access('access rules debug') && class_exists('RulesLog', FALSE) && RulesLog::logger()->hasErrors(); + return variable_get('rules_debug', 0) == RulesLog::WARN && user_access('access rules debug') && RulesLog::logger()->hasErrors(); } /** * Implements hook_exit(). */ function rules_exit() { + // Bail out if this is cached request and modules are not loaded. + if (!module_exists('rules') || !module_exists('user')) { + return; + } if (rules_show_debug_output()) { if ($log = RulesLog::logger()->render()) { // Keep the log in the session so we can show it on the next page. @@ -1294,11 +1533,13 @@ function rules_modules_disabled($modules) { ->fields('r') ->condition('id', $ids, 'IN') ->condition('active', 1) - ->execute()->rowCount(); + ->countQuery() + ->execute() + ->fetchField(); if ($count > 0) { $message = format_plural($count, '1 Rules configuration requires some of the disabled modules to function and cannot be executed any more.', - '@count Rules configuration require some of the disabled modules to function and cannot be executed any more.' + '@count Rules configurations require some of the disabled modules to function and cannot be executed any more.' ); drupal_set_message($message, 'warning'); } @@ -1315,10 +1556,32 @@ function rules_config_access($op, $rules_config = NULL, $account = NULL) { if (user_access('bypass rules access', $account)) { return TRUE; } - if (!isset($rules_config) || (isset($account) && $account->uid != $GLOBALS['user']->uid)) { + // Allow modules to grant / deny access. + $access = module_invoke_all('rules_config_access', $op, $rules_config, $account); + + // Only grant access if at least one module granted access and no one denied + // access. + if (in_array(FALSE, $access, TRUE)) { return FALSE; } - return user_access('administer rules', $account) && ($op == 'view' || $rules_config->access()); + elseif (in_array(TRUE, $access, TRUE)) { + return TRUE; + } + return FALSE; +} + +/** + * Implements hook_rules_config_access(). + */ +function rules_rules_config_access($op, $rules_config = NULL, $account = NULL) { + // Instead of returning FALSE return nothing, so others still can grant + // access. + if (!isset($rules_config) || (isset($account) && $account->uid != $GLOBALS['user']->uid)) { + return; + } + if (user_access('administer rules', $account) && ($op == 'view' || $rules_config->access())) { + return TRUE; + } } /** @@ -1357,25 +1620,25 @@ function rules_menu() { /** * Helper function to keep track of external documentation pages for Rules. * - * @param $topic + * @param string $topic * The topic key for used for identifying help pages. * - * @return + * @return string|array|false * Either a URL for the given page, or the full list of external help pages. */ function rules_external_help($topic = NULL) { $help = array( - 'rules' => 'http://drupal.org/node/298480', - 'terminology' => 'http://drupal.org/node/1299990', - 'condition-components' => 'http://drupal.org/node/1300034', - 'data-selection' => 'http://drupal.org/node/1300042', - 'chained-tokens' => 'http://drupal.org/node/1300042', - 'loops' => 'http://drupal.org/node/1300058', - 'components' => 'http://drupal.org/node/1300024', - 'component-types' => 'http://drupal.org/node/1300024', - 'variables' => 'http://drupal.org/node/1300024', - 'scheduler' => 'http://drupal.org/node/1300068', - 'coding' => 'http://drupal.org/node/878720', + 'rules' => 'https://www.drupal.org/node/298480', + 'terminology' => 'https://www.drupal.org/node/1299990', + 'condition-components' => 'https://www.drupal.org/node/1300034', + 'data-selection' => 'https://www.drupal.org/node/1300042', + 'chained-tokens' => 'https://www.drupal.org/node/1300042', + 'loops' => 'https://www.drupal.org/node/1300058', + 'components' => 'https://www.drupal.org/node/1300024', + 'component-types' => 'https://www.drupal.org/node/1300024', + 'variables' => 'https://www.drupal.org/node/1300024', + 'scheduler' => 'https://www.drupal.org/node/1300068', + 'coding' => 'https://www.drupal.org/node/878720', ); if (isset($topic)) { @@ -1448,3 +1711,46 @@ function rules_tokens($type, $tokens, $data, $options = array()) { return entity_token_tokens('struct', $tokens, array('struct' => $wrapper), $options); } } + +/** + * Helper function that retrieves a metadata wrapper with all properties. + * + * Note that without this helper, bundle-specific properties aren't added. + */ +function rules_get_entity_metadata_wrapper_all_properties(RulesAbstractPlugin $element) { + return entity_metadata_wrapper($element->settings['type'], NULL, array( + 'property info alter' => 'rules_entity_metadata_wrapper_all_properties_callback', + )); +} + +/** + * Callback that returns a metadata wrapper with all properties. + */ +function rules_entity_metadata_wrapper_all_properties_callback(EntityMetadataWrapper $wrapper, $property_info) { + $info = $wrapper->info(); + $properties = entity_get_all_property_info($info['type']); + $property_info['properties'] += $properties; + return $property_info; +} + +/** + * Helper to enable or disable the invocation of rules events. + * + * Rules invocation is disabled by default, such that Rules does not operate + * when Drupal is not fully bootstrapped. It gets enabled in rules_init() and + * rules_enable(). + * + * @param bool|null $enable + * NULL to leave the setting as is and TRUE / FALSE to change the behaviour. + * + * @return bool + * Whether the rules invocation is enabled or disabled. + */ +function rules_event_invocation_enabled($enable = NULL) { + static $invocation_enabled = FALSE; + if (isset($enable)) { + $invocation_enabled = (bool) $enable; + } + // Disable invocation if configured or if site runs in maintenance mode. + return $invocation_enabled && !defined('MAINTENANCE_MODE'); +} diff --git a/sites/all/modules/contrib/admin/rules/rules.rules.inc b/sites/all/modules/contrib/admin/rules/rules.rules.inc index 120773d9..b828c1d3 100644 --- a/sites/all/modules/contrib/admin/rules/rules.rules.inc +++ b/sites/all/modules/contrib/admin/rules/rules.rules.inc @@ -1,7 +1,8 @@ 'markup', '#markup' => t('Reaction rules, listed below, react on selected events on the site. Each reaction rule may fire any number of actions, and may have any number of conditions that must be met for the actions to be executed. You can also set up components – stand-alone sets of Rules configuration that can be used in Rules and other parts of your site. See the online documentation for an introduction on how to use Rules.', array('@url1' => url('admin/config/workflow/rules/components'), '@url2' => rules_external_help('rules'))), @@ -103,7 +104,6 @@ function rules_admin_components_overview($form, &$form_state, $base_path) { $collapsed = FALSE; } $form['help'] = array( - '#type' => 'markup', '#markup' => t('Components are stand-alone sets of Rules configuration that can be used by Rules and other modules on your site. Components are for example useful if you want to use the same conditions, actions or rules in multiple places, or call them from your custom module. You may also export each component separately. See the online documentation for more information about how to use components.', array('@url' => rules_external_help('components'))), ); @@ -160,7 +160,7 @@ function rules_admin_settings($form, &$form_state) { $pathauto_help = t("Note that Pathauto's URL path cleaning method can be configured at admin/config/search/path/settings.", array('!url' => url('admin/config/search/path/settings'))); } else { - $pathauto_help = t('Install the Pathauto module in order to get a configurable URL path cleaning method.'); + $pathauto_help = t('Install the Pathauto module in order to get a configurable URL path cleaning method.'); } $form['path']['rules_path_cleaning_callback'] = array( @@ -190,7 +190,7 @@ function rules_admin_settings($form, &$form_state) { $form['debug']['rules_debug_log'] = array( '#type' => 'checkbox', '#title' => t('Log debug information to the system log'), - '#default_value' => variable_get('rules_debug_log', 0), + '#default_value' => variable_get('rules_debug_log', FALSE), ); $form['debug']['rules_debug'] = array( '#type' => 'radios', @@ -209,7 +209,7 @@ function rules_admin_settings($form, &$form_state) { '#states' => array( // Hide the regions settings when the debug log is disabled. 'invisible' => array( - 'input[name="rules_debug"]' => array('value' => '0'), + 'input[name="rules_debug"]' => array('value' => 0), ), ), ); @@ -279,7 +279,7 @@ function rules_admin_settings_integrity_check_submit($form, &$form_state) { rules_config_update_dirty_flag($rules_config, TRUE, TRUE); if ($rules_config->dirty) { $count++; - $variables = array('%label' => $rules_config->label(), '%name' => $rules_config->name, '@plugin' => $rules_config->plugin(), '!uri'=> url(RulesPluginUI::path($rules_config->name))); + $variables = array('%label' => $rules_config->label(), '%name' => $rules_config->name, '@plugin' => $rules_config->plugin(), '!uri' => url(RulesPluginUI::path($rules_config->name))); drupal_set_message(t('The @plugin %label (%name) fails the integrity check and cannot be executed.', $variables), 'error'); } @@ -312,7 +312,7 @@ function rules_admin_settings_cache_rebuild_submit($form, &$form_state) { function rules_admin_add_reaction_rule($form, &$form_state, $base_path) { RulesPluginUI::formDefaults($form, $form_state); - $rules_config = rules_reaction_rule(); + $rules_config = isset($form_state['rules_config']) ? $form_state['rules_config'] : rules_reaction_rule(); $rules_config->form($form, $form_state, array('show settings' => TRUE, 'button' => TRUE)); $form['settings']['#collapsible'] = FALSE; @@ -329,18 +329,34 @@ function rules_admin_add_reaction_rule($form, &$form_state, $base_path) { // Incorporate the form to add the first event. $form['settings'] += rules_ui_add_event(array(), $form_state, $rules_config, $base_path); $form['settings']['event']['#tree'] = FALSE; + $form['settings']['event_settings']['#tree'] = FALSE; unset($form['settings']['help']); unset($form['settings']['submit']); $form['submit']['#value'] = t('Save'); $form_state += array('rules_config' => $rules_config); + $form['#validate'][] = 'rules_ui_add_reaction_rule_validate'; $form['#validate'][] = 'rules_ui_edit_element_validate'; - $form['#submit'][] = 'rules_ui_add_event_apply'; - $form['#submit'][] = 'rules_ui_edit_element_submit'; + $form['#submit'][] = 'rules_ui_add_reaction_rule_submit'; return $form; } +/** + * Form validation callback. + */ +function rules_ui_add_reaction_rule_validate(&$form, &$form_state) { + rules_ui_add_event_validate($form['settings'], $form_state); +} + +/** + * Form submit callback. + */ +function rules_ui_add_reaction_rule_submit(&$form, &$form_state) { + rules_ui_add_event_apply($form['settings'], $form_state); + rules_ui_edit_element_submit($form, $form_state); +} + /** * Add component form. */ diff --git a/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.info b/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.info index 90ecfe6d..6bce2396 100644 --- a/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.info +++ b/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.info @@ -2,13 +2,14 @@ name = Rules UI description = Administrative interface for managing rules. package = Rules core = 7.x -files[] = rules_admin.module -files[] = rules_admin.inc dependencies[] = rules +configure = admin/config/workflow/rules -; Information added by drupal.org packaging script on 2013-03-27 -version = "7.x-2.3" +; Test cases +files[] = tests/rules_admin.test + +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1364401818" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.module b/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.module index 8966ded5..f5a38e75 100644 --- a/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.module +++ b/sites/all/modules/contrib/admin/rules/rules_admin/rules_admin.module @@ -1,7 +1,8 @@ 'Rules UI Tests ', + 'description' => 'Tests Rules UI.', + 'group' => 'Rules', + ); + } + + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { + parent::setUp('rules', 'rules_admin', 'rules_test'); + RulesLog::logger()->clear(); + variable_set('rules_debug_log', TRUE); + } + + /** + * Tests that NOT condition labels are not HTML-encoded in the UI. + * + * @see https://www.drupal.org/project/rules/issues/1945006 + */ + public function testConditionLabel() { + // Create a simple user account with permission to create a rule. + $user = $this->drupalCreateUser(array('access administration pages', 'administer rules')); + $this->drupalLogin($user); + + // First we need an event. + $this->drupalGet('admin/config/workflow/rules/reaction/add'); + $edit = array( + 'settings[label]' => 'Test node event', + 'settings[name]' => 'test_node_event', + 'event' => 'node_insert', + ); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText('Editing reaction rule', 'Rule edit page is shown.'); + + // Now add a condition with a special character in the label. + $this->clickLink('Add condition'); + $this->assertText('Add a new condition', 'Condition edit page is shown.'); + $edit = array( + 'element_name' => 'rules_test_condition_apostrophe', + ); + $this->drupalPost(NULL, $edit, 'Continue'); + + // Negate the condition, as this is how it gets improperly HTML encoded. + $edit = array( + 'negate' => TRUE, + ); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertNoRaw("&#039;", 'Apostrophe is not HTML-encoded.'); + } + +} + +/** + * UI test cases for the minimal profile. + * + * The minimal profile is useful for testing because it has fewer dependencies + * so the tests run faster. Also, removing the profile-specific configuration + * reveals assumptions in the code. For example, the minimal profile doesn't + * define any content types, so when Rules expects to have content types to + * operate on that assumpation may cause errors. + */ +class RulesMinimalProfileTestCase extends DrupalWebTestCase { + + protected $profile = 'minimal'; + + /** + * Declares test metadata. + */ + public static function getInfo() { + return array( + 'name' => 'Rules UI Minimal Profile Tests ', + 'description' => 'Tests UI support for minimal profile.', + 'group' => 'Rules', + ); + } + + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { + parent::setUp('rules', 'rules_admin'); + RulesLog::logger()->clear(); + variable_set('rules_debug_log', TRUE); + } + + /** + * Tests node event UI without content types. + * + * @see https://www.drupal.org/project/rules/issues/2267341 + */ + public function testNodeEventUi() { + // Create a simple user account with permission to create a rule. + $user = $this->drupalCreateUser(array('access administration pages', 'administer rules')); + $this->drupalLogin($user); + + $this->drupalGet('admin/config/workflow/rules/reaction/add'); + $edit = array( + 'settings[label]' => 'Test node event', + 'settings[name]' => 'test_node_event', + 'event' => 'node_insert', + ); + $this->drupalPostAJAX(NULL, $edit, 'event'); + $this->assertText('Restrict by type', 'Restrict by type selection is visible.'); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText('Editing reaction rule', 'Rule edit page is shown.'); + } + +} diff --git a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.i18n.inc b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.i18n.inc index 003540d8..da49f39b 100644 --- a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.i18n.inc +++ b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.i18n.inc @@ -11,7 +11,7 @@ class RulesI18nStringController extends EntityDefaultI18nStringController { /** - * Overriden to customize i18n object info. + * Overridden to customize i18n object info. * * @see EntityDefaultI18nStringController::hook_object_info() */ @@ -22,7 +22,7 @@ class RulesI18nStringController extends EntityDefaultI18nStringController { } /** - * Overriden to customize the used menu wildcard. + * Overridden to customize the used menu wildcard. */ protected function menuWildcard() { return '%rules_config'; @@ -34,6 +34,7 @@ class RulesI18nStringController extends EntityDefaultI18nStringController { protected function menuBasePath() { return 'admin/config/workflow/rules/reaction'; } + } /** @@ -42,7 +43,7 @@ class RulesI18nStringController extends EntityDefaultI18nStringController { class RulesI18nStringObjectWrapper extends i18n_string_object_wrapper { /** - * Get translatable properties + * Get translatable properties. */ protected function build_properties() { $strings = parent::build_properties(); @@ -91,4 +92,5 @@ class RulesI18nStringObjectWrapper extends i18n_string_object_wrapper { } } } + } diff --git a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.info b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.info index 41014dfc..835d413f 100644 --- a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.info +++ b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.info @@ -7,9 +7,9 @@ core = 7.x files[] = rules_i18n.i18n.inc files[] = rules_i18n.rules.inc files[] = rules_i18n.test -; Information added by drupal.org packaging script on 2013-03-27 -version = "7.x-2.3" + +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1364401818" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.install b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.install new file mode 100644 index 00000000..7debfd98 --- /dev/null +++ b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.install @@ -0,0 +1,19 @@ +language; + drupal_static_reset('i18n_object_info'); + drupal_static_reset('entity_get_info'); + drupal_static_reset('entity_i18n_controller'); + cache_clear_all("entity_info:$langcode", 'cache'); +} diff --git a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.module b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.module index 8a87615e..e7cea79d 100644 --- a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.module +++ b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.module @@ -5,7 +5,6 @@ * Rules i18n integration. */ - /** * Implements hook_menu(). */ @@ -111,7 +110,10 @@ function rules_i18n_rules_config_update($rules_config, $original = NULL) { * Implements hook_rules_config_delete(). */ function rules_i18n_rules_config_delete($rules_config) { - i18n_string_object_remove('rules_config', $rules_config); + // Only react on real delete, not revert. + if (!$rules_config->hasStatus(ENTITY_IN_CODE)) { + i18n_string_object_remove('rules_config', $rules_config); + } } /** diff --git a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.rules.inc b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.rules.inc index 4f5d0eeb..b4c4f9ce 100644 --- a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.rules.inc +++ b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.rules.inc @@ -115,7 +115,7 @@ function rules_i18n_rules_evaluator_info() { 'type' => array('text', 'list', 'token', 'list'), // Be sure to translate after doing PHP evaluation. 'weight' => -8, - ), + ), ); } @@ -128,6 +128,9 @@ class RulesI18nStringEvaluator extends RulesDataInputEvaluator { return user_access('translate admin strings'); } + /** + * Overrides RulesDataInputEvaluator::prepare(). + */ public function prepare($text, $var_info, $param_info = NULL) { if (!empty($param_info['translatable'])) { $this->setting = TRUE; @@ -177,9 +180,12 @@ class RulesI18nStringEvaluator extends RulesDataInputEvaluator { return $value; } + /** + * Overrides RulesDataInputEvaluator::help(). + */ public static function help($var_info, $param_info = array()) { if (!empty($param_info['translatable'])) { - if ($param_info['custom translation language']) { + if (!empty($param_info['custom translation language'])) { $text = t('Translations can be provided at the %translate tab. The argument value is translated to the configured language.', array('%translate' => t('Translate'))); } else { @@ -193,4 +199,5 @@ class RulesI18nStringEvaluator extends RulesDataInputEvaluator { return $render; } } + } diff --git a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.test b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.test index 1cb7a0da..615d3dda 100644 --- a/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.test +++ b/sites/all/modules/contrib/admin/rules/rules_i18n/rules_i18n.test @@ -10,6 +10,9 @@ */ class RulesI18nTestCase extends DrupalWebTestCase { + /** + * Declares test metadata. + */ public static function getInfo() { return array( 'name' => 'Rules I18n', @@ -19,7 +22,10 @@ class RulesI18nTestCase extends DrupalWebTestCase { ); } - public function setUp() { + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { parent::setUp('rules_i18n'); $this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages')); $this->drupalLogin($this->admin_user); @@ -52,11 +58,11 @@ class RulesI18nTestCase extends DrupalWebTestCase { } elseif ($this->xpath('//input[@type="checkbox" and @name=:name and @checked="checked"]', array(':name' => 'enabled[' . $language_code . ']'))) { // It's installed and enabled. No need to do anything. - $this->assertTrue(true, 'Language [' . $language_code . '] already installed and enabled.'); + $this->assertTrue(TRUE, 'Language [' . $language_code . '] already installed and enabled.'); } else { // It's installed but not enabled. Enable it. - $this->assertTrue(true, 'Language [' . $language_code . '] already installed.'); + $this->assertTrue(TRUE, 'Language [' . $language_code . '] already installed.'); $this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration')); $this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.')); } @@ -139,7 +145,7 @@ class RulesI18nTestCase extends DrupalWebTestCase { $messages = drupal_get_messages(); $this->assertEqual($messages['status'][0], 'text-de', 'Text has been successfully translated.'); - // Enable the PHP module and make sure PHP in translations is not evaluted. + // Enable the PHP module and make sure PHP in translations is not evaluated. module_enable(array('php')); i18n_string_textgroup('rules')->update_translation("rules_config:{$set->name}:$id:text", 'de', 'text '); @@ -162,9 +168,9 @@ class RulesI18nTestCase extends DrupalWebTestCase { $set = rules_action_set(array('node' => array('type' => 'node'))); $set->action('rules_i18n_select', array( - 'data:select' => 'node:body:value', - 'language' => 'de', - 'data_translated:var' => 'body', + 'data:select' => 'node:body:value', + 'language' => 'de', + 'data_translated:var' => 'body', )); $set->action('drupal_message', array('message:select' => 'body')); $set->save(); @@ -180,4 +186,5 @@ class RulesI18nTestCase extends DrupalWebTestCase { $messages = drupal_get_messages(); $this->assertEqual($messages['status'][0], "German body.\n", 'Translated text has been selected.'); } + } diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.handler.inc b/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.handler.inc index 07e36521..60341576 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.handler.inc +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.handler.inc @@ -1,5 +1,10 @@ TRUE, ), 'filter' => array( - 'handler' => 'views_handler_filter', + 'handler' => 'views_handler_filter_string', ), 'sort' => array( 'handler' => 'views_handler_sort', diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.views_default.inc b/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.views_default.inc index cdb06fb9..708dd631 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.views_default.inc +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler.views_default.inc @@ -9,7 +9,7 @@ * Implements hook_views_default_views(). */ function rules_scheduler_views_default_views() { - $view = new view; + $view = new view(); $view->name = 'rules_scheduler'; $view->description = 'Scheduled Rules components'; $view->tag = ''; @@ -86,7 +86,7 @@ function rules_scheduler_views_default_views() { $handler->display->display_options['fields']['config']['field'] = 'config'; $handler->display->display_options['fields']['config']['alter']['alter_text'] = 0; $handler->display->display_options['fields']['config']['alter']['make_link'] = 1; - $handler->display->display_options['fields']['config']['alter']['path'] = 'admin/config/workflow/rules/config/[config]'; + $handler->display->display_options['fields']['config']['alter']['path'] = 'admin/config/workflow/rules/components/manage/[config]'; $handler->display->display_options['fields']['config']['alter']['absolute'] = 0; $handler->display->display_options['fields']['config']['alter']['trim'] = 0; $handler->display->display_options['fields']['config']['alter']['word_boundary'] = 1; @@ -163,7 +163,7 @@ function rules_scheduler_views_default_views() { t('No tasks have been scheduled.'), t('Tid'), t('Component name'), - t('admin/config/workflow/rules/config/[config]'), + t('admin/config/workflow/rules/components/manage/[config]'), t('Scheduled date'), t('User provided identifier'), t('Operations'), diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler_views_filter.inc b/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler_views_filter.inc index 3945407e..5994d832 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler_views_filter.inc +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/includes/rules_scheduler_views_filter.inc @@ -4,8 +4,10 @@ * @file * An extended subclass for component filtering. */ + class rules_scheduler_views_filter extends views_handler_filter_in_operator { - function get_value_options() { + + public function get_value_options() { if (!isset($this->value_options)) { $this->value_title = t('Component'); $result = db_select('rules_scheduler', 'r') @@ -19,4 +21,5 @@ class rules_scheduler_views_filter extends views_handler_filter_in_operator { $this->value_options = $config_names; } } -} \ No newline at end of file + +} diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.admin.inc b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.admin.inc index 2956a474..dfc36188 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.admin.inc +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.admin.inc @@ -9,7 +9,7 @@ * Schedule page with a view for the scheduled tasks. */ function rules_scheduler_schedule_page() { - // Display view for all scheduled tasks + // Display view for all scheduled tasks. if (module_exists('views')) { // We cannot use views_embed_view() here as we need to set the path for the // component filter form. @@ -18,7 +18,7 @@ function rules_scheduler_schedule_page() { $task_list = $view->preview(); } else { - $task_list = t('To display scheduled tasks you have to install the Views module.'); + $task_list = t('To display scheduled tasks you have to install the Views module.'); } $page['task_view'] = array( '#markup' => $task_list, @@ -44,7 +44,7 @@ function rules_scheduler_form($form, &$form_state) { $form['delete_by_config'] = array( '#type' => 'fieldset', '#title' => t('Delete tasks by component name'), - '#disabled' => empty($config_options) + '#disabled' => empty($config_options), ); $form['delete_by_config']['config'] = array( '#title' => t('Component'), @@ -57,7 +57,7 @@ function rules_scheduler_form($form, &$form_state) { '#type' => 'submit', '#value' => t('Delete tasks'), '#submit' => array('rules_scheduler_form_delete_by_config_submit'), - ); + ); return $form; } @@ -90,7 +90,6 @@ function rules_scheduler_delete_task($form, &$form_state, $task) { else { $msg = t('This task executes component %label and will be executed on %date. The action cannot be undone.', array( '%label' => $config->label(), - '%id' => $task['identifier'], '%date' => format_date($task['date']), )); } @@ -116,7 +115,7 @@ function rules_scheduler_schedule_form($form, &$form_state, $rules_config, $base $form_state['component'] = $rules_config->name; $action = rules_action('schedule', array('component' => $rules_config->name)); $action->form($form, $form_state); - // The component should be fixed, so hide the paramter for it. + // The component should be fixed, so hide the parameter for it. $form['parameter']['component']['#access'] = FALSE; $form['submit'] = array( '#type' => 'submit', diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.drush.inc b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.drush.inc index 24405b2a..a6ed7956 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.drush.inc +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.drush.inc @@ -12,7 +12,7 @@ function rules_scheduler_drush_command() { $items = array(); $items['rules-scheduler-tasks'] = array( - 'description' => 'Checks for scheduled tasks to be added to the queue.', + 'description' => 'Check for scheduled tasks to be added to the queue.', 'options' => array( 'claim' => 'Optionally claim tasks from the queue to work on. Any value set will override the default time spent on this queue.', ), @@ -21,7 +21,7 @@ function rules_scheduler_drush_command() { 'examples' => array( 'drush rusch' => 'Add scheduled tasks to the queue.', 'drush rusch --claim' => 'Add scheduled tasks to the queue and claim items for the default amount of time.', - 'drush rusch --claim=30' => 'Add schedules tasks to the queue and claim items for 30 seconds.', + 'drush rusch --claim=30' => 'Add scheduled tasks to the queue and claim items for 30 seconds.', ), ); @@ -41,8 +41,8 @@ function rules_scheduler_drush_help($section) { /** * Command callback for processing the rules_scheduler_tasks queue. * - * @see rules_scheduler_cron_queue_info(). - * @see rules_scheduler_cron(). + * @see rules_scheduler_cron_queue_info() + * @see rules_scheduler_cron() */ function drush_rules_scheduler_tasks() { if (rules_scheduler_queue_tasks()) { diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.info b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.info index cfdfbfb8..44731c29 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.info +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.info @@ -3,18 +3,17 @@ description = Schedule the execution of Rules components using actions. dependencies[] = rules package = Rules core = 7.x -files[] = rules_scheduler.admin.inc -files[] = rules_scheduler.module -files[] = rules_scheduler.install -files[] = rules_scheduler.rules.inc -files[] = rules_scheduler.test -files[] = includes/rules_scheduler.views_default.inc -files[] = includes/rules_scheduler.views.inc +files[] = includes/rules_scheduler.handler.inc + +; Views handlers files[] = includes/rules_scheduler_views_filter.inc -; Information added by drupal.org packaging script on 2013-03-27 -version = "7.x-2.3" +; Test cases +files[] = tests/rules_scheduler.test +files[] = tests/rules_scheduler_test.inc + +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1364401818" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.install b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.install index 74200408..0aa81432 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.install +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.install @@ -30,11 +30,12 @@ function rules_scheduler_schema() { 'type' => 'int', 'not null' => TRUE, ), - 'state' => array( - 'type' => 'text', + 'data' => array( + 'type' => 'blob', + 'size' => 'big', 'not null' => FALSE, 'serialize' => TRUE, - 'description' => 'The whole, serialized evaluation state.', + 'description' => 'The whole, serialized evaluation data.', ), 'identifier' => array( 'type' => 'varchar', @@ -43,6 +44,12 @@ function rules_scheduler_schema() { 'not null' => FALSE, 'description' => 'The user defined string identifying this task.', ), + 'handler' => array( + 'type' => 'varchar', + 'length' => '255', + 'not null' => FALSE, + 'description' => 'The fully-qualified class name of the queue item handler.', + ), ), 'primary key' => array('tid'), 'indexes' => array( @@ -55,6 +62,24 @@ function rules_scheduler_schema() { return $schema; } +/** + * Implements hook_install(). + */ +function rules_scheduler_install() { + // Create the queue to hold scheduled tasks. + $queue = DrupalQueue::get('rules_scheduler_tasks', TRUE); + $queue->createQueue(); +} + +/** + * Implements hook_uninstall(). + */ +function rules_scheduler_uninstall() { + // Clean up after ourselves by deleting the queue and all items in it. + $queue = DrupalQueue::get('rules_scheduler_tasks'); + $queue->deleteQueue(); +} + /** * Upgrade from Rules scheduler 6.x-1.x to 7.x. */ @@ -84,11 +109,11 @@ function rules_scheduler_update_7200() { 'type' => 'int', 'not null' => TRUE, ), - 'state' => array( + 'data' => array( 'type' => 'text', 'not null' => FALSE, 'serialize' => TRUE, - 'description' => 'The whole, serialized evaluation state.', + 'description' => 'The whole, serialized evaluation data.', ), 'identifier' => array( 'type' => 'varchar', @@ -122,6 +147,47 @@ function rules_scheduler_update_7202() { db_add_unique_key('rules_scheduler', 'id', array('config', 'identifier')); } +/** + * Add a database column for specifying a queue item handler. + */ +function rules_scheduler_update_7203() { + db_add_field('rules_scheduler', 'handler', array( + 'type' => 'varchar', + 'length' => '255', + 'not null' => FALSE, + 'description' => 'The fully-qualified class name of the queue item handler.', + )); +} + +/** + * Rename rules_scheduler.state into rules_scheduler.data. + */ +function rules_scheduler_update_7204() { + if (db_field_exists('rules_scheduler', 'state')) { + db_change_field('rules_scheduler', 'state', 'data', array( + 'type' => 'text', + 'not null' => FALSE, + 'serialize' => TRUE, + 'description' => 'The whole, serialized evaluation data.', + )); + } +} + +/** + * Use blob:big for rules_scheduler.data for compatibility with PostgreSQL. + */ +function rules_scheduler_update_7205() { + if (db_field_exists('rules_scheduler', 'data')) { + db_change_field('rules_scheduler', 'data', 'data', array( + 'type' => 'blob', + 'size' => 'big', + 'not null' => FALSE, + 'serialize' => TRUE, + 'description' => 'The whole, serialized evaluation data.', + )); + } +} + /** * Rules upgrade callback for mapping the action name. */ diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.module b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.module index baab87c1..a4c3e4b9 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.module +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.module @@ -11,25 +11,7 @@ define('RULES_SCHEDULER_PATH', 'admin/config/workflow/rules/schedule'); * Implements hook_cron(). */ function rules_scheduler_cron() { - // Limit adding tasks to 1000 per cron run. - $result = db_select('rules_scheduler', 'r', array('fetch' => PDO::FETCH_ASSOC)) - ->fields('r') - ->condition('date', time(), '<=') - ->range(0, 1000) - ->execute(); - - $queue = DrupalQueue::get('rules_scheduler_tasks'); - foreach ($result as $task) { - // Add the task to the queue and remove the entry afterwards. - if ($queue->createItem($task)) { - db_delete('rules_scheduler') - ->condition('tid', $task['tid']) - ->execute(); - $task_created = TRUE; - } - } - - if (!empty($task_created)) { + if (rules_scheduler_queue_tasks()) { // hook_exit() is not invoked for cron runs, so register it as shutdown // callback for logging the rules log to the watchdog. drupal_register_shutdown_function('rules_exit'); @@ -52,19 +34,43 @@ function rules_scheduler_cron_queue_info() { /** * Queue worker callback for running a single task. + * + * @param array $task + * The task to process. */ function rules_scheduler_run_task(array $task) { - if ($component = rules_get_cache('comp_' . $task['config'])) { - $replacements = array('%label' => $component->label(), '%plugin' => $component->plugin()); - $replacements['%identifier'] = $task['identifier'] ? $task['identifier'] : t('without identifier'); - rules_log('Scheduled evaluation of %plugin %label, task %identifier.', $replacements, RulesLog::INFO, $component, TRUE); - $state = unserialize($task['state']); - $state->restoreBlocks(); - // Finally evaluate the component with the given state. - $component->evaluate($state); - rules_log('Finished evaluation of %plugin %label, task %identifier.', $replacements, RulesLog::INFO, $component, FALSE); - $state->cleanUp(); + try { + // BC support for tasks that have been already queued, before update + // rules_scheduler_update_7204() ran. + if (isset($task['state'])) { + $task['data'] = $task['state']; + } + rules_scheduler_task_handler($task)->runTask(); } + catch (RulesEvaluationException $e) { + rules_log($e->msg, $e->args, $e->severity); + rules_log('Unable to execute task with identifier %id scheduled on date %date.', array('%id' => $task['identifier'], '%date' => format_date($task['date'])), RulesLog::ERROR); + } +} + +/** + * Returns the task handler for a given task. + * + * @param array $task + * A task (queue item) array. + * + * @throws RulesEvaluationException + * If the task handler class is missing. + * + * @return RulesSchedulerTaskHandlerInterface + * The task handler. + */ +function rules_scheduler_task_handler(array $task) { + $class = !empty($task['handler']) ? $task['handler'] : 'RulesSchedulerDefaultTaskHandler'; + if (!class_exists($class)) { + throw new RulesEvaluationException('Missing task handler implementation %class.', array('%class' => $class), NULL, RulesLog::ERROR); + } + return new $class($task); } /** @@ -97,7 +103,7 @@ function rules_scheduler_menu() { 'access arguments' => array('administer rules'), 'file' => 'rules_scheduler.admin.inc', ); - $items[RULES_SCHEDULER_PATH .'/%rules_scheduler_task/delete'] = array( + $items[RULES_SCHEDULER_PATH . '/%rules_scheduler_task/delete'] = array( 'title' => 'Delete a scheduled task', 'type' => MENU_CALLBACK, 'page callback' => 'drupal_get_form', @@ -109,7 +115,10 @@ function rules_scheduler_menu() { } /** - * Load a task by a given task ID. + * Loads a task by a given task ID. + * + * @param int $tid + * The task ID. */ function rules_scheduler_task_load($tid) { $result = db_select('rules_scheduler', 'r') @@ -120,7 +129,10 @@ function rules_scheduler_task_load($tid) { } /** - * Delete a task by a given task ID. + * Deletes a task by a given task ID. + * + * @param int $tid + * The task ID. */ function rules_scheduler_task_delete($tid) { db_delete('rules_scheduler') @@ -131,15 +143,22 @@ function rules_scheduler_task_delete($tid) { /** * Schedule a task to be executed later on. * - * @param $task + * @param array $task * An array representing the task with the following keys: - * - config: The machine readable name of the to be scheduled component. + * - config: The machine readable name of the to-be-scheduled component. * - date: Timestamp when the component should be executed. - * - state: An rules evaluation state to use for scheduling. + * - state: (deprecated) Rules evaluation state to use for scheduling. + * - data: Any additional data to store with the task. + * - handler: The name of the task handler class. * - identifier: User provided string to identify the task per scheduled * configuration. */ function rules_scheduler_schedule_task($task) { + // Map the deprecated 'state' property into 'data'. + if (isset($task['state'])) { + $task['data'] = $task['state']; + unset($task['state']); + } if (!empty($task['identifier'])) { // If there is a task with the same identifier and component, we replace it. db_delete('rules_scheduler') @@ -150,14 +169,44 @@ function rules_scheduler_schedule_task($task) { drupal_write_record('rules_scheduler', $task); } +/** + * Queue tasks that are ready for execution. + * + * @return bool + * TRUE if any queue items where created, otherwise FALSE. + */ +function rules_scheduler_queue_tasks() { + $items_created = FALSE; + // Limit adding tasks to 1000 per cron run. + $result = db_select('rules_scheduler', 'r', array('fetch' => PDO::FETCH_ASSOC)) + ->fields('r') + ->condition('date', time(), '<=') + ->orderBy('date') + ->range(0, 1000) + ->execute(); + + $queue = DrupalQueue::get('rules_scheduler_tasks'); + foreach ($result as $task) { + // Add the task to the queue and remove the entry afterwards. + if ($queue->createItem($task)) { + $items_created = TRUE; + rules_scheduler_task_handler($task)->afterTaskQueued(); + } + } + return $items_created; +} + /** * Implements hook_rules_config_delete(). */ function rules_scheduler_rules_config_delete($rules_config) { - // Delete all tasks scheduled for this config. - db_delete('rules_scheduler') - ->condition('config', $rules_config->name) - ->execute(); + // Only react on real delete, not revert. + if (!$rules_config->hasStatus(ENTITY_IN_CODE)) { + // Delete all tasks scheduled for this config. + db_delete('rules_scheduler') + ->condition('config', $rules_config->name) + ->execute(); + } } /** @@ -166,6 +215,6 @@ function rules_scheduler_rules_config_delete($rules_config) { function rules_scheduler_views_api() { return array( 'api' => '3.0-alpha1', - 'path' => drupal_get_path('module', 'rules_scheduler') .'/includes', + 'path' => drupal_get_path('module', 'rules_scheduler') . '/includes', ); } diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.rules.inc b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.rules.inc index dfcda868..4f31f70a 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.rules.inc +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.rules.inc @@ -5,6 +5,7 @@ * Rules integration for the rules scheduler module. * * @addtogroup rules + * * @{ */ @@ -89,7 +90,7 @@ function rules_scheduler_action_schedule($args, $element) { rules_scheduler_schedule_task(array( 'date' => $args['date'], 'config' => $args['component'], - 'state' => $new_state, + 'data' => $new_state, 'identifier' => $args['identifier'], )); } @@ -115,7 +116,9 @@ function rules_scheduler_action_schedule_info_alter(&$element_info, RulesPlugin } /** - * Validate callback for the schedule action to make sure the component exists and is not dirty. + * Validate callback for the schedule action. + * + * Makes sure the component exists and is not dirty. * * @see rules_element_invoke_component_validate() */ @@ -137,7 +140,7 @@ function rules_scheduler_action_schedule_validate(RulesPlugin $element) { */ function rules_scheduler_action_schedule_help() { return t("Note that component evaluation is triggered by cron – make sure cron is configured correctly by checking your site's !status. The scheduling time accuracy depends on your configured cron interval. See the online documentation for more information on how to schedule evaluation of components.", - array('!status' => l('Status report', 'admin/reports/status'), + array('!status' => l(t('Status report'), 'admin/reports/status'), '@url' => rules_external_help('scheduler'))); } @@ -192,7 +195,7 @@ function rules_scheduler_action_delete($component_name = NULL, $task_identifier } /** - * Cancel scheduled task action validation callback. + * Cancels scheduled task action validation callback. */ function rules_scheduler_action_delete_validate($element) { if (empty($element->settings['task']) && empty($element->settings['task:select']) && @@ -206,7 +209,7 @@ function rules_scheduler_action_delete_validate($element) { * Help for the cancel action. */ function rules_scheduler_action_delete_help() { - return t('This action allows you to delete scheduled tasks that are waiting for future execution.') .' '. t('They can be addressed by an identifier or by the component name, whereas if both are specified only tasks fulfilling both requirements will be deleted.'); + return t('This action allows you to delete scheduled tasks that are waiting for future execution.') . ' ' . t('They can be addressed by an identifier or by the component name, whereas if both are specified only tasks fulfilling both requirements will be deleted.'); } /** diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.test b/sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler.test similarity index 54% rename from sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.test rename to sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler.test index 30e48905..4e19e80c 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/rules_scheduler.test +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler.test @@ -5,9 +5,15 @@ * Rules Scheduler tests. */ +/** + * Test cases for the Rules Scheduler module. + */ class RulesSchedulerTestCase extends DrupalWebTestCase { - static function getInfo() { + /** + * Declares test metadata. + */ + public static function getInfo() { return array( 'name' => 'Rules Scheduler tests', 'description' => 'Test scheduling components.', @@ -15,10 +21,13 @@ class RulesSchedulerTestCase extends DrupalWebTestCase { ); } - function setUp() { - parent::setUp('rules_scheduler'); + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { + parent::setUp('rules_scheduler', 'rules_scheduler_test'); RulesLog::logger()->clear(); - variable_set('rules_debug_log', 1); + variable_set('rules_debug_log', TRUE); } /** @@ -27,7 +36,7 @@ class RulesSchedulerTestCase extends DrupalWebTestCase { * Note that this also makes sure Rules properly handles timezones, else this * test could fail due to a wrong 'now' timestamp. */ - function testComponentSchedule() { + public function testComponentSchedule() { $set = rules_rule_set(array( 'node1' => array('type' => 'node', 'label' => 'node'), )); @@ -51,7 +60,7 @@ class RulesSchedulerTestCase extends DrupalWebTestCase { $rule->execute($node); // Run cron to let the rules scheduler do its work. - drupal_cron_run(); + $this->cronRun(); $node = node_load($node->nid, NULL, TRUE); $this->assertFalse($node->status, 'The component has been properly scheduled.'); @@ -59,9 +68,9 @@ class RulesSchedulerTestCase extends DrupalWebTestCase { } /** - * Make sure recurion prevention is working fine for scheduled rule sets. + * Makes sure recursion prevention is working fine for scheduled rule sets. */ - function testRecursionPrevention() { + public function testRecursionPrevention() { $set = rules_rule_set(array( 'node1' => array('type' => 'node', 'label' => 'node'), )); @@ -78,7 +87,7 @@ class RulesSchedulerTestCase extends DrupalWebTestCase { $rule->event('node_update'); $rule->action('schedule', array( 'component' => 'rules_test_set_2', - 'identifier' => '', + 'identifier' => 'test_recursion_prevention', 'date' => 'now', 'param_node1:select' => 'node', )); @@ -87,14 +96,54 @@ class RulesSchedulerTestCase extends DrupalWebTestCase { // Create a node, what triggers the rule. $node = $this->drupalCreateNode(array('title' => 'The title.', 'status' => 1)); // Run cron to let the rules scheduler do its work. - drupal_cron_run(); + $this->cronRun(); $node = node_load($node->nid, NULL, TRUE); $this->assertFalse($node->status, 'The component has been properly scheduled.'); - $text1 = RulesLog::logger()->render(); - $text2 = RulesTestCase::t('Not evaluating reaction rule %unlabeled to prevent recursion.', array('unlabeled' => $rule->name)); - $this->assertTrue((strpos($text1, $text2) !== FALSE), "Scheduled recursion prevented."); + + // Create a simple user account with permission to see the dblog. + $user = $this->drupalCreateUser(array('access site reports')); + $this->drupalLogin($user); + + // View the database log. + $this->drupalGet('admin/reports/dblog'); + + // Can't use + // $this->clickLink('Rules debug information: " Scheduled evaluation...') + // because xpath doesn't allow : or " in the string. + // So instead, use our own xpath to figure out the href of the second link + // on the page (the first link is the most recent log entry, which is the + // log entry for the user login, above.) + + // All links. + $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => 'admin/reports/event/')); + // Strip off /?q= from href. + $href = explode('=', $links[1]['href']); + // Click the link for the RulesLog entry. + $this->drupalGet($href[1]); + $this->assertRaw(RulesTestCase::t('Not evaluating reaction rule %unlabeled to prevent recursion.', array('unlabeled' => $rule->name)), "Scheduled recursion prevented."); RulesLog::logger()->checkLog(); } -} + /** + * Tests that custom task handlers are properly invoked. + */ + public function testCustomTaskHandler() { + // Set up a scheduled task that will simply write a variable when executed. + $variable = 'rules_schedule_task_handler_variable'; + rules_scheduler_schedule_task(array( + 'date' => REQUEST_TIME, + 'identifier' => '', + 'config' => '', + 'data' => array('variable' => $variable), + 'handler' => 'RulesTestTaskHandler', + )); + + // Run cron to let the rules scheduler do its work. + $this->cronRun(); + + // The task handler should have set the variable to TRUE now. + $this->assertTrue(variable_get($variable)); + } + +} diff --git a/sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler_test.info b/sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler_test.info index 3ce5f836..b5b2bc31 100644 --- a/sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler_test.info +++ b/sites/all/modules/contrib/admin/rules/rules_scheduler/tests/rules_scheduler_test.info @@ -5,9 +5,8 @@ core = 7.x files[] = rules_scheduler_test.inc hidden = TRUE -; Information added by Drupal.org packaging script on 2015-03-16 -version = "7.x-2.9" +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1426527210" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/tests/rules.test b/sites/all/modules/contrib/admin/rules/tests/rules.test index 0bddc2ef..fe0a1d93 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules.test +++ b/sites/all/modules/contrib/admin/rules/tests/rules.test @@ -5,9 +5,15 @@ * Rules tests. */ +/** + * Rules test cases. + */ class RulesTestCase extends DrupalWebTestCase { - static function getInfo() { + /** + * Declares test metadata. + */ + public static function getInfo() { return array( 'name' => 'Rules Engine tests', 'description' => 'Test using the rules API to create and evaluate rules.', @@ -15,16 +21,19 @@ class RulesTestCase extends DrupalWebTestCase { ); } - function setUp() { + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { parent::setUp('rules', 'rules_test'); RulesLog::logger()->clear(); - variable_set('rules_debug_log', 1); + variable_set('rules_debug_log', TRUE); } /** * Calculates the output of t() given an array of placeholders to replace. */ - static function t($text, $strings) { + public static function t($text, $strings) { $placeholders = array(); foreach ($strings as $key => $string) { $key = !is_numeric($key) ? $key : $string; @@ -33,6 +42,9 @@ class RulesTestCase extends DrupalWebTestCase { return strtr($text, $placeholders); } + /** + * Helper function to create a test Rule. + */ protected function createTestRule() { $rule = rule(); $rule->condition('rules_test_condition_true') @@ -53,7 +65,7 @@ class RulesTestCase extends DrupalWebTestCase { /** * Tests creating a rule and iterating over the rule elements. */ - function testRuleCreation() { + public function testRuleCreation() { $rule = $this->createTestRule(); $rule->integrityCheck(); $rule->execute(); @@ -78,12 +90,15 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test handling dependencies. + * Tests handling dependencies. */ - function testdependencies() { + public function testDependencies() { $action = rules_action('rules_node_publish_action'); $this->assertEqual($action->dependencies(), array('rules_test'), 'Providing module is returned as dependency.'); + $container = new RulesTestContainer(); + $this->assertEqual($container->dependencies(), array('rules_test'), 'Providing module for container plugin is returned as dependency.'); + // Test handling unmet dependencies. $rule = rules_config_load('rules_export_test'); $this->assertTrue(in_array('comment', $rule->dependencies) && !$rule->dirty, 'Dependencies have been imported.'); @@ -144,10 +159,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test setting up an action with some action_info and serializing and - * executing it. + * Tests setting up an action, serializing, and executing it. */ - function testActionSetup() { + public function testActionSetup() { $action = rules_action('rules_node_publish_action'); $s = serialize($action); @@ -165,7 +179,7 @@ class RulesTestCase extends DrupalWebTestCase { $action2->executeByArgs(array('node' => $node)); $this->assertEqual($node->status, 1, 'Action executed correctly'); - // Test calling an extended + overriden method. + // Test calling an extended + overridden method. $this->assertEqual($action2->help(), 'custom', 'Using custom help callback.'); // Inspect the cache @@ -174,23 +188,23 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test executing with wrong arguments. + * Tests executing with wrong arguments. */ - function testActionExecutionFails() { + public function testActionExecutionFails() { $action = rules_action('rules_node_publish_action'); try { $action->execute(); $this->fail("Execution hasn't created an exception."); } catch (RulesEvaluationException $e) { - $this->pass("RulesEvaluationException was thrown: ". $e); + $this->pass("RulesEvaluationException was thrown: " . $e); } } /** - * Test setting up a rule and mapping variables. + * Tests setting up a rule and mapping variables. */ - function testVariableMapping() { + public function testVariableMapping() { $rule = rule(array( 'node' => array('type' => 'node'), 'node_unchanged' => array('type' => 'node'), @@ -209,10 +223,30 @@ class RulesTestCase extends DrupalWebTestCase { RulesLog::logger()->checkLog(); } + /** + * Tests making use of class based actions. + */ + public function testClassBasedActions() { + $cache = rules_get_cache(); + $this->assertTrue(!empty($cache['action_info']['rules_test_class_action']), 'Action has been discovered.'); + $action = rules_action('rules_test_class_action'); + + $parameters = $action->parameterInfo(); + $this->assertTrue($parameters['node'], 'Action parameter needs a value.'); + + $node = $this->drupalCreateNode(); + $action->execute($node); + $log = RulesLog::logger()->get(); + $last = array_pop($log); + $last = array_pop($log); + $this->assertEqual($last[0], 'Action called with node ' . $node->nid, 'Action called'); + RulesLog::logger()->checkLog(); + } + /** * Tests CRUD functionality. */ - function testRulesCRUD() { + public function testRulesCRUD() { $rule = $this->createTestRule(); $rule->integrityCheck()->save('test'); @@ -273,16 +307,16 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test automatic saving of variables. + * Tests automatic saving of variables. */ - function testActionSaving() { + public function testActionSaving() { // Test saving a parameter. $action = rules_action('rules_node_publish_action_save'); $node = $this->drupalCreateNode(array('status' => 0, 'type' => 'page')); $action->executeByArgs(array('node' => $node)); $this->assertEqual($node->status, 1, 'Action executed correctly on node.'); - // Sync node_load cache with node_save + // Sync node_load cache with node_save. entity_get_controller('node')->resetCache(); $node = node_load($node->nid); @@ -312,9 +346,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test adding a variable and optional parameters. + * Tests adding a variable and optional parameters. */ - function testVariableAdding() { + public function testVariableAdding() { $node = $this->drupalCreateNode(); $rule = rule(array('nid' => array('type' => 'integer'))); $rule->condition('rules_test_condition_true') @@ -328,7 +362,6 @@ class RulesTestCase extends DrupalWebTestCase { $vars = $rule->conditions()->offsetGet(0)->availableVariables(); $this->assertEqual(!isset($vars['node_loaded']), 'Loaded variable is not available to conditions.'); - // Test adding a variable with a custom variable name. $node = $this->drupalCreateNode(); $rule = rule(array('nid' => array('type' => 'integer'))); @@ -341,9 +374,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test custom access for using component actions/conditions. + * Tests custom access for using component actions/conditions. */ - function testRuleComponentAccess() { + public function testRuleComponentAccess() { // Create a normal user. $normal_user = $this->drupalCreateUser(); // Create a role for granting access to the rule component. @@ -365,15 +398,15 @@ class RulesTestCase extends DrupalWebTestCase { user_role_change_permissions($this->normal_role, array('use Rules component rules_test_roles' => TRUE)); $this->assertTrue(rules_action('component_rules_test_roles')->access(), 'Authenticated user with the correct role can use the rule component.'); - // Reset global user to anonyous. + // Reset global user to anonymous. $user = user_load(0); $this->assertFalse(rules_action('component_rules_test_roles')->access(), 'Anonymous user can\'t use the rule component.'); } /** - * Test passing arguments by reference to an action. + * Tests passing arguments by reference to an action. */ - function testPassingByReference() { + public function testPassingByReference() { // Keeping references of variables is unsupported, though the // EntityMetadataArrayObject may be used to achieve that. $array = array('foo' => 'bar'); @@ -383,9 +416,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test sorting rule elements. + * Tests sorting rule elements. */ - function testSorting() { + public function testSorting() { $rule = $this->createTestRule(); $conditions = $rule->conditions(); $conditions[0]->weight = 10; @@ -404,7 +437,7 @@ class RulesTestCase extends DrupalWebTestCase { /** * Tests using data selectors. */ - function testDataSelectors() { + public function testDataSelectors() { $body[LANGUAGE_NONE][0] = array('value' => 'The body & nothing.'); $node = $this->drupalCreateNode(array('body' => $body, 'type' => 'page', 'summary' => '')); @@ -445,7 +478,7 @@ class RulesTestCase extends DrupalWebTestCase { $this->fail("Validation hasn't created an exception."); } catch (RulesIntegrityException $e) { - $this->pass("Validation error correctly detected: ". $e); + $this->pass("Validation error correctly detected: " . $e); } // Test auto creation of nested data structures, like the node body field. @@ -485,7 +518,7 @@ class RulesTestCase extends DrupalWebTestCase { /** * Tests making use of rule sets. */ - function testRuleSets() { + public function testRuleSets() { $set = rules_rule_set(array( 'node' => array('type' => 'node', 'label' => 'node'), )); @@ -520,7 +553,7 @@ class RulesTestCase extends DrupalWebTestCase { /** * Tests invoking components from the action. */ - function testComponentInvocations() { + public function testComponentInvocations() { $set = rules_rule_set(array( 'node1' => array('type' => 'node', 'label' => 'node'), )); @@ -568,12 +601,12 @@ class RulesTestCase extends DrupalWebTestCase { $this->assertTrue($node->status == 1, 'Component provided in code has been executed.'); } - /** - * Test asserting metadata, customizing action info and make sure integrity - * is checked. + * Tests asserting metadata. + * + * Customizes action info and makes sure integrity is checked. */ - function testMetadataAssertion() { + public function testMetadataAssertion() { $action = rules_action('rules_node_make_sticky_action'); // Test failing integrity check. @@ -591,9 +624,9 @@ class RulesTestCase extends DrupalWebTestCase { // Test asserting additional metadata. $rule = rule(array('node' => array('type' => 'node'))); // Customize action info using the settings. - $rule->condition('data_is', array('data:select' => 'node:type', 'value' => 'page')) + $rule->condition('data_is', array('data:select' => 'node:type', 'value' => 'page')) // Configure an condition using the body. As the body is a field, - // tis requires the bundle to be correctly asserted. + // this requires the bundle to be correctly asserted. ->condition(rules_condition('data_is', array('data:select' => 'node:body:value', 'value' => 'foo'))->negate()) // The action also requires the page bundle in order to work. ->action($action); @@ -604,7 +637,6 @@ class RulesTestCase extends DrupalWebTestCase { $rule->execute($node); $this->assertTrue($node->sticky, 'Rule with asserted metadata executed.'); - // Test asserting metadata on a derived property, i.e. not a variable. $rule = rule(array('node' => array('type' => 'node'))); $rule->condition('entity_is_of_type', array('entity:select' => 'node:reference', 'type' => 'node')) @@ -653,9 +685,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test using loops. + * Tests using loops. */ - function testLoops() { + public function testLoops() { // Test passing the list parameter as argument to ensure that is working // generally for plugin container too. drupal_get_messages(NULL, TRUE); @@ -678,7 +710,7 @@ class RulesTestCase extends DrupalWebTestCase { 'list' => array( 'type' => 'list', 'label' => 'A list of nodes', - ) + ), )); $loop = rules_loop(array('list:select' => 'list', 'item:var' => 'node')); $loop->action('data_set', array('data:select' => 'node:sticky', 'value' => TRUE)); @@ -694,9 +726,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test access checks. + * Tests access checks. */ - function testAccessCheck() { + public function testAccessCheck() { $rule = rule(); // Try to set a property which is provided by the test module and is not // accessible, so the access check has to return FALSE. @@ -705,9 +737,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test returning provided variables. + * Tests returning provided variables. */ - function testReturningVariables() { + public function testReturningVariables() { $node = $this->drupalCreateNode(); $action = rules_action('entity_fetch', array('type' => 'node', 'id' => $node->nid)); list($node2) = $action->execute(); @@ -750,7 +782,7 @@ class RulesTestCase extends DrupalWebTestCase { /** * Tests using input evaluators. */ - function testInputEvaluators() { + public function testInputEvaluators() { $node = $this->drupalCreateNode(array('title' => 'The body & nothing.', 'type' => 'page')); $rule = rule(array('nid' => array('type' => 'integer'))); @@ -773,9 +805,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test importing and exporting a rule. + * Tests importing and exporting a rule. */ - function testRuleImportExport() { + public function testRuleImportExport() { $rule = rule(array('nid' => array('type' => 'integer'))); $rule->name = "rules_export_test"; $rule->action('rules_action_load_node') @@ -876,9 +908,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Test the named parameter mode. + * Tests the named parameter mode. */ - function testNamedParameters() { + public function testNamedParameters() { $rule = rule(array('node' => array('type' => 'node'))); $rule->action('rules_action_node_set_title', array('title' => 'foo')); $rule->integrityCheck(); @@ -891,9 +923,9 @@ class RulesTestCase extends DrupalWebTestCase { } /** - * Make sure Rules aborts when NULL values are used. + * Makes sure Rules aborts when NULL values are used. */ - function testAbortOnNULLValues() { + public function testAbortOnNULLValues() { $rule = rule(array('node' => array('type' => 'node'))); $rule->action('drupal_message', array('message:select' => 'node:log')); $rule->integrityCheck(); @@ -907,6 +939,7 @@ class RulesTestCase extends DrupalWebTestCase { $msg = RulesTestCase::t('The variable or parameter %message is empty.', array('message')); $this->assertTrue(strpos($text, $msg) !== FALSE, 'Evaluation aborted due to an empty argument value.'); } + } /** @@ -914,7 +947,10 @@ class RulesTestCase extends DrupalWebTestCase { */ class RulesTestDataCase extends DrupalWebTestCase { - static function getInfo() { + /** + * Declares test metadata. + */ + public static function getInfo() { return array( 'name' => 'Rules Data tests', 'description' => 'Tests rules data saving and type matching.', @@ -922,9 +958,12 @@ class RulesTestDataCase extends DrupalWebTestCase { ); } - function setUp() { + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { parent::setUp('rules', 'rules_test'); - variable_set('rules_debug_log', 1); + variable_set('rules_debug_log', TRUE); // Make sure we don't ran over issues with the node_load static cache. entity_get_controller('node')->resetCache(); } @@ -932,7 +971,7 @@ class RulesTestDataCase extends DrupalWebTestCase { /** * Tests intelligently saving data. */ - function testDataSaving() { + public function testDataSaving() { $node = $this->drupalCreateNode(); $state = new RulesState(rule()); $state->addVariable('node', $node, array('type' => 'node')); @@ -965,9 +1004,9 @@ class RulesTestDataCase extends DrupalWebTestCase { } /** - * Test type matching + * Tests type matching. */ - function testTypeMatching() { + public function testTypeMatching() { $entity = array('type' => 'entity'); $node = array('type' => 'node'); $this->assertTrue(RulesData::typesMatch($node, $entity), 'Types match.'); @@ -987,7 +1026,7 @@ class RulesTestDataCase extends DrupalWebTestCase { /** * Tests making use of custom wrapper classes. */ - function testCustomWrapperClasses() { + public function testCustomWrapperClasses() { // Test loading a vocabulary by name, which is done by a custom wrapper. $set = rules_action_set(array('vocab' => array('type' => 'taxonomy_vocabulary')), array('vocab')); $set->action('drupal_message', array('message:select' => 'vocab:name')); @@ -1017,7 +1056,7 @@ class RulesTestDataCase extends DrupalWebTestCase { /** * Makes sure the RulesIdentifiableDataWrapper is working correctly. */ - function testRulesIdentifiableDataWrapper() { + public function testRulesIdentifiableDataWrapper() { $node = $this->drupalCreateNode(); $wrapper = new RulesTestTypeWrapper('rules_test_type', $node); $this->assertTrue($wrapper->value() == $node, 'Data correctly wrapped.'); @@ -1039,7 +1078,7 @@ class RulesTestDataCase extends DrupalWebTestCase { $this->fail("Loading hasn't created an exception."); } catch (EntityMetadataWrapperException $e) { - $this->pass("Exception was thrown: ". $e->getMessage()); + $this->pass("Exception was thrown: " . $e->getMessage()); } // Test saving a savable custom, identifiable wrapper. @@ -1052,15 +1091,18 @@ class RulesTestDataCase extends DrupalWebTestCase { $node = node_load($node->nid, NULL, TRUE); $this->assertEqual($node->status, 1, 'Savable non-entity has been saved.'); } -} +} /** * Test triggering rules. */ class RulesTriggerTestCase extends DrupalWebTestCase { - static function getInfo() { + /** + * Declares test metadata. + */ + public static function getInfo() { return array( 'name' => 'Reaction Rules', 'description' => 'Tests triggering reactive rules.', @@ -1068,12 +1110,18 @@ class RulesTriggerTestCase extends DrupalWebTestCase { ); } - function setUp() { + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { parent::setUp('rules', 'rules_test'); RulesLog::logger()->clear(); - variable_set('rules_debug_log', 1); + variable_set('rules_debug_log', TRUE); } + /** + * Helper function to create a test Rule. + */ protected function createTestRule($action = TRUE, $event = 'node_presave') { $rule = rules_reaction_rule(); $rule->event($event) @@ -1088,20 +1136,19 @@ class RulesTriggerTestCase extends DrupalWebTestCase { /** * Tests CRUD for reaction rules - making sure the events are stored properly. */ - function testReactiveRuleCreation() { + public function testReactiveRuleCreation() { $rule = $this->createTestRule(); $rule->save(); $result = db_query("SELECT event FROM {rules_trigger} WHERE id = :id", array(':id' => $rule->id)); $this->assertEqual($result->fetchField(), 'node_presave', 'Associated event has been saved.'); // Try updating. - $events =& $rule->events(); - unset($events[0]); - $events[] = 'node_insert'; - $events[] = 'node_update'; + $rule->removeEvent('node_presave'); + $rule->event('node_insert'); + $rule->event('node_update'); $rule->active = FALSE; $rule->integrityCheck()->save(); $result = db_query("SELECT event FROM {rules_trigger} WHERE id = :id", array(':id' => $rule->id)); - $this->assertEqual($result->fetchCol(), array_values($events), 'Updated associated events.'); + $this->assertEqual($result->fetchCol(), array_values($rule->events()), 'Updated associated events.'); // Try deleting. $rule->delete(); $result = db_query("SELECT event FROM {rules_trigger} WHERE id = :id", array(':id' => $rule->id)); @@ -1111,7 +1158,7 @@ class RulesTriggerTestCase extends DrupalWebTestCase { /** * Tests creating and triggering a basic reaction rule. */ - function testBasicReactionRule() { + public function testBasicReactionRule() { $node = $this->drupalCreateNode(array('type' => 'page')); $rule = $this->createTestRule(); $rule->integrityCheck()->save(); @@ -1126,13 +1173,13 @@ class RulesTriggerTestCase extends DrupalWebTestCase { RulesLog::logger()->checkLog(); $this->assertFalse(node_load($nid), 'Rule successfully triggered and executed'); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); } /** - * Test a rule using a handler to load a variable. + * Tests a rule using a handler to load a variable. */ - function testVariableHandler() { + public function testVariableHandler() { $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); $rule = $this->createTestRule(FALSE, 'node_update'); $rule->action('rules_node_publish_action_save', array('node:select' => 'node_unchanged')); @@ -1153,19 +1200,19 @@ class RulesTriggerTestCase extends DrupalWebTestCase { $this->assertFalse($node->sticky, 'Parameter has been loaded and saved.'); $this->assertTrue($node->status, 'Action has been executed.'); - // Ensure the rule was evaluated a second time + // Ensure the rule was evaluated a second time. $text = RulesLog::logger()->render(); $msg = RulesTestCase::t('Evaluating conditions of rule %rule 1', array('rule 1')); $pos = strpos($text, $msg); $pos = ($pos !== FALSE) ? strpos($text, $msg, $pos) : FALSE; $this->assertTrue($pos !== FALSE, "Recursion prevented."); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); } /** - * Test aborting silently when handlers are not able to load. + * Tests aborting silently when handlers are not able to load. */ - function testVariableHandlerFailing() { + public function testVariableHandlerFailing() { $rule = $this->createTestRule(FALSE, 'node_presave'); $rule->action('rules_node_publish_action_save', array('node:select' => 'node_unchanged')); $rule->integrityCheck()->save(); @@ -1173,16 +1220,18 @@ class RulesTriggerTestCase extends DrupalWebTestCase { // On insert it's not possible to get the unchanged node during presave. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); $text = RulesTestCase::t('Unable to load variable %node_unchanged, aborting.', array('node_unchanged')); $this->assertTrue(strpos(RulesLog::logger()->render(), $text) !== FALSE, "Aborted evaluation."); } /** - * Tests preventing recursive rule invocations by creating a rule that reacts - * on node-update and generates a node update that would trigger it itself. + * Tests preventing recursive rule invocations. + * + * Creates a rule that reacts on node-update then generates a node update + * that would trigger it itself. */ - function testRecursionPrevention() { + public function testRecursionPrevention() { $rule = $this->createTestRule(FALSE, 'node_update'); $rule->action('rules_node_make_sticky_action'); $rule->integrityCheck()->save(); @@ -1192,17 +1241,19 @@ class RulesTriggerTestCase extends DrupalWebTestCase { node_save($node); $text = RulesTestCase::t('Not evaluating reaction rule %label to prevent recursion.', array('label' => $rule->name)); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); $this->assertTrue((strpos(RulesLog::logger()->render(), $text) !== FALSE), "Recursion prevented."); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); } /** - * Ensure the recursion prevention still allows to let the rule trigger again + * Tests recursion prevention with altered arguments. + * + * Ensure the recursion prevention still allows the rule to trigger again * during evaluation of the same event set, if the event isn't caused by the - * rule itself - thus we won't run in an infinte loop. + * rule itself - thus we won't run in an infinite loop. */ - function testRecursionOnDifferentArguments() { + public function testRecursionOnDifferentArguments() { // Create rule1 - which might recurse. $rule = $this->createTestRule(FALSE, 'node_update'); $rule->action('rules_node_make_sticky_action'); @@ -1221,7 +1272,7 @@ class RulesTriggerTestCase extends DrupalWebTestCase { $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); node_save($node); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); $text = RulesLog::logger()->render(); $pos = strpos($text, RulesTestCase::t('Evaluating conditions of rule %rule 1', array('rule 1'))); $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Evaluating conditions of rule %rule 2', array('rule 2')), $pos) : FALSE; @@ -1234,9 +1285,10 @@ class RulesTriggerTestCase extends DrupalWebTestCase { /** * Tests the provided default rule 'rules_test_default_1'. */ - function testDefaultRule() { + public function testDefaultRule() { $rule = rules_config_load('rules_test_default_1'); $this->assertTrue($rule->status & ENTITY_IN_CODE && !($rule->status & ENTITY_IN_DB), 'Default rule can be loaded and has the right status.'); + $this->assertTrue($rule->tags == array('Admin', 'Tag2'), 'Default rule has correct tags.'); // Enable. $rule->active = TRUE; $rule->save(); @@ -1251,6 +1303,34 @@ class RulesTriggerTestCase extends DrupalWebTestCase { $msg = drupal_get_messages(); $this->assertEqual($msg['status'][0], 'A node has been updated.', 'Default rule has been triggered.'); } + + /** + * Tests creating and triggering a reaction rule with event settings. + */ + public function testEventSettings() { + $rule = rules_reaction_rule(); + $rule->event('node_presave', array('bundle' => 'article')) + ->condition('data_is_empty', array('data:select' => 'node:field-tags')) + ->action('node_publish', array('node:select' => 'node')); + $rule->integrityCheck()->save(); + + $node = $this->drupalCreateNode(array('type' => 'page', 'status' => 0)); + $this->assertEqual($node->status, 0, 'Rule has not been triggered.'); + $node = $this->drupalCreateNode(array('type' => 'article', 'status' => 0)); + $this->assertEqual($node->status, 1, 'Rule has been triggered.'); + RulesLog::logger()->checkLog(); + + // Make sure an invalid bundle raises integrity problems. + $rule->event('node_presave', array('bundle' => 'invalid')); + try { + $rule->integrityCheck(); + $this->fail('Integrity check failed.'); + } + catch (RulesIntegrityException $e) { + $this->pass('Integrity check failed: ' . $e); + } + } + } /** @@ -1258,7 +1338,10 @@ class RulesTriggerTestCase extends DrupalWebTestCase { */ class RulesIntegrationTestCase extends DrupalWebTestCase { - static function getInfo() { + /** + * Declares test metadata. + */ + public static function getInfo() { return array( 'name' => 'Rules Core Integration', 'description' => 'Tests provided integration for drupal core.', @@ -1266,16 +1349,19 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { ); } - function setUp() { + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { parent::setUp('rules', 'rules_test', 'php', 'path'); RulesLog::logger()->clear(); - variable_set('rules_debug_log', 1); + variable_set('rules_debug_log', TRUE); } /** - * Just make sure the access callback run without errors. + * Just makes sure the access callback run without errors. */ - function testAccessCallbacks() { + public function testAccessCallbacks() { $cache = rules_get_cache(); foreach (array('action', 'condition', 'event') as $type) { foreach (rules_fetch_data($type . '_info') as $name => $info) { @@ -1287,9 +1373,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test data integration. + * Tests data integration. */ - function testDataIntegration() { + public function testDataIntegration() { // Test data_create action. $action = rules_action('data_create', array( 'type' => 'log_entry', @@ -1305,14 +1391,13 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $pos = strpos($text, RulesTestCase::t('Added the provided variable %data_created of type %log_entry', array('data_created', 'log_entry'))); $this->assertTrue($pos !== FALSE, 'Data of type log entry has been created.'); - // Test variable_add action. $action = rules_action('variable_add', array( 'type' => 'text_formatted', 'value' => array( 'value' => 'test text', 'format' => 1, - ) + ), )); $action->access(); $action->execute(); @@ -1320,13 +1405,12 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $pos = strpos($text, RulesTestCase::t('Added the provided variable %variable_added of type %text_formatted', array('variable_added', 'text_formatted'))); $this->assertTrue($pos !== FALSE, 'Data of type text formatted has been created.'); - // Test using the list actions. $rule = rule(array( 'list' => array( 'type' => 'list', 'label' => 'A list of text', - ) + ), )); $rule->action('list_add', array('list:select' => 'list', 'item' => 'bar2')); $rule->action('list_add', array('list:select' => 'list', 'item' => 'bar', 'pos' => 'start')); @@ -1338,7 +1422,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $this->assertEqual($list->value(), array('bar', 'foo', 'foo2'), 'List items removed and added.'); $this->assertFalse(rules_condition('list_contains')->execute($list, 'foo-bar'), 'Condition "List item contains" evaluates to FALSE'); $this->assertTrue(rules_condition('list_contains')->execute($list, 'foo'), 'Condition "List item contains" evaluates to TRUE'); - //debug(RulesLog::logger()->render()); + // debug(RulesLog::logger()->render()); // Test data_is condition with IN operation. $rule = rule(array('node' => array('type' => 'node'))); @@ -1348,8 +1432,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $node = $this->drupalCreateNode(array('title' => 'foo')); $rule->execute($node); - $this->assertEqual($node->title, 'bar', "Data comparision using IN operation evaluates to TRUE."); - + $this->assertEqual($node->title, 'bar', "Data comparison using IN operation evaluates to TRUE."); // Test Condition: Data is empty. $rule = rule(array('node' => array('type' => 'node'))); @@ -1420,7 +1503,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $set->action('data_convert', array('type' => 'integer', 'value:select' => 'source', 'rounding_behavior' => 'down')); $set->action('data_set', array('data:select' => 'result', 'value:select' => 'conversion_result')); list($result) = $set->execute('9.6'); - $this->assertEqual($result, 9, 'Converted decimal to integer using roundin behavio down.'); + $this->assertEqual($result, 9, 'Converted decimal to integer using rounding behavior down.'); $set = rules_action_set(array( 'result' => array('type' => 'integer', 'parameter' => FALSE), @@ -1452,7 +1535,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { /** * Tests entity related integration. */ - function testEntityIntegration() { + public function testEntityIntegration() { global $user; $page = $this->drupalCreateNode(array('type' => 'page')); @@ -1462,7 +1545,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { ->execute(entity_metadata_wrapper('node', $article), 'field_tags'); $this->assertTrue($result); - // Test entiy_is_of_bundle condition. + // Test entity_is_of_bundle condition. $result = rules_condition('entity_is_of_bundle', array( 'type' => 'node', 'bundle' => array('article'), @@ -1534,9 +1617,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Saved %node of type %node.', array('node')), $pos) : FALSE; $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Evaluating the action %entity_delete.', array('entity_delete')), $pos) : FALSE; $this->assertTrue($pos !== FALSE, 'Data has been fetched, saved and deleted.'); - //debug(RulesLog::logger()->render()); - - + // debug(RulesLog::logger()->render()); $node = entity_property_values_create_entity('node', array( 'type' => 'article', @@ -1573,7 +1654,6 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $this->assertEqual(entity_metadata_wrapper('node', $node->nid)->title->value(), 'bar', 'Entity is of type condition correctly asserts the entity type.'); - // Test the entity_query action. $node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'foo2')); $rule = rule(); @@ -1589,9 +1669,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test integration for the taxonomy module. + * Tests integration for the taxonomy module. */ - function testTaxonomyIntegration() { + public function testTaxonomyIntegration() { $term = entity_property_values_create_entity('taxonomy_term', array( 'name' => $this->randomName(), 'vocabulary' => 1, @@ -1685,9 +1765,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test integration for the node module. + * Tests integration for the node module. */ - function testNodeIntegration() { + public function testNodeIntegration() { $tests = array( array('node_unpublish', 'node_is_published', 'node_publish', 'status'), array('node_make_unsticky', 'node_is_sticky', 'node_make_sticky', 'sticky'), @@ -1700,15 +1780,15 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { rules_action($action1)->execute($node); $node = node_load($node->nid, NULL, TRUE); - $this->assertFalse($node->$property, 'Action has permanently disabled node '. $property); + $this->assertFalse($node->$property, 'Action has permanently disabled node ' . $property); $return = rules_condition($condition)->execute($node); - $this->assertFalse($return, 'Condition determines node '. $property . ' is disabled.'); + $this->assertFalse($return, 'Condition determines node ' . $property . ' is disabled.'); rules_action($action2)->execute($node); $node = node_load($node->nid, NULL, TRUE); - $this->assertTrue($node->$property, 'Action has permanently enabled node '. $property); + $this->assertTrue($node->$property, 'Action has permanently enabled node ' . $property); $return = rules_condition($condition)->execute($node); - $this->assertTrue($return, 'Condition determines node '. $property . ' is enabled.'); + $this->assertTrue($return, 'Condition determines node ' . $property . ' is enabled.'); } $return = rules_condition('node_is_of_type', array('type' => array('page', 'article')))->execute($node); @@ -1716,7 +1796,6 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $return = rules_condition('node_is_of_type', array('type' => array('article')))->execute($node); $this->assertFalse($return, 'Condition determines node is not of type article.'); - // Test auto saving of a new node after it has been inserted into the DB. $rule = rules_reaction_rule(); $rand = $this->randomName(); @@ -1730,9 +1809,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test integration for the user module. + * Tests integration for the user module. */ - function testUserIntegration() { + public function testUserIntegration() { $rid = $this->drupalCreateRole(array('administer nodes'), 'foo'); $user = $this->drupalCreateUser(); @@ -1779,9 +1858,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test integration for the php module. + * Tests integration for the php module. */ - function testPHPIntegration() { + public function testPHPIntegration() { $node = $this->drupalCreateNode(array('title' => 'foo')); $rule = rule(array('var_name' => array('type' => 'node'))); $rule->condition('php_eval', array('code' => 'return TRUE;')) @@ -1795,12 +1874,12 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $this->assertEqual(array_pop($msg['status']), "Title: foo Token: foo", 'PHP input evaluation has been applied.'); $this->assertEqual(array_pop($msg['status']), "Executed-foo", 'PHP code condition and action have been evaluated.'); - // Test PHP data processor + // Test PHP data processor. $rule = rule(array('var_name' => array('type' => 'node'))); $rule->action('drupal_message', array( 'message:select' => 'var_name:title', 'message:process' => array( - 'php' => array('code' => 'return "Title: $value";') + 'php' => array('code' => 'return "Title: $value";'), ), )); $rule->execute($node); @@ -1811,9 +1890,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test the "rules_core" integration. + * Tests the "rules_core" integration. */ - function testRulesCoreIntegration() { + public function testRulesCoreIntegration() { // Make sure the date input evaluator evaluates properly using strtotime(). $node = $this->drupalCreateNode(array('title' => 'foo')); $rule = rule(array('node' => array('type' => 'node'))); @@ -1882,9 +1961,9 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { } /** - * Test site/system integration. + * Tests site/system integration. */ - function testSystemIntegration() { + public function testSystemIntegration() { // Test using the 'site' variable. $condition = rules_condition('data_is', array('data:select' => 'site:current-user:name', 'value' => $GLOBALS['user']->name)); $this->assertTrue($condition->execute(), 'Retrieved the current user\'s name.'); @@ -1921,7 +2000,6 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { $this->drupalGet('node/' . $node->nid); $this->assertEqual($this->getUrl(), url('user', array('absolute' => TRUE, 'fragment' => 'fragment')), 'Redirect has been issued.'); - // Test sending mail. $settings = array('to' => 'mail@example.com', 'subject' => 'subject', 'message' => 'hello.'); rules_action('mail', $settings)->execute(); @@ -1931,15 +2009,44 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { rules_action('mail', $settings + array('from' => 'sender@example.com'))->execute(); $this->assertMail('from', 'sender@example.com', 'Specified from address has been used'); - // Test sending mail to all users of a role. First make sure there is a - // custom role and a user for it. - $user = $this->drupalCreateUser(array('administer nodes')); - $roles = $user->roles; - // Remove the authenticate role so we only use the new role created by + // Test sending mail to all users of a role. First clear the mail + // collector to remove the mail sent in the previous line of code. + variable_set('drupal_test_email_collector', array()); + + // Now make sure there is a custom role and two users with that role. + $user1 = $this->drupalCreateUser(array('administer nodes')); + $roles = $user1->roles; + // Remove the authenticated role so we only use the new role created by // drupalCreateUser(). unset($roles[DRUPAL_AUTHENTICATED_RID]); + + // Now create a second user with the same role. + $user2 = $this->drupalCreateUser(); + user_save($user2, array('roles' => $roles)); + + // Now create a third user without the same role - this user should NOT + // receive the role email. + $user3 = $this->drupalCreateUser(array('administer blocks')); + $additional_roles = $user3->roles; + unset($additional_roles[DRUPAL_AUTHENTICATED_RID]); + + // Execute action and check that only two mails were sent. rules_action('mail_to_users_of_role', $settings + array('roles' => array_keys($roles)))->execute(); - $this->assertMail('to', $user->mail, 'Mail to users of a role has been sent.'); + $mails = $this->drupalGetMails(); + $this->assertEqual(count($mails), 2, '2 e-mails were sent to users of a role.'); + + // Check each mail to ensure that only $user1 and $user2 got the mail. + $mail = array_pop($mails); + $this->assertTrue($mail['to'] == $user2->mail, 'Mail to user of a role has been sent.'); + $mail = array_pop($mails); + $this->assertTrue($mail['to'] == $user1->mail, 'Mail to user of a role has been sent.'); + + // Execute action again, this time to send mail to both roles. + // This time check that three mails were sent - one for each user.. + variable_set('drupal_test_email_collector', array()); + rules_action('mail_to_users_of_role', $settings + array('roles' => array_keys($roles + $additional_roles)))->execute(); + $mails = $this->drupalGetMails(); + $this->assertEqual(count($mails), 3, '3 e-mails were sent to users of multiple roles.'); // Test reacting on new log entries and make sure the log entry is usable. $rule = rules_reaction_rule(); @@ -1955,7 +2062,7 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { /** * Tests the path module integration. */ - function testPathIntegration() { + public function testPathIntegration() { rules_action('path_alias')->execute('foo', 'bar'); $path = path_load('foo'); $this->assertTrue($path['alias'] == 'bar', 'URL alias has been created.'); @@ -1983,4 +2090,126 @@ class RulesIntegrationTestCase extends DrupalWebTestCase { RulesLog::logger()->checkLog(); } + +} + +/** + * Tests event dispatcher functionality. + */ +class RulesEventDispatcherTestCase extends DrupalWebTestCase { + + /** + * Declares test metadata. + */ + public static function getInfo() { + return array( + 'name' => 'Rules event dispatchers', + 'description' => 'Tests event dispatcher functionality.', + 'group' => 'Rules', + ); + } + + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { + parent::setUp('rules', 'rules_test'); + } + + /** + * Tests start and stop functionality. + */ + public function testStartAndStop() { + $handler = rules_get_event_handler('rules_test_event'); + $rule = rules_reaction_rule(); + $rule->event('rules_test_event'); + + // The handler should not yet be watching. + $this->assertFalse($handler->isWatching()); + + // Once saved, the event cache rebuild should start the watcher. + $rule->save(); + RulesEventSet::rebuildEventCache(); + $this->assertTrue($handler->isWatching()); + + // Deleting should stop the watcher. + $rule->delete(); + $this->assertFalse($handler->isWatching()); + } + + /** + * Tests start and stop functionality when used with multiple events. + */ + public function testStartAndStopMultiple() { + $handler = rules_get_event_handler('rules_test_event'); + + // Initially, the task handler should not be watching. + $this->assertFalse($handler->isWatching()); + + // Set up five rules that all use the same event. + $rules = array(); + foreach (array(1, 2, 3, 4, 5) as $key) { + $rules[$key] = rules_reaction_rule(); + $rules[$key]->event('rules_test_event'); + $rules[$key]->save(); + } + + // Once saved, the event cache rebuild should start the watcher. + RulesEventSet::rebuildEventCache(); + $this->assertTrue($handler->isWatching()); + + // It should continue watching until all events are deleted. + foreach ($rules as $key => $rule) { + $rule->delete(); + $this->assertEqual($key !== 5, $handler->isWatching()); + } + } + +} + +/** + * Test early bootstrap Rules invocation. + */ +class RulesInvocationEnabledTestCase extends DrupalWebTestCase { + + /** + * Declares test metadata. + */ + public static function getInfo() { + return array( + 'name' => 'Rules invocation enabled', + 'description' => 'Tests that Rules events are enabled during menu item loads.', + 'group' => 'Rules', + ); + } + + /** + * Overrides DrupalWebTestCase::setUp(). + */ + protected function setUp() { + parent::setUp('dblog', 'rules', 'rules_test', 'rules_test_invocation'); + } + + /** + * Tests that a Rules event is triggered on node menu item loading. + * + * @see rules_test_invocation_node_load() + */ + public function testInvocationOnNodeMenuLoading() { + // Create a test node. + $node = $this->drupalCreateNode(array('title' => 'Test')); + // Enable Rules logging on the INFO level so that entries are written to + // dblog. + variable_set('rules_log_errors', RulesLog::INFO); + // Create an empty rule that will fire in our node load hook. + $rule = rules_reaction_rule(); + $rule->event('rules_test_event'); + $rule->save('test_rule'); + + // Visit the node page which should trigger the load hook. + $this->drupalGet('node/' . $node->nid); + $result = db_query("SELECT * FROM {watchdog} WHERE type = 'rules' AND message = 'Reacting on event %label.'")->fetch(); + $this->assertFalse(empty($result), 'Rules event was triggered and logged.'); + } + } diff --git a/sites/all/modules/contrib/admin/rules/tests/rules_test.info b/sites/all/modules/contrib/admin/rules/tests/rules_test.info index 6d149eec..740f0a62 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules_test.info +++ b/sites/all/modules/contrib/admin/rules/tests/rules_test.info @@ -3,12 +3,10 @@ description = "Support module for the Rules tests." package = Testing core = 7.x files[] = rules_test.rules.inc -files[] = rules_test.rules_defaults.inc hidden = TRUE -; Information added by drupal.org packaging script on 2013-03-27 -version = "7.x-2.3" +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1364401818" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/tests/rules_test.module b/sites/all/modules/contrib/admin/rules/tests/rules_test.module index 3abecd72..4c54fafe 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules_test.module +++ b/sites/all/modules/contrib/admin/rules/tests/rules_test.module @@ -1,7 +1,8 @@ nid); } +/** + * Access callback. Returns TRUE except when $op == 'edit'. + */ function rules_test_no_access($op) { return $op == 'edit' ? FALSE : TRUE; } diff --git a/sites/all/modules/contrib/admin/rules/tests/rules_test.rules.inc b/sites/all/modules/contrib/admin/rules/tests/rules_test.rules.inc index 0c9362b3..3f302f46 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules_test.rules.inc +++ b/sites/all/modules/contrib/admin/rules/tests/rules_test.rules.inc @@ -1,9 +1,21 @@ array( + 'label' => t('Test event'), + 'class' => 'RulesTestEventHandler', + ), + ); +} /** * Implements hook_rules_file_info(). @@ -42,6 +54,10 @@ function rules_test_rules_condition_info() { 'label' => t('Test condition returning false'), 'group' => t('Rules test'), ); + $items['rules_test_condition_apostrophe'] = array( + 'label' => t("Test use of an apostrophe (') in a condition label"), + 'group' => t('Rules test'), + ); // A condition for testing passing entities wrapped. $items['rules_test_condition_node_wrapped'] = array( 'label' => t('Content is published'), @@ -76,6 +92,20 @@ function rules_test_condition_false() { return FALSE; } +/** + * Condition testing use of an apostrophe in a condition label. + * + * Specifically, we want to ensure that special characters do not show up as + * HTML-encoded in the user interface. + */ +function rules_test_condition_apostrophe($settings, $state, $element) { + if (!$element instanceof RulesCondition) { + throw new Exception('Rules element has not been passed to condition.'); + } + rules_log('condition apostrophe called'); + return TRUE; +} + /** * Condition implementation receiving the node wrapped. */ @@ -189,7 +219,11 @@ function rules_test_rules_action_info() { 'base' => 'rules_test_type_save', 'label' => t('Save test type'), 'parameter' => array( - 'node' => array('type' => 'rules_test_type', 'label' => t('Test content'), 'save' => TRUE), + 'node' => array( + 'type' => 'rules_test_type', + 'label' => t('Test content'), + 'save' => TRUE, + ), ), 'group' => t('Node'), ), @@ -203,6 +237,37 @@ function rules_test_action() { rules_log('action called'); } +/** + * Action for testing writing class-based actions. + */ +class RulesTestClassAction extends RulesActionHandlerBase { + + /** + * Defines the action. + */ + public static function getInfo() { + return array( + 'name' => 'rules_test_class_action', + 'label' => t('Test class based action'), + 'group' => t('Node'), + 'parameter' => array( + 'node' => array( + 'type' => 'node', + 'label' => t('Node'), + ), + ), + ); + } + + /** + * Executes the action. + */ + public function execute($node) { + rules_log('Action called with node ' . $node->nid); + } + +} + /** * Implements hook_rules_data_info(). */ @@ -230,15 +295,88 @@ function rules_test_rules_data_info_alter(&$data_info) { */ class RulesTestTypeWrapper extends RulesIdentifiableDataWrapper implements RulesDataWrapperSavableInterface { + /** + * Overrides RulesIdentifiableDataWrapper::extractIdentifier(). + */ protected function extractIdentifier($data) { return $data->nid; } + /** + * Overrides RulesIdentifiableDataWrapper::load(). + */ protected function load($id) { return node_load($id); } + /** + * Implements RulesDataWrapperSavableInterface::save(). + */ public function save() { node_save($this->value()); } + +} + +/** + * Implements hook_rules_plugin_info(). + */ +function rules_test_rules_plugin_info() { + return array( + 'rules test container' => array( + 'label' => t('Test container'), + 'class' => 'RulesTestContainer', + 'embeddable' => 'RulesActionContainer', + ), + ); +} + +/** + * Test container plugin. + */ +class RulesTestContainer extends RulesContainerPlugin { + protected $itemName = 'rules test container'; + + /** + * Evaluate the element on a given rules evaluation state. + */ + public function evaluate(RulesState $state) { + // Do nothing. + } + +} + +/** + * Test event handler class. + */ +class RulesTestEventHandler extends RulesEventDefaultHandler implements RulesEventDispatcherInterface { + + /** + * Name of the variable in which to store the state of the event handler. + * + * @var string + */ + protected $variableName = 'rules_test_event_handler_watch'; + + /** + * Implements RulesEventDispatcherInterface::startWatching(). + */ + public function startWatching() { + variable_set($this->variableName, TRUE); + } + + /** + * Implements RulesEventDispatcherInterface::stopWatching(). + */ + public function stopWatching() { + variable_set($this->variableName, FALSE); + } + + /** + * Implements RulesEventDispatcherInterface::isWatching(). + */ + public function isWatching() { + return (bool) variable_get($this->variableName); + } + } diff --git a/sites/all/modules/contrib/admin/rules/tests/rules_test.rules_defaults.inc b/sites/all/modules/contrib/admin/rules/tests/rules_test.rules_defaults.inc index 8b792ad1..8e9f0f13 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules_test.rules_defaults.inc +++ b/sites/all/modules/contrib/admin/rules/tests/rules_test.rules_defaults.inc @@ -1,16 +1,18 @@ label = 'example default rule'; + // Add rules tags. + $rule->tags = array('Admin', 'Tag2'); $rule->active = FALSE; $rule->event('node_update') ->condition(rules_condition('data_is', array('data:select' => 'node:status', 'value' => TRUE))->negate()) @@ -77,9 +79,10 @@ function _rules_export_get_test_export() { "PLUGIN" : "reaction rule", "WEIGHT" : "-1", "ACTIVE" : false, + "OWNER" : "rules", "TAGS" : [ "bar", "baz", "foo" ], "REQUIRES" : [ "rules", "comment" ], - "ON" : [ "comment_insert" ], + "ON" : { "comment_insert" : [] }, "IF" : [ { "OR" : [ { "NOT node_is_sticky" : { "node" : [ "comment:node" ] } }, diff --git a/sites/all/modules/contrib/admin/rules/tests/rules_test.test.inc b/sites/all/modules/contrib/admin/rules/tests/rules_test.test.inc index 2067c805..7c242438 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules_test.test.inc +++ b/sites/all/modules/contrib/admin/rules/tests/rules_test.test.inc @@ -1,7 +1,8 @@ type, $type); } /** - * Condition: Check if the node is published + * Condition: Check if the node is published. */ function rules_condition_content_is_published($node, $settings) { return $node->status == 1; } /** - * Loads a node + * Loads a node. */ function rules_action_load_node($nid, $vid = NULL) { return array('node_loaded' => node_load($nid, $vid ? $vid : NULL)); diff --git a/sites/all/modules/contrib/admin/rules/tests/rules_test_invocation.info b/sites/all/modules/contrib/admin/rules/tests/rules_test_invocation.info index 51aea33e..22b53a8d 100644 --- a/sites/all/modules/contrib/admin/rules/tests/rules_test_invocation.info +++ b/sites/all/modules/contrib/admin/rules/tests/rules_test_invocation.info @@ -4,9 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2015-03-16 -version = "7.x-2.9" +; Information added by Drupal.org packaging script on 2019-01-24 +version = "7.x-2.12" core = "7.x" project = "rules" -datestamp = "1426527210" - +datestamp = "1548305586" diff --git a/sites/all/modules/contrib/admin/rules/ui/rules.autocomplete.js b/sites/all/modules/contrib/admin/rules/ui/rules.autocomplete.js index a888fd51..a6ac9932 100644 --- a/sites/all/modules/contrib/admin/rules/ui/rules.autocomplete.js +++ b/sites/all/modules/contrib/admin/rules/ui/rules.autocomplete.js @@ -81,7 +81,7 @@ Drupal.rules = Drupal.rules || {}; this.jqObject.bind("autocompleteselect", function(event, ui) { // If a group was selected then set the groupSelected to true for the - // overriden close function from jquery autocomplete. + // overridden close function from jquery autocomplete. if (ui.item.value.substring(ui.item.value.length - 1, ui.item.value.length) == ":") { instance.groupSelected = true; } @@ -103,14 +103,18 @@ Drupal.rules = Drupal.rules || {}; }); }); + // Newer versions of jQuery UI use element.data('ui-autocomplete'), older + // versions use element.data('autocomplete'). + var autocompleteDataKey = typeof(this.jqObject.data('autocomplete')) === 'object' ? 'autocomplete' : 'ui-autocomplete'; + // Since jquery autocomplete by default strips html text by using .text() // we need our own _renderItem function to display html content. - this.jqObject.data("autocomplete")._renderItem = function(ul, item) { + this.jqObject.data(autocompleteDataKey)._renderItem = function(ul, item) { return $("
  • ").data("item.autocomplete", item).append("" + item.label + "").appendTo(ul); }; // Override close function - this.jqObject.data("autocomplete").close = function (event) { + this.jqObject.data(autocompleteDataKey).close = function (event) { var value = this.element.val(); // If the selector is not a group, then trigger the close event an and // hide the menu. @@ -119,7 +123,10 @@ Drupal.rules = Drupal.rules || {}; if (this.menu.element.is(":visible")) { this._trigger("close", event); this.menu.element.hide(); - this.menu.deactivate(); + // Use deactivate for older versions of jQuery UI. + if (typeof(this.menu.deactivate) === 'function') { + this.menu.deactivate(); + } } } else { @@ -173,7 +180,7 @@ Drupal.rules = Drupal.rules || {}; }; /** - * Toogle the autcomplete window. + * Toggle the autocomplete window. */ Drupal.rules.autocomplete.prototype.toggle = function() { if (this.jqObject.autocomplete("widget").is(":visible")) { diff --git a/sites/all/modules/contrib/admin/rules/ui/rules.ui.css b/sites/all/modules/contrib/admin/rules/ui/rules.ui.css index b567051e..aa7d2aef 100644 --- a/sites/all/modules/contrib/admin/rules/ui/rules.ui.css +++ b/sites/all/modules/contrib/admin/rules/ui/rules.ui.css @@ -1,10 +1,12 @@ -@CHARSET "UTF-8"; +@charset "utf-8"; -.rules-show-js, html.js .rules-hide-js { +.rules-show-js, +html.js .rules-hide-js { display: none; } -.rules-hide-js, html.js .rules-show-js { +.rules-hide-js, +html.js .rules-show-js { display: block; } @@ -58,7 +60,8 @@ ul.rules-operations-add li { right: 0px; } -.rules-elements-table caption, .rules-overview-table caption { +.rules-elements-table caption, +.rules-overview-table caption { font-size: 110%; font-weight: bold; padding-bottom: 0.5em; @@ -85,8 +88,7 @@ ul.rules-operations-add li { .rules-debug-collapsible-link { position: relative; cursor: pointer; - - /* The span element with the icon which opens the log, has a whitepsace. + /* The span element with the icon which opens the log, has a whitespace. Since we don't want the user to mark this white space, we prevent this using the this code.*/ -moz-user-select: -moz-none; @@ -191,6 +193,6 @@ ul.rules-autocomplete .ui-corner-all { } /* IE 6 hack for max-height. */ -* html ul.rule-autocomplete{ +* html ul.rule-autocomplete { height: 23em; } diff --git a/sites/all/modules/contrib/admin/rules/ui/ui.controller.inc b/sites/all/modules/contrib/admin/rules/ui/ui.controller.inc index dd80a291..e2d1c115 100644 --- a/sites/all/modules/contrib/admin/rules/ui/ui.controller.inc +++ b/sites/all/modules/contrib/admin/rules/ui/ui.controller.inc @@ -1,7 +1,8 @@ 'rules_menu_add_element_title', // Wrap the integer in an array, so it is passed as is. 'title arguments' => array(array($base_count + 4)), @@ -52,7 +53,7 @@ class RulesUIController { 'title callback' => 'rules_get_title', 'title arguments' => array('Adding event to !plugin "!label"', $base_count + 1), 'page callback' => 'drupal_get_form', - 'page arguments' => array('rules_ui_add_event', $base_count + 1, $base_path), + 'page arguments' => array('rules_ui_add_event_page', $base_count + 1, $base_path), 'access callback' => 'rules_config_access', 'access arguments' => array('update', $base_count + 1), 'load arguments' => array($base_count + 1), @@ -60,7 +61,7 @@ class RulesUIController { 'file path' => drupal_get_path('module', 'rules'), ); $items[$base_path . '/manage/%rules_config/delete/event'] = array( - //@todo: improve title. + // @todo Improve title. 'title' => 'Remove event', 'page callback' => 'drupal_get_form', 'page arguments' => array('rules_ui_remove_event', $base_count + 1, $base_count + 4, $base_path), @@ -157,8 +158,10 @@ class RulesUIController { } /** - * Generates the render array for a overview configuration table for arbitrary - * rule configs that match the given conditions. + * Generates the render array for an overview configuration table. + * + * Generates the render array for an overview configuration table for + * arbitrary rule configs that match the given conditions. * * Note: The generated overview table contains multiple links for editing the * rule configurations. For the links to properly work use @@ -182,7 +185,7 @@ class RulesUIController { * currently set RulesPluginUI::$basePath. If no base path has been set * yet, the current path is used by default. * - * @return Array + * @return array * A renderable array. */ public function overviewTable($conditions = array(), $options = array()) { @@ -192,10 +195,14 @@ class RulesUIController { 'show events' => isset($conditions['plugin']) && $conditions['plugin'] == 'reaction rule', 'show execution op' => !(isset($conditions['plugin']) && $conditions['plugin'] == 'reaction rule'), ); + // By default show only configurations owned by rules. + $conditions += array( + 'owner' => 'rules', + ); if (!empty($options['base path'])) { RulesPluginUI::$basePath = $options['base path']; } - else if (!isset(RulesPluginUI::$basePath)) { + elseif (!isset(RulesPluginUI::$basePath)) { // Default to the current path, only if no path has been set yet. RulesPluginUI::$basePath = current_path(); } @@ -237,7 +244,7 @@ class RulesUIController { $table['#attributes']['class'][] = 'rules-overview-table'; $table['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.css'; - // TODO: hide configs where access() is FALSE. + // @todo Hide configs where access() is FALSE. return $table; } @@ -257,8 +264,8 @@ class RulesUIController { $events = array(); if ($config instanceof RulesTriggerableInterface) { foreach ($config->events() as $event_name) { - $this->event_info += array($event_name => array('label' => t('Unknown event "!event_name"', array('!event_name' => $event_name)))); - $events[] = check_plain($this->event_info[$event_name]['label']); + $event_handler = rules_get_event_handler($event_name, $config->getEventSettings($event_name)); + $events[] = $event_handler->summary(); } } $row[] = implode(", ", $events); @@ -275,12 +282,16 @@ class RulesUIController { // Add operations depending on the options and the exportable status. if (!$config->hasStatus(ENTITY_FIXED)) { - $row[] = l(t('edit'), RulesPluginUI::path($name), array('attributes' => array('class' => array('edit', 'action')))); - $row[] = l(t('translate'), RulesPluginUI::path($name, 'translate'), array('attributes' => array('class' => array('translate', 'action')))); + $row[] = l(t('edit'), RulesPluginUI::path($name), array('attributes' => array('class' => array('edit', 'action')))); + if (module_exists('rules_i18n')) { + $row[] = l(t('translate'), RulesPluginUI::path($name, 'translate'), array('attributes' => array('class' => array('translate', 'action')))); + } } else { $row[] = ''; - $row[] = ''; + if (module_exists('rules_i18n')) { + $row[] = ''; + } } if (!$options['hide status op']) { @@ -313,4 +324,5 @@ class RulesUIController { $row[] = l(t('export'), RulesPluginUI::path($name, 'export'), array('attributes' => array('class' => array('export', 'action')))); return $row; } + } diff --git a/sites/all/modules/contrib/admin/rules/ui/ui.core.inc b/sites/all/modules/contrib/admin/rules/ui/ui.core.inc index b72ede0d..abffb0dc 100644 --- a/sites/all/modules/contrib/admin/rules/ui/ui.core.inc +++ b/sites/all/modules/contrib/admin/rules/ui/ui.core.inc @@ -1,7 +1,8 @@ save() afterwards. @@ -135,11 +138,13 @@ class RulesElementMap { } return isset($this->index[$id]) ? $this->index[$id] : FALSE; } + } /** - * Faces UI extender for all kind of Rules plugins. Provides various useful - * methods for any rules UI. + * Faces UI extender for all kind of Rules plugins. + * + * Provides various useful methods for any rules UI. */ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { @@ -149,10 +154,11 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { protected $element; /** - * The base path determines where a Rules overview UI lives. All forms that - * want to display Rules (overview) forms need to set this variable. This is - * necessary in order to get correct operation links, paths, redirects, bread - * crumbs etc. for the form() and overviewTable() methods. + * The base path determines where a Rules overview UI lives. + * + * All forms that want to display Rules (overview) forms need to set this + * variable. This is necessary in order to get correct operation links, + * paths, redirects, breadcrumbs etc. for the form() and overviewTable() methods. * * @see RulesUIController * @see rules_admin_reaction_overview() @@ -193,8 +199,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } /** - * Implements RulesPluginUIInterface. - * Generates the element edit form. + * Implements RulesPluginUIInterface. Generates the element edit form. * * Note: Make sure that you set RulesPluginUI::$basePath before using this * method, otherwise paths, links, redirects etc. won't be correct. @@ -257,7 +262,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { ); } if (!empty($form['provides'])) { - $help = '
    ' . t('Adjust the names and labels of provided variables, but note that renaming of already utilizied variables invalidates the existing uses.') . '
    '; + $help = '
    ' . t('Adjust the names and labels of provided variables, but note that renaming of already utilized variables invalidates the existing uses.') . '
    '; $form['provides'] += array( '#tree' => TRUE, '#prefix' => '

    ' . t('Provided variables') . '

    ' . $help, @@ -304,8 +309,8 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } // For translatable parameters, pre-populate an internal translation source - // key so data type forms or input evaluators (i18n) may produce suiting - // help. + // key so data type forms or input evaluators (i18n) may show a suitable + // help message. if (drupal_multilingual() && !empty($info['translatable'])) { $parameter = $this->element->pluginParameterInfo(); $info['custom translation language'] = !empty($parameter['language']); @@ -320,7 +325,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } // Add a link for switching the input mode when JS is enabled and a button - // to switch it without javascript, in case switching is possible. + // to switch it without JavaScript, in case switching is possible. if ($supports_input_mode && empty($info['restriction'])) { $value = $mode == 'selector' ? t('Switch to the direct input mode') : t('Switch to data selection'); @@ -420,6 +425,8 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { '#required' => TRUE, '#weight' => -5, ); + // @todo For Drupal 8 use "owner" for generating machine names and + // module only for the modules providing default configurations. if (!empty($this->element->module) && !empty($this->element->name) && $this->element->module == 'rules' && strpos($this->element->name, 'rules_') === 0) { // Remove the Rules module prefix from the machine name. $machine_name = substr($this->element->name, strlen($this->element->module) + 1); @@ -455,7 +462,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { $description = t('The variables used by the component. They can not be edited for configurations that are provided in code.'); } else { - $description = t('Variables are normally input parameters for the component – data that should be available for the component to act on. Additionaly, action components may provide variables back to the caller. Each variable must have a specified data type, a label and a unique machine readable name containing only lowercase alphanumeric characters and underscores. See the online documentation for more information about variables.', + $description = t('Variables are normally input parameters for the component – data that should be available for the component to act on. Additionally, action components may provide variables back to the caller. Each variable must have a specified data type, a label and a unique machine readable name containing only lowercase alphanumeric characters and underscores. See the online documentation for more information about variables.', array('@url' => rules_external_help('variables')) ); } @@ -500,7 +507,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { '#type' => 'checkbox', '#title' => t('Configure access for using this component with a permission.'), '#default_value' => !empty($this->element->access_exposed), - '#description' => t('By default, the @plugin-type for using this component may be only used by users that have access to configure the component. If checked, access is determined by a permission instead.', array('@plugin-type' => $plugin_type)) + '#description' => t('By default, the @plugin-type for using this component may be only used by users that have access to configure the component. If checked, access is determined by a permission instead.', array('@plugin-type' => $plugin_type)), ); $form['settings']['access']['permissions'] = array( '#type' => 'container', @@ -514,7 +521,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } } - // TODO: Attach field form thus description. + // @todo Attach field form thus description. } /** @@ -565,11 +572,11 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { $form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state); $this->element->label = $form_values['label']; // If the name was changed we have to redirect to the URL that contains - // the new name, instead of rebuilding on the old URL with the old name + // the new name, instead of rebuilding on the old URL with the old name. if ($form['settings']['name']['#default_value'] != $form_values['name']) { $module = isset($this->element->module) ? $this->element->module : 'rules'; $this->element->name = $module . '_' . $form_values['name']; - $form_state['redirect'] = RulesPluginUI::path($this->element->name); + $form_state['redirect'] = RulesPluginUI::path($this->element->name, 'edit', $this->element); } $this->element->tags = empty($form_values['tags']) ? array() : drupal_explode_tags($form_values['tags']); @@ -667,22 +674,28 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { * Returns the name of class for the given data type. * * @param $data_type - * The name of the data typ + * The name of the data type * @param $parameter_info - * (optional) An array of info about the to be configured parameter. + * (optional) An array of info about the to be configured parameter. If + * given, this array is complemented with data type defaults also. */ - public function getDataTypeClass($data_type, $parameter_info = array()) { - if (!empty($parameter_info['ui class'])) { - return $parameter_info['ui class']; - } + public function getDataTypeClass($data_type, &$parameter_info = array()) { $cache = rules_get_cache(); $data_info = $cache['data_info']; - return (is_string($data_type) && isset($data_info[$data_type]['ui class'])) ? $data_info[$data_type]['ui class'] : 'RulesDataUI'; + // Add in data-type defaults. + if (empty($parameter_info['ui class'])) { + $parameter_info['ui class'] = (is_string($data_type) && isset($data_info[$data_type]['ui class'])) ? $data_info[$data_type]['ui class'] : 'RulesDataUI'; + } + if (is_subclass_of($parameter_info['ui class'], 'RulesDataInputOptionsListInterface')) { + $parameter_info['options list'] = array($parameter_info['ui class'], 'optionsList'); + } + return $parameter_info['ui class']; } /** * Implements RulesPluginUIInterface. - * Show a preview of the configuration settings. + * + * Shows a preview of the configuration settings. */ public function buildContent() { $config_name = $this->element->root()->name; @@ -691,7 +704,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { '#title' => $this->element->label(), '#href' => $this->element->isRoot() ? RulesPluginUI::path($config_name) : RulesPluginUI::path($config_name, 'edit', $this->element), '#prefix' => '
    ', - '#suffix' => '
    ' + '#suffix' => '', ); // Put the elements below in a "description" div. $content['description'] = array( @@ -705,14 +718,14 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { $element = array(); if (!empty($this->element->settings[$name . ':select'])) { $element['content'] = array( - '#markup' => '[' . $this->element->settings[$name . ':select'] . ']', + '#markup' => '[' . $this->element->settings[$name . ':select'] . ']', ); } elseif (isset($this->element->settings[$name]) && (!isset($parameter['default value']) || $parameter['default value'] != $this->element->settings[$name])) { - $method = empty($parameter['options list']) ? 'render' : 'renderOptionsLabel'; $class = $this->getDataTypeClass($parameter['type'], $parameter); - // We cannot use method_exists() here as it would trigger a PHP bug, - // @see http://drupal.org/node/1258284 + $method = empty($parameter['options list']) ? 'render' : 'renderOptionsLabel'; + // We cannot use method_exists() here as it would trigger a PHP bug. + // @see https://www.drupal.org/node/1258284 $element = call_user_func(array($class, $method), $this->element->settings[$name], $name, $parameter, $this->element); } // Only add parameters that are really configured / not default. @@ -795,7 +808,6 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { */ public function help() {} - /** * Deprecated by the controllers overviewTable() method. */ @@ -804,6 +816,8 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } /** + * Generates an operation path. + * * Generates a path using the given operation for the element with the given * id of the configuration with the given name. */ @@ -817,12 +831,19 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { else { $base_path = isset($element) && $element instanceof RulesTriggerableInterface ? 'admin/config/workflow/rules/reaction' : 'admin/config/workflow/rules/components'; } - return implode('/', array_filter(array($base_path . '/manage', $name, $op, $element_id, $parameter))); + + // Only append the '/manage' path if it is not already present. + if (substr($base_path, -strlen('/manage')) != '/manage') { + $base_path .= '/manage'; + } + + return implode('/', array_filter(array($base_path, $name, $op, $element_id, $parameter))); } /** - * Determines the default redirect target for an edited/deleted element. This - * is a parent element which is either a rule or the configuration root. + * Determines the default redirect target for an edited/deleted element. + * + * This is a parent element which is either a rule or the configuration root. */ public static function defaultRedirect(RulesPlugin $element) { while (!$element->isRoot()) { @@ -835,47 +856,10 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } /** - * Returns an array of options to use with a select for the items specified - * in the given hook. - * - * @param $item_type - * The item type to get options for. One of 'data', 'event', 'condition' and - * 'action'. - * @param $items - * (optional) An array of items to restrict the options to. - * - * @return - * An array of options. + * @see RulesUICategory::getOptions() */ public static function getOptions($item_type, $items = NULL) { - $sorted_data = array(); - $ungrouped = array(); - $data = $items ? $items : rules_fetch_data($item_type . '_info'); - foreach ($data as $name => $info) { - // Verfiy the current user has access to use it. - if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) { - continue; - } - if (!empty($info['group'])) { - $sorted_data[drupal_ucfirst($info['group'])][$name] = drupal_ucfirst($info['label']); - } - else { - $ungrouped[$name] = drupal_ucfirst($info['label']); - } - } - asort($ungrouped); - foreach ($sorted_data as $key => $choices) { - asort($choices); - $sorted_data[$key] = $choices; - } - ksort($sorted_data); - // Always move the 'Components' group down it it exists. - if (isset($sorted_data[t('Components')])) { - $copy = $sorted_data[t('Components')]; - unset($sorted_data[t('Components')]); - $sorted_data[t('Components')] = $copy; - } - return $ungrouped + $sorted_data; + return RulesUICategory::getOptions($item_type, $items = NULL); } public static function formDefaults(&$form, &$form_state) { @@ -909,6 +893,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { ->fetchCol('tag'); return drupal_map_assoc($result); } + } /** @@ -917,6 +902,8 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { class RulesAbstractPluginUI extends RulesPluginUI { /** + * Overrides RulesPluginUI::form(). + * * Overridden to invoke the abstract plugins form alter callback and to add * the negation checkbox for conditions. */ @@ -953,6 +940,7 @@ class RulesAbstractPluginUI extends RulesPluginUI { form_set_error(implode('][', $e->keys), $e->getMessage()); } } + } /** @@ -971,11 +959,11 @@ class RulesContainerPluginUI extends RulesPluginUI { '#tree' => TRUE, '#theme' => 'rules_elements', '#empty' => t('None'), - '#caption' => t('Elements') + '#caption' => t('Elements'), ); $form['elements']['#attributes']['class'][] = 'rules-container-plugin'; - // Recurse over all element childrens or use the provided iterator. + // Recurse over all element children or use the provided iterator. $iterator = isset($iterator) ? $iterator : $this->element->elements(); $root_depth = $this->element->depth(); foreach ($iterator as $key => $child) { @@ -991,11 +979,11 @@ class RulesContainerPluginUI extends RulesPluginUI { $form['elements'][$id]['weight'] = array( '#type' => 'weight', '#default_value' => $child->weight, - '#delta' => 20, + '#delta' => 50, ); $form['elements'][$id]['parent_id'] = array( '#type' => 'hidden', - // If another iterator is passed in, the childs parent may not equal + // If another iterator is passed in, the child parent may not equal // the current element. Thus ask the child for its parent. '#default_value' => $child->parentElement()->elementId(), ); @@ -1061,12 +1049,10 @@ class RulesContainerPluginUI extends RulesPluginUI { return $render; } - public function buildContent() { $content = parent::buildContent(); // Don't link the title for embedded container plugins, except for rules. if (!$this->element->isRoot() && !($this->element instanceof Rule)) { - $content['label']['#type'] = 'markup'; $content['label']['#markup'] = check_plain($content['label']['#title']); unset($content['label']['#title']); } @@ -1109,6 +1095,7 @@ class RulesContainerPluginUI extends RulesPluginUI { } return $content; } + } /** @@ -1123,7 +1110,7 @@ class RulesConditionContainerUI extends RulesContainerPluginUI { $form['elements']['#attributes']['class'][] = 'rules-condition-container'; $form['elements']['#caption'] = t('Conditions'); - // By default skip + // By default skip. if (!empty($options['init']) && !$this->element->isRoot()) { $config = $this->element->root(); $form['init_help'] = array( @@ -1154,6 +1141,7 @@ class RulesConditionContainerUI extends RulesContainerPluginUI { $this->element->negate($form_values['negate']); } } + } /** @@ -1162,10 +1150,148 @@ class RulesConditionContainerUI extends RulesContainerPluginUI { class RulesActionContainerUI extends RulesContainerPluginUI { public function form(&$form, &$form_state, $options = array(), $iterator = NULL) { - parent::form($form, $form_state, $options, $iterator); + parent::form($form, $form_state, $options, $iterator); // Add the add-* operation links. $form['elements']['#add'] = self::addOperations(); $form['elements']['#attributes']['class'][] = 'rules-action-container'; $form['elements']['#caption'] = t('Actions'); } + +} + +/** + * Class holding category related methods. + */ +class RulesUICategory { + + /** + * Gets info about all available categories, or about a specific category. + * + * @return array + */ + public static function getInfo($category = NULL) { + $data = rules_fetch_data('category_info'); + if (isset($category)) { + return $data[$category]; + } + return $data; + } + + /** + * Returns a group label, e.g. as usable for opt-groups in a select list. + * + * @param array $item_info + * The info-array of an item, e.g. an entry of hook_rules_action_info(). + * @param bool $in_category + * (optional) Whether group labels for grouping inside a category should be + * return. Defaults to FALSE. + * + * @return string|bool + * The group label to use, or FALSE if none can be found. + */ + public static function getItemGroup($item_info, $in_category = FALSE) { + if (isset($item_info['category']) && !$in_category) { + return self::getCategory($item_info, 'label'); + } + elseif (!empty($item_info['group'])) { + return $item_info['group']; + } + return FALSE; + } + + /** + * Gets the category for the given item info array. + * + * @param array $item_info + * The info-array of an item, e.g. an entry of hook_rules_action_info(). + * @param string|null $key + * (optional) The key of the category info to return, e.g. 'label'. If none + * is given the whole info array is returned. + * + * @return array|mixed|false + * Either the whole category info array or the value of the given key. If + * no category can be found, FALSE is returned. + */ + public static function getCategory($item_info, $key = NULL) { + if (isset($item_info['category'])) { + $info = self::getInfo($item_info['category']); + return isset($key) ? $info[$key] : $info; + } + return FALSE; + } + + /** + * Returns an array of options to use with a select. + * + * Returns an array of options to use with a selectfor the items specified + * in the given hook. + * + * @param $item_type + * The item type to get options for. One of 'data', 'event', 'condition' and + * 'action'. + * @param $items + * (optional) An array of items to restrict the options to. + * + * @return array + * An array of options. + */ + public static function getOptions($item_type, $items = NULL) { + $sorted_data = array(); + $ungrouped = array(); + $data = $items ? $items : rules_fetch_data($item_type . '_info'); + foreach ($data as $name => $info) { + // Verify the current user has access to use it. + if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) { + continue; + } + if ($group = RulesUICategory::getItemGroup($info)) { + $sorted_data[drupal_ucfirst($group)][$name] = drupal_ucfirst($info['label']); + } + else { + $ungrouped[$name] = drupal_ucfirst($info['label']); + } + } + asort($ungrouped); + foreach ($sorted_data as $key => $choices) { + asort($choices); + $sorted_data[$key] = $choices; + } + + // Sort the grouped data by category weights, defaulting to weight 0 for + // groups without a respective category. + $sorted_groups = array(); + foreach (array_keys($sorted_data) as $label) { + $sorted_groups[$label] = array('weight' => 0, 'label' => $label); + } + // Add in category weights. + foreach (RulesUICategory::getInfo() as $info) { + if (isset($sorted_groups[$info['label']])) { + $sorted_groups[$info['label']] = $info; + } + } + uasort($sorted_groups, '_rules_ui_sort_categories'); + + // Now replace weights with group content. + foreach ($sorted_groups as $group => $weight) { + $sorted_groups[$group] = $sorted_data[$group]; + } + return $ungrouped + $sorted_groups; + } + +} + +/** + * Helper for sorting categories. + */ +function _rules_ui_sort_categories($a, $b) { + // @see element_sort() + $a_weight = isset($a['weight']) ? $a['weight'] : 0; + $b_weight = isset($b['weight']) ? $b['weight'] : 0; + if ($a_weight == $b_weight) { + // @see element_sort_by_title() + $a_title = isset($a['label']) ? $a['label'] : ''; + $b_title = isset($b['label']) ? $b['label'] : ''; + return strnatcasecmp($a_title, $b_title); + } + return ($a_weight < $b_weight) ? -1 : 1; } diff --git a/sites/all/modules/contrib/admin/rules/ui/ui.data.inc b/sites/all/modules/contrib/admin/rules/ui/ui.data.inc index 41e2d661..35165878 100644 --- a/sites/all/modules/contrib/admin/rules/ui/ui.data.inc +++ b/sites/all/modules/contrib/admin/rules/ui/ui.data.inc @@ -1,10 +1,10 @@ call('loadBasicInclude'); - $options = entity_property_options_flatten($info['options list']($element, $name)); + $options = entity_property_options_flatten(call_user_func($info['options list'], $element, $name)); if (!is_array($value) && isset($options[$value])) { $value = $options[$value]; } @@ -145,6 +171,18 @@ class RulesDataUI { ); } } + + /** + * Returns the data type and parameter information for the given arguments. + * + * This helper may be used by options list callbacks operation at data-type + * level, see RulesDataInputOptionsListInterface. + */ + public static function getTypeInfo(RulesPlugin $element, $name) { + $parameters = $element->pluginParameterInfo(); + return array($parameters[$name]['type'], $parameters[$name]); + } + } /** @@ -152,10 +190,16 @@ class RulesDataUI { */ class RulesDataUIText extends RulesDataUI implements RulesDataDirectInputFormInterface { + /** + * Overrides RulesDataUI::getDefaultMode(). + */ public static function getDefaultMode() { return 'input'; } + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { if (!empty($info['options list'])) { // Make sure the .rules.inc of the providing module is included as the @@ -169,6 +213,7 @@ class RulesDataUIText extends RulesDataUI implements RulesDataDirectInputFormInt else { $form[$name] = array( '#type' => 'textarea', + '#rows' => 3, ); RulesDataInputEvaluator::attachForm($form, $settings, $info, $element->availableVariables()); } @@ -178,17 +223,20 @@ class RulesDataUIText extends RulesDataUI implements RulesDataDirectInputFormInt '#default_value' => $settings[$name], '#required' => empty($info['optional']), '#after_build' => array('rules_ui_element_fix_empty_after_build'), - '#rows' => 3, ); return $form; } + /** + * Implements RulesDataDirectInputFormInterface::render(). + */ public static function render($value) { return array( 'content' => array('#markup' => check_plain($value)), '#attributes' => array('class' => array('rules-parameter-text')), ); } + } /** @@ -196,6 +244,9 @@ class RulesDataUIText extends RulesDataUI implements RulesDataDirectInputFormInt */ class RulesDataUITextToken extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); if ($form[$name]['#type'] == 'textarea') { @@ -205,6 +256,7 @@ class RulesDataUITextToken extends RulesDataUIText { } return $form; } + } /** @@ -212,6 +264,9 @@ class RulesDataUITextToken extends RulesDataUIText { */ class RulesDataUITextFormatted extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); $settings += array($name => isset($info['default value']) ? $info['default value'] : array('value' => NULL, 'format' => NULL)); @@ -223,21 +278,26 @@ class RulesDataUITextFormatted extends RulesDataUIText { return $form; } + /** + * Implements RulesDataDirectInputFormInterface::render(). + */ public static function render($value) { return array( 'content' => array('#markup' => check_plain($value['value'])), '#attributes' => array('class' => array('rules-parameter-text-formatted')), ); } + } - - /** * UI for decimal data. */ class RulesDataUIDecimal extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); if (empty($info['options list'])) { @@ -247,6 +307,7 @@ class RulesDataUIDecimal extends RulesDataUIText { $form[$name]['#rows'] = 1; return $form; } + } /** @@ -254,6 +315,9 @@ class RulesDataUIDecimal extends RulesDataUIText { */ class RulesDataUIInteger extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); if (empty($info['options list'])) { @@ -262,6 +326,28 @@ class RulesDataUIInteger extends RulesDataUIText { $form[$name]['#element_validate'][] = 'rules_ui_element_integer_validate'; return $form; } + +} + +/** + * UI for IP addresses. + */ +class RulesDataUIIPAddress extends RulesDataUIText { + + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ + public static function inputForm($name, $info, $settings, RulesPlugin $element) { + $form = parent::inputForm($name, $info, $settings, $element); + if (empty($info['options list'])) { + $form[$name]['#type'] = 'textfield'; + $form[$name]['#description'] = t('If not provided, the IP address of the current user will be used.'); + } + $form[$name]['#element_validate'][] = 'rules_ui_element_ip_address_validate'; + $form[$name]['#rows'] = 1; + return $form; + } + } /** @@ -269,27 +355,40 @@ class RulesDataUIInteger extends RulesDataUIText { */ class RulesDataUIBoolean extends RulesDataUI implements RulesDataDirectInputFormInterface { + /** + * Overrides RulesDataUI::getDefaultMode(). + */ public static function getDefaultMode() { return 'input'; } + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $settings += array($name => isset($info['default value']) ? $info['default value'] : NULL); // Note: Due to the checkbox even optional parameter always receive a value. $form[$name] = array( - '#type' => 'checkbox', - '#title' => check_plain($info['label']), + '#type' => 'radios', '#default_value' => $settings[$name], + '#options' => array( + TRUE => t('@label: True.', array('@label' => $info['label'])), + FALSE => t('@label: False.', array('@label' => $info['label'])), + ), ); return $form; } + /** + * Implements RulesDataDirectInputFormInterface::render(). + */ public static function render($value) { return array( 'content' => array('#markup' => !empty($value) ? t('true') : t('false')), '#attributes' => array('class' => array('rules-parameter-boolean')), ); } + } /** @@ -297,6 +396,9 @@ class RulesDataUIBoolean extends RulesDataUI implements RulesDataDirectInputForm */ class RulesDataUIDate extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $settings += array($name => isset($info['default value']) ? $info['default value'] : (empty($info['optional']) ? gmdate('Y-m-d H:i:s', time()) : NULL)); @@ -313,11 +415,14 @@ class RulesDataUIDate extends RulesDataUIText { array('%format' => gmdate('Y-m-d H:i:s', time() + 86400), '!strtotime' => l('strtotime()', 'http://php.net/strtotime'))); - //TODO: Leverage the jquery datepicker+timepicker once a module providing - //the timpeicker is available. + // @todo Leverage the jquery datepicker+timepicker once a module providing + // The timepicker is available. return $form; } + /** + * Implements RulesDataDirectInputFormInterface::render(). + */ public static function render($value) { $value = is_numeric($value) ? format_date($value, 'short') : check_plain($value); return array( @@ -325,6 +430,7 @@ class RulesDataUIDate extends RulesDataUIText { '#attributes' => array('class' => array('rules-parameter-date')), ); } + } /** @@ -332,6 +438,9 @@ class RulesDataUIDate extends RulesDataUIText { */ class RulesDataUIDuration extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); $form[$name]['#type'] = 'rules_duration'; @@ -339,6 +448,9 @@ class RulesDataUIDuration extends RulesDataUIText { return $form; } + /** + * Implements RulesDataDirectInputFormInterface::render(). + */ public static function render($value) { $value = is_numeric($value) ? format_interval($value) : check_plain($value); return array( @@ -346,6 +458,7 @@ class RulesDataUIDuration extends RulesDataUIText { '#attributes' => array('class' => array('rules-parameter-duration')), ); } + } /** @@ -353,12 +466,16 @@ class RulesDataUIDuration extends RulesDataUIText { */ class RulesDataUIURI extends RulesDataUIText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); $form[$name]['#rows'] = 1; - $form[$name]['#description'] = t('You may enter relative URLs like %url as well as absolute URLs like %absolute-url.', array('%url' => 'user/login?destination=node', '%absolute-url' => 'http://drupal.org')); + $form[$name]['#description'] = t('You may enter relative URLs like %url as well as absolute URLs like %absolute-url.', array('%url' => 'user/login?destination=node', '%absolute-url' => 'https://www.drupal.org')); return $form; } + } /** @@ -366,11 +483,16 @@ class RulesDataUIURI extends RulesDataUIText { */ class RulesDataUIListText extends RulesDataUIText { + /** + * Overrides RulesDataUI::getDefaultMode(). + */ public static function getDefaultMode() { return 'input'; } /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + * * @todo This does not work for inputting textual values including "\n". */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { @@ -391,12 +513,16 @@ class RulesDataUIListText extends RulesDataUIText { return $form; } + /** + * Implements RulesDataDirectInputFormInterface::render(). + */ public static function render($value) { return array( 'content' => array('#markup' => check_plain(implode(', ', $value))), '#attributes' => array('class' => array('rules-parameter-list')), ); } + } /** @@ -404,6 +530,9 @@ class RulesDataUIListText extends RulesDataUIText { */ class RulesDataUIListInteger extends RulesDataUIListText { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $settings += array($name => isset($info['default value']) ? $info['default value'] : NULL); $form = parent::inputForm($name, $info, $settings, $element); @@ -417,6 +546,7 @@ class RulesDataUIListInteger extends RulesDataUIListText { } return $form; } + } /** @@ -424,6 +554,9 @@ class RulesDataUIListInteger extends RulesDataUIListText { */ class RulesDataUIListToken extends RulesDataUIListInteger { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); @@ -433,6 +566,7 @@ class RulesDataUIListToken extends RulesDataUIListInteger { } return $form; } + } /** @@ -440,10 +574,16 @@ class RulesDataUIListToken extends RulesDataUIListInteger { */ class RulesDataUIEntity extends RulesDataUIText { + /** + * Overrides RulesDataUI::getDefaultMode(). + */ public static function getDefaultMode() { return 'selector'; } + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); if (empty($info['options list'])) { @@ -459,6 +599,7 @@ class RulesDataUIEntity extends RulesDataUIText { } return $form; } + } /** @@ -466,9 +607,46 @@ class RulesDataUIEntity extends RulesDataUIText { */ class RulesDataUIEntityExportable extends RulesDataUIEntity { + /** + * Overrides RulesDataUI::getDefaultMode(). + */ public static function getDefaultMode() { return 'input'; } + +} + +/** + * Data UI variant displaying a select list of available bundle entities. + * + * This is used for "bundle entities" implemented via the 'bundle of' feature + * of entity.module. + */ +class RulesDataUIBundleEntity extends RulesDataUIEntity implements RulesDataInputOptionsListInterface { + + /** + * Overrides RulesDataUI::getDefaultMode(). + */ + public static function getDefaultMode() { + return 'input'; + } + + /** + * Implements RulesDataInputOptionsListInterface::optionsList(). + */ + public static function optionsList(RulesPlugin $element, $name) { + list($data_type, $parameter_info) = RulesDataUI::getTypeInfo($element, $name); + $bundles = array(); + $entity_info = entity_get_info(); + $bundle_of_type = $entity_info[$data_type]['bundle of']; + if (isset($entity_info[$bundle_of_type]['bundles'])) { + foreach ($entity_info[$bundle_of_type]['bundles'] as $bundle_name => $bundle_info) { + $bundles[$bundle_name] = $bundle_info['label']; + } + } + return $bundles; + } + } /** @@ -476,27 +654,26 @@ class RulesDataUIEntityExportable extends RulesDataUIEntity { * * @see RulesTaxonomyVocabularyWrapper */ -class RulesDataUITaxonomyVocabulary extends RulesDataUIEntity { +class RulesDataUITaxonomyVocabulary extends RulesDataUIEntity implements RulesDataInputOptionsListInterface { + /** + * Overrides RulesDataUI::getDefaultMode(). + */ public static function getDefaultMode() { return 'input'; } - public static function inputForm($name, $info, $settings, RulesPlugin $element) { - // Add an options list of all vocabularies if there is none yet. - if (!isset($info['options list'])) { - $info['options list'] = array('RulesDataUITaxonomyVocabulary', 'optionsList'); - } - return parent::inputForm($name, $info, $settings, $element); - } - - public static function optionsList() { + /** + * Implements RulesDataInputOptionsListInterface::optionsList(). + */ + public static function optionsList(RulesPlugin $element, $name) { $options = array(); foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocab) { $options[$machine_name] = $vocab->name; } return $options; } + } /** @@ -504,6 +681,9 @@ class RulesDataUITaxonomyVocabulary extends RulesDataUIEntity { */ class RulesDataUIListEntity extends RulesDataUIListInteger { + /** + * Implements RulesDataDirectInputFormInterface::inputForm(). + */ public static function inputForm($name, $info, $settings, RulesPlugin $element) { $form = parent::inputForm($name, $info, $settings, $element); if (empty($info['options list'])) { @@ -518,4 +698,5 @@ class RulesDataUIListEntity extends RulesDataUIListInteger { } return $form; } + } diff --git a/sites/all/modules/contrib/admin/rules/ui/ui.forms.inc b/sites/all/modules/contrib/admin/rules/ui/ui.forms.inc index 1439304a..b20f8990 100644 --- a/sites/all/modules/contrib/admin/rules/ui/ui.forms.inc +++ b/sites/all/modules/contrib/admin/rules/ui/ui.forms.inc @@ -1,7 +1,8 @@ form_validate($form, $form_state); @@ -78,7 +80,6 @@ function rules_ui_form_edit_rules_config_submit($form, &$form_state) { function rules_ui_form_clone_rules_config($form, &$form_state, $rules_config, $base_path) { RulesPluginUI::$basePath = $base_path; $rules_config = clone $rules_config; - $rules_config->module = 'rules'; $rules_config->id = NULL; $rules_config->name = ''; $rules_config->label .= ' (' . t('cloned') . ')'; @@ -166,13 +167,29 @@ function rules_ui_confirm_operations($op, $rules_config) { switch ($op) { case 'enable': - return array(t('Are you sure you want to enable the %plugin %label?', $vars), ''); + return array( + t('Are you sure you want to enable the %plugin %label?', $vars), + '', + ); + case 'disable': - return array(t('Are you sure you want to disable the %plugin %label?', $vars), ''); + return array( + t('Are you sure you want to disable the %plugin %label?', $vars), + '', + ); + case 'revert': - return array(t('Are you sure you want to revert the %plugin %label?', $vars), t('This action cannot be undone.')); + return array( + t('Are you sure you want to revert the %plugin %label?', $vars), + t('This action cannot be undone.'), + ); + case 'delete': - return array(t('Are you sure you want to delete the %plugin %label?', $vars), t('This action cannot be undone.')); + return array( + t('Are you sure you want to delete the %plugin %label?', $vars), + t('This action cannot be undone.'), + ); + default: return FALSE; } @@ -194,9 +211,10 @@ function rules_ui_form_rules_config_confirm_op($form, &$form_state, $rules_confi } /** - * Applies the operation and returns the message to show to the user. Also the - * operation is logged to the watchdog. Note that the string is defined two - * times so that the translation extractor can find it. + * Applies the operation and returns the message to show to the user. + * + * The operation is also logged to the watchdog. Note that the string is + * defined two times so that the translation extractor can find it. */ function rules_ui_confirm_operation_apply($op, $rules_config) { $vars = array('%plugin' => $rules_config->plugin(), '%label' => $rules_config->label()); @@ -291,8 +309,9 @@ function rules_ui_add_element($form, &$form_state, $rules_config, $plugin_name, /** * Add element submit callback. + * * Used for "abstract plugins" to create the initial element object with the - * given implemenation name and rebuild the form. + * given implementation name and rebuild the form. */ function rules_ui_add_element_submit($form, &$form_state) { $element = rules_plugin_factory($form_state['plugin'], $form_state['values']['element_name']); @@ -349,7 +368,10 @@ function rules_ui_delete_element($form, &$form_state, $rules_config, $rules_elem } } - $confirm_question = t('Are you sure you want to delete the %element_plugin %element_name?', array('%element_plugin' => $rules_element->plugin(), '%element_name' => $rules_element->label(), '%plugin' => $rules_config->plugin(), '%label' => $rules_config->label())); + $confirm_question = t('Are you sure you want to delete the %element_plugin %element_name?', array( + '%element_plugin' => $rules_element->plugin(), + '%element_name' => $rules_element->label(), + )); return confirm_form($form, $confirm_question, RulesPluginUI::path($rules_config->name), t('This action cannot be undone.'), t('Delete'), t('Cancel')); } @@ -364,7 +386,6 @@ function rules_ui_delete_element_submit($form, &$form_state) { } } - /** * Configure a rule element. */ @@ -394,43 +415,20 @@ function rules_ui_edit_element_submit($form, &$form_state) { } /** - * Add a new event. + * Form builder for the "add event" page. */ -function rules_ui_add_event($form, &$form_state, RulesReactionRule $rules_config, $base_path) { +function rules_ui_add_event_page($form, &$form_state, RulesTriggerableInterface $rules_config, $base_path) { RulesPluginUI::$basePath = $base_path; - $form_state += array('rules_config' => $rules_config); - $events = array_diff_key(rules_fetch_data('event_info'), array_flip($rules_config->events())); - - $form['help'] = array( - '#markup' => t('Select the event to add. However note that all added events need to provide all variables that should be available to your rule.'), - ); - $form['event'] = array( - '#type' => 'select', - '#title' => t('React on event'), - '#options' => RulesPluginUI::getOptions('event', $events), - '#description' => t('Whenever the event occurs, rule evaluation is triggered.'), - ); - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Add'), - ); - $form_state['redirect'] = RulesPluginUI::path($rules_config->name); + RulesPluginUI::formDefaults($form, $form_state); + $form = rules_ui_add_event($form, $form_state, $rules_config, $base_path); + $form['#validate'][] = 'rules_ui_add_event_validate'; return $form; } -/** - * Submit callback that just adds the selected event. - * - * @see rules_admin_add_reaction_rule() - */ -function rules_ui_add_event_apply($form, &$form_state) { - $form_state['rules_config']->event($form_state['values']['event']); -} - /** * Submit the event configuration. */ -function rules_ui_add_event_submit($form, &$form_state) { +function rules_ui_add_event_page_submit($form, &$form_state) { rules_ui_add_event_apply($form, $form_state); $rules_config = $form_state['rules_config']; @@ -453,13 +451,71 @@ function rules_ui_add_event_submit($form, &$form_state) { } /** - * Form to remove a event from a rule. + * Add a new event. + */ +function rules_ui_add_event($form, &$form_state, RulesReactionRule $rules_config, $base_path) { + $form_state += array('rules_config' => $rules_config); + $events = array_diff_key(rules_fetch_data('event_info'), array_flip($rules_config->events())); + + $form['help'] = array( + '#markup' => t('Select the event to add. However note that all added events need to provide all variables that should be available to your rule.'), + ); + $form['event'] = array( + '#type' => 'select', + '#title' => t('React on event'), + '#options' => RulesPluginUI::getOptions('event', $events), + '#description' => t('Whenever the event occurs, rule evaluation is triggered.'), + '#ajax' => rules_ui_form_default_ajax(), + '#required' => TRUE, + ); + if (!empty($form_state['values']['event'])) { + $handler = rules_get_event_handler($form_state['values']['event']); + $form['event_settings'] = $handler->buildForm($form_state); + } + else { + $form['event_settings'] = array(); + } + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Add'), + ); + $form_state['redirect'] = RulesPluginUI::path($rules_config->name); + return $form; +} + +/** + * Validation callback for adding an event. + */ +function rules_ui_add_event_validate($form, $form_state) { + $handler = rules_get_event_handler($form_state['values']['event']); + $handler->extractFormValues($form['event_settings'], $form_state); + try { + $handler->validate(); + } + catch (RulesIntegrityException $e) { + form_set_error(implode('][', $e->keys), $e->getMessage()); + } +} + +/** + * Submit callback that just adds the selected event. + * + * @see rules_admin_add_reaction_rule() + */ +function rules_ui_add_event_apply($form, &$form_state) { + $handler = rules_get_event_handler($form_state['values']['event']); + $handler->extractFormValues($form['event_settings'], $form_state); + $form_state['rules_config']->event($form_state['values']['event'], $handler->getSettings()); +} + +/** + * Form to remove an event from a rule. */ function rules_ui_remove_event($form, &$form_state, $rules_config, $event, $base_path) { RulesPluginUI::$basePath = $base_path; $form_state += array('rules_config' => $rules_config, 'rules_event' => $event); - $events = rules_fetch_data('event_info'); - $form_state['event_label'] = $events[$event]['label']; + $event_info = rules_get_event_info($event); + $form_state['event_label'] = $event_info['label']; $confirm_question = t('Are you sure you want to remove the event?'); return confirm_form($form, $confirm_question, RulesPluginUI::path($rules_config->name), t('You are about to remove the event %event.', array('%event' => $form_state['event_label'])), t('Remove'), t('Cancel')); } @@ -564,6 +620,7 @@ function rules_ui_import_form_submit($form, &$form_state) { /** * FAPI process callback for the data selection widget. + * * This finalises the auto completion callback path by appending the form build * id. */ @@ -623,12 +680,12 @@ function rules_ui_form_data_selection_auto_completion($parameter, $form_build_id drupal_json_output($matches); } - /** - * FAPI validation of an integer element. Copy of the private function - * _element_validate_integer(). + * FAPI validation of an integer element. + * + * Copy of the core Drupal private function _element_validate_integer(). */ -function rules_ui_element_integer_validate($element, &$form_state) {; +function rules_ui_element_integer_validate($element, &$form_state) { $value = $element['#value']; if (isset($value) && $value !== '' && (!is_numeric($value) || intval($value) != $value)) { form_error($element, t('%name must be an integer value.', array('%name' => isset($element['#title']) ? $element['#title'] : t('Element')))); @@ -636,8 +693,9 @@ function rules_ui_element_integer_validate($element, &$form_state) {; } /** - * FAPI validation of a decimal element. Improved version of the private - * function _element_validate_number(). + * FAPI validation of a decimal element. + * + * Improved version of the private function _element_validate_number(). */ function rules_ui_element_decimal_validate($element, &$form_state) { // Substitute the decimal separator ",". @@ -651,9 +709,21 @@ function rules_ui_element_decimal_validate($element, &$form_state) { } /** - * FAPI validation of a date element. Makes sure the specified date format is - * correct and converts date values specifiy a fixed (= non relative) date to - * a timestamp. Relative dates are handled by the date input evaluator. + * FAPI callback to validate an IP address. + */ +function rules_ui_element_ip_address_validate($element, &$form_state) { + $value = $element['#value']; + if ($value != '' && !filter_var($value, FILTER_VALIDATE_IP)) { + form_error($element, t('%name is not a valid IP address.', array('%name' => $element['#title']))); + } +} + +/** + * FAPI validation of a date element. + * + * Makes sure the specified date format is correct and converts date values + * specify a fixed (= non relative) date to a timestamp. Relative dates are + * handled by the date input evaluator. */ function rules_ui_element_date_validate($element, &$form_state) { $value = $element['#value']; @@ -722,8 +792,9 @@ function rules_ui_element_duration_multipliers() { } /** - * Helper function to determine the value for a rules duration form - * element. + * Helper function a rules duration form element. + * + * Determines the value for a rules duration form element. */ function rules_ui_element_duration_value($element, $input = FALSE) { // This runs before child elements are processed, so we cannot calculate the @@ -737,6 +808,7 @@ function rules_ui_element_duration_value($element, $input = FALSE) { /** * FAPI after build callback for the duration parameter type form. + * * Fixes up the form value by applying the multiplier. */ function rules_ui_element_duration_after_build($element, &$form_state) { @@ -766,7 +838,6 @@ function rules_ui_element_fix_empty_after_build($element, &$form_state) { return $element; } - /** * FAPI after build callback for specifying a list of values. * @@ -833,6 +904,7 @@ function rules_ui_element_machine_name_validate($element, &$form_state) { /** * FAPI callback to validate the form for editing variable info. + * * @see RulesPluginUI::getVariableForm() */ function rules_ui_element_variable_form_validate($elements, &$form_state) { diff --git a/sites/all/modules/contrib/admin/rules/ui/ui.plugins.inc b/sites/all/modules/contrib/admin/rules/ui/ui.plugins.inc index bac9984e..d3a061af 100644 --- a/sites/all/modules/contrib/admin/rules/ui/ui.plugins.inc +++ b/sites/all/modules/contrib/admin/rules/ui/ui.plugins.inc @@ -1,7 +1,8 @@ rule = $object; $this->conditions = $this->rule->conditionContainer(); } - public function form(&$form, &$form_state, $options = array()) { + public function form(&$form, &$form_state, $options = array(), $iterator = NULL) { $form_state['rules_element'] = $this->rule; $label = $this->element->label(); // Automatically add a counter to unlabelled rules. @@ -58,7 +65,7 @@ class RulesRuleUI extends RulesActionContainerUI { /** * Applies the values of the form to the rule configuration. */ - function form_extract_values($form, &$form_state) { + public function form_extract_values($form, &$form_state) { $form_values = RulesPluginUI::getFormStateValues($form, $form_state); // Run condition and action container value extraction. if (isset($form['conditions'])) { @@ -70,13 +77,13 @@ class RulesRuleUI extends RulesActionContainerUI { parent::form_extract_values($form, $form_state); } - public function operations() { // When rules are listed only show the edit and delete operations. $ops = parent::operations(); $ops['#links'] = array_intersect_key($ops['#links'], array_flip(array('edit', 'delete'))); return $ops; } + } /** @@ -84,26 +91,46 @@ class RulesRuleUI extends RulesActionContainerUI { */ class RulesReactionRuleUI extends RulesRuleUI { - public function form(&$form, &$form_state, $options = array()) { + public function form(&$form, &$form_state, $options = array(), $iterator = NULL) { $form['events'] = array( '#type' => 'container', '#weight' => -10, '#access' => empty($options['init']), ); - $event_info = rules_fetch_data('event_info'); $form['events']['table'] = array( '#theme' => 'table', '#caption' => 'Events', - '#header' => array('Event', 'Operations'), + '#header' => array(t('Event'), t('Operations')), '#empty' => t('None'), ); $form['events']['table']['#attributes']['class'][] = 'rules-elements-table'; foreach ($this->rule->events() as $event_name) { - $event_info += array($event_name => array('label' => t('Unknown event "!event_name"', array('!event_name' => $event_name)))); + $event_handler = rules_get_event_handler($event_name, $this->rule->getEventSettings($event_name)); + + $event_operations = array( + '#theme' => 'links__rules', + '#attributes' => array( + 'class' => array( + 'rules-operations', + 'action-links', + 'rules_rule_event', + ), + ), + '#links' => array( + 'delete_event' => array( + 'title' => t('delete'), + 'href' => RulesPluginUI::path($this->rule->name, 'delete/event/' . $event_name), + 'query' => drupal_get_destination(), + ), + ), + ); + $form['events']['table']['#rows'][$event_name] = array( - check_plain($event_info[$event_name]['label']), - '' . l(t('delete'), RulesPluginUI::path($this->rule->name, 'delete/event/' . $event_name)) . '', + 'data' => array( + $event_handler->summary(), + array('data' => $event_operations), + ), ); } @@ -150,6 +177,7 @@ class RulesReactionRuleUI extends RulesRuleUI { $this->rule->active = $form_values['active']; $this->rule->weight = $form_values['weight']; } + } /** @@ -166,6 +194,7 @@ class RulesRuleSetUI extends RulesActionContainerUI { $form['elements']['#attributes']['class'][] = 'rules-rule-set'; $form['elements']['#caption'] = t('Rules'); } + } /** @@ -173,7 +202,7 @@ class RulesRuleSetUI extends RulesActionContainerUI { */ class RulesLoopUI extends RulesActionContainerUI { - public function form(&$form, &$form_state, $options = array()) { + public function form(&$form, &$form_state, $options = array(), $iterator = NULL) { parent::form($form, $form_state, $options); $settings = $this->element->settings; @@ -199,7 +228,7 @@ class RulesLoopUI extends RulesActionContainerUI { ); } - function form_extract_values($form, &$form_state) { + public function form_extract_values($form, &$form_state) { parent::form_extract_values($form, $form_state); $form_values = RulesPluginUI::getFormStateValues($form, $form_state); @@ -231,4 +260,5 @@ class RulesLoopUI extends RulesActionContainerUI { ); return $content; } + } diff --git a/sites/all/modules/contrib/admin/rules/ui/ui.theme.inc b/sites/all/modules/contrib/admin/rules/ui/ui.theme.inc index 42bf6e82..52212fe3 100644 --- a/sites/all/modules/contrib/admin/rules/ui/ui.theme.inc +++ b/sites/all/modules/contrib/admin/rules/ui/ui.theme.inc @@ -1,10 +1,10 @@ t('Weight'), 'class' => array('tabledrag-hide'))); + $table['#header'] = array( + t('Data type'), + t('Label'), + t('Machine name'), + t('Usage'), + array('data' => t('Weight'), 'class' => array('tabledrag-hide')), + ); $table['#attributes']['id'] = 'rules-' . drupal_html_id($elements['#title']) . '-id'; foreach (element_children($elements['items']) as $key) { @@ -107,6 +114,7 @@ function theme_rules_ui_variable_form($variables) { /** * Themes a view of multiple configuration items. + * * @ingroup themeable */ function theme_rules_content_group($variables) { @@ -125,6 +133,7 @@ function theme_rules_content_group($variables) { /** * Themes the view of a single parameter configuration. + * * @ingroup themeable */ function theme_rules_parameter_configuration($variables) { @@ -149,6 +158,7 @@ function theme_rules_parameter_configuration($variables) { /** * Themes info about variables. + * * @ingroup themeable */ function theme_rules_variable_view($variables) { @@ -169,6 +179,7 @@ function theme_rules_variable_view($variables) { /** * Themes help for using the data selector. + * * @ingroup themeable */ function theme_rules_data_selector_help($variables) { @@ -193,13 +204,18 @@ function theme_rules_data_selector_help($variables) { ); foreach (RulesData::matchingDataSelector($variables_info, $param_info) as $selector => $info) { $info += array('label' => '', 'description' => ''); - $render['table']['#rows'][] = array(check_plain($selector), check_plain(drupal_ucfirst($info['label'])), check_plain($info['description'])); + $render['table']['#rows'][] = array( + check_plain($selector), + check_plain(drupal_ucfirst($info['label'])), + check_plain($info['description']), + ); } return drupal_render($render); } /** * Themes the rules log debug output. + * * @ingroup themeable */ function theme_rules_log($variables) { @@ -230,6 +246,7 @@ function theme_rules_log($variables) { /** * Theme rules debug log elements. + * * @ingroup themeable */ function theme_rules_debug_element($variables) { @@ -248,6 +265,7 @@ function theme_rules_debug_element($variables) { /** * Themes rules autocomplete forms. + * * @ingroup themeable */ function theme_rules_autocomplete($variables) { @@ -278,6 +296,7 @@ function theme_rules_autocomplete($variables) { /** * General theme function for displaying settings related help. + * * @ingroup themeable */ function theme_rules_settings_help($variables) { diff --git a/sites/all/modules/contrib/admin/token/tests/token_test.info b/sites/all/modules/contrib/admin/token/tests/token_test.info index 9e0b2368..02d93794 100644 --- a/sites/all/modules/contrib/admin/token/tests/token_test.info +++ b/sites/all/modules/contrib/admin/token/tests/token_test.info @@ -5,9 +5,9 @@ core = 7.x files[] = token_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2015-02-28 -version = "7.x-1.6" +; Information added by Drupal.org packaging script on 2017-01-25 +version = "7.x-1.7" core = "7.x" project = "token" -datestamp = "1425149060" +datestamp = "1485316088" diff --git a/sites/all/modules/contrib/admin/token/token.info b/sites/all/modules/contrib/admin/token/token.info index a9efec27..afa8c62e 100644 --- a/sites/all/modules/contrib/admin/token/token.info +++ b/sites/all/modules/contrib/admin/token/token.info @@ -3,9 +3,9 @@ description = Provides a user interface for the Token API and some missing core core = 7.x files[] = token.test -; Information added by Drupal.org packaging script on 2015-02-28 -version = "7.x-1.6" +; Information added by Drupal.org packaging script on 2017-01-25 +version = "7.x-1.7" core = "7.x" project = "token" -datestamp = "1425149060" +datestamp = "1485316088" diff --git a/sites/all/modules/contrib/admin/token/token.install b/sites/all/modules/contrib/admin/token/token.install index c2b57fd0..24d09f32 100644 --- a/sites/all/modules/contrib/admin/token/token.install +++ b/sites/all/modules/contrib/admin/token/token.install @@ -21,18 +21,12 @@ function token_requirements($phase = 'runtime') { $problems = array_unique($problem['problems']); $problems = array_map('check_plain', $problems); $token_problems[$problem_key] = $problem['label'] . theme('item_list', array('items' => $problems)); + $requirements['token-' . $problem_key] = array( + 'title' => $problem['label'], + 'value' => theme('item_list', array('items' => $problems)), + 'severity' => $problem['severity'], + ); } - else { - unset($token_problems[$problem_key]); - } - } - if (!empty($token_problems)) { - $requirements['token_problems'] = array( - 'title' => $t('Tokens'), - 'value' => $t('Problems detected'), - 'severity' => REQUIREMENT_WARNING, - 'description' => '

    ' . implode('

    ', $token_problems) . '

    ', //theme('item_list', array('items' => $token_problems)), - ); } } @@ -272,19 +266,24 @@ function token_get_token_problems() { $token_info = token_info(); $token_problems = array( 'not-array' => array( - 'label' => t('The following tokens or token types are not defined as arrays:'), + 'label' => t('Tokens or token types not defined as arrays'), + 'severity' => REQUIREMENT_ERROR, ), 'missing-info' => array( - 'label' => t('The following tokens or token types are missing required name and/or description information:'), + 'label' => t('Tokens or token types missing name property'), + 'severity' => REQUIREMENT_WARNING, ), 'type-no-tokens' => array( - 'label' => t('The following token types do not have any tokens defined:'), + 'label' => t('Token types do not have any tokens defined'), + 'severity' => REQUIREMENT_INFO, ), 'tokens-no-type' => array( - 'label' => t('The following token types are not defined but have tokens:'), + 'label' => t('Token types are not defined but have tokens'), + 'severity' => REQUIREMENT_INFO, ), 'duplicate' => array( - 'label' => t('The following token or token types are defined by multiple modules:') + 'label' => t('Token or token types are defined by multiple modules'), + 'severity' => REQUIREMENT_ERROR, ), ); @@ -295,9 +294,12 @@ function token_get_token_problems() { $token_problems['not-array']['problems'][] = "\$info['types']['$type']"; continue; } - elseif (!isset($type_info['name']) || !isset($type_info['description'])) { + elseif (!isset($type_info['name'])) { $token_problems['missing-info']['problems'][] = "\$info['types']['$type']"; } + elseif (is_array($type_info['name'])) { + $token_problems['duplicate']['problems'][] = "\$info['types']['$type']"; + } elseif (empty($token_info['tokens'][$real_type])) { $token_problems['type-no-tokens']['problems'][] = "\$info['tokens']['$real_type']"; } @@ -315,10 +317,10 @@ function token_get_token_problems() { $token_problems['not-array']['problems'][] = "\$info['tokens']['$type']['$token']"; continue; } - elseif (!isset($tokens[$token]['name']) || !isset($tokens[$token]['description'])) { + elseif (!isset($tokens[$token]['name'])) { $token_problems['missing-info']['problems'][] = "\$info['tokens']['$type']['$token']"; } - elseif (is_array($tokens[$token]['name']) || is_array($tokens[$token]['description'])) { + elseif (is_array($tokens[$token]['name'])) { $token_problems['duplicate']['problems'][] = "\$info['tokens']['$type']['$token']"; } } diff --git a/sites/all/modules/contrib/admin/token/token.module b/sites/all/modules/contrib/admin/token/token.module index 269f3765..bbca2aab 100644 --- a/sites/all/modules/contrib/admin/token/token.module +++ b/sites/all/modules/contrib/admin/token/token.module @@ -274,7 +274,11 @@ function token_form_block_admin_configure_alter(&$form, $form_state) { */ function token_field_widget_form_alter(&$element, &$form_state, $context) { if (!empty($element['#description']) && !empty($context['instance']['description'])) { - $element['#description'] = filter_xss_admin(token_replace($context['instance']['description'])); + $instance = $context['instance']; + if (module_exists('i18n_field')) { + $instance = i18n_string_object_translate('field_instance', $instance); + } + $element['#description'] = field_filter_xss(token_replace($instance['description'])); } } @@ -719,15 +723,13 @@ function token_element_validate(&$element, &$form_state) { // Validate if an element must have a minimum number of tokens. if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) { - // @todo Change this error message to include the minimum number. - $error = format_plural($element['#min_tokens'], 'The %element-title cannot contain fewer than one token.', 'The %element-title must contain at least @count tokens.', array('%element-title' => $title)); + $error = format_plural($element['#min_tokens'], '%name must contain at least one token.', '%name must contain at least @count tokens.', array('%name' => $title)); form_error($element, $error); } // Validate if an element must have a maximum number of tokens. if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) { - // @todo Change this error message to include the maximum number. - $error = format_plural($element['#max_tokens'], 'The %element-title must contain as most one token.', 'The %element-title must contain at most @count tokens.', array('%element-title' => $title)); + $error = format_plural($element['#max_tokens'], '%name must contain at most one token.', '%name must contain at most @count tokens.', array('%name' => $title)); form_error($element, $error); } @@ -735,7 +737,7 @@ function token_element_validate(&$element, &$form_state) { if (isset($element['#token_types'])) { $invalid_tokens = token_get_invalid_tokens_by_context($tokens, $element['#token_types']); if ($invalid_tokens) { - form_error($element, t('The %element-title is using the following invalid tokens: @invalid-tokens.', array('%element-title' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens)))); + form_error($element, t('%name is using the following invalid tokens: @invalid-tokens.', array('%name' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens)))); } } @@ -983,7 +985,7 @@ function _token_build_tree($token_type, array $options) { // parent. $token_parents[] = $token_type; } - elseif (in_array($token, array_slice($token_parents, 1))) { + elseif (in_array($token, array_slice($token_parents, 1), TRUE)) { // Prevent duplicate recursive tokens. For example, this will prevent // the tree from generating the following tokens or deeper: // [comment:parent:parent] diff --git a/sites/all/modules/contrib/admin/token/token.pages.inc b/sites/all/modules/contrib/admin/token/token.pages.inc index 681affcc..4fe39684 100644 --- a/sites/all/modules/contrib/admin/token/token.pages.inc +++ b/sites/all/modules/contrib/admin/token/token.pages.inc @@ -198,9 +198,8 @@ function _token_token_tree_format_row($token, array $token_info, $is_group = FAL $row = $defaults; $row['id'] = _token_clean_css_identifier($token); - $row['data']['token'] = array(); $row['data']['name'] = $token_info['name']; - $row['data']['description'] = $token_info['description']; + $row['data']['description'] = isset($token_info['description']) ? $token_info['description'] : ''; if ($is_group) { // This is a token type/group. @@ -208,6 +207,7 @@ function _token_token_tree_format_row($token, array $token_info, $is_group = FAL } else { // This is a token. + $row['data']['token'] = array(); $row['data']['token']['data'] = $token; $row['data']['token']['class'][] = 'token-key'; if (isset($token_info['value'])) { diff --git a/sites/all/modules/contrib/admin/token/token.tokens.inc b/sites/all/modules/contrib/admin/token/token.tokens.inc index e0c0b5e9..f584bdaf 100644 --- a/sites/all/modules/contrib/admin/token/token.tokens.inc +++ b/sites/all/modules/contrib/admin/token/token.tokens.inc @@ -1392,7 +1392,6 @@ function field_token_info_alter(&$info) { */ function field_tokens($type, $tokens, array $data = array(), array $options = array()) { $replacements = array(); - $sanitize = !empty($options['sanitize']); $langcode = isset($options['language']) ? $options['language']->language : NULL; // Entity tokens. @@ -1437,7 +1436,7 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar /** * Pre-render callback for field output used with tokens. */ -function token_pre_render_field_token(&$elements) { +function token_pre_render_field_token($elements) { // Remove the field theme hook, attachments, and JavaScript states. unset($elements['#theme']); unset($elements['#states']); diff --git a/sites/all/modules/contrib/editor/wysiwyg_filter/README.txt b/sites/all/modules/contrib/editor/wysiwyg_filter/README.txt index 7f8f40e4..a593540b 100644 --- a/sites/all/modules/contrib/editor/wysiwyg_filter/README.txt +++ b/sites/all/modules/contrib/editor/wysiwyg_filter/README.txt @@ -50,19 +50,28 @@ the URLs). INSTALLATION ============ -- Copy all contents of this package to your modules directory preserving - subdirectory structure. +For module installation instructions please see: +http://drupal.org/documentation/install/modules-themes/modules-7 -- Goto admin/build/modules to install the module. -- Goto admin/settings/filters and create a new input format as follows: +CONFIGURATION +============= - - Input format name: WYSIWYG Filter (or something similar of your choice). - - Check the filters: WYSIWYG Filter and HTML Corrector. Save. - - Goto Rearrange tab. - - Drag the WYSIWYG Filter on top of the HTML Corrector. Save. - - Goto the Configure tab of your newly created WYSIWYG Filter and setup the - available options to suit your needs. +After installation you can configure the WYWIWYG filter: + +1) On your site visit Admin > Configuration > Text formats (under 'Content + authoring'): admin/config/content/formats + +2) Add a new text format, or configure the existing text format that you would + like to apply the WYSIWYG filter to. + +3) Tick the 'WYSIWYG filter' option under 'Enabled filters'. + +4) Configure the WYSIWYG filter options to suit your needs under the 'Filter + settings' heading and save when done. + +Note: Be aware of the 'Filter processing order'. WYSIWYG Filter should normally +be arranged above the 'HTML Corrector' if it is being used. SECURITY ISSUES diff --git a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.admin.inc b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.admin.inc index 92c29afb..c5c136fe 100644 --- a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.admin.inc +++ b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.admin.inc @@ -16,7 +16,7 @@ * * remove format suffix, will be first array index * * care for pre- and post-processing, or remove it like parsed-elements * * rewrite validate to get correct values like $form['filters']['settings'][$name] / $form_state['values']['filters'][$name]['settings'] - * + * */ function wysiwyg_filter_filter_wysiwyg_settings(&$form, &$form_state, $filter, $format, $defaults, $filters) { global $base_url; @@ -26,10 +26,10 @@ function wysiwyg_filter_filter_wysiwyg_settings(&$form, &$form_state, $filter, $ $settings = $filter->settings; $settings += $defaults; - + // carry over settings for other formats $filterform = array(); - + // *** valid elements *** $valid_elements = $settings['valid_elements']; $valid_elements_rows = min(20, max(5, substr_count($valid_elements, "\n") + 2)); @@ -111,6 +111,7 @@ This option allows you to specify which HTML elements and attributes are allowed $valid_elements_parsed = wysiwyg_filter_parse_valid_elements($settings['valid_elements']); foreach (wysiwyg_filter_get_advanced_rules() as $rule_key => $rule_info) { $field_name = "rule_$rule_key"; + $field_bypass_name = "rule_bypass_$rule_key"; $default_value = wysiwyg_filter_array2csv($settings[$field_name]); $filterform[$field_name] = array( '#type' => 'textarea', @@ -120,10 +121,16 @@ This option allows you to specify which HTML elements and attributes are allowed '#rows' => min(10, max(2, substr_count($default_value, "\n") + 2)), '#description' => $rule_info['description'], ); + $filterform[$field_bypass_name] = array( + '#type' => 'checkbox', + '#title' => t('Bypass %rule', array('%rule' => $rule_info['title'])), + '#default_value' => !empty($settings[$field_bypass_name]), + '#description' => t('Bypassing this rule may lead to security vulnerabilities. Only grant this filter to trusted roles.'), + ); // Display warning if the field is empty but the rule definition is not // complete. - if (empty($default_value) && !_wysiwyg_filter_is_rule_definition_complete($rule_info, $valid_elements_parsed, $enabled_style_properties)) { + if (empty($settings[$field_bypass_name]) && empty($default_value) && !_wysiwyg_filter_is_rule_definition_complete($rule_info, $valid_elements_parsed, $enabled_style_properties)) { drupal_set_message($rule_info['required_by_message'], 'warning'); } } @@ -139,6 +146,7 @@ This option allows you to specify which HTML elements and attributes are allowed '#options' => array( 'disabled' => t('Disabled - Do not add rel="nofollow" to any link.'), 'whitelist' => t('Whitelist - Add rel="nofollow" to all links except those leading to domain names specified in the list below.'), + 'whitelist_current' => t('Whitelist - Add rel="nofollow" to all links except those leading to current domain.'), 'blacklist' => t('Blacklist - Add rel="nofollow" to all links leading to domain names specified in the list below.'), ), '#default_value' => $settings['nofollow_policy'], @@ -163,7 +171,7 @@ This option allows you to specify which HTML elements and attributes are allowed /* * Implements hook_form_FORM_ID_alter - * + * * add validate and submit handlers */ function wysiwyg_filter_form_filter_admin_format_form_alter(&$form, &$form_state, $form_id) { @@ -208,23 +216,6 @@ function _wysiwyg_filter_is_rule_definition_complete($rule_info, $elements, $sty return TRUE; } -/** - * Clear any warning message we might have set previously. - */ -function _wysiwyg_filter_clear_messages() { - $messages = drupal_get_messages('warning'); - if (!empty($messages)) { - foreach (wysiwyg_filter_get_advanced_rules() as $rule_info) { - $my_messages[] = $rule_info['required_by_message']; - } - foreach ($messages['warning'] as $warning) { - if (!in_array($warning, $my_messages)) { - drupal_set_message($warning, 'warning'); - } - } - } -} - /** * Validate filter settings form. * @@ -252,22 +243,24 @@ function wysiwyg_filter_filter_wysiwyg_settings_validate($form, &$form_state) { form_set_error('valid_elements', t('The following elements cannot be allowed: %elements.', array('%elements' => implode(', ', $forbidden_elements)))); } - // *** validate nofollow_domains *** + // *** validate advanced rules *** foreach (wysiwyg_filter_get_advanced_rules() as $rule_key => $rule_info) { - $field_name = "rule_$rule_key"; - $expressions = array_filter(explode(',', preg_replace('#\s+#', ',', trim($values[$field_name])))); // form2db - $errors = array(); - foreach ($expressions as $expression) { - if (preg_match('`[*?]\*|\*\?`', $expression)) { - $errors[] = t('Invalid expression %expression. Please, do not use more than one consecutive asterisk (**) or one that is next to a question mark wildcard (?* or *?).', array('%expression' => $expression)); + if (empty($settings["rule_bypass_$rule_key"])) { + $field_name = "rule_$rule_key"; + $expressions = array_filter(explode(',', preg_replace('#\s+#', ',', trim($values[$field_name])))); // form2db + $errors = array(); + foreach ($expressions as $expression) { + if (preg_match('`[*?]\*|\*\?`', $expression)) { + $errors[] = t('Invalid expression %expression. Please, do not use more than one consecutive asterisk (**) or one that is next to a question mark wildcard (?* or *?).', array('%expression' => $expression)); + } + if (!preg_match($rule_info['validate_regexp'], $expression)) { + $errors[] = t('Invalid expression %expression. Please, check the syntax of the %field field.', array('%expression' => $expression, '%field' => $rule_info['title'])); + } } - if (!preg_match($rule_info['validate_regexp'], $expression)) { - $errors[] = t('Invalid expression %expression. Please, check the syntax of the %field field.', array('%expression' => $expression, '%field' => $rule_info['title'])); + if (!empty($errors)) { + form_set_error($field_name, implode('
    ', $errors)); } } - if (!empty($errors)) { - form_set_error($field_name, implode('
    ', $errors)); - } } // *** validate nofollow_domains *** @@ -289,10 +282,10 @@ function wysiwyg_filter_filter_wysiwyg_settings_validate($form, &$form_state) { */ function wysiwyg_filter_filter_wysiwyg_settings_submit($form, &$form_state) { $values =& $form_state['values']['filters']['wysiwyg']['settings']; - + // *** prepare valid_elements - just trim *** $values['valid_elements'] = trim($values['valid_elements']); - + // *** prepare rules - csv2array *** foreach (array_keys(wysiwyg_filter_get_advanced_rules()) as $rule_key) { $field_name = "rule_$rule_key"; @@ -305,7 +298,7 @@ function wysiwyg_filter_filter_wysiwyg_settings_submit($form, &$form_state) { /* * CSV to Array - * + * * @param atring $v * @param bool $space2comma - shall we convet whitespace to commas before processing? * @return array @@ -316,7 +309,7 @@ function wysiwyg_filter_csv2array($v, $space2comma = TRUE) { } /* * Array to CSV - * + * * @param array $v * @return string */ diff --git a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.inc b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.inc index c76308b3..f2cf9d64 100644 --- a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.inc +++ b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.inc @@ -16,19 +16,30 @@ */ function wysiwyg_filter_get_advanced_rules() { global $base_url, $base_path; + // CSS identifiers: https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier + // In CSS, identifiers (including element names, classes, and IDs in + // selectors) can contain only the characters [a-zA-Z0-9] and ISO 10646 + // characters U+00A0 and higher, plus the hyphen (-) and the underscore (_); + // they cannot start with a digit, two hyphens, or a hyphen followed by a + // digit. Identifiers can also contain escaped characters and any ISO 10646 + // character as a numeric code (see next item). For instance, the identifier + // "B&W?" may be written as "B\&W\?" or "B\26 W\3F". + // We voluntarily do not add non-ascii characters to the rule, neither do + // we implement the exceptions "two hyphens, or a hyphen followed by a + // digit". return array( 'valid_classes' => array( 'title' => t('Rules for Class Names'), - 'description' => t('Enter a comma separated list of rules for Class Names. Whitespaces and line-breaks are ignored. Class Names should start with an upper or lower case letter "a to z" and can be followed by one or more upper or lower case letters "a to z", digits "0 to 9", hyphens "-" and/or underscores "_". The asterisk character "*" can be used in rules to represent any number of characters from the second position of the rule. Example: "userclass*, my-font-*" are valid rules for Class Names, whereas "*class" is invalid.'), - 'validate_regexp' => '`^[a-zA-Z][-_a-zA-Z0-9?*]*$`', + 'description' => t('Enter a comma separated list of rules for Class Names. Whitespaces and line-breaks are ignored. Class Names should start with an upper or lower case letter "a to z", hyphens "-" and/or underscores "_", and can be followed by one or more upper or lower case letters "a to z", digits "0 to 9", hyphens "-" and/or underscores "_". The asterisk character "*" can be used in rules to represent any number of characters from the second position of the rule. Example: "userclass*, my-font-*" are valid rules for Class Names, whereas "*class" is invalid.'), + 'validate_regexp' => '`^[-_a-zA-Z][-_a-zA-Z0-9?*]*$`', 'asterisk_expansion' => '[-_a-zA-Z0-9]*', 'required_by' => 'class', 'required_by_message' => t('The class attribute is used in your HTML elements and attributes rules. You should specify the Rules for Class Names field in the "Advanced rules" section below. Leaving it unspecified will result in all class attributes filtered out.'), ), 'valid_ids' => array( 'title' => t('Rules for Element IDs'), - 'description' => t('Enter a comma separated list of rules for Element IDs. Whitespaces and line-breaks are ignored. Element IDs should start with an upper or lower case letter "a to z" and can be followed by one or more upper or lower case letters "a to z", digits "0 to 9", hyphens "-" and/or underscores "_". The asterisk character "*" can be used in rules to represent any number of characters from the second position of the rule. Example: "foo*" is a valid rule for Element IDs, whereas "*bar" is invalid.'), - 'validate_regexp' => '`^[a-zA-Z][-_a-zA-Z0-9?*]*$`', + 'description' => t('Enter a comma separated list of rules for Element IDs. Whitespaces and line-breaks are ignored. Element IDs should start with an upper or lower case letter "a to z", hyphens "-" and/or underscores "_", and can be followed by one or more upper or lower case letters "a to z", digits "0 to 9", hyphens "-" and/or underscores "_". The asterisk character "*" can be used in rules to represent any number of characters from the second position of the rule. Example: "foo*" is a valid rule for Element IDs, whereas "*bar" is invalid.'), + 'validate_regexp' => '`^[-_a-zA-Z][-_a-zA-Z0-9?*]*$`', 'asterisk_expansion' => '[-_a-zA-Z0-9]*', 'required_by' => 'id', 'required_by_message' => t('The id attribute is used in your HTML elements and attributes rules. You should specify the Rules for Element IDs field in the "Advanced rules" section below. Leaving it unspecified will result in all id attributes filtered out.'), @@ -123,6 +134,10 @@ function wysiwyg_filter_get_elements_blacklist() { * @see wysiwyg_filter_process() */ function wysiwyg_filter_parse_valid_elements($valid_elements) { + $parsed_elements = &drupal_static(serialize($valid_elements)); + if (isset($parsed_elements)) { + return $parsed_elements; + } // Remove whitespaces and split valid elements from a comma separate list of items into an array. $valid_elements = array_map('drupal_strtolower', array_filter(explode(',', preg_replace('#\s+#', '', $valid_elements)))); $parsed_elements = array(); @@ -130,7 +145,7 @@ function wysiwyg_filter_parse_valid_elements($valid_elements) { foreach ($valid_elements as $valid_element) { // Extract the element name and its allowed attributes list // including special characters that will be parsed later. - if (preg_match('`^(@|[#+-]{0,1}[a-z0-9/]+)(\[([^]]*)\])*$`', $valid_element, $matches)) { + if (preg_match('`^(@|[#+-]{0,1}[a-z0-9-/]+)(\[([^]]*)\])*$`', $valid_element, $matches)) { // Element names can be specified by the special character "@" (used // to allow a common set of attributes for all valid HTML elements) // or a list of names separated by the special character "/". @@ -197,9 +212,9 @@ function wysiwyg_filter_parse_valid_elements($valid_elements) { } } - // Obtain list of element names/synonyms (separated by /). - // Consider synonyms as different elements with same exact attributes. - foreach ($elements as $element) { + // Get the element name. + // If it has synonyms (separated by /), these are handled below. + $element = array_shift($elements); if ($element == '@') { // These attributes should be enabled for all elements. foreach ($attributes as $attribute => $attribute_options) { @@ -236,6 +251,10 @@ function wysiwyg_filter_parse_valid_elements($valid_elements) { } } } + // For each synonym, instead of an attribute list, provide the name of the + // element it is converted to. + foreach ($elements as $synonym) { + $parsed_elements[$synonym] = $element; } } } @@ -243,6 +262,10 @@ function wysiwyg_filter_parse_valid_elements($valid_elements) { // Append commonly allowed attributes to each allowed element. if (!empty($common_attributes)) { foreach ($parsed_elements as $element => &$attributes) { + // Skip synonyms. + if (!is_array($parsed_elements[$element])) { + continue; + } // Do not append common attributes when all are allowed. if (isset($parsed_elements[$element]['*'])) { continue; @@ -291,10 +314,11 @@ function wysiwyg_filter_parse_valid_elements($valid_elements) { * @see wysiwyg_filter_settings_filter() */ function wysiwyg_filter_get_style_property_groups() { - $regexp_integer = '[-]?[0-9]{1,3}'; - $regexp_number = '[-]?(?:[0-9]{0,3}|[0-9]{0,3}\.[0-9]{1,4})'; - $regexp_length = $regexp_number . '(?:px|pt|em|ex|in|cm|mm|pc)?'; - $regexp_percent = '[-]?[12]?[0-9]{1,2}%'; + $regexp_integer = '[-]?[0-9]+'; + $regexp_number = '[-]?(?:[0-9]+|[0-9]*\.[0-9]+)'; + // https://www.w3.org/TR/css3-values/#lengths + $regexp_length = $regexp_number . '(?:px|pt|em|ex|in|cm|mm|pc|q|ch|rem|vw|vh|vmin|vmax)?'; + $regexp_percent = '[-]?[0-9]*[.]?[0-9]*%'; $regexp_color = '#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|rgb\(\s*[0-9]{0,3}%?(?:\s*,\s*[0-9]{0,3}%?){2}\s*\)|[a-zA-Z]+'; $regexp_bgcolor = $regexp_color . '|transparent'; $regexp_border_width = $regexp_length . '|thin|medium|thick'; @@ -388,6 +412,7 @@ function wysiwyg_filter_get_style_property_groups() { 'border-right-style' => '(?:' . $regexp_border_style . ')', 'border-bottom-style' => '(?:' . $regexp_border_style . ')', 'border-left-style' => '(?:' . $regexp_border_style . ')', + 'border-radius' => '(?:' . $regexp_length . ')', ), ), 'dimension' => array( @@ -419,7 +444,7 @@ function wysiwyg_filter_get_style_property_groups() { 'title' => t('Layout properties'), 'properties' => array( 'clear' => '(?:left|right|both|none)', - 'display' => '(?:none|inline|block|list-item|run-in|compact|marker|table-(?:(?:row|header|group|column)-group|row|column|cell|caption)|(?:inline-)?table)', + 'display' => '(?:none|inline|inline-block|block|list-item|run-in|compact|marker|table-(?:(?:row|header|group|column)-group|row|column|cell|caption)|(?:inline-)?table)', 'float' => '(?:left|right|none)', 'position' => '(?:static|relative|absolute|fixed)', 'visibility' => '(?:visible|hidden|collapse)', @@ -464,7 +489,7 @@ function wysiwyg_filter_get_style_property_groups() { $groups['color']['properties']['background-repeat'] . '|' . $groups['color']['properties']['background-attachment'] . '|' . $groups['color']['properties']['background-position'] . ')'; - $groups['color']['properties']['background'] = '(?:' . $regexp . ')(?:(?:\s+' . $regexp . ')+)'; + $groups['color']['properties']['background'] = '(?:' . $regexp . ')(?:(?:\s*' . $regexp . ')+)'; // 'font' property. $regexp = '(?:' . @@ -473,7 +498,10 @@ function wysiwyg_filter_get_style_property_groups() { $groups['font']['properties']['font-weight'] . '|' . '(?:' . $groups['font']['properties']['font-size'] . ')(?:\s*/\s*' . $groups['dimension']['properties']['line-height'] . ')?|' . $groups['font']['properties']['font-family'] . ')'; - $groups['font']['properties']['font'] = '(?:' . $regexp . ')(?:(?:\s+' . $regexp . ')+)'; + $groups['font']['properties']['font'] = '(?:' . $regexp . ')(?:(?:\s*' . $regexp . ')+)'; + + // Allow other modules to alter groups + drupal_alter('wysiwyg_filter_style_property_groups', $groups); return $groups; } @@ -496,9 +524,12 @@ function wysiwyg_filter_get_filter_options($format_name, $settings) { 'nofollow_domains' => $settings['nofollow_domains'], ); foreach (wysiwyg_filter_get_advanced_rules() as $rule_key => $rule_info) { - $filter_options[$rule_key] = array(); - foreach ($settings["rule_$rule_key"] as $rule) { - $filter_options[$rule_key][] = '`^' . str_replace("\xFF", $rule_info['asterisk_expansion'], preg_quote(str_replace('*', "\xFF", $rule), '`')) . '$`'; + $filter_options["rule_bypass_$rule_key"] = !empty($settings["rule_bypass_$rule_key"]); + if (!$filter_options["rule_bypass_$rule_key"]) { + $filter_options[$rule_key] = array(); + foreach ($settings["rule_$rule_key"] as $rule) { + $filter_options[$rule_key][] = '`^' . str_replace("\xFF", $rule_info['asterisk_expansion'], preg_quote(str_replace('*', "\xFF", $rule), '`')) . '$`'; + } } } return $filter_options; diff --git a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.info b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.info index 891b6601..32cee8a7 100644 --- a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.info +++ b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.info @@ -9,9 +9,9 @@ files[] = wysiwyg_filter.install files[] = wysiwyg_filter.module files[] = wysiwyg_filter.pages.inc -; Information added by Drupal.org packaging script on 2016-02-04 -version = "7.x-1.6-rc3" +; Information added by Drupal.org packaging script on 2017-10-24 +version = "7.x-1.6-rc9" core = "7.x" project = "wysiwyg_filter" -datestamp = "1454601540" +datestamp = "1508857147" diff --git a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.module b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.module index 5eca4ea4..1473dd49 100644 --- a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.module +++ b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.module @@ -21,7 +21,7 @@ function wysiwyg_filter_filter_info() { $filters = array(); global $base_url; $parts = parse_url($base_url); - + $defaults = array( 'valid_elements' => wysiwyg_filter_default_valid_elements(), 'allow_comments' => 0, @@ -33,13 +33,14 @@ function wysiwyg_filter_filter_info() { endforeach; foreach(wysiwyg_filter_get_advanced_rules() as $rule => $stuff): $defaults["rule_$rule"] = array(); + $defaults["rule_bypass_$rule"] = 0; endforeach; - + $filters['wysiwyg'] = array( - 'title' => t('WYSIWYG Filter'), - 'description' => t('Allows you to restrict whether users can post HTML and which tags and attributes per HTML tag to filter out.'), - 'process callback' => 'wysiwyg_filter_filter_wysiwyg_process', - 'settings callback' => 'wysiwyg_filter_filter_wysiwyg_settings', + 'title' => t('WYSIWYG Filter'), + 'description' => t('Allows you to restrict whether users can post HTML and which tags and attributes per HTML tag to filter out.'), + 'process callback' => 'wysiwyg_filter_filter_wysiwyg_process', + 'settings callback' => 'wysiwyg_filter_filter_wysiwyg_settings', 'tips callback' => 'wysiwyg_filter_filter_wysiwyg_tips', 'default settings' => $defaults ); @@ -78,7 +79,7 @@ function wysiwyg_filter_wysiwyg_editor_settings_alter(&$editor_settings, $contex // Filter is enabled in the input format related to the current given context. if ($context['profile']->editor == 'tinymce'): // first get the filters and their settings - if (isset($context['profile']->format)): + if (isset($context['profile']->format)): $format_name = $context['profile']->format; $filters = filter_list_format($format_name); if($filters && array_key_exists('wysiwyg', $filters)): diff --git a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.pages.inc b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.pages.inc index ce2a02e8..a4452256 100644 --- a/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.pages.inc +++ b/sites/all/modules/contrib/editor/wysiwyg_filter/wysiwyg_filter.pages.inc @@ -50,16 +50,29 @@ function wysiwyg_filter_filter_wysiwyg_process($text, $filter, $format, $langcod // Named entities. $text = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $text); + // Preg modifiers: + // - x=extended (pattern with comments) + // - s=dotall (here for multiline comments) + // - m=multiline (so $ only matches EOF) + // - u=unicode return preg_replace_callback('% ( <(?=[^a-zA-Z!/]) # a lone < | # or # a comment | # or - <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string + < # a string that starts with a < + ( # ...and contains any number of + "[^"]*" # double-quoted strings + | + \'[^\']*\' # single-quoted strings + | + [^"\'>] # any other char + )* + (>|$) # up until the > or the end of the string | # or > # just a > - )%x', '_wysiwyg_filter_xss_split', $text); + )%xsmu', '_wysiwyg_filter_xss_split', $text); } /** @@ -94,7 +107,7 @@ function _wysiwyg_filter_xss_split($m, $store = FALSE) { return '<'; } - if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|()$%', $string, $matches)) { + if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9-/]+)([^>]*)>?|()$%', $string, $matches)) { // Seriously malformed return ''; } @@ -104,6 +117,11 @@ function _wysiwyg_filter_xss_split($m, $store = FALSE) { $attrlist = &$matches[3]; $comment = &$matches[4]; + // Convert synonyms to the element they get converted to. + if (!empty($filter_options['valid_elements'][$elem]) && is_string($filter_options['valid_elements'][$elem])) { + $elem = $filter_options['valid_elements'][$elem]; + } + if (!empty($comment)) { // Allow or disallow HTML comments. return (!empty($filter_options['allow_comments']) ? $comment : ''); @@ -152,13 +170,27 @@ function _wysiwyg_filter_xss_attributes($attr, $element = '') { $filter_options = $attr; return; } - // Shortcuts for filter options. $allowed_attributes = &$filter_options['valid_elements'][$element]; $allowed_properties = &$filter_options['style_properties']; - $allowed_style_urls = &$filter_options['style_urls']; - $allowed_class_names = &$filter_options['valid_classes']; - $allowed_element_ids = &$filter_options['valid_ids']; + if ($filter_options['rule_bypass_style_urls']) { + $allowed_style_urls = array(); + } + else { + $allowed_style_urls = &$filter_options['style_urls']; + } + $bypass_valid_classes = $filter_options['rule_bypass_valid_classes']; + if (!$bypass_valid_classes) { + $allowed_class_names = &$filter_options['valid_classes']; + } + $bypass_valid_ids = $filter_options['rule_bypass_valid_ids']; + if ($bypass_valid_ids) { + $allowed_element_ids = array('/.*/'); + } + else { + $allowed_element_ids = &$filter_options['valid_ids']; + } + $nofollow_policy = &$filter_options['nofollow_policy']; $nofollow_domains = &$filter_options['nofollow_domains']; @@ -263,13 +295,14 @@ function _wysiwyg_filter_xss_attributes($attr, $element = '') { } // The attribute list ends with a valueless attribute like "selected". - if ($mode == 1 && !$skip) { + // is_array() ensures this isn't run for synonyms. + if ($mode == 1 && !$skip && is_array($attrarr[$attrname])) { $attrarr[$attrname] = array(); } // Check the current HTML element for required attributes. foreach ($allowed_attributes as $attrname => $attrinfo) { - if (!empty($attrinfo['required']) && !isset($attrarr[$attrname])) { + if (!empty($attrinfo['required']) && empty($attrarr[$attrname]['value'])) { // Ignore the whole element if required attribute is not present. return FALSE; } @@ -391,13 +424,19 @@ function _wysiwyg_filter_xss_attributes($attr, $element = '') { // sign is not allowed, there's no need here to check for bad protocols. $dirty_names = array_filter(array_map('trim', explode(' ', decode_entities($attrinfo['value'])))); $valid_names = array(); - foreach ($dirty_names as $dirty_name) { - foreach ($allowed_class_names as $regexp) { - if (preg_match($regexp, $dirty_name)) { - $valid_names[] = $dirty_name; + if ($bypass_valid_classes) { + $valid_names = $dirty_names; + } + else { + foreach ($dirty_names as $dirty_name) { + foreach ($allowed_class_names as $regexp) { + if (preg_match($regexp, $dirty_name)) { + $valid_names[] = $dirty_name; + } } } } + if (empty($valid_names)) { // Ignore attribute if no class name remains after validation. continue; @@ -426,6 +465,7 @@ function _wysiwyg_filter_xss_attributes($attr, $element = '') { // Ignore attribute if it contains invalid value. continue; } + // Element ID is valid, check_plain result. $attrinfo['value'] = check_plain($attrinfo['value']); } @@ -440,6 +480,11 @@ function _wysiwyg_filter_xss_attributes($attr, $element = '') { // If this is element, then check domain name for rel="nofollow" policies in effect. if ($element == 'a' && $attrname == 'href' && $nofollow_policy != 'disabled' && !$add_nofollow) { $domain_found = FALSE; + if ($nofollow_policy == 'whitelist_current') { + global $base_url; + $parts = parse_url($base_url); + $nofollow_domains = array($parts['host']); + } foreach ($nofollow_domains as $domain) { $domain = str_replace('.', '\.', $domain); // escape dots if (preg_match('#://.*' . $domain . '([^a-z0-9]|$)#i', $attrinfo['value'])) { @@ -447,7 +492,8 @@ function _wysiwyg_filter_xss_attributes($attr, $element = '') { break; } } - if (($nofollow_policy == 'blacklist' && $domain_found) || ($nofollow_policy == 'whitelist' && !$domain_found)) { + $link_is_relative = !parse_url($attrinfo['value'], PHP_URL_HOST); + if (($nofollow_policy == 'blacklist' && $domain_found) || (($nofollow_policy == 'whitelist' || $nofollow_policy == 'whitelist_current') && !$domain_found && !$link_is_relative)) { $add_nofollow = TRUE; } } diff --git a/sites/all/modules/contrib/form/webform/CHANGELOG.txt b/sites/all/modules/contrib/form/webform/CHANGELOG.txt deleted file mode 100644 index 981dd2d2..00000000 --- a/sites/all/modules/contrib/form/webform/CHANGELOG.txt +++ /dev/null @@ -1,37 +0,0 @@ -Webform 2.x Changelog ---------------------- - -2.x to 3.0 ----------- -- Module directory structure moved around. -- Webform configuration moved to an entirely separate tab. -- E-mail templates are now editable by administrators. -- Conditional fields. -- Submissions may be saved as a draft and resumed later. -- Webform may now be attached to any content type. -- Public API for allowing other modules to provide components. -- Public API for interacting with submission save, insert, update, and delete. -- New rendering capabilities for HTML presentation of submissions and e-mails. -- Print module support. -- Basic Views module support. -- Popup calendar support on Date components (with Date Popup module). -- New Mollom module integration. - -1.x to 2.0 ----------- -- Redirect POST option removed. -- Webform components moved to the "Form components" tab when editing. -- Webform node structure changed. All webform additions to the node are placed in $node->webform. -- Clone option added to components. -- Database storage improved to be more consistent and efficient. -- Additional e-mails may be sent by modifying the $node->webform['additional_emails'] variable in the Additional Validation field. -- The values of select and hidden fields may receive e-mails by using the option in Conditional e-mail recipients field. -- E-mail from name, from address, and from subject may be entered in a text field. -- The complete webform may be shown in the teaser view of a node. -- Submit button text may be changed. -- Theme function theme_webform_create_mailmessage() has been renamed to theme_webform_mail_message(). -- $cid parameter added to theme_webform_mail_message() to create unique e-mails depending on a particular recipient or component. -- Theme function theme_webform_mail_headers added. -- Component descriptions are textareas rather than textfields. -- _webform_filtervalues() has been renamed to _webform_filter_values. - diff --git a/sites/all/modules/contrib/form/webform/README.txt b/sites/all/modules/contrib/form/webform/README.txt index 7adb34a1..63abeb5c 100644 --- a/sites/all/modules/contrib/form/webform/README.txt +++ b/sites/all/modules/contrib/form/webform/README.txt @@ -1,14 +1,15 @@ Description ----------- This module adds a webform content type to your Drupal site. -A webform can be a questionnaire, contact or request form. These can be used +A webform can be a questionnaire, contact or request form. These can be used by visitor to make contact or to enable a more complex survey than polls -provide. Submissions from a webform are saved in a database table and +provide. Submissions from a webform are saved in a database table and can optionally be mailed to e-mail addresses upon submission. Requirements ------------ Drupal 7.x +See https://www.drupal.org/project/webform for additional requirements. Installation ------------ @@ -23,19 +24,29 @@ Installation Upgrading from previous versions -------------------------------- -Note that if you are upgrading from a Drupal 6 installation of Webform, you MUST -have been running Webform 3.x on your Drupal 6 site before upgrading to Drupal -7 and Webform 3.x. You cannot upgrade directly from Webform 6.x-2.x to Webform -7.x-3.x. +Note that you must be running the latest 3.x version of Webform (for either +Drupal 6 or Drupal 7) before upgrading to Webform 4.x. -1. Copy the entire webform directory the Drupal modules directory. +If you have contributed modules, custom modules, or theming on your Webforms, +please read over the documentation for upgrading your code for Webform 4.x at +https://drupal.org/node/1609324. -2. Login as the FIRST user or change the $access_check in update.php to FALSE +1. MAKE A DATABASE BACKUP. Upgrading to Webform 4.x makes a signficant number of + database changes. If you encounter an error and need to downgrade, you must + restore the previous database. You can make a database backup with your + hosting provider, using the Backup and Migrate module, or from the command + line. -3. Run update.php (at http://www.example.com/update.php) +2. Copy the entire webform directory the Drupal modules directory, replacing the + old copy of Webform. DO NOT KEEP THE OLD COPY in the same directory or + anywhere Drupal could possibily find it. Delete it from the server. + +3. Login as an administrative user or change the $update_free_access in + update.php to TRUE. + +4. Run update.php (at http://www.example.com/update.php). Support ------- Please use the issue queue for filing bugs with this module at http://drupal.org/project/issues/webform - diff --git a/sites/all/modules/contrib/form/webform/THEMING.txt b/sites/all/modules/contrib/form/webform/THEMING.txt index f0001b88..bf771a03 100644 --- a/sites/all/modules/contrib/form/webform/THEMING.txt +++ b/sites/all/modules/contrib/form/webform/THEMING.txt @@ -1,9 +1,9 @@ Overview -------- -Webform supports theming similar to the CCK or Views modules. Any webform -may be themed on the server side, though doing so may require a reasonable -amount of knowledge about the Drupal Form API. More information about the Form -API may be found at http://api.drupal.org/api/file/developer/topics/forms_api.html +Webform supports theming similar to the CCK or Views modules. Any webform may be +themed on the server side, though doing so may require a reasonable amount of +knowledge about the Drupal Form API. More information about the Form API may be +found at: http://api.drupal.org/api/file/developer/topics/forms_api.html Theme submission e-mails ----------------------- @@ -63,10 +63,10 @@ the confirmation page of a single node or all webforms on your site. - + - You may edit the webform-confirmation.tpl.php file in your theme directory, @@ -111,5 +111,4 @@ The template file for theming submissions is webform-submission.tpl.php. You can use webform-submission-[node id here].tpl.php for individual nodes if desired. Note that the contents of this template are used not only for display of submissions in the Webform interface but also in e-mails when printing out -the %email_values token. - +the [submission:values] token. diff --git a/sites/all/modules/contrib/form/webform/components/date.inc b/sites/all/modules/contrib/form/webform/components/date.inc index 07a1322c..23a9648e 100644 --- a/sites/all/modules/contrib/form/webform/components/date.inc +++ b/sites/all/modules/contrib/form/webform/components/date.inc @@ -15,16 +15,19 @@ function _webform_defaults_date() { 'pid' => 0, 'weight' => 0, 'value' => '', - 'mandatory' => 0, + 'required' => 0, 'extra' => array( 'timezone' => 'user', + 'exclude' => array(), 'start_date' => '-2 years', 'end_date' => '+2 years', 'year_textfield' => 0, 'datepicker' => 1, 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, 'private' => FALSE, + 'analysis' => FALSE, ), ); } @@ -67,12 +70,25 @@ function _webform_edit_date($component) { '#type' => 'radios', '#title' => t('Default value timezone'), '#default_value' => empty($component['extra']['timezone']) ? 'user' : $component['extra']['timezone'], - '#description' => t('If using relative dates for a default value (e.g. "today") base the current day on this timezone.'), + '#description' => t('If using relative dates for a default value (for example, "today") base the current day on this timezone.'), '#options' => array('user' => t('User timezone'), 'site' => t('Website timezone')), '#weight' => 2, '#access' => variable_get('configurable_timezones', 1), ); + $form['extra']['exclude'] = array( + '#type' => 'checkboxes', + '#title' => t('Hide'), + '#default_value' => $component['extra']['exclude'], + '#options' => array( + 'day' => t('Day'), + 'month' => t('Month'), + 'year' => t('Year'), + ), + '#description' => t('A hidden day or month will be set to 1. A hidden year will be set to the year of the default value.'), + '#weight' => 3, + ); + $form['display']['datepicker'] = array( '#type' => 'checkbox', '#title' => t('Enable popup calendar'), @@ -110,27 +126,63 @@ function _webform_edit_date($component) { '#parents' => array('extra', 'end_date'), ); + $form['#validate'] = array('_webform_edit_date_validate'); return $form; } +/** + * Implements hook_form_id_validate(). + * + * Warns user about hiding all the date fields and not using the date picker. + */ +function _webform_edit_date_validate($form, &$form_state) { + // Reduce checkbox values to simple non-associative array of values. + form_set_value($form['extra']['exclude'], array_filter(array_values($form_state['values']['extra']['exclude'])), $form_state); + + // Note that Drupal 7 doesn't support setting errors no checkboxes due to + // browser issues. See: https://www.drupal.org/node/222380 + if (count($form_state['values']['extra']['exclude']) == 3) { + form_set_error('extra][exclude', 'The day, month and year can\'t all be hidden.'); + } + if ($form_state['values']['extra']['exclude'] == array('month')) { + form_set_error('extra][exclude', 'You cannot hide just the month.'); + } + + // Validate that the start and end dates are valid. Don't validate the default + // date because with token substitution, it might not be valid at component + // definition time. Also note that validation was introduced in 7.x-4.8 and + // previously-defined webform may have invalid start and end dates. + foreach (array('start_date', 'end_date') as $field) { + if (trim($form_state['values']['extra'][$field]) && !($date[$field] = webform_strtodate('c', $form_state['values']['extra'][$field]))) { + form_set_error("extra][$field", t('The @field could not be interpreted in GNU Date Input Format.', + array('@field' => $form['validation'][$field]['#title']))); + } + } + if (!empty($date['start_date']) && !empty($date['end_date']) && $date['end_date'] < $date['start_date']) { + form_set_error('extra][end_date', t('The End date must be on or after the Start date.')); + } +} + /** * Implements _webform_render_component(). */ -function _webform_render_date($component, $value = NULL, $filter = TRUE) { +function _webform_render_date($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( '#type' => 'date', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], - '#required' => $component['mandatory'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#required' => $component['required'], '#start_date' => trim($component['extra']['start_date']), '#end_date' => trim($component['extra']['end_date']), + '#reference_timestamp' => $submission && $submission->completed ? $submission->completed : NULL, '#year_textfield' => $component['extra']['year_textfield'], - '#default_value' => $filter ? _webform_filter_values($component['value'], $node, NULL, NULL, FALSE) : $component['value'], + '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'], '#timezone' => $component['extra']['timezone'], + '#exclude' => $component['extra']['exclude'], '#process' => array('webform_expand_date'), '#theme' => 'webform_date', '#theme_wrappers' => array('webform_element'), @@ -160,9 +212,10 @@ function _webform_render_date($component, $value = NULL, $filter = TRUE) { * Form API #process function for Webform date fields. */ function webform_expand_date($element) { + $timezone = $element['#timezone'] != 'user' ? NULL : 'user'; + // Accept a string or array value for #default_value. if (!empty($element['#default_value']) && is_string($element['#default_value'])) { - $timezone = $element['#timezone'] != 'user' ? NULL : 'user'; $timestring = webform_strtodate('c', $element['#default_value'], $timezone); $element['#default_value'] = webform_date_array($timestring, 'date'); } @@ -171,7 +224,7 @@ function webform_expand_date($element) { unset($element['#value']); } - // Set defaults according to existing #default_value (set by Form API) + // Set defaults according to existing #default_value (set by Form API). if (isset($element['#default_value']['month']) || isset($element['#default_value']['day']) || isset($element['#default_value']['year'])) { $default_values = array( 'month' => $element['#default_value']['month'], @@ -190,31 +243,100 @@ function webform_expand_date($element) { // Let Drupal do it's normal expansion. $element = form_process_date($element); + // Convert relative dates to absolute and calculate the year, month and day. + $timezone = $element['#timezone'] != 'user' ? NULL : 'user'; + foreach (array('start', 'end') as $start_end) { + $element_field = &$element["#{$start_end}_date"]; + $element_field = $element_field ? webform_strtodate('Y-m-d', $element_field, $timezone, $element['#reference_timestamp']) : ''; + if ($element_field) { + $parts = explode('-', $element_field); + } + else { + $parts = $start_end == 'start' ? array(webform_strtodate('Y', '-2 years'), 1, 1) : array(webform_strtodate('Y', '+2 years'), 12, 31); + $element_field = ''; + } + // Drop PHP reference. + unset($element_field); + $parts[3] = $parts[0] . '-' . $parts[1] . '-' . $parts[2]; + $range[$start_end] = array_combine(array('year', 'month', 'day', 'date'), $parts); + } + + // The start date is not guaranteed to be early than the end date for + // historical reasons. + if ($range['start']['date'] > $range['end']['date']) { + $temp = $range['start']; + $range['start'] = $range['end']; + $range['end'] = $temp; + } + + // Restrict the months and days when not all options are valid choices. + if ($element['#start_date'] && $element['#end_date']) { + $delta_months = ($range['end']['year'] * 12 + $range['end']['month']) - ($range['start']['year'] * 12 + $range['start']['month']); + if ($delta_months < 11) { + // There are 10 or fewer months between the start and end date. If there + // were 11, then every month would be possible, and the menu select + // should not be pruned. + $month_options = &$element['month']['#options']; + if ($range['start']['month'] <= $range['end']['month']) { + $month_options = array_intersect_key($month_options, array_flip(range($range['start']['month'], $range['end']['month']))); + } + else { + $month_options = array_intersect_key($month_options, array_flip(range($range['start']['month'], 12))) + + array_intersect_key($month_options, array_flip(range(1, $range['end']['month']))); + } + // Drop PHP reference. + unset($month_options); + if ($delta_months <= 1) { + // The start and end date are either on the same month or consecutive + // months. See if the days should be pruned. + $day_options = &$element['day']['#options']; + if ($range['start']['month'] == $range['end']['month']) { + // Range is within the same month. The days are a simple range from + // start day to end day. + $day_options = array_intersect_key($day_options, array_flip(range($range['start']['day'], $range['end']['day']))); + } + elseif ($range['start']['day'] > $range['end']['day'] + 1) { + // Range spans two months and at least one day would be omitted. + $days_in_month = date('t', mktime(0, 0, 0, $range['start']['month'], 1, $range['start']['year'])); + $day_options = array_intersect_key($day_options, array_flip(range($range['start']['day'], $days_in_month))) + + array_intersect_key($day_options, array_flip(range(1, $range['end']['day']))); + } + // Drop PHP reference. + unset($day_options); + } + } + } + // Set default values. foreach ($default_values as $type => $value) { switch ($type) { case 'month': $none = t('Month'); + $hidden_default = 1; break; + case 'day': $none = t('Day'); + $hidden_default = 1; break; + case 'year': $none = t('Year'); + $hidden_default = !empty($element['#default_value']['year']) ? $element['#default_value']['year'] : webform_strtodate('Y', 'today', $timezone); break; } unset($element[$type]['#value']); $element[$type]['#title'] = $none; $element[$type]['#title_display'] = 'invisible'; - $element[$type]['#default_value'] = isset($default_values[$type]) ? $default_values[$type] : NULL; + $element[$type]['#default_value'] = $default_values[$type]; $element[$type]['#options'] = array('' => $none) + $element[$type]['#options']; - } - - // Convert relative dates to absolute ones. - foreach (array('start_date', 'end_date') as $start_end) { - $timezone = $element['#timezone'] != 'user' ? NULL : 'user'; - $date = webform_strtodate('Y-m-d', $element['#' . $start_end], $timezone); - $element['#' . $start_end] = $date ? $date : ''; + if (in_array($type, $element['#exclude'])) { + $element[$type] += array( + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + $element[$type]['#default_value'] = $hidden_default; + } } // Tweak the year field. @@ -225,9 +347,7 @@ function webform_expand_date($element) { unset($element['year']['#options']); } elseif ($element['#start_date'] || $element['#end_date']) { - $start_year = $element['#start_date'] ? webform_strtodate('Y', $element['#start_date']) : webform_strtodate('Y', '-2 years'); - $end_year = $element['#end_date'] ? webform_strtodate('Y', $element['#end_date']) : webform_strtodate('Y', '+2 years'); - $element['year']['#options'] = array('' => t('Year')) + drupal_map_assoc(range($start_year, $end_year)); + $element['year']['#options'] = array('' => t('Year')) + drupal_map_assoc(range($range['start']['year'], $range['end']['year'])); } return $element; @@ -250,6 +370,13 @@ function theme_webform_date($variables) { $element['day']['#attributes']['class'][] = 'error'; } + // Add HTML5 required attribute, if needed. + if ($element['#required']) { + $element['year']['#attributes']['required'] = 'required'; + $element['month']['#attributes']['required'] = 'required'; + $element['day']['#attributes']['required'] = 'required'; + } + $class = array('webform-container-inline'); // Add the JavaScript calendar if available (provided by Date module package). @@ -262,7 +389,7 @@ function theme_webform_date($variables) { if ($element['#end_date']) { $calendar_class[] = 'webform-calendar-end-' . $element['#end_date']; } - $calendar_class[] ='webform-calendar-day-' . variable_get('date_first_day', 0); + $calendar_class[] = 'webform-calendar-day-' . variable_get('date_first_day', 0); $calendar = theme('webform_calendar', array('component' => $element['#webform_component'], 'calendar_classes' => $calendar_class)); } @@ -280,17 +407,35 @@ function theme_webform_date($variables) { * Element validation for Webform date fields. */ function webform_validate_date($element, $form_state) { - // Check if the user filled the required fields. - foreach (array('day', 'month', 'year') as $field_type) { - if (!is_numeric($element[$field_type]['#value']) && $element['#required']) { - form_error($element, t('!name field is required.', array('!name' => $element['#title']))); - return; + $field_types = array('day', 'month', 'year'); + + // Determine if the user has specified a date. Hidden parts of the date will + // be submitted automatically. + foreach ($field_types as $field_type) { + if (!in_array($field_type, $element['#exclude']) && $element[$field_type]['#value'] !== '') { + $field_found = TRUE; } } - if ($element['month']['#value'] !== '' || $element['day']['#value'] !== '' || $element['year']['#value'] !== '') { + if (isset($field_found)) { + // Check that each part of the date has been filled in. + foreach ($field_types as $field_type) { + if (empty($element[$field_type]['#value'])) { + form_error($element[$field_type], t('!part in !name is missing.', array('!name' => $element['#title'], '!part' => $element[$field_type]['#title']))); + $missing_fields = TRUE; + } + } + if (isset($missing_fields)) { + return; + } + + // Ensure date is made up of integers. + foreach (array('year', 'month', 'day') as $date_part) { + $element[$date_part]['#value'] = (int) $element[$date_part]['#value']; + } + // Check for a valid date. - if (!checkdate((int) $element['month']['#value'], (int) $element['day']['#value'], (int) $element['year']['#value'])) { + if (!checkdate($element['month']['#value'], $element['day']['#value'], $element['year']['#value'])) { form_error($element, t('Entered !name is not a valid date.', array('!name' => $element['#title']))); return; } @@ -299,7 +444,8 @@ function webform_validate_date($element, $form_state) { $timestamp = strtotime($element['year']['#value'] . '-' . $element['month']['#value'] . '-' . $element['day']['#value']); $format = webform_date_format('short'); - // Flip start and end if needed. + // Flip start and end if needed. Prior to 7.x-4.8, it was possible to save + // a date component with the end date earlier than the start date. $date1 = strtotime($element['#start_date']); $date2 = strtotime($element['#end_date']); if ($date1 !== FALSE && $date2 !== FALSE) { @@ -312,19 +458,18 @@ function webform_validate_date($element, $form_state) { } // Check that the date is after the start date. - if ($start_date !== FALSE) { - if ($timestamp < $start_date) { - form_error($element, t('The entered date must be @start_date or later.', array('@start_date' => date($format, $start_date)))); - } + if ($start_date !== FALSE && $timestamp < $start_date) { + form_error($element, t('The entered date must be @start_date or later.', array('@start_date' => date($format, $start_date)))); } // Check that the date is before the end date. - if ($end_date !== FALSE) { - if ($timestamp > $end_date) { - form_error($element, t('The entered date must be @end_date or earlier.', array('@end_date' => date($format, $end_date)))); - } + if ($end_date !== FALSE && $timestamp > $end_date) { + form_error($element, t('The entered date must be @end_date or earlier.', array('@end_date' => date($format, $end_date)))); } } + elseif ($element['#required']) { + form_error($element, t('!name field is required.', array('!name' => $element['#title']))); + } } /** @@ -338,15 +483,18 @@ function _webform_submit_date($component, $value) { /** * Implements _webform_display_component(). */ -function _webform_display_date($component, $value, $format = 'html') { +function _webform_display_date($component, $value, $format = 'html', $submission = array()) { $value = webform_date_array(isset($value[0]) ? $value[0] : '', 'date'); return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#theme' => 'webform_display_date', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), '#format' => $format, + '#exclude' => $component['extra']['exclude'], + '#value' => $value, '#translatable' => array('title'), ); @@ -360,7 +508,7 @@ function theme_webform_display_date($variables) { $output = ' '; if ($element['#value']['year'] && $element['#value']['month'] && $element['#value']['day']) { $timestamp = webform_strtotime($element['#value']['month'] . '/' . $element['#value']['day'] . '/' . $element['#value']['year']); - $format = webform_date_format('medium'); + $format = webform_date_format(NULL, $element['#exclude']); $output = format_date($timestamp, 'custom', $format, 'UTC'); } @@ -370,15 +518,19 @@ function theme_webform_display_date($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_date($component, $sids = array()) { +function _webform_analysis_date($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']) - ->orderBy('sid'); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']) + ->orderBy('wsd.sid'); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $result = $query->execute(); @@ -396,7 +548,10 @@ function _webform_analysis_date($component, $sids = array()) { $nonblanks = count($dates); $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks)); $rows[1] = array(t('User entered value'), $nonblanks); - return $rows; + + return array( + 'table_rows' => $rows, + ); } /** @@ -405,7 +560,7 @@ function _webform_analysis_date($component, $sids = array()) { function _webform_table_date($component, $value) { if ($value[0]) { $timestamp = webform_strtotime($value[0]); - $format = webform_date_format('short'); + $format = webform_date_format('short', $component['extra']['exclude']); return format_date($timestamp, 'custom', $format, 'UTC'); } else { @@ -420,7 +575,7 @@ function _webform_csv_headers_date($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } @@ -430,7 +585,13 @@ function _webform_csv_headers_date($component, $export_options) { function _webform_csv_data_date($component, $export_options, $value) { if ($value[0]) { $timestamp = webform_strtotime($value[0]); - $format = webform_date_format('short'); + if (!empty($export_options['iso8601_date'])) { + // ISO 8601 date: 2004-02-12. + $format = 'Y-m-d'; + } + else { + $format = webform_date_format('short'); + } return format_date($timestamp, 'custom', $format, 'UTC'); } else { diff --git a/sites/all/modules/contrib/form/webform/components/email.inc b/sites/all/modules/contrib/form/webform/components/email.inc index 9a65e7f7..bbabf696 100644 --- a/sites/all/modules/contrib/form/webform/components/email.inc +++ b/sites/all/modules/contrib/form/webform/components/email.inc @@ -15,15 +15,20 @@ function _webform_defaults_email() { 'pid' => 0, 'weight' => 0, 'value' => '', - 'mandatory' => 0, + 'required' => 0, 'extra' => array( + 'multiple' => 0, + 'format' => 'short', 'width' => '', 'unique' => 0, 'disabled' => 0, 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, + 'placeholder' => '', 'attributes' => array(), 'private' => FALSE, + 'analysis' => FALSE, ), ); } @@ -52,22 +57,41 @@ function _webform_edit_email($component) { '#type' => 'textfield', '#title' => t('Default value'), '#default_value' => $component['value'], - '#description' => t('The default value of the field.') . theme('webform_token_help'), + '#description' => t('The default value of the field.') . ' ' . theme('webform_token_help'), '#size' => 60, '#maxlength' => 127, '#weight' => 0, - '#attributes' => ($component['value'] == '%useremail' && count(form_get_errors()) == 0) ? array('disabled' => TRUE) : array(), + '#attributes' => ($component['value'] == '[current-user:mail]' && count(form_get_errors()) == 0) ? array('disabled' => TRUE) : array(), '#id' => 'email-value', ); $form['user_email'] = array( '#type' => 'checkbox', '#title' => t('User email as default'), - '#default_value' => $component['value'] == '%useremail' ? 1 : 0, + '#default_value' => $component['value'] == '[current-user:mail]' ? 1 : 0, '#description' => t('Set the default value of this field to the user email, if he/she is logged in.'), - '#attributes' => array('onclick' => 'getElementById("email-value").value = (this.checked ? "%useremail" : ""); getElementById("email-value").disabled = this.checked;'), + '#attributes' => array('onclick' => 'getElementById("email-value").value = (this.checked ? "[current-user:mail]" : ""); getElementById("email-value").disabled = this.checked;'), '#weight' => 0, '#element_validate' => array('_webform_edit_email_validate'), ); + $form['extra']['multiple'] = array( + '#type' => 'checkbox', + '#title' => t('Multiple'), + '#default_value' => $component['extra']['multiple'], + '#description' => t('Allow multiple e-mail addresses, separated by commas.'), + '#weight' => 0, + ); + if (webform_variable_get('webform_email_address_format') == 'long') { + $form['extra']['format'] = array( + '#type' => 'radios', + '#title' => t('Format'), + '#options' => array( + 'long' => t('Allow long format: "Example Name" <name@example.com>'), + 'short' => t('Short format only: name@example.com'), + ), + '#default_value' => $component['extra']['format'], + '#description' => t('Not all servers support the "long" format.'), + ); + } $form['display']['width'] = array( '#type' => 'textfield', '#title' => t('Width'), @@ -77,11 +101,18 @@ function _webform_edit_email($component) { '#maxlength' => 10, '#parents' => array('extra', 'width'), ); + $form['display']['placeholder'] = array( + '#type' => 'textfield', + '#title' => t('Placeholder'), + '#default_value' => $component['extra']['placeholder'], + '#description' => t('The placeholder will be shown in the field until the user starts entering a value.') . ' ' . t('Often used for example values, such as "john@example.com".'), + '#parents' => array('extra', 'placeholder'), + ); $form['display']['disabled'] = array( '#type' => 'checkbox', '#title' => t('Disabled'), '#return_value' => 1, - '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'), + '#description' => t('Make this field non-editable. Useful for displaying default value. Changeable via JavaScript or developer tools.'), '#weight' => 11, '#default_value' => $component['extra']['disabled'], '#parents' => array('extra', 'disabled'), @@ -103,31 +134,34 @@ function _webform_edit_email($component) { */ function _webform_edit_email_validate($element, &$form_state) { if ($form_state['values']['user_email']) { - $form_state['values']['value'] = '%useremail'; + $form_state['values']['value'] = '[current-user:mail]'; } } /** * Implements _webform_render_component(). */ -function _webform_render_email($component, $value = NULL, $filter = TRUE) { - global $user; +function _webform_render_email($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( '#type' => 'webform_email', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#default_value' => $filter ? _webform_filter_values($component['value'], $node) : $component['value'], - '#required' => $component['mandatory'], + '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#attributes' => $component['extra']['attributes'], '#element_validate' => array('_webform_validate_email'), '#theme_wrappers' => array('webform_element'), '#translatable' => array('title', 'description'), ); + if ($component['required']) { + $element['#attributes']['required'] = 'required'; + } + // Add an e-mail class for identifying the difference from normal textfields. $element['#attributes']['class'][] = 'email'; @@ -136,10 +170,35 @@ function _webform_render_email($component, $value = NULL, $filter = TRUE) { $element['#element_validate'][] = 'webform_validate_unique'; } - if (isset($value)) { + if ($component['extra']['format'] == 'long') { + // html5 email elements enforce short-form email validation in addition to + // pattern validation. This means that long format email addresses must be + // rendered as text. + $element['#attributes']['type'] = 'text'; + + // html5 patterns have implied delimiters and start and end patterns. + // The are also case sensitive, not global, and not multi-line. + // See https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation + // See http://stackoverflow.com/questions/19605773/html5-email-validation + $address = '[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*'; + $name = '("[^<>"]*?"|[^<>",]*?)'; + $short_long = "($name *<$address>|$address)"; + $element['#attributes']['pattern'] = $component['extra']['multiple'] + ? "$short_long(, *$short_long)*" + : $short_long; + } + elseif ($component['extra']['multiple']) { + $element['#attributes']['multiple'] = 'multiple'; + } + + if (isset($value[0])) { $element['#default_value'] = $value[0]; } + if ($component['extra']['placeholder']) { + $element['#attributes']['placeholder'] = $component['extra']['placeholder']; + } + if ($component['extra']['disabled']) { if ($filter) { $element['#attributes']['readonly'] = 'readonly'; @@ -181,33 +240,36 @@ function theme_webform_email($variables) { } /** - * A Drupal Form API Validation function. Validates the entered values from - * email components on the client-side form. + * A Drupal Form API Validation function. * - * @param $form_element + * Validates the entered values from email components on the client-side form. + * Calls a form_set_error if the e-mail is not valid. + * + * @param array $form_element * The e-mail form element. - * @param $form_state + * @param array $form_state * The full form state for the webform. - * @return - * None. Calls a form_set_error if the e-mail is not valid. */ function _webform_validate_email($form_element, &$form_state) { $component = $form_element['#webform_component']; - $value = trim($form_element['#value']); - if ($value !== '' && !valid_email_address($value)) { - form_error($form_element, t('%value is not a valid email address.', array('%value' => $value))); - } - else { - form_set_value($form_element, $value, $form_state); - } + $format = webform_variable_get('webform_email_address_format') == 'long' ? $component['extra']['format'] : 'short'; + webform_email_validate($form_element['#value'], + implode('][', $form_element['#parents']), + // Required validation is done elsewhere. + TRUE, + $component['extra']['multiple'], + // No tokens are allowed in user input. + FALSE, + $format); } /** * Implements _webform_display_component(). */ -function _webform_display_email($component, $value, $format = 'html') { +function _webform_display_email($component, $value, $format = 'html', $submission = array()) { return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#theme' => 'webform_display_email', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), @@ -229,14 +291,18 @@ function theme_webform_display_email($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_email($component, $sids = array()) { +function _webform_analysis_email($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $nonblanks = 0; @@ -254,8 +320,16 @@ function _webform_analysis_email($component, $sids = array()) { $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks)); $rows[1] = array(t('User entered value'), $nonblanks); - $rows[2] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0')); - return $rows; + + $other[0] = array( + t('Average submission length in words (ex blanks)'), + ($nonblanks != 0 ? number_format($wordcount / $nonblanks, 2) : '0'), + ); + + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -265,6 +339,13 @@ function _webform_table_email($component, $value) { return check_plain(empty($value[0]) ? '' : $value[0]); } +/** + * Implements _webform_action_set_component(). + */ +function _webform_action_set_email($component, &$element, &$form_state, $value) { + $element['#value'] = $value; + form_set_value($element, $value, $form_state); +} /** * Implements _webform_csv_headers_component(). @@ -273,7 +354,7 @@ function _webform_csv_headers_email($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } diff --git a/sites/all/modules/contrib/form/webform/components/fieldset.inc b/sites/all/modules/contrib/form/webform/components/fieldset.inc index 63ad33a1..763e5211 100644 --- a/sites/all/modules/contrib/form/webform/components/fieldset.inc +++ b/sites/all/modules/contrib/form/webform/components/fieldset.inc @@ -19,6 +19,7 @@ function _webform_defaults_fieldset() { 'collapsible' => 0, 'collapsed' => 0, 'description' => '', + 'description_above' => FALSE, 'private' => FALSE, ), ); @@ -51,15 +52,15 @@ function _webform_edit_fieldset($component) { /** * Implements _webform_render_component(). */ -function _webform_render_fieldset($component, $value = NULL, $filter = TRUE) { +function _webform_render_fieldset($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( '#type' => 'fieldset', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : NULL, '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#collapsible' => $component['extra']['collapsible'], '#collapsed' => $component['extra']['collapsed'], '#attributes' => array('class' => array('webform-component-fieldset')), @@ -71,17 +72,18 @@ function _webform_render_fieldset($component, $value = NULL, $filter = TRUE) { } /** - * Pre-render function to set a fieldset ID. + * Pre-render function to set a unique fieldset class name. */ function webform_fieldset_prerender($element) { - $element['#id'] = 'webform-component-' . str_replace('_', '-', implode('--', array_slice($element['#parents'], 1))); + $element['#id'] = NULL; + $element['#attributes']['class'][] = 'webform-component--' . str_replace('_', '-', implode('--', array_slice($element['#parents'], 1))); return $element; } /** * Implements _webform_display_component(). */ -function _webform_display_fieldset($component, $value, $format = 'html') { +function _webform_display_fieldset($component, $value, $format = 'html', $submission = array()) { if ($format == 'text') { $element = array( '#title' => $component['name'], diff --git a/sites/all/modules/contrib/form/webform/components/file.inc b/sites/all/modules/contrib/form/webform/components/file.inc index 561c0754..b8233e50 100644 --- a/sites/all/modules/contrib/form/webform/components/file.inc +++ b/sites/all/modules/contrib/form/webform/components/file.inc @@ -9,10 +9,15 @@ * Implements _webform_defaults_component(). */ function _webform_defaults_file() { + // If private file storage is enabled, make it the default for security + // reasons. See: https://www.drupal.org/psa-2016-003 + $available_schemes = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE); + $scheme = isset($available_schemes['private']) ? 'private' : 'public'; + return array( 'name' => '', 'form_key' => NULL, - 'mandatory' => 0, + 'required' => 0, 'pid' => 0, 'weight' => 0, 'extra' => array( @@ -21,13 +26,16 @@ function _webform_defaults_file() { 'addextensions' => '', 'size' => '2 MB', ), - 'scheme' => 'public', + 'rename' => '', + 'scheme' => $scheme, 'directory' => '', 'progress_indicator' => 'throbber', 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, 'attributes' => array(), 'private' => FALSE, + 'analysis' => FALSE, ), ); } @@ -45,6 +53,10 @@ function _webform_theme_file() { 'render element' => 'element', 'file' => 'components/file.inc', ), + 'webform_managed_file' => array( + 'render element' => 'element', + 'file' => 'components/file.inc', + ), ); } @@ -52,137 +64,148 @@ function _webform_theme_file() { * Implements _webform_edit_component(). */ function _webform_edit_file($component) { - $form = array(); - $form['#element_validate'] = array('_webform_edit_file_check_directory'); - $form['#after_build'] = array('_webform_edit_file_check_directory'); + $form = array(); + $form['#element_validate'] = array('_webform_edit_file_check_directory'); + $form['#after_build'] = array('_webform_edit_file_check_directory'); - $form['validation']['size'] = array( - '#type' => 'textfield', - '#title' => t('Max upload size'), - '#default_value' => $component['extra']['filtering']['size'], - '#description' => t('Enter the max file size a user may upload such as 2 MB or 800 KB. Your server has a max upload size of @size.', array('@size' => format_size(file_upload_max_size()))), - '#size' => 10, - '#parents' => array('extra', 'filtering', 'size'), - '#element_validate' => array('_webform_edit_file_size_validate'), - '#weight' => 1, - ); + $form['validation']['size'] = array( + '#type' => 'textfield', + '#title' => t('Max upload size'), + '#default_value' => $component['extra']['filtering']['size'], + '#description' => t('Enter the max file size a user may upload such as 2 MB or 800 KB. Your server has a max upload size of @size.', array('@size' => format_size(file_upload_max_size()))), + '#size' => 10, + '#parents' => array('extra', 'filtering', 'size'), + '#element_validate' => array('_webform_edit_file_size_validate'), + '#weight' => 1, + ); - $form['validation']['extensions'] = array( - '#element_validate' => array('_webform_edit_file_extensions_validate'), - '#parents' => array('extra', 'filtering'), - '#theme' => 'webform_edit_file_extensions', - '#theme_wrappers' => array('form_element'), - '#title' => t('Allowed file extensions'), - '#attached' => array( - 'js' => array(drupal_get_path('module', 'webform') . '/js/webform-admin.js'), - 'css' => array(drupal_get_path('module', 'webform') . '/css/webform-admin.css'), - ), - '#weight' => 2, - ); + $form['validation']['extensions'] = array( + '#element_validate' => array('_webform_edit_file_extensions_validate'), + '#parents' => array('extra', 'filtering'), + '#theme' => 'webform_edit_file_extensions', + '#theme_wrappers' => array('form_element'), + '#title' => t('Allowed file extensions'), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'webform') . '/js/webform-admin.js'), + 'css' => array(drupal_get_path('module', 'webform') . '/css/webform-admin.css'), + ), + '#type' => 'webform_file_extensions', + '#weight' => 2, + ); - // Find the list of all currently valid extensions. - $current_types = isset($component['extra']['filtering']['types']) ? $component['extra']['filtering']['types'] : array(); + // Find the list of all currently valid extensions. + $current_types = isset($component['extra']['filtering']['types']) ? $component['extra']['filtering']['types'] : array(); - $types = array('gif', 'jpg', 'png'); - $form['validation']['extensions']['types']['webimages'] = array( - '#type' => 'checkboxes', - '#title' => t('Web images'), - '#options' => drupal_map_assoc($types), - '#default_value' => array_intersect($current_types, $types), - ); + $types = array('gif', 'jpg', 'png'); + $form['validation']['extensions']['types']['webimages'] = array( + '#type' => 'checkboxes', + '#title' => t('Web images'), + '#options' => drupal_map_assoc($types), + '#default_value' => array_intersect($current_types, $types), + ); - $types = array('bmp', 'eps', 'tif', 'pict', 'psd'); - $form['validation']['extensions']['types']['desktopimages'] = array( - '#type' => 'checkboxes', - '#title' => t('Desktop images'), - '#options' => drupal_map_assoc($types), - '#default_value' => array_intersect($current_types, $types), - ); + $types = array('bmp', 'eps', 'tif', 'pict', 'psd'); + $form['validation']['extensions']['types']['desktopimages'] = array( + '#type' => 'checkboxes', + '#title' => t('Desktop images'), + '#options' => drupal_map_assoc($types), + '#default_value' => array_intersect($current_types, $types), + ); - $types = array('txt', 'rtf', 'html', 'odf', 'pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'xml'); - $form['validation']['extensions']['types']['documents'] = array( - '#type' => 'checkboxes', - '#title' => t('Documents'), - '#options' => drupal_map_assoc($types), - '#default_value' => array_intersect($current_types, $types), - ); + $types = array('txt', 'rtf', 'html', 'pdf', 'doc', 'docx', 'odt', 'ppt', 'pptx', 'odp', 'xls', 'xlsx', 'ods', 'xml'); + $form['validation']['extensions']['types']['documents'] = array( + '#type' => 'checkboxes', + '#title' => t('Documents'), + '#options' => drupal_map_assoc($types), + '#default_value' => array_intersect($current_types, $types), + ); - $types = array('avi', 'mov', 'mp3', 'ogg', 'wav'); - $form['validation']['extensions']['types']['media'] = array( - '#type' => 'checkboxes', - '#title' => t('Media'), - '#options' => drupal_map_assoc($types), - '#default_value' => array_intersect($current_types, $types), - ); + $types = array('avi', 'mov', 'mp3', 'ogg', 'wav'); + $form['validation']['extensions']['types']['media'] = array( + '#type' => 'checkboxes', + '#title' => t('Media'), + '#options' => drupal_map_assoc($types), + '#default_value' => array_intersect($current_types, $types), + ); - $types = array('bz2', 'dmg', 'gz', 'jar', 'rar', 'sit', 'tar', 'zip'); - $form['validation']['extensions']['types']['archives'] = array( - '#type' => 'checkboxes', - '#title' => t('Archives'), - '#options' => drupal_map_assoc($types), - '#default_value' => array_intersect($current_types, $types), - ); + $types = array('bz2', 'dmg', 'gz', 'jar', 'rar', 'sit', 'tar', 'zip'); + $form['validation']['extensions']['types']['archives'] = array( + '#type' => 'checkboxes', + '#title' => t('Archives'), + '#options' => drupal_map_assoc($types), + '#default_value' => array_intersect($current_types, $types), + ); - $form['validation']['extensions']['addextensions'] = array( - '#type' => 'textfield', - '#title' => t('Additional extensions'), - '#default_value' => $component['extra']['filtering']['addextensions'], - '#description' => t('Enter a list of additional file extensions for this upload field, separated by commas.
    Entered extensions will be appended to checked items above.'), - '#size' => 20, - '#weight' => 3, - ); + $form['validation']['extensions']['addextensions'] = array( + '#type' => 'textfield', + '#title' => t('Additional extensions'), + '#default_value' => $component['extra']['filtering']['addextensions'], + '#description' => t('Enter a list of additional file extensions for this upload field, separated by commas.
    Entered extensions will be appended to checked items above.'), + '#size' => 20, + '#weight' => 3, + ); - $scheme_options = array(); - foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) { - $scheme_options[$scheme] = $stream_wrapper['name']; - } - $form['extra']['scheme'] = array( - '#type' => 'radios', - '#title' => t('Upload destination'), - '#options' => $scheme_options, - '#default_value' => $component['extra']['scheme'], - '#description' => t('Private file storage has significantly more overhead than public files, but restricts file access to users who can view submissions.'), - '#weight' => 4, - '#access' => count($scheme_options) > 1, - ); - $form['extra']['directory'] = array( - '#type' => 'textfield', - '#title' => t('Upload directory'), - '#default_value' => $component['extra']['directory'], - '#description' => t('You may optionally specify a sub-directory to store your files.'), - '#weight' => 5, - '#field_prefix' => 'webform/', - ); + $scheme_options = array(); + foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) { + $scheme_options[$scheme] = $stream_wrapper['name']; + } + $form['extra']['scheme'] = array( + '#type' => 'radios', + '#title' => t('Upload destination'), + '#options' => $scheme_options, + '#default_value' => $component['extra']['scheme'], + '#description' => t('Public files upload destination is dangerous for forms that are available to anonymous and/or untrusted users. For more information, see DRUPAL-PSA-2016-003.', array('@psa' => 'https://www.drupal.org/psa-2016-003')), + '#weight' => 4, + '#access' => count($scheme_options) > 1, + ); + $form['extra']['directory'] = array( + '#type' => 'textfield', + '#title' => t('Upload directory'), + '#default_value' => $component['extra']['directory'], + '#description' => t('You may optionally specify a sub-directory to store your files.') . ' ' . theme('webform_token_help'), + '#weight' => 5, + '#field_prefix' => 'webform/', + ); - $form['display']['progress_indicator'] = array( - '#type' => 'radios', - '#title' => t('Progress indicator'), - '#options' => array( - 'throbber' => t('Throbber'), - 'bar' => t('Bar with progress meter'), - ), - '#default_value' => $component['extra']['progress_indicator'], - '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'), - '#weight' => 16, - '#access' => file_progress_implementation(), - '#parents' => array('extra', 'progress_indicator'), - ); + $form['extra']['rename'] = array( + '#type' => 'textfield', + '#title' => t('Rename files'), + '#default_value' => $component['extra']['rename'], + '#description' => t('You may optionally use tokens to create a pattern used to rename files upon submission. Omit the extension; it will be added automatically.') . ' ' . theme('webform_token_help', array('groups' => array('node', 'submission'))), + '#weight' => 6, + '#element_validate' => array('_webform_edit_file_rename_validate'), + '#access' => webform_variable_get('webform_token_access'), + ); - // TODO: Make managed_file respect the "size" parameter. - /* - $form['display']['width'] = array( - '#type' => 'textfield', - '#title' => t('Width'), - '#default_value' => $component['extra']['width'], - '#description' => t('Width of the file field.') . ' ' . t('Leaving blank will use the default size.'), - '#size' => 5, - '#maxlength' => 10, - '#weight' => 4, - '#parents' => array('extra', 'width') - ); - */ + $form['display']['progress_indicator'] = array( + '#type' => 'radios', + '#title' => t('Progress indicator'), + '#options' => array( + 'throbber' => t('Throbber'), + 'bar' => t('Bar with progress meter'), + ), + '#default_value' => $component['extra']['progress_indicator'], + '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'), + '#weight' => 16, + '#access' => file_progress_implementation(), + '#parents' => array('extra', 'progress_indicator'), + ); - return $form; + return $form; +} + +/** + * Form API validator ensures rename string is empty or contains one token. + * + * A Form API element validate function to ensure that the rename string is + * either empty or contains at least one token. + */ +function _webform_edit_file_rename_validate($element, &$form_state, $form) { + $rename = trim($form_state['values']['extra']['rename']); + form_set_value($element, $rename, $form_state); + if (strlen($rename) && !count(token_scan($rename))) { + form_error($element, t('To create unique file names, use at least one token in the file name pattern.')); + } } /** @@ -192,12 +215,12 @@ function _webform_edit_file_size_validate($element) { if (!empty($element['#value'])) { $set_filesize = parse_size($element['#value']); if ($set_filesize == FALSE) { - form_error($element, t('File size @value is not a valid filesize. Use a value such as 2 MB or 800 KB.', array('@value' => $element['#value']))); + form_error($element, t('File size @value is not a valid file size. Use a value such as 2 MB or 800 KB.', array('@value' => $element['#value']))); } else { $max_filesize = parse_size(file_upload_max_size()); if ($max_filesize < $set_filesize) { - form_error($element, t('An upload size of @value is too large, you are allow to upload files @max or less.', array('@value' => $element['#value'], '@max' => format_size($max_filesize)))); + form_error($element, t('An upload size of @value is too large. You are allowed to upload files that are @max or less.', array('@value' => $element['#value'], '@max' => format_size($max_filesize)))); } } } @@ -213,15 +236,15 @@ function _webform_edit_file_check_directory($element) { $directory = $element['extra']['directory']['#value']; $destination_dir = file_stream_wrapper_uri_normalize($scheme . '://webform/' . $directory); + $tokenized_dir = drupal_strtolower(webform_replace_tokens($destination_dir, $element['#node'])); // Sanity check input to prevent use parent (../) directories. - if (preg_match('/\.\.[\/\\\]/', $destination_dir . '/')) { - form_error($element['extra']['directory'], t('The save directory %directory is not valid.', array('%directory' => $directory))); + if (preg_match('/\.\.[\/\\\]/', $tokenized_dir . '/')) { + form_error($element['extra']['directory'], t('The save directory %directory is not valid.', array('%directory' => $tokenized_dir))); } else { - $destination_success = file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY); - if (!$destination_success) { - form_error($element['extra']['directory'], t('The save directory %directory could not be created. Check that the webform files directory is writable.', array('%directory' => $directory))); + if (!file_prepare_directory($tokenized_dir, FILE_CREATE_DIRECTORY)) { + form_error($element['extra']['directory'], t('The save directory %directory could not be created. Check that the webform files directory is writable.', array('%directory' => $tokenized_dir))); } } @@ -241,6 +264,10 @@ function _webform_edit_file_extensions_validate($element, &$form_state) { foreach (array_keys($element['types'][$category]['#value']) as $extension) { if ($element['types'][$category][$extension]['#value']) { $extensions[] = $extension; + // "jpeg" is an exception. It is allowed anytime "jpg" is allowed. + if ($extension == 'jpg') { + $extensions[] = 'jpeg'; + } } } } @@ -267,7 +294,6 @@ function theme_webform_edit_file_extensions($variables) { $rows = array(); foreach (element_children($element['types']) as $filtergroup) { $row = array(); - $first_row = count($rows); if ($element['types'][$filtergroup]['#type'] == 'checkboxes') { $select_link = ' (' . t('select') . ')'; $row[] = $element['types'][$filtergroup]['#title']; @@ -279,12 +305,14 @@ function theme_webform_edit_file_extensions($variables) { } // Add the row for additional types. - $row = array(); - $title = $element['addextensions']['#title']; - $element['addextensions']['#title'] = NULL; - $row[] = array('data' => $title, 'colspan' => 2); - $row[] = drupal_render($element['addextensions']); - $rows[] = $row; + if (!isset($element['addextensions']['#access']) || $element['addextensions']['#access']) { + $row = array(); + $title = $element['addextensions']['#title']; + $element['addextensions']['#title'] = NULL; + $row[] = array('data' => $title, 'colspan' => 2); + $row[] = drupal_render($element['addextensions']); + $rows[] = $row; + } $header = array(array('data' => t('Category'), 'colspan' => '2'), array('data' => t('Types'))); @@ -302,7 +330,7 @@ function theme_webform_edit_file_extensions($variables) { /** * Implements _webform_render_component(). */ -function _webform_render_file($component, $value = NULL, $filter = TRUE) { +function _webform_render_file($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; // Cap the upload size according to the PHP limit. @@ -314,9 +342,10 @@ function _webform_render_file($component, $value = NULL, $filter = TRUE) { $element = array( '#type' => 'managed_file', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#theme' => 'webform_managed_file', + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#required' => $component['mandatory'], + '#required' => $component['required'], '#default_value' => isset($value[0]) ? $value[0] : NULL, '#attributes' => $component['extra']['attributes'], '#upload_validators' => array( @@ -324,27 +353,75 @@ function _webform_render_file($component, $value = NULL, $filter = TRUE) { 'file_validate_extensions' => array(implode(' ', $component['extra']['filtering']['types'])), ), '#pre_render' => array_merge(element_info_property('managed_file', '#pre_render'), array('webform_file_allow_access')), - '#upload_location' => $component['extra']['scheme'] . '://webform/' . $component['extra']['directory'], + '#upload_location' => $component['extra']['scheme'] . '://webform/' . ($filter + ? drupal_strtolower(webform_replace_tokens($component['extra']['directory'], $node)) + : $component['extra']['directory']), '#progress_indicator' => $component['extra']['progress_indicator'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#weight' => $component['weight'], '#theme_wrappers' => array('webform_element'), '#translatable' => array('title', 'description'), ); + if ($filter) { + $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators'])); + } + return $element; } +/** + * Returns HTML for a webform managed file element. + * + * See #2495821 and #2497909. The core theme_file_managed_file creates a + * wrapper around the element with the element's id, thereby creating 2 elements + * with the same id. + * + * @param array $variables + * An associative array containing: + * - element: A render element representing the file. + * + * @return string + * The HTML. + */ +function theme_webform_managed_file($variables) { + $element = $variables['element']; + + $attributes = array(); + + // For webform use, do not add the id to the wrapper. + // @code + // if (isset($element['#id'])) { + // $attributes['id'] = $element['#id']; + // } + // @endcode + if (!empty($element['#attributes']['class'])) { + $attributes['class'] = (array) $element['#attributes']['class']; + } + $attributes['class'][] = 'form-managed-file'; + + // This wrapper is required to apply JS behaviors and CSS styling. + $output = ''; + $output .= ''; + $output .= drupal_render_children($element); + $output .= ''; + return $output; +} + /** * Implements _webform_submit_component(). */ function _webform_submit_file($component, $value) { - if (is_array($value)) { - return !empty($value['fid']) ? $value['fid'] : ''; - } - else { - return !empty($value) ? $value : ''; + $fid = is_array($value) + ? (!empty($value['fid']) ? $value['fid'] : '') + : (!empty($value) ? $value : ''); + // Extend access to this file, even if the submission has not been saved yet. + // This may happen when previewing a private file which was selected but not + // explicitly uploaded, and then previewed. + if ($fid) { + $_SESSION['webform_files'][$fid] = $fid; } + return $fid; } /** @@ -369,10 +446,11 @@ function webform_file_allow_access($element) { /** * Implements _webform_display_component(). */ -function _webform_display_file($component, $value, $format = 'html') { +function _webform_display_file($component, $value, $format = 'html', $submission = array()) { $fid = isset($value[0]) ? $value[0] : NULL; return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#value' => $fid ? webform_get_file($fid) : NULL, '#weight' => $component['weight'], '#theme' => 'webform_display_file', @@ -383,7 +461,7 @@ function _webform_display_file($component, $value, $format = 'html') { } /** - * Format the output of text data for this component + * Format the output of text data for this component. */ function theme_webform_display_file($variables) { $element = $variables['element']; @@ -409,22 +487,25 @@ function _webform_delete_file($component, $value) { */ function _webform_attachments_file($component, $value) { $file = (array) webform_get_file($value[0]); - //This is necessary until the next release of mimemail is out, see [#1388786] - $file['filepath'] = $file['uri']; $files = array($file); return $files; } + /** * Implements _webform_analysis_component(). */ -function _webform_analysis_file($component, $sids = array()) { +function _webform_analysis_file($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $nonblanks = 0; @@ -443,8 +524,11 @@ function _webform_analysis_file($component, $sids = array()) { $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks)); $rows[1] = array(t('User uploaded file'), $nonblanks); - $rows[2] = array(t('Average uploaded file size'), ($sizetotal != 0 ? (int) (($sizetotal/$nonblanks)/1024) . ' KB' : '0')); - return $rows; + $other[0] = array(t('Average uploaded file size'), ($sizetotal != 0 ? (int) (($sizetotal / $nonblanks) / 1024) . ' KB' : '0')); + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -455,7 +539,7 @@ function _webform_table_file($component, $value) { $file = webform_get_file($value[0]); if (!empty($file->fid)) { $output = '' . check_plain(webform_file_name($file->uri)) . ''; - $output .= ' (' . (int) ($file->filesize/1024) . ' KB)'; + $output .= ' (' . (int) ($file->filesize / 1024) . ' KB)'; } return $output; } @@ -467,7 +551,7 @@ function _webform_csv_headers_file($component, $export_options) { $header = array(); // Two columns in header. $header[0] = array('', ''); - $header[1] = array($component['name'], ''); + $header[1] = array($export_options['header_keys'] ? $component['form_key'] : $component['name'], ''); $header[2] = array(t('Name'), t('Filesize (KB)')); return $header; } @@ -477,7 +561,7 @@ function _webform_csv_headers_file($component, $export_options) { */ function _webform_csv_data_file($component, $export_options, $value) { $file = webform_get_file($value[0]); - return empty($file->filename) ? array('', '') : array(webform_file_url($file->uri), (int) ($file->filesize/1024)); + return empty($file->filename) ? array('', '') : array(webform_file_url($file->uri), (int) ($file->filesize / 1024)); } /** @@ -530,3 +614,59 @@ function webform_file_usage_adjust($submission) { } } +/** + * Rename any files which are eligible for renaming. + * + * Renames if this submission is being submitted for the first time. + */ +function webform_file_rename($node, $submission) { + if (isset($submission->file_usage)) { + foreach ($submission->file_usage['renameable'] as $cid => $fids) { + foreach ($fids as $fid) { + webform_file_process_rename($node, $submission, $node->webform['components'][$cid], $fid); + } + } + } +} + +/** + * Renames the uploaded file name using tokens. + * + * @param object $node + * The webform node object. + * @param object $submission + * The webform submission object. + * @param array $component + * Component settings array for which fid is going to be processed. + * @param int $fid + * A file id to be processed. + */ +function webform_file_process_rename($node, $submission, $component, $fid) { + $file = webform_get_file($fid); + + if ($file) { + // Get the destination uri. + $destination_dir = $component['extra']['scheme'] . '://webform/' . drupal_strtolower(webform_replace_tokens($component['extra']['directory'], $node)); + $destination_dir = file_stream_wrapper_uri_normalize($destination_dir); + + // Get the file extension. + $info = pathinfo($file->uri); + $extension = $info['extension']; + + // Prepare new file name without extension. + $new_file_name = webform_replace_tokens($component['extra']['rename'], $node, $submission, NULL, TRUE); + $new_file_name = trim($new_file_name); + $new_file_name = _webform_transliterate($new_file_name); + $new_file_name = str_replace('/', '_', $new_file_name); + $new_file_name = preg_replace('/[^a-zA-Z0-9_\- ]/', '', $new_file_name); + if (strlen($new_file_name)) { + // Prepare the new uri with new filename. + $destination = "$destination_dir/$new_file_name.$extension"; + + // Compare the uri and Rename the file name. + if ($file->uri != $destination) { + file_move($file, $destination, FILE_EXISTS_RENAME); + } + } + } +} diff --git a/sites/all/modules/contrib/form/webform/components/grid.inc b/sites/all/modules/contrib/form/webform/components/grid.inc index b374a207..bffa2366 100644 --- a/sites/all/modules/contrib/form/webform/components/grid.inc +++ b/sites/all/modules/contrib/form/webform/components/grid.inc @@ -15,24 +15,28 @@ function _webform_defaults_grid() { return array( 'name' => '', 'form_key' => NULL, - 'mandatory' => 0, + 'required' => 0, 'pid' => 0, 'weight' => 0, + 'value' => '', 'extra' => array( 'options' => '', 'questions' => '', 'optrand' => 0, 'qrand' => 0, + 'unique' => 0, 'title_display' => 0, 'custom_option_keys' => 0, 'custom_question_keys' => 0, + 'sticky' => TRUE, 'description' => '', + 'description_above' => FALSE, 'private' => FALSE, + 'analysis' => TRUE, ), ); } - /** * Implements _webform_theme_component(). */ @@ -55,34 +59,58 @@ function _webform_theme_grid() { function _webform_edit_grid($component) { $form = array(); + $form['help'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => !empty($component['cid']), + '#title' => t('About options and questions…'), + '#description' => t('Options and questions may be configured here, in additional nested Select Options components, or even both.'), + '#weight' => -4, + 'pros_and_cons' => array( + '#theme' => 'table', + '#header' => array('', t('Options and questions configured here'), t('Configured in additional nested components'), t('Both')), + '#rows' => array( + array(t('Questions'), t('Enter the questions below.'), t('Configure and save this grid, then add additional Select Options components nested (indented) below this grid.'), t('Additional questions from nested components will be displayed below any questions configured here.')), + array(t('Options'), t('Enter options below.'), t('May be different for each question. Initially the same as defined below.'), t('Options from additional nested components will be merged with any options configured here.')), + array(t('Checkboxes'), t('No. Radio buttons only.'), t('Yes. Some or all questions may be multiple choice with check boxes.'), ''), + array(t('Default'), t('Yes. Must be same for all questions.'), t('Yes. May all be the same or different.'), ''), + array(t('Pre-built option lists'), t('No.'), t('Yes.'), ''), + array(t('Required'), t('Yes. Must be same for all questions.'), t('Yes. May all be the same or different.'), ''), + array(t('Question conditionals'), t('No.'), t('Yes. Individual questions may be used in conditional rules and/or actions.'), t('The whole grid may be conditionally shown or required.')), + array(t('Other types of nested components'), t('No.'), t('Yes. Other component types may also be included in the grid. They will be displayed where the options would normally be.'), ''), + ), + ), + ); + if (module_exists('options_element')) { $form['options'] = array( '#type' => 'fieldset', '#title' => t('Options'), '#collapsible' => TRUE, - '#description' => t('Options to select across the top. Usually these are ratings such as "poor" through "excellent" or "strongly disagree" through "strongly agree".'), '#attributes' => array('class' => array('webform-options-element')), '#element_validate' => array('_webform_edit_validate_options'), + '#weight' => -3, ); $form['options']['options'] = array( '#type' => 'options', '#options' => _webform_select_options_from_text($component['extra']['options'], TRUE), - '#optgroups' => FALSE, - '#default_value' => FALSE, - '#default_value_allowed' => FALSE, + '#default_value' => $component['value'], + '#default_value_allowed' => TRUE, '#optgroups' => FALSE, '#key_type' => 'mixed', '#key_type_toggle' => t('Customize option keys (Advanced)'), '#key_type_toggled' => $component['extra']['custom_option_keys'], + '#default_value_pattern' => '^%.+\[.+\]$', + '#description' => t('Options to select across the top, such as "Poor" through "Excellent". Indicate the default to the left of the desired item. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme('webform_token_help'), ); $form['questions'] = array( '#type' => 'fieldset', '#title' => t('Questions'), '#collapsible' => TRUE, - '#description' => t('Questions list down the side of the grid.'), '#attributes' => array('class' => array('webform-options-element')), '#element_validate' => array('_webform_edit_validate_options'), + '#weight' => -2, ); $form['questions']['options'] = array( '#type' => 'options', @@ -90,10 +118,10 @@ function _webform_edit_grid($component) { '#optgroups' => FALSE, '#default_value' => FALSE, '#default_value_allowed' => FALSE, - '#optgroups' => FALSE, '#key_type' => 'mixed', '#key_type_toggle' => t('Customize question keys (Advanced)'), '#key_type_toggled' => $component['extra']['custom_question_keys'], + '#description' => t('Questions list down the side of the grid. For a heading column on the right, append "|" and the right-side heading. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme('webform_token_help'), ); } else { @@ -101,11 +129,11 @@ function _webform_edit_grid($component) { '#type' => 'textarea', '#title' => t('Options'), '#default_value' => $component['extra']['options'], - '#description' => t('Options to select across the top. One option per line. Key-value pairs MUST be specified as "safe_key|Some readable option". Use of only alphanumeric characters and underscores is recommended in keys.') . theme('webform_token_help'), + '#description' => t('Options to select across the top, such as "Poor" through "Excellent" or "Stronly Disagree" through "Strongly Agree".') . + '

    ' . t('One key-value option per line. Key-value pairs MUST be specified as "safe_key|Some readable option". Use of only alphanumeric characters and underscores is recommended in keys.') . '

    ' . theme('webform_token_help'), '#cols' => 60, '#rows' => 5, '#weight' => -3, - '#required' => TRUE, '#wysiwyg' => FALSE, '#element_validate' => array('_webform_edit_validate_select'), ); @@ -113,14 +141,23 @@ function _webform_edit_grid($component) { '#type' => 'textarea', '#title' => t('Questions'), '#default_value' => $component['extra']['questions'], - '#description' => t('Questions list down the side of the grid. One question per line. Key-value pairs MUST be specified as "safe_key|Some readable option". Use of only alphanumeric characters and underscores is recommended in keys.') . theme('webform_token_help'), + '#description' => t('Questions list down the side of the grid. One question per line. Key-value pairs MUST be specified as "safe_key|Some readable question". For a heading column on the right, append "|" and the right-side heading. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme('webform_token_help') . ' ' . + '

    ' . t('Or for more control over the appearance and configuration, create additional additional Select Options or other type components nested under this grid. They will operate as separate components, but be displayed within this grid.') . '

    ', '#cols' => 60, '#rows' => 5, '#weight' => -2, - '#required' => TRUE, '#wysiwyg' => FALSE, '#element_validate' => array('_webform_edit_validate_select'), ); + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Default value'), + '#default_value' => $component['value'], + '#description' => t('The default option of the grid identified by its key.') . ' ' . theme('webform_token_help'), + '#size' => 60, + '#maxlength' => 1024, + '#weight' => 1, + ); } $form['display']['optrand'] = array( @@ -128,43 +165,73 @@ function _webform_edit_grid($component) { '#title' => t('Randomize Options'), '#default_value' => $component['extra']['optrand'], '#description' => t('Randomizes the order of options on the top when they are displayed in the form.'), - '#parents' => array('extra', 'optrand') + '#parents' => array('extra', 'optrand'), ); $form['display']['qrand'] = array( '#type' => 'checkbox', '#title' => t('Randomize Questions'), '#default_value' => $component['extra']['qrand'], '#description' => t('Randomize the order of the questions on the side when they are displayed in the form.'), - '#parents' => array('extra', 'qrand') + '#parents' => array('extra', 'qrand'), ); + $form['display']['sticky'] = array( + '#type' => 'checkbox', + '#title' => t('Sticky table header'), + '#default_value' => $component['extra']['sticky'], + '#description' => t('Use a sticky (non-scrolling) table header.'), + '#parents' => array('extra', 'sticky'), + ); + + $form['validation']['unique'] = array( + '#type' => 'checkbox', + '#title' => t('Unique'), + '#return_value' => 1, + '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'), + '#weight' => 1, + '#default_value' => $component['extra']['unique'], + '#parents' => array('extra', 'unique'), + ); + return $form; } /** * Implements _webform_render_component(). */ -function _webform_render_grid($component, $value = NULL, $filter = TRUE) { +function _webform_render_grid($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; + $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $options = _webform_select_options_from_text($component['extra']['options'], TRUE); + if ($filter) { + $questions = _webform_select_replace_tokens($questions, $node); + $options = _webform_select_replace_tokens($options, $node); + } + $element = array( '#type' => 'webform_grid', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#required' => $component['mandatory'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], - '#grid_questions' => _webform_select_options_from_text($component['extra']['questions'], TRUE), - '#grid_options' => _webform_select_options_from_text($component['extra']['options'], TRUE), + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#grid_questions' => $questions, + '#grid_options' => $options, + '#default_value' => isset($value) || !strlen($component['value']) ? $value : array_fill_keys(array_keys($questions), $component['value']), + '#grid_default' => $component['value'], '#optrand' => $component['extra']['optrand'], '#qrand' => $component['extra']['qrand'], + '#sticky' => $component['extra']['sticky'], '#theme' => 'webform_grid', '#theme_wrappers' => array('webform_element'), '#process' => array('webform_expand_grid'), + '#element_validate' => array('webform_validate_grid'), '#translatable' => array('title', 'description', 'grid_options', 'grid_questions'), ); - if ($value) { - $element['#default_value'] = $value; + // Enforce uniqueness. + if ($component['extra']['unique']) { + $element['#element_validate'][] = '_webform_edit_grid_unique_validate'; } return $element; @@ -176,58 +243,160 @@ function _webform_render_grid($component, $value = NULL, $filter = TRUE) { function webform_expand_grid($element) { $options = $element['#grid_options']; $questions = $element['#grid_questions']; + $weights = array(); - if (!empty($element['#optrand'])) { - _webform_shuffle_options($options); - } - - if (!empty($element['#qrand'])) { - _webform_shuffle_options($questions); + // Process questions and options from nested components. + foreach (element_children($element) as $key) { + $question = $element[$key]; + // Both forms and grid displays have #webform_component. + if (isset($question['#webform_component']) && + $question['#webform_component']['type'] == 'select' && + !$question['#webform_component']['extra']['aslist'] && + !$question['#webform_component']['extra']['other_option']) { + $options = webform_grid_merge_options($options, $question['#options']); + $weights[$key] = $question['#weight']; + } } + // Add the internal grid questions. + $weight = -1000; + $value = isset($element['#default_value']) ? $element['#default_value'] : array(); foreach ($questions as $key => $question) { if ($question != '') { + $question_value = isset($value[$key]) && $value[$key] !== '' ? $value[$key] : NULL; $element[$key] = array( + '#grid_question' => TRUE, '#title' => $question, '#required' => $element['#required'], - '#options' => $options, + '#options' => $element['#grid_options'], '#type' => 'radios', + '#default_value' => $question_value, + '#value' => $question_value, '#process' => array('form_process_radios', 'webform_expand_select_ids'), // Webform handles validation manually. '#validated' => TRUE, '#webform_validated' => FALSE, '#translatable' => array('title'), + '#weight' => $weight, ); + + // Add HTML5 required attribute, if needed. + if ($element['#required']) { + $element[$key]['#attributes']['required'] = 'required'; + } + + $weights[$key] = $weight; + $weight++; } } - $value = isset($element['#default_value']) ? $element['#default_value'] : array(); - foreach (element_children($element) as $key) { - if (isset($value[$key])) { - $element[$key]['#default_value'] = ($value[$key] !== '') ? $value[$key] : NULL; - } - else { - $element[$key]['#default_value'] = NULL; - } + if (!empty($element['#optrand'])) { + _webform_shuffle_options($options); + } + $element['#grid_options'] = $options; + + asort($weights); + if (!empty($element['#qrand'])) { + _webform_shuffle_options($weights); + } + if ($weights) { + $weight = min($weights); + } + foreach ($weights as $key => $old_weight) { + $element[$key]['#options'] = webform_grid_remove_options($options, $element[$key]['#options']); + $element[$key]['#weight'] = $weight++; + $element['#grid_questions'][$key] = $element[$key]['#title']; } return $element; } +/** + * Helper. Merge select component options in order. + * + * @param array $existing + * An array of existing values into which any values from $new that aren't in + * $existing are inserted. + * @param array $new + * Values to be inserted into $existing. + * + * @return array + * The merged array. + */ +function webform_grid_merge_options(array $existing, array $new) { + $insert = NULL; + $queue = array(); + foreach ($new as $key => $value) { + if (isset($existing[$key])) { + // Insert the queue before the found item. + $insert = array_search($key, array_keys($existing)); + if ($queue) { + $existing = array_slice($existing, 0, $insert, TRUE) + + $queue + + array_slice($existing, $insert, NULL, TRUE); + $insert += count($queue); + $queue = array(); + } + $insert++; + } + elseif (is_null($insert)) { + // It is not yet clear yet where to put this item. Add it to the queue. + $queue[$key] = $value; + } + else { + // PHP array_splice does not preserved the keys of the inserted array, + // but array_slice does (if the preserve keys parameter is TRUE). + $existing = array_slice($existing, 0, $insert, TRUE) + + array($key => $value) + + array_slice($existing, $insert, NULL, TRUE); + $insert++; + } + } + // Append any left over queued items. + $existing += $queue; + return $existing; +} + +/** + * Helper. Replace missing options with empty values. + * + * @param array $header + * An array of options to be used at the grid table header. + * @param array $row_options + * An array of options to be used for this row. + * + * @return array + * The $row_options with any missing options replaced with empty values. + */ +function webform_grid_remove_options(array $header, array $row_options) { + foreach ($header as $key => $value) { + if (!isset($row_options[$key])) { + $header[$key] = ''; + } + } + return $header; +} + /** * Implements _webform_display_component(). */ -function _webform_display_grid($component, $value, $format = 'html') { +function _webform_display_grid($component, $value, $format = 'html', $submission = array()) { + $node = node_load($component['nid']); $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $questions = _webform_select_replace_tokens($questions, $node); $options = _webform_select_options_from_text($component['extra']['options'], TRUE); + $options = _webform_select_replace_tokens($options, $node); $element = array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#format' => $format, '#grid_questions' => $questions, '#grid_options' => $options, + '#default_value' => $value, + '#sticky' => $component['extra']['sticky'], '#theme' => 'webform_display_grid', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), '#sorted' => TRUE, @@ -247,42 +416,88 @@ function _webform_display_grid($component, $value, $format = 'html') { return $element; } +/** + * Preprocess function for displaying a grid component. + */ +function template_preprocess_webform_display_grid(&$variables) { + $element =& $variables['element']; + // Expand the grid, suppressing randomization. This builds the grid + // questions and options. + $element['#qrand'] = FALSE; + $element['#optrand'] = FALSE; + $element['#required'] = FALSE; + $element = webform_expand_grid($element); +} + /** * Format the text output for this component. */ function theme_webform_display_grid($variables) { $element = $variables['element']; - - $component = $element['#webform_component']; $format = $element['#format']; if ($format == 'html') { + $right_titles = _webform_grid_right_titles($element); $rows = array(); - $header = array(array('data' => '', 'class' => array('webform-grid-question'))); - foreach ($element['#grid_options'] as $option) { - $header[] = array('data' => _webform_filter_xss($option), 'class' => array('checkbox', 'webform-grid-option')); - } - foreach ($element['#grid_questions'] as $question_key => $question) { + + // Set the header for the table. + $header = _webform_grid_header($element, $right_titles); + + foreach (element_children($element) as $question_key) { + $question_element = $element[$question_key]; $row = array(); - $row[] = array('data' => _webform_filter_xss($question), 'class' => array('webform-grid-question')); - foreach ($element['#grid_options'] as $option_value => $option_label) { - if (strcmp($element[$question_key]['#value'], $option_value) == 0) { - $row[] = array('data' => 'X', 'class' => array('checkbox', 'webform-grid-option')); - } - else { - $row[] = array('data' => ' ', 'class' => array('checkbox', 'webform-grid-option')); + $questions = explode('|', $question_element['#title'], 2); + $values = $question_element['#value']; + $values = is_array($values) ? $values : array($values); + $row[] = array('data' => webform_filter_xss($questions[0]), 'class' => array('webform-grid-question')); + if (isset($element['#grid_questions'][$question_key])) { + foreach ($element['#grid_options'] as $option_value => $option_label) { + if (in_array($option_value, $values)) { + $row[] = array('data' => 'X', 'class' => array('checkbox', 'webform-grid-option')); + } + else { + $row[] = array('data' => ' ', 'class' => array('checkbox', 'webform-grid-option')); + } } } + else { + $question_element['#title_display'] = 'none'; + $row[] = array( + 'data' => drupal_render($question_element), + 'colspan' => count($element['#grid_options']), + ); + } + if ($right_titles) { + $row[] = array('data' => isset($questions[1]) ? webform_filter_xss($questions[1]) : '', 'class' => array('webform-grid-question')); + } $rows[] = $row; } $option_count = count($header) - 1; - $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('webform-grid', 'webform-grid-' . $option_count)))); + $output = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => $element['#sticky'], 'attributes' => array('class' => array('webform-grid', 'webform-grid-' . $option_count)))); } else { $items = array(); - foreach (element_children($element) as $key) { - $items[] = ' - ' . $element[$key]['#title'] . ': ' . (isset($element['#grid_options'][$element[$key]['#value']]) ? $element['#grid_options'][$element[$key]['#value']] : ''); + foreach (element_children($element) as $question_key) { + $question_element = $element[$question_key]; + if (isset($element['#grid_questions'][$question_key])) { + $values = $question_element['#value']; + $values = is_array($values) ? $values : array($values); + foreach ($values as $value_key => $value) { + if (isset($element['#grid_options'][$value])) { + $values[$value_key] = $element['#grid_options'][$value]; + } + else { + unset($values[$value_key]); + } + } + $value = implode(', ', $values); + } + else { + $element[$question_key]['#title'] = ''; + $value = drupal_render($element[$question_key]); + } + $items[] = ' - ' . _webform_grid_question_header($question_element['#title']) . ': ' . $value; } $output = implode("\n", $items); } @@ -293,23 +508,30 @@ function theme_webform_display_grid($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_grid($component, $sids = array()) { +function _webform_analysis_grid($component, $sids = array(), $single = FALSE, $join = NULL) { // Generate the list of options and questions. - $options = _webform_select_options_from_text($component['extra']['options'], TRUE); + $node = node_load($component['nid']); $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $questions = _webform_select_replace_tokens($questions, $node); + $options = _webform_select_options_from_text($component['extra']['options'], TRUE); + $options = _webform_select_replace_tokens($options, $node); // Generate a lookup table of results. $query = db_select('webform_submitted_data', 'wsd') ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']) - ->condition('data', '', '<>') - ->groupBy('no') - ->groupBy('data'); - $query->addExpression('COUNT(sid)', 'datacount'); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']) + ->condition('wsd.data', '', '<>') + ->groupBy('wsd.no') + ->groupBy('wsd.data'); + $query->addExpression('COUNT(wsd.sid)', 'datacount'); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $result = $query->execute(); @@ -324,49 +546,62 @@ function _webform_analysis_grid($component, $sids = array()) { // Add options as a header row. foreach ($options as $option) { - $header[] = _webform_filter_xss($option); + $header[] = webform_filter_xss($option); } // Add questions as each row. foreach ($questions as $qkey => $question) { - $row = array(_webform_filter_xss($question)); + $row = array(webform_filter_xss($question)); foreach ($options as $okey => $option) { $row[] = !empty($counts[$qkey][$okey]) ? $counts[$qkey][$okey] : 0; } $rows[] = $row; } - $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('webform-grid')))); - - return array(array(array('data' => $output, 'colspan' => 2))); + // Return return the table unless there are no internal questions in the grid. + if ($rows) { + return array( + 'table_header' => $header, + 'table_rows' => $rows, + ); + } } /** * Implements _webform_table_component(). */ function _webform_table_grid($component, $value) { + $node = node_load($component['nid']); $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $questions = _webform_select_replace_tokens($questions, $node); $options = _webform_select_options_from_text($component['extra']['options'], TRUE); + $options = _webform_select_replace_tokens($options, $node); $output = ''; // Set the value as a single string. foreach ($questions as $key => $label) { if (isset($value[$key]) && isset($options[$value[$key]])) { - $output .= _webform_filter_xss($label) . ': ' . _webform_filter_xss($options[$value[$key]]) . '
    '; + $output .= webform_filter_xss(_webform_grid_question_header($label)) . ': ' . webform_filter_xss($options[$value[$key]]) . '
    '; } } - return $output; + // Return output if the grid contains internal questions. + if (count($questions)) { + return $output; + } } /** * Implements _webform_csv_headers_component(). */ function _webform_csv_headers_grid($component, $export_options) { + $node = node_load($component['nid']); + $items = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $items = _webform_select_replace_tokens($items, $node); + $header = array(); $header[0] = array(''); - $header[1] = array($component['name']); - $items = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $header[1] = array($export_options['header_keys'] ? $component['form_key'] : $component['name']); $count = 0; foreach ($items as $key => $item) { // Empty column per sub-field in main header. @@ -375,7 +610,7 @@ function _webform_csv_headers_grid($component, $export_options) { $header[1][] = ''; } // The value for this option. - $header[2][] = $item; + $header[2][] = $export_options['header_keys'] ? $key : _webform_grid_question_header($item); $count++; } @@ -386,8 +621,12 @@ function _webform_csv_headers_grid($component, $export_options) { * Implements _webform_csv_data_component(). */ function _webform_csv_data_grid($component, $export_options, $value) { + $node = node_load($component['nid']); $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE); + $questions = _webform_select_replace_tokens($questions, $node); $options = _webform_select_options_from_text($component['extra']['options'], TRUE); + $options = _webform_select_replace_tokens($options, $node); + $return = array(); foreach ($questions as $key => $question) { if (isset($value[$key]) && isset($options[$value[$key]])) { @@ -400,32 +639,216 @@ function _webform_csv_data_grid($component, $export_options, $value) { return $return; } +/** + * A Form API element validate function to check that all choices are unique. + */ +function _webform_edit_grid_unique_validate($element) { + // Grids may contain nested multiple value select components. + // Create a flat array of values. + $values = array(); + $element['#value'] = (array) $element['#value']; + array_walk_recursive($element['#value'], function ($a) use (&$values) { + $values[] = $a; + }); + + $nr_unique = count(array_unique($values)); + $nr_values = count($values); + $nr_possible = count($element['#grid_options']); + if (strlen($element['#grid_default']) && isset($element['#grid_options'][$element['#grid_default']])) { + // A default is defined and is one of the options. Don't count default values + // toward uniqueness. + $nr_defaults = count(array_keys($element['#value'], $element['#grid_default'])); + if ($nr_defaults) { + $nr_values -= $nr_defaults; + $nr_unique--; + } + } + if ($nr_unique < $nr_values && $nr_unique < $nr_possible) { + // Fewer unique values than values means that at least one value is duplicated. + // Fewer unique values than possible values means that there is a possible choice + // that wasn't used. + form_error($element, t('!title is not allowed to have the same answer for more than one question.', array('!title' => $element['#title']))); + } +} + +/** + * Theme function to render a grid component. + */ function theme_webform_grid($variables) { $element = $variables['element']; + $right_titles = _webform_grid_right_titles($element); $rows = array(); - $header = array(array('data' => '', 'class' => array('webform-grid-question'))); + // Set the header for the table. - foreach ($element['#grid_options'] as $option) { - $header[] = array('data' => _webform_filter_xss($option), 'class' => array('checkbox', 'webform-grid-option')); - } + $header = _webform_grid_header($element, $right_titles); foreach (element_children($element) as $key) { $question_element = $element[$key]; + $title_element =& $question_element; + if ($question_element['#type'] == 'select_or_other') { + $title_element =& $question_element['select']; + } + $question_titles = explode('|', $title_element['#title'], 2); // Create a row with the question title. - $row = array(array('data' => _webform_filter_xss($question_element['#title']), 'class' => array('webform-grid-question'))); + $required = !empty($question_element['#required']) ? theme('form_required_marker', array('element' => $question_element)) : ''; + $row = array(array('data' => t('!title !required', array('!title' => webform_filter_xss($question_titles[0]), '!required' => $required)), 'class' => array('webform-grid-question'))); // Render each radio button in the row. - $radios = form_process_radios($question_element); - foreach (element_children($radios) as $key) { - $radios[$key]['#title'] = $question_element['#title'] . ' - ' . $radios[$key]['#title']; - $radios[$key]['#title_display'] = 'invisible'; - $row[] = array('data' => drupal_render($radios[$key]), 'class' => array('checkbox', 'webform-grid-option')); + if ($question_element['#type'] == 'radios' || $question_element['#type'] == 'checkboxes') { + $radios = form_process_radios($question_element); + foreach (element_children($radios) as $key) { + $radio_title = $radios[$key]['#title']; + if (!strlen($radio_title)) { + $row[] = ' '; + } + else { + $radios[$key]['#title'] = $question_element['#title'] . ' - ' . $radio_title; + $radios[$key]['#title_display'] = 'invisible'; + $row[] = array('data' => drupal_render($radios[$key]), 'class' => array('checkbox', 'webform-grid-option'), 'data-label' => array($radio_title)); + } + } } - $rows[] = $row; + else { + $title_element['#title_display'] = 'none'; + $row[] = array( + 'data' => drupal_render($question_element), + 'colspan' => count($element['#grid_options']), + ); + } + if ($right_titles) { + $row[] = array('data' => isset($question_titles[1]) ? webform_filter_xss($question_titles[1]) : '', 'class' => array('webform-grid-question')); + } + + // Convert the parents array into a string, excluding the "submitted" wrapper. + $nested_level = $question_element['#parents'][0] == 'submitted' ? 1 : 0; + $parents = str_replace('_', '-', implode('--', array_slice($question_element['#parents'], $nested_level))); + + $rows[] = array( + 'data' => $row, + 'class' => empty($question_element['#grid_question']) + ? array( + 'webform-component', + 'webform-component-' . str_replace('_', '-', $question_element['#type']), + 'webform-component--' . $parents, + ) + : array(), + ); } $option_count = count($header) - 1; - return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('webform-grid', 'webform-grid-' . $option_count)))); + return theme('table', array( + 'header' => $header, + 'rows' => $rows, + 'sticky' => $element['#sticky'], + 'attributes' => array( + 'class' => array( + 'webform-grid', + 'webform-grid-' . $option_count, + ), + ), + )); +} + +/** + * Generate a table header suitable for form or html display. + * + * @param array $element + * The element array. + * @param bool $right_titles + * If TRUE, display a right-side title column. + * + * @return array + * An array of headers. + */ +function _webform_grid_header(array $element, $right_titles) { + $titles = explode('|', $element['#title'], 2); + $header = array( + array( + 'data' => _webform_grid_header_title($element, $titles[0]), + 'class' => array('webform-grid-question'), + ), + ); + foreach ($element['#grid_options'] as $option) { + $header[] = array( + 'data' => webform_filter_xss($option), + 'class' => array('checkbox', 'webform-grid-option'), + 'scope' => 'col', + ); + } + if ($right_titles) { + $header[] = array( + 'data' => _webform_grid_header_title($element, isset($titles[1]) ? $titles[1] : ''), + 'class' => array('webform-grid-question'), + ); + } + return $header; +} + +/** + * Create internal component title for table header, if any. + */ +function _webform_grid_header_title($element, $title) { + $header_title = ''; + if ($element['#title_display'] == 'internal') { + $variables = array('element' => $element); + $variables['element']['#title_display'] = 'before'; + $variables['element']['#title'] = $title; + $header_title = theme('form_element_label', $variables); + } + return $header_title; +} + +/** + * Determine if a right-side title column has been specified. + */ +function _webform_grid_right_titles($element) { + if ($element['#title_display'] == 'internal' && substr_count($element['#title'], '|')) { + return TRUE; + } + foreach ($element['#grid_questions'] as $question_key => $question) { + if (substr_count($question, '|')) { + return TRUE; + } + } + return FALSE; +} + +/** + * Create a question header for left, right or left/right question headers. + */ +function _webform_grid_question_header($text) { + return implode('/', array_filter(explode('|', $text))); +} + +/** + * Element validation for Webform grid fields. + * + * Requires a component implementation because the default required validation + * passes when at least one value is supplied, rather than every value. This + * makes the server validation match the browser validation. + */ +function webform_validate_grid($element, $form_state) { + if ($element['#required']) { + $values = $form_state['input']; + foreach ($element['#parents'] as $key) { + $values = isset($values[$key]) ? $values[$key] : $values; + } + // Remove any values that aren't grid question (i.e. nested components). + $grid_questions = $element['#grid_questions']; + $values = array_intersect_key($values, $grid_questions); + // Remove any unanswered grid questions. + $answers = array_filter($values, function ($item) { + return !is_null($item); + }); + // Give required errors for any questions that aren't answered. + foreach (array_diff_key($grid_questions, $answers) as $question_key => $question) { + // If the question is still required (e.g not modified by an after_build + // function), give the required error. + if (!empty($element[$question_key]['#required'])) { + form_error($element[$question_key], t('!question field within !name is required.', array('!question' => $question, '!name' => $element['#title']))); + } + } + } } diff --git a/sites/all/modules/contrib/form/webform/components/hidden.inc b/sites/all/modules/contrib/form/webform/components/hidden.inc index e173c29c..ca6618e0 100644 --- a/sites/all/modules/contrib/form/webform/components/hidden.inc +++ b/sites/all/modules/contrib/form/webform/components/hidden.inc @@ -18,6 +18,7 @@ function _webform_defaults_hidden() { 'extra' => array( 'private' => FALSE, 'hidden_type' => 'value', + 'analysis' => FALSE, ), ); } @@ -43,7 +44,7 @@ function _webform_edit_hidden($component) { '#type' => 'textarea', '#title' => t('Default value'), '#default_value' => $component['value'], - '#description' => t('The default value of the field.') . theme('webform_token_help'), + '#description' => t('The default value of the field.') . ' ' . theme('webform_token_help'), '#cols' => 60, '#rows' => 5, '#weight' => 0, @@ -67,20 +68,16 @@ function _webform_edit_hidden($component) { /** * Implements _webform_render_component(). */ -function _webform_render_hidden($component, $value = NULL, $filter = TRUE) { +function _webform_render_hidden($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; - // Set filtering options for "value" types, which are not displayed to the - // end user so they do not need to be sanitized. - $strict = $component['extra']['hidden_type'] != 'value'; - $allow_anonymous = $component['extra']['hidden_type'] == 'value'; - $default_value = $filter ? _webform_filter_values($component['value'], $node, NULL, NULL, $strict, $allow_anonymous) : $component['value']; + $default_value = $filter ? webform_replace_tokens($component['value'], $node) : $component['value']; if (isset($value[0])) { $default_value = $value[0]; } $element = array( - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? NULL : $component['name'], '#weight' => $component['weight'], '#translatable' => array('title'), ); @@ -92,6 +89,12 @@ function _webform_render_hidden($component, $value = NULL, $filter = TRUE) { else { $element['#type'] = 'hidden'; $element['#default_value'] = $default_value; + + // Same-page conditionals depend on the wrapper around elements for getting + // values. Wrap, but hide, the wrapper around hidden elements. + $element['#theme_wrappers'] = array('webform_element'); + $element['#wrapper_attributes']['class'] = array(); + $element['#wrapper_attributes']['style'] = array('display: none'); } return $element; @@ -100,7 +103,7 @@ function _webform_render_hidden($component, $value = NULL, $filter = TRUE) { /** * Implements _webform_display_component(). */ -function _webform_display_hidden($component, $value, $format = 'html') { +function _webform_display_hidden($component, $value, $format = 'html', $submission = array()) { $element = array( '#title' => $component['name'], '#markup' => isset($value[0]) ? $value[0] : NULL, @@ -114,6 +117,9 @@ function _webform_display_hidden($component, $value, $format = 'html') { return $element; } +/** + * Theme callback. + */ function theme_webform_display_hidden($variables) { $element = $variables['element']; @@ -123,14 +129,18 @@ function theme_webform_display_hidden($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_hidden($component, $sids = array()) { +function _webform_analysis_hidden($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $nonblanks = 0; @@ -146,11 +156,17 @@ function _webform_analysis_hidden($component, $sids = array()) { $submissions++; } - $rows[0] = array( t('Empty'), ($submissions - $nonblanks)); - $rows[1] = array( t('Non-empty'), $nonblanks); - $rows[2] = array( t('Average submission length in words (ex blanks)'), - ($nonblanks !=0 ? number_format($wordcount/$nonblanks, 2) : '0')); - return $rows; + $rows[0] = array(t('Empty'), ($submissions - $nonblanks)); + $rows[1] = array(t('Non-empty'), $nonblanks); + $other[0] = array( + t('Average submission length in words (ex blanks)'), + $nonblanks != 0 ? number_format($wordcount / $nonblanks, 2) : '0', + ); + + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -161,13 +177,21 @@ function _webform_table_hidden($component, $value) { } /** - * Implements _webform_csv_data_component(). + * Implements _webform_action_set_component(). + */ +function _webform_action_set_hidden($component, &$element, &$form_state, $value) { + $element['#value'] = $value; + form_set_value($element, $value, $form_state); +} + +/** + * Implements _webform_csv_headers_component(). */ function _webform_csv_headers_hidden($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } diff --git a/sites/all/modules/contrib/form/webform/components/markup.inc b/sites/all/modules/contrib/form/webform/components/markup.inc index 100a8602..2460e714 100644 --- a/sites/all/modules/contrib/form/webform/components/markup.inc +++ b/sites/all/modules/contrib/form/webform/components/markup.inc @@ -18,6 +18,19 @@ function _webform_defaults_markup() { 'extra' => array( 'format' => NULL, 'private' => FALSE, + 'display_on' => 'form', + ), + ); +} + +/** + * Implements _webform_theme_component(). + */ +function _webform_theme_markup() { + return array( + 'webform_display_markup' => array( + 'render element' => 'element', + 'file' => 'components/markup.inc', ), ); } @@ -31,12 +44,25 @@ function _webform_edit_markup($component) { '#type' => 'text_format', '#title' => t('Value'), '#default_value' => $component['value'], - '#description' => t('Markup allows you to enter custom HTML or PHP logic into your form.') . theme('webform_token_help'), + '#description' => t('Markup allows you to enter custom HTML into your form.') . ' ' . theme('webform_token_help', array('groups' => array('node', 'submission'))), '#weight' => -1, '#format' => $component['extra']['format'], '#element_validate' => array('_webform_edit_markup_validate'), ); + $form['display']['display_on'] = array( + '#type' => 'select', + '#title' => t('Display on'), + '#default_value' => $component['extra']['display_on'], + '#options' => array( + 'form' => t('form only'), + 'display' => t('viewed submission only'), + 'both' => t('both form and viewed submission'), + ), + '#weight' => 1, + '#parents' => array('extra', 'display_on'), + ); + return $form; } @@ -53,25 +79,98 @@ function _webform_edit_markup_validate($form, &$form_state) { /** * Implements _webform_render_component(). */ -function _webform_render_markup($component, $value = NULL, $filter = TRUE) { - $node = isset($component['nid']) ? node_load($component['nid']) : NULL; +function _webform_render_markup($component, $value = NULL, $filter = TRUE, $submission = NULL) { $element = array( '#type' => 'markup', '#title' => $filter ? NULL : $component['name'], '#weight' => $component['weight'], - '#markup' => $filter ? _webform_filter_values(check_markup($component['value'], $component['extra']['format'], '', TRUE), $node, NULL, NULL, FALSE) : $component['value'], + '#markup' => $component['value'], '#format' => $component['extra']['format'], '#theme_wrappers' => array('webform_element'), '#translatable' => array('title', 'markup'), + '#access' => $component['extra']['display_on'] != 'display', + '#webform_nid' => isset($component['nid']) ? $component['nid'] : NULL, + '#webform_submission' => $submission, + '#webform_format' => $component['extra']['format'], ); + if ($filter) { + $element['#after_build'] = array('_webform_render_markup_after_build'); + } + return $element; } +/** + * Helper function to replace tokens in markup component. + * + * Required to have access to current form values from $form_state. + */ +function _webform_render_markup_after_build($form_element, &$form_state) { + global $user; + $node = node_load($form_element['#webform_nid']); + $submission = NULL; + + // Convert existing submission data to an in-progress submission. + $form_state_for_submission = $form_state; + $form_state_for_submission['values']['submitted'] = $form_state['#conditional_values']; + module_load_include('inc', 'webform', 'includes/webform.submissions'); + $submission = webform_submission_create($node, $user, $form_state_for_submission, TRUE, $form_element['#webform_submission']); + + // Replace tokens using the current or generated submission. + $value = webform_replace_tokens($form_element['#markup'], $node, $submission, NULL, $form_element['#webform_format']); + + // If the markup value has been set by a conditional, display that value. + $component = $form_element['#webform_component']; + if ($node) { + $sorter = webform_get_conditional_sorter($node); + // If the form was retrieved from the form cache, the conditionals may not + // have been executed yet. + if (!$sorter->isExecuted()) { + $sorter->executeConditionals(isset($submission) ? $submission->data : array()); + } + $conditional_value = $sorter->componentMarkup($component['cid'], $component['page_num']); + if (isset($conditional_value)) { + // Provide original value, should conditional logic no longer set the + // value. + $form_element['#wrapper_attributes']['data-webform-markup'] = $value; + if (is_string($conditional_value)) { + $value = check_markup($conditional_value, $component['extra']['format']); + } + } + } + + $form_element['#markup'] = $value; + return $form_element; +} + /** * Implements _webform_display_component(). */ -function _webform_display_markup($component, $value, $format = 'html') { - return array(); +function _webform_display_markup($component, $value, $format = 'html', $submission = array()) { + $node = isset($component['nid']) ? node_load($component['nid']) : NULL; + $value = webform_replace_tokens($component['value'], $node, $submission, NULL, $component['extra']['format']); + + // If the markup value has been set by a conditional, display that value. + if ($node && is_string($conditional_value = webform_get_conditional_sorter($node)->componentMarkup($component['cid'], $component['page_num']))) { + $value = check_markup($conditional_value, $component['extra']['format']); + } + + return array( + '#weight' => $component['weight'], + '#theme' => 'webform_display_markup', + '#format' => $format, + '#value' => $value, + '#translatable' => array('title'), + '#access' => $component['extra']['display_on'] != 'form', + ); +} + +/** + * Format the output of data for this component. + */ +function theme_webform_display_markup($variables) { + $element = $variables['element']; + return $element['#access'] ? $element['#value'] : ''; } diff --git a/sites/all/modules/contrib/form/webform/components/number.inc b/sites/all/modules/contrib/form/webform/components/number.inc index 16152f11..c2bf76e7 100644 --- a/sites/all/modules/contrib/form/webform/components/number.inc +++ b/sites/all/modules/contrib/form/webform/components/number.inc @@ -15,7 +15,7 @@ function _webform_defaults_number() { 'pid' => 0, 'weight' => 0, 'value' => '', - 'mandatory' => 0, + 'required' => 0, 'extra' => array( 'type' => 'textfield', 'field_prefix' => '', @@ -24,8 +24,11 @@ function _webform_defaults_number() { 'unique' => 0, 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, + 'placeholder' => '', 'attributes' => array(), 'private' => FALSE, + 'analysis' => FALSE, 'min' => '', 'max' => '', 'step' => '', @@ -54,6 +57,16 @@ function _webform_theme_number() { ); } +/** + * Fix the view field(s) that are automatically generated for number components. + */ +function _webform_view_field_number($component, $fields) { + foreach ($fields as &$field) { + $field['webform_datatype'] = 'number'; + } + return $fields; +} + /** * Implements _webform_edit_component(). */ @@ -63,7 +76,7 @@ function _webform_edit_number($component) { '#type' => 'textfield', '#title' => t('Default value'), '#default_value' => $component['value'], - '#description' => t('The default value of the field.') . theme('webform_token_help'), + '#description' => t('The default value of the field.') . ' ' . theme('webform_token_help'), '#size' => 60, '#maxlength' => 1024, '#weight' => 0, @@ -80,6 +93,14 @@ function _webform_edit_number($component) { '#weight' => -1, '#parents' => array('extra', 'type'), ); + $form['display']['placeholder'] = array( + '#type' => 'textfield', + '#title' => t('Placeholder'), + '#default_value' => $component['extra']['placeholder'], + '#description' => t('The placeholder will be shown in the field until the user starts entering a value.'), + '#weight' => 1, + '#parents' => array('extra', 'placeholder'), + ); $form['display']['field_prefix'] = array( '#type' => 'textfield', '#title' => t('Prefix text placed to the left of the field'), @@ -104,7 +125,7 @@ function _webform_edit_number($component) { '#type' => 'checkbox', '#title' => t('Disabled'), '#return_value' => 1, - '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'), + '#description' => t('Make this field non-editable. Useful for displaying default value. Changeable via JavaScript or developer tools.'), '#weight' => 11, '#default_value' => $component['extra']['disabled'], '#parents' => array('extra', 'disabled'), @@ -158,7 +179,7 @@ function _webform_edit_number($component) { '#type' => 'checkbox', '#title' => t('Integer'), '#return_value' => 1, - '#description' => t('Permit only integer values as input. e.g. 12.34 would be invalid.'), + '#description' => t('Permit only integer values as input. For example, 12.34 would be invalid.'), '#weight' => 1.5, '#default_value' => $component['extra']['integer'], '#parents' => array('extra', 'integer'), @@ -167,7 +188,7 @@ function _webform_edit_number($component) { '#type' => 'textfield', '#title' => t('Minimum'), '#default_value' => $component['extra']['min'], - '#description' => t('Minimum numeric value. e.g. 0 would ensure positive numbers.'), + '#description' => t('Minimum numeric value. For example, 0 would ensure positive numbers.'), '#size' => 5, '#maxlength' => 10, '#weight' => 2.1, @@ -189,7 +210,7 @@ function _webform_edit_number($component) { '#type' => 'textfield', '#title' => t('Step'), '#default_value' => $component['extra']['step'], - '#description' => t('Limit options to a specific increment. e.g. a step of "5" would allow values 5, 10, 15, etc.'), + '#description' => t('Limit options to a specific increment. For example, a step of "5" would allow values 5, 10, 15, etc.'), '#size' => 5, '#maxlength' => 10, '#weight' => 3, @@ -225,11 +246,16 @@ function theme_webform_number($variables) { // This IF statement is mostly in place to allow our tests to set type="text" // because SimpleTest does not support type="number". if (!isset($element['#attributes']['type'])) { - $element['#attributes']['type'] = 'number'; + // HTML5 number fields are no long used pending better browser support. + // See issues #2290029, #2202905. + // @code + // $element['#attributes']['type'] = 'number'; + // @endcode + $element['#attributes']['type'] = 'text'; } // Step property *must* be a full number with 0 prefix if a decimal. - if (!empty($element['#step']) && !is_int($element['#step'] * 1)) { + if (!empty($element['#step']) && filter_var((float) $element['#step'], FILTER_VALIDATE_INT) === FALSE) { $decimals = strlen($element['#step']) - strrpos($element['#step'], '.') - 1; $element['#step'] = sprintf('%1.' . $decimals . 'F', $element['#step']); } @@ -257,18 +283,18 @@ function theme_webform_number($variables) { /** * Implements _webform_render_component(). */ -function _webform_render_number($component, $value = NULL, $filter = TRUE) { +function _webform_render_number($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#default_value' => $filter ? _webform_filter_values($component['value'], $node, NULL, NULL, FALSE) : $component['value'], - '#required' => $component['mandatory'], + '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? _webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']), - '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? _webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']), - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']), + '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']), + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#attributes' => $component['extra']['attributes'], '#element_validate' => array('_webform_validate_number'), '#theme_wrappers' => array('webform_element'), @@ -282,6 +308,14 @@ function _webform_render_number($component, $value = NULL, $filter = TRUE) { '#translatable' => array('title', 'description', 'field_prefix', 'field_suffix'), ); + if ($component['required']) { + $element['#attributes']['required'] = 'required'; + } + + if ($component['extra']['placeholder']) { + $element['#attributes']['placeholder'] = $component['extra']['placeholder']; + } + // Set the decimal count to zero for integers. if ($element['#integer'] && $element['#decimals'] === '') { $element['#decimals'] = 0; @@ -295,7 +329,7 @@ function _webform_render_number($component, $value = NULL, $filter = TRUE) { } // Ensure #step starts with a zero if a decimal. - if (!is_int($element['#step'] * 1)) { + if (filter_var((float) $element['#step'], FILTER_VALIDATE_INT) === FALSE) { $decimals = strlen($element['#step']) - strrpos($element['#step'], '.') - 1; $element['#step'] = sprintf('%1.' . $decimals . 'F', $element['#step']); } @@ -307,7 +341,7 @@ function _webform_render_number($component, $value = NULL, $filter = TRUE) { // Set the size property based on #max, to ensure consistent behavior for // browsers that do not support type = number. if ($element['#max']) { - $element['#size'] = strlen($element['#max']) + 1; + $element['#size'] = strlen($element['#max']) + 1; } } else { @@ -358,10 +392,11 @@ function _webform_render_number($component, $value = NULL, $filter = TRUE) { /** * Implements _webform_display_component(). */ -function _webform_display_number($component, $value, $format = 'html') { +function _webform_display_number($component, $value, $format = 'html', $submission = array()) { $empty = !isset($value[0]) || $value[0] === ''; return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#theme' => 'webform_display_number', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), @@ -387,16 +422,20 @@ function theme_webform_display_number($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_number($component, $sids = array(), $single = FALSE) { +function _webform_analysis_number($component, $sids = array(), $single = FALSE, $join = NULL) { $advanced_stats = $single; $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $population = array(); @@ -408,9 +447,9 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE) $result = $query->execute(); foreach ($result as $data) { $value = trim($data['data']); - $number = (float)$value; - $non_empty += (integer)($value !== ''); - $non_zero += (integer)($number != 0.0); + $number = (float) $value; + $non_empty += (integer) ($value !== ''); + $non_zero += (integer) ($number != 0.0); $sum += $number; $population[] = $number; $submissions++; @@ -439,20 +478,21 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE) $average = _webform_number_format($component, $average); $sum = _webform_number_format($component, $sum); - $rows[0] = array(t('Zero/blank'), ($submissions - $non_zero)); - $rows[1] = array(t('User entered value'), $non_empty); - $rows[2] = array(t('Sum') . ($advanced_stats ? ' (Σ)' : ''), $sum); - $rows[3] = array($average_title, $average); + $rows[] = array(t('Zero/blank'), ($submissions - $non_zero)); + $rows[] = array(t('User entered value'), $non_empty); + + $other[] = array(t('Sum') . ($advanced_stats ? ' (Σ)' : ''), $sum); + $other[] = array($average_title, $average); if (!$advanced_stats && $sum != 0) { - $rows[4] = array('', l(t('More stats »'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid'])); + $other[] = l(t('More stats »'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']); } // Normal distribution information. if ($advanced_stats && $population_count && $sum != 0) { // Standard deviation. $stddev = 0; - foreach($population as $value) { + foreach ($population as $value) { // Obtain the total of squared variances. $stddev += pow(($value - $average), 2); } @@ -465,7 +505,10 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE) // Skip the rest of the distribution rows if standard deviation is 0. if (empty($stddev)) { - return $rows; + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } // Build normal distribution table rows. @@ -494,21 +537,21 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE) $stddev = _webform_number_format($component, $stddev); $low = _webform_number_format($component, $population[0]); $high = _webform_number_format($component, end($population)); - foreach($limit as $key => $value) { + foreach ($limit as $key => $value) { $limit[$key] = _webform_number_format($component, $value); } - // Column headings (override potential theme uppercase, e.g. Seven in D7). + // Column headings (override potential theme uppercase, for example, Seven in D7). $header = array( t('Normal Distribution'), - array('data' => '-4' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '-3' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '-2' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '-1' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '+1' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '+2' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '+3' . $sigma, 'style' => 'text-transform: lowercase;',), - array('data' => '+4' . $sigma, 'style' => 'text-transform: lowercase;',), + array('data' => '-4' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '-3' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '-2' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '-1' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '+1' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '+2' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '+3' . $sigma, 'style' => 'text-transform: lowercase;'), + array('data' => '+4' . $sigma, 'style' => 'text-transform: lowercase;'), ); // Insert row labels. @@ -516,14 +559,17 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE) array_unshift($count, t('Count')); array_unshift($percent, t('% of !description', array('!description' => $description))); - $output = theme('table', array('header' => $header, 'rows' => array($limit, $count, $percent))); + $normal_distribution = theme('table', array('header' => $header, 'rows' => array($limit, $count, $percent), 'sticky' => FALSE)); - $rows[4] = array(t('Range'), t('!low to !high', array('!low' => $low, '!high' => $high))); - $rows[5] = array(t('Standard deviation (!sigma)', array('!sigma' => $sigma)), $stddev); - $rows[6] = array(array('data' => $output, 'colspan' => 2)); + $other[] = array(t('Range'), t('!low to !high', array('!low' => $low, '!high' => $high))); + $other[] = array(t('Standard deviation (!sigma)', array('!sigma' => $sigma)), $stddev); + $other[] = $normal_distribution; } - return $rows; + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -533,6 +579,14 @@ function _webform_table_number($component, $value) { return isset($value[0]) ? _webform_number_format($component, $value[0]) : ''; } +/** + * Implements _webform_action_set_component(). + */ +function _webform_action_set_number($component, &$element, &$form_state, $value) { + $element['#value'] = $value; + form_set_value($element, $value, $form_state); +} + /** * Implements _webform_csv_headers_component(). */ @@ -540,7 +594,7 @@ function _webform_csv_headers_number($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } @@ -555,15 +609,14 @@ function _webform_csv_data_number($component, $export_options, $value) { } /** - * A Drupal Form API Validation function. Validates the entered values from - * number components on the client-side form. + * A Drupal Form API Validation function. * - * @param $element + * Validates the entered values from number components on the client-side form. + * + * @param array $element * The form element. May either be a select or a webform_number element. - * @param $form_state + * @param array $form_state * The full form state for the webform. - * @return - * None. Calls a form_set_error if the number is not valid. */ function _webform_validate_number($element, &$form_state) { // Trim spaces for basic cleanup. @@ -573,7 +626,7 @@ function _webform_validate_number($element, &$form_state) { if ($value != '') { // First check that the entered value matches the expected value. if (!webform_number_format_match($value, $element['#point'], $element['#separator'])) { - form_error($element, t('%name field value must format numbers as "@example".', array('%name' => $element['#title'], '@example' => webform_number_format(12345.6789, $element['#decimals'], $element['#point'], $element['#separator'])))); + form_error($element, t('!name field value must format numbers as "@example".', array('!name' => $element['#title'], '@example' => webform_number_format(12345.6789, $element['#decimals'], $element['#point'], $element['#separator'])))); return; } @@ -592,24 +645,24 @@ function _webform_validate_number($element, &$form_state) { $max = $element['#min']; } if ($numeric_value > $max || $numeric_value < $min) { - form_error($element, t('%name field value of @value should be in the range @min to @max.', array('%name' => $element['#title'], '@value' => $value, '@min' => $element['#min'], '@max' => $element['#max']))); + form_error($element, t('!name field value of @value should be in the range @min to @max.', array('!name' => $element['#title'], '@value' => $value, '@min' => $element['#min'], '@max' => $element['#max']))); } } elseif ($element['#max'] != '' && $numeric_value > $element['#max']) { - form_error($element, t('%name field value must be less than @max.', array('%name' => $element['#title'], '@max' => $element['#max']))); + form_error($element, t('!name field value must be less than @max.', array('!name' => $element['#title'], '@max' => $element['#max']))); } elseif ($element['#min'] != '' && $numeric_value < $element['#min']) { - form_error($element, t('%name field value must be greater than @min.', array('%name' => $element['#title'], '@min' => $element['#min']))); + form_error($element, t('!name field value must be greater than @min.', array('!name' => $element['#title'], '@min' => $element['#min']))); } // Integer test. - if ($element['#integer'] && !is_int($numeric_value * 1)) { - form_error($element, t('%name field value of @value must be an integer.', array('%name' => $element['#title'], '@value' => $value))); + if ($element['#integer'] && filter_var((float) $numeric_value, FILTER_VALIDATE_INT) === FALSE) { + form_error($element, t('!name field value of @value must be an integer.', array('!name' => $element['#title'], '@value' => $value))); } // Step test. $starting_number = $element['#min'] ? $element['#min'] : 0; - if ($element['#step'] != 0 && webform_modulo($numeric_value - $starting_number, $element['#step']) != 0) { + if ($element['#step'] != 0 && webform_modulo($numeric_value - $starting_number, $element['#step']) != 0.0) { $samples = array( $starting_number, $starting_number + ($element['#step'] * 1), @@ -617,15 +670,15 @@ function _webform_validate_number($element, &$form_state) { $starting_number + ($element['#step'] * 3), ); if ($starting_number) { - form_error($element, t('%name field value must be @start plus a multiple of @step. i.e. @samples, etc.', array('%name' => $element['#title'], '@start' => $element['#min'], '@step' => $element['#step'], '@samples' => implode(', ', $samples)))); + form_error($element, t('!name field value must be @start plus a multiple of @step. i.e. @samples, etc.', array('!name' => $element['#title'], '@start' => $element['#min'], '@step' => $element['#step'], '@samples' => implode(', ', $samples)))); } else { - form_error($element, t('%name field value must be a multiple of @step. i.e. @samples, etc.', array('%name' => $element['#title'], '@step' => $element['#step'], '@samples' => implode(', ', $samples)))); + form_error($element, t('!name field value must be a multiple of @step. i.e. @samples, etc.', array('!name' => $element['#title'], '@step' => $element['#step'], '@samples' => implode(', ', $samples)))); } } } else { - form_error($element, t('%name field value of @value must be numeric.', array('%name' => $element['#title'], '@value' => $value))); + form_error($element, t('!name field value of @value must be numeric.', array('!name' => $element['#title'], '@value' => $value))); } } } @@ -667,11 +720,12 @@ function _webform_edit_number_validate($element, &$form_state) { if (!is_numeric($values['min'])) { form_error($element, t('Minimum must be numeric.')); } - if ($values['integer'] && !is_int($values['min'] * 1)) { + if ($values['integer'] && filter_var((float) $values['min'], FILTER_VALIDATE_INT) === FALSE) { form_error($element, t('Minimum must have an integer value.')); } } break; + case 'max': if ($values['max'] == '') { if (isset($values['type']) && $values['type'] === 'select') { @@ -682,18 +736,19 @@ function _webform_edit_number_validate($element, &$form_state) { if (!is_numeric($values['max'])) { form_error($element, t('Maximum must be numeric.')); } - if ($values['integer'] && !is_int($values['max'] * 1)) { + if ($values['integer'] && filter_var((float) $values['max'], FILTER_VALIDATE_INT) === FALSE) { form_error($element, t('Maximum must have an integer value.')); } } break; + case 'step': if ($values['step'] !== '') { if (!is_numeric($values['step'])) { form_error($element, t('Step must be numeric.')); } else { - if ($values['integer'] && !is_int($values['step'] * 1)) { + if ($values['integer'] && filter_var((float) $values['step'], FILTER_VALIDATE_INT) === FALSE) { form_error($element, t('Step must have an integer value.')); } } @@ -727,13 +782,14 @@ function _webform_number_select_options($component) { $options[$f . ''] = $f . ''; } - // TODO: HTML5 browsers apparently do not include the max value if it does + // @todo: HTML5 browsers apparently do not include the max value if it does // not line up with step. Restore this if needed in the future. // Add end limit if it's been skipped due to step. - //if (end($options) != $max) { - // $options[$f] = $max; - //} - + // @code + // if (end($options) != $max) { + // $options[$f] = $max; + // } + // @endcode if ($flipped) { $options = array_reverse($options, TRUE); } @@ -759,10 +815,38 @@ function _webform_number_format($component, $value) { * This function allows the thousands separator to be optional, but decimal * points must be in the right location. * + * A valid number is: + * 1. optional minus sign. + * 2. optional space. + * 3. the rest of the string can't be just a decimal or blank. + * 4. optional integer portion, with thousands separators. + * 5. optional decimal portion, starting is a decimal separator. + * Don't use preg_quote because a space is a valid thousands separator and + * needs quoting for the 'x' option to preg_match. + * * Based on http://stackoverflow.com/questions/5917082/regular-expression-to-match-numbers-with-or-without-commas-and-decimals-in-text. */ function webform_number_format_match($value, $point, $separator) { - return preg_match('/^(-? ?[1-9](?:\d{0,2})(?:' . ($separator ? (preg_quote($separator, '/') . '?') : '') . '\d{3})*(?:' . preg_quote($point, '/') . '\d*[0-9])?|0?' . preg_quote($point, '/') . '\d*[1-9]|0)$/', $value); + $thousands = $separator ? "\\$separator?" : ''; + $decimal = "\\$point"; + return preg_match("/ + ^ # Start of string + -? # Optional minus sign + \ ? # Optional space + (?!\.?$) # Assert looking ahead, not just a decimal or nothing + (?: # Interger portion (non-grouping) + \d{1,3} # 1 to 3 digits + (?: # Thousands group(s) + $thousands # Optional thousands separator + \d{2,3} # 2 or 3 digits. Some countries use groups of 2 sometimes + )* # 0 or more of these thousands groups + )? # End of optional integer portion + (?: # Decimal portion (non-grouping) + $decimal # Decimal point + \d* # 0 or more digits + )? # End of optional decimal portion + $ + /x", $value); } /** @@ -780,7 +864,7 @@ function webform_number_format($value, $decimals = NULL, $point = '.', $separato // If no decimal places are specified, do a best guess length of decimals. if (is_null($decimals) || $decimals === '') { // If it's an integer, no decimals needed. - if (is_int(($value . '') * 1)) { + if (filter_var((float) $value, FILTER_VALIDATE_INT) !== FALSE) { $decimals = 0; } else { @@ -799,8 +883,11 @@ function webform_number_format($value, $decimals = NULL, $point = '.', $separato * * @param string $value * The string value to be standardized into a numeric string. - * @param $point + * @param string $point * The point separator between the whole number and the decimals. + * + * @return string + * The converted number. */ function webform_number_standardize($value, $point) { // For simplicity, strip everything that's not the decimal point. @@ -816,5 +903,58 @@ function webform_number_standardize($value, $point) { * See https://drupal.org/node/1601968. */ function webform_modulo($a, $b) { - return $a - $b * (($b < 0) ? ceil($a / $b) : floor($a / $b)); -} \ No newline at end of file + $modulo = $a - $b * (($b < 0) ? ceil($a / $b) : floor($a / $b)); + if (webform_compare_floats($modulo, 0.0) == 0 || webform_compare_floats($modulo, $b) == 0) { + $modulo = 0.0; + } + return $modulo; +} + +/** + * Compare two floats. + * + * See @link http://php.net/manual/en/language.types.float.php @endlink. + * + * Comparison of floating point numbers for equality is surprisingly difficult, + * as evidenced by the references below. The simple test in this function works + * for numbers that are relatively close to 1E1. For very small numbers, it will + * show false equality. For very large numbers, it will show false inequality. + * Better implementations are hidered by the absense of PHP platform-specific + * floating point constants to properly set the minimum absolute and relative + * error in PHP. + * + * The use case for webform conditionals excludes very small or very large + * numeric comparisons. + * + * See @link http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm @endlink + * See @link http://floating-point-gui.de/errors/comparison/ @endlink + * See @link http://en.wikipedia.org/wiki/IEEE_754-1985#Denormalized_numbers @endlink + * + * @param float $number_1 + * The first number. + * @param float $number_2 + * The second number. + * + * @return int|null + * < 0 if number_1 is less than number_2; > 0 if number_1 is greater than + * number_2, 0 if they are equal, and NULL if either is not numeric. + */ +function webform_compare_floats($number_1, $number_2) { + if (!is_numeric($number_1) || !is_numeric($number_2)) { + return NULL; + } + + $number_1 = (float) $number_1; + $number_2 = (float) $number_2; + + $epsilon = 0.000001; + if (abs($number_1 - $number_2) < $epsilon) { + return 0; + } + elseif ($number_1 > $number_2) { + return 1; + } + else { + return -1; + } +} diff --git a/sites/all/modules/contrib/form/webform/components/pagebreak.inc b/sites/all/modules/contrib/form/webform/components/pagebreak.inc index 269f2adf..d97c74ff 100644 --- a/sites/all/modules/contrib/form/webform/components/pagebreak.inc +++ b/sites/all/modules/contrib/form/webform/components/pagebreak.inc @@ -45,8 +45,6 @@ function _webform_edit_pagebreak($component) { '#value' => '0', ); - $form['display'] = array('#type' => 'markup'); // Hide the display options. - $form['extra']['next_page_label'] = array( '#type' => 'textfield', '#title' => t('Next page button label'), @@ -56,8 +54,8 @@ function _webform_edit_pagebreak($component) { ); $form['extra']['prev_page_label'] = array( '#type' => 'textfield', - '#title' => t('Prev page button label'), - '#description' => t('This is used for the Prev Page button on the page after this page break. Default: < Prev Page'), + '#title' => t('Previous page button label'), + '#description' => t('This is used for the Previous Page button on the page after this page break. Default: < Prev Page'), '#default_value' => $component['extra']['prev_page_label'], '#size' => 30, ); @@ -68,7 +66,7 @@ function _webform_edit_pagebreak($component) { /** * Implements _webform_render_component(). */ -function _webform_render_pagebreak($component, $value = NULL, $filter = TRUE) { +function _webform_render_pagebreak($component, $value = NULL, $filter = TRUE, $submission = NULL) { $element = array( '#type' => 'hidden', '#value' => $component['name'], @@ -78,9 +76,9 @@ function _webform_render_pagebreak($component, $value = NULL, $filter = TRUE) { } /** - * Implements _webform_render_component(). + * Implements _webform_display_component(). */ -function _webform_display_pagebreak($component, $value = NULL, $format = 'html') { +function _webform_display_pagebreak($component, $value = NULL, $format = 'html', $submission = array()) { $element = array( '#theme' => 'webform_display_pagebreak', '#title' => $component['name'], @@ -97,5 +95,5 @@ function _webform_display_pagebreak($component, $value = NULL, $format = 'html') function theme_webform_display_pagebreak($variables) { $element = $variables['element']; - return $element['#format'] == 'html' ? '

    ' . check_plain($element['#title']) . '

    ' : "--" . $element['#title'] . "--\n"; + return $element['#format'] == 'html' ? '

    ' . check_plain($element['#title']) . '

    ' : '==' . $element['#title'] . "==\n"; } diff --git a/sites/all/modules/contrib/form/webform/components/select.inc b/sites/all/modules/contrib/form/webform/components/select.inc index 617c7fdc..2b1b7fff 100644 --- a/sites/all/modules/contrib/form/webform/components/select.inc +++ b/sites/all/modules/contrib/form/webform/components/select.inc @@ -12,7 +12,7 @@ function _webform_defaults_select() { return array( 'name' => '', 'form_key' => NULL, - 'mandatory' => 0, + 'required' => 0, 'pid' => 0, 'weight' => 0, 'value' => '', @@ -20,14 +20,17 @@ function _webform_defaults_select() { 'items' => '', 'multiple' => NULL, 'aslist' => NULL, + 'empty_option' => '', 'optrand' => 0, 'other_option' => NULL, 'other_text' => t('Other...'), 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, 'custom_keys' => FALSE, 'options_source' => '', 'private' => FALSE, + 'analysis' => TRUE, ), ); } @@ -57,6 +60,15 @@ function _webform_edit_select($component) { ), ); + // Default component if nested under a grid. + if (!isset($component['cid']) && $component['pid'] && + ($node = node_load($component['nid'])) && ($parent = $node->webform['components'][$component['pid']]) && + $parent['type'] == 'grid') { + $component['value'] = $parent['value']; + $component['extra']['items'] = $parent['extra']['options']; + $component['required'] = $parent['required']; + } + $other = array(); if ($info = _webform_select_options_info()) { $options = array('' => t('None')); @@ -69,7 +81,6 @@ function _webform_edit_select($component) { '#type' => 'select', '#options' => $options, '#default_value' => $component['extra']['options_source'], - '#weight' => 1, '#description' => t('Use a pre-built list of options rather than entering options manually. Options will not be editable if using pre-built list.'), '#parents' => array('extra', 'options_source'), '#weight' => 5, @@ -83,6 +94,7 @@ function _webform_edit_select($component) { '#default_value' => $component['extra']['other_option'], '#description' => t('Check this option if you want to allow users to enter an option not on the list.'), '#parents' => array('extra', 'other_option'), + '#attributes' => array('class' => array('other-option-checkbox')), '#weight' => 2, ); $other['other_text'] = array( @@ -92,6 +104,11 @@ function _webform_edit_select($component) { '#description' => t('If allowing other options, enter text to be used for other-enabling option.'), '#parents' => array('extra', 'other_text'), '#weight' => 3, + '#states' => array( + 'visible' => array( + ':input.other-option-checkbox' => array('checked' => TRUE), + ), + ), ); } @@ -110,7 +127,7 @@ function _webform_edit_select($component) { $form['items']['options'] = array( '#type' => 'options', '#limit' => 500, - '#optgroups' => $component['extra']['aslist'], + '#optgroups' => TRUE, '#multiple' => $component['extra']['multiple'], '#multiple_toggle' => t('Multiple'), '#default_value' => $component['value'], @@ -130,7 +147,7 @@ function _webform_edit_select($component) { '#type' => 'textarea', '#title' => t('Options'), '#default_value' => $component['extra']['items'], - '#description' => t('Key-value pairs MUST be specified as "safe_key|Some readable option". Use of only alphanumeric characters and underscores is recommended in keys. One option per line. Option groups may be specified with <Group Name>. <> can be used to insert items at the root of the menu after specifying a group.') . theme('webform_token_help'), + '#description' => t('Key-value pairs MUST be specified as "safe_key|Some readable option". Use of only alphanumeric characters and underscores is recommended in keys. One option per line. Option groups may be specified with <Group Name>. <> can be used to insert items at the root of the menu after specifying a group.') . ' ' . theme('webform_token_help'), '#cols' => 60, '#rows' => 5, '#weight' => 0, @@ -148,7 +165,7 @@ function _webform_edit_select($component) { '#type' => 'textfield', '#title' => t('Default value'), '#default_value' => $component['value'], - '#description' => t('The default value of the field identified by its key. For multiple selects use commas to separate multiple defaults.') . theme('webform_token_help'), + '#description' => t('The default value of the field identified by its key. For multiple selects use commas to separate multiple defaults.') . ' ' . theme('webform_token_help'), '#size' => 60, '#maxlength' => 1024, '#weight' => 0, @@ -169,6 +186,22 @@ function _webform_edit_select($component) { '#description' => t('Check this option if you want the select component to be displayed as a select list box instead of radio buttons or checkboxes. Option groups (nested options) are only supported with listbox components.'), '#parents' => array('extra', 'aslist'), ); + $form['display']['empty_option'] = array( + '#type' => 'textfield', + '#title' => t('Empty option'), + '#default_value' => $component['extra']['empty_option'], + '#size' => 60, + '#maxlength' => 255, + '#description' => t('The list item to show when no default is provided. Leave blank for "- None -" or "- Select -".'), + '#parents' => array('extra', 'empty_option'), + '#states' => array( + 'visible' => array( + ':input[name="extra[aslist]"]' => array('checked' => TRUE), + ':input[name="extra[multiple]"]' => array('checked' => FALSE), + ':input[name="value"]' => array('filled' => FALSE), + ), + ), + ); $form['display']['optrand'] = array( '#type' => 'checkbox', '#title' => t('Randomize options'), @@ -198,7 +231,8 @@ function _webform_edit_validate_select($element, &$form_state) { $line = trim($line); if (preg_match('/^\<([^>]*)\>$/', $line, $matches)) { $group = $matches[1]; - $key = NULL; // No need to store group names. + // No need to store group names. + $key = NULL; } elseif (preg_match('/^([^|]*)\|(.*)$/', $line, $matches)) { $key = $matches[1]; @@ -264,6 +298,11 @@ function _webform_edit_validate_options($element, &$form_state) { // Options saved for grid components. else { $form_state['values']['extra']['custom_' . rtrim($key, 's') . '_keys'] = $element_options['custom_keys']; + // There is only one 'value', but grids have two options widgets, one for questions and one for options. + // Only options have a default 'value'. Note that multiple selection is now allowed for grid options. + if ($key == 'options') { + $form_state['values']['value'] = $element_options['default_value']; + } } } @@ -285,32 +324,49 @@ function _webform_edit_validate_set_aslist($options, &$form_state) { /** * Implements _webform_render_component(). */ -function _webform_render_select($component, $value = NULL, $filter = TRUE) { +function _webform_render_select($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#required' => $component['mandatory'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#theme_wrappers' => array('webform_element'), - '#pre_render' => array(), // Needed to disable double-wrapping of radios and checkboxes. '#translatable' => array('title', 'description', 'options'), ); + // Prevent double-wrapping of radios and checkboxes. + if (!$component['extra']['aslist']) { + $element['#pre_render'] = array(); + } + // Convert the user-entered options list into an array. - $default_value = $filter ? _webform_filter_values($component['value'], $node, NULL, NULL, FALSE) : $component['value']; + $default_value = $filter ? webform_replace_tokens($component['value'], $node) : $component['value']; $options = _webform_select_options($component, !$component['extra']['aslist'], $filter); + if (empty($options)) { + // Make element inaccessible if there are no options as there is no point in showing it. + $element['#access'] = FALSE; + } if ($component['extra']['optrand']) { _webform_shuffle_options($options); } + // Add HTML5 required attribute, if needed and possible (not working on more than one checkboxes). + if ($component['required'] && ($component['extra']['aslist'] || !$component['extra']['multiple'] || count($options) == 1)) { + $element['#attributes']['required'] = 'required'; + } + // Add default options if using a select list with no default. This trigger's // Drupal 7's adding of the option for us. See @form_process_select(). if ($component['extra']['aslist'] && !$component['extra']['multiple'] && $default_value === '') { $element['#empty_value'] = ''; + if (strlen($component['extra']['empty_option'])) { + $element['#empty_option'] = $component['extra']['empty_option']; + $element['#translatable'][] = 'empty_option'; + } } // Set the component options. @@ -322,8 +378,8 @@ function _webform_render_select($component, $value = NULL, $filter = TRUE) { $value = $component['extra']['multiple'] ? array_filter(array_map('trim', explode(',', $default_value)), 'strlen') : $default_value; } - // Convert all values into an array; component may now be single but was previously multiple, or vice-versa - $value = (array)$value; + // Convert all values into an array; component may now be single but was previously multiple, or vice-versa. + $value = (array) $value; // Set the default value. Note: "No choice" is stored as an empty string, // which will match a 0 key for radios; NULL is used to avoid unintentional @@ -352,6 +408,9 @@ function _webform_render_select($component, $value = NULL, $filter = TRUE) { $element['#other_delimiter'] = ', '; // Merge in Webform's #process function for Select or other. $element['#process'] = array_merge(element_info_property('select_or_other', '#process'), array('webform_expand_select_or_other')); + // Inherit select_or_other settings or set defaults. + $element['#disabled'] = isset($component['extra']['#disabled']) ? $component['extra']['#disabled'] : element_info_property('select_or_other', 'disabled'); + $element['#size'] = isset($component['extra']['#size']) ? $component['extra']['#size'] : element_info_property('select_or_other', 'size'); if ($component['extra']['multiple']) { $element['#multiple'] = TRUE; @@ -435,7 +494,7 @@ function webform_expand_select_ids($element) { $element[$key]['#id'] = $id . '-' . $delta; // Prevent scripts or CSS in the labels for each checkbox or radio. - $element[$key]['#title'] = _webform_filter_xss($element[$key]['#title']); + $element[$key]['#title'] = isset($element[$key]['#title']) ? webform_filter_xss($element[$key]['#title']) : ''; } return $element; } @@ -443,16 +502,21 @@ function webform_expand_select_ids($element) { /** * Implements _webform_display_component(). */ -function _webform_display_select($component, $value, $format = 'html') { +function _webform_display_select($component, $value, $format = 'html', $submission = array()) { + // Sort values by numeric key. These may be in alphabetic order from the database query, + // which is not numeric order for keys '10' and higher. + $value = (array) $value; + ksort($value, SORT_NUMERIC); return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#multiple' => $component['extra']['multiple'], '#theme' => 'webform_display_select', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), '#format' => $format, '#options' => _webform_select_options($component, !$component['extra']['aslist']), - '#value' => (array) $value, + '#value' => $value, '#translatable' => array('title', 'options'), ); } @@ -460,9 +524,21 @@ function _webform_display_select($component, $value, $format = 'html') { /** * Implements _webform_submit_component(). * - * Convert FAPI 0/1 values into something saveable. + * Handle select or other... modifications and convert FAPI 0/1 values into + * something saveable. */ function _webform_submit_select($component, $value) { + if (module_exists('select_or_other') && $component['extra']['other_option'] && is_array($value) && count($value) == 2 && isset($value['select'])) { + if (is_array($value['select']) && isset($value['select']['select_or_other'])) { + unset($value['select']['select_or_other']); + $value['select'][] = $value['other']; + } + elseif ($value['select'] == 'select_or_other') { + $value['select'] = $value['other']; + } + $value = $value['select']; + } + // Build a list of all valid keys expected to be submitted. $options = _webform_select_options($component, TRUE); @@ -475,7 +551,7 @@ function _webform_submit_select($component, $value) { // Checkboxes submit an integer value of 0 when unchecked. A checkbox // with a value of '0' is valid, so we can't use empty() here. if ($option_value === 0 && !$component['extra']['aslist'] && $component['extra']['multiple']) { - unset($value[$option_value]); + // Don't save unchecked values. } else { $return[] = $option_value; @@ -487,6 +563,11 @@ function _webform_submit_select($component, $value) { $return[] = $option_value; } } + + // If no elements are selected, then save an empty string to indicate that this components should not be defaulted again. + if (!$return) { + $return = array(''); + } } elseif (is_string($value)) { $return = $value; @@ -521,7 +602,7 @@ function theme_webform_display_select($variables) { if ($option_value !== '') { // Administer provided values. if (isset($options[$option_value])) { - $items[] = $element['#format'] == 'html' ? _webform_filter_xss($options[$option_value]) : $options[$option_value]; + $items[] = $element['#format'] == 'html' ? webform_filter_xss($options[$option_value]) : $options[$option_value]; } // User-specified in the "other" field. else { @@ -534,7 +615,7 @@ function theme_webform_display_select($variables) { if (isset($element['#value'][0]) && $element['#value'][0] !== '') { // Administer provided values. if (isset($options[$element['#value'][0]])) { - $items[] = $element['#format'] == 'html' ? _webform_filter_xss($options[$element['#value'][0]]) : $options[$element['#value'][0]]; + $items[] = $element['#format'] == 'html' ? webform_filter_xss($options[$element['#value'][0]]) : $options[$element['#value'][0]]; } // User-specified in the "other" field. else { @@ -564,65 +645,89 @@ function theme_webform_display_select($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_select($component, $sids = array(), $single = FALSE) { +function _webform_analysis_select($component, $sids = array(), $single = FALSE, $join = NULL) { $options = _webform_select_options($component, TRUE); - $show_other_results = $single; - $sid_placeholders = count($sids) ? array_fill(0, count($sids), "'%s'") : array(); - $sid_filter = count($sids) ? " AND sid IN (" . implode(",", $sid_placeholders) . ")" : ""; - - $option_operator = $show_other_results ? 'NOT IN' : 'IN'; + // Create a generic query for the component. $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) - ->fields('wsd', array('data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']) - ->condition('data', '', '<>') - ->condition('data', array_keys($options), $option_operator) - ->groupBy('data'); - $query->addExpression('COUNT(data)', 'datacount'); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']) + ->condition('wsd.data', '', '<>'); - if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + if ($sids) { + $query->condition('wsd.sid', $sids, 'IN'); } - $count_query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']) - ->condition('data', '', '<>'); - $count_query->addExpression('COUNT(*)', 'datacount'); - if (count($sids)) { - $count_query->condition('sid', $sids, 'IN'); + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); + } + + // Clone the query for later use, if needed. + if ($component['extra']['other_option']) { + $count_query = clone $query; + if ($single) { + $other_query = clone $query; + } } - $result = $query->execute(); $rows = array(); + $other = array(); $normal_count = 0; - foreach ($result as $data) { - $display_option = $single ? $data['data'] : $options[$data['data']]; - $rows[$data['data']] = array(_webform_filter_xss($display_option), $data['datacount']); - $normal_count += $data['datacount']; - } - if (!$show_other_results) { + if ($options) { + // Gather the normal results first (not "other" options). + $query->addExpression('COUNT(wsd.data)', 'datacount'); + $result = $query + ->condition('wsd.data', array_keys($options), 'IN') + ->fields('wsd', array('data')) + ->groupBy('wsd.data') + ->execute(); + foreach ($result as $data) { + $display_option = isset($options[$data['data']]) ? $options[$data['data']] : $data['data']; + $rows[$data['data']] = array(webform_filter_xss($display_option), $data['datacount']); + $normal_count += $data['datacount']; + } + // Order the results according to the normal options array. $ordered_rows = array(); foreach (array_intersect_key($options, $rows) as $key => $label) { $ordered_rows[] = $rows[$key]; } - - // Add a row for any unknown or user-entered values. - if ($component['extra']['other_option']) { - $full_count = $count_query->execute()->fetchField(); - $other_count = $full_count - $normal_count; - $display_option = !empty($component['extra']['other_text']) ? check_plain($component['extra']['other_text']) : t('Other...'); - $other_text = $other_count ? $other_count . ' (' . l(t('view'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']) . ')' : $other_count; - $ordered_rows[] = array($display_option, $other_text); - } - $rows = $ordered_rows; } - return $rows; + // Add a row for displaying the total unknown or user-entered values. + if ($component['extra']['other_option']) { + $count_query->addExpression('COUNT(*)', 'datacount'); + $full_count = $count_query->execute()->fetchField(); + $other_count = $full_count - $normal_count; + $display_option = !empty($component['extra']['other_text']) ? check_plain($component['extra']['other_text']) : t('Other...'); + $other_text = ($other_count && !$single) ? $other_count . ' (' . l(t('view'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']) . ')' : $other_count; + $rows[] = array($display_option, $other_text); + + // If showing all results, execute the "other" query and append their rows. + if ($single) { + $other_query->addExpression('COUNT(wsd.data)', 'datacount'); + $other_query + ->fields('wsd', array('data')) + ->groupBy('wsd.data'); + if ($options) { + $other_query->condition('wsd.data', array_keys($options), 'NOT IN'); + } + $other_result = $other_query->execute(); + foreach ($other_result as $data) { + $other[] = array(check_plain($data['data']), $data['datacount']); + } + if ($other) { + array_unshift($other, '' . t('Other responses') . ''); + } + } + } + + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -633,12 +738,13 @@ function _webform_table_select($component, $value) { $options = _webform_select_options($component, TRUE); $value = (array) $value; + ksort($value, SORT_NUMERIC); $items = array(); // Set the value as a single string. foreach ($value as $option_value) { if ($option_value !== '') { if (isset($options[$option_value])) { - $items[] = _webform_filter_xss($options[$option_value]); + $items[] = webform_filter_xss($options[$option_value]); } else { $items[] = check_plain($option_value); @@ -649,6 +755,24 @@ function _webform_table_select($component, $value) { return implode('
    ', $items); } +/** + * Implements _webform_action_set_component(). + */ +function _webform_action_set_select($component, &$element, &$form_state, $value) { + // Set the value as an array for multiple select or single value otherwise. + if ($element['#type'] == 'checkboxes') { + $checkbox_values = $element['#options']; + array_walk($checkbox_values, function (&$option_value, $key) use ($value) { + $option_value = (int) (strval($key) === $value); + }); + } + else { + $value = $component['extra']['multiple'] ? array($value) : $value; + } + $element['#value'] = $value; + form_set_value($element, $value, $form_state); +} + /** * Implements _webform_csv_headers_component(). */ @@ -661,10 +785,15 @@ function _webform_csv_headers_select($component, $export_options) { if ($component['extra']['multiple'] && $export_options['select_format'] == 'separate') { $headers[0][] = ''; - $headers[1][] = $component['name']; - $items = _webform_select_options($component, TRUE, FALSE); + $headers[1][] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; + $items = _webform_select_options($component, TRUE); if ($component['extra']['other_option']) { - $other_label = !empty($component['extra']['other_text']) ? check_plain($component['extra']['other_text']) : t('Other...'); + if ($export_options['header_keys']) { + $other_label = $component['form_key'] . '_other'; + } + else { + $other_label = !empty($component['extra']['other_text']) ? check_plain($component['extra']['other_text']) : t('Other...'); + } $items[$other_label] = $other_label; } $count = 0; @@ -686,7 +815,7 @@ function _webform_csv_headers_select($component, $export_options) { else { $headers[0][] = ''; $headers[1][] = ''; - $headers[2][] = $component['name']; + $headers[2][] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; } return $headers; } @@ -695,12 +824,14 @@ function _webform_csv_headers_select($component, $export_options) { * Implements _webform_csv_data_component(). */ function _webform_csv_data_select($component, $export_options, $value) { - $options = _webform_select_options($component, TRUE, FALSE); + $options = _webform_select_options($component, TRUE); $return = array(); if ($component['extra']['multiple']) { foreach ($options as $key => $item) { - $index = array_search($key, (array) $value); + // Strict search is needed to avoid a key of 0 from matching an empty + // value. + $index = array_search((string) $key, (array) $value, TRUE); if ($index !== FALSE) { if ($export_options['select_format'] == 'separate') { $return[] = 'X'; @@ -737,6 +868,23 @@ function _webform_csv_data_select($component, $export_options, $value) { return $return; } +/** + * Implements _webform_options_component(). + * + * This function is confusingly an alias of _webform_select_options(). However + * this version is intended to be accessed publicly via + * webform_component_invoke(), since it is a Webform "hook", rather than an + * internal, private function. To get the list of select list options for + * a component, use: + * + * @code + * $options = webform_component_invoke($component['type'], 'options', $component); + * @endcode + */ +function _webform_options_select($component, $flat = FALSE) { + return _webform_select_options($component, $flat); +} + /** * Menu callback; Return a predefined list of select options as JSON. */ @@ -745,7 +893,7 @@ function webform_select_options_ajax($source_name = '') { $component['extra']['options_source'] = $source_name; if ($source_name && isset($info[$source_name])) { - $options = _webform_select_options_to_text(_webform_select_options($component, !$component['extra']['aslist'], FALSE)); + $options = _webform_select_options_to_text(_webform_select_options($component, FALSE, FALSE)); } else { $options = ''; @@ -764,15 +912,36 @@ function webform_select_options_ajax($source_name = '') { */ function _webform_select_options($component, $flat = FALSE, $filter = TRUE) { if ($component['extra']['options_source']) { - $options = _webform_select_options_callback($component['extra']['options_source'], $component, $flat, $filter); + $options = _webform_select_options_callback($component['extra']['options_source'], $component, $flat); } else { - $options = _webform_select_options_from_text($component['extra']['items'], $flat, $filter); + $options = _webform_select_options_from_text($component['extra']['items'], $flat); + } + + // Replace tokens if needed in the options. + if ($filter) { + $node = node_load($component['nid']); + $options = _webform_select_replace_tokens($options, $node); } return isset($options) ? $options : array(); } +/** + * Replace tokens in the values of a list of select options. + */ +function _webform_select_replace_tokens($options, $node) { + foreach ($options as $key => $option) { + if (is_array($option)) { + $options[$key] = _webform_select_replace_tokens($option, $node); + } + else { + $options[$key] = webform_replace_tokens($option, $node); + } + } + return $options; +} + /** * Load Webform select option info from 3rd party modules. */ @@ -796,16 +965,14 @@ function _webform_select_options_info() { /** * Execute a select option callback. * - * @param $name + * @param string $name * The name of the options group. - * @param $component + * @param array $component * The full Webform component. - * @param $flat + * @param bool $flat * Whether the information returned should exclude any nested groups. - * @param $filter - * Whether information returned should be sanitized. Defaults to TRUE. */ -function _webform_select_options_callback($name, $component, $flat = FALSE, $filter = TRUE) { +function _webform_select_options_callback($name, $component, $flat = FALSE) { $info = _webform_select_options_info(); // Include any necessary files. @@ -816,7 +983,7 @@ function _webform_select_options_callback($name, $component, $flat = FALSE, $fil } // Execute the callback function. - if (isset($info[$name]['options callback']) && function_exists($info[$name]['options callback'])) { + if (isset($info[$name]['options callback']) && is_callable($info[$name]['options callback'])) { $function = $info[$name]['options callback']; $arguments = array(); @@ -824,22 +991,24 @@ function _webform_select_options_callback($name, $component, $flat = FALSE, $fil $arguments = $info[$name]['options arguments']; } - return $function($component, $flat, $filter, $arguments); + return $function($component, $flat, $arguments); } } /** - * Utility function to split user-entered values from new-line separated - * text into an array of options. + * Splits user values from new-line separated text into an array of options. * - * @param $text + * @param string $text * Text to be converted into a select option array. - * @param $flat + * @param bool $flat * Optional. If specified, return the option array and exclude any optgroups. - * @param $filter - * Optional. Whether or not to filter returned values. + * + * @return array + * An array of options suitable for use as a #options property. Note that + * values are not filtered and may contain tokens. Individual values should be + * run through webform_replace_tokens() if displaying to an end-user. */ -function _webform_select_options_from_text($text, $flat = FALSE, $filter = TRUE) { +function _webform_select_options_from_text($text, $flat = FALSE) { static $option_cache = array(); // Keep each processed option block in an array indexed by the MD5 hash of @@ -853,7 +1022,7 @@ function _webform_select_options_from_text($text, $flat = FALSE, $filter = TRUE) $group = NULL; foreach ($rows as $option) { $option = trim($option); - /** + /* * If the Key of the option is within < >, treat as an optgroup * * @@ -867,17 +1036,16 @@ function _webform_select_options_from_text($text, $flat = FALSE, $filter = TRUE) unset($group); } elseif (!$flat) { - $group = $filter ? _webform_filter_values($matches[1], NULL, NULL, NULL, FALSE) : $matches[1]; + $group = $matches[1]; } } elseif (preg_match('/^([^|]+)\|(.*)$/', $option, $matches)) { - $key = $filter ? _webform_filter_values($matches[1], NULL, NULL, NULL, FALSE) : $matches[1]; - $value = $filter ? _webform_filter_values($matches[2], NULL, NULL, NULL, FALSE) : $matches[2]; + $key = $matches[1]; + $value = $matches[2]; isset($group) ? $options[$group][$key] = $value : $options[$key] = $value; } else { - $filtered_option = $filter ? _webform_filter_values($option, NULL, NULL, NULL, FALSE) : $option; - isset($group) ? $options[$group][$filtered_option] = $filtered_option : $options[$filtered_option] = $filtered_option; + isset($group) ? $options[$group][$option] = $option : $options[$option] = $option; } } diff --git a/sites/all/modules/contrib/form/webform/components/textarea.inc b/sites/all/modules/contrib/form/webform/components/textarea.inc index eb56ec77..c4556b78 100644 --- a/sites/all/modules/contrib/form/webform/components/textarea.inc +++ b/sites/all/modules/contrib/form/webform/components/textarea.inc @@ -15,7 +15,7 @@ function _webform_defaults_textarea() { 'pid' => 0, 'weight' => 0, 'value' => '', - 'mandatory' => 0, + 'required' => 0, 'extra' => array( 'cols' => '', 'rows' => '', @@ -23,13 +23,15 @@ function _webform_defaults_textarea() { 'resizable' => 1, 'disabled' => 0, 'description' => '', + 'description_above' => FALSE, + 'placeholder' => '', 'attributes' => array(), 'private' => FALSE, + 'analysis' => FALSE, ), ); } - /** * Implements _webform_theme_component(). */ @@ -51,7 +53,7 @@ function _webform_edit_textarea($component) { '#type' => 'textarea', '#title' => t('Default value'), '#default_value' => $component['value'], - '#description' => t('The default value of the field.') . theme('webform_token_help'), + '#description' => t('The default value of the field.') . ' ' . theme('webform_token_help'), '#cols' => 60, '#rows' => 5, '#weight' => 0, @@ -82,11 +84,18 @@ function _webform_edit_textarea($component) { '#default_value' => $component['extra']['resizable'], '#parents' => array('extra', 'resizable'), ); + $form['display']['placeholder'] = array( + '#type' => 'textfield', + '#title' => t('Placeholder'), + '#default_value' => $component['extra']['placeholder'], + '#description' => t('The placeholder will be shown in the field until the user starts entering a value.'), + '#parents' => array('extra', 'placeholder'), + ); $form['display']['disabled'] = array( '#type' => 'checkbox', '#title' => t('Disabled'), '#return_value' => 1, - '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'), + '#description' => t('Make this field non-editable. Useful for displaying default value. Changeable via JavaScript or developer tools.'), '#weight' => 11, '#default_value' => $component['extra']['disabled'], '#parents' => array('extra', 'disabled'), @@ -97,25 +106,34 @@ function _webform_edit_textarea($component) { /** * Implements _webform_render_component(). */ -function _webform_render_textarea($component, $value = NULL, $filter = TRUE) { +function _webform_render_textarea($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( '#type' => 'textarea', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#default_value' => $filter ? _webform_filter_values($component['value'], $node) : $component['value'], - '#required' => $component['mandatory'], + '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#rows' => !empty($component['extra']['rows']) ? $component['extra']['rows'] : 5, '#cols' => !empty($component['extra']['cols']) ? $component['extra']['cols'] : 60, '#attributes' => $component['extra']['attributes'], - '#resizable' => (bool) $component['extra']['resizable'], // MUST be FALSE to disable. + // MUST be FALSE to disable. + '#resizable' => (bool) $component['extra']['resizable'], '#theme_wrappers' => array('webform_element'), '#translatable' => array('title', 'description'), ); + if ($component['required']) { + $element['#attributes']['required'] = 'required'; + } + + if ($component['extra']['placeholder']) { + $element['#attributes']['placeholder'] = $component['extra']['placeholder']; + } + if ($component['extra']['disabled']) { if ($filter) { $element['#attributes']['readonly'] = 'readonly'; @@ -125,7 +143,7 @@ function _webform_render_textarea($component, $value = NULL, $filter = TRUE) { } } - if (isset($value)) { + if (isset($value[0])) { $element['#default_value'] = $value[0]; } @@ -135,9 +153,10 @@ function _webform_render_textarea($component, $value = NULL, $filter = TRUE) { /** * Implements _webform_display_component(). */ -function _webform_display_textarea($component, $value, $format = 'html') { +function _webform_display_textarea($component, $value, $format = 'html', $submission = array()) { return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#theme' => 'webform_display_textarea', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), @@ -162,14 +181,18 @@ function theme_webform_display_textarea($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_textarea($component, $sids = array()) { +function _webform_analysis_textarea($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $nonblanks = 0; @@ -187,8 +210,16 @@ function _webform_analysis_textarea($component, $sids = array()) { $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks)); $rows[1] = array(t('User entered value'), $nonblanks); - $rows[2] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0')); - return $rows; + + $other[] = array( + t('Average submission length in words (ex blanks)'), + $nonblanks != 0 ? number_format($wordcount / $nonblanks, 2) : '0', + ); + + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -198,6 +229,14 @@ function _webform_table_textarea($component, $value) { return empty($value[0]) ? '' : check_plain($value[0]); } +/** + * Implements _webform_action_set_component(). + */ +function _webform_action_set_textarea($component, &$element, &$form_state, $value) { + $element['#value'] = $value; + form_set_value($element, $value, $form_state); +} + /** * Implements _webform_csv_headers_component(). */ @@ -205,7 +244,7 @@ function _webform_csv_headers_textarea($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } diff --git a/sites/all/modules/contrib/form/webform/components/textfield.inc b/sites/all/modules/contrib/form/webform/components/textfield.inc index 11e11ab0..1bddc466 100644 --- a/sites/all/modules/contrib/form/webform/components/textfield.inc +++ b/sites/all/modules/contrib/form/webform/components/textfield.inc @@ -15,18 +15,22 @@ function _webform_defaults_textfield() { 'pid' => 0, 'weight' => 0, 'value' => '', - 'mandatory' => 0, + 'required' => 0, 'extra' => array( 'width' => '', 'maxlength' => '', + 'minlength' => '', 'field_prefix' => '', 'field_suffix' => '', 'disabled' => 0, 'unique' => 0, 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, + 'placeholder' => '', 'attributes' => array(), 'private' => FALSE, + 'analysis' => FALSE, ), ); } @@ -52,7 +56,7 @@ function _webform_edit_textfield($component) { '#type' => 'textfield', '#title' => t('Default value'), '#default_value' => $component['value'], - '#description' => t('The default value of the field.') . theme('webform_token_help'), + '#description' => t('The default value of the field.') . ' ' . theme('webform_token_help'), '#size' => 60, '#maxlength' => 1024, '#weight' => 0, @@ -67,6 +71,14 @@ function _webform_edit_textfield($component) { '#weight' => 0, '#parents' => array('extra', 'width'), ); + $form['display']['placeholder'] = array( + '#type' => 'textfield', + '#title' => t('Placeholder'), + '#default_value' => $component['extra']['placeholder'], + '#description' => t('The placeholder will be shown in the field until the user starts entering a value.'), + '#weight' => 1, + '#parents' => array('extra', 'placeholder'), + ); $form['display']['field_prefix'] = array( '#type' => 'textfield', '#title' => t('Prefix text placed to the left of the textfield'), @@ -74,7 +86,7 @@ function _webform_edit_textfield($component) { '#description' => t('Examples: $, #, -.'), '#size' => 20, '#maxlength' => 127, - '#weight' => 1.1, + '#weight' => 2.1, '#parents' => array('extra', 'field_prefix'), ); $form['display']['field_suffix'] = array( @@ -84,14 +96,14 @@ function _webform_edit_textfield($component) { '#description' => t('Examples: lb, kg, %.'), '#size' => 20, '#maxlength' => 127, - '#weight' => 1.2, + '#weight' => 2.2, '#parents' => array('extra', 'field_suffix'), ); $form['display']['disabled'] = array( '#type' => 'checkbox', '#title' => t('Disabled'), '#return_value' => 1, - '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'), + '#description' => t('Make this field non-editable. Useful for displaying default value. Changeable via JavaScript or developer tools.'), '#weight' => 11, '#default_value' => $component['extra']['disabled'], '#parents' => array('extra', 'disabled'), @@ -115,30 +127,53 @@ function _webform_edit_textfield($component) { '#weight' => 2, '#parents' => array('extra', 'maxlength'), ); + $form['validation']['minlength'] = array( + '#type' => 'textfield', + '#title' => t('Minlength'), + '#default_value' => $component['extra']['minlength'], + '#description' => t('Minimum length of the textfield value. The component may still be empty unless it is set as Required.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 3, + '#parents' => array('extra', 'minlength'), + ); return $form; } /** * Implements _webform_render_component(). */ -function _webform_render_textfield($component, $value = NULL, $filter = TRUE) { +function _webform_render_textfield($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( '#type' => 'textfield', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#default_value' => $filter ? _webform_filter_values($component['value'], $node, NULL, NULL, FALSE) : $component['value'], - '#required' => $component['mandatory'], + '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? _webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']), - '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? _webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']), - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']), + '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']), + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#attributes' => $component['extra']['attributes'], '#theme_wrappers' => array('webform_element'), - '#translatable' => array('title', 'description', 'field_prefix', 'field_suffix'), + '#translatable' => array( + 'title', + 'description', + 'field_prefix', + 'field_suffix', + ), ); + if ($component['required']) { + $element['#attributes']['required'] = 'required'; + } + + if ($component['extra']['placeholder']) { + $element['#attributes']['placeholder'] = $component['extra']['placeholder']; + } + if ($component['extra']['disabled']) { if ($filter) { $element['#attributes']['readonly'] = 'readonly'; @@ -160,8 +195,11 @@ function _webform_render_textfield($component, $value = NULL, $filter = TRUE) { if ($component['extra']['maxlength'] > 0) { $element['#maxlength'] = $component['extra']['maxlength']; } + if ($component['extra']['minlength'] > 0) { + $element['#minlength'] = $component['extra']['minlength']; + } - if (isset($value)) { + if (isset($value[0])) { $element['#default_value'] = $value[0]; } @@ -171,9 +209,10 @@ function _webform_render_textfield($component, $value = NULL, $filter = TRUE) { /** * Implements _webform_display_component(). */ -function _webform_display_textfield($component, $value, $format = 'html') { +function _webform_display_textfield($component, $value, $format = 'html', $submission = array()) { return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#theme' => 'webform_display_textfield', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), @@ -199,14 +238,18 @@ function theme_webform_display_textfield($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_textfield($component, $sids = array()) { +function _webform_analysis_textfield($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $nonblanks = 0; @@ -224,8 +267,16 @@ function _webform_analysis_textfield($component, $sids = array()) { $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks)); $rows[1] = array(t('User entered value'), $nonblanks); - $rows[2] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0')); - return $rows; + + $other[] = array( + t('Average submission length in words (ex blanks)'), + $nonblanks != 0 ? number_format($wordcount / $nonblanks, 2) : '0', + ); + + return array( + 'table_rows' => $rows, + 'other_data' => $other, + ); } /** @@ -235,6 +286,14 @@ function _webform_table_textfield($component, $value) { return check_plain(empty($value[0]) ? '' : $value[0]); } +/** + * Implements _webform_action_set_component(). + */ +function _webform_action_set_textfield($component, &$element, &$form_state, $value) { + $element['#value'] = $value; + form_set_value($element, $value, $form_state); +} + /** * Implements _webform_csv_headers_component(). */ @@ -242,7 +301,7 @@ function _webform_csv_headers_textfield($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } diff --git a/sites/all/modules/contrib/form/webform/components/time.inc b/sites/all/modules/contrib/form/webform/components/time.inc index cd715356..aaedb738 100644 --- a/sites/all/modules/contrib/form/webform/components/time.inc +++ b/sites/all/modules/contrib/form/webform/components/time.inc @@ -18,14 +18,18 @@ function _webform_defaults_time() { 'pid' => 0, 'weight' => 0, 'value' => '', - 'mandatory' => 0, + 'required' => 0, 'extra' => array( 'timezone' => 'user', + 'start_time' => '', + 'end_time' => '', 'hourformat' => '12-hour', 'minuteincrements' => 1, 'title_display' => 0, 'description' => '', + 'description_above' => FALSE, 'private' => FALSE, + 'analysis' => FALSE, ), ); } @@ -60,11 +64,29 @@ function _webform_edit_time($component) { '#maxlength' => 127, '#weight' => 0, ); + $form['validation']['start_time'] = array( + '#type' => 'textfield', + '#title' => t('Start time'), + '#default_value' => $component['extra']['start_time'], + '#description' => t('The earliest time that may be entered into the field.'), + '#size' => 10, + '#weight' => 3, + '#parents' => array('extra', 'start_time'), + ); + $form['validation']['end_time'] = array( + '#type' => 'textfield', + '#title' => t('End time'), + '#default_value' => $component['extra']['end_time'], + '#description' => t('The latest time that may be entered into the field.'), + '#size' => 10, + '#weight' => 4, + '#parents' => array('extra', 'end_time'), + ); $form['extra']['timezone'] = array( '#type' => 'radios', '#title' => t('Default value timezone'), '#default_value' => $component['extra']['timezone'], - '#description' => t('If using relative dates for a default value (e.g. "now") base the current time on this timezone.'), + '#description' => t('If using relative dates for a default value (for example, "now") base the current time on this timezone.'), '#options' => array('user' => t('User timezone'), 'site' => t('Website timezone')), '#weight' => 2, '#access' => variable_get('configurable_timezones', 1), @@ -91,26 +113,47 @@ function _webform_edit_time($component) { '#weight' => 3, '#parents' => array('extra', 'minuteincrements'), ); + $form['#validate'] = array('_webform_edit_time_validate'); return $form; } +/** + * Implements hook_form_id_validate(). + * + * Validate start and end times. + */ +function _webform_edit_time_validate($form, &$form_state) { + // Validate that the start and end times are valid. Don't validate the default + // time because with token substitution, it might not be valid at component + // definition time. The end time may be before the start time to facilitate + // time ranges spanning midnight. + foreach (array('start_time', 'end_time') as $field) { + $time[$field] = FALSE; + if (trim($form_state['values']['extra'][$field]) && ($time[$field] = strtotime('1-1-1970 UTC ' . $form_state['values']['extra'][$field])) === FALSE) { + form_set_error("extra][$field", t("The @field isn't a valid time.", array('@field' => $form['validation'][$field]['#title']))); + } + } +} + /** * Implements _webform_render_component(). */ -function _webform_render_time($component, $value = NULL, $filter = TRUE) { +function _webform_render_time($component, $value = NULL, $filter = TRUE, $submission = NULL) { $node = isset($component['nid']) ? node_load($component['nid']) : NULL; $element = array( '#type' => 'webform_time', - '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'], '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', - '#required' => $component['mandatory'], + '#required' => $component['required'], '#weight' => $component['weight'], - '#description' => $filter ? _webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], + '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'], '#element_validate' => array('webform_validate_time'), + '#start_time' => trim($component['extra']['start_time']), + '#end_time' => trim($component['extra']['end_time']), '#hourformat' => $component['extra']['hourformat'], '#minuteincrements' => $component['extra']['minuteincrements'], - '#default_value' => $filter ? _webform_filter_values($component['value'], $node, NULL, NULL, FALSE) : $component['value'], + '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'], '#timezone' => $component['extra']['timezone'], '#process' => array('webform_expand_time'), '#theme' => 'webform_time', @@ -149,54 +192,58 @@ function webform_expand_time($element) { 'second' => '', ); } + $start_hour = $element['#start_time'] ? date('G', strtotime('1-1-1970 ' . $element['#start_time'])) : FALSE; + $end_hour = $element['#end_time'] ? date('G', strtotime('1-1-1970 ' . $element['#end_time'])) : FALSE; + $reduced_range = ($start_hour !== FALSE && $start_hour > 0) || ($end_hour !== FALSE && $end_hour < 23); + $format_12_hour = $element['#hourformat'] == '12-hour'; - $first_hour = 0; - $last_hour = 23; - if ($element['#hourformat'] == '12-hour') { - $first_hour = 1; - $last_hour = 12; + // Generate the choices for the hour drop-down select. + $hours = $format_12_hour && !$reduced_range ? array_slice(range(0, 12), 1, 12, TRUE) : range(0, 23); + if ($format_12_hour && $reduced_range) { + $hours = array_map(function ($hour) { + return (1 + ($hour + 11) % 12) . ($hour < 12 ? ' am' : ' pm'); + }, $hours); + } + + // Prune the hours to the allowed range. + if ($reduced_range) { + // $start_hour of FALSE type-juggles nicely to 0. + $end_hour = $end_hour === FALSE ? 23 : $end_hour; + if ($start_hour <= $end_hour) { + $hours = array_intersect_key($hours, array_flip(range($start_hour, $end_hour))); + } + else { + $hours = array_intersect_key($hours, array_flip(range($start_hour, 23))) + + array_intersect_key($hours, array_flip(range(0, $end_hour))); + } + } + + // Generate the choices for the minute drop-down select. + $minutes = range(0, 59, $element['#minuteincrements']); + $minutes = array_combine($minutes, array_map(function ($minute) { + return substr('00' . $minute, -2); + }, $minutes)); + + // Add the labels to the drop-down selects. + $hours = array('' => t('Hour')) + $hours; + $minutes = array('' => t('Minute')) + $minutes; + + // Adjust the default for minutes if needed, rounding down if needed. + // Rounding down eliminate the problem of rounding up going to the next hour. + // Worse, rounding 23:59 up would actually be the next day, which can't be + // represented because time components aren't linked to date components. + if (!isset($minutes[$default_values['minute']])) { + $default_values['minute'] -= $default_values['minute'] % $element['#minuteincrements']; + } + + // Set the overall default value. + if ($default_values['hour'] !== '') { + $element['#default_value'] = webform_date_string($default_values); + } + + // Convert default to 12-hour if needed. + if ($format_12_hour && !$reduced_range) { $default_values = webform_time_convert($default_values, '12-hour'); - $default_values['ampm'] = $default_values['ampm'] ? $default_values['ampm'] : 'am'; - } - - // Generate the choices for drop-down selects. - $hours[''] = t('hour'); - $minutes[''] = t('minute'); - for ($i = $first_hour; $i <= $last_hour; $i++) { - $hours[$i] = $i; - } - for ($i = 0; $i <= 59; $i += $element['#minuteincrements']) { - $minutes[$i] = $i < 10 ? "0$i" : $i; - } - $ampms = array('am' => t('am'), 'pm' => t('pm')); - - // Adjust the default for minutes if needed, rounding up to the closest value. - if (!isset($minutes[$default_values['minute']])) { - foreach ($minutes as $minute => $padded_minute) { - if ($minute > $default_values['minute']) { - $default_values['minute'] = $minute; - break; - } - } - } - - // If the above loop didn't set a value, it's because rounding up would go to - // the next hour. This gets quite a bit more complicated, since we need to - // deal with looping around on hours, as well as flipping am/pm. - if (!isset($minutes[$default_values['minute']])) { - $default_values['minute'] = 0; - $default_values['hour']++; - // If the hour rolls over also, set hour to the first hour in the list. - if (!isset($hours[$default_values['hour']])) { - $default_values['hour'] = $element['#hourformat'] == '12-hour' ? 1 : 0; - } - // If the hour has been incremented to 12:00 in 12-hour format, flip am/pm. - // Note that technically midnight and noon are neither am or pm, but common - // convention (and US standard) is to represent 12:00am as midnight. - // See http://en.wikipedia.org/wiki/Midnight#Start_and_end_of_day. - if ($element['#hourformat'] == '12-hour' && $default_values['hour'] == 12) { - $default_values['ampm'] = $default_values['ampm'] == 'am' ? 'pm' : 'am'; - } } $element['hour'] = array( @@ -215,19 +262,14 @@ function webform_expand_time($element) { '#default_value' => $default_values['minute'], '#options' => $minutes, ); - if (strcmp($element['#hourformat'], '12-hour') == 0) { + if ($format_12_hour && !$reduced_range) { $element['ampm'] = array( '#type' => 'radios', - '#default_value' => $default_values['ampm'], - '#options' => $ampms, + '#default_value' => $default_values['ampm'] ? $default_values['ampm'] : 'am', + '#options' => array('am' => t('am'), 'pm' => t('pm')), ); } - // Set the overall default value. - if ($default_values['hour'] !== '') { - $element['#default_value'] = webform_date_string($default_values); - } - return $element; } @@ -246,29 +288,66 @@ function theme_webform_time($variables) { $element['minute']['#attributes']['class'][] = 'error'; } + // Add HTML5 required attribute, if needed. + if ($element['#required']) { + $element['hour']['#attributes']['required'] = 'required'; + $element['minute']['#attributes']['required'] = 'required'; + if (!empty($element['ampm'])) { + $element['ampm']['am']['#attributes']['required'] = 'required'; + $element['ampm']['pm']['#attributes']['required'] = 'required'; + } + } + $output = '
    ' . drupal_render($element['hour']) . drupal_render($element['minute']) . drupal_render($element['ampm']) . '
    '; return $output; } +/** + * Validate that the time data is valid, calling form_error() if not. + */ function webform_validate_time($element, $form_state) { - $form_key = $element['#webform_component']['form_key']; - $name = $element['#webform_component']['name']; - // Check if the user filled the required fields. - foreach ($element['#hourformat'] == '12-hour' ? array('hour', 'minute', 'ampm') : array('hour', 'minute') as $field_type) { - if ($element[$field_type]['#value'] === '' && $element['#required']) { - form_error($element, t('%field field is required.', array('%field' => $name))); - return; + if ($element['#required']) { + foreach (array('hour', 'minute', 'ampm') as $field_type) { + if (isset($element[$field_type]) && $element[$field_type]['#value'] === '') { + form_error($element, t('!name field is required.', array('!name' => $element['#title']))); + return; + } } } - // Check for a valid time. - if ($element['hour']['#value'] !== '' || $element['minute']['#value'] !== '') { + // Check for a valid time. Allow a minute with no hour as "no time set". + if ($element['hour']['#value'] !== '') { if (!is_numeric($element['hour']['#value']) || !is_numeric($element['minute']['#value']) || (isset($element['ampm']) && $element['ampm']['#value'] === '')) { - form_error($element, t('Entered %name is not a valid time.', array('%name' => $name))); + form_error($element, t('Entered !name is not a valid time.', array('!name' => $element['#title']))); return; } + + // Enforce the start and end times, if any. + $timestamp = strtotime($element['hour']['#value'] . ':' . $element['minute']['#value'] . ' ' . + (isset($element['ampm']) ? $element['ampm']['#value'] : '')); + $start_time = strtotime($element['#start_time']); + $end_time = strtotime($element['#end_time']); + $subs = array( + '@start_time' => $element['#start_time'], + '@end_time' => $element['#end_time'], + ); + if ($start_time !== FALSE && $end_time !== FALSE && $start_time > $end_time) { + // Validate as "over midnight" date range. + if ($end_time < $timestamp && $timestamp < $start_time) { + form_error($element, t('The entered time must be from @start_time to midnight to @end_time.', $subs)); + } + } + else { + // Validate the start and end times are a regular (over noon) time range. + if ($start_time !== FALSE && $timestamp < $start_time) { + form_error($element, t('The entered time must be no earlier than @start_time.', $subs)); + } + if ($end_time !== FALSE && $timestamp > $end_time) { + form_error($element, t('The entered time must be no later than @end_time.', $subs)); + } + } } } @@ -288,7 +367,7 @@ function _webform_submit_time($component, $value) { /** * Implements _webform_display_component(). */ -function _webform_display_time($component, $value, $format = 'html') { +function _webform_display_time($component, $value, $format = 'html', $submission = array()) { $value = webform_date_array(isset($value[0]) ? $value[0] : '', 'time'); if ($component['extra']['hourformat'] == '12-hour') { $value = webform_time_convert($value, '12-hour'); @@ -296,6 +375,7 @@ function _webform_display_time($component, $value, $format = 'html') { return array( '#title' => $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', '#weight' => $component['weight'], '#theme' => 'webform_display_time', '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), @@ -326,15 +406,19 @@ function theme_webform_display_time($variables) { /** * Implements _webform_analysis_component(). */ -function _webform_analysis_time($component, $sids = array()) { +function _webform_analysis_time($component, $sids = array(), $single = FALSE, $join = NULL) { $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) ->fields('wsd', array('no', 'data')) - ->condition('nid', $component['nid']) - ->condition('cid', $component['cid']) - ->orderBy('sid'); + ->condition('wsd.nid', $component['nid']) + ->condition('wsd.cid', $component['cid']) + ->orderBy('wsd.sid'); if (count($sids)) { - $query->condition('sid', $sids, 'IN'); + $query->condition('wsd.sid', $sids, 'IN'); + } + + if ($join) { + $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid'); } $result = $query->execute(); @@ -352,7 +436,10 @@ function _webform_analysis_time($component, $sids = array()) { $nonblanks = count($times); $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks)); $rows[1] = array(t('User entered value'), $nonblanks); - return $rows; + + return array( + 'table_rows' => $rows, + ); } /** @@ -381,7 +468,7 @@ function _webform_csv_headers_time($component, $export_options) { $header = array(); $header[0] = ''; $header[1] = ''; - $header[2] = $component['name']; + $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name']; return $header; } @@ -391,7 +478,8 @@ function _webform_csv_headers_time($component, $export_options) { function _webform_csv_data_time($component, $export_options, $value) { if ($value[0]) { $time = webform_date_array($value[0], 'time'); - if ($component['extra']['hourformat'] == '24-hour') { + // An ISO 8601 time is the same as 24-hour time. + if (!empty($export_options['iso8601_time']) || $component['extra']['hourformat'] == '24-hour') { return sprintf('%02d', $time['hour']) . ':' . sprintf('%02d', $time['minute']); } else { @@ -407,11 +495,12 @@ function _webform_csv_data_time($component, $export_options, $value) { /** * Convert a time between a 24-hour and a 12-hour value. * - * @param $array + * @param array $array * An array of hour, minute, second, and optionally ampm. - * @param $format + * @param string $format * Either 12-hour or 24-hour. - * @return + * + * @return array * An array with hour, minute, second, and ampm (if using "12-hour"). */ function webform_time_convert($array, $format) { diff --git a/sites/all/modules/contrib/form/webform/css/webform-admin.css b/sites/all/modules/contrib/form/webform/css/webform-admin.css index 793e25fa..4044504e 100644 --- a/sites/all/modules/contrib/form/webform/css/webform-admin.css +++ b/sites/all/modules/contrib/form/webform/css/webform-admin.css @@ -24,6 +24,46 @@ margin-left: 20px; } +/* Component table */ +.webform-component-formkey, +.webform-component-value { + max-width: 7em; + word-wrap: break-word; +} + +/* Analysis pages */ +.webform-analysis-component { + width: 33.33%; + margin: 0 -2px; + padding: 0; + display: inline-block; + vertical-align: top; +} +@media all and (max-width: 1024px) { + .webform-analysis-component { + width: 50%; + } +} +@media all and (max-width: 600px) { + .webform-analysis-component { + width: 100%; + } +} +.webform-analysis-component-grid { + width: 100%; + clear: both; +} +.webform-analysis-component-inner { + padding: 12px; + overflow: auto; +} +.webform-analysis-component table { + width: 100%; +} +.webform-analysis-component table td { + white-space: normal; +} + /* Element for selecting components, i.e. included components for e-mails. */ .webform-component-select-wrapper { max-height: 300px; @@ -45,6 +85,9 @@ height: 12px; margin: 0 2px 2px; } +.webform-component-select-suffix { + margin-top: 10px; +} .webform-select-list-format table { border: 1px solid; width: auto; @@ -61,20 +104,6 @@ td.webform-pagebreak { font-weight: bold; } -/* Special theming for the options element widget (if installed) */ -.webform-options-element thead { - display: none; -} -.webform-options-element fieldset { - border: none; - background: none; - margin: 0; - padding: 0; -} -.webform-options-element fieldset legend { - display: none; -} - /* Checkboxes for allowed file extensions */ table.webform-file-extensions td { vertical-align: top; @@ -99,9 +128,6 @@ table.webform-file-extensions input.form-text { .webform-container-inline div.form-item { display: inline; } -.webform-default-value { - color: #999; -} .webform-results-per-page a.selected { font-weight: bold; } @@ -114,6 +140,100 @@ html.js div.webform-position { tr.webform-add-form .tabledrag-changed { display: none; } +#webform-emails tr.webform-add-form, #webform-components tr.webform-add-form { background-color: inherit; } + +/* E-mail configuration */ +html.js .webform-email-mapping { + display: none; +} +td.webform-email-option { + text-align: right; +} + +/* Conditionals */ +.webform-conditional, +.webform-conditional-new { + display: block; + position: relative; + padding-left: 1em; + padding-right: 9em; + margin-left: 3em; + max-width: 750px; +} + +.webform-conditional-new { + text-align: right; + margin-left: 12em; + padding-right: 0; +} + +.webform-conditional-if { + position: absolute; + left: -.5em; + margin-top: .2em; +} + +.webform-conditional-rule { + margin: .5em 0; +} + +.webform-conditional-condition { + display: inline; +} + +.webform-conditional-operations { + position: absolute; + right: 0; + margin-top: .1em; +} + +.webform-andor { + padding-left: 0.5em; +} +.webform-subconditional { + font-size: 1.5em; + line-height: 0; +} + +.webform-indentation { + float: left; + width: 2em; +} + +.webform-conditional-new input[disabled] { + visibility: hidden; +} + +.webform-conditional-new input.form-submit, +.webform-conditional-operations input.form-submit { + margin: 0 2px; + padding: 0 6px 2px 6px; +} + +#webform-conditionals-table input.progress-disabled, +.webform-conditional-operations input.progress-disabled { + float: none; +} + +#webform-conditionals-table .ahah-progress-throbber { + float: none; + display: inline; +} + +#webform-conditionals-table .ahah-progress-throbber .throbber { + float: none; + display: inline; + padding-right: 12px; +} + +.webform-conditional-andor { + display: inline; +} + +.webform-conditional-andor .form-item { + margin: 0; + padding: 0; +} diff --git a/sites/all/modules/contrib/form/webform/css/webform.css b/sites/all/modules/contrib/form/webform/css/webform.css index 036d199a..956209f0 100644 --- a/sites/all/modules/contrib/form/webform/css/webform.css +++ b/sites/all/modules/contrib/form/webform/css/webform.css @@ -28,3 +28,53 @@ html.js input.webform-calendar { .webform-container-inline div.ajax-progress-bar div { display: inherit; } +.webform-container-inline.webform-component-textarea label { + vertical-align: top; +} +.webform-container-inline.webform-component-textarea .form-textarea-wrapper { + display: inline-block; +} +.webform-component-textarea .grippie { + display: block; +} +.webform-progressbar { + width: 90%; + margin: 0 auto; + text-align: center; +} +.webform-progressbar-inner { + height: 1em; + background-color: #74c421; + height: 3px; +} +.webform-progressbar-outer { + position: relative; + border: 1px solid #356900; + width: 100%; + height: 3px; + margin: 0.35em -1px 2em; + background-color: white; +} +.webform-progressbar-page { + position: absolute; + width: 7px; + height: 7px; + margin: -6px -4px; + border: 1px solid #356900; + background-color: white; + border-radius: 5px; +} +.webform-progressbar-page.completed { + background-color: #74c421; +} +.webform-progressbar-page.current { + background-color: #74c421; +} +.webform-progressbar-page .webform-progressbar-page-number { + display: none; +} +.webform-progressbar-page .webform-progressbar-page-label { + position: relative; + top: 10px; + margin: 0 -10em; +} diff --git a/sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter.inc b/sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter.inc new file mode 100644 index 00000000..b7556526 --- /dev/null +++ b/sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter.inc @@ -0,0 +1,103 @@ +options = $options; + $this->export_wordwrap = webform_variable_get('webform_export_wordwrap'); + } + + /** + * Determines whether a cell is eligible for word-wrapping. + * + * This is based upon position in file and the contents of cell. + * + * Return true when the global word-wrapping option is enabled and the cell + * is anything other than the first column in either of the first two rows. + * By default, these rows are long and are intended to overlap the columns + * to the right. Also returns true when the cell contains a return character. + * + * @param int $row + * Row number, counting from 0. + * @param int $column + * Column number, counting from 0. + * @param string $value + * The value of the cell. + * + * @return bool + * Whether the cell position is eligible for wordwrapping. + */ + public function wrappable($row, $column, $value) { + return strpos($value, "\n") !== FALSE || + $this->export_wordwrap && ($row > 2 || $column > 0 || $this->options['header_keys'] < 0); + } + + /** + * Add a single row to the export file. + * + * @param $file_handle + * A PHP file handle to the export file. + * @param array $data + * An array of formatted data for this row. One cell per item. + * @param int $row_count + * The current number of rows in the export file. + */ + public function add_row(&$file_handle, array $data, $row_count) { + } + + /** + * Provide headers to the page when an export file is being downloaded. + * + * @param string $filename + * The name of the file being downloaded, for example, export.xls. + */ + public function set_headers($filename) { + drupal_add_http_header('Content-Type', 'application/force-download'); + drupal_add_http_header('Pragma', 'public'); + drupal_add_http_header('Cache-Control', 'max-age=0'); + } + + /** + * Write the start of the export file. + * + * @param $file_handle + * A PHP file handle to the export file. + */ + public function bof(&$file_handle) { + } + + /** + * Write the end of the export file. + * + * @param $file_handle + * A PHP file handle to the export file. + * @param int $row_count + * @param int $col_count + */ + public function eof(&$file_handle, $row_count, $col_count) { + } + + /** + * Allow final processing of the results. + * + * @param $results + * An array of result data, including: + * - node: The node whose results are being downloaded. + * - file_name: The full file path of the generated file. + * - row_count: The final number of rows in the generated file. + */ + public function post_process(&$results) { + } + +} diff --git a/sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter_delimited.inc b/sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter_delimited.inc new file mode 100644 index 00000000..f0dd6b91 --- /dev/null +++ b/sites/all/modules/contrib/form/webform/includes/exporters/webform_exporter_delimited.inc @@ -0,0 +1,60 @@ +line_ending = webform_variable_get('webform_csv_line_ending'); + $this->delimiter = isset($options['delimiter']) ? $options['delimiter'] : ','; + // Convert tabs. + if ($this->delimiter == '\t') { + $this->delimiter = "\t"; + } + $options['delimiter'] = $this->delimiter; + parent::__construct($options); + } + + /** + * {@inheritdoc} + */ + public function add_row(&$file_handle, array $data, $row_count) { + foreach ($data as $key => $value) { + // Escape inner quotes and wrap all contents in new quotes. + $data[$key] = '"' . str_replace('"', '""', $data[$key]) . '"'; + + // Remove