Browse Source

first commit

master
ppound 3 years ago
commit
666a54805a
  1. 26
      DEVELOPER.txt
  2. 339
      LICENSE.txt
  3. 91
      README.txt
  4. 323
      includes/faces.inc
  5. 2933
      includes/rules.core.inc
  6. 421
      includes/rules.event.inc
  7. 920
      includes/rules.plugins.inc
  8. 387
      includes/rules.processor.inc
  9. 804
      includes/rules.state.inc
  10. 745
      includes/rules.upgrade.inc
  11. 102
      modules/comment.rules.inc
  12. 464
      modules/data.eval.inc
  13. 759
      modules/data.rules.inc
  14. 186
      modules/entity.eval.inc
  15. 591
      modules/entity.rules.inc
  16. 225
      modules/events.inc
  17. 143
      modules/node.eval.inc
  18. 163
      modules/node.rules.inc
  19. 152
      modules/path.eval.inc
  20. 173
      modules/path.rules.inc
  21. 184
      modules/php.eval.inc
  22. 159
      modules/php.rules.inc
  23. 280
      modules/rules_core.eval.inc
  24. 341
      modules/rules_core.rules.inc
  25. 305
      modules/system.eval.inc
  26. 345
      modules/system.rules.inc
  27. 150
      modules/taxonomy.rules.inc
  28. 135
      modules/user.eval.inc
  29. 277
      modules/user.rules.inc
  30. 1103
      rules.api.php
  31. 252
      rules.drush.inc
  32. 91
      rules.features.inc
  33. 36
      rules.info
  34. 563
      rules.install
  35. 1774
      rules.module
  36. 133
      rules.rules.inc
  37. 434
      rules_admin/rules_admin.inc
  38. 16
      rules_admin/rules_admin.info
  39. 135
      rules_admin/rules_admin.module
  40. 376
      rules_admin/tests/rules_admin.test
  41. 66
      rules_admin/tests/rules_admin_minimal_profile.test
  42. 107
      rules_i18n/rules_i18n.i18n.inc
  43. 15
      rules_i18n/rules_i18n.info
  44. 19
      rules_i18n/rules_i18n.install
  45. 133
      rules_i18n/rules_i18n.module
  46. 209
      rules_i18n/rules_i18n.rules.inc
  47. 197
      rules_i18n/rules_i18n.test
  48. 104
      rules_scheduler/includes/rules_scheduler.handler.inc
  49. 84
      rules_scheduler/includes/rules_scheduler.views.inc
  50. 180
      rules_scheduler/includes/rules_scheduler.views_default.inc
  51. 25
      rules_scheduler/includes/rules_scheduler_views_filter.inc
  52. 139
      rules_scheduler/rules_scheduler.admin.inc
  53. 81
      rules_scheduler/rules_scheduler.drush.inc
  54. 19
      rules_scheduler/rules_scheduler.info
  55. 225
      rules_scheduler/rules_scheduler.install
  56. 220
      rules_scheduler/rules_scheduler.module
  57. 217
      rules_scheduler/rules_scheduler.rules.inc
  58. 151
      rules_scheduler/tests/rules_scheduler.test
  59. 24
      rules_scheduler/tests/rules_scheduler_test.inc
  60. 12
      rules_scheduler/tests/rules_scheduler_test.info
  61. 6
      rules_scheduler/tests/rules_scheduler_test.module
  62. 2261
      tests/rules.test
  63. 12
      tests/rules_test.info
  64. 58
      tests/rules_test.module
  65. 382
      tests/rules_test.rules.inc
  66. 124
      tests/rules_test.rules_defaults.inc
  67. 75
      tests/rules_test.test.inc
  68. 11
      tests/rules_test_invocation.info
  69. 13
      tests/rules_test_invocation.module
  70. 202
      ui/rules.autocomplete.js
  71. 68
      ui/rules.debug.js
  72. 44
      ui/rules.ui-rtl.css
  73. 203
      ui/rules.ui.css
  74. 70
      ui/rules.ui.seven-rtl.css
  75. 96
      ui/rules.ui.seven.css
  76. 342
      ui/ui.controller.inc
  77. 1370
      ui/ui.core.inc
  78. 702
      ui/ui.data.inc
  79. 1005
      ui/ui.forms.inc
  80. 264
      ui/ui.plugins.inc
  81. 306
      ui/ui.theme.inc

26
DEVELOPER.txt

@ -0,0 +1,26 @@
---------------------------------------------------
Some notes for developers working on the rules code
---------------------------------------------------
Terminology & Overview
-----------------------
* Rules plugins extend the "rules language". Thus conditions and actions are
implemented with a plugin, but also loops or ORs are plugins. Each plugin is
declared to be used in the condition or the action part - specified by the
interface.
* The action and condition plugin are a so called "AbstractPlugin" which means
they have to be implemented by modules to be actually usable. In fact the
callbacks provided by the action or condition implementation are
incorporated in the plugin object using faces. That way an action or
condition element behaves exactly like any other plugin instance.
* Any rule element is an instance of a RulesPlugin.
* A rules configuration consists of multiple rule elements while one is the
root element, which may be a 'rule' but also any other plugin.
* Each rules configuration may be persistently saved to the db using the
entity CRUD API. Using the API on contained rule elements is working too and
results in the whole configuration being updated.
* Rules provides per plugin UI components, what makes the UI parts re-usable
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.

339
LICENSE.txt

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

91
README.txt

@ -0,0 +1,91 @@
--------------------------------------------------------------------------------
Rules
--------------------------------------------------------------------------------
Maintainers:
* Wolfgang Ziegler (fago), nuppla@zites.net
The Rules module allows site administrators to define conditionally executed
actions based on occurring events (ECA-rules).
Project homepage: https://www.drupal.org/project/rules
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:
https://www.drupal.org/node/298480
* Rules depends on the Entity API module, download and install it from
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.
* The administrative user interface can be found at admin/config/workflow/rules
Documentation
-------------
* 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
---------------
* If you enable the Rules scheduler module, you get new actions that allow you
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
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
--------------------------------------------
* In order to upgrade Rules from 6.x-1.x to 7.x-2.x just run "update.php". This
is going to make sure Rules 2.x is properly installed, but it will leave your
Rules 1.x configurations untouched. Thus, your rules won't be upgraded yet.
* To convert your Rules 1.x configurations to Rules 2.x go to
'admin/config/workflow/rules/upgrade'.
* At this page, you may choose the Rules 1.x rules and rule sets to upgrade
and whether the converted configurations should be immediately saved to
your database or whether the configuration export should be generated.
* 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.
* 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
before you convert the rule.
* If all required modules are installed and have been upgraded but the rule
conversion still fails, the cause might be that a module has not yet
upgraded its Rules integration or does not implement the Rules conversion
functionality. In that case, file an issue for the module that provided
the action or condition causing the conversion to fail.
* Note that any rule configurations containing token replacements or PHP
input evaluations might need some manual corrections in order to stay
working. This is, as some used token replacements might not be available
in Drupal 7 any more and the PHP code might need to be updated in order
to be compatible with Drupal 7.
* Once the upgrade was successful, you may delete the left over Rules 1.x
configurations by going to 'admin/config/workflow/rules/upgrade/clear'.
* The Rules Scheduler module also comes with an upgrade routine that is
invoked as usual via "update.php". Its actions can be upgraded via the usual
Rules upgrade tool, see above.
However, there is currently no support for upgrading already scheduled
tasks. That means, all previously on Drupal 6 scheduled tasks won't apply
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 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.

323
includes/faces.inc

@ -0,0 +1,323 @@
<?php
/**
* @file
* Extendable Object Faces API. Provided by the faces module.
*/
if (!interface_exists('FacesExtenderInterface', FALSE)) {
/**
* Interface for extenders.
*/
interface FacesExtenderInterface {
/**
* Constructs an instance of the extender.
*/
public function __construct(FacesExtendable $object);
/**
* Returns the extended object.
*/
public function getExtendable();
}
/**
* The Exception thrown by the FacesExtendable.
*/
class FacesExtendableException extends ErrorException {}
}
if (!class_exists('FacesExtender', FALSE)) {
/**
* A common base class for FacesExtenders.
*
* Extenders may access protected methods and properties of the extendable
* using the property() and call() methods.
*/
abstract class FacesExtender implements FacesExtenderInterface {
/**
* @var FacesExtendable
*/
protected $object;
public function __construct(FacesExtendable $object) {
$this->object = $object;
}
/**
* Returns the extended object.
*/
public function getExtendable() {
return $this->object;
}
/**
* Makes protected properties of the extendable accessible.
*/
protected function &property($name) {
$var =& $this->object->property($name);
return $var;
}
/**
* Invokes any method on the extended object, including protected methods.
*
* @param string $name
* The method name.
* @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);
}
}
}
if (!class_exists('FacesExtendable', FALSE)) {
/**
* An extendable base class.
*/
abstract class FacesExtendable {
protected $facesMethods = array();
protected $faces = array();
protected $facesIncludes = array();
protected $facesClassInstances = array();
static protected $facesIncluded = array();
/**
* Wraps calls to module_load_include() to prevent multiple inclusions.
*
* @see module_load_include()
*/
protected static function load_include($args) {
$args += array('type' => 'inc', 'module' => '', 'name' => NULL);
$key = implode(':', $args);
if (!isset(self::$facesIncluded[$key])) {
self::$facesIncluded[$key] = TRUE;
module_load_include($args['type'], $args['module'], $args['name']);
}
}
/**
* Magic method: Invoke the dynamically implemented methods.
*/
public function __call($name, $arguments = array()) {
if (isset($this->facesMethods[$name])) {
$method = $this->facesMethods[$name];
// Include code, if necessary.
if (isset($this->facesIncludes[$name])) {
self::load_include($this->facesIncludes[$name]);
$this->facesIncludes[$name] = NULL;
}
if (isset($method[0])) {
// We always pass the object reference and the name of the method.
$arguments[] = $this;
$arguments[] = $name;
return call_user_func_array($method[0], $arguments);
}
// Call the method on the extender object, but don't use extender()
// for performance reasons.
if (!isset($this->facesClassInstances[$method[1]])) {
$this->facesClassInstances[$method[1]] = new $method[1]($this);
}
return call_user_func_array(array($this->facesClassInstances[$method[1]], $name), $arguments);
}
$class = check_plain(get_class($this));
throw new FacesExtendableException("There is no method $name for this instance of the class $class.");
}
/**
* 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])) {
$this->facesClassInstances[$class] = new $class($this);
}
return $this->facesClassInstances[$class];
}
/**
* 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 object has been extended by an appropriate
* implementation.
*
* @param $interface
* (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.
*/
public function facesAs($interface = NULL) {
if (!isset($interface)) {
return array_values($this->faces);
}
return in_array($interface, $this->faces) || $this instanceof $interface;
}
/**
* Extend the object by a class to implement the given interfaces.
*
* @param $interface
* The interface name or an array of interface names.
* @param $className
* The extender class, which has to implement the FacesExtenderInterface.
* @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
* required as 'type' defaults to 'inc' and 'name' to NULL.
*/
public function extendByClass($interface, $className, array $includes = array()) {
$parents = class_implements($className);
if (!in_array('FacesExtenderInterface', $parents)) {
throw new FacesExtendableException("The class " . check_plain($className) . " doesn't implement the FacesExtenderInterface.");
}
$interfaces = is_array($interface) ? $interface : array($interface);
foreach ($interfaces as $interface) {
if (!in_array($interface, $parents)) {
throw new FacesExtendableException("The class " . check_plain($className) . " doesn't implement the interface " . check_plain($interface) . ".");
}
$this->faces[$interface] = $interface;
$this->faces += class_implements($interface);
$face_methods = get_class_methods($interface);
$this->addIncludes($face_methods, $includes);
foreach ($face_methods as $method) {
$this->facesMethods[$method] = array(1 => $className);
}
}
}
/**
* Extend the object by the given functions to implement the given
* interface. There has to be an implementation function for each method of
* the interface.
*
* @param $interface
* The interface name or FALSE to extend the object without a given
* interface.
* @param array $callbacks
* An array, where the keys are methods of the given interface and the
* values the callback functions to use.
* @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
* extendByClass() $include parameter for more details about how to
* describe a single file.
*/
public function extend($interface, array $callbacks = array(), array $includes = array()) {
$face_methods = $interface ? get_class_methods($interface) : array_keys($callbacks);
if ($interface) {
if (array_diff($face_methods, array_keys($callbacks))) {
throw new FacesExtendableException("Missing methods for implementing the interface " . check_plain($interface) . ".");
}
$this->faces[$interface] = $interface;
$this->faces += class_implements($interface);
}
$this->addIncludes($face_methods, $includes);
foreach ($face_methods as $method) {
$this->facesMethods[$method] = array(0 => $callbacks[$method]);
}
}
/**
* Override the implementation of an extended method.
*
* @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 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
* extendByClass() $include parameter for more details about how to
* describe a single file.
*/
public function override(array $callbacks = array(), array $includes = array()) {
if (array_diff_key($callbacks, $this->facesMethods)) {
throw new FacesExtendableException("A not implemented method is to be overridden.");
}
$this->addIncludes(array_keys($callbacks), $includes);
foreach ($callbacks as $method => $callback) {
$this->facesMethods[$method] = array(0 => $callback);
}
}
/**
* 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) {
$includes = isset($includes['module']) && is_string($includes['module']) ? array_fill_keys($methods, $includes) : $includes;
$this->facesIncludes = $includes + array_diff_key($this->facesIncludes, array_flip($methods));
}
/**
* Only serialize what is really necessary.
*/
public function __sleep() {
return array('facesMethods', 'faces', 'facesIncludes');
}
/**
* 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.
*/
public function destroy() {
// Avoid circular references.
$this->facesClassInstances = array();
}
/**
* Makes protected properties accessible.
*/
public function &property($name) {
if (property_exists($this, $name)) {
return $this->$name;
}
}
/**
* Invokes any method.
*
* This also allows to pass arguments by reference, so it may be used to
* pass arguments by reference to dynamically extended methods.
*
* @param string $name
* The method name.
* @param array $args
* An array of arguments to pass to the method.
*/
public function call($name, array $args = array()) {
if (method_exists($this, $name)) {
return call_user_func_array(array($this, $name), $args);
}
return $this->__call($name, $args);
}
}
}

2933
includes/rules.core.inc

File diff suppressed because it is too large Load Diff

421
includes/rules.event.inc

@ -0,0 +1,421 @@
<?php
/**
* @file
* Contains event handler interface and base classes.
*/
/**
* Interface for handling rules events.
*
* Configurable events (i.e. events making use of settings) have a custom
* event suffix, which gets appended to the base event name. The configured
* event name of, e.g. the event for viewing an article node, would be
* node_view--article, whereas "node_view" is the base event name and "article"
* the event suffix as returned from
* RulesEventHandlerInterface::getEventNameSuffix(). The event suffix is
* generated based upon the event settings and must map to this settings, i.e.
* each set of event settings must always generate the same suffix.
* For a configurable event to be invoked, rules_invoke_event() has to be called
* with the configured event name, e.g.
* @code
* rules_invoke_event('node_view--' . $node->type, $node, $view_mode);
* @endcode
* If the event settings are optional, both events have to be invoked whereas
* usually the more general event is invoked last. E.g.:
* @code
* rules_invoke_event('node_view--' . $node->type, $node, $view_mode);
* rules_invoke_event('node_view', $node, $view_mode);
* @endcode
*
* Rules event handlers have to be declared using the 'class' key in
* hook_rules_event_info(), or may be discovered automatically, see
* rules_discover_plugins() for details.
*
* @see RulesEventHandlerBase
* @see RulesEventDefaultHandler
*/
interface RulesEventHandlerInterface {
/**
* Constructs the event handler.
*
* @param string $event_name
* The base event string.
* @param array $info
* The event info of the given event.
*/
public function __construct($event_name, $info);
/**
* Sets the event settings.
*
* @param array $settings
* An array of settings to set.
*
* @return RulesEventHandlerInterface
* The handler itself for chaining.
*/
public function setSettings(array $settings);
/**
* Gets the event settings.
*
* @return array
* The array of settings.
*/
public function getSettings();
/**
* Returns an array of default settings.
*
* @return array
* The array of default settings.
*/
public function getDefaults();
/**
* Returns a user-facing summary of the settings.
*
* @return string
* The summary in HTML, i.e. properly escaped or filtered.
*/
public function summary();
/**
* Builds the event settings form.
*
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return array
* The form structure.
*/
public function buildForm(array &$form_state);
/**
* Validate the event settings independent from a form submission.
*
* @throws RulesIntegrityException
* In case of validation errors, RulesIntegrityExceptions are thrown.
*/
public function validate();
/**
* Extract the form values and update the event settings.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* An associative array containing the current state of the form.
*/
public function extractFormValues(array &$form, array &$form_state);
/**
* Returns the suffix to be added to the base event named based upon settings.
*
* If event settings are used, the event name Rules uses for the configured
* event is {EVENT_NAME}--{SUFFIX}.
*
* @return string
* The suffix string. Return an empty string for not appending a suffix.
*/
public function getEventNameSuffix();
/**
* Returns info about the variables provided by this event.
*
* @return array
* An array of provided variables, keyed by variable names and with the
* variable info array as value.
*/
public function availableVariables();
/**
* Returns the base name of the event the event handler belongs to.
*
* @return string
* The name of the event the event handler belongs to.
*/
public function getEventName();
/**
* Returns the info array of the event the event handler belongs to.
*
* @return string
* The info array of the event the event handler belongs to.
*/
public function getEventInfo();
}
/**
* Interface for event dispatchers.
*/
interface RulesEventDispatcherInterface extends RulesEventHandlerInterface {
/**
* Starts the event watcher.
*/
public function startWatching();
/**
* Stops the event watcher.
*/
public function stopWatching();
/**
* Returns whether the event dispatcher is currently active.
*
* @return bool
* TRUE if the event dispatcher is currently active, FALSE otherwise.
*/
public function isWatching();
}
/**
* Base class for event handler.
*/
abstract class RulesEventHandlerBase implements RulesEventHandlerInterface {
/**
* The event name.
*
* @var string
*/
protected $eventName;
/**
* The event info.
*
* @var array
*/
protected $eventInfo;
/**
* The event settings.
*
* @var array
*/
protected $settings = array();
/**
* Implements RulesEventHandlerInterface::__construct().
*/
public function __construct($event_name, $info) {
$this->eventName = $event_name;
$this->eventInfo = $info;
$this->settings = $this->getDefaults();
}
/**
* Implements RulesEventHandlerInterface::getSettings().
*/
public function getSettings() {
return $this->settings;
}
/**
* Implements RulesEventHandlerInterface::setSettings().
*/
public function setSettings(array $settings) {
$this->settings = $settings + $this->getDefaults();
return $this;
}
/**
* Implements RulesEventHandlerInterface::validate().
*/
public function validate() {
// Nothing to check by default.
}
/**
* Implements RulesEventHandlerInterface::extractFormValues().
*/
public function extractFormValues(array &$form, array &$form_state) {
foreach ($this->getDefaults() as $key => $setting) {
$this->settings[$key] = isset($form_state['values'][$key]) ? $form_state['values'][$key] : $setting;
}
}
/**
* Implements RulesEventHandlerInterface::availableVariables().
*/
public function availableVariables() {
return isset($this->eventInfo['variables']) ? $this->eventInfo['variables'] : array();
}
/**
* Implements RulesEventHandlerInterface::getEventName().
*/
public function getEventName() {
return $this->eventName;
}
/**
* 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 {
/**
* Implements RulesEventHandlerInterface::buildForm().
*/
public function buildForm(array &$form_state) {
return array();
}
/**
* Implements RulesEventHandlerInterface::getConfiguredEventName().
*/
public function getEventNameSuffix() {
return '';
}
/**
* Implements RulesEventHandlerInterface::summary().
*/
public function summary() {
return check_plain($this->eventInfo['label']);
}
/**
* Implements RulesEventHandlerInterface::getDefaults().
*/
public function getDefaults() {
return array();
}
/**
* Implements RulesEventHandlerInterface::getSettings().
*/
public function getSettings() {
return NULL;
}
}
/**
* Exposes the bundle of an entity as event setting.
*/
class RulesEventHandlerEntityBundle extends RulesEventHandlerBase {
protected $entityType;
protected $entityInfo;
protected $bundleKey;
/**
* Implements RulesEventHandlerInterface::__construct().
*/
public function __construct($event_name, $info) {
parent::__construct($event_name, $info);
// Cut off the suffix, e.g. remove 'view' from node_view.
$this->entityType = implode('_', explode('_', $event_name, -1));
$this->entityInfo = entity_get_info($this->entityType);
if (!$this->entityInfo) {
throw new InvalidArgumentException('Unsupported event name passed.');
}
}
/**
* Implements RulesEventHandlerInterface::summary().
*/
public function summary() {
$bundle = &$this->settings['bundle'];
$bundle_label = isset($this->entityInfo['bundles'][$bundle]['label']) ? $this->entityInfo['bundles'][$bundle]['label'] : $bundle;
$suffix = isset($bundle) ? ' ' . t('of @bundle-key %name', array('@bundle-key' => $this->getBundlePropertyLabel(), '%name' => $bundle_label)) : '';
return check_plain($this->eventInfo['label']) . $suffix;
}
/**
* Implements RulesEventHandlerInterface::buildForm().
*/
public function buildForm(array &$form_state) {
$form['bundle'] = array(
'#type' => 'select',
'#title' => t('Restrict by @bundle', array('@bundle' => $this->getBundlePropertyLabel())),
'#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'];
}
return $form;
}
/**
* 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().
*/
public function extractFormValues(array &$form, array &$form_state) {
$this->settings['bundle'] = !empty($form_state['values']['bundle']) ? $form_state['values']['bundle'] : NULL;
}
/**
* Implements RulesEventHandlerInterface::validate().
*/
public function validate() {
if ($this->settings['bundle'] && empty($this->entityInfo['bundles'][$this->settings['bundle']])) {
throw new RulesIntegrityException(t('The @bundle %bundle of %entity_type is not known.',
array(
'%bundle' => $this->settings['bundle'],
'%entity_type' => $this->entityInfo['label'],
'@bundle' => $this->getBundlePropertyLabel(),
)), array(NULL, 'bundle'));
}
}
/**
* Implements RulesEventHandlerInterface::getConfiguredEventName().
*/
public function getEventNameSuffix() {
return $this->settings['bundle'];
}
/**
* Implements RulesEventHandlerInterface::getDefaults().
*/
public function getDefaults() {
return array(
'bundle' => NULL,
);
}
/**
* Implements RulesEventHandlerInterface::availableVariables().
*/
public function availableVariables() {
$variables = $this->eventInfo['variables'];
if ($this->settings['bundle']) {
// Add the bundle to all variables of the entity type.
foreach ($variables as $name => $variable_info) {
if ($variable_info['type'] == $this->entityType) {
$variables[$name]['bundle'] = $this->settings['bundle'];
}
}
}
return $variables;
}
}

920
includes/rules.plugins.inc

@ -0,0 +1,920 @@
<?php
/**
* @file
* Contains plugin info and implementations not needed for rule evaluation.
*/
/**
* Implements a rules action.
*/
class RulesAction extends RulesAbstractPlugin implements RulesActionInterface {
/**
* @var string
*/
protected $itemName = 'action';
/**
* Execute the callback and update/save data as specified by the action.
*/
protected function executeCallback(array $args, RulesState $state = NULL) {
rules_log('Evaluating the action %name.', array(
'%name' => $this->label($this->elementName),
), RulesLog::INFO, $this);
$return = $this->__call('execute', empty($this->info['named parameter']) ? $args : array($args));
// Get the (partially) wrapped arguments.
$args = $state->currentArguments;
if (is_array($return)) {
foreach ($return as $name => $data) {
// Add provided variables.
if (isset($this->info['provides'][$name])) {
$var_name = isset($this->settings[$name . ':var']) ? $this->settings[$name . ':var'] : $name;
if (!$state->varInfo($var_name)) {
$state->addVariable($var_name, $data, $this->info['provides'][$name]);
rules_log('Added the provided variable %name of type %type', array('%name' => $var_name, '%type' => $this->info['provides'][$name]['type']), RulesLog::INFO, $this);
if (!empty($this->info['provides'][$name]['save']) && $state->variables[$var_name] instanceof EntityMetadataWrapper) {
$state->saveChanges($var_name, $state->variables[$var_name]);
}
}
}
// Support updating variables by returning the values.
elseif (!isset($this->info['provides'][$name])) {
// Update the data value using the wrapper.
if (isset($args[$name]) && $args[$name] instanceof EntityMetadataWrapper) {
try {
$args[$name]->set($data);
}
catch (EntityMetadataWrapperException $e) {
throw new RulesEvaluationException('Unable to update the argument for parameter %name: %error', array('%name' => $name, '%error' => $e->getMessage()), $this);
}
}
elseif (array_key_exists($name, $args)) {
// Map back to the source variable name and update it.
$var_name = !empty($this->settings[$name . ':select']) ? str_replace('-', '_', $this->settings[$name . ':select']) : $name;
$state->variables[$var_name] = $data;
}
}
}
}
// Save parameters as defined in the parameter info.
if ($return !== FALSE) {
foreach ($this->info['parameter'] as $name => $info) {
if (!empty($info['save']) && $args[$name] instanceof EntityMetadataWrapper) {
if (isset($this->settings[$name . ':select'])) {
$state->saveChanges($this->settings[$name . ':select'], $args[$name]);
}
else {
// Wrapper has been configured via direct input, so just save.
rules_log('Saved argument of type %type for parameter %name.', array('%name' => $name, '%type' => $args[$name]->type()));
$args[$name]->save();
}
}
}
}
}
}
/**
* Implements a rules condition.
*/
class RulesCondition extends RulesAbstractPlugin implements RulesConditionInterface {
/**
* @var string
*/
protected $itemName = 'condition';
/**
* @var bool
*/
protected $negate = FALSE;
public function providesVariables() {
return array();
}
public function negate($negate = TRUE) {
$this->negate = (bool) $negate;
return $this;
}
public function isNegated() {
return $this->negate;
}
protected function executeCallback(array $args, RulesState $state = NULL) {
$return = (bool) $this->__call('execute', empty($this->info['named parameter']) ? $args : array($args));
$return = $this->negate ? !$return : $return;
rules_log('The condition %name evaluated to %bool', array(
'%name' => $this->label($this->elementName),
'%bool' => $return ? 'TRUE' : 'FALSE',
), RulesLog::INFO, $this);
return $return;
}
public function __sleep() {
return parent::__sleep() + array('negate' => 'negate');
}
/**
* Just return the boolean result.
*/
protected function returnVariables(RulesState $state, $result = NULL) {
return $result;
}
protected function exportToArray() {
$not = $this->negate ? 'NOT ' : '';
$export = $this->exportSettings();
// Abbreviate the export making "USING" implicit.
return array($not . $this->elementName => isset($export['USING']) ? $export['USING'] : array());
}
public function import(array $export) {
$this->elementName = rules_array_key($export);
if (strpos($this->elementName, 'NOT ') === 0) {
$this->elementName = substr($this->elementName, 4);
$this->negate = TRUE;
}
// After setting the element name, setup the element again so the right
// element info is loaded.
$this->setUp();
// Re-add 'USING' which has been removed for abbreviation.
$this->importSettings(array('USING' => reset($export)));
}
public function label() {
$label = parent::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()) {
parent::__construct($variables, $providesVars);
// Initialize the conditions container.
if (!isset($this->conditions)) {
$this->conditions = rules_and();
// Don't use setParent() to avoid having it added to the children.
$this->conditions->parent = $this;
}
}
/**
* Gets an iterator over all contained conditions.
*
* Note that this iterator also implements the ArrayAccess interface.
*
* @return RulesRecursiveElementIterator
*/
public function conditions() {
return $this->conditions->getIterator();
}
/**
* Returns the "And" condition container, which contains all conditions of
* this rule.
*
* @return RulesAnd
*/
public function conditionContainer() {
return $this->conditions;
}
public function __sleep() {
return parent::__sleep() + drupal_map_assoc(array('conditions', 'label'));
}
/**
* Gets an iterator over all contained actions.
*
* Note that this iterator also implements the ArrayAccess interface.
*
* @return RulesRecursiveElementIterator
*/
public function actions() {
return parent::getIterator();
}
/**
* Adds a condition.
*
* 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);
return $this;
}
public function sortChildren($deep = FALSE) {
$this->conditions->sortChildren($deep);
parent::sortChildren($deep);
}
public function evaluate(RulesState $state) {
rules_log('Evaluating conditions of rule %label.', array('%label' => $this->label), RulesLog::INFO, $this);
if ($this->conditions->evaluate($state)) {
rules_log('Rule %label fires.', array('%label' => $this->label), RulesLog::INFO, $this, TRUE);
parent::evaluate($state);
rules_log('Rule %label has fired.', array('%label' => $this->label), RulesLog::INFO, $this, FALSE);
}
}
/**
* Fires the rule, i.e. evaluates the rule without checking its conditions.
*
* @see RulesPlugin::evaluate()
*/
public function fire(RulesState $state) {
rules_log('Firing rule %label.', array('%label' => $this->label), RulesLog::INFO, $this);
parent::evaluate($state);
}
public function integrityCheck() {
parent::integrityCheck();
$this->conditions->integrityCheck();
return $this;
}
public function access() {
return (!isset($this->conditions) || $this->conditions->access()) && parent::access();
}
public function dependencies() {
return array_keys(array_flip($this->conditions->dependencies()) + array_flip(parent::dependencies()));
}
public function destroy() {
$this->conditions->destroy();
parent::destroy();
}
/**
* @return RulesRecursiveElementIterator
*/
public function getIterator() {
$array = array_merge(array($this->conditions), $this->children);
return new RulesRecursiveElementIterator($array);
}
protected function stateVariables($element = NULL) {
// Don't add in provided action variables for the conditions.
if (isset($element) && $element === $this->conditions) {
return $this->availableVariables();
}
$vars = parent::stateVariables($element);
// Take variable info assertions of conditions into account.
if ($assertions = $this->conditions->variableInfoAssertions()) {
$vars = RulesData::addMetadataAssertions($vars, $assertions);
}
return $vars;
}
protected function exportFlat() {
return $this->isRoot();
}
protected function exportToArray() {
$export = parent::exportToArray();
if (!$this->isRoot()) {
$export[strtoupper($this->plugin())]['LABEL'] = $this->label;
}
return $export;
}
protected function exportChildren($key = NULL) {
$export = array();
if ($this->conditions->children) {
$export = $this->conditions->exportChildren('IF');
}
return $export + parent::exportChildren('DO');
}
public function import(array $export) {
if (!$this->isRoot() && isset($export[strtoupper($this->plugin())]['LABEL'])) {
$this->label = $export[strtoupper($this->plugin())]['LABEL'];
}
parent::import($export);
}
protected function importChildren($export, $key = NULL) {
if (!empty($export['IF'])) {
$this->conditions->importChildren($export, 'IF');
}
parent::importChildren($export, 'DO');
}
public function __clone() {
parent::__clone();
$this->conditions = clone $this->conditions;
$this->conditions->parent = $this;
}
/**
* Overrides RulesPlugin::variableInfoAssertions().
*
* Rules may not provide any variable info assertions, as Rules are only
* conditionally executed.
*/
protected function variableInfoAssertions() {
return array();
}
/**
* Overridden to ensure the whole Rule is deleted at once.
*/
public function delete($keep_children = FALSE) {
parent::delete($keep_children);
}
/**
* Overridden to expose the variables of all actions for embedded rules.
*/
public function providesVariables() {
$provides = parent::providesVariables();
if (!$this->isRoot()) {
foreach ($this->actions() as $action) {
$provides += $action->providesVariables();
}
}
return $provides;
}
public function resetInternalCache() {
parent::resetInternalCache();
$this->conditions->resetInternalCache();
}
}
/**
* Represents rules getting triggered by events.
*/
class RulesReactionRule extends Rule implements RulesTriggerableInterface {
/**
* @var string
*/
protected $itemName = 'reaction rule';
/**
* @var array
*/
protected $events = array();
/**
* @var array
*/
protected $eventSettings = array();
/**
* Implements RulesTriggerableInterface::events().
*/
public function events() {
return $this->events;
}
/**
* Implements RulesTriggerableInterface::removeEvent().
*/
public function removeEvent($event) {
if (($id = array_search($event, $this->events)) !== FALSE) {
unset($this->events[$id]);
}
return $this;
}
/**
* Implements RulesTriggerableInterface::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;
}
/**
* Reaction rules can't add variables to the parent scope, so clone $state.
*/
public function evaluate(RulesState $state) {
// Implement recursion prevention for reaction rules.
if ($state->isBlocked($this)) {
return rules_log('Not evaluating @plugin %label to prevent recursion.', array('%label' => $this->label(), '@plugin' => $this->plugin()), RulesLog::INFO, $this);
}
$state->block($this);
$copy = clone $state;
parent::evaluate($copy);
$state->unblock($this);
}
public function access() {
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;
}
}
return parent::access();
}
public function dependencies() {
$modules = array_flip(parent::dependencies());
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);
}
public function providesVariables() {
return array();
}
public function parameterInfo($optional = FALSE) {
// If executed directly, all variables as defined by the event need to
// be passed.
return rules_filter_array($this->availableVariables(), 'handler', FALSE);
}
public function availableVariables() {
if (!isset($this->availableVariables)) {
if (isset($this->parent)) {
// Return the event variables provided by the event set, once cached.
$this->availableVariables = $this->parent->stateVariables();
}
else {
// The intersection of the variables provided by the events are
// available.
foreach ($this->events as $event_name) {
$handler = rules_get_event_handler($event_name, $this->getEventSettings($event_name));
if (isset($this->availableVariables)) {
$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 = $handler->availableVariables();
}
}
$this->availableVariables = isset($this->availableVariables) ? RulesState::defaultVariables() + $this->availableVariables : RulesState::defaultVariables();
}
}
return $this->availableVariables;
}
public function __sleep() {
return parent::__sleep() + drupal_map_assoc(array('events', 'eventSettings'));
}
protected function exportChildren($key = 'ON') {
$export = array();
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') {
// Detect and support old-style exports: a numerically indexed array of
// event names.
if (isset($export[$key])) {
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();
}
}
/**
* A logical AND.
*/
class RulesAnd extends RulesConditionContainer {
/**
* @var string
*/
protected $itemName = 'and';
public function evaluate(RulesState $state) {
foreach ($this->children as $condition) {
if (!$condition->evaluate($state)) {
rules_log('%condition evaluated to %bool.', array(
'%condition' => $this->label(),
'%bool' => 'FALSE',
));
return $this->negate;
}
}
rules_log('%condition evaluated to %bool.', array(
'%condition' => $this->label(),
'%bool' => 'TRUE',
));
return !$this->negate;
}
public function label() {
return !empty($this->label) ? $this->label : ($this->negate ? t('NOT AND') : t('AND'));
}
}
/**
* A logical OR.
*/
class RulesOr extends RulesConditionContainer {
/**
* @var string
*/
protected $itemName = 'or';
public function evaluate(RulesState $state) {
foreach ($this->children as $condition) {
if ($condition->evaluate($state)) {
rules_log('%condition evaluated to %bool.', array(
'%condition' => $this->label(),
'%bool' => 'TRUE',
));
return !$this->negate;
}
}
rules_log('%condition evaluated to %bool.', array(
'%condition' => $this->label(),
'%bool' => 'FALSE',
));
return $this->negate;
}
public function label() {
return !empty($this->label) ? $this->label : ($this->negate ? t('NOT OR') : t('OR'));
}
/**
* Overrides RulesContainerPlugin::stateVariables().
*
* Overridden to exclude all variable assertions as in an OR we cannot assert
* the children are successfully evaluated.
*/
protected function stateVariables($element = NULL) {
$vars = $this->availableVariables();
if (isset($element)) {
// Add in variables provided by siblings executed before the element.
foreach ($this->children as $child) {
if ($child === $element) {
break;
}
$vars += $child->providesVariables();
}
}
return $vars;
}
}
/**
* A loop element.
*/
class RulesLoop extends RulesActionContainer {
/**
* @var string
*/
protected $itemName = 'loop';
protected $listItemInfo;
public function __construct($settings = array(), $variables = NULL) {
$this->setUp();
$this->settings = (array) $settings + array(
'item:var' => 'list_item',
'item:label' => t('Current list item'),
);
if (!empty($variables)) {
$this->info['variables'] = $variables;
}
}
public function pluginParameterInfo() {
$info['list'] = array(
'type' => 'list',
'restriction' => 'selector',
'label' => t('List'),
'description' => t('The list to loop over. The loop will step through each item in the list, allowing further actions on them. See <a href="@url"> the online handbook</a> for more information on how to use loops.',
array('@url' => rules_external_help('loops'))),
);
return $info;
}
public function integrityCheck() {
parent::integrityCheck();
$this->checkVarName($this->settings['item:var']);
}
public function listItemInfo() {
if (!isset($this->listItemInfo)) {
if ($info = $this->getArgumentInfo('list')) {
// Pass through the variable info keys like property info.
$this->listItemInfo = array_intersect_key($info, array_flip(array('type', 'property info', 'bundle')));
$this->listItemInfo['type'] = isset($info['type']) ? entity_property_list_extract_type($info['type']) : 'unknown';
}
else {
$this->listItemInfo = array('type' => 'unknown');
}
$this->listItemInfo['label'] = $this->settings['item:label'];
}
return $this->listItemInfo;
}
public function evaluate(RulesState $state) {
try {
$param_info = $this->pluginParameterInfo();
$list = $this->getArgument('list', $param_info['list'], $state);
$item_var_info = $this->listItemInfo();
$item_var_name = $this->settings['item:var'];
if (isset($this->settings['list:select'])) {
rules_log('Looping over the list items of %selector', array('%selector' => $this->settings['list:select']), RulesLog::INFO, $this);
}
// Loop over the list and evaluate the children for each list item.
foreach ($list as $key => $item) {
// Use a separate state so variables are available in the loop only.
$state2 = clone $state;
$state2->addVariable($item_var_name, $list[$key], $item_var_info);
parent::evaluate($state2);
// Update variables from parent scope.
foreach ($state->variables as $var_key => &$var_value) {
if (array_key_exists($var_key, $state2->variables)) {
$var_value = $state2->variables[$var_key];
}
}
}
}
catch (RulesEvaluationException $e) {
rules_log($e->msg, $e->args, $e->severity);
rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this);
}
}
protected function stateVariables($element = NULL) {
return array($this->settings['item:var'] => $this->listItemInfo()) + parent::stateVariables($element);
}
public function label() {
return !empty($this->label) ? $this->label : t('Loop');
}
protected function exportChildren($key = 'DO') {
return parent::exportChildren($key);
}
protected function importChildren($export, $key = 'DO') {
parent::importChildren($export, $key);
}
protected function exportSettings() {
$export = parent::exportSettings();
$export['ITEM'][$this->settings['item:var']] = $this->settings['item:label'];
return $export;
}
protected function importSettings($export) {
parent::importSettings($export);
if (isset($export['ITEM'])) {
$this->settings['item:var'] = rules_array_key($export['ITEM']);
$this->settings['item:label'] = reset($export['ITEM']);
}
}
}
/**
* An action set component.
*/
class RulesActionSet extends RulesActionContainer {
/**
* @var string
*/
protected $itemName = 'action set';
}
/**
* A set of rules to execute upon defined variables.
*/
class RulesRuleSet extends RulesActionContainer {
/**
* @var string
*/
protected $itemName = 'rule set';
/**
* @return RulesRuleSet
*/
public function rule($rule) {
return $this->action($rule);
}
protected function exportChildren($key = 'RULES') {
return parent::exportChildren($key);
}
protected function importChildren($export, $key = 'RULES') {
parent::importChildren($export, $key);
}
}
/**
* This class is used for caching the rules to be evaluated per event.
*/
class RulesEventSet extends RulesRuleSet {
/**
* @var string
*/
protected $itemName = 'event set';
/**
* Event sets may recurse as we block recursions on rule-level.
*
* @var bool
*/
public $recursion = TRUE;
public function __construct($info = array()) {
$this->setup();
$this->info = $info;
}
public function executeByArgs($args = array()) {
rules_log('Reacting on event %label.', array('%label' => $this->info['label']), RulesLog::INFO, NULL, TRUE);
$state = $this->setUpState($args);
module_invoke_all('rules_config_execute', $this);
$this->evaluate($state);
$state->cleanUp($this);
rules_log('Finished reacting on event %label.', array('%label' => $this->info['label']), RulesLog::INFO, NULL, FALSE);
}
/**
* 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()
*/
public static function rebuildEventCache() {
// Set up the per-event cache.
$events = rules_fetch_data('event_info');
$sets = array();
// Add all rules associated with this event to an EventSet for caching.
$rules = rules_config_load_multiple(FALSE, array('plugin' => 'reaction rule', 'active' => TRUE));
foreach ($rules as $name => $rule) {
foreach ($rule->events() as $event_name) {
$event_base_name = rules_get_event_base_name($event_name);
// Skip not defined events.
if (empty($events[$event_base_name])) {
continue;
}
// Create an event set if not yet done.
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.
if ($rule->dirty) {
rules_config_update_dirty_flag($rule);
}
if (!$rule->dirty) {
// Clone the rule to avoid modules getting the changed version from
// the static cache.
$sets[$event_name]->rule(clone $rule);
}
}
}
// Create cache items for all created sets.
foreach ($sets as $event_name => $set) {
$set->sortChildren();
$set->optimize();
// Allow modules to alter the cached event set.
drupal_alter('rules_event_set', $event_name, $set);
rules_set_cache('event_' . $event_name, $set);
}
// 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) {
return $this->availableVariables();
}
/**
* Do not save since this class is for caching purposes only.
*
* @see RulesPlugin::save()
*/
public function save($name = NULL, $module = 'rules') {
return FALSE;
}
}

387
includes/rules.processor.inc

@ -0,0 +1,387 @@
<?php
/**
* @file
* Contains classes for data processing.
*
* Data processors can be used to process element arguments on evaluation time,
* e.g. to apply input evaluators or to apply simple calculations to number
* arguments.
*/
/**
* Common base class for Rules data processors.
*/
abstract class RulesDataProcessor {
/**
* The processors' setting value.
*/
protected $setting = NULL;
/**
* Allows chaining processors. If set, the next processor to invoke.
*/
protected $processor = NULL;
/**
* Constructor.
*/
protected function __construct($setting, $param_info, $var_info = array(), $processor = NULL) {
$this->setting = $setting;
$this->processor = $processor;
}
/**
* Return $this or skip this processor by returning the next processor.
*/
protected function getPreparedValue() {
return isset($this->setting) && array_filter($this->setting) ? $this : $this->processor;
}
/**
* Determines whether the current user has permission to edit this chain of
* data processors.
*
* @return bool
* Whether the current user has permission to edit this chain of data
* processors.
*/
public function editAccess() {
return $this->access() && (!isset($this->processor) || $this->processor->editAccess());
}
/**
* Prepares the processor for parameters.
*
* 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 array $var_info
* An array of info about the available variables.
*/
public static function prepareSetting(&$setting, $param_info, $var_info = array()) {
$processor = NULL;
foreach (self::processors($param_info, FALSE) as $name => $info) {
if (!empty($setting[$name])) {
$object = new $info['class']($setting[$name], $param_info, $var_info, $processor);
$processor = $object->getPreparedValue();
}
}
$setting = $processor;
}
/**
* Attaches the form of applicable data processors.
*/
public static function attachForm(&$form, $settings, $param_info, $var_info, $access_check = TRUE) {
// If $settings is already prepared get the settings from the processors.
if ($settings instanceof RulesDataProcessor) {
$settings = $settings->getChainSettings();
}
foreach (self::processors($param_info, $access_check) as $name => $info) {
$settings += array($name => array());
$form[$name] = call_user_func(array($info['class'], 'form'), $settings[$name], $var_info);
$form[$name]['#weight'] = $info['weight'];
}
}
/**
* Returns defined data processors applicable for the given parameter.
*
* 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);
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'])) {
return array();
}
// Filter the items by type.
if (isset($param_info['type']) && !isset($items[$hook][$param_info['type']])) {
$items[$hook][$param_info['type']] = array();
foreach ($items[$hook]['all'] as $name => $info) {
// Check whether the parameter type matches the supported types.
$info += array('type' => 'text');
if (RulesData::typesMatch($param_info, $info, FALSE)) {
$items[$hook][$param_info['type']][$name] = $info;
}
}
}
// Apply the access check.
$return = isset($param_info['type']) ? $items[$hook][$param_info['type']] : $items[$hook]['all'];
if ($access_check) {
foreach ($return as $base => $info) {
if (!call_user_func(array($info['class'], 'access'))) {
unset($return[$base]);
}
}
}
return $return;
}
public static function _item_sort($a, $b) {
return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : 0);
}
/**
* Gets the settings array for this and all contained chained processors.
*/
public function getChainSettings() {
foreach ($this->unchain() as $name => $processor) {
$settings[$name] = $processor->getSetting();
}
return isset($settings) ? $settings : array();
}
/**
* Returns an array of modules which we depend on.
*/
public function dependencies() {
$used_processor_info = array_intersect_key($this->processors(), $this->unchain());
$modules = array();
foreach ($used_processor_info as $name => $info) {
$modules[] = $info['module'];
}
return array_filter($modules);
}
/**
* @return
* An array of processors keyed by processor name.
*/
protected function unchain() {
$processor = $this;
while ($processor instanceof RulesDataProcessor) {
$processors[get_class($processor)] = $processor;
$processor = $processor->processor;
}
// Note: Don't use the static context to call processors() here as we need a
// late binding to invoke the input evaluators version, if needed.
$return = array();
foreach ($this->processors() as $name => $info) {
if (isset($processors[$info['class']])) {
$return[$name] = $processors[$info['class']];
}
}
return $return;
}
/**
* Gets the settings of this processor.
*/
public function getSetting() {
return $this->setting;
}
/**
* 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 RulesState $state
* The rules evaluation state.
* @param RulesPlugin $element
* The element for which we process the value.
*
* @return
* The processed value.
*/
abstract public function process($value, $info, RulesState $state, RulesPlugin $element);
/**
* 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;
}
/**
* Defines the processor form element.
*
* @param $settings
* The settings of the processor.
* @param array $var_info
* An array of info about the available variables.
*
* @return
* A form element structure.
*/
protected static function form($settings, $var_info) {
return array();
}
}
/**
* 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 {
/**
* Overridden to invoke prepare().
*/
protected function __construct($setting, $param_info, $var_info = array(), $processor = NULL) {
$this->setting = TRUE;
$this->processor = $processor;
$this->prepare($setting, $var_info, $param_info);
}
/**
* Overridden to generate evaluator $options and invoke evaluate().
*/
public function process($value, $info, RulesState $state, RulesPlugin $element, $options = NULL) {
$options = isset($options) ? $options : $this->getEvaluatorOptions($info, $state, $element);
$value = isset($this->processor) ? $this->processor->process($value, $info, $state, $element, $options) : $value;
return $this->evaluate($value, $options, $state);
}
/**
* Generates the evaluator $options.
*/
protected function getEvaluatorOptions($info, $state, $element) {
$cache = rules_get_cache();
$languages = language_list();
$info += array(
'cleaning callback' => isset($cache['data info'][$info['type']]['cleaning callback']) ? $cache['data info'][$info['type']]['cleaning callback'] : FALSE,
'sanitize' => FALSE,
);
$options = array_filter(array(
'language' => $info['#langcode'] != LANGUAGE_NONE && isset($languages[$info['#langcode']]) ? $languages[$info['#langcode']] : NULL,
'callback' => $info['cleaning callback'],
'sanitize' => $info['sanitize'],
));
return $options;
}
/**
* 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;
foreach (self::evaluators($param_info, FALSE) as $name => $info) {
$object = new $info['class']($setting, $param_info, $var_info, $processor);
$processor = $object->getPreparedValue();
}
$setting = $processor;
}
protected function getPreparedValue() {
return isset($this->setting) ? $this : $this->processor;
}
/**
* 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) {
$form['help'][$name] = call_user_func(array($info['class'], 'help'), $var_info, $param_info);
$form['help'][$name]['#weight'] = $info['weight'];
}
}
/**
* 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().
*/
public static function processors($param_info = NULL, $access_check = TRUE, $hook = 'evaluator_info') {
return parent::processors($param_info, $access_check, $hook);
}
/**
* Prepares the evaluation.
*
* 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 array $variables
* An array of info about available variables.
* @param array $param_info
* (optional) An array of information about the handled parameter value.
* For backward compatibility, this parameter is not required.
*/
abstract public function prepare($text, $variables);
/**
* Apply the input evaluator.
*
* @param string $text
* The text to evaluate.
* @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.
* - callback: A callback function that will be used to post-process
* replacements that might be incorporated, so they can be cleaned in a
* certain way.
* - sanitize: A boolean flag indicating whether incorporated replacements
* should be sanitized.
* @param RulesState $state
* The rules evaluation state.
*
* @return
* The evaluated text.
*/
abstract public function evaluate($text, $options, RulesState $state);
/**
* Provide some usage help for the evaluator.
*
* @param array $variables
* An array of info about available variables.
* @param array $param_info
* (optional) An array of information about the handled parameter value.
* For backward compatibility, this parameter is not required.
*
* @return array
* A renderable array.
*/
public static function help($variables) {
return array();
}
}

804
includes/rules.state.inc

@ -0,0 +1,804 @@
<?php
/**
* @file
* Contains the state and data related stuff.
*/
/**
* The rules evaluation state.
*
* A rule element may clone the state, so any added variables are only visible
* for elements in the current PHP-variable-scope.
*/
class RulesState {
/**
* Globally keeps the ids of rules blocked due to recursion prevention.
*
* @var array
*/
static protected $blocked = array();
/**
* The known variables.
*
* @var array
*/
public $variables = array();
/**
* Holds info about the variables.
*
* @var array
*/
protected $info = array();
/**
* Keeps wrappers to be saved later on.
*/
protected $save;
/**
* Holds the arguments while an element is executed.
*
* May be used by the element to easily access the wrapped arguments.
*/
public $currentArguments;
/**
* Variable for saving currently blocked configs for serialization.
*/
protected $currentlyBlocked;
/**
* Constructs a RulesState object.
*/
public function __construct() {
// Use an object in order to ensure any cloned states reference the same
// save information.
$this->save = new ArrayObject();
$this->addVariable('site', FALSE, self::defaultVariables('site'));
}
/**
* Adds the given variable to the given execution state.
*/
public function addVariable($name, $data, $info) {
$this->info[$name] = $info + array(
'skip save' => FALSE,
'type' => 'unknown',
'handler' => FALSE,
);
if (empty($this->info[$name]['handler'])) {
$this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
}
}
/**
* Runs post-evaluation tasks, such as saving variables.
*/
public function cleanUp() {
// Make changes permanent.
foreach ($this->save->getArrayCopy() as $selector => $wrapper) {
$this->saveNow($selector);
}
unset($this->currentArguments);
}
/**
* Block a rules configuration from execution.
*/
public function block($rules_config) {
if (empty($rules_config->recursion) && $rules_config->id) {
self::$blocked[$rules_config->id] = TRUE;
}
}
/**
* Unblock a rules configuration from execution.
*/
public function unblock($rules_config) {
if (empty($rules_config->recursion) && $rules_config->id) {
unset(self::$blocked[$rules_config->id]);
}
}
/**
* Returns whether a rules configuration should be blocked from execution.
*/
public function isBlocked($rule_config) {
return !empty($rule_config->id) && isset(self::$blocked[$rule_config->id]);
}
/**
* Get the info about the state variables or a single variable.
*/
public function varInfo($name = NULL) {
if (isset($name)) {
return isset($this->info[$name]) ? $this->info[$name] : FALSE;
}
return $this->info;
}
/**
* Returns whether the given wrapper is savable.
*/
public function isSavable($wrapper) {
return ($wrapper instanceof EntityDrupalWrapper && entity_type_supports($wrapper->type(), 'save')) || $wrapper instanceof RulesDataWrapperSavableInterface;
}
/**
* Returns whether the variable with the given name is an entity.
*/
public function isEntity($name) {
$entity_info = entity_get_info();
return isset($this->info[$name]['type']) && isset($entity_info[$this->info[$name]['type']]);
}
/**
* Gets a variable.
*
* If necessary, the specified handler is invoked to fetch the variable.
*
* @param string $name
* The name of the variable to return.
*
* @return
* The variable or a EntityMetadataWrapper containing the variable.
*
* @throws RulesEvaluationException
* Throws a RulesEvaluationException in case we have info about the
* requested variable, but it is not defined.
*/
public function &get($name) {
if (!array_key_exists($name, $this->variables)) {
// If there is handler to load the variable, do it now.
if (!empty($this->info[$name]['handler'])) {
$data = call_user_func($this->info[$name]['handler'], rules_unwrap_data($this->variables), $name, $this->info[$name]);
$this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
$this->info[$name]['handler'] = FALSE;
if (!isset($data)) {
throw new RulesEvaluationException('Unable to load variable %name, aborting.', array('%name' => $name), NULL, RulesLog::INFO);
}
}
else {
throw new RulesEvaluationException('Unable to get variable %name, it is not defined.', array('%name' => $name), NULL, RulesLog::ERROR);
}
}
return $this->variables[$name];
}
/**
* Apply permanent changes provided the wrapper's data type is savable.
*
* @param $selector
* The data selector of the wrapper to save or just a variable name.
* @param $wrapper
* @param bool $immediate
* Pass FALSE to postpone saving to later on. Else it's immediately saved.
*/
public function saveChanges($selector, $wrapper, $immediate = FALSE) {
$info = $wrapper->info();
if (empty($info['skip save']) && $this->isSavable($wrapper)) {
$this->save($selector, $wrapper, $immediate);
}
// No entity, so try saving the parent.
elseif (empty($info['skip save']) && isset($info['parent']) && !($wrapper instanceof EntityDrupalWrapper)) {
// Cut of the last part of the selector.
$selector = implode(':', explode(':', $selector, -1));
$this->saveChanges($selector, $info['parent'], $immediate);
}
return $this;
}
/**
* Remembers to save the wrapper on cleanup or does it now.
*/
protected function save($selector, EntityMetadataWrapper $wrapper, $immediate) {
// Convert variable names and selectors to both use underscores.
$selector = strtr($selector, '-', '_');
if (isset($this->save[$selector])) {
if ($this->save[$selector][0]->getIdentifier() == $wrapper->getIdentifier()) {
// The entity is already remembered. So do a combined save.
$this->save[$selector][1] += self::$blocked;
}
else {
// The wrapper is already in there, but wraps another entity. So first
// save the old one, then care about the new one.
$this->saveNow($selector);
}
}
if (!isset($this->save[$selector])) {
// In case of immediate saving don't clone the wrapper, so saving a new
// entity immediately makes the identifier available afterwards.
$this->save[$selector] = array($immediate ? $wrapper : clone $wrapper, self::$blocked);
}
if ($immediate) {
$this->saveNow($selector);
}
}
/**
* Saves the wrapper for the given selector.
*/
protected function saveNow($selector) {
// Add the set of blocked elements for the recursion prevention.
$previously_blocked = self::$blocked;
self::$blocked += $this->save[$selector][1];
// Actually save!
$wrapper = $this->save[$selector][0];
$entity = $wrapper->value();
// When operating in hook_entity_insert() $entity->is_new might be still
// set. In that case remove the flag to avoid causing another insert instead
// of an update.
if (!empty($entity->is_new) && $wrapper->getIdentifier()) {
$entity->is_new = FALSE;
}
rules_log('Saved %selector of type %type.', array('%selector' => $selector, '%type' => $wrapper->type()));
$wrapper->save();
// Restore the state's set of blocked elements.
self::$blocked = $previously_blocked;
unset($this->save[$selector]);
}
/**
* Merges info from the given state into the existing state.
*
* Merges the info about to-be-saved variables from the given state into the
* existing state. Therefore we can aggregate saves from invoked components.
* Merged-in saves are removed from the given state, but not-mergeable saves
* remain there.
*
* @param $state
* The state for which to merge the to be saved variables in.
* @param $component
* The component which has been invoked, thus needs to be blocked for the
* merged in saves.
* @param $settings
* The settings of the element that invoked the component. Contains
* information about variable/selector mappings between the states.
*/
public function mergeSaveVariables(RulesState $state, RulesPlugin $component, $settings) {
// For any saves that we take over, also block the component.
$this->block($component);
foreach ($state->save->getArrayCopy() as $selector => $data) {
$parts = explode(':', $selector, 2);
// Adapt the selector to fit for the parent state and move the wrapper.
if (isset($settings[$parts[0] . ':select'])) {
$parts[0] = $settings[$parts[0] . ':select'];
$this->save(implode(':', $parts), $data[0], FALSE);
unset($state->save[$selector]);
}
}
$this->unblock($component);
}
/**
* Returns an entity metadata wrapper as specified in the selector.
*
* @param string $selector
* The selector string, e.g. "node:author:mail".
* @param string $langcode
* (optional) The language code used to get the argument value if the
* argument value should be translated. Defaults to LANGUAGE_NONE.
*
* @return EntityMetadataWrapper
* The wrapper for the given selector.
*
* @throws RulesEvaluationException
* Throws a RulesEvaluationException in case the selector cannot be applied.
*/
public function applyDataSelector($selector, $langcode = LANGUAGE_NONE) {
$parts = explode(':', str_replace('-', '_', $selector), 2);
$wrapper = $this->get($parts[0]);
if (count($parts) == 1) {
return $wrapper;
}
elseif (!$wrapper instanceof EntityMetadataWrapper) {
throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not wrapped correctly.', array('%selector' => $selector));
}
try {
foreach (explode(':', $parts[1]) as $name) {
if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) {
// Make sure we are using the right language. Wrappers might be cached
// and have previous langcodes set, so always set the right language.
if ($wrapper instanceof EntityStructureWrapper) {
$wrapper->language($langcode);
}
$wrapper = $wrapper->get($name);
}
else {
throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not a list or a structure: %wrapper.', array('%selector' => $selector, '%wrapper' => $wrapper));
}
}
}
catch (EntityMetadataWrapperException $e) {
// In case of an exception, re-throw it.
throw new RulesEvaluationException('Unable to apply data selector %selector: %error', array('%selector' => $selector, '%error' => $e->getMessage()));
}
return $wrapper;
}
/**
* 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() {
$this->currentlyBlocked = self::$blocked;
return array('info', 'variables', 'currentlyBlocked');
}
/**
* Magic method. Unserialize variables and their info.
*/
public function __wakeup() {
$this->save = new ArrayObject();
}
/**
* 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.
*/
public function restoreBlocks() {
self::$blocked = $this->currentlyBlocked;
}
/**
* Defines always-available variables.
*
* @param $key
* (optional)
*/
public static function defaultVariables($key = NULL) {
// Add a variable for accessing site-wide data properties.
$vars['site'] = array(
'type' => 'site',
'label' => t('Site information'),
'description' => t("Site-wide settings and other global information."),
// Add the property info via a callback making use of the cached info.
'property info alter' => array('RulesData', 'addSiteMetadata'),
'property info' => array(),
'optional' => TRUE,
);
return isset($key) ? $vars[$key] : $vars;
}
}
/**
* A class holding static methods related to data.
*/
class RulesData {
/**
* Returns whether the type match. They match if type1 is compatible to type2.
*
* @param $var_info
* The name of the type to check for whether it is compatible to type2.
* @param $param_info
* The type expression to check for.
* @param bool $ancestors
* (optional) Whether sub-type relationships for checking type compatibility
* should be taken into account. Defaults to TRUE.
*
* @return bool
* Whether the types match.
*/
public static function typesMatch($var_info, $param_info, $ancestors = TRUE) {
$var_type = $var_info['type'];
$param_type = $param_info['type'];
if ($param_type == '*' || $param_type == 'unknown') {
return TRUE;
}
if ($var_type == $param_type) {
// Make sure the bundle matches, if specified by the parameter.
return !isset($param_info['bundles']) || isset($var_info['bundle']) && in_array($var_info['bundle'], $param_info['bundles']);
}
// Parameters may specify multiple types using an array.
$valid_types = is_array($param_type) ? $param_type : array($param_type);
if (in_array($var_type, $valid_types)) {
return TRUE;
}
// Check for sub-type relationships.
if ($ancestors && !isset($param_info['bundles'])) {
$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 FALSE;
}
protected static function typeCalcAncestors(&$cache, $type) {
if (!isset($cache['data_info'][$type]['ancestors'])) {
$cache['data_info'][$type]['ancestors'] = array();
if (isset($cache['data_info'][$type]['parent']) && $parent = $cache['data_info'][$type]['parent']) {
$cache['data_info'][$type]['ancestors'][$parent] = TRUE;
self::typeCalcAncestors($cache, $parent);
// Add all parent ancestors to our own ancestors.
$cache['data_info'][$type]['ancestors'] += $cache['data_info'][$parent]['ancestors'];
}
// For special lists like list<node> add in "list" as valid parent.
if (entity_property_list_extract_type($type)) {
$cache['data_info'][$type]['ancestors']['list'] = TRUE;
}
}
}
/**
* 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 string $prefix
* An optional prefix for the data selectors.
* @param int $recursions
* The number of recursions used to go down the tree. Defaults to 2.
* @param bool $suggestions
* Whether possibilities to recurse are suggested as soon as the deepest
* level of recursions is reached. Defaults to TRUE.
*
* @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.
$data = NULL;
if (is_array($source)) {
foreach ($source as $name => $info) {
$source[$name] = rules_wrap_data($data, $info, TRUE);
}
}
$matches = array();
foreach ($source as $name => $wrapper) {
$info = $wrapper->info();
$name = str_replace('_', '-', $name);
if (self::typesMatch($info, $param_info)) {
$matches[$prefix . $name] = $info;
if (!is_array($source) && $source instanceof EntityListWrapper) {
// Add some more possible list items.
for ($i = 1; $i < 4; $i++) {
$matches[$prefix . $i] = $info;
}
}
}
// Recurse later on to get an improved ordering of the results.
if ($wrapper instanceof EntityStructureWrapper || $wrapper instanceof EntityListWrapper) {
$recurse[$prefix . $name] = $wrapper;
if ($recursions > 0) {
$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.
$matches[$prefix . $name . ':'] = $wrapper->info();
if (!is_array($source) && $source instanceof EntityListWrapper) {
// Add some more possible list items.
for ($i = 1; $i < 4; $i++) {
$matches[$prefix . $i . ':'] = $wrapper->info();
}
}
}
}
}
return $matches;
}
/**
* 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()
*/
public static function addMetadataAssertions($var_info, $assertions) {
foreach ($assertions as $selector => $assertion) {
// Convert the selector back to underscores, such it matches the varname.
$selector = str_replace('-', '_', $selector);
$parts = explode(':', $selector);
if (isset($var_info[$parts[0]])) {
// Apply the selector to determine the right target array. We build an
// array like
// $var_info['rules assertion']['property1']['property2']['#info'] = ..
$target = &$var_info[$parts[0]]['rules assertion'];
foreach (array_slice($parts, 1) as $part) {
$target = &$target[$part];
}
// In case the assertion is directly for a variable, we have to modify
// the variable info directly. In case the asserted property is nested
// the info-has to be altered by RulesData::applyMetadataAssertions()
// before the child-wrapper is created.
if (count($parts) == 1) {
// Support asserting a type in case of generic entity references only.
$var_type = &$var_info[$parts[0]]['type'];
if (isset($assertion['type']) && ($var_type == 'entity' || $var_type == 'list<entity>')) {
$var_type = $assertion['type'];
unset($assertion['type']);
}
// Add any single bundle directly to the variable info, so the
// variable fits as argument for parameters requiring the bundle.
if (isset($assertion['bundle']) && count($bundles = (array) $assertion['bundle']) == 1) {
$var_info[$parts[0]]['bundle'] = reset($bundles);
}
}
// Add the assertions, but merge them with any previously added
// assertions if necessary.
$target['#info'] = isset($target['#info']) ? rules_update_array($target['#info'], $assertion) : $assertion;
// Add in a callback that the entity metadata wrapper pick up for
// altering the property info, such that we can add in the assertions.
$var_info[$parts[0]] += array('property info alter' => array('RulesData', 'applyMetadataAssertions'));
// In case there is a VARNAME_unchanged variable as it is used in update
// hooks, assume the assertions are valid for the unchanged variable
// too.
if (isset($var_info[$parts[0] . '_unchanged'])) {
$name = $parts[0] . '_unchanged';
$var_info[$name]['rules assertion'] = $var_info[$parts[0]]['rules assertion'];
$var_info[$name]['property info alter'] = array('RulesData', 'applyMetadataAssertions');
if (isset($var_info[$parts[0]]['bundle']) && !isset($var_info[$name]['bundle'])) {
$var_info[$name]['bundle'] = $var_info[$parts[0]]['bundle'];
}
}
}
}
return $var_info;
}
/**
* Property info alter callback for the entity metadata wrapper.
*
* Used for applying the rules metadata assertions.
*
* @see RulesData::addMetadataAssertions()
*/
public static function applyMetadataAssertions(EntityMetadataWrapper $wrapper, $property_info) {
$info = $wrapper->info();
if (!empty($info['rules assertion'])) {
$assertion = $info['rules assertion'];
// In case there are list-wrappers pass through the assertions of the item
// but make sure we only apply the assertions for the list items for
// which the conditions are executed.
if (isset($info['parent']) && $info['parent'] instanceof EntityListWrapper) {
$assertion = isset($assertion[$info['name']]) ? $assertion[$info['name']] : array();
}
// Support specifying multiple bundles, whereas the added properties are
// the intersection of the bundle properties.
if (isset($assertion['#info']['bundle'])) {
$bundles = (array) $assertion['#info']['bundle'];
foreach ($bundles as $bundle) {
$properties[] = isset($property_info['bundles'][$bundle]['properties']) ? $property_info['bundles'][$bundle]['properties'] : array();
}
// Add the intersection.
$property_info['properties'] += count($properties) > 1 ? call_user_func_array('array_intersect_key', $properties) : reset($properties);
}
// Support adding directly asserted property info.
if (isset($assertion['#info']['property info'])) {
$property_info['properties'] += $assertion['#info']['property info'];
}
// Pass through any rules assertion of properties to their info, so any
// derived wrappers apply it.
foreach (element_children($assertion) as $key) {
$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 property
// info.
if (isset($assertion[$key]['#info']['type'])) {
$type = $assertion[$key]['#info']['type'];
// Support asserting a type in case of generic entity references only.
if ($property_info['properties'][$key]['type'] == 'entity' && entity_get_info($type)) {
$property_info['properties'][$key]['type'] = $type;
}
}
if (isset($assertion[$key]['#info']['bundle'])) {
$bundle = (array) $assertion[$key]['#info']['bundle'];
// Add any single bundle directly to the variable info, so the
// property fits as argument for parameters requiring the bundle.
if (count($bundle) == 1) {
$property_info['properties'][$key]['bundle'] = reset($bundle);
}
}
}
}
return $property_info;
}
/**
* 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()
*/
public static function addSiteMetadata(EntityMetadataWrapper $wrapper, $property_info) {
$site_info = entity_get_property_info('site');
$property_info['properties'] += $site_info['properties'];
// Also invoke the usual callback for altering metadata, in case actions
// have specified further metadata.
return RulesData::applyMetadataAssertions($wrapper, $property_info);
}
}
/**
* A wrapper class similar to the EntityDrupalWrapper, but for non-entities.
*
* This class is intended to serve as base for a custom wrapper classes of
* identifiable data types, which are non-entities. By extending this class only
* the extractIdentifier() and load() methods have to be defined.
* In order to make the data type savable implement the
* RulesDataWrapperSavableInterface.
*
* That way it is possible for non-entity data types to be work with Rules, i.e.
* one can implement a 'ui class' with a direct input form returning the
* identifier of the data. However, instead of that it is suggested to implement
* an entity type, such that the same is achieved via general API functions like
* entity_load().
*/
abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper {
/**
* Contains the id.
*/
protected $id = FALSE;
/**
* Construct a new wrapper object.
*
* @param $type
* The type of the passed data.
* @param $data
* (optional) The data to wrap or its identifier.
* @param array $info
* (optional) Used internally to pass info about properties down the tree.
*/
public function __construct($type, $data = NULL, $info = array()) {
parent::__construct($type, $data, $info);
$this->setData($data);
}
/**
* Sets the data internally accepting both the data id and object.
*/
protected function setData($data) {
if (isset($data) && $data !== FALSE && !is_object($data)) {
$this->id = $data;
$this->data = FALSE;
}
elseif (is_object($data)) {
// We got the data object passed.
$this->data = $data;
$id = $this->extractIdentifier($data);
$this->id = isset($id) ? $id : FALSE;
}
}
/**
* Returns the identifier of the wrapped data.
*/
public function getIdentifier() {
return $this->dataAvailable() && $this->value() ? $this->id : NULL;
}
/**
* Overridden.
*/
public function value(array $options = array()) {
$this->setData(parent::value());
if (!$this->data && !empty($this->id)) {
// Lazy load the data if necessary.
$this->data = $this->load($this->id);
if (!$this->data) {
throw new EntityMetadataWrapperException('Unable to load the ' . check_plain($this->type) . ' with the id ' . check_plain($this->id) . '.');
}
}
return $this->data;
}
/**
* Overridden to support setting the data by either the object or the id.
*/
public function set($value) {
if (!$this->validate($value)) {
throw new EntityMetadataWrapperException('Invalid data value given. Be sure it matches the required data type and format.');
}
// As custom wrapper classes can only appear for Rules variables, but not
// as properties we don't have to care about updating the parent.
$this->clear();
$this->setData($value);
return $this;
}
/**
* Overridden.
*/
public function clear() {
$this->id = NULL;
parent::clear();
}
/**
* Prepare for serialization.
*/
public function __sleep() {
$vars = parent::__sleep();
// Don't serialize the loaded data, except for the case the data is not
// saved yet.
if (!empty($this->id)) {
unset($vars['data']);
}
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.
$this->data = FALSE;
}
}
/**
* Extract the identifier of the given data object.
*
* @return
* The extracted identifier.
*/
abstract protected function extractIdentifier($data);
/**
* Load a data object given an identifier.
*
* @return
* The loaded data object, or FALSE if loading failed.
*/
abstract protected function load($id);
}
/**
* Used to declare custom wrapper classes as savable.
*/
interface RulesDataWrapperSavableInterface {
/**
* Save the currently wrapped data.
*/
public function save();
}

745
includes/rules.upgrade.inc

@ -0,0 +1,745 @@
<?php
/**
* @file
* Contains code for upgrading rule configurations from 6.x-1.x to 7.x-2.x.
*/
/**
* Form builder for the upgrade page.
*/
function rules_upgrade_form($form, &$form_state) {
if (!empty($form_state['export'])) {
foreach ($form_state['export'] as $key => $export) {
// Rules have been already converted and exported, so show the export.
$form['export'][$key] = array(
'#type' => 'textarea',
'#title' => t('Export %name', array('%name' => $key)),
'#description' => t('For importing copy the content of the text area and paste it into the import page of the Rules admin UI. In case the export does not pass the integrity check during import, try using the save to database method instead and manually fix your configuration after conversion.'),
'#rows' => 10,
'#default_value' => $export,
);
}
return $form;
}
$form['help'] = array(
'#prefix' => '<p>',
'#suffix' => '</p>',
'#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 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();
if (!db_table_exists('rules_rules')) {
drupal_set_message('There are no Rules 1.x rules or rule sets left to convert.', 'error');
}
else {
foreach (_rules_upgrade_fetch_all_rules() as $name => $rule) {
if (!empty($rule['#set']) && strpos($rule['#set'], 'event_') === 0) {
$option_rules[$name] = $name . ': ' . $rule['#label'];
}
}
$query = db_select('rules_sets', 'r')->fields('r');
foreach ($query->execute() as $row) {
$set = unserialize($row->data);
$option_sets[$row->name] = $row->name . ': ' . $set['label'];
}
$form['clear'] = array(
'#prefix' => '<p>',
'#suffix' => '</p>',
'#markup' => t('Once you have successfully converted your configuration, you can clean up your database and <a href="!url">delete</a> all Rules 1.x configurations.', array('!url' => url('admin/config/workflow/rules/upgrade/clear'))),
);
}
$form['rules'] = array(
'#type' => 'select',
'#title' => t('Rules'),
'#options' => $option_rules,
'#multiple' => TRUE,
);
$form['sets'] = array(
'#type' => 'select',
'#title' => t('Rule sets'),
'#options' => $option_sets,
'#multiple' => TRUE,
);
$form['method'] = array(
'#type' => 'radios',
'#title' => t('Method'),
'#options' => array(
'export' => t('Convert configuration and export it.'),
'save' => t('Convert configuration and save it.'),
),
'#default_value' => 'export',
);
$form['actions']['convert'] = array(
'#type' => 'submit',
'#value' => t('Convert'),
'#disabled' => !db_table_exists('rules_rules'),
);
return $form;
}
/**
* Submit handler for the form.
*/
function rules_upgrade_form_submit($form, &$form_state) {
// Load all rules includes and install files so modules may put there upgrade
// information in both locations.
module_load_all_includes('rules.inc');
module_load_all_includes('install');
$configs = array();
try {
foreach ($form_state['values']['rules'] as $name) {
drupal_set_message(t('Converting %plugin %name...', array('%plugin' => t('rule'), '%name' => $name)));
$configs[$name] = rules_upgrade_convert_rule($name, _rules_upgrade_fetch_item($name, 'rules_rules'));
}
foreach ($form_state['values']['sets'] as $name) {
drupal_set_message(t('Converting %plugin %name...', array('%plugin' => t('rule set'), '%name' => $name)));
$configs[$name] = rules_upgrade_convert_rule_set($name, _rules_upgrade_fetch_item($name, 'rules_sets'));
}
drupal_set_message(t('Completed.'));
if ($form_state['values']['method'] == 'save') {
foreach ($configs as $config) {
$config->save();
}
drupal_set_message(t('Converted configurations have been saved to the database and will appear in the Rules administration interface.'));
}
elseif ($form_state['values']['method'] == 'export') {
$export = array();
foreach ($configs as $name => $config) {
$export[$name] = $config->export();
}
$form_state['export'] = $export;
$form_state['rebuild'] = TRUE;
}
}
catch (RulesException $e) {
drupal_set_message($e->getMessage(), 'error');
}
}
/**
* Confirm form for deleting data.
*/
function rules_upgrade_confirm_clear_form($form, $form_state) {
$confirm_question = t('Are you sure you want to drop the Rules 1.x tables from the database?');
$confirm_question_long = t('Are you sure you want to drop the Rules 1.x tables from the database? All Rules 1.x configurations will be deleted regardless whether they have been already converted.') . ' ' . t('This action cannot be undone.');
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');
db_drop_table('rules_scheduler_d6');
drupal_set_message(t('Rules 1.x configurations have been deleted.'));
$form_state['redirect'] = 'admin';
}
/**
* Fetches a single item (rule | rule set).
*/
function _rules_upgrade_fetch_item($name, $table) {
$query = db_select($table, 'r')->fields('r')->condition('name', $name);
$row = $query->execute()->fetchAssoc();
return unserialize($row['data']);
}
/**
* Fetches all rules.
*/
function _rules_upgrade_fetch_all_rules() {
$static = drupal_static(__FUNCTION__);
if (!isset($static)) {
$query = db_select('rules_rules', 'r')->fields('r');
$static['rules'] = array();
foreach ($query->execute() as $row) {
$static['rules'][$row->name] = unserialize($row->data);
}
}
return $static['rules'];
}
/**
* Converts a single reaction rule.
*/
function rules_upgrade_convert_rule($name, $cfg_old) {
$config = rules_upgrade_plugin_factory($cfg_old);
$config->name = $name;
if ($config instanceof RulesReactionRule) {
rules_upgrade_convert_element($cfg_old, $config);
}
return $config;
}
/**
* Converts a single rule set, including all of its rules.
*/
function rules_upgrade_convert_rule_set($name, $cfg_old) {
$config = rules_plugin_factory('rule set');
$config->name = $name;
foreach (array('label', 'weight') as $key) {
if (isset($cfg_old[$key])) {
$config->$key = $cfg_old[$key];
}
}
if (isset($cfg_old['arguments'])) {
$vars = &$config->componentVariables();
foreach ($cfg_old['arguments'] as $var_name => $info) {
// Map data types as required.
if ($info['type'] == 'string') {
$info['type'] = 'text';
}
$vars[$var_name] = $info;
}
}
// Add in all rules of the set.
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);
rules_upgrade_convert_element($rule, $new_rule);
$new_rule->setParent($config);
}
}
return $config;
}
/**
* Convert a single element.
*
* @param array $element
* The element to convert.
* @param RulesPlugin $target
* The converted element to write to.
*/
function rules_upgrade_convert_element(array $element, RulesPlugin $target) {
foreach (array('active', 'label', 'weight') as $key) {
if (isset($element['#' . $key])) {
$target->$key = $element['#' . $key];
}
}
// Go through the parameters and take over its configuration if possible.
foreach ($target->pluginParameterInfo() as $name => $info) {
rules_upgrade_element_parameter_settings($element, $target, $name);
}
// @todo Care about php input evaluator for non-text parameters.
// Take care of variable names and labels.
foreach ($target->pluginProvidesVariables() as $name => $info) {
rules_upgrade_element_variable_settings($element, $target, $name);
}
if ($target instanceof RulesConditionInterface && !empty($element['#negate'])) {
$target->negate(TRUE);
}
if ($target instanceof RulesReactionRule) {
// Cut of the 'event_' prefix.
$target->event(substr($element['#set'], 6));
}
if ($element['#type'] == 'rule') {
if (!empty($element['#conditions'])) {
foreach (element_children($element['#conditions']) as $key) {
$child = rules_upgrade_plugin_factory($element['#conditions'][$key]);
rules_upgrade_convert_element($element['#conditions'][$key], $child);
$target->condition($child);
}
}
if (!empty($element['#actions'])) {
foreach (element_children($element['#actions']) as $key) {
$child = rules_upgrade_plugin_factory($element['#actions'][$key]);
rules_upgrade_convert_element($element['#actions'][$key], $child);
$target->action($child);
}
}
}
// Invoke action/condition specific hooks and a general one.
if (($element['#type'] == 'action' || $element['#type'] == 'condition')) {
if (function_exists($function = $element['#name'] . '_upgrade')) {
$element_name = $function($element, $target);
}
elseif (isset($element['#info']['base']) && function_exists($function = $element['#info']['base'] . '_upgrade')) {
$element_name = $function($element, $target);
}
}
drupal_alter('rules_element_upgrade', $element, $target);
// Recurse down, if necessary.
foreach (element_children($element) as $key) {
$child = rules_upgrade_plugin_factory($element[$key]);
rules_upgrade_convert_element($element[$key], $child);
$child->setParent($target);
}
if ($target instanceof RulesContainerPlugin) {
$target->sortChildren();
}
}
/**
* Creates the right element.
*/
function rules_upgrade_plugin_factory($element) {
if ($element['#type'] == 'rule' && !empty($element['#set']) && strpos($element['#set'], 'event_') === 0) {
return rules_plugin_factory('reaction rule');
}
switch ($element['#type']) {
case 'OR':
return rules_plugin_factory('or');
case 'AND':
return rules_plugin_factory('and');
default:
return rules_plugin_factory($element['#type']);
case 'action':
case 'condition':
if (isset($element['#name'])) {
// Try to come up with the right action/condition name ourself, then
// invoke a hook.
$cache = rules_get_cache();
$items = $cache[$element['#type'] == 'action' ? 'action_info' : 'condition_info'];
if (isset($items[$element['#name']])) {
$element_name = $element['#name'];
}
elseif (($name = str_replace('rules_', '', $element['#name'])) && isset($items[$name])) {
$element_name = $name;
}
elseif (($name = str_replace($element['#type'] . '_', '', $element['#name'])) && isset($items[$name])) {
$element_name = $name;
}
elseif (($name = str_replace('rules_' . $element['#type'] . '_', '', $element['#name'])) && isset($items[$name])) {
$element_name = $name;
}
elseif (isset($element['#info']['base']) && isset($items[$element['#info']['base']])) {
$element_name = $name;
}
// 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'))) {
$element_name = $function($element);
}
if (!isset($element_name)) {
throw new RulesIntegrityException(t("Cannot find @plugin %name. Maybe a required is missing or the module has not implemented the upgrade functionality.", array('@plugin' => $element['#type'], '%name' => $element['#name'])));
}
return rules_plugin_factory($element['#type'], $element_name);
}
break;
}
}
/**
* Converts the settings for a given parameter.
*/
function rules_upgrade_element_parameter_settings($element, $target, $name, $new_name = NULL) {
if (!isset($new_name)) {
$new_name = $name;
}
if (isset($element['#settings'][$name])) {
// In case a single token has been used, just convert it to a data
// selector.
if (is_string($element['#settings'][$name]) && preg_match("/\[(.*)\]$/", $element['#settings'][$name], $matches)) {
$target->settings[$new_name . ':select'] = $matches[1];
}
else {
$target->settings[$new_name] = $element['#settings'][$name];
}
}
elseif (isset($element['#settings']['#argument map'][$name])) {
$target->settings[$new_name . ':select'] = $element['#settings']['#argument map'][$name];
}
}
/**
* Converts the settings for a given variable.
*/
function rules_upgrade_element_variable_settings($element, $target, $name, $new_name = NULL) {
if (!isset($new_name)) {
$new_name = $name;
}
if (isset($element['#settings']['#argument map'][$name])) {
$target->settings[$new_name . ':var'] = $element['#settings']['#argument map'][$name];
$target->settings[$new_name . ':label'] = $element['#info']['new variables'][$target->settings[$new_name . ':var']]['label'];
}
}
/**
* Upgrade callbacks for upgrading the provided Rules 1.x 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.
*/
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');
rules_upgrade_element_parameter_settings($element, $target, 'author', 'param_author');
rules_upgrade_element_parameter_settings($element, $target, 'type', 'param_type');
rules_upgrade_element_variable_settings($element, $target, 'node_added', 'entity_created');
if (!empty($element['#settings']['node_access'])) {
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.
*/
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.
*/
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.
*/
function rules_condition_text_compare_upgrade_map_name($element) {
// @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'])) {
$target->settings['type'] = $type;
rules_upgrade_element_parameter_settings($element, $target, $type, 'value');
rules_upgrade_element_variable_settings($element, $target, $type, 'variable_added');
}
else {
rules_upgrade_element_parameter_settings($element, $target, $type, 'entity');
}
}
/**
* System.module integration.
*/
function rules_action_set_breadcrumb_upgrade_map_name($element) {
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'];
$target->settings['url'] .= $settings['query'] ? '?' . $settings['query'] : '';
$target->settings['url'] .= $settings['fragment'] ? '#' . $settings['fragment'] : '';
if ($settings['immediate']) {
drupal_set_message(t("Warning: The 'immediate' option for the page redirect action has been dropped in Rules 2.x."));
}
}
function rules_action_watchdog_upgrade_map_name($element) {
// @todo Support action in Rules 2.x!
return NULL;
}
/**
* 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.
return NULL;
}
function rules_action_taxonomy_term_remove_from_content_upgrade_map_name($element) {
// @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.
*/
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'];
}

102
modules/comment.rules.inc

@ -0,0 +1,102 @@
<?php
/**
* @file
* Rules integration for the comment module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_event_info().
*/
function rules_comment_event_info() {
$defaults = array(
'group' => t('comment'),
'module' => 'comment',
'access callback' => 'rules_comment_integration_access',
'class' => 'RulesCommentEventHandler',
);
return array(
'comment_insert' => $defaults + array(
'label' => t('After saving a new comment'),
'variables' => array(
'comment' => array('type' => 'comment', 'label' => t('created comment')),
),
),
'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_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_view' => $defaults + array(
'label' => t('A comment is viewed'),
'variables' => array(
'comment' => array('type' => 'comment', 'label' => t('viewed comment')),
),
'help' => t("Note that if drupal's page cache is enabled, this event won't be generated for pages served from cache."),
),
'comment_delete' => $defaults + array(
'label' => t('After deleting a comment'),
'variables' => array(
'comment' => array('type' => 'comment', 'label' => t('deleted comment')),
),
),
);
}
/**
* Comment integration access callback.
*/
function rules_comment_integration_access($type, $name) {
if ($type == 'event' || $type == 'condition') {
return entity_access('view', 'comment');
}
}
/**
* 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');
}
}
/**
* @} End of "addtogroup rules"
*/

464
modules/data.eval.inc

@ -0,0 +1,464 @@
<?php
/**
* @file
* Contains rules integration for the data module needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Action: Modify data.
*/
function rules_action_data_set($wrapper, $value, $settings, $state, $element) {
if ($wrapper instanceof EntityMetadataWrapper) {
try {
// Update the value first then save changes, if possible.
$wrapper->set($value);
}
catch (EntityMetadataWrapperException $e) {
throw new RulesEvaluationException('Unable to modify data "@selector": ' . $e->getMessage(), array('@selector' => $settings['data:select']));
}
// Save changes if a property of a variable has been changed.
if (strpos($element->settings['data:select'], ':') !== FALSE) {
$info = $wrapper->info();
// We always have to save the changes in the parent entity. E.g. when the
// node author is changed, we don't want to save the author but the node.
$state->saveChanges(implode(':', explode(':', $settings['data:select'], -1)), $info['parent']);
}
}
else {
// A not wrapped variable (e.g. a number) is being updated. Just overwrite
// the variable with the new value.
return array('data' => $value);
}
}
/**
* Info alter callback for the data_set action.
*/
function rules_action_data_set_info_alter(&$element_info, $element) {
$element->settings += array('data:select' => NULL);
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;
}
}
/**
* Action: Calculate a value.
*/
function rules_action_data_calc($input1, $op, $input2, $settings, $state, $element) {
$info = $element->pluginParameterInfo();
// Make sure to apply date offsets intelligently.
if ($info['input_1']['type'] == 'date' && $info['input_2']['type'] == 'duration') {
$input2 = ($op == '-') ? $input2 * -1 : $input2;
return array('result' => (int) RulesDateOffsetProcessor::applyOffset($input1, $input2));
}
switch ($op) {
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.
$variables = $element->providesVariables();
$var_info = reset($variables);
if ($var_info['type'] == 'integer') {
$result = (int) $result;
}
return array('result' => $result);
}
}
/**
* Info alter callback for the data_calc action.
*/
function rules_action_data_calc_info_alter(&$element_info, RulesPlugin $element) {
if ($info = $element->getArgumentInfo('input_1')) {
// Only allow durations as offset for date values.
if ($info['type'] == 'date') {
$element_info['parameter']['input_2']['type'] = 'duration';
}
// Specify the data type of the result.
$element_info['provides']['result']['type'] = $info['type'];
if ($info['type'] == 'integer' && ($info2 = $element->getArgumentInfo('input_2')) && $info2['type'] == 'decimal') {
$element_info['provides']['result']['type'] = 'decimal';
}
// A division with two integers results in a decimal.
elseif (isset($element->settings['op']) && $element->settings['op'] == '/') {
$element_info['provides']['result']['type'] = 'decimal';
}
}
}
/**
* Action: Add a list item.
*/
function rules_action_data_list_add($list, $item, $unique = FALSE, $pos = 'end', $settings, $state) {
// Optionally, only add the list item if it is not yet contained.
if ($unique && rules_condition_data_list_contains($list, $item, $settings, $state)) {
return;
}
switch ($pos) {
case 'start':
array_unshift($list, $item);
break;
default:
$list[] = $item;
break;
}
return array('list' => $list);
}
/**
* Info alteration callback for the "Add and Remove a list item" actions.
*/
function rules_data_list_info_alter(&$element_info, RulesAbstractPlugin $element) {
// Update the required type for the list item if it is known.
$element->settings += array('list:select' => NULL);
if ($wrapper = $element->applyDataSelector($element->settings['list:select'])) {
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;
}
}
}
/**
* Action: Remove a list item.
*/
function rules_action_data_list_remove($list, $item) {
foreach (array_keys($list, $item) as $key) {
unset($list[$key]);
}
return array('list' => $list);
}
/**
* Action: Add variable.
*/
function rules_action_variable_add($args, $element) {
return array('variable_added' => $args['value']);
}
/**
* Info alteration callback for variable add action.
*/
function rules_action_variable_add_info_alter(&$element_info, RulesAbstractPlugin $element) {
if (isset($element->settings['type']) && $type = $element->settings['type']) {
$cache = rules_get_cache();
$type_info = $cache['data_info'][$type];
$element_info['parameter']['value']['type'] = $type;
$element_info['provides']['variable_added']['type'] = $type;
// For lists, we default to an empty list so subsequent actions can add
// items.
if (entity_property_list_extract_type($type)) {
$element_info['parameter']['value']['default value'] = array();
}
}
}
/**
* Action: Convert a value.
*/
function rules_action_data_convert($arguments, RulesPlugin $element, $state) {
$value_info = $element->getArgumentInfo('value');
$from_type = $value_info['type'];
$target_type = $arguments['type'];
// First apply the rounding behavior if given.
if (isset($arguments['rounding_behavior'])) {
switch ($arguments['rounding_behavior']) {
case 'up':
$arguments['value'] = ceil($arguments['value']);
break;
case 'down':
$arguments['value'] = floor($arguments['value']);
break;
default:
case 'round':
$arguments['value'] = round($arguments['value']);
break;
}
}
switch ($target_type) {
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);
}
/**
* Info alteration callback for variable add action.
*/
function rules_action_data_convert_info_alter(&$element_info, RulesAbstractPlugin $element) {
if (isset($element->settings['type']) && $type = $element->settings['type']) {
$element_info['provides']['conversion_result']['type'] = $type;
// 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']);
}
// Configure compatible source-types:
switch ($type) {
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;
}
}
/**
* Action: Create data.
*/
function rules_action_data_create($args, $element) {
$type = $args['type'];
$values = array();
foreach ($element->pluginParameterInfo() as $name => $info) {
if ($name != 'type') {
// Remove the parameter name prefix 'param_'.
$values[substr($name, 6)] = $args[$name];
}
}
$cache = rules_get_cache();
$type_info = $cache['data_info'][$type];
if (isset($type_info['creation callback'])) {
try {
$data = $type_info['creation callback']($values, $type);
return array('data_created' => $data);
}
catch (EntityMetadataWrapperException $e) {
throw new RulesEvaluationException('Unable to create @data": ' . $e->getMessage(), array('@data' => $type), $element);
}
}
else {
throw new RulesEvaluationException('Unable to create @data, no creation callback found.', array('@data' => $type), $element, RulesLog::ERROR);
}
}
/**
* Info alteration callback for data create action.
*/
function rules_action_data_create_info_alter(&$element_info, RulesAbstractPlugin $element) {
if (!empty($element->settings['type'])) {
$type = $element->settings['type'];
$cache = rules_get_cache();
$type_info = $cache['data_info'][$type];
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', 'allow null')));
if (empty($property_info['required'])) {
$element_info['parameter']['param_' . $property]['optional'] = TRUE;
$element_info['parameter']['param_' . $property]['allow null'] = TRUE;
}
}
}
$element_info['provides']['data_created']['type'] = $type;
}
}
/**
* Creation callback for array structured data.
*/
function rules_action_data_create_array($values = array(), $type) {
// $values is an array already, so we can just pass it to the wrapper.
return rules_wrap_data($values, array('type' => $type));
}
/**
* Condition: Compare data.
*/
function rules_condition_data_is($data, $op, $value) {
switch ($op) {
default:
case '==':
// In case both values evaluate to FALSE, further differentiate between
// NULL values and values evaluating to FALSE.
if (!$data && !$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.
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);
}
}
/**
* Info alteration callback for the data_is condition.
*
* If we check the bundle property of a variable, add an assertion so that later
* evaluated elements can make use of this information.
*/
function rules_condition_data_is_info_alter(&$element_info, RulesAbstractPlugin $element) {
$element->settings += array('data:select' => NULL, 'op' => '==');
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;
}
}
/**
* Condition: List contains.
*/
function rules_condition_data_list_contains($list, $item, $settings, $state) {
$wrapper = $state->currentArguments['item'];
if ($wrapper instanceof EntityStructureWrapper && $id = $wrapper->getIdentifier()) {
// Check for equal items using the identifier if there is one.
foreach ($state->currentArguments['list'] as $i) {
if ($i->getIdentifier() == $id) {
return TRUE;
}
}
return FALSE;
}
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.
*/
function rules_condition_data_is_empty($data) {
// Note that some primitive variables might not be wrapped at all.
if ($data instanceof EntityMetadataWrapper) {
try {
// We cannot use the dataAvailable() method from the wrapper because it
// is protected, so we catch possible exceptions with the value() method.
$value = $data->value();
return empty($value);
}
catch (EntityMetadataWrapperException $e) {
// An exception means that the wrapper is somehow broken and we treat
// that as empty.
return TRUE;
}
}
return empty($data);
}
/**
* Condition: Textual comparison.
*/
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));
case 'regex':
return (bool) preg_match('/' . str_replace('/', '\\/', $text2) . '/', $text);
}
}
/**
* @} End of "addtogroup rules"
*/

759
modules/data.rules.inc

@ -0,0 +1,759 @@
<?php
/**
* @file
* General data related rules integration.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_category_info() on behalf of the pseudo data module.
*/
function rules_data_category_info() {
return array(
'rules_data' => 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() {
return array('modules/data.eval');
}
/**
* Implements hook_rules_action_info() on behalf of the pseudo data module.
*
* @see rules_core_modules()
*/
function rules_data_action_info() {
$return['data_set'] = array(
'label' => t('Set a data value'),
'parameter' => array(
'data' => array(
'type' => '*',
'label' => t('Data'),
'description' => t('Specifies the data to be modified using a data selector, e.g. "node:author:name".'),
'restriction' => 'selector',
'wrapped' => TRUE,
'allow null' => TRUE,
),
'value' => array(
'type' => '*',
'label' => t('Value'),
'description' => t('The new value to set for the specified data.'),
'allow null' => TRUE,
'optional' => TRUE,
),
),
'group' => t('Data'),
'base' => 'rules_action_data_set',
);
$return['data_calc'] = array(
'label' => t('Calculate a value'),
'parameter' => array(
'input_1' => array(
'type' => array('decimal', 'date'),
'label' => t('Input value 1'),
'description' => t('The first input value for the calculation.'),
),
'op' => array(
'type' => 'text',
'label' => t('Operator'),
'description' => t('The calculation operator.'),
'options list' => 'rules_action_data_calc_operator_options',
'restriction' => 'input',
'default value' => '+',
),
'input_2' => array(
'type' => 'decimal',
'label' => t('Input value 2'),
'description' => t('The second input value.'),
),
),
'group' => t('Data'),
'base' => 'rules_action_data_calc',
'provides' => array(
'result' => array(
'type' => 'unknown',
'label' => t('Calculation result'),
),
),
);
$return['list_add'] = array(
'label' => t('Add an item to a list'),
'parameter' => array(
'list' => array(
'type' => 'list',
'label' => t('List', array(), array('context' => 'data_types')),
'description' => t('The data list, to which an item is to be added.'),
'restriction' => 'selector',
'allow null' => TRUE,
'save' => TRUE,
),
'item' => array(
'type' => 'unknown',
'label' => t('Item to add'),
),
'unique' => array(
'type' => 'boolean',
'label' => t('Enforce uniqueness'),
'description' => t('Only add the item to the list if it is not yet contained.'),
'optional' => TRUE,
'default value' => FALSE,
),
'pos' => array(
'type' => 'text',
'label' => t('Insert position'),
'optional' => TRUE,
'default value' => 'end',
'options list' => 'rules_action_data_list_add_positions',
),
),
'group' => t('Data'),
'base' => 'rules_action_data_list_add',
'callbacks' => array(
'info_alter' => 'rules_data_list_info_alter',
'form_alter' => 'rules_data_list_form_alter',
),
);
$return['list_remove'] = array(
'label' => t('Remove an item from a list'),
'parameter' => array(
'list' => array(
'type' => 'list',
'label' => t('List', array(), array('context' => 'data_types')),
'description' => t('The data list for which an item is to be removed.'),
'restriction' => 'selector',
'save' => TRUE,
),
'item' => array(
'type' => 'unknown',
'label' => t('Item to remove'),
),
),
'group' => t('Data'),
'base' => 'rules_action_data_list_remove',
'callbacks' => array(
'info_alter' => 'rules_data_list_info_alter',
'form_alter' => 'rules_data_list_form_alter',
),
);
$return['variable_add'] = array(
'label' => t('Add a variable'),
'named parameter' => TRUE,
'parameter' => array(
'type' => array(
'type' => 'text',
'label' => t('Type'),
'options list' => 'rules_data_action_variable_add_options',
'description' => t('Specifies the type of the variable that should be added.'),
'restriction' => 'input',
),
'value' => array(
'type' => 'unknown',
'label' => t('Value'),
'optional' => TRUE,
'description' => t('Optionally, specify the initial value of the variable.'),
),
),
'provides' => array(
'variable_added' => array(
'type' => 'unknown',
'label' => t('Added variable'),
),
),
'group' => t('Data'),
'base' => 'rules_action_variable_add',
'callbacks' => array(
'form_alter' => 'rules_action_type_form_alter',
'validate' => 'rules_action_create_type_validate',
),
);
if (rules_data_action_data_create_options()) {
$return['data_create'] = array(
'label' => t('Create a data structure'),
'named parameter' => TRUE,
'parameter' => array(
'type' => array(
'type' => 'text',
'label' => t('Type'),
'options list' => 'rules_data_action_data_create_options',
'description' => t('Specifies the type of the data structure that should be created.'),
'restriction' => 'input',
),
// Further needed parameters depend on the type.
),
'provides' => array(
'data_created' => array(
'type' => 'unknown',
'label' => t('Created data'),
),
),
'group' => t('Data'),
'base' => 'rules_action_data_create',
'callbacks' => array(
'form_alter' => 'rules_action_type_form_alter',
'validate' => 'rules_action_create_type_validate',
),
);
}
$return['data_convert'] = array(
'label' => t('Convert data type'),
'parameter' => array(
'type' => array(
'type' => 'token',
'label' => t('Target type'),
'description' => t('The data type to convert a value to.'),
'options list' => 'rules_action_data_convert_types_options',
'restriction' => 'input',
),
'value' => array(
'type' => array('decimal', 'integer', 'text', 'token'),
'label' => t('Value to convert'),
'default mode' => 'selector',
),
),
'provides' => array(
'conversion_result' => array(
'type' => 'unknown',
'label' => t('Conversion result'),
),
),
'group' => t('Data'),
'base' => 'rules_action_data_convert',
'named parameter' => TRUE,
'callbacks' => array(
'form_alter' => 'rules_action_type_form_alter',
),
);
return $return;
}
/**
* Data conversation action: Options list callback for the target type.
*/
function rules_action_data_convert_types_options(RulesPlugin $element, $param_name) {
return array(
'decimal' => t('Decimal'),
'integer' => t('Integer'),
'text' => t('Text'),
'token' => t('Token'),
);
}
/**
* Data conversation action: Options list callback for rounding behavior.
*/
function rules_action_data_convert_rounding_behavior_options(RulesPlugin $element, $param_name) {
return array(
'down' => t('Always down (9.5 -> 9)'),
'round' => t('Round, half up (9.5 -> 10)'),
'up' => t('Always up (9.5 -> 10)'),
);
}
/**
* Customize access check for data set action.
*/
function rules_action_data_set_access(RulesAbstractPlugin $element) {
if (isset($element->settings['data:select']) && $wrapper = $element->applyDataSelector($element->settings['data:select'])) {
return $wrapper instanceof EntityMetadataWrapper && $wrapper->access('edit');
}
}
/**
* Custom validation callback for the data set action.
*/
function rules_action_data_set_validate(RulesAbstractPlugin $element) {
$element->settings += array('data:select' => NULL);
$info = $element->applyDataSelector($element->settings['data:select'])->info();
if (strpos($element->settings['data:select'], ':') !== FALSE && empty($info['setter callback'])) {
throw new RulesIntegrityException(t("The selected data property doesn't support writing."), array($element, 'parameter', 'data'));
}
}
/**
* Form alter callback for the data_set action.
*/
function rules_action_data_set_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
if (!empty($options['init']) && !isset($form_state['rules_element_step'])) {
$form['negate']['#access'] = FALSE;
unset($form['parameter']['value']);
unset($form['parameter']['language']);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Continue'),
'#limit_validation_errors' => array(array('parameter', 'data')),
'#submit' => array('rules_form_submit_rebuild'),
);
$form_state['rules_element_step'] = 'data_value';
// Clear the parameter mode for the value parameter, so its gets the proper
// default value based upon the type of the selected data on rebuild.
unset($form_state['parameter_mode']['value']);
}
else {
// Change the data parameter to be not editable.
$form['parameter']['data']['settings']['#access'] = FALSE;
// @todo Improve display.
$form['parameter']['data']['info'] = array(
'#prefix' => '<p>',
'#markup' => t('<strong>Selected data:</strong> %selector', array('%selector' => $element->settings['data:select'])),
'#suffix' => '</p>',
);
}
}
/**
* Form alter callback for the data calculation action.
*/
function rules_action_data_calc_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
$form['reload'] = array(
'#weight' => 5,
'#type' => 'submit',
'#name' => 'reload',
'#value' => t('Reload form'),
'#limit_validation_errors' => array(array('parameter', 'input_1')),
'#submit' => array('rules_form_submit_rebuild'),
'#ajax' => rules_ui_form_default_ajax(),
);
}
/**
* Validate callback for entity create, add variable and data create actions.
*/
function rules_action_create_type_validate($element) {
if (!isset($element->settings['type'])) {
throw new RulesIntegrityException(t('Invalid type specified.'), array($element, 'parameter', 'type'));
}
}
/**
* Form alter callback for the list add and remove actions.
*
* Use multiple steps to configure the action to update the item configuration
* form once we know the data type.
*
* @see rules_data_list_info_alter()
*/
function rules_data_list_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
if (!empty($options['init']) && !isset($form_state['rules_element_step'])) {
unset($form['parameter']['item'], $form['parameter']['pos']);
$form_state['rules_element_step'] = 1;
$form['negate']['#access'] = FALSE;
$form['parameter']['unique']['#access'] = FALSE;
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Continue'),
'#limit_validation_errors' => array(array('parameter', 'list')),
'#submit' => array('rules_form_submit_rebuild'),
);
}
else {
// Change the list parameter to be not editable any more.
$form['parameter']['list']['settings']['#access'] = FALSE;
$form['parameter']['list']['info'] = array(
'#prefix' => '<p>',
'#markup' => t('<strong>Selected list:</strong> %selector', array('%selector' => $element->settings['list:select'])),
'#suffix' => '</p>',
);
}
}
/**
* Form alter callback for actions relying on the entity type or the data type.
*/
function rules_action_type_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
$first_step = empty($element->settings['type']);
$form['reload'] = array(
'#weight' => 5,
'#type' => 'submit',
'#name' => 'reload',
'#value' => $first_step ? t('Continue') : t('Reload form'),
'#limit_validation_errors' => array(array('parameter', 'type')),
'#submit' => array('rules_action_type_form_submit_rebuild'),
'#ajax' => rules_ui_form_default_ajax(),
);
// Use ajax and trigger as the reload button.
$form['parameter']['type']['settings']['type']['#ajax'] = $form['reload']['#ajax'] + array(
'event' => 'change',
'trigger_as' => array('name' => 'reload'),
);
if ($first_step) {
// In the first step show only the type select.
foreach (element_children($form['parameter']) as $key) {
if ($key != 'type') {
unset($form['parameter'][$key]);
}
}
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.
unset($form['parameter']['type']['settings']['type']['#ajax']);
unset($form['reload']['#ajax']);
}
else {
// Hide the reload button in case js is enabled and it's not the first step.
$form['reload']['#attributes'] = array('class' => array('rules-hide-js'));
}
}
/**
* FAPI submit callback for reloading the type form for entities or data types.
*/
function rules_action_type_form_submit_rebuild($form, &$form_state) {
rules_form_submit_rebuild($form, $form_state);
// Clear the parameter modes for the parameters, so they get the proper
// default values based upon the data types on rebuild.
$form_state['parameter_mode'] = array();
}
/**
* Options list callback for possible insertion positions.
*/
function rules_action_data_list_add_positions() {
return array(
'end' => t('Append the item to the end.'),
'start' => t('Prepend the item to the front.'),
);
}
/**
* Options list callback for variable add action.
*/
function rules_data_action_variable_add_options() {
return RulesPluginUI::getOptions('data');
}
/**
* Options list callback for the data calculation action.
*/
function rules_action_data_calc_operator_options(RulesPlugin $element, $param_name) {
$options = array(
'+' => '( + )',
'-' => '( - )',
'*' => '( * )',
'/' => '( / )',
'min' => 'min',
'max' => 'max',
);
// Only show +/- in case a date has been selected.
if (($info = $element->getArgumentInfo('input_1')) && $info['type'] == 'date') {
unset($options['*']);
unset($options['/']);
unset($options['min']);
unset($options['max']);
}
return $options;
}
/**
* Options list callback for data create action.
*/
function rules_data_action_data_create_options() {
$cache = rules_get_cache();
$data_info = $cache['data_info'];
$entity_info = entity_get_info();
// Remove entities.
$data_info = array_diff_key($data_info, $entity_info);
$options = array();
foreach ($data_info as $type => $properties) {
if (isset($properties['creation callback'])) {
// Add data types with creation callback only.
$options[$type] = $properties['label'];
}
}
natcasesort($options);
return $options;
}
/**
* Implements hook_rules_condition_info() on behalf of the pseudo data module.
*
* @see rules_core_modules()
*/
function rules_data_condition_info() {
return array(
'data_is' => array(
'label' => t('Data comparison'),
'parameter' => array(
'data' => array(
'type' => '*',
'label' => t('Data to compare'),
'description' => t('The data to be compared, specified by using a data selector, e.g. "node:author:name".'),
'allow null' => TRUE,
),
'op' => array(
'type' => 'text',
'label' => t('Operator'),
'description' => t('The comparison operator.'),
'optional' => TRUE,
'default value' => '==',
'options list' => 'rules_condition_data_is_operator_options',
'restriction' => 'input',
),
'value' => array(
'type' => '*',
'label' => t('Data value'),
'description' => t('The value to compare the data with.'),
'allow null' => TRUE,
),
),
'group' => t('Data'),
'base' => 'rules_condition_data_is',
),
'data_is_empty' => array(
'label' => t('Data value is empty'),
'parameter' => array(
'data' => array(
'type' => '*',
'label' => t('Data to check'),
'description' => t('The data to be checked to be empty, specified by using a data selector, e.g. "node:author:name".'),
'allow null' => TRUE,
'wrapped' => TRUE,
),
),
'group' => t('Data'),
'base' => 'rules_condition_data_is_empty',
),
'list_contains' => array(
'label' => t('List contains item'),
'parameter' => array(
'list' => array(
'type' => 'list',
'label' => t('List', array(), array('context' => 'data_types')),
'restriction' => 'selector',
),
'item' => array(
'type' => 'unknown',
'label' => t('Item'),
'description' => t('The item to check for.'),
),
),
'group' => t('Data'),
'base' => 'rules_condition_data_list_contains',
'callbacks' => array(
'info_alter' => 'rules_data_list_info_alter',
'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(
'text' => array(
'type' => 'text',
'label' => t('Text'),
'restriction' => 'selector',
),
'match' => array(
'type' => 'text',
'label' => t('Matching text'),
),
'operation' => array(
'type' => 'text',
'label' => t('Comparison operation'),
'options list' => 'rules_data_text_comparison_operation_list',
'restriction' => 'input',
'default value' => 'contains',
'optional' => TRUE,
'description' => t('In case the comparison operation @regex is selected, the matching pattern will be interpreted as a <a href="@regex-wikipedia">regular expression</a>. Tip: <a href="@RegExr">RegExr: Online Regular Expression Testing Tool</a> is helpful for learning, writing, and testing Regular Expressions.', array('@regex-wikipedia' => 'http://en.wikipedia.org/wiki/Regular_expression', '@RegExr' => 'http://gskinner.com/RegExr/', '@regex' => t('regular expression'))),
),
),
'group' => t('Data'),
'base' => 'rules_data_text_comparison',
),
);
}
/**
* 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 it's compared.
if ($wrapper = $element->applyDataSelector($element->settings['data:select'])) {
$info = $wrapper->info();
if (isset($info['parent']) && $info['parent'] instanceof EntityDrupalWrapper) {
$entity_info = $info['parent']->entityInfo();
if (isset($entity_info['entity keys']['bundle']) && $entity_info['entity keys']['bundle'] == $info['name']) {
// Assert that the entity is of bundle $value.
$value = is_array($element->settings['value']) ? $element->settings['value'] : array($element->settings['value']);
// Chop of the last part of the selector.
$parts = explode(':', $element->settings['data:select'], -1);
return array(implode(':', $parts) => array('bundle' => $value));
}
}
}
}
/**
* Form alter callback for the condition data_is.
*
* Use multiple steps to configure the condition as the needed type of the value
* depends on the selected data.
*/
function rules_condition_data_is_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
if (!empty($options['init']) && !isset($form_state['rules_element_step'])) {
unset($form['parameter']['op'], $form['parameter']['value']);
$form['negate']['#access'] = FALSE;
$form_state['rules_element_step'] = 'data_value';
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Continue'),
'#limit_validation_errors' => array(array('parameter', 'data'), array('parameter', 'op')),
'#submit' => array('rules_form_submit_rebuild'),
);
// Clear the parameter mode for the value parameter, so its gets the proper
// default value based upon the type of the the selected data on rebuild.
unset($form_state['parameter_mode']['value']);
}
else {
// Change the data parameter to be not editable.
$form['parameter']['data']['settings']['#access'] = FALSE;
// @todo Improve display.
$form['parameter']['data']['info'] = array(
'#prefix' => '<p>',
'#markup' => t('<strong>Selected data:</strong> %selector', array('%selector' => $element->settings['data:select'])),
'#suffix' => '</p>',
);
// Limit the operations to what makes sense for the selected data type.
$info = $element->pluginParameterInfo();
$data_info = $info['value'];
if ($element->settings['op'] == 'IN') {
$data_info['type'] = entity_property_list_extract_type($data_info['type']);
}
if (!RulesData::typesMatch($data_info, array('type' => array('decimal', 'date')))) {
$options =& $form['parameter']['op']['settings']['op']['#options'];
unset($options['<'], $options['>']);
}
// Remove 'contains' if it is not selected, as it is deprecated by the
// text comparison condition.
if ($element->settings['op'] != 'contains') {
unset($form['parameter']['op']['settings']['op']['#options']['contains']);
}
// Auto-refresh the form if the operation is changed, so the input form
// changes in case "is one of" requires a list value.
$form['parameter']['op']['settings']['op']['#ajax'] = rules_ui_form_default_ajax() + array(
'trigger_as' => array('name' => 'reload'),
);
// Provide a reload button for non-JS users.
$form['reload'] = array(
'#type' => 'submit',
'#value' => t('Reload form'),
'#limit_validation_errors' => array(array('parameter', 'data'), array('parameter', 'op')),
'#submit' => array('rules_form_submit_rebuild'),
'#ajax' => rules_ui_form_default_ajax(),
'#weight' => 5,
);
// Hide the reload button in case JS is enabled.
$form['reload']['#attributes'] = array('class' => array('rules-hide-js'));
}
}
/**
* Provides configuration help for the data_is condition.
*/
function rules_condition_data_is_help() {
return array('#markup' => t('Compare two data values of the same type with each other.'));
}
/**
* Options list callback for condition data_is.
*/
function rules_condition_data_is_operator_options() {
return array(
'==' => t('equals'),
'IN' => t('is one of'),
'<' => t('is lower than'),
'>' => t('is greater than'),
// Note: This is deprecated by the text comparison condition.
'contains' => t('contains'),
);
}
/**
* Options list callback for condition text_matches.
*/
function rules_data_text_comparison_operation_list() {
return array(
'contains' => t('contains'),
'starts' => t('starts with'),
'ends' => t('ends with'),
'regex' => t('regular expression'),
);
}
/**
* Returns the options list as specified by the selected property of the first parameter.
*
* @see rules_data_list_info_alter()
* @see rules_action_data_set_info_alter()
* @see rules_condition_data_is_info_alter()
*/
function rules_data_selector_options_list(RulesAbstractPlugin $element) {
$name = rules_array_key($element->pluginParameterInfo());
// If the selected data property has an option list, make use of it.
if (isset($element->settings[$name . ':select']) && $wrapper = $element->applyDataSelector($element->settings[$name . ':select'])) {
return $wrapper->optionsList($element instanceof RulesActionInterface ? 'edit' : 'view');
}
}
/**
* 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'),
);
}
/**
* @} End of "addtogroup rules"
*/

186
modules/entity.eval.inc

@ -0,0 +1,186 @@
<?php
/**
* @file
* Contains rules integration for entities needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Action: Fetch data.
*/
function rules_action_entity_fetch($type, $id, $revision) {
$info = entity_get_info($type);
// Support the revision parameter, if applicable.
if (!empty($info['entity keys']['revision']) && isset($revision)) {
$conditions = array($info['entity keys']['revision'] => $revision);
}
$return = entity_load($type, array($id), isset($conditions) ? $conditions : array());
$entity = reset($return);
if (!$entity) {
throw new RulesEvaluationException('Unable to load @entity with id "@id"', array('@id' => $id, '@entity' => $type));
}
return array('entity_fetched' => $entity);
}
/**
* Info alteration callback for the entity fetch action.
*/
function rules_action_entity_fetch_info_alter(&$element_info, RulesAbstractPlugin $element) {
$element->settings += array('type' => NULL);
$info = entity_get_info($element->settings['type']);
// Fix the type of the identifier.
$element_info['parameter']['id']['type'] = isset($info['entity keys']['name']) ? 'text' : 'integer';
// Add an optional revision parameter, if supported.
if (!empty($info['entity keys']['revision'])) {
$element_info['parameter']['revision_id'] = array(
'type' => 'integer',
'label' => t('Revision identifier'),
'optional' => TRUE,
);
}
$element_info['provides']['entity_fetched']['type'] = $element->settings['type'];
}
/**
* Action: Query entities.
*/
function rules_action_entity_query($type, $property, $value, $limit) {
$return = entity_property_query($type, $property, $value, $limit);
return array('entity_fetched' => array_values($return));
}
/**
* Info alteration callback for the entity query action.
*/
function rules_action_entity_query_info_alter(&$element_info, RulesAbstractPlugin $element) {
$element->settings += array('type' => NULL, 'property' => NULL);
if ($element->settings['type']) {
$element_info['parameter']['property']['options list'] = 'rules_action_entity_query_property_options_list';
if ($element->settings['property']) {
$wrapper = rules_get_entity_metadata_wrapper_all_properties($element);
if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) {
$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;
}
}
}
$element_info['provides']['entity_fetched']['type'] = 'list<' . $element->settings['type'] . '>';
}
/**
* Action: Create entities.
*/
function rules_action_entity_create($args, $element) {
$values = array();
foreach ($element->pluginParameterInfo() as $name => $info) {
if ($name != 'type') {
// Remove the parameter name prefix 'param_'.
$values[substr($name, 6)] = $args[$name];
}
}
try {
$data = entity_property_values_create_entity($args['type'], $values);
return array('entity_created' => $data);
}
catch (EntityMetadataWrapperException $e) {
throw new RulesEvaluationException('Unable to create entity @type": ' . $e->getMessage(), array('@type' => $args['type']), $element);
}
}
/**
* Info alteration callback for the entity create action.
*/
function rules_action_entity_create_info_alter(&$element_info, RulesAbstractPlugin $element) {
if (!empty($element->settings['type']) && entity_get_info($element->settings['type'])) {
$wrapper = entity_metadata_wrapper($element->settings['type']);
// Add the data type's needed parameter for loading to the parameter info.
foreach ($wrapper as $name => $child) {
$info = $child->info();
if (!empty($info['required'])) {
$info += array('type' => 'text');
// 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['provides']['entity_created']['type'] = $element->settings['type'];
if (($bundleKey = $wrapper->entityKey('bundle')) && isset($element->settings['param_' . $bundleKey])) {
$element_info['provides']['entity_created']['bundle'] = $element->settings['param_' . $bundleKey];
}
}
}
/**
* Action: Save entities.
*/
function rules_action_entity_save($wrapper, $immediate = FALSE, $settings, $state, $element) {
$state->saveChanges($settings['data:select'], $wrapper, $immediate);
}
/**
* Action: Delete entities.
*/
function rules_action_entity_delete($wrapper, $settings, $state, $element) {
try {
$wrapper->delete();
}
catch (EntityMetadataWrapperException $e) {
throw new RulesEvaluationException($e->getMessage(), array(), $element);
}
}
/**
* Condition: Entity is new.
*/
function rules_condition_entity_is_new($wrapper, $settings, $state, $element) {
return !$wrapper->getIdentifier() || !empty($wrapper->value()->is_new);
}
/**
* Condition: Entity has field.
*/
function rules_condition_entity_has_field($wrapper, $field_name, $settings, $state) {
return isset($wrapper->$field_name) || isset($wrapper->value()->$field_name);
}
/**
* Condition: Entity is of type.
*/
function rules_condition_entity_is_of_type($wrapper, $type) {
return $wrapper->type() == $type;
}
/**
* Condition: Entity is of type and bundle.
*/
function rules_condition_entity_is_of_bundle($wrapper, $type, $bundles) {
return $wrapper->type() == $type && in_array($wrapper->getBundle(), $bundles);
}
/**
* Condition: User has access to field.
*/
function rules_condition_entity_field_access(EntityDrupalWrapper $wrapper, $field_name, $op, $account = NULL) {
$field = field_info_field($field_name);
return !empty($field) && field_access($op, $field, $wrapper->type(), $wrapper->value(), $account = NULL);
}
/**
* @} End of "addtogroup rules"
*/

591
modules/entity.rules.inc

@ -0,0 +1,591 @@
<?php
/**
* @file
* General entity related rules integration.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_file_info() on behalf of the entity module.
*
* @see rules_core_modules()
*/
function rules_entity_file_info() {
return array('modules/entity.eval');
}
/**
* Implements hook_rules_category_info() on behalf of the pseudo entity module.
*/
function rules_entity_category_info() {
return array(
'rules_entity' => 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() {
if (rules_entity_action_type_options('entity_fetch')) {
$return['entity_fetch'] = array(
'label' => t('Fetch entity by id'),
'parameter' => array(
'type' => array(
'type' => 'text',
'label' => t('Entity type'),
'options list' => 'rules_entity_action_type_options',
'description' => t('Specifies the type of entity that should be fetched.'),
'restriction' => 'input',
),
'id' => array('type' => 'unknown', 'label' => t('Identifier')),
),
'provides' => array(
'entity_fetched' => array('type' => 'unknown', 'label' => t('Fetched entity')),
),
'group' => t('Entities'),
'access callback' => 'rules_entity_action_access',
'base' => 'rules_action_entity_fetch',
'callbacks' => array(
'access' => 'rules_action_entity_createfetch_access',
'form_alter' => 'rules_action_type_form_alter',
),
);
$return['entity_query'] = array(
'label' => t('Fetch entity by property'),
'parameter' => array(
'type' => array(
'type' => 'text',
'label' => t('Entity type'),
'options list' => 'rules_entity_action_type_options',
'description' => t('Specifies the type of the entity that should be fetched.'),
'restriction' => 'input',
),
'property' => array(
'type' => 'text',
'label' => t('Property'),
'description' => t('The property by which the entity is to be selected.'),
'restriction' => 'input',
),
'value' => array(
'type' => 'unknown',
'label' => t('Value'),
'description' => t('The property value of the entity to be fetched.'),
),
'limit' => array(
'type' => 'integer',
'label' => t('Limit result count'),
'description' => t('Limit the maximum number of fetched entities.'),
'optional' => TRUE,
'default value' => '10',
),
),
'provides' => array(
'entity_fetched' => array('type' => 'list', 'label' => t('Fetched entity')),
),
'group' => t('Entities'),
'access callback' => 'rules_entity_action_access',
'base' => 'rules_action_entity_query',
'callbacks' => array(
'form_alter' => 'rules_action_type_form_alter',
),
);
}
if (rules_entity_action_type_options('entity_create')) {
$return['entity_create'] = array(
'label' => t('Create a new entity'),
'named parameter' => TRUE,
'parameter' => array(
'type' => array(
'type' => 'text',
'label' => t('Entity type'),
'options list' => 'rules_entity_action_type_options',
'description' => t('Specifies the type of the entity that should be created.'),
'restriction' => 'input',
),
// Further needed parameter depends on the type.
),
'provides' => array(
'entity_created' => array(
'type' => 'unknown',
'label' => t('Created entity'),
'save' => TRUE,
),
),
'group' => t('Entities'),
'access callback' => 'rules_entity_action_access',
'base' => 'rules_action_entity_create',
'callbacks' => array(
'access' => 'rules_action_entity_createfetch_access',
'form_alter' => 'rules_action_type_form_alter',
'validate' => 'rules_action_create_type_validate',
),
);
}
$return['entity_save'] = array(
'label' => t('Save entity'),
'parameter' => array(
'data' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity, which should be saved permanently.'),
'restriction' => 'selector',
'wrapped' => TRUE,
),
'immediate' => array(
'type' => 'boolean',
'label' => t('Force saving immediately'),
'description' => t('Usually saving is postponed till the end of the evaluation, so that multiple saves can be fold into one. If this set, saving is forced to happen immediately.'),
'default value' => FALSE,
'optional' => TRUE,
'restriction' => 'input',
),
),
'group' => t('Entities'),
'access callback' => 'rules_entity_action_access',
'base' => 'rules_action_entity_save',
'callbacks' => array(
'access' => 'rules_action_entity_savedelete_access',
),
);
$return['entity_delete'] = array(
'label' => t('Delete entity'),
'parameter' => array(
'data' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity, which should be deleted permanently.'),
'restriction' => 'selector',
'wrapped' => TRUE,
),
),
'group' => t('Entities'),
'access callback' => 'rules_entity_action_access',
'base' => 'rules_action_entity_delete',
'callbacks' => array(
'access' => 'rules_action_entity_savedelete_access',
),
);
return $return;
}
/**
* Custom access callback for data create and fetch action.
*/
function rules_action_entity_createfetch_access(RulesAbstractPlugin $element) {
$op = $element->getElementName() == 'entity_create' ? 'create' : 'view';
return entity_access($op, $element->settings['type']);
}
/**
* Custom access callback for the data query action.
*/
function rules_action_entity_query_access(RulesAbstractPlugin $element) {
if (!rules_action_entity_createfetch_access($element)) {
return FALSE;
}
$properties = entity_get_all_property_info($element->settings['type']);
if (isset($element->settings['property']) && isset($properties[$element->settings['property']]['access callback'])) {
return call_user_func($properties[$element->settings['property']]['access callback'], 'view', $element->settings['property'], $element->settings['type'], NULL, NULL);
}
return TRUE;
}
/**
* Options list callback for a parameter of entity_create.
*/
function rules_action_entity_parameter_options_list(RulesPlugin $element, $param_name) {
// Remove the parameter name prefix 'param_'.
$property_name = substr($param_name, 6);
$wrapper = entity_metadata_wrapper($element->settings['type']);
// The possible values of the "value" parameter are those of the data param.
return $wrapper->$property_name->optionsList();
}
/**
* Custom access callback for data save and delete action.
*/
function rules_action_entity_savedelete_access(RulesAbstractPlugin $element) {
if ($wrapper = $element->applyDataSelector($element->settings['data:select'])) {
$op = $element->getElementName() == 'entity_save' ? 'save' : 'delete';
return $wrapper instanceof EntityDrupalWrapper && $wrapper->entityAccess($op);
}
return FALSE;
}
/**
* Returns the options list for choosing a property of an entity type.
*/
function rules_action_entity_query_property_options_list(RulesAbstractPlugin $element) {
$element->settings += array('type' => NULL);
if ($element->settings['type']) {
$properties = entity_get_all_property_info($element->settings['type']);
return rules_extract_property($properties, 'label');
}
}
/**
* Returns the options list specified for the chosen property.
*/
function rules_action_entity_query_value_options_list(RulesAbstractPlugin $element) {
// Get the possible values for the selected property.
$element->settings += array('type' => NULL, 'property' => NULL);
if ($element->settings['type'] && $element->settings['property']) {
$wrapper = rules_get_entity_metadata_wrapper_all_properties($element);
if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) {
return $property->optionsList('view');
}
}
}
/**
* Options list callback for data actions.
*
* @param $element
* The element to return options for.
* @param $param
* The name of the parameter to return options for.
*/
function rules_entity_action_type_options($element, $name = NULL) {
// We allow calling this function with just the element name too. That way
// we ease manual re-use.
$name = is_object($element) ? $element->getElementName() : $element;
return ($name == 'entity_create') ? rules_entity_type_options('create') : rules_entity_type_options();
}
/**
* Returns options containing entity types having the given key set in the info.
*
* Additionally, we exclude all entity types that are marked as configuration.
*/
function rules_entity_type_options($key = NULL) {
$info = entity_get_info();
$types = array();
foreach ($info as $type => $entity_info) {
if (empty($entity_info['configuration']) && empty($entity_info['exportable'])) {
if (!isset($key) || entity_type_supports($type, $key)) {
$types[$type] = $entity_info['label'];
}
}
}
return $types;
}
/**
* Options list callback for getting a list of possible entity bundles.
*
* @param $element
* The element to return options for.
*/
function rules_entity_bundle_options(RulesAbstractPlugin $element) {
$bundles = array();
if (isset($element->settings['type'])) {
$entity_info = entity_get_info($element->settings['type']);
foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
$bundles[$bundle_name] = $bundle_info['label'];
}
}
return $bundles;
}
/**
* Entity actions access callback.
*
* Returns TRUE if at least one type is available for configuring the action.
*/
function rules_entity_action_access($type, $name) {
if ($name == 'entity_fetch' || $name == 'entity_create' || $name == 'entity_query') {
$types = array_keys(rules_entity_action_type_options($name));
$op = $name == 'entity_create' ? 'create' : 'view';
}
elseif ($name == 'entity_save' || $name == 'entity_delete') {
$types = array_keys(entity_get_info());
$op = $name == 'entity_save' ? 'save' : 'delete';
}
foreach ($types as $key => $type) {
if (!entity_access($op, $type)) {
unset($types[$key]);
}
}
return !empty($types);
}
/**
* Implements hook_rules_condition_info() on behalf of the entity module.
*
* @see rules_core_modules()
*/
function rules_entity_condition_info() {
return array(
'entity_is_new' => array(
'label' => t('Entity is new'),
'parameter' => array(
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity for which to evaluate the condition.'),
'restriction' => 'selector',
),
),
'group' => t('Entities'),
'base' => 'rules_condition_entity_is_new',
),
'entity_has_field' => array(
'label' => t('Entity has field'),
'parameter' => array(
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity for which to evaluate the condition.'),
'restriction' => 'selector',
),
'field' => array(
'type' => 'text',
'label' => t('Field'),
'description' => t('The name of the field to check for.'),
'options list' => 'rules_condition_entity_has_field_options',
'restriction' => 'input',
),
),
'group' => t('Entities'),
'base' => 'rules_condition_entity_has_field',
),
'entity_is_of_type' => array(
'label' => t('Entity is of type'),
'parameter' => array(
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity for which to evaluate the condition.'),
),
'type' => array(
'type' => 'token',
'label' => t('Entity type'),
'description' => t('The entity type to check for.'),
'options list' => 'rules_entity_action_type_options',
'restriction' => 'input',
),
),
'group' => t('Entities'),
'base' => 'rules_condition_entity_is_of_type',
),
'entity_is_of_bundle' => array(
'label' => t('Entity is of bundle'),
'parameter' => array(
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity for which to evaluate the condition.'),
),
'type' => array(
'type' => 'token',
'label' => t('Entity type'),
'description' => t('The type of the checked entity.'),
'options list' => 'rules_entity_action_type_options',
'restriction' => 'input',
),
'bundle' => array(
'type' => 'list<text>',
'label' => t('Entity bundle'),
'description' => t('The condition is met if the entity is of one of the selected bundles.'),
'options list' => 'rules_entity_bundle_options',
'restriction' => 'input',
),
),
'group' => t('Entities'),
'base' => 'rules_condition_entity_is_of_bundle',
),
'entity_field_access' => array(
'label' => t('User has field access'),
'parameter' => array(
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
'description' => t('Specifies the entity for which to evaluate the condition.'),
'restriction' => 'selector',
'wrapped' => TRUE,
),
'field' => array(
'type' => 'token',
'label' => t('Field name'),
'description' => t('The name of the field to check for.'),
'options list' => 'rules_condition_entity_has_field_options',
'restriction' => 'input',
),
'op' => array(
'type' => 'text',
'label' => t('Access operation'),
'options list' => 'rules_condition_entity_field_access_op_options',
'restriction' => 'input',
'optional' => TRUE,
'default value' => 'view',
),
'account' => array(
'type' => 'user',
'label' => t('User account'),
'description' => t('Specifies the user account for which to check access. If left empty, the currently logged in user will be used.'),
'restriction' => 'selector',
'optional' => TRUE,
'default value' => NULL,
),
),
'group' => t('Entities'),
'base' => 'rules_condition_entity_field_access',
),
);
}
/**
* Help callback for condition entity_is_new.
*/
function rules_condition_entity_is_new_help() {
return t('This condition determines whether the specified entity has just been created and has not yet been saved to the database.');
}
/**
* Returns options for choosing a field for the selected entity.
*/
function rules_condition_entity_has_field_options(RulesAbstractPlugin $element) {
// 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();
}
else {
$fields = field_info_fields();
}
$field_list = drupal_map_assoc(array_keys($fields));
ksort($field_list);
return $field_list;
}
/**
* Returns options for choosing a field_access() operation.
*/
function rules_condition_entity_field_access_op_options(RulesAbstractPlugin $element) {
return array(
'view' => t('View'),
'edit' => t('Edit'),
);
}
/**
* Assert that the entity has the field, if there is metadata for the field.
*/
function rules_condition_entity_has_field_assertions($element) {
// Assert the field is there if the condition matches.
if ($wrapper = $element->applyDataSelector($element->settings['entity:select'])) {
$type = $wrapper->type();
$field_property = $element->settings['field'];
// Get all possible properties and check whether we have one for the field.
$properties = entity_get_all_property_info($type == 'entity' ? NULL : $type);
if (isset($properties[$field_property])) {
$assertion = array('property info' => array($field_property => $properties[$field_property]));
return array($element->settings['entity:select'] => $assertion);
}
}
}
/**
* Assert the selected entity type.
*/
function rules_condition_entity_is_of_type_assertions($element) {
if ($type = $element->settings['type']) {
return array('entity' => array('type' => $type));
}
}
/**
* Assert the selected entity type and bundle.
*/
function rules_condition_entity_is_of_bundle_assertions($element) {
if ($bundle = $element->settings['bundle']) {
$assertions = array();
$assertions['entity']['type'] = $element->settings['type'];
$assertions['entity']['bundle'] = $bundle;
return $assertions;
}
}
/**
* Process callback for the condition entity_is_of_bundle.
*/
function rules_condition_entity_is_of_bundle_process(RulesAbstractPlugin $element) {
// If we know the entity type, auto-populate it.
if (($info = $element->getArgumentInfo('entity')) && $info['type'] != 'entity') {
$element->settings['type'] = $info['type'];
}
}
/**
* Form alter callback for the condition entity_is_of_bundle.
*
* Use multiple steps to configure the condition as the needed bundle field list
* depends on the selected entity type.
*/
function rules_condition_entity_is_of_bundle_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
if (empty($element->settings['entity:select'])) {
$step = 1;
}
elseif (empty($element->settings['type'])) {
$step = 2;
}
else {
$step = 3;
}
$form['reload'] = array(
'#weight' => $form['submit']['#weight'] + 1,
'#type' => 'submit',
'#name' => 'reload',
'#value' => $step != 3 ? t('Continue') : t('Reload form'),
'#limit_validation_errors' => array(array('parameter', 'entity'), array('parameter', 'type')),
'#submit' => array('rules_form_submit_rebuild'),
'#ajax' => rules_ui_form_default_ajax('fade'),
'#attributes' => array('class' => array('rules-hide-js')),
);
// Use ajax and trigger as the reload button.
$form['parameter']['type']['settings']['type']['#ajax'] = $form['reload']['#ajax'] + array(
'event' => 'change',
'trigger_as' => array('name' => 'reload'),
);
switch ($step) {
case 1:
$form['reload']['#limit_validation_errors'] = array(array('parameter', 'entity'));
unset($form['parameter']['type']);
unset($form['reload']['#attributes']['class']);
// 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.
unset($form['parameter']['type']);
}
break;
}
}
/**
* @} End of "addtogroup rules"
*/

225
modules/events.inc

@ -0,0 +1,225 @@
<?php
/**
* @file
* Invokes events on behalf core modules.
*
* For non-core modules, the code to invoke events should be found in the
* module providing rules integration.
*
* @addtogroup rules
*
* @{
*/
/**
* Gets an unchanged entity that doesn't contain any recent changes.
*
* This handler assumes the name of the variable for the changed entity is the
* same as for the unchanged entity but without the trailing "_unchanged"; e.g.,
* for the "node_unchanged" variable the handler assumes there is a "node"
* variable.
*/
function rules_events_entity_unchanged($arguments, $name, $info) {
// Cut of the trailing _unchanged.
$var_name = substr($name, 0, -10);
$entity = $arguments[$var_name];
if (isset($entity->original)) {
return $entity->original;
}
}
/*
* Generic entity events, used for core-entities for which we provide Rules
* integration only. We are implementing the generic-entity hooks instead of
* the entity-type specific hooks to ensure we come last.
* See https://www.drupal.org/node/1211946 for details.
*/
/**
* Implements hook_entity_view().
*/
function rules_entity_view($entity, $type, $view_mode, $langcode) {
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;
}
}
/**
* Implements hook_entity_presave().
*/
function rules_entity_presave($entity, $type) {
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;
}
}
/**
* Implements hook_entity_update().
*/
function rules_entity_update($entity, $type) {
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;
}
}
/**
* Implements hook_entity_insert().
*/
function rules_entity_insert($entity, $type) {
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;
}
}
/**
* Implements hook_entity_delete().
*/
function rules_entity_delete($entity, $type) {
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;
}
}
/**
* Implements hook_user_login().
*/
function rules_user_login(&$edit, $account) {
rules_invoke_event('user_login', $account);
}
/**
* Implements hook_user_logout().
*/
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
* invoke the init event.
*/
/**
* Implements hook_cron().
*/
function rules_cron() {
rules_invoke_event('cron');
}
/**
* Implements hook_watchdog().
*/
function rules_watchdog($log_entry) {
rules_invoke_event('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']);
}
/**
* Gets all view modes of an entity for an entity_view event.
*/
function rules_get_entity_view_modes($name, $var_info) {
// Read the entity type from a special key out of the variable info.
$entity_type = $var_info['options list entity type'];
$info = entity_get_info($entity_type);
foreach ($info['view modes'] as $mode => $mode_info) {
$modes[$mode] = $mode_info['label'];
}
return $modes;
}
/**
* @} End of "addtogroup rules"
*/

143
modules/node.eval.inc

@ -0,0 +1,143 @@
<?php
/**
* @file
* Contains rules integration for the node module needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Base class providing node condition defaults.
*/
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 for selected content types.
*/
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<text>',
'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 published.
*/
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 sticky.
*/
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;
}
}
/**
* @} End of "addtogroup rules"
*/

163
modules/node.rules.inc

@ -0,0 +1,163 @@
<?php
/**
* @file
* Rules integration for the node module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_category_info() on behalf of the node module.
*/
function rules_node_category_info() {
return array(
'node' => array(
'label' => t('Node'),
'equals group' => t('Node'),
),
);
}
/**
* Implements hook_rules_file_info() on behalf of the node module.
*/
function rules_node_file_info() {
return array('modules/node.eval');
}
/**
* Implements hook_rules_event_info() on behalf of the node module.
*/
function rules_node_event_info() {
$items = array(
'node_insert' => array(
'label' => t('After saving new content'),
'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'),
'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'),
'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'),
'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(
'type' => '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' => 'node',
),
),
'access callback' => 'rules_node_integration_access',
'class' => 'RulesNodeEventHandler',
),
'node_delete' => array(
'label' => t('After deleting content'),
'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.
$items['node_presave']['variables']['node']['skip save'] = TRUE;
return $items;
}
/**
* Returns some parameter suitable for using it with a node.
*/
function rules_events_node_variables($node_label, $update = FALSE) {
$args = array(
'node' => array('type' => 'node', 'label' => $node_label),
);
if ($update) {
$args += array(
'node_unchanged' => array(
'type' => 'node',
'label' => t('unchanged content'),
'handler' => 'rules_events_entity_unchanged',
),
);
}
return $args;
}
/**
* Implements hook_rules_action_info() on behalf of the node module.
*/
function rules_node_action_info() {
$defaults = array(
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content'), 'save' => TRUE),
),
'category' => 'node',
'access callback' => 'rules_node_admin_access',
);
// Add support for hand-picked core actions.
$core_actions = node_action_info();
$actions = array('node_publish_action', 'node_unpublish_action', 'node_make_sticky_action', 'node_make_unsticky_action', 'node_promote_action', 'node_unpromote_action');
foreach ($actions as $base) {
$action_name = str_replace('_action', '', $base);
$items[$action_name] = $defaults + array(
'label' => $core_actions[$base]['label'],
'base' => $base,
);
}
return $items;
}
/**
* Node integration access callback.
*/
function rules_node_integration_access($type, $name) {
if ($type == 'event' || $type == 'condition') {
return entity_access('view', 'node');
}
}
/**
* Node integration admin access callback.
*/
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');
}
}
/**
* @} End of "addtogroup rules"
*/

152
modules/path.eval.inc

@ -0,0 +1,152 @@
<?php
/**
* @file
* Contains rules integration for the path module needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Action implementation: Path alias.
*/
function rules_action_path_alias($source, $alias, $langcode = LANGUAGE_NONE) {
if (!$alias) {
path_delete(array('source' => $source, 'language' => $langcode));
}
elseif (!$source) {
path_delete(array('alias' => $alias, 'language' => $langcode));
}
// Only set the alias if the alias is not taken yet.
elseif (!path_load(array('alias' => $alias, 'language' => $langcode))) {
// Update the existing path or create a new one.
if ($path = path_load(array('source' => $source, 'language' => $langcode))) {
$path['alias'] = $alias;
}
else {
$path = array('source' => $source, 'alias' => $alias, 'language' => $langcode);
}
path_save($path);
}
else {
rules_log('The configured alias %alias already exists. Aborting.', array('%alias' => $alias));
}
}
/**
* Action Implementation: Set the URL alias for a node.
*/
function rules_action_node_path_alias($node, $alias) {
$langcode = isset($node->language) ? $node->language : LANGUAGE_NONE;
// Only set the alias if the alias is not taken yet.
if (($path = path_load(array('alias' => $alias, 'language' => $langcode))) && (empty($node->path['pid']) || $node->path['pid'] != $path['pid'])) {
rules_log('The configured alias %alias already exists. Aborting.', array('%alias' => $alias));
return FALSE;
}
$node->path['alias'] = $alias;
}
/**
* Action Implementation: Set the URL alias for a node.
*/
function rules_action_taxonomy_term_path_alias($term, $alias) {
// Only set the alias if the alias is not taken yet.
if (($path = path_load(array('alias' => $alias, 'language' => LANGUAGE_NONE))) && (empty($term->path['pid']) || $term->path['pid'] != $path['pid'])) {
rules_log('The configured alias %alias already exists. Aborting.', array('%alias' => $alias));
return FALSE;
}
$term->path['alias'] = $alias;
}
/**
* Condition implementation: Check if the path has an alias.
*/
function rules_condition_path_has_alias($source, $langcode = LANGUAGE_NONE) {
return (bool) drupal_lookup_path('alias', $source, $langcode);
}
/**
* Condition implementation: Check if the URL alias exists.
*/
function rules_condition_path_alias_exists($alias, $langcode = LANGUAGE_NONE) {
return (bool) drupal_lookup_path('source', $alias, $langcode);
}
/**
* Cleans the given path.
*
* 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
* in the site's settings.php file.
*/
function rules_path_default_cleaning_method($path) {
$replace = variable_get('rules_path_replacement_char', '-');
if ($replace) {
// If the transliteration module is enabled, transliterate the alias first.
if (module_exists('transliteration') && variable_get('rules_path_transliteration', TRUE)) {
$path = transliteration_get($path);
}
$array = variable_get('rules_clean_path', array('/[^a-zA-Z0-9\-_]+/', $replace));
$array[2] = $path;
// Replace it and remove trailing and leading replacement characters.
$output = trim(call_user_func_array('preg_replace', $array), $replace);
if (variable_get('rules_path_lower_case', TRUE)) {
$output = drupal_strtolower($output);
}
return $output;
}
else {
return $path;
}
}
/**
* Cleans the given string so it can be used as part of a URL path.
*/
function rules_clean_path($path) {
$function = variable_get('rules_path_cleaning_callback', 'rules_path_default_cleaning_method');
if (!function_exists($function)) {
rules_log('An invalid URL path cleaning callback has been configured. Falling back to the default cleaning method.', array(), RulesLog::WARN);
$function = 'rules_path_default_cleaning_method';
}
return $function($path);
}
/**
* CTools path cleaning callback.
*
* @see rules_admin_settings()
*/
function rules_path_clean_ctools($path) {
// Make use of the CTools cleanstring implementation.
ctools_include('cleanstring');
$settings = array(
'separator' => variable_get('rules_path_replacement_char', '-'),
'transliterate' => module_exists('transliteration') && variable_get('rules_path_transliteration', TRUE),
'lower case' => variable_get('rules_path_lower_case', TRUE),
);
return ctools_cleanstring($path, $settings);
}
/**
* Pathauto path cleaning callback.
*
* @see rules_admin_settings()
*/
function rules_path_clean_pathauto($path) {
module_load_include('inc', 'pathauto');
return pathauto_cleanstring($path);
}
/**
* @} End of "addtogroup rules"
*/

173
modules/path.rules.inc

@ -0,0 +1,173 @@
<?php
/**
* @file
* Rules integration for the path module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_file_info() on behalf of the path module.
*/
function rules_path_file_info() {
return array('modules/path.eval');
}
/**
* Implements hook_rules_action_info() on behalf of the path module.
*/
function rules_path_action_info() {
return array(
'path_alias' => array(
'label' => t('Create or delete any URL alias'),
'group' => t('Path'),
'parameter' => array(
'source' => 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.'),
'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.'),
'optional' => TRUE,
'cleaning callback' => 'rules_path_clean_replacement_values',
),
'language' => array(
'type' => 'token',
'label' => t('Language'),
'description' => t('If specified, the language for which the URL alias applies.'),
'options list' => 'entity_metadata_language_list',
'optional' => TRUE,
'default value' => LANGUAGE_NONE,
),
),
'base' => 'rules_action_path_alias',
'callbacks' => array('dependencies' => 'rules_path_dependencies'),
'access callback' => 'rules_path_integration_access',
),
'node_path_alias' => array(
'label' => t("Create or delete a content's URL alias"),
'group' => t('Path'),
'parameter' => array(
'node' => array(
'type' => 'node',
'label' => t('Content'),
'save' => TRUE,
),
'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.'),
'optional' => TRUE,
'cleaning callback' => 'rules_path_clean_replacement_values',
),
),
'base' => 'rules_action_node_path_alias',
'callbacks' => array('dependencies' => 'rules_path_dependencies'),
'access callback' => 'rules_path_integration_access',
),
'taxonomy_term_path_alias' => array(
'label' => t("Create or delete a taxonomy term's URL alias"),
'group' => t('Path'),
'parameter' => array(
'node' => array(
'type' => 'taxonomy_term',
'label' => t('Taxonomy term'),
'save' => TRUE,
),
'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.'),
'optional' => TRUE,
'cleaning callback' => 'rules_path_clean_replacement_values',
),
),
'base' => 'rules_action_node_path_alias',
'callbacks' => array('dependencies' => 'rules_path_dependencies'),
'access callback' => 'rules_path_integration_access',
),
);
}
/**
* Callback to specify the path module as dependency.
*/
function rules_path_dependencies() {
return array('path');
}
/**
* Path integration access callback.
*/
function rules_path_integration_access($type, $name) {
if ($type == 'action' && $name == 'path_alias') {
return user_access('administer url aliases');
}
return user_access('create url aliases');
}
/**
* Implements hook_rules_condition_info() on behalf of the path module.
*/
function rules_path_condition_info() {
return array(
'path_has_alias' => array(
'label' => t('Path has URL alias'),
'group' => t('Path'),
'parameter' => array(
'source' => array(
'type' => 'text',
'label' => t('Existing system path'),
'description' => t('Specifies the existing path you wish to check for. For example: node/28, forum/1, taxonomy/term/1+2.'),
'optional' => TRUE,
),
'language' => array(
'type' => 'token',
'label' => t('Language'),
'description' => t('If specified, the language for which the URL alias applies.'),
'options list' => 'entity_metadata_language_list',
'optional' => TRUE,
'default value' => LANGUAGE_NONE,
),
),
'base' => 'rules_condition_path_has_alias',
'callbacks' => array('dependencies' => 'rules_path_dependencies'),
'access callback' => 'rules_path_integration_access',
),
'path_alias_exists' => array(
'label' => t('URL alias exists'),
'group' => t('Path'),
'parameter' => array(
'alias' => array(
'type' => 'text',
'label' => t('URL alias'),
'description' => t('Specify the URL alias to check for. For example, "about" for an about page.'),
'optional' => TRUE,
'cleaning callback' => 'rules_path_clean_replacement_values',
),
'language' => array(
'type' => 'token',
'label' => t('Language'),
'description' => t('If specified, the language for which the URL alias applies.'),
'options list' => 'entity_metadata_language_list',
'optional' => TRUE,
'default value' => LANGUAGE_NONE,
),
),
'base' => 'rules_condition_path_alias_exists',
'callbacks' => array('dependencies' => 'rules_path_dependencies'),
'access callback' => 'rules_path_integration_access',
),
);
}
/**
* @} End of "addtogroup rules"
*/

184
modules/php.eval.inc

@ -0,0 +1,184 @@
<?php
/**
* @file
* Contains rules integration for the php module needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* A class implementing a rules input evaluator processing PHP.
*/
class RulesPHPEvaluator extends RulesDataInputEvaluator {
/**
* 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, '<?') !== FALSE) {
$used_vars = array();
foreach ($var_info as $name => $info) {
if (strpos($text, '$' . $name) !== FALSE) {
$used_vars[] = $name;
}
}
return $used_vars;
}
}
/**
* Overrides RulesDataInputEvaluator::prepare().
*/
public function prepare($text, $var_info) {
// A returned NULL skips the evaluator.
$this->setting = self::getUsedVars($text, $var_info);
}
/**
* 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;
foreach ($this->setting as $key => $var_name) {
$vars[$var_name] = $state->get($var_name);
}
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');
$render = array(
'#type' => 'fieldset',
'#title' => t('PHP Evaluation'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
) + rules_php_evaluator_help($var_info);
return $render;
}
}
/**
* A data processor using PHP.
*/
class RulesPHPDataProcessor extends RulesDataProcessor {
/**
* Overrides RulesDataProcessor::form().
*/
protected static function form($settings, $var_info) {
$settings += array('code' => '');
$form = array(
'#type' => 'fieldset',
'#title' => t('PHP evaluation'),
'#collapsible' => TRUE,
'#collapsed' => empty($settings['code']),
'#description' => t('Enter PHP code to process the selected argument value.'),
);
$form['code'] = array(
'#type' => 'textarea',
'#title' => t('Code'),
'#description' => t('Enter PHP code without &lt;?php ?&gt; delimiters that returns the processed value. The selected value is available in the variable $value. Example: %code', array('%code' => 'return $value + 1;')),
'#default_value' => $settings['code'],
'#weight' => 5,
);
return $form;
}
/**
* 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));
}
}
/**
* Action and condition callback: Execute PHP code.
*/
function rules_execute_php_eval($code, $settings, $state, $element) {
$data = array();
if (!empty($settings['used_vars'])) {
foreach ($settings['used_vars'] as $key => $var_name) {
$data[$var_name] = $state->get($var_name);
}
}
return rules_php_eval_return($code, rules_unwrap_data($data));
}
/**
* Evaluates the given PHP code, with the given variables defined.
*
* @param string $code
* The PHP code to run, including <?php and ?>
* @param array $arguments
* Array containing variables to be extracted to the code.
*
* @return
* The output of the php code.
*/
function rules_php_eval($code, $arguments = array()) {
extract($arguments);
ob_start();
print eval('?>' . $code);
$output = ob_get_contents();
ob_end_clean();
return $output;
}
/**
* Evaluates 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.
*
* @param string $code
* The PHP code to run, without <?php or ?>
* @param array $arguments
* Array containing variables to be extracted to the code.
*
* @return
* The return value of the evaled code.
*/
function rules_php_eval_return($code, $arguments = array()) {
extract($arguments);
return eval($code);
}
/**
* @} End of "addtogroup rules"
*/

159
modules/php.rules.inc

@ -0,0 +1,159 @@
<?php
/**
* @file
* Rules integration for the php module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_file_info() on behalf of the php module.
*/
function rules_php_file_info() {
return array('modules/php.eval');
}
/**
* Implements hook_rules_evaluator_info() on behalf of the php module.
*/
function rules_php_evaluator_info() {
return array(
'php' => array(
'class' => 'RulesPHPEvaluator',
'type' => array('text', 'uri'),
'weight' => -10,
'module' => 'php',
),
);
}
/**
* Implements hook_rules_data_processor_info() on behalf of the php module.
*/
function rules_php_data_processor_info() {
return array(
'php' => array(
'class' => 'RulesPHPDataProcessor',
'type' => array('text', 'token', 'decimal', 'integer', 'date', 'duration', 'boolean', 'uri'),
'weight' => 10,
'module' => 'php',
),
);
}
/**
* Implements hook_rules_action_info() on behalf of the php module.
*/
function rules_php_action_info() {
return array(
'php_eval' => array(
'label' => t('Execute custom PHP code'),
'group' => t('PHP'),
'parameter' => array(
'code' => array(
'restriction' => 'input',
'type' => 'text',
'label' => t('PHP code'),
'description' => t('Enter PHP code without &lt;?php ?&gt; delimiters.'),
),
),
'base' => 'rules_execute_php_eval',
'access callback' => 'rules_php_integration_access',
),
);
}
/**
* Alter the form for improved UX.
*/
function rules_execute_php_eval_form_alter(&$form, &$form_state) {
// Remove the PHP evaluation help to avoid confusion whether <?php tags should
// be used. But keep the help about available variables.
$form['parameter']['code']['settings']['help']['php']['#type'] = 'container';
$form['parameter']['code']['settings']['help']['php']['top']['#markup'] = t('The following variables are available and may be used by your PHP code:');
}
/**
* Process the settings to prepare code execution.
*/
function rules_execute_php_eval_process(RulesAbstractPlugin $element) {
$element->settings['used_vars'] = RulesPHPEvaluator::getUsedVars('<?' . $element->settings['code'], $element->availableVariables());
}
/**
* Specify the php module as dependency.
*/
function rules_execute_php_eval_dependencies() {
return array('php');
}
/**
* PHP integration access callback.
*/
function rules_php_integration_access() {
return user_access('use PHP for settings');
}
/**
* Implements hook_rules_condition_info() on behalf of the PHP module.
*/
function rules_php_condition_info() {
return array(
'php_eval' => array(
'label' => t('Execute custom PHP code'),
'group' => t('PHP'),
'parameter' => array(
'code' => array(
'restriction' => 'input',
'type' => 'text',
'label' => t('PHP code'),
'description' => t('Enter PHP code without &lt;?php ?&gt; delimiters that returns a boolean value; e.g. <code>@code</code>.', array('@code' => "return arg(0) == 'node';")),
),
),
'base' => 'rules_execute_php_eval',
'access callback' => 'rules_php_integration_access',
),
);
}
/**
* Generates help for the PHP actions, conditions and input evaluator.
*/
function rules_php_evaluator_help($var_info, $action_help = FALSE) {
$render['top'] = array(
'#prefix' => '<p>',
'#suffix' => '</p>',
'#markup' => t('PHP code inside of &lt;?php ?&gt; delimiters will be evaluated and replaced by its output. E.g. &lt;? echo 1+1?&gt; will be replaced by 2.') . ' ' . t('Furthermore you can make use of the following variables:'),
);
$render['vars'] = array(
'#theme' => 'table',
'#header' => array(t('Variable name'), t('Type'), t('Description')),
'#attributes' => array('class' => array('rules-php-help')),
);
$cache = rules_get_cache();
foreach ($var_info as $name => $info) {
$row = array();
$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']);
$render['vars']['#rows'][] = $row;
}
if ($action_help) {
$render['updated_help'] = array(
'#prefix' => '<p>',
'#suffix' => '</p>',
'#markup' => t("If you want to change a variable just return an array of new variable values, e.g.: !code", array('!code' => '<pre>return array("node" => $node);</pre>')),
);
}
return $render;
}
/**
* @} End of "addtogroup rules"
*/

280
modules/rules_core.eval.inc

@ -0,0 +1,280 @@
<?php
/**
* @file
* Contains rules core integration needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Action and condition callback: Invokes a rules component.
*
* We do not use the execute() method, but handle executing ourself. That way
* we can utilize the existing state for saving passed variables.
*/
function rules_element_invoke_component($arguments, RulesPlugin $element) {
$info = $element->info();
$state = $arguments['state'];
$wrapped_args = $state->currentArguments;
if ($component = rules_get_cache('comp_' . $info['#config_name'])) {
$replacements = array('%label' => $component->label(), '@plugin' => $component->plugin());
// Handle recursion prevention.
if ($state->isBlocked($component)) {
return rules_log('Not evaluating @plugin %label to prevent recursion.', $replacements, RulesLog::INFO, $component);
}
$state->block($component);
rules_log('Evaluating @plugin %label.', $replacements, RulesLog::INFO, $component, TRUE);
module_invoke_all('rules_config_execute', $component);
// Manually create a new evaluation state and evaluate the component.
$args = array_intersect_key($wrapped_args, $component->parameterInfo());
$new_state = $component->setUpState($wrapped_args);
$return = $component->evaluate($new_state);
// Care for the right return value in case we have to provide vars.
if ($component instanceof RulesActionInterface && !empty($info['provides'])) {
$return = array();
foreach ($info['provides'] as $var => $var_info) {
$return[$var] = $new_state->get($var);
}
}
// Now merge the info about to be saved variables in the parent state.
$state->mergeSaveVariables($new_state, $component, $element->settings);
$state->unblock($component);
// 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;
}
else {
throw new RulesEvaluationException('Unable to get the component %name', array('%name' => $info['#config_name']), $element, RulesLog::ERROR);
}
}
/**
* 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.
$this->setting = NULL;
}
}
/**
* Overrides RulesDataInputEvaluator::evaluate().
*/
public function evaluate($text, $options, RulesState $state) {
return self::gmstrtotime($text);
}
/**
* Convert a time string to a GMT (UTC) unix timestamp.
*/
public static function gmstrtotime($date) {
// Pass the current timestamp in UTC to ensure the retrieved time is UTC.
return strtotime($date, time());
}
/**
* Determine whether the given date string specifies a fixed date.
*/
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.
*
* 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.
$this->setting = NULL;
}
}
/**
* 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
// aliases are applied and query parameters and fragments get handled.
$url = drupal_parse_url($uri);
$url_options = array('absolute' => TRUE);
$url_options['query'] = $url['query'];
$url_options['fragment'] = $url['fragment'];
return url($url['path'], $url_options);
}
elseif (valid_url($uri)) {
return $uri;
}
throw new RulesEvaluationException('Input evaluation generated an invalid URI.', array(), NULL, RulesLog::WARN);
}
}
/**
* A data processor for applying date offsets.
*/
class RulesDateOffsetProcessor extends RulesDataProcessor {
/**
* Overrides RulesDataProcessor::form().
*/
protected static function form($settings, $var_info) {
$settings += array('value' => '');
$form = array(
'#type' => 'fieldset',
'#title' => t('Add offset'),
'#collapsible' => TRUE,
'#collapsed' => empty($settings['value']),
'#description' => t('Add an offset to the selected date.'),
);
$form['value'] = array(
'#type' => 'rules_duration',
'#title' => t('Offset'),
'#description' => t('Note that you can also specify negative numbers.'),
'#default_value' => $settings['value'],
'#weight' => 5,
);
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']);
}
/**
* Intelligently applies the given date offset in seconds.
*
* Intelligently apply duration values > 1 day, i.e. convert the duration
* to its biggest possible unit (months, days) and apply it to the date with
* the given unit. That's necessary as the number of days in a month
* differs, as well as the number of hours for a day (on DST changes).
*/
public static function applyOffset($timestamp, $offset) {
if (abs($offset) >= 86400) {
// Get the days out of the seconds.
$days = intval($offset / 86400);
$sec = $offset % 86400;
// Get the months out of the number of days.
$months = intval($days / 30);
$days = $days % 30;
// Apply the offset using the DateTime::modify and convert it back to a
// timestamp.
$date = date_create("@$timestamp");
$date->modify("$months months $days days $sec seconds");
return $date->format('U');
}
else {
return $timestamp + $offset;
}
}
}
/**
* A data processor for applying numerical offsets.
*/
class RulesNumericOffsetProcessor extends RulesDataProcessor {
/**
* Overrides RulesDataProcessor::form().
*/
protected static function form($settings, $var_info) {
$settings += array('value' => '');
$form = array(
'#type' => 'fieldset',
'#title' => t('Add offset'),
'#collapsible' => TRUE,
'#collapsed' => empty($settings['value']),
'#description' => t('Add an offset to the selected number. E.g. an offset of "1" adds 1 to the number before it is passed on as argument.'),
);
$form['value'] = array(
'#type' => 'textfield',
'#title' => t('Offset'),
'#description' => t('Note that you can also specify negative numbers.'),
'#default_value' => $settings['value'],
'#element_validate' => array('rules_ui_element_integer_validate'),
'#weight' => 5,
);
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.
*
* This class is capable of loading vocabularies by machine name.
*/
class RulesTaxonomyVocabularyWrapper extends EntityDrupalWrapper {
/**
* Overridden to support identifying vocabularies by machine names.
*/
protected function setEntity($data) {
if (isset($data) && $data !== FALSE && !is_object($data) && !is_numeric($data)) {
// The vocabulary name has been passed.
parent::setEntity(taxonomy_vocabulary_machine_name_load($data));
}
else {
parent::setEntity($data);
}
}
/**
* Overridden to permit machine names as values.
*/
public function validate($value) {
if (isset($value) && is_string($value)) {
return TRUE;
}
return parent::validate($value);
}
}
/**
* @} End of "addtogroup rules"
*/

341
modules/rules_core.rules.inc

@ -0,0 +1,341 @@
<?php
/**
* @file
* Rules integration with Drupal core.
*
* Provides data types, conditions, and actions to invoke configured components.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_category_info() on behalf of the rules_core.
*/
function rules_rules_core_category_info() {
return array(
'rules_components' => array(
'label' => t('Components'),
'equals group' => t('Components'),
'weight' => 50,
),
);
}
/**
* Implements hook_rules_file_info() on behalf of the pseudo rules_core module.
*
* @see rules_core_modules()
*/
function rules_rules_core_file_info() {
return array('modules/rules_core.eval');
}
/**
* Implements hook_rules_data_info() on behalf of the pseudo rules_core module.
*
* @see rules_core_modules()
*/
function rules_rules_core_data_info() {
$return = array(
'text' => array(
'label' => t('text'),
'ui class' => 'RulesDataUIText',
'token type' => 'rules_text',
),
'token' => array(
'label' => t('text token'),
'parent' => 'text',
'ui class' => 'RulesDataUITextToken',
'token type' => 'rules_token',
),
// A formatted text as used by entity metadata.
'text_formatted' => array(
'label' => t('formatted text'),
'ui class' => 'RulesDataUITextFormatted',
'wrap' => TRUE,
'property info' => entity_property_text_formatted_info(),
),
'decimal' => array(
'label' => t('decimal number'),
'parent' => 'text',
'ui class' => 'RulesDataUIDecimal',
'token type' => 'rules_decimal',
),
'integer' => array(
'label' => t('integer'),
'class' => 'RulesIntegerWrapper',
'parent' => 'decimal',
'ui class' => 'RulesDataUIInteger',
'token type' => 'rules_integer',
),
'date' => array(
'label' => t('date'),
'ui class' => 'RulesDataUIDate',
'token type' => 'rules_date',
),
'duration' => array(
'label' => t('duration'),
'parent' => 'integer',
'ui class' => 'RulesDataUIDuration',
'token type' => 'rules_duration',
),
'boolean' => array(
'label' => t('truth value'),
'ui class' => 'RulesDataUIBoolean',
'token type' => 'rules_boolean',
),
'uri' => array(
'label' => t('URI'),
'parent' => 'text',
// Clean inserted tokens with "rawurlencode".
'cleaning callback' => 'rawurlencode',
'ui class' => 'RulesDataUIURI',
'token type' => 'rules_uri',
),
'list' => array(
'label' => t('list', array(), array('context' => 'data_types')),
'wrap' => TRUE,
'group' => t('List', array(), array('context' => 'data_types')),
),
'list<text>' => array(
'label' => t('list of text'),
'ui class' => 'RulesDataUIListText',
'wrap' => TRUE,
'group' => t('List', array(), array('context' => 'data_types')),
),
'list<integer>' => array(
'label' => t('list of integer'),
'ui class' => 'RulesDataUIListInteger',
'wrap' => TRUE,
'group' => t('List', array(), array('context' => 'data_types')),
),
'list<token>' => array(
'label' => t('list of text tokens'),
'ui class' => 'RulesDataUIListToken',
'wrap' => TRUE,
'group' => t('List', array(), array('context' => 'data_types')),
),
'entity' => array(
'label' => t('any entity'),
'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'])) {
$return[$type] = array(
'label' => strtolower($info['label'][0]) . substr($info['label'], 1),
'parent' => 'entity',
'wrap' => TRUE,
'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';
}
}
}
if (module_exists('taxonomy')) {
// For exportability identify vocabularies by name.
$return['taxonomy_vocabulary']['wrapper class'] = 'RulesTaxonomyVocabularyWrapper';
$return['taxonomy_vocabulary']['ui class'] = 'RulesDataUITaxonomyVocabulary';
}
return $return;
}
/**
* Implements hook_rules_data_info_alter() on behalf of the pseudo rules_core module.
*
* Makes sure there is a list<type> data type for each type registered.
*
* @see rules_rules_data_info_alter()
*/
function rules_rules_core_data_info_alter(&$data_info) {
foreach ($data_info as $type => $info) {
if (!entity_property_list_extract_type($type)) {
$list_type = "list<$type>";
if (!isset($data_info[$list_type])) {
$data_info[$list_type] = array(
'label' => t('list of @type_label items', array('@type_label' => $info['label'])),
'wrap' => TRUE,
'group' => t('List', array(), array('context' => 'data_types')),
);
if (isset($info['parent']) && $info['parent'] == 'entity') {
$data_info[$list_type]['ui class'] = 'RulesDataUIListEntity';
}
}
}
}
}
/**
* Implements hook_rules_evaluator_info() on behalf of the pseudo rules_core
* module.
*
* @see rules_core_modules()
*/
function rules_rules_core_evaluator_info() {
return array(
// Process strtotime() inputs to timestamps.
'date' => array(
'class' => 'RulesDateInputEvaluator',
'type' => 'date',
'weight' => -10,
),
// Post-process any input value to absolute URIs.
'uri' => array(
'class' => 'RulesURIInputEvaluator',
'type' => 'uri',
'weight' => 50,
),
);
}
/**
* Implements hook_rules_data_processor_info() on behalf of the pseudo
* rules_core module.
*
* @see rules_core_modules()
*/
function rules_rules_core_data_processor_info() {
return array(
'date_offset' => array(
'class' => 'RulesDateOffsetProcessor',
'type' => 'date',
'weight' => -2,
),
'num_offset' => array(
'class' => 'RulesNumericOffsetProcessor',
'type' => array('integer', 'decimal'),
'weight' => -2,
),
);
}
/**
* Implements hook_rules_condition_info() on behalf of the pseudo rules_core
* module.
*
* @see rules_core_modules()
*/
function rules_rules_core_condition_info() {
$defaults = array(
'group' => t('Components'),
'base' => 'rules_element_invoke_component',
'named parameter' => TRUE,
'access callback' => 'rules_element_invoke_component_access_callback',
);
$items = array();
foreach (rules_get_components(FALSE, 'condition') as $name => $config) {
$items['component_' . $name] = $defaults + array(
'label' => $config->plugin() . ': ' . drupal_ucfirst($config->label()),
'parameter' => $config->parameterInfo(),
);
$items['component_' . $name]['#config_name'] = $name;
}
return $items;
}
/**
* Implements hook_rules_action_info() on behalf of the pseudo rules_core
* module.
*
* @see rules_core_modules()
*/
function rules_rules_core_action_info() {
$defaults = array(
'group' => t('Components'),
'base' => 'rules_element_invoke_component',
'named parameter' => TRUE,
'access callback' => 'rules_element_invoke_component_access_callback',
);
$items = array();
foreach (rules_get_components(FALSE, 'action') as $name => $config) {
$items['component_' . $name] = $defaults + array(
'label' => $config->plugin() . ': ' . drupal_ucfirst($config->label()),
'parameter' => $config->parameterInfo(),
'provides' => $config->providesVariables(),
);
$items['component_' . $name]['#config_name'] = $name;
}
return $items;
}
/**
* Implements RulesPluginUIInterface::operations() for the action.
*/
function rules_element_invoke_component_operations(RulesPlugin $element) {
$defaults = $element->extender('RulesPluginUI')->operations();
$info = $element->info();
// Add an operation for editing the component.
$defaults['#links']['component'] = array(
'title' => t('edit component'),
'href' => RulesPluginUI::path($info['#config_name']),
);
return $defaults;
}
/**
* Validate callback to make sure the invoked component exists and is not dirty.
*
* @see rules_scheduler_action_schedule_validate()
*/
function rules_element_invoke_component_validate(RulesPlugin $element) {
$info = $element->info();
$component = rules_config_load($info['#config_name']);
// Check if a component exists.
if (!$component) {
throw new RulesIntegrityException(t('The component %config does not exist.', array('%config' => $info['#config_name'])), $element);
}
// Check if a component is marked as dirty.
rules_config_update_dirty_flag($component);
if (!empty($component->dirty)) {
throw new RulesIntegrityException(t('The utilized component %config fails the integrity check.', array('%config' => $info['#config_name'])), $element);
}
}
/**
* 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.
$info = $element->info();
$pipe['rules_config'][] = $info['#config_name'];
}
/**
* Access callback for the invoke component condition/action.
*/
function rules_element_invoke_component_access_callback($type, $name) {
// Cut of the leading 'component_' from the action name.
$component = rules_config_load(substr($name, 10));
if (!$component) {
// Missing component.
return FALSE;
}
// If access is not exposed for this component, default to component access.
if (empty($component->access_exposed)) {
return $component->access();
}
// Apply the permissions.
return user_access('bypass rules access') || user_access("use Rules component $component->name");
}
/**
* @} End of "addtogroup rules"
*/

305
modules/system.eval.inc

@ -0,0 +1,305 @@
<?php
/**
* @file
* Contains rules integration for the system module needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Action: Show a drupal message.
*/
function rules_action_drupal_message($message, $status, $repeat) {
drupal_set_message(filter_xss_admin($message), $status, $repeat);
}
/**
* Action: Write a watchdog db log message.
*/
function rules_action_drupal_watchdog($type, $message, $severity, $link_text, $link_path) {
if (!empty($link_path)) {
// Use $link_path for the text if no specific text was supplied.
$link = l(!empty($link_text) ? $link_text : $link_path, url($link_path));
}
else {
$link = NULL;
}
watchdog($type, $message, array(), $severity, $link);
}
/**
* Action: Page redirect.
*
* @see rules_page_build()
* @see rules_drupal_goto_alter()
*/
function rules_action_drupal_goto($url, $force = FALSE, $destination = FALSE) {
// Don't let administrators lock them out from Rules administration pages.
if (isset($_GET['q']) && strpos($_GET['q'], 'admin/config/workflow/rules') === 0) {
rules_log('Skipped page redirect on Rules administration page.', array(), RulesLog::WARN);
return;
}
// Do not redirect during batch processing.
if (($batch = batch_get()) && isset($batch['current_set'])) {
rules_log('Skipped page redirect during batch processing.');
return;
}
// Keep the current destination parameter if there is one set.
if ($destination) {
$url .= strpos($url, '?') === FALSE ? '?' : '&';
$url .= drupal_http_build_query(drupal_get_destination());
}
// If force is enabled, remove any destination parameter.
if ($force && isset($_GET['destination'])) {
unset($_GET['destination']);
}
// 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().
$GLOBALS['_rules_action_drupal_goto_do'] = array($url, $force);
}
/**
* Action: Set breadcrumb.
*/
function rules_action_breadcrumb_set(array $titles, array $paths) {
$trail = array(l(t('Home'), ''));
foreach ($titles as $i => $title) {
// Skip empty titles.
if ($title = trim($title)) {
// Output plaintext instead of a link if there is a title without a path.
$path = trim($paths[$i]);
if (!empty($paths[$i]) && $paths[$i] != '<none>') {
$trail[] = l($title, trim($paths[$i]));
}
else {
$trail[] = check_plain($title);
}
}
}
drupal_set_breadcrumb($trail);
}
/**
* Action Implementation: Send mail.
*/
function rules_action_mail($to, $subject, $message, $from = NULL, $langcode, $settings, RulesState $state, RulesPlugin $element) {
$to = str_replace(array("\r", "\n"), '', $to);
$from = !empty($from) ? str_replace(array("\r", "\n"), '', $from) : NULL;
$params = array(
'subject' => $subject,
'message' => $message,
'langcode' => $langcode,
);
// Set a unique key for this mail.
$name = isset($element->root()->name) ? $element->root()->name : 'unnamed';
$key = 'rules_action_mail_' . $name . '_' . $element->elementId();
$languages = language_list();
$language = isset($languages[$langcode]) ? $languages[$langcode] : language_default();
$message = drupal_mail('rules', $key, $to, $language, $params, $from);
if ($message['result']) {
watchdog('rules', 'Successfully sent email to %recipient', array('%recipient' => $to));
}
}
/**
* Action: Send mail to all users of a specific role group(s).
*/
function rules_action_mail_to_users_of_role($roles, $subject, $message, $from = NULL, $settings, RulesState $state, RulesPlugin $element) {
$from = !empty($from) ? str_replace(array("\r", "\n"), '', $from) : NULL;
// All authenticated users, which is everybody.
if (in_array(DRUPAL_AUTHENTICATED_RID, $roles)) {
$result = db_query('SELECT mail FROM {users} WHERE uid > 0');
}
else {
// 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)', array(':rids' => $roles));
}
// Now, actually send the mails.
$params = array(
'subject' => $subject,
'message' => $message,
);
// Set a unique key for this mail.
$name = isset($element->root()->name) ? $element->root()->name : 'unnamed';
$key = 'rules_action_mail_to_users_of_role_' . $name . '_' . $element->elementId();
$languages = language_list();
$message = array('result' => TRUE);
foreach ($result as $row) {
$message = drupal_mail('rules', $key, $row->mail, language_default(), $params, $from);
// 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'] !== 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)));
}
}
/**
* Implements hook_mail().
*
* Sets the message subject and body as configured.
*/
function rules_mail($key, &$message, $params) {
$message['subject'] .= str_replace(array("\r", "\n"), '', $params['subject']);
$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.
$this->setting = token_scan($text) ? TRUE : NULL;
}
/**
* 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.
* So we properly map variable names to variable data types and then run the
* replacement ourself.
*/
public function evaluate($text, $options, RulesState $state) {
$var_info = $state->varInfo();
$options += array('sanitize' => FALSE);
$replacements = array();
$data = array();
// We also support replacing tokens in a list of textual values.
$whole_text = is_array($text) ? implode('', $text) : $text;
foreach (token_scan($whole_text) as $var_name => $tokens) {
$var_name = str_replace('-', '_', $var_name);
if (isset($var_info[$var_name]) && ($token_type = _rules_system_token_map_type($var_info[$var_name]['type']))) {
// We have to key $data with the type token uses for the variable.
$data = rules_unwrap_data(array($token_type => $state->get($var_name)), array($token_type => $var_info[$var_name]));
$replacements += token_generate($token_type, $tokens, $data, $options);
}
else {
$replacements += token_generate($var_name, $tokens, array(), $options);
}
// Remove tokens if no replacement value is found. As token_replace() does
// if 'clear' is set.
$replacements += array_fill_keys($tokens, '');
}
// Optionally clean the list of replacement values.
if (!empty($options['callback']) && function_exists($options['callback'])) {
$function = $options['callback'];
$function($replacements, $data, $options);
}
// Actually apply the replacements.
$tokens = array_keys($replacements);
$values = array_values($replacements);
if (is_array($text)) {
foreach ($text as $i => $text_item) {
$text[$i] = str_replace($tokens, $values, $text_item);
}
return $text;
}
return str_replace($tokens, $values, $text);
}
/**
* Create documentation about the available replacement patterns.
*
* @param array $var_info
* Array with the available variables.
*
* @return array
* Renderable array with the replacement pattern documentation.
*/
public static function help($var_info) {
$render = array(
'#type' => 'fieldset',
'#title' => t('Replacement patterns'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#description' => t('Note that token replacements containing chained objects – such as [node:author:uid] – are not listed here, but are still available. The <em>data selection</em> input mode may help you find more complex replacement patterns. See <a href="@url">the online documentation</a> for more information about complex replacement patterns.',
array('@url' => rules_external_help('chained-tokens'))),
);
$token_info = token_info();
foreach ($var_info as $name => $info) {
$token_types[$name] = _rules_system_token_map_type($info['type']);
}
foreach ($token_types as $name => $token_type) {
if (isset($token_info['types'][$token_type])) {
$render[$name] = array(
'#theme' => 'table',
'#header' => array(t('Token'), t('Label'), t('Description')),
'#prefix' => '<h3>' . t('Replacement patterns for %label', array('%label' => $var_info[$name]['label'])) . '</h3>',
);
foreach ($token_info['tokens'][$token_type] as $token => $info) {
$token = '[' . str_replace('_', '-', $name) . ':' . $token . ']';
$render[$name]['#rows'][$token] = array(
check_plain($token),
check_plain($info['name']),
check_plain($info['description']),
);
}
}
}
return $render;
}
}
/**
* Looks for a token type mapping. Defaults to passing through the type.
*/
function _rules_system_token_map_type($type) {
$entity_info = entity_get_info();
if (isset($entity_info[$type]['token type'])) {
return $entity_info[$type]['token type'];
}
$cache = rules_get_cache();
if (isset($cache['data_info'][$type]['token type'])) {
return $cache['data_info'][$type]['token type'];
}
return $type;
}
/**
* @} End of "addtogroup rules"
*/

345
modules/system.rules.inc

@ -0,0 +1,345 @@
<?php
/**
* @file
* Rules integration for the system module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_file_info() on behalf of the system module.
*/
function rules_system_file_info() {
return array('modules/system.eval');
}
/**
* Implements hook_rules_event_info() on behalf of the system module.
*/
function rules_system_event_info() {
return array(
'init' => array(
'label' => t('Drupal is initializing'),
'group' => t('System'),
'help' => t("Be aware that some actions might initialize the theme system. After that, it's impossible for any module to change the used theme."),
'access callback' => 'rules_system_integration_access',
),
'cron' => array(
'label' => t('Cron maintenance tasks are performed'),
'group' => t('System'),
'access callback' => 'rules_system_integration_access',
),
'watchdog' => array(
'label' => t('System log entry is created'),
'variables' => array(
'log_entry' => array(
'type' => 'log_entry',
'label' => t('Log entry'),
),
),
'group' => t('System'),
'access callback' => 'rules_system_integration_access',
),
);
}
/**
* Implements hook_rules_data_info() on behalf of the system module.
*
* @see rules_core_modules()
*/
function rules_system_data_info() {
return array(
'log_entry' => array(
'label' => t('Watchdog log entry'),
'wrap' => TRUE,
'property info' => _rules_system_watchdog_log_entry_info(),
),
);
}
/**
* Defines property info for watchdog log entries.
*
* Used by the log entry data type to provide a useful metadata wrapper.
*/
function _rules_system_watchdog_log_entry_info() {
return array(
'type' => array(
'type' => 'text',
'label' => t('The category to which this message belongs'),
),
'message' => array(
'type' => 'text',
'label' => t('Log message'),
'getter callback' => 'rules_system_log_get_message',
'sanitized' => TRUE,
),
'severity' => array(
'type' => 'integer',
'label' => t('Severity'),
'options list' => 'watchdog_severity_levels',
),
'request_uri' => array(
'type' => 'uri',
'label' => t('Request uri'),
),
'link' => array(
'type' => 'text',
'label' => t('An associated, HTML formatted link'),
),
);
}
/**
* Implements hook_rules_action_info() on behalf of the system module.
*/
function rules_system_action_info() {
return array(
'drupal_message' => array(
'label' => t('Show a message on the site'),
'group' => t('System'),
'parameter' => array(
'message' => array(
'type' => 'text',
'label' => t('Message'),
'sanitize' => TRUE,
'translatable' => TRUE,
),
'type' => array(
'type' => 'token',
'label' => t('Message type'),
'options list' => 'rules_action_drupal_message_types',
'default value' => 'status',
'optional' => TRUE,
),
'repeat' => array(
'type' => 'boolean',
'label' => t('Repeat message'),
'description' => t("If disabled and the message has been already shown, then the message won't be repeated."),
'default value' => TRUE,
'optional' => TRUE,
'restriction' => 'input',
),
),
'base' => 'rules_action_drupal_message',
'access callback' => 'rules_system_integration_access',
),
'drupal_watchdog' => array(
'label' => t('Logs a message to the system dblog'),
'group' => t('System'),
'parameter' => array(
'type' => array(
'type' => 'text',
'label' => t('Type'),
'description' => t('The category to which this message belongs, displayed in the first column of the dblog.'),
),
'message' => array(
'type' => 'text',
'label' => t('Message'),
'description' => t('The text of the message.'),
'translatable' => TRUE,
),
'severity' => array(
'type' => 'token',
'label' => t('Severity'),
'options list' => 'watchdog_severity_levels',
'default value' => WATCHDOG_NOTICE,
'optional' => TRUE,
),
'link_text' => array(
'type' => 'text',
'label' => t('Link text'),
'description' => t('The text to display for the link (optional)'),
'optional' => TRUE,
),
'link_path' => array(
'type' => 'uri',
'label' => t('Link path'),
'description' => t('A Drupal path or path alias. Enter (optional) queries after "?" and (optional) anchor after "#".'),
'optional' => TRUE,
),
),
'base' => 'rules_action_drupal_watchdog',
'access callback' => 'rules_system_integration_access',
),
'redirect' => array(
'label' => t('Page redirect'),
'group' => t('System'),
'parameter' => array(
'url' => array(
'type' => 'uri',
'label' => t('URL'),
'description' => t('A Drupal path, path alias, or external URL to redirect to. Enter (optional) queries after "?" and (optional) anchor after "#".'),
),
'force' => array(
'type' => 'boolean',
'label' => t('Force redirect'),
'restriction' => 'input',
'description' => t("Force the redirect even if another destination parameter is present. Per default Drupal would redirect to the path given as destination parameter, in case it is set. Usually the destination parameter is set by appending it to the URL, e.g. !example_url", array('!example_url' => 'http://example.com/user/login?destination=node/2')),
'optional' => TRUE,
'default value' => TRUE,
),
'destination' => array(
'type' => 'boolean',
'label' => t('Append destination parameter'),
'restriction' => 'input',
'description' => t('Whether to append a destination parameter to the URL, so another redirect issued later on would lead back to the origin page.'),
'optional' => TRUE,
'default value' => FALSE,
),
),
'base' => 'rules_action_drupal_goto',
'access callback' => 'rules_system_integration_access',
),
'breadcrumb_set' => array(
'label' => t('Set breadcrumb'),
'group' => t('System'),
'parameter' => array(
'titles' => array(
'type' => 'list<text>',
'label' => t('Titles'),
'description' => t('A list of titles for the breadcrumb links.'),
'translatable' => TRUE,
),
'paths' => array(
'type' => 'list<text>',
'label' => t('Paths'),
'description' => t('A list of Drupal paths for the breadcrumb links, matching the order of the titles.'),
),
),
'base' => 'rules_action_breadcrumb_set',
'access callback' => 'rules_system_integration_access',
),
'mail' => array(
'label' => t('Send mail'),
'group' => t('System'),
'parameter' => array(
'to' => array(
'type' => 'text',
'label' => t('To'),
'description' => t('The e-mail address or addresses where the message will be sent to. The formatting of this string must comply with RFC 2822.'),
),
'subject' => array(
'type' => 'text',
'label' => t('Subject'),
'description' => t("The mail's subject."),
'translatable' => TRUE,
),
'message' => array(
'type' => 'text',
'label' => t('Message'),
'description' => t("The mail's message body."),
'translatable' => TRUE,
),
'from' => array(
'type' => 'text',
'label' => t('From'),
'description' => t("The mail's from address. Leave it empty to use the site-wide configured address."),
'optional' => TRUE,
),
'language' => array(
'type' => 'token',
'label' => t('Language'),
'description' => t('If specified, the language used for getting the mail message and subject.'),
'options list' => 'entity_metadata_language_list',
'optional' => TRUE,
'default value' => LANGUAGE_NONE,
'default mode' => 'selector',
),
),
'base' => 'rules_action_mail',
'access callback' => 'rules_system_integration_access',
),
'mail_to_users_of_role' => array(
'label' => t('Send mail to all users of a role'),
'group' => t('System'),
'parameter' => array(
'roles' => array(
'type' => 'list<integer>',
'label' => t('Roles'),
'options list' => 'entity_metadata_user_roles',
'description' => t('Select the roles whose users should receive the mail.'),
),
'subject' => array(
'type' => 'text',
'label' => t('Subject'),
'description' => t("The mail's subject."),
),
'message' => array(
'type' => 'text',
'label' => t('Message'),
'description' => t("The mail's message body."),
),
'from' => array(
'type' => 'text',
'label' => t('From'),
'description' => t("The mail's from address. Leave it empty to use the site-wide configured address."),
'optional' => TRUE,
),
),
'base' => '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',
),
);
}
/**
* Help callback for the "Send mail to users of a role" action.
*/
function rules_action_mail_to_users_of_role_help() {
return t('WARNING: This may cause problems if there are too many users of these roles on your site, as your server may not be able to handle all the mail requests all at once.');
}
/**
* System integration access callback.
*/
function rules_system_integration_access($type, $name) {
return user_access('administer site configuration');
}
/**
* Options list callback defining drupal_message types.
*/
function rules_action_drupal_message_types() {
return array(
'status' => t('Status'),
'warning' => t('Warning'),
'error' => t('Error'),
);
}
/**
* Implements hook_rules_evaluator_info() on behalf of the system module.
*/
function rules_system_evaluator_info() {
return array(
'token' => array(
'class' => 'RulesTokenEvaluator',
'type' => array('text', 'uri', 'list<text>', 'list<uri>'),
'weight' => 0,
),
);
}
/**
* @} End of "addtogroup rules"
*/

150
modules/taxonomy.rules.inc

@ -0,0 +1,150 @@
<?php
/**
* @file
* Rules integration for the taxonomy_term module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_event_info().
*/
function rules_taxonomy_event_info() {
$defaults_term = array(
'group' => t('Taxonomy'),
'access callback' => 'rules_taxonomy_term_integration_access',
'module' => 'taxonomy',
'class' => 'RulesTaxonomyEventHandler',
);
$defaults_vocab = array(
'group' => t('Taxonomy'),
'access callback' => 'rules_taxonomy_vocabulary_integration_access',
'module' => 'taxonomy',
);
return array(
'taxonomy_term_insert' => $defaults_term + array(
'label' => t('After saving a new term'),
'variables' => array(
'term' => array('type' => 'taxonomy_term', 'label' => t('created term')),
),
),
'taxonomy_term_update' => $defaults_term + array(
'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',
),
),
),
'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',
),
),
),
'taxonomy_term_delete' => $defaults_term + array(
'label' => t('After deleting a term'),
'variables' => array(
'term' => array('type' => 'taxonomy_term', 'label' => t('deleted term')),
),
),
'taxonomy_vocabulary_insert' => $defaults_vocab + array(
'label' => t('After saving a new vocabulary'),
'variables' => array(
'vocabulary' => array('type' => 'taxonomy_vocabulary', 'label' => t('created vocabulary')),
),
),
'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',
),
),
),
'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',
),
),
),
'taxonomy_vocabulary_delete' => $defaults_vocab + array(
'label' => t('After deleting a vocabulary'),
'variables' => array(
'vocabulary' => array(
'type' => 'taxonomy_vocabulary',
'label' => t('deleted vocabulary'),
),
),
),
);
}
/**
* Taxonomy term integration access callback.
*/
function rules_taxonomy_term_integration_access($type, $name) {
if ($type == 'event' || $type == 'condition') {
return entity_access('view', 'taxonomy_term');
}
}
/**
* Taxonomy vocabulary integration access callback.
*/
function rules_taxonomy_vocabulary_integration_access($type, $name) {
if ($type == 'event' || $type == 'condition') {
return entity_access('view', 'taxonomy_vocabulary');
}
}
/**
* 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');
}
}
/**
* @} End of "addtogroup rules"
*/

135
modules/user.eval.inc

@ -0,0 +1,135 @@
<?php
/**
* @file
* Contains rules integration for the user module needed during evaluation.
*
* @addtogroup rules
*
* @{
*/
/**
* Condition user: condition to check whether user has particular roles.
*/
function rules_condition_user_has_role($account, $roles, $operation = 'AND') {
switch ($operation) {
case 'OR':
foreach ($roles as $rid) {
if (isset($account->roles[$rid])) {
return TRUE;
}
}
return FALSE;
case 'AND':
foreach ($roles as $rid) {
if (!isset($account->roles[$rid])) {
return FALSE;
}
}
return TRUE;
}
}
/**
* Condition: User is blocked.
*/
function rules_condition_user_is_blocked($account) {
return $account->status == 0;
}
/**
* Action: Adds roles to a particular user.
*/
function rules_action_user_add_role($account, $roles) {
if ($account->uid || !empty($account->is_new)) {
// Get role list (minus the anonymous).
$role_list = user_roles(TRUE);
foreach ($roles as $rid) {
$account->roles[$rid] = $role_list[$rid];
}
if (!empty($account->is_new) && $account->uid) {
// user_save() inserts roles after invoking hook_user_insert() anyway, so
// we skip saving to avoid errors due saving them twice.
return FALSE;
}
}
else {
return FALSE;
}
}
/**
* Action: Remove roles from a given user.
*/
function rules_action_user_remove_role($account, $roles) {
if ($account->uid || !empty($account->is_new)) {
foreach ($roles as $rid) {
// If the user has this role, remove it.
if (isset($account->roles[$rid])) {
unset($account->roles[$rid]);
}
}
if (!empty($account->is_new) && $account->uid) {
// user_save() inserts roles after invoking hook_user_insert() anyway, so
// we skip saving to avoid errors due saving them twice.
return FALSE;
}
}
else {
return FALSE;
}
}
/**
* Action: Block a user.
*/
function rules_action_user_block($account) {
$account->status = 0;
drupal_session_destroy_uid($account->uid);
}
/**
* Action: Unblock a user.
*/
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));
}
}
}
/**
* @} End of "addtogroup rules"
*/

277
modules/user.rules.inc

@ -0,0 +1,277 @@
<?php
/**
* @file
* Rules integration for the user module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_file_info() on behalf of the user module.
*/
function rules_user_file_info() {
return array('modules/user.eval');
}
/**
* Implements hook_rules_event_info().
*/
function rules_user_event_info() {
return array(
'user_insert' => array(
'label' => t('After saving a new user account'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('registered user')),
),
'access callback' => 'rules_user_integration_access',
),
'user_update' => array(
'label' => t('After updating an existing user account'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('updated user')),
'account_unchanged' => array('type' => 'user', 'label' => t('unchanged user'), 'handler' => 'rules_events_entity_unchanged'),
),
'access callback' => 'rules_user_integration_access',
),
'user_presave' => array(
'label' => t('Before saving a user account'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('saved user'), 'skip save' => TRUE),
'account_unchanged' => array('type' => 'user', 'label' => t('unchanged user'), 'handler' => 'rules_events_entity_unchanged'),
),
'access callback' => 'rules_user_integration_access',
),
'user_view' => array(
'label' => t('User account page is viewed'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('viewed user')),
'view_mode' => array(
'type' => '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',
'help' => t("Note that if drupal's page cache is enabled, this event won't be generated for pages served from cache."),
),
'user_delete' => array(
'label' => t('After a user account has been deleted'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('deleted user')),
),
'access callback' => 'rules_user_integration_access',
),
'user_login' => array(
'label' => t('User has logged in'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('logged in user')),
),
'access callback' => 'rules_user_integration_access',
),
'user_logout' => array(
'label' => t('User has logged out'),
'group' => t('User'),
'variables' => array(
'account' => array('type' => 'user', 'label' => t('logged out user')),
),
'access callback' => 'rules_user_integration_access',
),
);
}
/**
* Options list for user cancel methods.
*
* @todo Use for providing a user_cancel action.
*/
function rules_user_cancel_methods() {
module_load_include('inc', 'user', 'user.pages');
foreach (user_cancel_methods() as $method => $form) {
$methods[$method] = $form['#title'];
}
return $methods;
}
/**
* User integration access callback.
*/
function rules_user_integration_access($type, $name) {
if ($type == 'event' || $type == 'condition') {
return entity_access('view', 'user');
}
// Else return admin access.
return user_access('administer users');
}
/**
* Implements hook_rules_condition_info() on behalf of the user module.
*/
function rules_user_condition_info() {
return array(
'user_has_role' => array(
'label' => t('User has role(s)'),
'parameter' => array(
'account' => array('type' => 'user', 'label' => t('User')),
'roles' => array(
'type' => 'list<integer>',
'label' => t('Roles'),
'options list' => 'rules_user_roles_options_list',
),
'operation' => array(
'type' => 'text',
'label' => t('Match roles'),
'options list' => 'rules_user_condition_operations',
'restriction' => 'input',
'optional' => TRUE,
'default value' => 'AND',
'description' => t('If matching against all selected roles, the user must have <em>all</em> the roles selected.'),
),
),
'group' => t('User'),
'access callback' => 'rules_user_integration_access',
'base' => 'rules_condition_user_has_role',
),
'user_is_blocked' => array(
'label' => t('User is blocked'),
'parameter' => array(
'account' => array('type' => 'user', 'label' => t('User')),
),
'group' => t('User'),
'access callback' => 'rules_user_integration_access',
'base' => 'rules_condition_user_is_blocked',
),
);
}
/**
* User has role condition help callback.
*/
function rules_condition_user_has_role_help() {
return t('Whether the user has the selected role(s).');
}
/**
* Options list callback for the operation parameter of condition user has role.
*/
function rules_user_condition_operations() {
return array(
'AND' => t('all'),
'OR' => t('any'),
);
}
/**
* Implements hook_rules_action_info() on behalf of the user module.
*/
function rules_user_action_info() {
$defaults = array(
'parameter' => array(
'account' => array(
'type' => 'user',
'label' => t('User'),
'description' => t('The user whose roles should be changed.'),
'save' => TRUE,
),
'roles' => array(
'type' => 'list<integer>',
'label' => t('Roles'),
'options list' => 'rules_user_roles_options_list',
),
),
'group' => t('User'),
'access callback' => 'rules_user_role_change_access',
);
$items['user_add_role'] = $defaults + array(
'label' => t('Add user role'),
'base' => 'rules_action_user_add_role',
);
$items['user_remove_role'] = $defaults + array(
'label' => t('Remove user role'),
'base' => 'rules_action_user_remove_role',
);
$defaults = array(
'parameter' => array(
'account' => array(
'type' => 'user',
'label' => t('User'),
'save' => TRUE,
),
),
'group' => t('User'),
'access callback' => 'rules_user_integration_access',
);
$items['user_block'] = $defaults + array(
'label' => t('Block a user'),
'base' => 'rules_action_user_block',
);
$items['user_unblock'] = $defaults + array(
'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;
}
/**
* User integration role actions access callback.
*/
function rules_user_role_change_access() {
return entity_metadata_user_roles() && user_access('administer permissions');
}
/**
* Options list callback for user roles.
*/
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'),
);
}
/**
* @} End of "addtogroup rules"
*/

1103
rules.api.php

File diff suppressed because it is too large Load Diff

252
rules.drush.inc

@ -0,0 +1,252 @@
<?php
/**
* @file
* Rules module drush integration.
*/
/**
* Implements hook_drush_command().
*/
function rules_drush_command() {
$items = array();
$items['rules-list'] = array(
'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.',
'arguments' => array(
'rule' => 'Rule name to enable.',
),
'drupal dependencies' => array('rules'),
'aliases' => array('re'),
);
$items['rules-disable'] = array(
'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;
}
/**
* Implements hook_drush_help().
*/
function rules_drush_help($section) {
switch ($section) {
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.');
case 'drush:rules-disable':
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.');
}
}
/**
* Get a list of all rules.
*/
function drush_rules_list() {
$rules = rules_config_load_multiple(FALSE);
$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)),
),
);
$events[] = check_plain($event_info[$event_name]['label']);
}
}
$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)) : '',
);
}
}
if (version_compare(DRUSH_VERSION, '6.0', '<')) {
drush_print_table($rows, TRUE);
}
return $rows;
}
/**
* Enable a rule on the site.
*/
function drush_rules_enable() {
$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 (empty($rule->active)) {
$rule->active = TRUE;
$rule->save();
drush_log(dt('The rule "!name" has been enabled.', array('!name' => $rule_name)), 'success');
}
else {
drush_log(dt('The rule "!name" is already enabled.', array('!name' => $rule_name)), 'warning');
}
}
/**
* Disable a rule on the site.
*/
function drush_rules_disable() {
$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 (!empty($rule->active)) {
$rule->active = FALSE;
$rule->save();
drush_log(dt('The rule "!name" has been disabled.', array('!name' => $rule_name)), 'success');
}
else {
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');
}

91
rules.features.inc

@ -0,0 +1,91 @@
<?php
/**
* @file
* 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.
*/
class RulesFeaturesController extends EntityDefaultFeaturesController {
/**
* Defines the result for hook_features_api().
*/
public function api() {
$info = parent::api();
$info['rules_config']['default_file'] = FEATURES_DEFAULTS_CUSTOM;
$info['rules_config']['default_filename'] = 'rules_defaults';
return $info;
}
/**
* Generates the result for hook_features_export().
*
* Overridden to add in rules-specific stuff.
*/
public function export($data, &$export, $module_name = '') {
$pipe = parent::export($data, $export, $module_name);
foreach (entity_load_multiple_by_name($this->type, $data) as $name => $rules_config) {
// Add in the dependencies.
$export['dependencies'] += drupal_map_assoc($rules_config->dependencies());
// Add in plugin / element specific additions.
$iterator = new RecursiveIteratorIterator($rules_config, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $element) {
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 and conditions may override this with an implementation which
* actually does something.
*
* @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.
*
* @see hook_rules_plugin_info()
* @see hook_rules_action_info()
*/
interface RulesPluginFeaturesIntegrationInterface {
/**
* Allows customizing the features export for a given rule element.
*/
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 {
}

36
rules.info

@ -0,0 +1,36 @@
name = Rules
description = React on events and conditionally evaluate actions.
package = Rules
core = 7.x
files[] = rules.features.inc
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[] = system (>= 7.40)
dependencies[] = entity
dependencies[] = entity:entity_token
; Information added by Drupal.org packaging script on 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

563
rules.install

@ -0,0 +1,563 @@
<?php
/**
* @file
* Rules - Installation file.
*/
/**
* Implements hook_enable().
*/
function rules_enable() {
// Enable evaluation of Rules right after enabling the module.
rules_event_invocation_enabled(TRUE);
}
/**
* Implements hook_install().
*/
function rules_install() {
module_load_include('inc', 'rules', 'modules/events');
// Set the modules' weight to 20, see
// https://www.drupal.org/node/445084#comment-1533280 for the reasoning.
db_query("UPDATE {system} SET weight = 20 WHERE name = 'rules'");
}
/**
* Implements hook_uninstall().
*/
function rules_uninstall() {
variable_del('rules_debug');
variable_del('rules_debug_log');
variable_del('rules_log_errors');
variable_del('rules_log_level');
variable_del('rules_clean_path');
variable_del('rules_path_cleaning_callback');
variable_del('rules_path_lower_case');
variable_del('rules_path_replacement_char');
variable_del('rules_path_transliteration');
// Delete all the debug region variables and then clear the variables cache.
db_delete('variable')
->condition('name', 'rules_debug_region_%', 'LIKE')
->execute();
cache_clear_all('variables', 'cache_bootstrap');
}
/**
* Implements hook_schema().
*/
function rules_schema() {
$schema['rules_config'] = array(
'fields' => array(
'id' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'The internal identifier for any configuration.',
),
'name' => array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'The name of the configuration.',
),
'label' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The label of the configuration.',
'default' => 'unlabeled',
),
'plugin' => array(
'type' => 'varchar',
'length' => 127,
'not null' => TRUE,
'description' => 'The name of the plugin of this configuration.',
),
'active' => array(
'description' => 'Boolean indicating whether the configuration is active. Usage depends on how the using module makes use of it.',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'Weight of the configuration. Usage depends on how the using module makes use of it.',
),
'status' => array(
'type' => 'int',
'not null' => TRUE,
// Set the default to ENTITY_CUSTOM without using the constant as it is
// not safe to use it at this point.
'default' => 0x01,
'size' => 'tiny',
'description' => 'The exportable status of the entity.',
),
'dirty' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'Dirty configurations fail the integrity check, e.g. due to missing dependencies.',
),
'module' => array(
'description' => 'The name of the providing module if the entity has been defined in code.',
'type' => 'varchar',
'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,
'default' => 0,
'size' => 'tiny',
'description' => 'Whether to use a permission to control access for using components.',
),
'data' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
'description' => 'Everything else, serialized.',
),
),
'primary key' => array('id'),
'unique keys' => array(
'name' => array('name'),
),
'indexes' => array(
'plugin' => array('plugin', 'active'),
),
);
$schema['rules_trigger'] = array(
'fields' => array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The primary identifier of the configuration.',
),
'event' => array(
'type' => 'varchar',
'length' => '127',
'not null' => TRUE,
'default' => '',
'description' => 'The name of the event on which the configuration should be triggered.',
),
),
'primary key' => array('id', 'event'),
'foreign keys' => array(
'table' => 'rules_config',
'columns' => array('id' => 'id'),
),
);
$schema['rules_tags'] = array(
'fields' => array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The primary identifier of the configuration.',
),
'tag' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The tag string associated with this configuration',
),
),
'primary key' => array('id', 'tag'),
'foreign keys' => array(
'table' => 'rules_config',
'columns' => array('id' => 'id'),
),
);
$schema['rules_dependencies'] = array(
'fields' => array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The primary identifier of the configuration.',
),
'module' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The name of the module that is required for the configuration.',
),
),
'primary key' => array('id', 'module'),
'indexes' => array(
'module' => array('module'),
),
'foreign keys' => array(
'table' => 'rules_config',
'columns' => array('id' => 'id'),
),
);
$schema['cache_rules'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_rules']['description'] = 'Cache table for the rules engine to store configured items.';
return $schema;
}
/**
* Upgrade from Rules 6.x-1.x to 7.x.
*/
function rules_update_7200() {
// Create the new db tables first.
$schema['rules_config'] = array(
'fields' => array(
'id' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'The internal identifier for any configuration.',
),
'name' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The name of the configuration.',
),
'label' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The label of the configuration.',
'default' => 'unlabeled',
),
'plugin' => array(
'type' => 'varchar',
'length' => 127,
'not null' => TRUE,
'description' => 'The name of the plugin of this configuration.',
),
'active' => array(
'description' => 'Boolean indicating whether the configuration is active. Usage depends on how the using module makes use of it.',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'Weight of the configuration. Usage depends on how the using module makes use of it.',
),
'status' => array(
'type' => 'int',
'not null' => TRUE,
// Set the default to ENTITY_CUSTOM without using the constant as it is
// not safe to use it at this point.
'default' => 0x01,
'size' => 'tiny',
'description' => 'The exportable status of the entity.',
),
'module' => array(
'description' => 'The name of the providing module if the entity has been defined in code.',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'data' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
'description' => 'Everything else, serialized.',
),
),
'primary key' => array('id'),
'unique keys' => array(
'name' => array('name'),
),
);
$schema['rules_trigger'] = array(
'fields' => array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The primary identifier of the configuration.',
),
'event' => array(
'type' => 'varchar',
'length' => '127',
'not null' => TRUE,
'default' => '',
'description' => 'The name of the event on which the configuration should be triggered.',
),
),
'primary key' => array('id', 'event'),
'foreign keys' => array(
'table' => 'rules_config',
'columns' => array('id' => 'id'),
),
);
db_create_table('rules_config', $schema['rules_config']);
db_create_table('rules_trigger', $schema['rules_trigger']);
// The cache table already exists, but changed. So re-create it.
db_drop_table('cache_rules');
$schema['cache_rules'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_rules']['description'] = 'Cache table for the rules engine to store configured items.';
db_create_table('cache_rules', $schema['cache_rules']);
// Remove deprecated variables.
variable_del('rules_inactive_sets');
variable_del('rules_show_fixed');
variable_del('rules_hide_token_message');
variable_del('rules_counter');
return t('The database tables for Rules 2.x have been created. The old tables from Rules 1.x are still available and contain your rules, which are not updated yet.');
}
/**
* Add in the exportable entity db columns as required by the entity API.
*/
function rules_update_7201() {
// Previously this was update 7200, so check whether we need to run it really.
// The update has been moved as 7200 needs to be the 6.x-7.x upgrade.
if (!db_field_exists('rules_config', 'status')) {
db_add_field('rules_config', 'status', array(
'type' => 'int',
'not null' => TRUE,
'default' => ENTITY_CUSTOM,
'size' => 'tiny',
'description' => 'The exportable status of the entity.',
));
// The module column did already exist before.
}
}
/**
* Add an index for the rules configuration plugin column.
*/
function rules_update_7202() {
db_add_index('rules_config', 'plugin', array('plugin'));
}
/**
* Fix the length of the rules_config.name column.
*/
function rules_update_7203() {
db_drop_unique_key('rules_config', 'name');
$keys = array(
'unique keys' => array(
'name' => array('name'),
),
);
db_change_field('rules_config', 'name', 'name', array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'The name of the configuration.',
), $keys);
}
/**
* Add a table for rules-config tags.
*/
function rules_update_7204() {
if (!db_table_exists('rules_tags')) {
$schema['rules_tags'] = array(
'fields' => array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The primary identifier of the configuration.',
),
'tag' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The tag string associated with this configuration',
),
),
'primary key' => array('id', 'tag'),
'foreign keys' => array(
'table' => 'rules_config',
'columns' => array('id' => 'id'),
),
);
db_create_table('rules_tags', $schema['rules_tags']);
}
}
/**
* Add the rules_dependencies table and the rules_config.dirty column.
*/
function rules_update_7205() {
if (!db_table_exists('rules_dependencies')) {
$schema['rules_dependencies'] = array(
'fields' => array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The primary identifier of the configuration.',
),
'module' => array(
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'description' => 'The name of the module that is required for the configuration.',
),
),
'primary key' => array('id', 'module'),
'indexes' => array(
'module' => array('module'),
),
'foreign keys' => array(
'table' => 'rules_config',
'columns' => array('id' => 'id'),
),
);
db_create_table('rules_dependencies', $schema['rules_dependencies']);
}
if (!db_field_exists('rules_config', 'dirty')) {
db_add_field('rules_config', 'dirty', array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
));
}
}
/**
* Flush all caches.
*/
function rules_update_7206() {
// The update system is going to flush all caches anyway, so nothing to do.
}
/**
* Flush all caches.
*/
function rules_update_7207() {
// The update system is going to flush all caches anyway, so nothing to do.
}
/**
* Flush all caches to update the data_is_empty condition info.
*/
function rules_update_7208() {
// The update system is going to flush all caches anyway, so nothing to do.
}
/**
* Creates a flag that enables a permission for using components.
*/
function rules_update_7209() {
// Create a access exposed flag column.
db_add_field('rules_config', 'access_exposed', array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'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'));
}

1774
rules.module

File diff suppressed because it is too large Load Diff

133
rules.rules.inc

@ -0,0 +1,133 @@
<?php
/**
* @file
* Includes any rules integration provided by the module.
*/
/*
* Load all module includes as soon as this file gets included, which is done
* automatically by module_implements().
*/
foreach (rules_core_modules() as $module) {
module_load_include('inc', 'rules', "modules/$module.rules");
}
/**
* Defines a list of core module on whose behalf we provide module integration.
*
* We also add a pseudo 'data' module, which will be used for providing generic
* rules data integration, 'entity' for entity-related integration and 'rules'
* for providing some general stuff.
*/
function rules_core_modules() {
// Make use of the fast, advanced drupal static pattern.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast = &drupal_static(__FUNCTION__);
}
$modules = &$drupal_static_fast;
if (!isset($modules)) {
$modules = array('data', 'entity', 'node', 'system', 'user', 'rules_core');
foreach (array('comment', 'taxonomy', 'php', 'path') as $module) {
if (module_exists($module)) {
$modules[] = $module;
}
}
}
return $modules;
}
/**
* Returns all items for a hook applying the right module defaults.
*/
function _rules_rules_collect_items($hook) {
$items = array();
foreach (rules_core_modules() as $module) {
if (function_exists($function = "rules_{$module}_{$hook}")) {
$items += (array) $function();
}
}
return $items;
}
/**
* Implements hook_rules_file_info().
*/
function rules_rules_file_info() {
// Make use of the fast, advanced drupal static pattern.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast = &drupal_static(__FUNCTION__);
}
$items = &$drupal_static_fast;
if (!isset($items)) {
$items = array();
foreach (rules_core_modules() as $module) {
if (function_exists($function = "rules_{$module}_file_info")) {
$items = array_merge($items, (array) $function());
// Automatically add "$module.rules.inc" for each module.
$items[] = 'modules/' . $module . '.rules';
}
}
}
return $items;
}
/**
* Implements hook_rules_category_info().
*/
function rules_rules_category_info() {
return _rules_rules_collect_items('category_info');
}
/**
* Implements hook_rules_action_info().
*/
function rules_rules_action_info() {
return _rules_rules_collect_items('action_info');
}
/**
* Implements hook_rules_condition_info().
*/
function rules_rules_condition_info() {
return _rules_rules_collect_items('condition_info');
}
/**
* Implements hook_rules_event_info().
*/
function rules_rules_event_info() {
return _rules_rules_collect_items('event_info');
}
/**
* Implements hook_rules_data_info().
*/
function rules_rules_data_info() {
return _rules_rules_collect_items('data_info');
}
/**
* Implements hook_rules_data_info_alter().
*/
function rules_rules_data_info_alter(&$items) {
// For now just invoke the rules core implementation manually.
rules_rules_core_data_info_alter($items);
}
/**
* Implements hook_rules_evaluator_info().
*/
function rules_rules_evaluator_info() {
return _rules_rules_collect_items('evaluator_info');
}
/**
* Implements hook_rules_data_processor_info().
*/
function rules_rules_data_processor_info() {
return _rules_rules_collect_items('data_processor_info');
}

434
rules_admin/rules_admin.inc

@ -0,0 +1,434 @@
<?php
/**
* @file
* Implements rule management and configuration screens.
*/
/**
* Reaction rules overview.
*/
function rules_admin_reaction_overview($form, &$form_state, $base_path) {
RulesPluginUI::formDefaults($form, $form_state);
$conditions = array('plugin' => 'reaction rule', 'active' => TRUE);
$collapsed = TRUE;
if (empty($_GET['tag'])) {
$tag = 0;
}
else {
$tag = $_GET['tag'];
$conditions['tags'] = array($tag);
$collapsed = FALSE;
}
if (empty($_GET['event'])) {
$event = 0;
}
else {
$event = $_GET['event'];
// Filter using a wildcard suffix so configured event names with suffixes
// are found also.
$conditions['event'] = $event . '%';
$collapsed = FALSE;
}
$form['help'] = array(
'#markup' => t('Reaction rules, listed below, react on selected events on the site. Each reaction rule may fire any number of <em>actions</em>, and may have any number of <em>conditions</em> that must be met for the actions to be executed. You can also set up <a href="@url1">components</a> – stand-alone sets of Rules configuration that can be used in Rules and other parts of your site. See <a href="@url2">the online documentation</a> for an introduction on how to use Rules.',
array('@url1' => url('admin/config/workflow/rules/components'),
'@url2' => rules_external_help('rules'))),
);
$form['filter'] = array(
'#type' => 'fieldset',
'#title' => t('Filter'),
'#collapsible' => TRUE,
);
$form['filter']['#id'] = 'rules-filter-form';
$form['filter']['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.css';
$form['filter']['event'] = array(
'#type' => 'select',
'#title' => t('Filter by event'),
'#options' => array(0 => t('<All>')) + RulesPluginUI::getOptions('event'),
'#default_value' => $event,
);
$form['filter']['tag'] = array(
'#type' => 'select',
'#title' => t('Filter by tag'),
'#options' => array(0 => t('<All>')) + RulesPluginUI::getTags(),
'#default_value' => $tag,
);
$form['filter']['submit'] = array(
'#type' => 'submit',
'#value' => t('Filter'),
'#name' => '', // prevent from showing up in $_GET.
);
$options = array('show plugin' => FALSE, 'base path' => $base_path);
$form['active'] = rules_ui()->overviewTable($conditions, $options);
$form['active']['#caption'] = t('Active rules');
$form['active']['#empty'] = t('There are no active rules. <a href="!url">Add new rule</a>.', array('!url' => url('admin/config/workflow/rules/reaction/add')));
$conditions['active'] = FALSE;
$form['inactive'] = rules_ui()->overviewTable($conditions, $options);
$form['inactive']['#caption'] = t('Inactive rules');
$form['inactive']['#empty'] = t('There are no inactive rules.');
$form['filter']['#collapsed'] = $collapsed;
$form['#submit'][] = 'rules_form_submit_rebuild';
$form['#method'] = 'get';
return $form;
}
/**
* Components overview.
*/
function rules_admin_components_overview($form, &$form_state, $base_path) {
RulesPluginUI::formDefaults($form, $form_state);
$collapsed = TRUE;
if (empty($_GET['tag'])) {
$tag = 0;
}
else {
$tag = $_GET['tag'];
$conditions['tags'] = array($tag);
$collapsed = FALSE;
}
if (empty($_GET['plugin'])) {
// Get the plugin name usable as component.
$conditions['plugin'] = array_keys(rules_filter_array(rules_fetch_data('plugin_info'), 'component', TRUE));
$plugin = 0;
}
else {
$plugin = $_GET['plugin'];
$conditions['plugin'] = $plugin;
$collapsed = FALSE;
}
$form['help'] = array(
'#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 <a href="@url">the online documentation</a> for more information about how to use components.',
array('@url' => rules_external_help('components'))),
);
$form['filter'] = array(
'#type' => 'fieldset',
'#title' => t('Filter'),
'#collapsible' => TRUE,
);
$form['filter']['#id'] = 'rules-filter-form';
$form['filter']['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.css';
$form['filter']['plugin'] = array(
'#type' => 'select',
'#title' => t('Filter by plugin'),
'#options' => array(0 => t('<All>')) + rules_admin_component_options(),
'#default_value' => $plugin,
);
$form['filter']['tag'] = array(
'#type' => 'select',
'#title' => t('Filter by tag'),
'#options' => array(0 => '<All>') + RulesPluginUI::getTags(),
'#default_value' => $tag,
);
$form['filter']['submit'] = array(
'#type' => 'submit',
'#value' => t('Filter'),
'#name' => '', // prevent from showing up in $_GET.
);
$form['table'] = RulesPluginUI::overviewTable($conditions, array('hide status op' => TRUE));
$form['table']['#empty'] = t('There are no rule components.');
$form['filter']['#collapsed'] = $collapsed;
$form['#submit'][] = 'rules_form_submit_rebuild';
$form['#method'] = 'get';
return $form;
}
/**
* Rules settings form.
*/
function rules_admin_settings($form, &$form_state) {
if (module_exists('path')) {
// Present a list of available path cleaning callbacks.
// @see rules_clean_path()
$options = array(
'rules_path_default_cleaning_method' => t('Rules (built in)'),
);
if (module_exists('ctools')) {
$options['rules_path_clean_ctools'] = t('CTools');
}
if (module_exists('pathauto')) {
$options['rules_path_clean_pathauto'] = t('Pathauto');
$pathauto_help = t("Note that Pathauto's URL path cleaning method can be configured at <a href='!url'>admin/config/search/path/settings</a>.", array('!url' => url('admin/config/search/path/settings')));
}
else {
$pathauto_help = t('Install the <a href="https://www.drupal.org/project/pathauto">Pathauto module</a> in order to get a configurable URL path cleaning method.');
}
$form['path']['rules_path_cleaning_callback'] = array(
'#type' => 'select',
'#title' => t('URL path cleaning method'),
'#description' => t('Choose the path cleaning method to be applied when generating URL path aliases.') . ' ' . $pathauto_help,
'#default_value' => variable_get('rules_path_cleaning_callback', 'rules_path_default_cleaning_method'),
'#options' => $options,
);
}
$form['rules_log_errors'] = array(
'#type' => 'radios',
'#title' => t('Logging of Rules evaluation errors'),
'#options' => array(
RulesLog::WARN => t('Log all warnings and errors'),
RulesLog::ERROR => t('Log errors only'),
),
'#default_value' => variable_get('rules_log_errors', RulesLog::WARN),
'#description' => t('Evaluations errors are logged to the system log.'),
);
$form['debug'] = array(
'#type' => 'fieldset',
'#title' => t('Debugging'),
);
$form['debug']['rules_debug_log'] = array(
'#type' => 'checkbox',
'#title' => t('Log debug information to the system log'),
'#default_value' => variable_get('rules_debug_log', FALSE),
);
$form['debug']['rules_debug'] = array(
'#type' => 'radios',
'#title' => t('Show debug information'),
'#default_value' => variable_get('rules_debug', 0),
'#options' => array(
0 => t('Never'),
RulesLog::WARN => t('In case of errors'),
RulesLog::INFO => t('Always'),
),
'#description' => t('Debug information is only shown when rules are evaluated and is visible for users having the permission <a href="!url">%link</a>.', array('%link' => t('Access the Rules debug log'), '!url' => url('admin/people/permissions', array('fragment' => 'module-rules')))),
);
$form['debug']['regions'] = array(
'#type' => 'container',
'#states' => array(
// Hide the regions settings when the debug log is disabled.
'invisible' => array(
'input[name="rules_debug"]' => array('value' => 0),
),
),
);
$theme_default = variable_get('theme_default', 'bartik');
$admin_theme = variable_get('admin_theme', 'seven');
$form['debug']['regions']['rules_debug_region_' . $theme_default] = array(
'#type' => 'select',
'#title' => t('Default theme region'),
'#description' => t("The region, where the debug log should be displayed on the default theme %theme. For other themes, Rules will try to display the log on the same region, or hide it in case it doesn't exist.", array('%theme' => $theme_default)),
'#options' => system_region_list($theme_default, REGIONS_VISIBLE),
'#default_value' => variable_get('rules_debug_region_' . $theme_default, 'help'),
);
$form['debug']['regions']['rules_debug_region_' . $admin_theme] = array(
'#type' => 'select',
'#title' => t('Admin theme region'),
'#description' => t('The region, where the debug log should be displayed on the admin theme %theme.', array('%theme' => $admin_theme)),
'#options' => system_region_list($admin_theme, REGIONS_VISIBLE),
'#default_value' => variable_get('rules_debug_region_' . $admin_theme, 'help'),
);
if (db_table_exists('rules_rules')) {
drupal_set_message(t('There are left over rule configurations from a previous Rules 1.x installation. Proceed to the <a href="!url">upgrade page</a> to convert them and consult the README.txt for more details.', array('!url' => url('admin/config/workflow/rules/upgrade'))), 'warning');
}
return system_settings_form($form);
}
/**
* Advanced settings form.
*/
function rules_admin_settings_advanced($form, &$form_state) {
$form['integrity'] = array(
'#type' => 'fieldset',
'#title' => t('Integrity'),
'#description' => t('Rules checks the integrity of your configurations to discover and exclude broken configurations from evaluation.'),
);
$form['integrity']['start_integrity_check'] = array(
'#type' => 'submit',
'#value' => t('Recheck integrity'),
'#submit' => array('rules_admin_settings_integrity_check_submit'),
);
$form['cache'] = array(
'#type' => 'fieldset',
'#title' => t('Cache'),
'#description' => t('Rules caches information about available actions, conditions and data types. Additionally all components and reaction rules are cached for efficient evaluation.'),
);
$form['cache']['rebuild_rules_cache'] = array(
'#type' => 'submit',
'#value' => t("Rebuild Rules' cache"),
'#weight' => 2,
'#submit' => array('rules_admin_settings_cache_rebuild_submit'),
);
return $form;
}
/**
* Form submit callback to check the integrity of all configurations.
*/
function rules_admin_settings_integrity_check_submit($form, &$form_state) {
$start = microtime(TRUE);
$count = 0;
$rules_configs = rules_config_load_multiple(FALSE);
foreach ($rules_configs as $rules_config) {
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)));
drupal_set_message(t('The @plugin <a href="!uri">%label (%name)</a> fails the integrity check and cannot be executed.', $variables), 'error');
}
}
drupal_set_message(t('Integrity of %count configurations checked in %duration seconds. %count_failed broken configurations found.', array(
'%count' => count($rules_configs),
'%count_failed' => $count,
'%duration' => round(microtime(TRUE) - $start, 2),
)));
}
/**
* Form submit callback: Rebuild the Rules' cache.
*/
function rules_admin_settings_cache_rebuild_submit($form, &$form_state) {
$start = microtime(TRUE);
rules_clear_cache();
// Manually trigger cache rebuilding of all caches.
rules_get_cache();
_rules_rebuild_component_cache();
RulesEventSet::rebuildEventCache();
drupal_set_message(t('Rules cache rebuilt in %duration seconds.', array(
'%duration' => round(microtime(TRUE) - $start, 2),
)));
}
/**
* Add reaction rules form.
*/
function rules_admin_add_reaction_rule($form, &$form_state, $base_path) {
RulesPluginUI::formDefaults($form, $form_state);
$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;
$form['settings']['#type'] = 'container';
$form['settings']['label']['#default_value'] = '';
// Hide the rule elements stuff for now.
foreach (array('elements', 'conditions', 'add', 'events') as $key) {
$form[$key]['#access'] = FALSE;
}
foreach (array('active', 'weight') as $key) {
$form['settings'][$key]['#access'] = FALSE;
}
// 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_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.
*/
function rules_admin_add_component($form, &$form_state, $base_path) {
RulesPluginUI::$basePath = $base_path;
RulesPluginUI::formDefaults($form, $form_state);
$form['plugin_name'] = array(
'#type' => 'select',
'#title' => t('Component plugin'),
'#options' => rules_admin_component_options(),
'#description' => t('Choose which kind of component to create. Each component type is described in <a href="@url">the online documentation</a>.',
array('@url' => rules_external_help('component-types'))),
'#weight' => -2,
'#default_value' => isset($form_state['values']['plugin_name']) ? $form_state['values']['plugin_name'] : '',
);
if (!isset($form_state['rules_config'])) {
$form['continue'] = array(
'#type' => 'submit',
'#name' => 'continue',
'#submit' => array('rules_admin_add_component_create_submit'),
'#value' => t('Continue'),
);
}
else {
$form['plugin_name']['#disabled'] = TRUE;
$form_state['rules_config']->form($form, $form_state, array('show settings' => TRUE, 'button' => TRUE, 'init' => TRUE));
$form['settings']['#collapsible'] = FALSE;
$form['settings']['#type'] = 'container';
$form['settings']['label']['#default_value'] = '';
$form['settings']['#weight'] = -1;
// Hide the rule elements stuff for now.
foreach (array('elements', 'negate') as $key) {
$form[$key]['#access'] = FALSE;
}
foreach (array('active', 'weight') as $key) {
$form['settings'][$key]['#access'] = FALSE;
}
}
return $form;
}
function rules_admin_component_options() {
$cache = rules_get_cache();
return rules_extract_property(rules_filter_array($cache['plugin_info'], 'component', TRUE), 'label');
}
/**
* Submit callback that creates the new component object initially.
*/
function rules_admin_add_component_create_submit($form, &$form_state) {
$form_state['rules_config'] = rules_plugin_factory($form_state['values']['plugin_name']);
$form_state['rebuild'] = TRUE;
}
/**
* Validation callback for adding a component.
*/
function rules_admin_add_component_validate($form, &$form_state) {
if (isset($form_state['rules_config'])) {
$form_state['rules_config']->form_validate($form, $form_state);
}
}
/**
* Final submit callback for adding a component.
*/
function rules_admin_add_component_submit($form, &$form_state) {
$rules_config = $form_state['rules_config'];
$rules_config->form_submit($form, $form_state);
drupal_set_message(t('Your changes have been saved.'));
$form_state['redirect'] = RulesPluginUI::path($rules_config->name);
}

16
rules_admin/rules_admin.info

@ -0,0 +1,16 @@
name = Rules UI
description = Administrative interface for managing rules.
package = Rules
core = 7.x
dependencies[] = rules
configure = admin/config/workflow/rules
; Test cases
files[] = tests/rules_admin.test
files[] = tests/rules_admin_minimal_profile.test
; Information added by Drupal.org packaging script on 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

135
rules_admin/rules_admin.module

@ -0,0 +1,135 @@
<?php
/**
* @file
* Rules Admin User Interface.
*/
/**
* Implements hook_menu().
*/
function rules_admin_menu() {
// Reaction rules UI menu entries.
$reaction_path = 'admin/config/workflow/rules/reaction';
$items = rules_ui()->config_menu($reaction_path);
$items[$reaction_path] = array(
'title' => 'Rules',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -1,
);
$items[$reaction_path . '/add'] = array(
'title' => 'Add new rule',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_admin_add_reaction_rule', $reaction_path),
'access arguments' => array('administer rules'),
'type' => MENU_LOCAL_ACTION,
'file' => 'rules_admin.inc',
'weight' => 0,
);
$items[$reaction_path . '/import'] = array(
'title' => 'Import rule',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_import_form', $reaction_path),
'access arguments' => array('administer rules'),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
'type' => MENU_LOCAL_ACTION,
);
// Components UI menu entries.
$component_path = 'admin/config/workflow/rules/components';
$items += rules_ui()->config_menu($component_path);
$items[$component_path] = array(
'title' => 'Components',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_admin_components_overview', $component_path),
'access arguments' => array('administer rules'),
'type' => MENU_LOCAL_TASK,
'file' => 'rules_admin.inc',
'weight' => 0,
);
$items[$component_path . '/add'] = array(
'title' => 'Add new component',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_admin_add_component', $component_path),
'access arguments' => array('administer rules'),
'type' => MENU_LOCAL_ACTION,
'file' => 'rules_admin.inc',
'weight' => 0,
);
$items[$component_path . '/import'] = array(
'title' => 'Import component',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_import_form', $component_path),
'access arguments' => array('administer rules'),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
'type' => MENU_LOCAL_ACTION,
);
// Some general rules admin menu items.
$items['admin/config/workflow/rules'] = array(
'title' => 'Rules',
'position' => 'right',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_admin_reaction_overview', $reaction_path),
'description' => 'Manage reaction rules and rule components.',
'access arguments' => array('administer rules'),
'file' => 'rules_admin.inc',
);
$items['admin/config/workflow/rules/settings'] = array(
'title' => 'Settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_admin_settings'),
'access arguments' => array('administer rules'),
'type' => MENU_LOCAL_TASK,
'file' => 'rules_admin.inc',
'weight' => 1,
);
$items['admin/config/workflow/rules/settings/basic'] = array(
'title' => 'Basic',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/config/workflow/rules/settings/advanced'] = array(
'title' => 'Advanced',
'type' => MENU_LOCAL_TASK,
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_admin_settings_advanced'),
'access arguments' => array('administer rules'),
'file' => 'rules_admin.inc',
);
return $items;
}
/**
* Implements hook_form_alter().
*
* Since the overview forms are GET forms, we don't want them to send a wide
* variety of information. We need to use hook_form_alter() because the
* properties are added after form creation.
*/
function rules_admin_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'rules_admin_reaction_overview' || $form_id == 'rules_admin_components_overview') {
$form['form_build_id']['#access'] = FALSE;
$form['form_token']['#access'] = FALSE;
$form['form_id']['#access'] = FALSE;
}
}
/**
* Implements hook_system_info_alter().
*
* Adds configuration links for Rules and Rules Scheduler in the modules list.
* (This is done by the Rules UI module, without which there would be no
* configuration pages to visit.)
*/
function rules_admin_system_info_alter(&$info, $file, $type) {
if ($file->name == 'rules') {
$info['configure'] = 'admin/config/workflow/rules';
}
if ($file->name == 'rules_scheduler') {
$info['configure'] = 'admin/config/workflow/rules/schedule';
}
}

376
rules_admin/tests/rules_admin.test

@ -0,0 +1,376 @@
<?php
/**
* @file
* Rules UI tests.
*/
/**
* Tests for creating rules through the UI.
*/
class RulesUiTestCase extends DrupalWebTestCase {
/**
* Declares test metadata.
*/
public static function getInfo() {
return array(
'name' => '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("&amp;#039;", 'Apostrophe is not HTML-encoded.');
}
/**
* Tests that the title for embedded container plugins displays properly.
*
* @see https://www.drupal.org/project/rules/issues/3028444
*/
public function testContainerPluginLabel() {
// 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 an OR condition container.
$this->clickLink('Add or');
$this->assertText('Add a new condition set (OR)', 'Condition set add confirmation is shown.');
$this->drupalPost(NULL, array(), 'Continue');
$this->assertRaw('<div class="rules-element-label">OR</div>', 'Condition set label is shown.');
}
/**
* Tests setting component variables in the UI.
*
* @see https://www.drupal.org/project/rules/issues/3005864
*/
public function testComponentVariables() {
// Create a simple user account with permission to create a rule.
$user = $this->drupalCreateUser(array(
'access administration pages',
'administer rules',
'bypass rules access',
));
$this->drupalLogin($user);
// Variables cannot be set/changed for code-provided components, so we must
// build our test component here.
// Create an 'action set' rule component.
$action_set = rules_action_set(array(
'node' => array('type' => 'node', 'label' => 'Input node'),
));
$action_set->save('rules_test_variables');
// Verify that our test component appears in the UI.
$this->drupalGet('admin/config/workflow/rules/components');
$this->assertText(
'rules_test_variables',
'Test component is defined.'
);
// Visit the component edit page.
$this->clickLink('rules_test_variables');
$this->assertText(
'Variables are normally input parameters for the component',
'Component variables form is present.'
);
// Check for presence of pre-existing variable fields AND for presence
// of exactly one row of fields for new variable input.
$this->assertFieldByName(
'settings[vars][items][node][label]',
'Input node',
'Label of pre-existing variable is defined.'
);
$this->assertFieldByName(
'settings[vars][items][0][label]',
'',
'First row for new variable is present.'
);
// Should only have line [0], not [1] or [2].
$this->assertNoFieldByName(
'settings[vars][items][1][label]',
NULL,
'Second row for new variable is missing.'
);
$this->assertNoFieldByName(
'settings[vars][items][2][label]',
NULL,
'Third row for new variable is missing.'
);
// Save new variable.
$edit = array(
'settings[vars][items][0][type]' => 'integer',
'settings[vars][items][0][label]' => 'Integer value',
'settings[vars][items][0][name]' => 'integer_value',
);
$this->drupalPost(NULL, $edit, 'Save changes');
$this->assertFieldByName(
'settings[vars][items][node][label]',
'Input node',
'Label of pre-existing variable is defined.'
);
$this->assertFieldByName(
'settings[vars][items][integer_value][label]',
'Integer value',
'Label of variable is defined.'
);
$this->assertFieldByName(
'settings[vars][items][integer_value][name]',
'integer_value',
'Machine name of variable is defined.'
);
// Check to see if "Add more" button properly adds one more row for
// variable input while preserving what we've already entered.
$edit = array(
'settings[vars][items][0][type]' => 'decimal',
'settings[vars][items][0][label]' => 'Decimal value',
'settings[vars][items][0][name]' => 'decimal_value',
);
$this->drupalPostAjax(NULL, $edit, array('op' => 'Add more'));
$this->assertFieldByName(
'settings[vars][items][node][label]',
'Input node',
'Label of pre-existing variable is defined.'
);
$this->assertFieldByName(
'settings[vars][items][integer_value][label]',
'Integer value',
'Label of integer variable is defined.'
);
$this->assertFieldByName(
'settings[vars][items][decimal_value][label]',
'Decimal value',
'Label of decimal variable is defined.'
);
$this->assertFieldByName(
'settings[vars][items][0][label]',
NULL,
'First row for new variable is present.'
);
}
/**
* Tests setting component permissions in the UI.
*
* @see https://www.drupal.org/project/rules/issues/2340505
*/
public function testComponentPermissions() {
// Create a simple user account with permission to create a rule.
$user = $this->drupalCreateUser(array(
'access administration pages',
'administer rules',
'bypass rules access',
));
$this->drupalLogin($user);
// The rules_test module defines the component 'rules_test_action_set'.
// Verify that this code-provided component appears in the UI.
$this->drupalGet('admin/config/workflow/rules/components');
$this->assertText(
'rules_test_action_set',
'Test component is defined.'
);
// Visit the component edit page.
$this->clickLink('rules_test_action_set');
$this->assertText(
'Configure access for using this component with a permission.',
'Enable component permission checkbox is present.'
);
$this->assertText(
'Use Rules component rules_test_action_set',
'Permission configuration form is present.'
);
// Try to enable permissions on this component and set a component
// permission at the same time.
$edit = array(
'settings[access][access_exposed]' => TRUE,
'settings[access][permissions][matrix][checkboxes][1][use Rules component rules_test_action_set]' => TRUE,
);
$this->drupalPost(NULL, $edit, 'Save changes');
}
/**
* Tests overriding and reverting configurations.
*
* Verify that when we overwrite a default rule with an import, the status of
* that rule is overridden.
*
* @see https://www.drupal.org/project/rules/issues/2027717#comment-12904190
*/
public function testOverrideStatus() {
// Create a simple user account with permission to create a rule.
$user = $this->drupalCreateUser(array(
'access administration pages',
'administer rules',
'bypass rules access',
));
$this->drupalLogin($user);
// The rules_test module defines the rule 'rules_test_default_1' in code.
// Ensure this rule has status equals ENTITY_IN_CODE.
$rule = rules_config_load('rules_test_default_1');
$this->assertTrue(
$rule->hasStatus(ENTITY_IN_CODE),
'Rule defined in hook_default_rules_configuration() has status ENTITY_IN_CODE.'
);
// Verify the code-provided rule appears in the UI.
$this->drupalGet('admin/config/workflow/rules');
$this->assertText(
'example default rule',
'Example default rule is defined in code.'
);
$this->assertText(
'rules_test_default_1',
'Machine name shows up in UI.'
);
// Now we need to overwrite the 'rules_test_default_1' rule in the
// database by importing a rule with the same id and forcing the overwrite.
// First check that importing fails if the 'overwrite' box is not checked.
$this->drupalGet('admin/config/workflow/rules/reaction/import');
$edit = array(
'import' => $this->getTestRuleExport('rules_test_default_1'),
'overwrite' => FALSE,
);
$this->drupalPost(NULL, $edit, 'Import');
$this->assertText(
'Import of Rules configuration example imported default rule failed, a Rules configuration with the same machine name already exists. Check the overwrite option to replace it.',
'Rule overwrite failed.'
);
// Now set the 'overwrite' checkbox to force the overwrite and resubmit.
$edit = array(
'import' => $this->getTestRuleExport('rules_test_default_1'),
'overwrite' => TRUE,
);
$this->drupalPost(NULL, $edit, 'Import');
// Verify that the overwritten rule now has a status of ENTITY_OVERRIDDEN.
$this->assertText(
'example imported default rule',
'New example default rule has been imported.'
);
$this->assertText(
'rules_test_default_1',
'Machine name shows up in UI.'
);
$this->assertText(
'Overridden',
'Example default rule has overridden status.'
);
// Clear cache and ensure the rule is still overridden.
cache_clear_all();
// Visit reaction rules listing page to force refresh.
$this->clickLink('Rules');
$this->assertText(
'example imported default rule',
'Rule label unchanged after cache clear.'
);
$this->assertText(
'Overridden',
'Rule overridden status unchanged after cache clear.'
);
// A 'revert' link should now be available for the overridden rule.
$this->assertText('revert', 'Revert link is now present.');
// Revert the overridden rule and verify it's back to its original status.
$this->clickLink('revert');
$this->drupalPost(NULL, array(), 'Confirm');
$this->assertText(
'example default rule',
'Example default rule original label restored.'
);
$this->assertText(
'Reverted reaction rule example imported default rule to the defaults',
'Example default rule was reverted.'
);
$this->assertNoText('revert', 'Revert link is not present.');
}
/**
* Helper function to return a known JSON encoded rule export.
*
* @param string $machine_name
* The machine name of the returned rule.
*/
protected function getTestRuleExport($machine_name) {
return '{ "' . $machine_name . '" : {
"LABEL" : "example imported default rule",
"PLUGIN" : "reaction rule",
"ACTIVE" : false,
"OWNER" : "rules",
"TAGS" : [ "Admin", "Tag2" ],
"REQUIRES" : [ "rules" ],
"ON" : { "node_update" : [] },
"IF" : [
{ "NOT data_is" : { "data" : [ "node:status" ], "value" : true } },
{ "data_is" : { "data" : [ "node:type" ], "value" : "page" } }
],
"DO" : [ { "drupal_message" : { "message" : "A node has been updated." } } ]
}
}';
}
}

66
rules_admin/tests/rules_admin_minimal_profile.test

@ -0,0 +1,66 @@
<?php
/**
* @file
* Rules UI tests.
*/
/**
* 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 assumption may cause errors.
*/
class RulesAdminMinimalProfileTestCase extends DrupalWebTestCase {
/**
* {@inheritdoc}
*/
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.');
}
}

107
rules_i18n/rules_i18n.i18n.inc

@ -0,0 +1,107 @@
<?php
/**
* @file
* Internationalization integration based upon the entity API i18n stuff.
*/
/**
* Rules i18n integration controller.
*/
class RulesI18nStringController extends EntityDefaultI18nStringController {
/**
* Overridden to customize i18n object info.
*
* @see EntityDefaultI18nStringController::hook_object_info()
*/
public function hook_object_info() {
$info = parent::hook_object_info();
$info['rules_config']['class'] = 'RulesI18nStringObjectWrapper';
return $info;
}
/**
* Overridden to customize the used menu wildcard.
*/
protected function menuWildcard() {
return '%rules_config';
}
/**
* Provide the menu base path. We can provide only one though.
*/
protected function menuBasePath() {
return 'admin/config/workflow/rules/reaction';
}
}
/**
* Custom I18nString object wrapper; registers custom properties per config.
*/
class RulesI18nStringObjectWrapper extends i18n_string_object_wrapper {
/**
* Get translatable properties.
*/
protected function build_properties() {
$strings = parent::build_properties();
$properties = array();
// Also add in the configuration label, as the i18n String UI requires
// a String to be available always.
$properties['label'] = array(
'title' => t('Configuration name'),
'string' => $this->object->label,
);
$this->buildElementProperties($this->object, $properties);
// Add in translations for all elements.
foreach ($this->object->elements() as $element) {
$this->buildElementProperties($element, $properties);
}
$strings[$this->get_textgroup()]['rules_config'][$this->object->name] = $properties;
return $strings;
}
/**
* Adds in translatable properties of the given element.
*/
protected function buildElementProperties($element, &$properties) {
foreach ($element->pluginParameterInfo() as $name => $info) {
// Add in all directly provided input variables.
if (!empty($info['translatable']) && isset($element->settings[$name])) {
// If its an array of textual values, translate each value on its own.
if (is_array($element->settings[$name])) {
foreach ($element->settings[$name] as $i => $value) {
$properties[$element->elementId() . ':' . $name . ':' . $i] = array(
'title' => t('@plugin "@label" (id @id), @parameter, Value @delta', array(
'@plugin' => drupal_ucfirst($element->plugin()),
'@label' => $element->label(),
'@id' => $element->elementId(),
'@parameter' => $info['label'],
'@delta' => $i + 1
)),
'string' => $value,
);
}
}
else {
$properties[$element->elementId() . ':' . $name] = array(
'title' => t('@plugin "@label" (id @id), @parameter', array(
'@plugin' => drupal_ucfirst($element->plugin()),
'@label' => $element->label(),
'@id' => $element->elementId(),
'@parameter' => $info['label'],
)),
'string' => $element->settings[$name],
);
}
}
}
}
}

15
rules_i18n/rules_i18n.info

@ -0,0 +1,15 @@
name = Rules translation
description = Allows translating rules.
dependencies[] = rules
dependencies[] = i18n_string
package = Multilingual - Internationalization
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 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

19
rules_i18n/rules_i18n.install

@ -0,0 +1,19 @@
<?php
/**
* @file
* Install file for Rules i18n.
*/
/**
* Implements hook_install().
*/
function rules_i18n_install() {
global $language;
$langcode = $language->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');
}

133
rules_i18n/rules_i18n.module

@ -0,0 +1,133 @@
<?php
/**
* @file
* Rules i18n integration.
*/
/**
* Implements hook_menu().
*/
function rules_i18n_rules_ui_menu_alter(&$items, $base_path, $base_count) {
$items[$base_path . '/manage/%rules_config/edit'] = array(
'title' => 'Edit',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -100,
);
// I18n generates the menu items for reaction rules. For the others,
// we provide further i18n menu items for all other base paths.
if ($base_path != 'admin/config/workflow/rules/reaction') {
$items[$base_path . '/manage/%rules_config/translate'] = array(
'title' => 'Translate',
'page callback' => 'i18n_page_translate_localize',
'page arguments' => array('rules_config', $base_count + 1),
'access callback' => 'i18n_object_translate_access',
'access arguments' => array('rules_config', $base_count + 1),
'type' => MENU_LOCAL_TASK,
'file' => 'i18n.pages.inc',
'file path' => drupal_get_path('module', 'i18n'),
'weight' => 10,
);
$items[$base_path . '/manage/%rules_config/translate/%i18n_language'] = array(
'title' => 'Translate',
'page callback' => 'i18n_page_translate_localize',
'page arguments' => array('rules_config', $base_count + 1, $base_count + 3),
'access callback' => 'i18n_object_translate_access',
'access arguments' => array('rules_config', $base_count),
'type' => MENU_CALLBACK,
'file' => 'i18n.pages.inc',
'file path' => drupal_get_path('module', 'i18n'),
'weight' => 10,
);
}
}
/**
* Implements hook_entity_info_alter().
*/
function rules_i18n_entity_info_alter(&$info) {
// Enable i18n support via the entity API.
$info['rules_config']['i18n controller class'] = 'RulesI18nStringController';
}
/**
* Implements hook_rules_config_insert().
*/
function rules_i18n_rules_config_insert($rules_config) {
// Do nothing when rebuilding defaults to avoid multiple cache rebuilds.
// @see rules_i18n_rules_config_defaults_rebuild()
if (!empty($rules_config->is_rebuild)) {
return;
}
i18n_string_object_update('rules_config', $rules_config);
}
/**
* Implements hook_rules_config_update().
*/
function rules_i18n_rules_config_update($rules_config, $original = NULL) {
// Do nothing when rebuilding defaults to avoid multiple cache rebuilds.
// @see rules_i18n_rules_config_defaults_rebuild()
if (!empty($rules_config->is_rebuild)) {
return;
}
$original = $original ? $original : $rules_config->original;
// Account for name changes.
if ($original->name != $rules_config->name) {
i18n_string_update_context("rules:rules_config:{$original->name}:*", "rules:rules_config:{$rules_config->name}:*");
}
// We need to remove the strings of any disappeared properties, i.e. strings
// from translatable parameters of deleted actions.
// i18n_object() uses a static cache per config, so bypass it to wrap the
// original entity.
$object_key = i18n_object_key('rules_config', $original);
$old_i18n_object = new RulesI18nStringObjectWrapper('rules_config', $object_key, $original);
$old_strings = $old_i18n_object->get_strings(array('empty' => TRUE));
// Note: For the strings to have updated values, the updated entity needs to
// be handled last due to i18n's cache.
$strings = i18n_object('rules_config', $rules_config)->get_strings(array('empty' => TRUE));
foreach (array_diff_key($old_strings, $strings) as $name => $string) {
$string->remove(array('empty' => TRUE));
}
// Now update the remaining strings.
foreach ($strings as $string) {
$string->update(array('empty' => TRUE, 'update' => TRUE));
}
}
/**
* Implements hook_rules_config_delete().
*/
function rules_i18n_rules_config_delete($rules_config) {
// Only react on real delete, not revert.
if (!$rules_config->hasStatus(ENTITY_IN_CODE)) {
i18n_string_object_remove('rules_config', $rules_config);
}
}
/**
* Implements hook_rules_config_defaults_rebuild().
*/
function rules_i18n_rules_config_defaults_rebuild($rules_configs, $originals) {
// Once all defaults have been rebuilt, update all i18n strings at once. That
// way we build the rules cache once the rebuild is complete and avoid
// rebuilding caches for each updated rule.
foreach ($rules_configs as $name => $rule_config) {
if (empty($originals[$name])) {
rules_i18n_rules_config_insert($rule_config);
}
else {
rules_i18n_rules_config_update($rule_config, $originals[$name]);
}
}
}

209
rules_i18n/rules_i18n.rules.inc

@ -0,0 +1,209 @@
<?php
/**
* @file
* Internationalization rules integration.
*/
/**
* Implements hook_rules_action_info().
*/
function rules_i18n_rules_action_info() {
$items['rules_i18n_t'] = array(
'label' => t('Translate a text'),
'group' => t('Translation'),
'parameter' => array(
'text' => array(
'type' => 'text',
'label' => t('Text'),
'description' => t('The text to translate.'),
'translatable' => TRUE,
),
'language' => array(
'type' => 'token',
'label' => t('Language'),
'description' => t('The language to translate the text into.'),
'options list' => 'entity_metadata_language_list',
'default mode' => 'select',
),
),
'provides' => array(
'text' => array('type' => 'text', 'label' => t('The translated text')),
),
'base' => 'rules_i18n_action_t',
'access callback' => 'rules_i18n_rules_integration_access',
);
$items['rules_i18n_select'] = array(
'label' => t('Select a translated value'),
'group' => t('Translation'),
'parameter' => array(
'data' => array(
'type' => '*',
'label' => t('Data'),
'description' => t('Select a translated value, e.g. a translatable field. If the selected data is not translatable, the language neutral value will be selected.'),
'translatable' => TRUE,
'restrict' => 'select',
),
'language' => array(
'type' => 'token',
'label' => t('Language'),
'description' => t('The language to translate the value into.'),
'options list' => 'entity_metadata_language_list',
),
),
'provides' => array(
'data_translated' => array('type' => '*', 'label' => t('The translated value')),
),
'base' => 'rules_i18n_action_select',
'access callback' => 'rules_i18n_rules_integration_access',
);
return $items;
}
/**
* Access callback for the rules i18n integration.
*/
function rules_i18n_rules_integration_access() {
return user_access('translate interface');
}
/**
* Action callback: Translate a text.
*/
function rules_i18n_action_t($text) {
// Nothing to do, as our input evaluator has already translated it.
// @see RulesI18nStringEvaluator
return array('text' => $text);
}
/**
* Implements hook_form_FORMID_alter() for the action_t form.
*
* Implements the form_alter callback for the "Translate a text" action
* to set a default selector.
*/
function rules_i18n_action_t_form_alter(&$form, &$form_state, $options, $element) {
if (isset($form['parameter']['language']['settings']['language:select']) && empty($element->settings['language:select'])) {
$form['parameter']['language']['settings']['language:select']['#default_value'] = 'site:current-page:language';
}
}
/**
* Action callback: Select a translated value.
*/
function rules_i18n_action_select($data) {
// Nothing to do, as Rules applies the language to the data selector for us.
return array('data_translated' => $data);
}
/**
* Action "Select a translated value" info_alter callback.
*/
function rules_i18n_action_select_info_alter(&$element_info, $element) {
$element->settings += array('data:select' => NULL);
if ($wrapper = $element->applyDataSelector($element->settings['data:select'])) {
$info = $wrapper->info();
// Pass through the data type of the selected data.
$element_info['provides']['data_translated']['type'] = $wrapper->type();
}
}
/**
* Implements hook_rules_evaluator_info().
*/
function rules_i18n_rules_evaluator_info() {
return array(
'i18n' => array(
'class' => 'RulesI18nStringEvaluator',
'type' => array('text', 'list<text>', 'token', 'list<token>'),
// Be sure to translate after doing PHP evaluation.
'weight' => -8,
),
);
}
/**
* A class implementing a rules input evaluator processing tokens.
*/
class RulesI18nStringEvaluator extends RulesDataInputEvaluator {
/**
* Access callback.
*/
public static function access() {
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;
}
else {
// Else, skip this evaluator.
$this->setting = NULL;
}
}
/**
* Prepare the i18n-context string.
*
* We have to use process() here instead of evaluate() because we need more
* context than evaluate() provides.
*/
public function process($value, $info, RulesState $state, RulesPlugin $element, $options = NULL) {
$options = isset($options) ? $options : $this->getEvaluatorOptions($info, $state, $element);
$value = isset($this->processor) ? $this->processor->process($value, $info, $state, $element, $options) : $value;
if (isset($element->root()->name)) {
$config_name = $element->root()->name;
$id = $element->elementId();
$name = $info['#name'];
$options['i18n context'] = "rules:rules_config:$config_name:$id:$name";
return $this->evaluate($value, $options, $state);
}
return $value;
}
/**
* Translate the value.
*
* If the element provides a language parameter, we are using this target
* language provided via $options['language']. Sanitizing is handled by Rules,
* so disable that for i18n.
*/
public function evaluate($value, $options, RulesState $state) {
$langcode = isset($options['language']) ? $options['language']->language : NULL;
if (is_array($value)) {
foreach ($value as $key => $text) {
$value[$key] = i18n_string($options['i18n context'] . ':' . $key, $text, array('langcode' => $langcode, 'sanitize' => FALSE));
}
}
else {
$value = i18n_string($options['i18n context'], $value, array('langcode' => $langcode, 'sanitize' => FALSE));
}
return $value;
}
/**
* Overrides RulesDataInputEvaluator::help().
*/
public static function help($var_info, $param_info = array()) {
if (!empty($param_info['translatable'])) {
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 {
$text = t('Translations can be provided at the %translate tab. The argument value is translated to the current interface language.', array('%translate' => t('Translate')));
}
$render = array(
'#theme' => 'rules_settings_help',
'#text' => $text,
'#heading' => t('Translation'),
);
return $render;
}
}
}

197
rules_i18n/rules_i18n.test

@ -0,0 +1,197 @@
<?php
/**
* @file
* Rules i18n tests.
*/
/**
* Test the i18n integration.
*/
class RulesI18nTestCase extends DrupalWebTestCase {
/**
* Declares test metadata.
*/
public static function getInfo() {
return array(
'name' => 'Rules I18n',
'description' => 'Tests translating Rules configs.',
'group' => 'Rules',
'dependencies' => array('i18n_string'),
);
}
/**
* 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);
$this->addLanguage('de');
}
/**
* Copied from i18n module (class Drupali18nTestCase).
*
* We cannot extend from Drupali18nTestCase as else the test-bot would die.
*/
public function addLanguage($language_code) {
// Check to make sure that language has not already been installed.
$this->drupalGet('admin/config/regional/language');
if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) {
// Doesn't have language installed so add it.
$edit = array();
$edit['langcode'] = $language_code;
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
// Make sure we are not using a stale list.
drupal_static_reset('language_list');
$languages = language_list('language');
$this->assertTrue(array_key_exists($language_code, $languages), t('Language was installed successfully.'));
if (array_key_exists($language_code, $languages)) {
$this->assertRaw(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => $languages[$language_code]->name, '@locale-help' => url('admin/help/locale'))), t('Language has been created.'));
}
}
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.');
}
else {
// It's installed but not enabled. Enable it.
$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.'));
}
}
/**
* Tests translating rules configs.
*/
public function testRulesConfigTranslation() {
// Create a rule and translate it.
$rule = rule();
$rule->label = 'label-en';
$rule->action('drupal_message', array('message' => 'English message for [site:current-user].'));
$rule->save();
$actions = $rule->actions();
$id = $actions[0]->elementId();
// Add a translation.
i18n_string_textgroup('rules')->update_translation("rules_config:{$rule->name}:label", 'de', 'label-de');
i18n_string_textgroup('rules')->update_translation("rules_config:{$rule->name}:$id:message", 'de', 'German message für [site:current-user].');
// Execute the Rule and make sure the translated message has been output.
// To do so, set the global language to German.
$languages = language_list();
$GLOBALS['language'] = $languages['de'];
// Clear messages and execute the rule.
i18n_string_textgroup('rules')->cache_reset();
drupal_get_messages();
$rule->execute();
$messages = drupal_get_messages();
$this->assertEqual($messages['status'][0], 'German message für ' . $GLOBALS['user']->name . '.', 'Translated message has been output.');
// Test re-naming the rule.
$rule->name = 'rules_i18n_name_2';
$rule->save();
$translation = entity_i18n_string("rules:rules_config:{$rule->name}:label", $rule->label, 'de');
$this->assertEqual($translation, 'label-de', 'Translation survives a name change.');
// Test updating and make sure the translation stays.
$rule->label = 'Label new';
$rule->save();
$translation = entity_i18n_string("rules:rules_config:{$rule->name}:label", $rule->label, 'de');
$this->assertEqual($translation, 'label-de', 'Translation survives an update.');
// Test deleting the action and make sure the string is deleted too.
$actions[0]->delete();
$rule->save();
$translation = entity_i18n_string("rules_config:{$rule->name}:$id:message", 'English message for [site:current-user].', 'de');
$this->assertEqual($translation, 'English message for [site:current-user].', 'Translation of deleted action has been deleted.');
// Now delete the whole config and make sure all translations are deleted.
$rule->delete();
$translation = entity_i18n_string("rules_config:{$rule->name}:label", 'label-en', 'de');
$this->assertEqual($translation, 'label-en', 'Translation of deleted config has been deleted.');
}
/**
* Tests the "Translate a text" action.
*/
public function testI18nActionT() {
$set = rules_action_set(array());
$set->action('rules_i18n_t', array(
'text' => 'untranslated',
'language' => 'de',
));
$set->action('drupal_message', array('message:select' => 'text'));
$set->save('rules_i18n_test');
// Add a translation.
$actions = $set->getIterator();
$id = $actions[0]->elementId();
i18n_string_textgroup('rules')->update_translation("rules_config:{$set->name}:$id:text", 'de', 'text-de');
// Clear messages and execute it.
drupal_get_messages();
$set->execute();
$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 evaluated.
module_enable(array('php'));
i18n_string_textgroup('rules')->update_translation("rules_config:{$set->name}:$id:text", 'de', 'text <?php echo "eval";?>');
// Clear messages and execute it.
drupal_get_messages();
$set->execute();
$messages = drupal_get_messages();
$this->assertEqual($messages['status'][0], check_plain('text <?php echo "eval";?>'), 'PHP in translated text is not executed.');
}
/**
* Tests the "Select a translated value" action.
*/
public function testI18nActionSelect() {
// Make the body field and the node type 'page' translatable.
$field = field_info_field('body');
$field['translatable'] = TRUE;
field_update_field($field);
variable_set('language_content_type_page', 1);
$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',
));
$set->action('drupal_message', array('message:select' => 'body'));
$set->save();
$body['en'][0] = array('value' => 'English body.');
$body['de'][0] = array('value' => 'German body.');
$node = $this->drupalCreateNode(array('language' => 'en', 'body' => $body));
// Clear messages and execute it.
drupal_get_messages();
$set->execute($node);
$messages = drupal_get_messages();
$this->assertEqual($messages['status'][0], "German body.\n", 'Translated text has been selected.');
}
}

104
rules_scheduler/includes/rules_scheduler.handler.inc

@ -0,0 +1,104 @@
<?php
/**
* @file
* Views integration for the rules scheduler module.
*/
/**
* Default scheduled task handler.
*/
class RulesSchedulerDefaultTaskHandler implements RulesSchedulerTaskHandlerInterface {
/**
* The task array.
*
* @var array
*/
protected $task;
/**
* Constructs a repetitive task handler object.
*/
public function __construct(array $task) {
$this->task = $task;
}
/**
* Implements RulesSchedulerTaskHandlerInterface::runTask().
*/
public function runTask() {
if ($component = rules_get_cache('comp_' . $this->task['config'])) {
$replacements = array('%label' => $component->label(), '%plugin' => $component->plugin());
$replacements['%identifier'] = $this->task['identifier'] ? $this->task['identifier'] : t('without identifier');
rules_log('Scheduled evaluation of %plugin %label, task %identifier.', $replacements, RulesLog::INFO, $component, TRUE);
$state = unserialize($this->task['data']);
$state->restoreBlocks();
// Block the config to prevent any future recursion.
$state->block($component);
// Finally evaluate the component with the given state.
$component->evaluate($state);
$state->unblock($component);
rules_log('Finished evaluation of %plugin %label, task %identifier.', $replacements, RulesLog::INFO, $component, FALSE);
$state->cleanUp();
}
}
/**
* Implements RulesSchedulerTaskHandlerInterface::afterTaskQueued().
*/
public function afterTaskQueued() {
// Delete the task from the task list.
db_delete('rules_scheduler')
->condition('tid', $this->task['tid'])
->execute();
}
/**
* Implements RulesSchedulerTaskHandlerInterface::getTask().
*/
public function getTask() {
return $this->task;
}
}
/**
* Interface for scheduled task handlers.
*
* Task handlers control the behavior of a task when it's queued or executed.
* Unless specified otherwise, the RulesSchedulerDefaultTaskHandler task handler
* is used.
*
* @see rules_scheduler_run_task()
* @see rules_scheduler_cron()
* @see RulesSchedulerDefaultTaskHandler
*/
interface RulesSchedulerTaskHandlerInterface {
/**
* Processes a queue item.
*
* @throws RulesEvaluationException
* If there are any problems executing the task.
*
* @see rules_scheduler_run_task()
*/
public function runTask();
/**
* Processes a task after it has been queued.
*
* @see rules_scheduler_cron()
*/
public function afterTaskQueued();
/**
* Returns the task associated with the task handler.
*
* @return array
* The task (queue item) array.
*/
public function getTask();
}

84
rules_scheduler/includes/rules_scheduler.views.inc

@ -0,0 +1,84 @@
<?php
/**
* @file
* Views integration for the rules scheduler module.
*/
/**
* Implements hook_views_data().
*
* Specifies the list of future scheduled tasks displayed on the schedule page.
*/
function rules_scheduler_views_data() {
$table = array(
'rules_scheduler' => array(
'table' => array(
'group' => 'Rules scheduler',
'base' => array(
'field' => 'tid',
'title' => t('Scheduled Rules components'),
'help' => t("Scheduled Rules components that are executed based on time and cron"),
'weight' => -10,
),
),
'tid' => array(
'title' => t('Tid'),
'help' => t('The internal ID of the scheduled component'),
'field' => array(
'click sortable' => TRUE,
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
),
'config' => array(
'title' => t('Component name'),
'help' => t('The name of the component'),
'field' => array(
'click sortable' => TRUE,
),
'filter' => array(
'handler' => 'rules_scheduler_views_filter',
),
'argument' => array(
'handler' => 'views_handler_argument_string',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
),
'date' => array(
'title' => t('Scheduled date'),
'help' => t('Scheduled date and time stamp'),
'field' => array(
'handler' => 'views_handler_field_date',
'click sortable' => TRUE,
),
'filter' => array(
'handler' => 'views_handler_filter',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
),
'identifier' => array(
'title' => t('User provided identifier'),
'help' => t('ID to recognize this specific scheduled task'),
'field' => array(
'click sortable' => TRUE,
),
'filter' => array(
'handler' => 'views_handler_filter_string',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
),
),
);
return $table;
}

180
rules_scheduler/includes/rules_scheduler.views_default.inc

@ -0,0 +1,180 @@
<?php
/**
* @file
* Views integration for the rules scheduler module.
*/
/**
* Implements hook_views_default_views().
*/
function rules_scheduler_views_default_views() {
$view = new view();
$view->name = 'rules_scheduler';
$view->description = 'Scheduled Rules components';
$view->tag = '';
$view->base_table = 'rules_scheduler';
$view->api_version = '3.0-alpha1';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
/* Display: Defaults */
$handler = $view->new_display('default', 'Defaults', 'default');
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['access']['perm'] = 'administer rules';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = '30';
$handler->display->display_options['pager']['options']['offset'] = '0';
$handler->display->display_options['pager']['options']['id'] = '0';
$handler->display->display_options['style_plugin'] = 'table';
$handler->display->display_options['style_options']['columns'] = array(
'tid' => 'tid',
'config' => 'config',
'date' => 'date',
'identifier' => 'identifier',
'nothing' => 'nothing',
);
$handler->display->display_options['style_options']['default'] = 'date';
$handler->display->display_options['style_options']['info'] = array(
'tid' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
),
'config' => array(
'sortable' => 1,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
),
'date' => array(
'sortable' => 1,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
),
'identifier' => array(
'sortable' => 1,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
),
'nothing' => array(
'align' => '',
'separator' => '',
),
);
$handler->display->display_options['style_options']['override'] = 1;
$handler->display->display_options['style_options']['sticky'] = 0;
/* Empty text: Global: Text area */
$handler->display->display_options['empty']['area']['id'] = 'area';
$handler->display->display_options['empty']['area']['table'] = 'views';
$handler->display->display_options['empty']['area']['field'] = 'area';
$handler->display->display_options['empty']['area']['empty'] = FALSE;
$handler->display->display_options['empty']['area']['content'] = 'No tasks have been scheduled.';
$handler->display->display_options['empty']['area']['format'] = 'plain_text';
/* Field: Rules scheduler: Tid */
$handler->display->display_options['fields']['tid']['id'] = 'tid';
$handler->display->display_options['fields']['tid']['table'] = 'rules_scheduler';
$handler->display->display_options['fields']['tid']['field'] = 'tid';
/* Field: Rules scheduler: Component name */
$handler->display->display_options['fields']['config']['id'] = 'config';
$handler->display->display_options['fields']['config']['table'] = 'rules_scheduler';
$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/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;
$handler->display->display_options['fields']['config']['alter']['ellipsis'] = 1;
$handler->display->display_options['fields']['config']['alter']['strip_tags'] = 0;
$handler->display->display_options['fields']['config']['alter']['html'] = 0;
$handler->display->display_options['fields']['config']['element_label_colon'] = 1;
$handler->display->display_options['fields']['config']['element_default_classes'] = 1;
$handler->display->display_options['fields']['config']['hide_empty'] = 0;
$handler->display->display_options['fields']['config']['empty_zero'] = 0;
/* Field: Rules scheduler: Scheduled date */
$handler->display->display_options['fields']['date']['id'] = 'date';
$handler->display->display_options['fields']['date']['table'] = 'rules_scheduler';
$handler->display->display_options['fields']['date']['field'] = 'date';
/* Field: Rules scheduler: User provided identifier */
$handler->display->display_options['fields']['identifier']['id'] = 'identifier';
$handler->display->display_options['fields']['identifier']['table'] = 'rules_scheduler';
$handler->display->display_options['fields']['identifier']['field'] = 'identifier';
/* Field: Global: Custom text */
$handler->display->display_options['fields']['nothing']['id'] = 'nothing';
$handler->display->display_options['fields']['nothing']['table'] = 'views';
$handler->display->display_options['fields']['nothing']['field'] = 'nothing';
$handler->display->display_options['fields']['nothing']['label'] = 'Operations';
$handler->display->display_options['fields']['nothing']['alter']['text'] = 'delete';
$handler->display->display_options['fields']['nothing']['alter']['make_link'] = 1;
$handler->display->display_options['fields']['nothing']['alter']['path'] = 'admin/config/workflow/rules/schedule/[tid]/delete';
$handler->display->display_options['fields']['nothing']['alter']['absolute'] = 0;
$handler->display->display_options['fields']['nothing']['alter']['alt'] = 'Delete this scheduled task';
$handler->display->display_options['fields']['nothing']['alter']['trim'] = 0;
$handler->display->display_options['fields']['nothing']['alter']['word_boundary'] = 1;
$handler->display->display_options['fields']['nothing']['alter']['ellipsis'] = 1;
$handler->display->display_options['fields']['nothing']['alter']['strip_tags'] = 0;
$handler->display->display_options['fields']['nothing']['alter']['html'] = 0;
$handler->display->display_options['fields']['nothing']['element_label_colon'] = 1;
$handler->display->display_options['fields']['nothing']['element_default_classes'] = 1;
$handler->display->display_options['fields']['nothing']['hide_empty'] = 0;
$handler->display->display_options['fields']['nothing']['empty_zero'] = 0;
/* Sort criterion: Rules scheduler: Scheduled date */
$handler->display->display_options['sorts']['date']['id'] = 'date';
$handler->display->display_options['sorts']['date']['table'] = 'rules_scheduler';
$handler->display->display_options['sorts']['date']['field'] = 'date';
/* Argument: Rules scheduler: Component name */
$handler->display->display_options['arguments']['config']['id'] = 'config';
$handler->display->display_options['arguments']['config']['table'] = 'rules_scheduler';
$handler->display->display_options['arguments']['config']['field'] = 'config';
$handler->display->display_options['arguments']['config']['style_plugin'] = 'default_summary';
$handler->display->display_options['arguments']['config']['wildcard'] = '0';
$handler->display->display_options['arguments']['config']['default_argument_type'] = 'fixed';
$handler->display->display_options['arguments']['config']['glossary'] = 0;
$handler->display->display_options['arguments']['config']['limit'] = '0';
$handler->display->display_options['arguments']['config']['transform_dash'] = 0;
/* Filter: Rules scheduler: Component name */
$handler->display->display_options['filters']['config']['id'] = 'config';
$handler->display->display_options['filters']['config']['table'] = 'rules_scheduler';
$handler->display->display_options['filters']['config']['field'] = 'config';
$handler->display->display_options['filters']['config']['exposed'] = TRUE;
$handler->display->display_options['filters']['config']['expose']['operator'] = 'config_op';
$handler->display->display_options['filters']['config']['expose']['label'] = 'Component filter';
$handler->display->display_options['filters']['config']['expose']['identifier'] = 'config';
$handler->display->display_options['filters']['config']['expose']['remember'] = 1;
$handler->display->display_options['filters']['config']['expose']['use_operator'] = 0;
$handler->display->display_options['filters']['config']['expose']['reduce'] = 0;
$translatables['rules_scheduler'] = array(
t('Defaults'),
t('more'),
t('Apply'),
t('Reset'),
t('Sort By'),
t('Asc'),
t('Desc'),
t('Items per page'),
t('- All -'),
t('Offset'),
t('No tasks have been scheduled.'),
t('Tid'),
t('Component name'),
t('admin/config/workflow/rules/components/manage/[config]'),
t('Scheduled date'),
t('User provided identifier'),
t('Operations'),
t('delete'),
t('admin/config/workflow/rules/schedule/[tid]/delete'),
t('Delete this scheduled task'),
t('All'),
t('Component filter'),
);
$views = array();
$views[$view->name] = $view;
return $views;
}

25
rules_scheduler/includes/rules_scheduler_views_filter.inc

@ -0,0 +1,25 @@
<?php
/**
* @file
* An extended subclass for component filtering.
*/
class rules_scheduler_views_filter extends views_handler_filter_in_operator {
public function get_value_options() {
if (!isset($this->value_options)) {
$this->value_title = t('Component');
$result = db_select('rules_scheduler', 'r')
->fields('r', array('config'))
->distinct()
->execute();
$config_names = array();
foreach ($result as $record) {
$config_names[$record->config] = $record->config;
}
$this->value_options = $config_names;
}
}
}

139
rules_scheduler/rules_scheduler.admin.inc

@ -0,0 +1,139 @@
<?php
/**
* @file
* Admin forms for scheduling.
*/
/**
* Schedule page with a view for the scheduled tasks.
*/
function rules_scheduler_schedule_page() {
// 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.
$view = views_get_view('rules_scheduler');
$view->override_path = RULES_SCHEDULER_PATH;
$task_list = $view->preview();
}
else {
$task_list = t('To display scheduled tasks you have to install the <a href="https://www.drupal.org/project/views">Views</a> module.');
}
$page['task_view'] = array(
'#markup' => $task_list,
);
$form = drupal_get_form('rules_scheduler_form');
$page['delete'] = array(
'#markup' => drupal_render($form),
);
return $page;
}
/**
* Form for deletion of tasks by component.
*/
function rules_scheduler_form($form, &$form_state) {
$result = db_select('rules_scheduler', 'r')
->fields('r', array('config'))
->distinct()
->execute();
$config_options = array_intersect_key(rules_get_components(TRUE), $result->fetchAllAssoc('config'));
// Fieldset for canceling by component name.
$form['delete_by_config'] = array(
'#type' => 'fieldset',
'#title' => t('Delete tasks by component name'),
'#disabled' => empty($config_options),
);
$form['delete_by_config']['config'] = array(
'#title' => t('Component'),
'#type' => 'select',
'#options' => $config_options,
'#description' => t('Select the component for which to delete all scheduled tasks.'),
'#required' => TRUE,
);
$form['delete_by_config']['submit'] = array(
'#type' => 'submit',
'#value' => t('Delete tasks'),
'#submit' => array('rules_scheduler_form_delete_by_config_submit'),
);
return $form;
}
/**
* Submit handler for deleting future scheduled tasks.
*/
function rules_scheduler_form_delete_by_config_submit($form, &$form_state) {
$config = rules_config_load($form_state['values']['config']);
rules_action('schedule_delete')->execute($config->name);
drupal_set_message(t('All scheduled tasks associated with %config have been deleted.', array('%config' => $config->label())));
$form_state['redirect'] = RULES_SCHEDULER_PATH;
}
/**
* Confirmation form for deleting single tasks.
*/
function rules_scheduler_delete_task($form, &$form_state, $task) {
$form_state['task'] = $task;
$config = rules_config_load($task['config']);
$path['path'] = isset($_GET['destination']) ? $_GET['destination'] : RULES_SCHEDULER_PATH;
$title = t('Are you sure you want to delete the scheduled task %id?', array('%id' => $task['tid']));
if (!empty($task['identifier'])) {
$msg = t('This task with the custom identifier %id executes component %label on %date. The action cannot be undone.', array(
'%label' => $config->label(),
'%id' => $task['identifier'],
'%date' => format_date($task['date']),
));
}
else {
$msg = t('This task executes component %label and will be executed on %date. The action cannot be undone.', array(
'%label' => $config->label(),
'%date' => format_date($task['date']),
));
}
return confirm_form($form, $title, $path, $msg, t('Delete'), t('Cancel'));
}
/**
* Submit handler for deleting single tasks.
*/
function rules_scheduler_delete_task_submit($form, &$form_state) {
rules_scheduler_task_delete($form_state['task']['tid']);
drupal_set_message(t('Task %tid has been deleted.', array('%tid' => $form_state['task']['tid'])));
$form_state['redirect'] = RULES_SCHEDULER_PATH;
}
/**
* Configuration form to manually schedule a rules component.
*/
function rules_scheduler_schedule_form($form, &$form_state, $rules_config, $base_path) {
// Only components can be scheduled.
if (!($rules_config instanceof RulesTriggerableInterface)) {
RulesPluginUI::$basePath = $base_path;
$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 parameter for it.
$form['parameter']['component']['#access'] = FALSE;
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Schedule'),
);
$form['#validate'] = array('rules_ui_form_rules_config_validate');
return $form;
}
drupal_not_found();
exit;
}
/**
* Submit callback to execute the scheduling action.
*/
function rules_scheduler_schedule_form_submit($form, &$form_state) {
$action = $form_state['rules_element'];
$action->execute();
drupal_set_message(t('Component %label has been scheduled.', array('%label' => rules_config_load($form_state['component'])->label())));
$form_state['redirect'] = RULES_SCHEDULER_PATH;
}

81
rules_scheduler/rules_scheduler.drush.inc

@ -0,0 +1,81 @@
<?php
/**
* @file
* Rules Scheduler Drush integration.
*/
/**
* Implements hook_drush_command().
*/
function rules_scheduler_drush_command() {
$items = array();
$items['rules-scheduler-tasks'] = array(
'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.',
),
'drupal dependencies' => array('rules', 'rules_scheduler'),
'aliases' => array('rusch'),
'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 scheduled tasks to the queue and claim items for 30 seconds.',
),
);
return $items;
}
/**
* Implements hook_drush_help().
*/
function rules_scheduler_drush_help($section) {
switch ($section) {
case 'drush:rules-scheduler-tasks':
return dt('Checks for scheduled tasks to be added the queue. Can optionally claim tasks from the queue to work on.');
}
}
/**
* Command callback for processing the rules_scheduler_tasks queue.
*
* @see rules_scheduler_cron_queue_info()
* @see rules_scheduler_cron()
*/
function drush_rules_scheduler_tasks() {
if (rules_scheduler_queue_tasks()) {
// hook_exit() is not invoked for drush runs, so register it as shutdown
// callback for logging the rules log to the watchdog.
drupal_register_shutdown_function('rules_exit');
// Clear the log before running tasks via the queue to avoid logging
// unrelated logs from previous operations.
RulesLog::logger()->clear();
drush_log(dt('Added scheduled tasks to the queue.'), 'success');
}
$claim = drush_get_option('claim', FALSE);
if ($claim) {
// Fetch the queue information and let other modules alter it.
$queue_name = 'rules_scheduler_tasks';
$info = module_invoke('rules_scheduler', 'cron_queue_info');
drupal_alter('cron_queue_info', $info);
$function = $info[$queue_name]['worker callback'];
// The drush option can override the default process time.
$time = is_numeric($claim) ? (int) $claim : $info[$queue_name]['time'];
$end = time() + $time;
// Claim items and process the queue.
$queue = DrupalQueue::get($queue_name);
$claimed = 0;
while (time() < $end && ($item = $queue->claimItem())) {
$function($item->data);
$queue->deleteItem($item);
$claimed++;
}
if ($claimed) {
drush_log(dt('Claimed and worked on !claimed scheduled tasks for up to !time seconds.', array('!claimed' => $claimed, '!time' => $time)), 'success');
}
}
}

19
rules_scheduler/rules_scheduler.info

@ -0,0 +1,19 @@
name = Rules Scheduler
description = Schedule the execution of Rules components using actions.
dependencies[] = rules
package = Rules
core = 7.x
files[] = includes/rules_scheduler.handler.inc
; Views handlers
files[] = includes/rules_scheduler_views_filter.inc
; Test cases
files[] = tests/rules_scheduler.test
files[] = tests/rules_scheduler_test.inc
; Information added by Drupal.org packaging script on 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

225
rules_scheduler/rules_scheduler.install

@ -0,0 +1,225 @@
<?php
/**
* @file
* Rules Scheduler - Installation file.
*/
/**
* Implements hook_schema().
*/
function rules_scheduler_schema() {
$schema['rules_scheduler'] = array(
'description' => 'Stores scheduled tasks.',
'fields' => array(
'tid' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => "The scheduled task's id.",
),
'config' => array(
'type' => 'varchar',
'length' => '64',
'default' => '',
'not null' => TRUE,
'description' => "The scheduled configuration's name.",
),
'date' => array(
'description' => 'The Unix timestamp of when the task is to be scheduled.',
'type' => 'int',
'not null' => TRUE,
),
'data' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
'description' => 'The whole, serialized evaluation data.',
),
'identifier' => array(
'type' => 'varchar',
'length' => '255',
'default' => '',
'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(
'date' => array('date'),
),
'unique key' => array(
'id' => array('config', 'identifier'),
),
);
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.
*/
function rules_scheduler_update_7200() {
// Rename the old table so we can keep its content and start over with a
// fresh one.
db_rename_table('rules_scheduler', 'rules_scheduler_d6');
// Create the d7 table.
$schema['rules_scheduler'] = array(
'description' => 'Stores scheduled tasks.',
'fields' => array(
'tid' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => "The scheduled task's id.",
),
'config' => array(
'type' => 'varchar',
'length' => '255',
'default' => '',
'not null' => TRUE,
'description' => "The scheduled configuration's name.",
),
'date' => array(
'description' => 'The Unix timestamp of when the task is to be scheduled.',
'type' => 'int',
'not null' => TRUE,
),
'data' => array(
'type' => 'text',
'not null' => FALSE,
'serialize' => TRUE,
'description' => 'The whole, serialized evaluation data.',
),
'identifier' => array(
'type' => 'varchar',
'length' => '255',
'default' => '',
'not null' => FALSE,
'description' => 'The user defined string identifying this task.',
),
),
'primary key' => array('tid'),
'indexes' => array('date' => array('date')),
);
db_create_table('rules_scheduler', $schema['rules_scheduler']);
}
/**
* Fix the length of the rules_scheduler.name column.
*/
function rules_scheduler_update_7202() {
// Note that update 7201 (add the 'id' unique key') has been removed as it is
// incorporated by 7202. For anyone that has already run the previous update
// 7201, we have to first drop the unique key.
db_drop_unique_key('rules_scheduler', 'id');
db_change_field('rules_scheduler', 'config', 'config', array(
'type' => 'varchar',
'length' => '64',
'default' => '',
'not null' => TRUE,
'description' => "The scheduled configuration's name.",
));
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.
*/
function rules_scheduler_action_upgrade_map_name($element) {
return 'schedule';
}
/**
* Rules upgrade callback.
*/
function rules_scheduler_action_upgrade($element, $target) {
$target->settings['component'] = $element['#info']['set'];
$target->settings['date'] = $element['#settings']['task_date'];
$target->settings['identifier'] = $element['#settings']['task_identifier'];
unset($element['#info']['arguments']['task_date'], $element['#info']['arguments']['task_identifier']);
foreach ($element['#info']['arguments'] as $name => $info) {
rules_upgrade_element_parameter_settings($element, $target, $name, $info, 'param_' . $name);
}
}
/**
* Rules upgrade callback for mapping the action name.
*/
function rules_action_delete_scheduled_set_upgrade_map_name($element) {
return 'schedule_delete';
}
/**
* Rules upgrade callback.
*/
function rules_action_delete_scheduled_set_upgrade($element, $target) {
$target->settings['component'] = $element['#settings']['ruleset'];
$target->settings['task'] = $element['#settings']['task_identifier'];
}

220
rules_scheduler/rules_scheduler.module

@ -0,0 +1,220 @@
<?php
/**
* @file
* Rules scheduler module.
*/
define('RULES_SCHEDULER_PATH', 'admin/config/workflow/rules/schedule');
/**
* Implements hook_cron().
*/
function rules_scheduler_cron() {
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');
// Clear the log before running tasks via the queue to avoid logging
// unrelated logs from previous cron-operations.
RulesLog::logger()->clear();
}
}
/**
* Implements hook_cron_queue_info().
*/
function rules_scheduler_cron_queue_info() {
$queues['rules_scheduler_tasks'] = array(
'worker callback' => 'rules_scheduler_run_task',
'time' => 15,
);
return $queues;
}
/**
* Queue worker callback for running a single task.
*
* @param array $task
* The task to process.
*/
function rules_scheduler_run_task(array $task) {
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);
}
/**
* Implements hook_rules_ui_menu_alter().
*
* Adds a menu item for the 'schedule' operation.
*/
function rules_scheduler_rules_ui_menu_alter(&$items, $base_path, $base_count) {
$items[$base_path . '/manage/%rules_config/schedule'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Schedule !plugin "!label"', $base_count + 1),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_scheduler_schedule_form', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'file' => 'rules_scheduler.admin.inc',
'file path' => drupal_get_path('module', 'rules_scheduler'),
);
}
/**
* Implements hook_menu().
*/
function rules_scheduler_menu() {
$items = array();
$items[RULES_SCHEDULER_PATH] = array(
'title' => 'Schedule',
'type' => MENU_LOCAL_TASK,
'page callback' => 'rules_scheduler_schedule_page',
'access arguments' => array('administer rules'),
'file' => 'rules_scheduler.admin.inc',
);
$items[RULES_SCHEDULER_PATH . '/%rules_scheduler_task/delete'] = array(
'title' => 'Delete a scheduled task',
'type' => MENU_CALLBACK,
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_scheduler_delete_task', 5),
'access arguments' => array('administer rules'),
'file' => 'rules_scheduler.admin.inc',
);
return $items;
}
/**
* 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')
->fields('r')
->condition('tid', (int) $tid)
->execute();
return $result->fetchAssoc();
}
/**
* Deletes a task by a given task ID.
*
* @param int $tid
* The task ID.
*/
function rules_scheduler_task_delete($tid) {
db_delete('rules_scheduler')
->condition('tid', $tid)
->execute();
}
/**
* Schedule a task to be executed later on.
*
* @param array $task
* An array representing the task with the following keys:
* - config: The machine readable name of the to-be-scheduled component.
* - date: Timestamp when the component should be executed.
* - 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')
->condition('config', $task['config'])
->condition('identifier', $task['identifier'])
->execute();
}
drupal_write_record('rules_scheduler', $task);
}
/**
* Queue tasks that are ready for execution.
*
* @return bool
* TRUE if any queue items were 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) {
// 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();
}
}
/**
* Implements hook_views_api().
*/
function rules_scheduler_views_api() {
return array(
'api' => '3.0-alpha1',
'path' => drupal_get_path('module', 'rules_scheduler') . '/includes',
);
}

217
rules_scheduler/rules_scheduler.rules.inc

@ -0,0 +1,217 @@
<?php
/**
* @file
* Rules integration for the rules scheduler module.
*
* @addtogroup rules
*
* @{
*/
/**
* Implements hook_rules_action_info().
*/
function rules_scheduler_rules_action_info() {
$items['schedule'] = array(
'label' => t('Schedule component evaluation'),
'group' => t('Rules scheduler'),
'base' => 'rules_scheduler_action_schedule',
'named parameter' => TRUE,
'parameter' => array(
'component' => array(
'type' => 'text',
'label' => t('Component'),
'options list' => 'rules_scheduler_component_options_list',
'restriction' => 'input',
'description' => 'Select the component to schedule. Only components containing actions are available – no condition sets.',
),
'date' => array(
'type' => 'date',
'label' => t('Scheduled evaluation date'),
),
'identifier' => array(
'type' => 'text',
'label' => t('Identifier'),
'description' => t('A string used for identifying this task. Any existing tasks for this component with the same identifier will be replaced.'),
'optional' => TRUE,
),
// Further needed parameter by the component are added during processing.
),
);
// Add action to delete scheduled tasks.
$items['schedule_delete'] = array(
'label' => t('Delete scheduled tasks'),
'group' => t('Rules scheduler'),
'base' => 'rules_scheduler_action_delete',
'parameter' => array(
'component' => array(
'type' => 'text',
'label' => t('Component'),
'options list' => 'rules_scheduler_component_options_list',
'description' => t('The component for which scheduled tasks will be deleted.'),
'optional' => TRUE,
),
'task' => array(
'type' => 'text',
'label' => t('Task identifier'),
'description' => t('All tasks that are annotated with the given identifier will be deleted.'),
'optional' => TRUE,
),
),
);
return $items;
}
/**
* Options list callback returning a list of action components.
*/
function rules_scheduler_component_options_list() {
return rules_get_components(TRUE, 'action');
}
/**
* Base action implementation for scheduling components.
*/
function rules_scheduler_action_schedule($args, $element) {
$state = $args['state'];
if ($component = rules_get_cache('comp_' . $args['component'])) {
// Manually create a new evaluation state for scheduling the evaluation.
$new_state = new RulesState();
// Register all parameters as variables.
foreach ($element->pluginParameterInfo() as $name => $info) {
if (strpos($name, 'param_') === 0) {
// Remove the parameter name prefix 'param_'.
$var_name = substr($name, 6);
$new_state->addVariable($var_name, $state->currentArguments[$name], $info);
}
}
rules_scheduler_schedule_task(array(
'date' => $args['date'],
'config' => $args['component'],
'data' => $new_state,
'identifier' => $args['identifier'],
));
}
else {
throw new RulesEvaluationException('Unable to get the component %name', array('%name' => $args['component']), $element, RulesLog::ERROR);
}
}
/**
* Info alteration callback for the schedule action.
*/
function rules_scheduler_action_schedule_info_alter(&$element_info, RulesPlugin $element) {
if (isset($element->settings['component'])) {
// If run during a cache rebuild the cache might not be instantiated yet,
// so fail back to loading the component from database.
if (($component = rules_get_cache('comp_' . $element->settings['component'])) || $component = rules_config_load($element->settings['component'])) {
// Add in the needed parameters.
foreach ($component->parameterInfo() as $name => $info) {
$element_info['parameter']['param_' . $name] = $info;
}
}
}
}
/**
* Validate callback for the schedule action.
*
* Makes sure the component exists and is not dirty.
*
* @see rules_element_invoke_component_validate()
*/
function rules_scheduler_action_schedule_validate(RulesPlugin $element) {
$info = $element->info();
$component = rules_config_load($element->settings['component']);
if (!$component) {
throw new RulesIntegrityException(t('The component %config does not exist.', array('%config' => $element->settings['component'])), $element);
}
// Check if the component is marked as dirty.
rules_config_update_dirty_flag($component);
if (!empty($component->dirty)) {
throw new RulesIntegrityException(t('The utilized component %config fails the integrity check.', array('%config' => $element->settings['component'])), $element);
}
}
/**
* Help for the schedule action.
*/
function rules_scheduler_action_schedule_help() {
return t("Note that component evaluation is triggered by <em>cron</em> – make sure cron is configured correctly by checking your site's !status. The scheduling time accuracy depends on your configured cron interval. See <a href='@url'>the online documentation</a> for more information on how to schedule evaluation of components.",
array('!status' => l(t('Status report'), 'admin/reports/status'),
'@url' => rules_external_help('scheduler')));
}
/**
* Form alter callback for the schedule action.
*/
function rules_scheduler_action_schedule_form_alter(&$form, &$form_state, $options, RulesAbstractPlugin $element) {
$first_step = empty($element->settings['component']);
$form['reload'] = array(
'#weight' => 5,
'#type' => 'submit',
'#name' => 'reload',
'#value' => $first_step ? t('Continue') : t('Reload form'),
'#limit_validation_errors' => array(array('parameter', 'component')),
'#submit' => array('rules_action_type_form_submit_rebuild'),
'#ajax' => rules_ui_form_default_ajax(),
);
// Use ajax and trigger as the reload button.
$form['parameter']['component']['settings']['type']['#ajax'] = $form['reload']['#ajax'] + array(
'event' => 'change',
'trigger_as' => array('name' => 'reload'),
);
if ($first_step) {
// In the first step show only the component select.
foreach (element_children($form['parameter']) as $key) {
if ($key != 'component') {
unset($form['parameter'][$key]);
}
}
unset($form['submit']);
unset($form['provides']);
}
else {
// Hide the reload button in case js is enabled and it's not the first step.
$form['reload']['#attributes'] = array('class' => array('rules-hide-js'));
}
}
/**
* Action: Delete scheduled tasks.
*/
function rules_scheduler_action_delete($component_name = NULL, $task_identifier = NULL) {
$query = db_delete('rules_scheduler');
if (!empty($component_name)) {
$query->condition('config', $component_name);
}
if (!empty($task_identifier)) {
$query->condition('identifier', $task_identifier);
}
$query->execute();
}
/**
* Cancels scheduled task action validation callback.
*/
function rules_scheduler_action_delete_validate($element) {
if (empty($element->settings['task']) && empty($element->settings['task:select']) &&
empty($element->settings['component']) && empty($element->settings['component:select'])) {
throw new RulesIntegrityException(t('You have to specify at least either a component or a task identifier.'), $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.');
}
/**
* @} End of "addtogroup rules"
*/

151
rules_scheduler/tests/rules_scheduler.test

@ -0,0 +1,151 @@
<?php
/**
* @file
* Rules Scheduler tests.
*/
/**
* Test cases for the Rules Scheduler module.
*/
class RulesSchedulerTestCase extends DrupalWebTestCase {
/**
* Declares test metadata.
*/
public static function getInfo() {
return array(
'name' => 'Rules Scheduler tests',
'description' => 'Test scheduling components.',
'group' => 'Rules',
);
}
/**
* Overrides DrupalWebTestCase::setUp().
*/
protected function setUp() {
parent::setUp('rules_scheduler', 'rules_scheduler_test');
RulesLog::logger()->clear();
variable_set('rules_debug_log', TRUE);
}
/**
* Tests scheduling components from the action.
*
* Note that this also makes sure Rules properly handles timezones, else this
* test could fail due to a wrong 'now' timestamp.
*/
public function testComponentSchedule() {
$set = rules_rule_set(array(
'node1' => array('type' => 'node', 'label' => 'node'),
));
$set->rule(
rule()->condition('node_is_published', array('node:select' => 'node1'))
->action('node_unpublish', array('node:select' => 'node1'))
);
$set->integrityCheck()->save('rules_test_set_2');
// Use different names for the variables to ensure they are properly mapped.
$rule = rule(array(
'node2' => array('type' => 'node', 'label' => 'node'),
));
$rule->action('schedule', array(
'component' => 'rules_test_set_2',
'identifier' => 'node_[node2:nid]',
'date' => 'now',
'param_node1:select' => 'node2',
));
$node = $this->drupalCreateNode(array('title' => 'The title.', 'status' => 1));
$rule->execute($node);
// Run cron to let the rules scheduler do its work.
$this->cronRun();
$node = node_load($node->nid, NULL, TRUE);
$this->assertFalse($node->status, 'The component has been properly scheduled.');
RulesLog::logger()->checkLog();
}
/**
* Makes sure recursion prevention is working fine for scheduled rule sets.
*/
public function testRecursionPrevention() {
$set = rules_rule_set(array(
'node1' => array('type' => 'node', 'label' => 'node'),
));
$set->rule(
rule()->condition('node_is_published', array('node:select' => 'node1'))
->action('node_unpublish', array('node:select' => 'node1'))
);
$set->integrityCheck()->save('rules_test_set_2');
// Add an reaction rule that is triggered upon a node save. The scheduled
// component changes the node, thus it would be scheduled again and run in
// an endless loop.
$rule = rules_reaction_rule();
$rule->event('node_insert');
$rule->event('node_update');
$rule->action('schedule', array(
'component' => 'rules_test_set_2',
'identifier' => 'test_recursion_prevention',
'date' => 'now',
'param_node1:select' => 'node',
));
$rule->save();
// 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.
$this->cronRun();
$node = node_load($node->nid, NULL, TRUE);
$this->assertFalse($node->status, 'The component has been properly scheduled.');
// 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));
}
}

24
rules_scheduler/tests/rules_scheduler_test.inc

@ -0,0 +1,24 @@
<?php
/**
* @file
* Include file for Rules Scheduler tests.
*/
/**
* Test task handler class.
*/
class RulesTestTaskHandler extends RulesSchedulerDefaultTaskHandler {
/**
* Overrides RulesSchedulerDefaultTaskHandler::runTask().
*/
public function runTask() {
$task = $this->getTask();
$data = unserialize($task['data']);
// Set the variable defined in the test to TRUE.
variable_set($data['variable'], TRUE);
}
}

12
rules_scheduler/tests/rules_scheduler_test.info

@ -0,0 +1,12 @@
name = "Rules Scheduler Tests"
description = "Support module for the Rules Scheduler tests."
package = Testing
core = 7.x
files[] = rules_scheduler_test.inc
hidden = TRUE
; Information added by Drupal.org packaging script on 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

6
rules_scheduler/tests/rules_scheduler_test.module

@ -0,0 +1,6 @@
<?php
/**
* @file
* Rules Scheduler test module.
*/

2261
tests/rules.test

File diff suppressed because it is too large Load Diff

12
tests/rules_test.info

@ -0,0 +1,12 @@
name = "Rules Tests"
description = "Support module for the Rules tests."
package = Testing
core = 7.x
files[] = rules_test.rules.inc
hidden = TRUE
; Information added by Drupal.org packaging script on 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

58
tests/rules_test.module

@ -0,0 +1,58 @@
<?php
/**
* @file
* Rules test module.
*/
/**
* Implements hook_entity_property_info_alter() to add a property without
* access.
*/
function rules_test_entity_property_info_alter(&$info) {
$properties =& $info['site']['properties'];
$properties['no_access_user'] = array(
'label' => t("Logged in user"),
'description' => t("The currently logged in user."),
'getter callback' => 'entity_metadata_system_get_properties',
'access callback' => 'rules_test_no_access',
'type' => 'user',
);
$properties =& $info['node']['properties'];
$properties['reference'] = array(
'label' => t("Referenced entity"),
'getter callback' => 'rules_test_get_referenced_entity',
'type' => 'entity',
);
$properties['ref_nodes'] = array(
'label' => t("Referenced nodes"),
'getter callback' => 'rules_test_get_referenced_node',
'type' => 'list<node>',
);
}
/**
* Getter callback to get the referenced-entity property.
*/
function rules_test_get_referenced_entity($node) {
// For testing purposes we just return the node itself as property value.
return entity_metadata_wrapper('node', $node);
}
/**
* Getter callback to get the referenced-node list-property.
*/
function rules_test_get_referenced_node($node) {
// For testing purposes we just return the node itself as property value.
return array($node->nid);
}
/**
* Access callback. Returns TRUE except when $op == 'edit'.
*/
function rules_test_no_access($op) {
return $op == 'edit' ? FALSE : TRUE;
}

382
tests/rules_test.rules.inc

@ -0,0 +1,382 @@
<?php
/**
* @file
* Includes any rules integration provided by the module.
*/
/**
* Implements hook_rules_event_info().
*/
function rules_test_rules_event_info() {
return array(
'rules_test_event' => array(
'label' => t('Test event'),
'class' => 'RulesTestEventHandler',
),
);
}
/**
* Implements hook_rules_file_info().
*/
function rules_test_rules_file_info() {
return array('rules_test.test');
}
/**
* Implements hook_rules_condition_info().
*/
function rules_test_rules_condition_info() {
$items = array();
$defaults = array(
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content')),
),
'group' => t('Node'),
);
$items['rules_condition_content_is_type'] = array(
'label' => t('Content has type'),
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content')),
'type' => array('type' => 'list<text>', 'label' => t('Content types')),
),
'help' => t('Evaluates to TRUE, if the given content has one of the selected content types.'),
) + $defaults;
$items['rules_condition_content_is_published'] = $defaults + array(
'label' => t('Content is published'),
);
$items['rules_test_condition_true'] = array(
'label' => t('Test condition returning true'),
'group' => t('Rules test'),
);
$items['rules_test_condition_false'] = array(
'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'),
'parameter' => array(
'node' => array(
'type' => 'node',
'label' => t('Content'),
'wrapped' => TRUE,
),
),
'group' => t('Node'),
);
return $items;
}
/**
* Condition implementation returning true.
*/
function rules_test_condition_true($settings, $state, $element) {
if (!$element instanceof RulesCondition) {
throw new Exception('Rules element has not been passed to condition.');
}
rules_log('condition true called');
return TRUE;
}
/**
* Condition implementation returning false.
*/
function rules_test_condition_false() {
rules_log('condition false called');
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.
*/
function rules_test_condition_node_wrapped($wrapper) {
return $wrapper instanceof EntityMetadataWrapper;
}
/**
* Implements hook_rules_action_info().
*/
function rules_test_rules_action_info() {
$items['rules_test_action'] = array(
'label' => t('Test action'),
'group' => t('Rules test'),
);
return $items + array(
'rules_node_publish_action' => array(
'label' => t('Publish content, but do not save'),
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content')),
),
'callbacks' => array(
'help' => 'rules_test_custom_help',
),
'base' => 'node_publish_action',
),
'rules_node_publish_action_save' => array(
'label' => t('Publish content'),
'parameter' => array(
'node' => array(
'type' => 'node',
'label' => t('Content'),
'save' => TRUE,
),
),
'base' => 'node_publish_action',
),
'rules_node_make_sticky_action' => array(
'label' => t('Make content sticky'),
'parameter' => array(
'node' => array(
'type' => 'node',
'label' => t('Content'),
'save' => TRUE,
),
),
'base' => 'node_make_sticky_action',
),
// The same action again requiring a 'page' node.
'rules_node_page_make_sticky_action' => array(
'label' => t('Mage page content sticky'),
'parameter' => array(
'node' => array(
'type' => 'node',
'label' => t('Content'),
'save' => TRUE,
'bundles' => array('page'),
),
),
'base' => 'node_make_sticky_action',
),
'rules_action_test_reference' => array(
'label' => t('Change argument passed by reference'),
'parameter' => array(
// For references working right, we need a data type with a wrapper.
'arg' => array('type' => 'test'),
),
),
'rules_action_load_node' => array(
'label' => t('Fetch content by id'),
'parameter' => array(
'nid' => array('type' => 'integer', 'label' => t('Content ID')),
'vid' => array(
'type' => 'integer',
'label' => t('Content Revision ID'),
'description' => t("If you want to fetch a specific revision, specify it's revision id. Else leave it empty to fetch the currently active revision."),
'optional' => TRUE,
),
),
'provides' => array(
'node_loaded' => array(
'type' => 'node',
'label' => t('Loaded content'),
'label callback' => 'rules_action_load_node_variable_label',
),
),
'group' => t('Node'),
'access callback' => 'rules_node_integration_access',
),
'rules_action_delete_node' => array(
'label' => t('Delete content'),
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content')),
),
'group' => t('Node'),
'access callback' => 'rules_node_integration_access',
),
// An action for testing named parameters.
'rules_action_node_set_title' => array(
'label' => t('Content'),
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content')),
'title' => array('type' => 'text', 'label' => t('Text')),
),
'named parameter' => TRUE,
'group' => t('Node'),
'access callback' => 'rules_node_integration_access',
),
// Tests automatic saving with a non-entity data type.
'test_type_save' => array(
'base' => 'rules_test_type_save',
'label' => t('Save test type'),
'parameter' => array(
'node' => array(
'type' => 'rules_test_type',
'label' => t('Test content'),
'save' => TRUE,
),
),
'group' => t('Node'),
),
);
}
/**
* Test action doing nothing exception logging it has been called.
*/
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().
*/
function rules_test_rules_data_info() {
return array(
'rules_test_type' => array(
'label' => t('test type'),
'wrap' => TRUE,
'wrapper class' => 'RulesTestTypeWrapper',
),
);
}
/**
* Implements hook_rules_data_info_alter().
*/
function rules_test_rules_data_info_alter(&$data_info) {
$data_info['log_entry']['creation callback'] = 'rules_action_data_create_array';
}
/**
* The custom wrapper class for the rules test type.
*
* For testing we internally just make use of nodes.
*/
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);
}
}

124
tests/rules_test.rules_defaults.inc

@ -0,0 +1,124 @@
<?php
/**
* @file
* Includes any Rules integration provided by the module.
*/
/**
* Implements hook_default_rules_configuration().
*/
function rules_test_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())
->condition('data_is', array('data:select' => 'node:type', 'value' => 'page'))
->action('drupal_message', array('message' => 'A node has been updated.'));
$configs['rules_test_default_1'] = $rule;
$action_set = rules_action_set(array('node' => array('type' => 'node', 'label' => 'Content')));
$action_set->action('node_publish');
$configs['rules_test_action_set'] = $action_set;
// Test providing a rule using an export.
$configs['rules_export_test'] = rules_import(_rules_export_get_test_export());
// An action set used to test merging term parents.
$configs['rules_retrieve_term_parents'] = rules_import('{ "rules_retrieve_term_parents" : {
"LABEL" : "Retrieve term parents",
"PLUGIN" : "action set",
"REQUIRES" : [ "rules" ],
"USES VARIABLES" : {
"terms" : { "label" : "Terms", "type" : "list\u003ctaxonomy_term\u003e" },
"term_parents" : {
"label" : "Term parents",
"type" : "list\u003ctaxonomy_term\u003e",
"parameter" : false
}
},
"ACTION SET" : [
{ "LOOP" : {
"USING" : { "list" : [ "terms" ] },
"ITEM" : { "current_term" : "Current term" },
"DO" : [
{ "LOOP" : {
"USING" : { "list" : [ "current-term:parent" ] },
"ITEM" : { "current_parent" : "Current parent" },
"DO" : [
{ "list_add" : {
"list" : [ "term-parents" ],
"item" : [ "current-parent" ],
"unique" : 1
}
}
]
}
}
]
}
}
],
"PROVIDES VARIABLES" : [ "term_parents" ]
}
}');
return $configs;
}
/**
* Defines the export of rule for testing import/export.
*/
function _rules_export_get_test_export() {
return '{ "rules_export_test" : {
"LABEL" : "Test import rule2",
"PLUGIN" : "reaction rule",
"WEIGHT" : "-1",
"ACTIVE" : false,
"OWNER" : "rules",
"TAGS" : [ "bar", "baz", "foo" ],
"REQUIRES" : [ "rules", "comment" ],
"ON" : { "comment_insert" : [] },
"IF" : [
{ "OR" : [
{ "NOT node_is_sticky" : { "node" : [ "comment:node" ] } },
{ "node_is_of_type" : {
"node" : [ "comment:node" ],
"type" : { "value" : { "page" : "page" } }
}
},
{ "NOT AND" : [ { "OR" : [] } ] }
]
}
],
"DO" : [
{ "data_set" : {
"data" : [ "comment:node:created" ],
"value" : { "select" : "site:current-date", "date_offset" : { "value" : -604800 } }
}
},
{ "node_make_sticky" : { "node" : [ "comment:node" ] } },
{ "variable_add" : {
"USING" : { "type" : "token", "value" : "error" },
"PROVIDE" : { "variable_added" : { "level" : "Error level" } }
}
},
{ "drupal_message" : {
"message" : "fein, [comment:node:title] has been made sticky!",
"type" : [ "level" ]
}
},
{ "LOOP" : {
"USING" : { "list" : [ "site:current-user:roles" ] },
"ITEM" : { "current_role" : "Current role" },
"DO" : [ { "drupal_message" : { "message" : [ "current-role" ] } } ]
}
}
]
}
}';
}

75
tests/rules_test.test.inc

@ -0,0 +1,75 @@
<?php
/**
* @file
* Include file for testing file inclusion.
*/
/**
* Extender for the node data type.
*/
function rules_test_custom_node_save($object) {
throw new RulesEvaluationException('Custom save method invoked.');
}
/**
* Custom help callback for the rules_node_publish_action().
*/
function rules_test_custom_help() {
return 'custom';
}
/**
* Action callback.
*/
function rules_action_test_reference($data) {
$data['changed'] = TRUE;
return array('arg' => $data);
}
/**
* Condition: Check for selected content types.
*/
function rules_condition_content_is_type($node, $type) {
return in_array($node->type, $type);
}
/**
* Condition: Check if the node is published.
*/
function rules_condition_content_is_published($node, $settings) {
return $node->status == 1;
}
/**
* Loads a node.
*/
function rules_action_load_node($nid, $vid = NULL) {
return array('node_loaded' => node_load($nid, $vid ? $vid : NULL));
}
/**
* Action "Delete a node".
*/
function rules_action_delete_node($node) {
node_delete($node->nid);
}
/**
* An action making use of named parameters.
*/
function rules_action_node_set_title($arguments) {
// Make sure the data is unwrapped.
if ($arguments['node'] instanceof EntityMetadataWrapper) {
throw new Exception('Argument has not been correctly unwrapped.');
}
$arguments['node']->title = $arguments['title'];
return $arguments;
}
/**
* Action: Test saving - nothing to do here.
*/
function rules_test_type_save($node) {
}

11
tests/rules_test_invocation.info

@ -0,0 +1,11 @@
name = "Rules Test invocation"
description = "Helper module to test Rules invocations."
package = Testing
core = 7.x
hidden = TRUE
; Information added by Drupal.org packaging script on 2020-03-15
version = "7.x-2.x-dev"
core = "7.x"
project = "rules"
datestamp = "1584309182"

13
tests/rules_test_invocation.module

@ -0,0 +1,13 @@
<?php
/**
* @file
* Helper module for Rules invocation testing.
*/
/**
* Implements hook_node_load().
*/
function rules_test_invocation_node_load($nodes, $types) {
rules_invoke_event('rules_test_event');
}

202
ui/rules.autocomplete.js

@ -0,0 +1,202 @@
// Registers the rules namespace.
Drupal.rules = Drupal.rules || {};
(function($) {
Drupal.behaviors.rules_autocomplete = {
attach: function(context) {
var autocomplete_settings = Drupal.settings.rules_autocomplete;
$('input.rules-autocomplete').once(function() {
var input = this;
new Drupal.rules.autocomplete(input, autocomplete_settings[$(input).attr('id')]);
});
}
};
/**
* Rules autocomplete object.
*/
Drupal.rules.autocomplete = function(input, settings) {
this.id = settings.inputId;
this.uri = settings.source;
this.jqObject = $('#' + this.id);
this.cache = new Array();
this.jqObject.addClass('ui-corner-left');
this.opendByFocus = false;
this.focusOpens = true;
this.groupSelected = false;
this.button = $('<span>&nbsp;</span>');
this.button.attr( {
'tabIndex': -1,
'title': 'Show all items'
});
this.button.insertAfter(this.jqObject);
this.button.button( {
icons: {
primary: 'ui-icon-triangle-1-s'
},
text: false
});
// Don't round the left corners.
this.button.removeClass('ui-corner-all');
this.button.addClass('ui-corner-right ui-button-icon rules-autocomplete-button');
this.jqObject.autocomplete();
this.jqObject.autocomplete("option", "minLength", 0);
// Add a custom class, so we can style the autocomplete box without
// interfering with other jquery autocomplete widgets.
this.jqObject.autocomplete("widget").addClass('rules-autocomplete');
// Save the current rules_autocomplete object, so it can be used in
// handlers.
var instance = this;
// Event handlers
this.jqObject.focus(function() {
if (instance.focusOpens) {
instance.toggle();
instance.opendByFocus = true;
}
else {
instance.focusOpens = true;
}
});
// Needed when the window is closed but the textfield has the focus.
this.jqObject.click(function() {
// Since the focus event happens earlier then the focus event, we need to
// check here, if the window should be opened.
if (!instance.opendByFocus) {
instance.toggle();
}
else {
instance.opendByFocus = false;
}
});
this.jqObject.bind("autocompleteselect", function(event, ui) {
// If a group was selected then set the groupSelected to true for the
// overridden close function from jquery autocomplete.
if (ui.item.value.substring(ui.item.value.length - 1, ui.item.value.length) == ":") {
instance.groupSelected = true;
}
instance.focusOpens = false;
instance.opendByFocus = false;
});
this.jqObject.autocomplete("option", "source", function(request, response) {
if (request.term in instance.cache) {
response(instance.cache[request.term]);
return;
}
$.ajax( {
url: instance.uri + '/' + request.term,
dataType: "json",
success: function(data) {
instance.success(data, request, response);
}
});
});
// 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(autocompleteDataKey)._renderItem = function(ul, item) {
return $("<li></li>").data("item.autocomplete", item).append("<a>" + item.label + "</a>").appendTo(ul);
};
// Override close function
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.
if (value === undefined || instance.groupSelected === false) {
clearTimeout(this.closing);
if (this.menu.element.is(":visible")) {
this._trigger("close", event);
this.menu.element.hide();
// Use deactivate for older versions of jQuery UI.
if (typeof(this.menu.deactivate) === 'function') {
this.menu.deactivate();
}
}
}
else {
// Else keep all open and trigger a search for the group.
instance.jqObject.autocomplete("search", instance.jqObject.val());
// After the suggestion box was opened again, we want to be able to
// close it.
instance.groupSelected = false;
}
};
this.button.click(function() {
instance.toggle();
});
};
/**
* Success function for Rules autocomplete object.
*/
Drupal.rules.autocomplete.prototype.success = function(data, request, response) {
var list = new Array();
jQuery.each(data, function(index, value) {
list.push( {
label: value,
value: index
});
});
this.cache[request.term] = list;
response(list);
};
/**
* Open the autocomplete window.
* @param searchFor The term for will be searched for. If undefined then the
* entered input text will be used.
*/
Drupal.rules.autocomplete.prototype.open = function(searchFor) {
// If searchFor is undefined, we want to search for the passed argument.
this.jqObject.autocomplete("search", ((searchFor === undefined) ? this.jqObject.val() : searchFor));
this.button.addClass("ui-state-focus");
};
/**
* Close the autocomplete window.
*/
Drupal.rules.autocomplete.prototype.close = function() {
this.jqObject.autocomplete("close");
this.button.removeClass("ui-state-focus");
};
/**
* Toggle the autocomplete window.
*/
Drupal.rules.autocomplete.prototype.toggle = function() {
if (this.jqObject.autocomplete("widget").is(":visible")) {
this.close();
this.focusOpens = true;
}
else {
var groups = this.jqObject.val().split(":");
var selector = "";
for (var i=0; i<groups.length-1; i++) {
selector = selector.concat(groups[i]) + ":";
}
this.focusOpens = false;
this.jqObject.focus();
this.open(selector);
}
};
})(jQuery);

68
ui/rules.debug.js

@ -0,0 +1,68 @@
/**
* @file
* Adds the collapsible functionality to the rules debug log.
*/
// Registers the rules namespace.
Drupal.rules = Drupal.rules || {};
(function($) {
Drupal.behaviors.rules_debug_log = {
attach: function(context) {
$('.rules-debug-open').click(function () {
var icon = $(this).children('span.ui-icon');
if ($(this).next().is(':hidden')) {
Drupal.rules.changeDebugIcon(icon, true);
}
else {
Drupal.rules.changeDebugIcon(icon, false);
}
$(this).next().toggle();
}).next().hide();
$('.rules-debug-open-main').click(function () {
var icon = $(this).children('span.ui-icon');
if ($(this).parent().next().is(':hidden')) {
Drupal.rules.changeDebugIcon(icon, true);
$(this).parent().children('.rules-debug-open-all').text(Drupal.t('-Close all-'));
}
else {
Drupal.rules.changeDebugIcon(icon, false);
$(this).parent().children('.rules-debug-open-all').text(Drupal.t('-Open all-'));
}
$(this).parent().next().toggle();
}).parent().next().hide();
$('.rules-debug-open-all').click(function() {
if ($('.rules-debug-open-main').parent().next().is(':hidden')) {
$('.rules-debug-open').next().show();
Drupal.rules.changeDebugIcon($('.rules-debug-open').children('span.ui-icon'), true);
$('.rules-debug-open-main').parent().next().show();
Drupal.rules.changeDebugIcon($(this).prev().children('span.ui-icon'), true);
$(this).text(Drupal.t('-Close all-'));
}
else {
$('.rules-debug-open-main').parent().next().hide();
Drupal.rules.changeDebugIcon($('.rules-debug-open-main').children('span.ui-icon'), false);
$(this).text(Drupal.t('-Open all-'));
$('.rules-debug-open').next().hide();
Drupal.rules.changeDebugIcon($(this).prev().children('span.ui-icon'), false);
}
});
}
};
/**
* Changes the icon of a collapsible div.
*/
Drupal.rules.changeDebugIcon = function(item, open) {
if (open == true) {
item.removeClass('ui-icon-triangle-1-e');
item.addClass('ui-icon-triangle-1-s');
}
else {
item.removeClass('ui-icon-triangle-1-s');
item.addClass('ui-icon-triangle-1-e');
}
}
})(jQuery);

44
ui/rules.ui-rtl.css

@ -0,0 +1,44 @@
/**
* @file
* Right-to-Left stylesheet for the Rules module.
*/
.rules-elements-table ul.rules-operations li {
float: right;
}
.rules-elements-table ul.rules-operations a {
padding-right: 0px;
}
ul.rules-operations-add li {
float: right;
}
.rules-elements-table .tabledrag-toggle-weight-wrapper {
right: auto;
left: 0px;
}
.rules-elements-table caption,
.rules-overview-table caption {
text-align: right;
}
.rules-debug-icon-open {
float: right;
}
.rules-debug-log ul {
padding-left: 0;
padding-right: 2em;
}
.rules-element-content {
float: right;
}
ul.rules-autocomplete div {
padding-left: 0;
padding-right: 5px;
}

203
ui/rules.ui.css

@ -0,0 +1,203 @@
/**
* @file
* Stylesheet for the Rules module.
*/
@charset "utf-8";
.rules-show-js,
html.js .rules-hide-js {
display: none;
}
.rules-hide-js,
html.js .rules-show-js {
display: block;
}
.rules-elements-table ul.action-links {
margin: 0px;
padding: 0;
}
.rules-elements-table ul.rules-operations li {
list-style: none;
float: left; /* LTR */
}
.rules-elements-table ul.rules-operations a {
background: none;
padding-left: 0px; /* LTR */
}
table tr.rules-elements-add {
background-color: #e5eff4;
}
.rules-elements-table ul.rules-operations-add a {
line-height: 1em;
}
tr.rules-elements-add td {
padding-top: 2px;
padding-bottom: 2px;
}
ul.rules-operations-add li {
float: left; /* LTR */
list-style-position: inside;
}
.rules-elements-table {
margin-bottom: 3em;
}
/* We cannot set a positive margin-top for rules tables as the table drag link
should be positioned directly on top of the table. Thus we use a large bottom
margin and fix the upper most margin: */
#rules-form-wrapper:first-child {
margin-top: 1.5em;
}
/* Fix table drag weights to don't take extra space */
.rules-elements-table .tabledrag-toggle-weight-wrapper {
position: absolute;
right: 0px; /* LTR */
}
.rules-elements-table caption,
.rules-overview-table caption {
font-size: 110%;
font-weight: bold;
padding-bottom: 0.5em;
text-align: left; /* LTR */
}
.rules-overview-table {
margin: 1em 0;
}
.rules-content-group-integrity-error {
color: #df0101;
}
.rules-debug-log {
font: 81.3% "Lucida Grande","Lucida Sans Unicode",sans-serif;
background-color: #eeeeee;
border: 1px solid #cccccc;
color: #333333;
padding: 5px;
margin: 1.5em 0em;
}
.rules-debug-collapsible-link {
position: relative;
cursor: pointer;
/* 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;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
user-select: none;
}
.rules-debug-log-head {
font-weight: bold;
}
div.rules-debug-log-head {
margin: 0.5em 0em;
}
.rules-debug-icon-open {
position: relative;
float: left; /* LTR */
}
.rules-debug-open-all {
position: relative;
float: right;
}
.rules-debug-log ul {
padding-left: 2em; /* LTR */
}
.rules-debug-log .rules-debug-warn {
color: #df0101;
}
.rules-debug-log .rules-debug-error {
font-weight: bold;
color: #df0101;
}
#rules-filter-form {
margin-bottom: 1.5em;
}
.rules-parameter-label {
font-style: italic;
}
#rules-plugin-add-help {
margin-bottom: 1em;
}
.rules-element-content {
float: left; /* LTR */
}
form input.rules-switch-button {
-moz-border-radius: 5px 5px 5px 5px;
cursor: pointer;
font-size: 0.8em;
font-weight: normal;
margin-bottom: 1em;
padding: 2px;
text-align: center;
}
.rules-form-heading {
margin-top: 3em;
}
.rules-autocomplete-button {
top: 3px;
height: 22px;
}
ul.rules-autocomplete {
max-height: 23em;
overflow-y: auto;
}
ul.rules-autocomplete div {
padding-left: 5px; /* LTR */
}
ul.rules-autocomplete a.ui-corner-all {
padding: 0px;
}
ul.rules-autocomplete .rules-dsac-group {
background-color: #eee;
}
ul.rules-autocomplete .ui-corner-all {
-moz-border-radius: 0px;
}
/**
* Do not display the hide/show descriptions link above the permissions matrix.
*/
#rules-form-wrapper #edit-settings-access-permissions .compact-link {
display: none;
}
/* IE 6 hack for max-height. */
* html ul.rule-autocomplete {
height: 23em;
}

70
ui/rules.ui.seven-rtl.css

@ -0,0 +1,70 @@
/**
* @file
* Right-to-Left jQuery Rules UI stylesheet fix for the Seven theme.
*/
.ui-corner-tl {
-moz-border-radius-topleft: 0;
-webkit-border-top-left-radius: 0;
border-top-left-radius: 0;
-moz-border-radius-topright: 4px;
-webkit-border-top-right-radius: 4px;
border-top-right-radius: 4px;
}
.ui-corner-tr {
-moz-border-radius-topright: 0;
-webkit-border-top-right-radius: 0;
border-top-right-radius: 0;
-moz-border-radius-topleft: 4px;
-webkit-border-top-left-radius: 4px;
border-top-left-radius: 4px;
}
.ui-corner-bl {
-moz-border-radius-bottomleft: 0;
-webkit-border-bottom-left-radius: 0;
border-bottom-left-radius: 0;
-moz-border-radius-bottomright: 4px;
-webkit-border-bottom-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.ui-corner-br {
-moz-border-radius-bottomright: 0;
-webkit-border-bottom-right-radius: 0;
border-bottom-right-radius: 0;
-moz-border-radius-bottomleft: 4px;
-webkit-border-bottom-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.ui-corner-right {
-moz-border-radius-bottomright: 0;
-moz-border-radius-topright: 0;
-webkit-border-bottom-right-radius: 0;
-webkit-border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
-moz-border-radius-bottomleft: 4px;
-moz-border-radius-topleft: 4px;
-webkit-border-bottom-left-radius: 4px;
-webkit-border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
}
.ui-corner-left {
-moz-border-radius-bottomleft: 0;
-moz-border-radius-topleft: 0;
-webkit-border-bottom-left-radius: 0;
-webkit-border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
-moz-border-radius-bottomright: 4px;
-moz-border-radius-topright: 4px;
-webkit-border-bottom-right-radius: 4px;
-webkit-border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
}

96
ui/rules.ui.seven.css

@ -0,0 +1,96 @@
/**
* @file
* JQuery Rules UI stylesheet fix for the Seven theme.
*/
.ui-button {
border: 1px solid #cccccc;
background: #e6e6e6;
}
.ui-state-hover,
.ui-state-focus {
border: 1px solid #bbbbbb;
}
.ui-button.ui-state-active {
border: 1px solid #777777;
font-weight: bold;
}
/**
* Corner radius
*/
.ui-corner-tl {
-moz-border-radius-topleft: 4px; /* LTR */
-webkit-border-top-left-radius: 4px; /* LTR */
border-top-left-radius: 4px; /* LTR */
}
.ui-corner-tr {
-moz-border-radius-topright: 4px; /* LTR */
-webkit-border-top-right-radius: 4px; /* LTR */
border-top-right-radius: 4px; /* LTR */
}
.ui-corner-bl {
-moz-border-radius-bottomleft: 4px; /* LTR */
-webkit-border-bottom-left-radius: 4px; /* LTR */
border-bottom-left-radius: 4px; /* LTR */
}
.ui-corner-br {
-moz-border-radius-bottomright: 4px; /* LTR */
-webkit-border-bottom-right-radius: 4px; /* LTR */
border-bottom-right-radius: 4px; /* LTR */
}
.ui-corner-top {
-moz-border-radius-topleft: 4px;
-moz-border-radius-topright: 4px;
-webkit-border-top-left-radius: 4px;
-webkit-border-top-right-radius: 4px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.ui-corner-bottom {
-moz-border-radius-bottomleft: 4px;
-moz-border-radius-bottomright: 4px;
-webkit-border-bottom-left-radius: 4px;
-webkit-border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.ui-corner-right {
-moz-border-radius-bottomright: 4px; /* LTR */
-moz-border-radius-topright: 4px; /* LTR */
-webkit-border-bottom-right-radius: 4px; /* LTR */
-webkit-border-top-right-radius: 4px; /* LTR */
border-bottom-right-radius: 4px; /* LTR */
border-top-right-radius: 4px; /* LTR */
}
.ui-corner-left {
-moz-border-radius-bottomleft: 4px; /* LTR */
-moz-border-radius-topleft: 4px; /* LTR */
-webkit-border-bottom-left-radius: 4px; /* LTR */
-webkit-border-top-left-radius: 4px; /* LTR */
border-bottom-left-radius: 4px; /* LTR */
border-top-left-radius: 4px; /* LTR */
}
.ui-corner-all {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
}
/**
* Fix the position of the core-autocomplete popup when shown in the settings
* fieldset.
*/
.form-item-settings-tags {
position: relative;
}

342
ui/ui.controller.inc

@ -0,0 +1,342 @@
<?php
/**
* @file
* Contains the UI controller for Rules.
*/
/**
* Controller class for the Rules UI.
*
* The Rules UI controller defines the methods other modules may use in order
* to easily re-use the UI, regardless whether the rules admin module is
* enabled.
*/
class RulesUIController {
/**
* Generates menu items to manipulate rules configurations.
*
* @param string $base_path
* The path to the overview page from where the configurations are edited.
*/
public function config_menu($base_path) {
$items = array();
$base_count = count(explode('/', $base_path));
$items[$base_path . '/manage/%rules_config'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Editing !plugin "!label"', $base_count + 1),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_form_edit_rules_config', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'type' => MENU_VISIBLE_IN_BREADCRUMB,
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/add/%rules_element'] = array(
// Adding another part to the path would hit the menu path-part-limit
// for base paths like admin/config/workflow/rules. Therefore we have to
// use this ugly way for setting the title.
'title callback' => 'rules_menu_add_element_title',
// Wrap the integer in an array, so it is passed as is.
'title arguments' => array(array($base_count + 4)),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_add_element', $base_count + 1, $base_count + 4, $base_count + 3, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'load arguments' => array($base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/add/event'] = array(
'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_page', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'load arguments' => array($base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/delete/event'] = array(
// @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),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'description' => 'Remove an event from a reaction rule.',
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/edit/%rules_element'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Editing !plugin "!label"', $base_count + 3),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_edit_element', $base_count + 1, $base_count + 3, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'load arguments' => array($base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/autocomplete'] = array(
'page callback' => 'rules_ui_form_data_selection_auto_completion',
'page arguments' => array($base_count + 3, $base_count + 4, $base_count + 5),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'type' => MENU_CALLBACK,
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/delete/%rules_element'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Editing !plugin "!label"', $base_count + 3),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_delete_element', $base_count + 1, $base_count + 3, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'load arguments' => array($base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/%'] = array(
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_form_rules_config_confirm_op', $base_count + 1, $base_count + 2, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/clone'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Cloning !plugin "!label"', $base_count + 1),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_form_clone_rules_config', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/export'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Export of !plugin "!label"', $base_count + 1),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_form_export_rules_config', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('view', $base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
$items[$base_path . '/manage/%rules_config/execute'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Executing !plugin "!label"', $base_count + 1),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_ui_form_execute_rules_config', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'file' => 'ui/ui.forms.inc',
'file path' => drupal_get_path('module', 'rules'),
);
drupal_alter('rules_ui_menu', $items, $base_path, $base_count);
if (module_exists('rules_scheduler')) {
$items[$base_path . '/manage/%rules_config/schedule'] = array(
'title callback' => 'rules_get_title',
'title arguments' => array('Schedule !plugin "!label"', $base_count + 1),
'page callback' => 'drupal_get_form',
'page arguments' => array('rules_scheduler_schedule_form', $base_count + 1, $base_path),
'access callback' => 'rules_config_access',
'access arguments' => array('update', $base_count + 1),
'file' => 'rules_scheduler.admin.inc',
'file path' => drupal_get_path('module', 'rules_scheduler'),
);
}
return $items;
}
/**
* 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
* RulesUIController::config_menu($base_path) to generate appropriate menu
* items for the path at which the overview table is displayed.
*
* @param array $conditions
* An array of conditions as needed by rules_config_load_multiple().
* @param array $options
* An array with optional options. Known keys are:
* - 'hide status op': If set to TRUE, enable/disable links are not added.
* Defaults to FALSE.
* - 'show plugin': If set to FALSE, the plugin is not shown. Defaults to
* TRUE.
* - 'show events': If set to TRUE, the event column is shown. Defaults to
* TRUE if only reaction rules are listed.
* - 'show execution op': If set to TRUE an operation for execution a
* component is shown for components, as well as a link to schedule a
* component if the rules scheduler module is enabled.
* - 'base path': Optionally, a different base path to use instead of the
* currently set RulesPluginUI::$basePath. If no base path has been set
* yet, the current path is used by default.
*
* @return array
* A renderable array.
*/
public function overviewTable($conditions = array(), $options = array()) {
$options += array(
'hide status op' => FALSE,
'show plugin' => TRUE,
'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'];
}
elseif (!isset(RulesPluginUI::$basePath)) {
// Default to the current path, only if no path has been set yet.
RulesPluginUI::$basePath = current_path();
}
$entities = entity_load('rules_config', FALSE, $conditions);
ksort($entities);
// Prepare some variables used by overviewTableRow().
$this->event_info = rules_fetch_data('event_info');
$this->cache = rules_get_cache();
$rows = array();
foreach ($entities as $id => $entity) {
if (user_access('bypass rules access') || $entity->access()) {
$rows[] = $this->overviewTableRow($conditions, $id, $entity, $options);
}
}
// Assemble the right table header.
$header = array(t('Name'), t('Event'), t('Plugin'), t('Status'), array('data' => t('Operations')));
if (!$options['show events']) {
// Remove the event heading as there is no such column.
unset($header[1]);
}
if (!$options['show plugin']) {
unset($header[2]);
}
// Fix the header operation column colspan.
$num_cols = isset($rows[0]) ? count($rows[0]) : 0;
if (($addition = $num_cols - count($header)) > 0) {
$header[4]['colspan'] = $addition + 1;
}
$table = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('None.'),
);
$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.
return $table;
}
/**
* Generates the row for a single rules config.
*
* @param array $conditions
* An array of conditions as needed by rules_config_load_multiple().
* @param array $options
* An array with optional options. Known keys are:
* - 'hide status op': If set to TRUE, enable/disable links are not added.
* Defaults to FALSE.
* - 'show plugin': If set to FALSE, the plugin is not shown. Defaults to
* TRUE.
* - 'show events': If set to TRUE, the event column is shown. Defaults to
* TRUE if only reaction rules are listed.
* - 'show execution op': If set to TRUE an operation for execution a
* component is shown for components, as well as a link to schedule a
* component if the rules scheduler module is enabled.
* - 'base path': Optionally, a different base path to use instead of the
* currently set RulesPluginUI::$basePath. If no base path has been set
* yet, the current path is used by default.
*/
protected function overviewTableRow($conditions, $name, $config, $options) {
// Build content includes the label, as well as a short overview including
// the machine name.
$row[] = array('data' => $config->buildContent());
// Add events if the configs are assigned to events.
if ($options['show events']) {
$events = array();
if ($config instanceof RulesTriggerableInterface) {
foreach ($config->events() as $event_name) {
$event_handler = rules_get_event_handler($event_name, $config->getEventSettings($event_name));
$events[] = $event_handler->summary();
}
}
$row[] = implode(", ", $events);
}
if ($options['show plugin']) {
$plugin = $config->plugin();
$row[] = isset($this->cache['plugin_info'][$plugin]['label']) ? $this->cache['plugin_info'][$plugin]['label'] : $plugin;
}
$row[] = array('data' => array(
'#theme' => 'entity_status',
'#status' => $config->status,
));
// 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'))));
if (module_exists('rules_i18n')) {
$row[] = l(t('translate'), RulesPluginUI::path($name, 'translate'), array('attributes' => array('class' => array('translate', 'action'))));
}
}
else {
$row[] = '';
if (module_exists('rules_i18n')) {
$row[] = '';
}
}
if (!$options['hide status op']) {
// Add either an enable or disable link.
$text = $config->active ? t('disable') : t('enable');
$active_class = $config->active ? 'disable' : 'enable';
$link_path = RulesPluginUI::path($name, $active_class);
$row[] = $config->hasStatus(ENTITY_FIXED) ? '' : l($text, $link_path, array('attributes' => array('class' => array($active_class, 'action')), 'query' => drupal_get_destination()));
}
$row[] = l(t('clone'), RulesPluginUI::path($name, 'clone'), array('attributes' => array('class' => array('clone', 'action'))));
// Add execute link for for components.
if ($options['show execution op']) {
$row[] = ($config instanceof RulesTriggerableInterface) ? '' : l(t('execute'), RulesPluginUI::path($name, 'execute'), array('attributes' => array('class' => array('execute', 'action')), 'query' => drupal_get_destination()));
if (module_exists('rules_scheduler')) {
// Add schedule link for action components only.
$row[] = $config instanceof RulesActionInterface ? l(t('schedule'), RulesPluginUI::path($name, 'schedule'), array('attributes' => array('class' => array('schedule', 'action')), 'query' => drupal_get_destination())) : '';
}
}
if (!$config->hasStatus(ENTITY_IN_CODE) && !$config->hasStatus(ENTITY_FIXED)) {
$row[] = l(t('delete'), RulesPluginUI::path($name, 'delete'), array('attributes' => array('class' => array('delete', 'action')), 'query' => drupal_get_destination()));
}
elseif ($config->hasStatus(ENTITY_OVERRIDDEN) && !$config->hasStatus(ENTITY_FIXED)) {
$row[] = l(t('revert'), RulesPluginUI::path($name, 'revert'), array('attributes' => array('class' => array('revert', 'action')), 'query' => drupal_get_destination()));
}
else {
$row[] = '';
}
$row[] = l(t('export'), RulesPluginUI::path($name, 'export'), array('attributes' => array('class' => array('export', 'action'))));
return $row;
}
}

1370
ui/ui.core.inc

File diff suppressed because it is too large Load Diff

702
ui/ui.data.inc

@ -0,0 +1,702 @@
<?php
/**
* @file
* Contains data type related forms.
*/
/**
* Interface for data types providing a direct input form.
*/
interface RulesDataDirectInputFormInterface {
/**
* Constructs the direct input form.
*
* @return array
* The direct input form.
*/
public static function inputForm($name, $info, $settings, RulesPlugin $element);
/**
* Render the configured value.
*
* @return array
* A renderable array.
*/
public static function render($value);
}
/**
* Interface for data UI classes providing an options list.
*/
interface RulesDataInputOptionsListInterface extends RulesDataDirectInputFormInterface {
/**
* Returns the options list for the data type.
*
* For retrieving information about the used data type and parameter, the
* helper RulesDataUI::getTypeInfo() may be used as following:
* @code
* list($type, $parameter_info) = RulesDataUI::getTypeInfo($element, $name);
* @endcode
*
* @param RulesPlugin $element
* The rules element to get the options for.
* @param string $name
* The name of the parameter for which to get options.
*
* @return array
* An array of options as used by hook_options_list().
*/
public static function optionsList(RulesPlugin $element, $name);
}
/**
* Default UI related class for data types.
*/
class RulesDataUI {
/**
* Specifies the default input mode per data type.
*/
public static function getDefaultMode() {
return 'selector';
}
/**
* Provides the selection form for a parameter.
*/
public static function selectionForm($name, $info, $settings, RulesPlugin $element) {
if (!isset($settings[$name . ':select'])) {
$settings[$name . ':select'] = '';
$vars = $element->availableVariables();
// Default to variables with the same name as the parameter.
if (isset($vars[$name])) {
$settings[$name . ':select'] = $name;
}
// If there is only one match, use it by default.
elseif (count($matches = RulesData::matchingDataSelector($vars, $info, '', 1, FALSE)) == 1) {
$settings[$name . ':select'] = rules_array_key($matches);
}
}
$form[$name . ':select'] = array(
'#type' => 'rules_data_selection',
'#title' => t('Data selector'),
'#default_value' => $settings[$name . ':select'],
'#required' => empty($info['optional']),
'#autocomplete_path' => RulesPluginUI::path($element->root()->name, 'autocomplete' . '/' . $name),
// Make the autocomplete textfield big enough so that it can display
// descriptions without word wraps.
'#size' => 75,
'#description' => t("The data selector helps you drill down into the data available to Rules. <em>To make entity fields appear in the data selector, you may have to use the condition 'entity has field' (or 'content is of type').</em> More useful tips about data selection is available in <a href='@url'>the online documentation</a>.",
array('@url' => rules_external_help('data-selection'))),
);
$cache = rules_get_cache();
$form['types_help'] = array(
'#theme' => 'rules_settings_help',
'#heading' => t('Data types'),
);
if ($info['type'] == '*') {
$type_labels[] = t('any');
}
else {
$types = is_array($info['type']) ? $info['type'] : array($info['type']);
$type_labels = array();
foreach ($types as $type) {
$type_labels[] = drupal_ucfirst(isset($cache['data_info'][$type]['label']) ? $cache['data_info'][$type]['label'] : $type);
}
}
$form['types_help']['#text'] = format_plural(count($type_labels), 'Select data of the type %types.', 'Select data of the types %types.', array('%types' => implode(', ', $type_labels)));
if (!empty($info['translatable'])) {
if (empty($info['custom translation language'])) {
$text = t('If a multilingual data source (i.e. a translatable field) is given, the argument is translated to the current interface language.');
}
else {
$text = t('If a multilingual data source (i.e. a translatable field) is given, the argument is translated to the configured language.');
}
$form['translation'] = array(
'#theme' => 'rules_settings_help',
'#text' => $text,
'#heading' => t('Translation'),
);
}
$form['help'] = array(
'#theme' => 'rules_data_selector_help',
'#variables' => $element->availableVariables(),
'#parameter' => $info,
);
// Add data processor.
$settings += array($name . ':process' => array());
$form[$name . ':process'] = array();
RulesDataProcessor::attachForm($form[$name . ':process'], $settings[$name . ':process'], $info, $element->availableVariables());
return $form;
}
/**
* Renders the value with a label if an options list is available.
*
* Used for data UI classes implementing the
* RulesDataDirectInputFormInterface.
*
* In case an options list is available, the usual render() method won't
* be invoked, instead the selected entry is rendered via this method.
*
* @todo for Drupal 8: Refactor to avoid implementations have to care about
* option lists when generating the form, but not when rendering values.
*/
public static function renderOptionsLabel($value, $name, $info, RulesPlugin $element) {
if (!empty($info['options list'])) {
$element->call('loadBasicInclude');
$options = entity_property_options_flatten(call_user_func($info['options list'], $element, $name));
if (!is_array($value) && isset($options[$value])) {
$value = $options[$value];
}
elseif (is_array($value)) {
foreach ($value as $key => $single_value) {
if (isset($options[$single_value])) {
$value[$key] = $options[$single_value];
}
}
$value = implode(', ', $value);
}
return array(
'content' => array('#markup' => check_plain($value)),
'#attributes' => array('class' => array('rules-parameter-options-entry')),
);
}
}
/**
* 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]);
}
}
/**
* UI for textual data.
*/
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
// options list callback may reside there.
$element->call('loadBasicInclude');
$form[$name] = array(
'#type' => 'select',
'#options' => call_user_func($info['options list'], $element, $name),
);
}
else {
$form[$name] = array(
'#type' => 'textarea',
'#rows' => 3,
);
RulesDataInputEvaluator::attachForm($form, $settings, $info, $element->availableVariables());
}
$settings += array($name => isset($info['default value']) ? $info['default value'] : NULL);
$form[$name] += array(
'#title' => t('Value'),
'#default_value' => $settings[$name],
'#required' => empty($info['optional']),
'#after_build' => array('rules_ui_element_fix_empty_after_build'),
);
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')),
);
}
}
/**
* UI for text tokens.
*/
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') {
$form[$name]['#element_validate'][] = 'rules_ui_element_token_validate';
$form[$name]['#description'] = t('May only contain lowercase letters, numbers, and underscores and has to start with a letter.');
$form[$name]['#rows'] = 1;
}
return $form;
}
}
/**
* UI for formatted text.
*/
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));
$form[$name]['#type'] = 'text_format';
$form[$name]['#base_type'] = 'textarea';
$form[$name]['#default_value'] = $settings[$name]['value'];
$form[$name]['#format'] = $settings[$name]['format'];
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'])) {
$form[$name]['#type'] = 'textfield';
}
$form[$name]['#element_validate'][] = 'rules_ui_element_decimal_validate';
$form[$name]['#rows'] = 1;
return $form;
}
}
/**
* UI for integers.
*/
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'])) {
$form[$name]['#type'] = 'textfield';
}
$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;
}
}
/**
* UI for boolean data.
*/
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' => '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')),
);
}
}
/**
* UI for dates.
*/
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));
// Convert any configured timestamp into a readable format.
if (is_numeric($settings[$name])) {
$settings[$name] = gmdate('Y-m-d H:i:s', $settings[$name]);
}
$form = parent::inputForm($name, $info, $settings, $element);
$form[$name]['#type'] = 'textfield';
$form[$name]['#element_validate'][] = 'rules_ui_element_date_validate';
// Note that the date input evaluator takes care for parsing dates using
// strtotime() into a timestamp, which is the internal date format.
$form[$name]['#description'] = t('The date in GMT. You may enter a fixed time (like %format) or any other values in GMT known by the PHP !strtotime function (like "+1 day"). Relative dates like "+1 day" or "now" relate to the evaluation time.',
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 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(
'content' => array('#markup' => $value),
'#attributes' => array('class' => array('rules-parameter-date')),
);
}
}
/**
* UI for duration type parameter.
*/
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';
$form[$name]['#after_build'][] = 'rules_ui_element_duration_after_build';
return $form;
}
/**
* Implements RulesDataDirectInputFormInterface::render().
*/
public static function render($value) {
$value = is_numeric($value) ? format_interval($value) : check_plain($value);
return array(
'content' => array('#markup' => $value),
'#attributes' => array('class' => array('rules-parameter-duration')),
);
}
}
/**
* UI for the URI type parameter.
*/
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' => 'https://www.drupal.org'));
return $form;
}
}
/**
* UI for lists of textual data.
*/
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) {
$settings += array($name => isset($info['default value']) ? $info['default value'] : NULL);
$form = parent::inputForm($name, $info, $settings, $element);
if ($form[$name]['#type'] == 'textarea') {
// Fix up the value to be an array during after build.
$form[$name]['#delimiter'] = "\n";
$form[$name]['#after_build'][] = 'rules_ui_list_textarea_after_build';
$form[$name]['#pre_render'][] = 'rules_ui_list_textarea_pre_render';
$form[$name]['#default_value'] = !empty($settings[$name]) ? implode("\n", $settings[$name]) : NULL;
$form[$name]['#description'] = t('A list of values, one on each line.');
}
else {
$form[$name]['#multiple'] = TRUE;
}
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')),
);
}
}
/**
* UI for lists of integers.
*/
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);
if ($form[$name]['#type'] == 'textarea') {
$form[$name]['#description'] = t('A list of integers, separated by commas. E.g. enter "1, 2, 3".');
$form[$name]['#delimiter'] = ',';
$form[$name]['#default_value'] = !empty($settings[$name]) ? implode(", ", $settings[$name]) : NULL;
$form[$name]['#element_validate'][] = 'rules_ui_element_integer_list_validate';
$form[$name]['#rows'] = 1;
}
return $form;
}
}
/**
* UI for lists of tokens.
*/
class RulesDataUIListToken extends RulesDataUIListInteger {
/**
* Implements RulesDataDirectInputFormInterface::inputForm().
*/
public static function inputForm($name, $info, $settings, RulesPlugin $element) {
$form = parent::inputForm($name, $info, $settings, $element);
if ($form[$name]['#type'] == 'textarea') {
$form[$name]['#description'] = t('A list of text tokens, separated by commas. E.g. enter "one, two, three".');
$form[$name]['#element_validate'] = array('rules_ui_element_token_list_validate');
}
return $form;
}
}
/**
* UI for entity-based data types.
*/
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'])) {
$form[$name]['#type'] = 'textfield';
$entity_info = entity_get_info($info['type']);
if (empty($entity_info['entity keys']['name'])) {
$form[$name]['#element_validate'][] = 'rules_ui_element_integer_validate';
}
$form[$name]['#title'] = t('@entity identifier', array('@entity' => $entity_info['label']));
$entity_label = strtolower($entity_info['label'][0]) . substr($entity_info['label'], 1);
$form[$name]['#description'] = t('Specify an identifier of a @entity.', array('@entity' => $entity_label));
}
return $form;
}
}
/**
* UI for exportable entity-based data types.
*/
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;
}
}
/**
* UI for taxonomy vocabularies.
*
* @see RulesTaxonomyVocabularyWrapper
*/
class RulesDataUITaxonomyVocabulary extends RulesDataUIEntity implements RulesDataInputOptionsListInterface {
/**
* Overrides RulesDataUI::getDefaultMode().
*/
public static function getDefaultMode() {
return 'input';
}
/**
* 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;
}
}
/**
* UI for lists of entity-based data types.
*/
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'])) {
$entity_info = entity_get_info(entity_property_list_extract_type($info['type']));
if (!empty($entity_info['entity keys']['name'])) {
$form[$name]['#element_validate'] = array('rules_ui_element_token_list_validate');
}
$form[$name]['#title'] = t('@entity identifiers', array('@entity' => $entity_info['label']));
$entity_label = strtolower($entity_info['label'][0]) . substr($entity_info['label'], 1);
$form[$name]['#description'] = t('Specify a comma-separated list of identifiers of @entity entities.', array('@entity' => $entity_label));
}
return $form;
}
}

1005
ui/ui.forms.inc

File diff suppressed because it is too large Load Diff

264
ui/ui.plugins.inc

@ -0,0 +1,264 @@
<?php
/**
* @file
* Contains UI for diverse plugins provided by Rules.
*/
/**
* Rule specific UI.
*/
class RulesRuleUI extends RulesActionContainerUI {
protected $rule;
protected $conditions;
/**
* Constructs a RulesRuleUI object.
*
* @param FacesExtendable $object
*/
public function __construct(FacesExtendable $object) {
parent::__construct($object);
$this->rule = $object;
$this->conditions = $this->rule->conditionContainer();
}
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.
if ($label == t('unlabeled') && !$this->element->isRoot() && !empty($options['init'])) {
$parent = $this->element->parentElement();
$label .= ' ' . count($parent->getIterator());
}
// Components have already a label. If used inside a rule-set add a label
// though. It's called 'Name' in the UI though.
if (!$this->element->isRoot()) {
$form['label'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#default_value' => empty($options['init']) ? $label : '',
'#required' => TRUE,
'#weight' => 5,
'#description' => t('A human-readable name shortly describing the rule.'),
);
}
$form += array('conditions' => array('#weight' => -5, '#tree' => TRUE));
$this->conditions->form($form['conditions'], $form_state);
unset($form['conditions']['negate']);
// Add actions form.
$iterator = new RecursiveIteratorIterator($this->rule->actions(), RecursiveIteratorIterator::SELF_FIRST);
parent::form($form, $form_state, $options, $iterator);
// Hide nested elements during creation.
$form['elements']['#access'] = empty($options['init']);
$form['conditions']['elements']['#access'] = empty($options['init']);
$form_state['redirect'] = RulesPluginUI::path($this->element->root()->name, 'edit', $this->element);
if (!empty($options['button'])) {
$form['submit']['#value'] = t('Save changes');
}
}
/**
* Applies the values of the form to the rule configuration.
*/
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'])) {
$this->conditions->extender('RulesConditionContainerUI')->form_extract_values($form['conditions'], $form_state);
}
if (!empty($form_values['label'])) {
$this->element->label = $form_values['label'];
}
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;
}
}
/**
* Reaction rule specific UI.
*/
class RulesReactionRuleUI extends RulesRuleUI {
public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
$form['events'] = array(
'#type' => 'container',
'#weight' => -10,
'#access' => empty($options['init']),
);
$form['events']['table'] = array(
'#theme' => 'table',
'#caption' => 'Events',
'#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_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(
'data' => array(
$event_handler->summary(),
array('data' => $event_operations),
),
);
}
// Add the "add event" row.
$cell['colspan'] = 3;
$cell['data']['#theme'] = 'links__rules';
$cell['data']['#attributes']['class'][] = 'rules-operations-add';
$cell['data']['#attributes']['class'][] = 'action-links';
$cell['data']['#links']['add_event'] = array(
'title' => t('Add event'),
'href' => RulesPluginUI::path($this->rule->name, 'add/event'),
'query' => drupal_get_destination(),
);
$form['events']['table']['#rows'][] = array('data' => array($cell), 'class' => array('rules-elements-add'));
parent::form($form, $form_state, $options);
unset($form['label']);
}
/**
* Adds the configuration settings form (label, tags, description, ..).
*/
public function settingsForm(&$form, &$form_state) {
parent::settingsForm($form, $form_state);
$form['settings']['active'] = array(
'#type' => 'checkbox',
'#title' => t('Active'),
'#default_value' => !isset($this->rule->active) || $this->rule->active,
);
$form['settings']['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
'#default_value' => $this->element->weight,
'#weight' => 5,
'#delta' => 10,
'#description' => t('Order rules that react on the same event. Rules with a higher weight are evaluated after rules with less weight.'),
);
unset($form['settings']['component_provides']);
}
public function settingsFormExtractValues($form, &$form_state) {
$form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state);
parent::settingsFormExtractValues($form, $form_state);
$this->rule->active = $form_values['active'];
$this->rule->weight = $form_values['weight'];
}
}
/**
* Rule set specific UI.
*/
class RulesRuleSetUI extends RulesActionContainerUI {
public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
// Pass an iterator just iterating over the rules, thus no further child
// elements will be displayed.
parent::form($form, $form_state, $options, $this->element->getIterator());
// Only show the add rule link.
$form['elements']['#add']['#links'] = array_intersect_key($form['elements']['#add']['#links'], array('add_rule' => 1));
$form['elements']['#attributes']['class'][] = 'rules-rule-set';
$form['elements']['#caption'] = t('Rules');
}
}
/**
* UI for Rules loops.
*/
class RulesLoopUI extends RulesActionContainerUI {
public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
parent::form($form, $form_state, $options);
$settings = $this->element->settings;
$form['item'] = array(
'#type' => 'fieldset',
'#title' => t('Current list item'),
'#description' => t('The variable used for holding each list item in the loop. This variable will be available inside the loop only.'),
'#tree' => TRUE,
);
$form['item']['label'] = array(
'#type' => 'textfield',
'#title' => t('Variable label'),
'#default_value' => $settings['item:label'],
'#required' => TRUE,
);
$form['item']['var'] = array(
'#type' => 'textfield',
'#title' => t('Variable name'),
'#default_value' => $settings['item:var'],
'#description' => t('The variable name must contain only lowercase letters, numbers, and underscores and must be unique in the current scope.'),
'#element_validate' => array('rules_ui_element_machine_name_validate'),
'#required' => TRUE,
);
}
public function form_extract_values($form, &$form_state) {
parent::form_extract_values($form, $form_state);
$form_values = RulesPluginUI::getFormStateValues($form, $form_state);
$this->element->settings['item:var'] = $form_values['item']['var'];
$this->element->settings['item:label'] = $form_values['item']['label'];
}
public function form_validate($form, &$form_state) {
parent::form_validate($form, $form_state);
$vars = $this->element->availableVariables();
$name = $this->element->settings['item:var'];
if (isset($vars[$name])) {
form_error($form['item']['var'], t('The variable name %name is already taken.', array('%name' => $name)));
}
}
public function buildContent() {
$content = parent::buildContent();
$content['description']['item'] = array(
'#caption' => t('List item'),
'#theme' => 'rules_content_group',
);
$content['description']['item']['var'] = array(
'#theme' => 'rules_variable_view',
'#info' => $this->element->listItemInfo(),
'#name' => $this->element->settings['item:var'],
);
return $content;
}
}

306
ui/ui.theme.inc

@ -0,0 +1,306 @@
<?php
/**
* @file
* Rules theme functions.
*/
/**
* Themes a tree of rule elements in a draggable table.
*
* @ingroup themeable
*/
function theme_rules_elements($variables) {
$form = $variables['element'];
$form['#theme'] = 'table';
$form['#header'] = array(t('Elements'), t('Weight'), t('Operations'));
$form['#attributes']['id'] = 'rules-' . drupal_html_id($form['#caption']) . '-id';
foreach (element_children($form) as $element_id) {
$element = &$form[$element_id];
// Add special classes to be used for tabledrag.js.
$element['parent_id']['#attributes']['class'] = array('rules-parent-id');
$element['element_id']['#attributes']['class'] = array('rules-element-id');
$element['weight']['#attributes']['class'] = array('rules-element-weight');
$row = array();
$row[] = theme('indentation', array('size' => $element['#depth'])) . drupal_render($element['label']);
$row[] = drupal_render($element['weight']) . drupal_render($element['parent_id']) . drupal_render($element['element_id']);
$row[] = array('class' => 'rules-operations', 'data' => $element['operations']);
$row = array('data' => $row) + $element['#attributes'];
$row['class'][] = 'draggable';
if (!$element['#container']) {
$row['class'][] = 'tabledrag-leaf';
}
$form['#rows'][] = $row;
}
if (!empty($form['#rows'])) {
drupal_add_tabledrag($form['#attributes']['id'], 'match', 'parent', 'rules-parent-id', 'rules-parent-id', 'rules-element-id', TRUE, 10);
drupal_add_tabledrag($form['#attributes']['id'], 'order', 'sibling', 'rules-element-weight');
}
else {
$form['#rows'][] = array(array('data' => t('None'), 'colspan' => 3));
}
if (!empty($form['#add'])) {
$row = array();
$row[] = array('data' => $form['#add'], 'colspan' => 3);
$form['#rows'][] = array('data' => $row, 'class' => array('rules-elements-add'));
}
// Add a wrapping div.
return '<div class="rules-elements-table">' . drupal_render($form) . '</div>';
}
/**
* Themes the rules form for editing the used variables.
*
* @see RulesPluginUI::getVariableForm()
*
* @ingroup themeable
*/
function theme_rules_ui_variable_form($variables) {
$elements = $variables['element'];
$table['#theme'] = 'table';
$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) {
$element = &$elements['items'][$key];
// Add special classes to be used for tabledrag.js.
$element['weight']['#attributes']['class'] = array('rules-element-weight');
$row = array();
$row[] = array('data' => $element['type']);
$row[] = array('data' => $element['label']);
$row[] = array('data' => $element['name']);
$row[] = array('data' => $element['usage']);
$row[] = array('data' => $element['weight']);
$row = array('data' => $row) + $element['#attributes'];
$row['class'][] = 'draggable';
$table['#rows'][] = $row;
}
$elements['items']['#printed'] = TRUE;
if (!empty($table['#rows'])) {
drupal_add_tabledrag($table['#attributes']['id'], 'order', 'sibling', 'rules-element-weight');
}
// Theme it like a form item, but with the description above the content.
$attributes['class'][] = 'form-item';
$attributes['class'][] = 'rules-variables-form';
$output = '<div' . drupal_attributes($attributes) . '>' . "\n";
$output .= theme('form_element_label', $variables);
if (!empty($elements['#description'])) {
$output .= ' <div class="description">' . $elements['#description'] . "</div>\n";
}
$output .= ' ' . drupal_render($table) . "\n";
// Add in any further children elements.
foreach (element_children($elements, TRUE) as $key) {
$output .= drupal_render($elements[$key]);
}
$output .= "</div>\n";
return $output;
}
/**
* Themes a view of multiple configuration items.
*
* @ingroup themeable
*/
function theme_rules_content_group($variables) {
$element = $variables['element'];
$output = array();
foreach (element_children($element) as $key) {
$output[] = drupal_render($element[$key]);
}
$output = array_filter($output);
$heading = !empty($element['#caption']) ? "<span class='rules-content-heading'>" . $element['#caption'] . ': </span>' : '';
if (!empty($output)) {
$element['#attributes']['class'][] = 'rules-element-content-group';
return '<div' . drupal_attributes($element['#attributes']) . '>' . $heading . implode(', ', $output) . '</div>';
}
}
/**
* Themes the view of a single parameter configuration.
*
* @ingroup themeable
*/
function theme_rules_parameter_configuration($variables) {
$element = $variables['element'];
$content = drupal_render_children($element);
// Add the full content to the span's title, but don't use drupal_attributes
// for that as this would invoke check_plain() again.
$title = strip_tags($content);
$element['#attributes']['class'][] = 'rules-parameter-configuration';
$attributes = drupal_attributes($element['#attributes']) . " title='$title'";
$label_attributes['class'][] = 'rules-parameter-label';
if (!empty($element['#info']['description'])) {
$label_attributes['title'] = $element['#info']['description'];
}
$label_attributes = drupal_attributes($label_attributes);
$output = "<span $label_attributes>" . check_plain($element['#info']['label']) . ': </span>';
$output .= "<span $attributes>" . truncate_utf8($content, 30, TRUE, TRUE) . "</span>";
return $output;
}
/**
* Themes info about variables.
*
* @ingroup themeable
*/
function theme_rules_variable_view($variables) {
$element = $variables['element'];
$label_attributes['class'][] = 'rules-variable-label';
$label_attributes['title'] = '';
if (!empty($element['#info']['description'])) {
$label_attributes['title'] = $element['#info']['description'] . ' ';
}
$label_attributes['title'] .= t('Data type: !type', array('!type' => $element['#info']['type']));
$label_attributes = drupal_attributes($label_attributes);
$output = check_plain($element['#info']['label']);
$output .= ' (' . check_plain($element['#name']) . ')';
return "<span $label_attributes>" . $output . '</span>';
}
/**
* Themes help for using the data selector.
*
* @ingroup themeable
*/
function theme_rules_data_selector_help($variables) {
$variables_info = $variables['variables'];
$param_info = $variables['parameter'];
$render = array(
'#type' => 'fieldset',
'#title' => t('Data selectors'),
'#pre_render' => array(),
'#attributes' => array(),
);
// Make it manually collapsible as we cannot use #collapsible without the
// FAPI element processor.
$render['#attached']['js'][] = 'misc/collapse.js';
$render['#attributes']['class'][] = 'collapsible';
$render['#attributes']['class'][] = 'collapsed';
$render['table'] = array(
'#theme' => 'table',
'#header' => array(t('Selector'), t('Label'), t('Description')),
);
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']),
);
}
return drupal_render($render);
}
/**
* Themes the rules log debug output.
*
* @ingroup themeable
*/
function theme_rules_log($variables) {
$element = $variables['element'];
drupal_add_css(drupal_get_path('module', 'rules') . '/ui/rules.ui.css');
// Add jquery ui core css and functions, which are needed for the icons.
drupal_add_library('system', 'ui');
drupal_add_js(drupal_get_path('module', 'rules') . '/ui/rules.debug.js');
$output = '<div class="rules-debug-log">';
$output .= '<h3 class="rules-debug-log-head">';
$output .= '<span class="rules-debug-open-main rules-debug-collapsible-link">';
$output .= '<span unselectable="on" class="ui-icon ui-icon-triangle-1-e rules-debug-icon-open">&nbsp;</span>';
$output .= t('Rules evaluation log');
$output .= '</span>';
$output .= '</span>';
$output .= '<span class="rules-debug-open-all rules-debug-collapsible-link">-' . t('Open all') . '-<span>';
$output .= '</h3>';
$output .= '<div>';
$output .= $element['#children'];
$output .= '</div>';
$output .= '</div>';
return $output;
}
/**
* Theme rules debug log elements.
*
* @ingroup themeable
*/
function theme_rules_debug_element($variables) {
$output = '<div class="rules-debug-block">';
$output .= '<div class="rules-debug-log-head rules-debug-open rules-debug-collapsible-link">"';
$output .= '<span unselectable="on" class="ui-icon ui-icon-triangle-1-e rules-debug-icon-open">&nbsp;</span>';
$output .= $variables['head'];
if (isset($variables['link'])) {
$output .= ' ' . $variables['link'];
}
$output .= '</div>';
$output .= $variables['log'];
$output .= '</div>';
return $output;
}
/**
* Themes rules autocomplete forms.
*
* @ingroup themeable
*/
function theme_rules_autocomplete($variables) {
$element = $variables['element'];
drupal_add_js(drupal_get_path('module', 'rules') . '/ui/rules.autocomplete.js');
drupal_add_library('system', 'ui.autocomplete');
drupal_add_library('system', 'ui.button');
$element['#attributes']['type'] = 'text';
element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength'));
_form_set_class($element, array('form-text', 'rules-autocomplete'));
$id = $element['#attributes']['id'];
$id_button = drupal_html_id($id . '-button');
$js_vars['rules_autocomplete'][$id] = array(
'inputId' => $id,
'source' => url($element['#autocomplete_path'], array('absolute' => TRUE)),
);
drupal_add_js($js_vars, 'setting');
$output = '<div class="rules-autocomplete">';
$output .= '<input' . drupal_attributes($element['#attributes']) . ' />';
$output .= '</div>';
return $output;
}
/**
* General theme function for displaying settings related help.
*
* @ingroup themeable
*/
function theme_rules_settings_help($variables) {
$text = $variables['text'];
$heading = $variables['heading'];
return "<p class=\"rules-settings-help\"><strong>$heading:</strong> $text</p>";
}
Loading…
Cancel
Save