first import

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-08 11:40:19 +02:00
commit 1bc61b12ad
8435 changed files with 1582817 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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: http://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:
http://drupal.org/node/298480
* Rules depends on the Entity API module, download and install it from
http://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 http://drupal.org/node/298476
* Check out the developer targeted docs at http://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
http://drupal.org/cron
* If the Views module (http://drupal.org/project/views) is installed, the module
displays the list of scheduled tasks in the UI.
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 unfortuntely 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.

View File

@@ -0,0 +1,311 @@
<?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.
*/
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;
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. May be used to invoke
* protected methods.
*
* @param $name
* The method name.
* @param $arguments
* 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.
*/
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, thus it
* returns TRUE if this oject has been extended by an appropriate
* implementation.
*
* @param $interface
* Optional. A interface to test for. If it's omitted, all interfaces that
* the object can be faced as are returned.
* @return
* 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 $class
* The extender class, which has to implement the FacesExtenderInterface.
* @param $include
* 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 $methods
* An array, where the keys are methods of the given interface and the
* values the callback functions to use.
* @param $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 $methods
* An array of methods of the interface, that should be overriden, where
* the keys are methods to override and the values the callback functions
* to use.
* @param $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 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 $name
* The method name.
* @param $arguments
* 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,766 @@
<?php
/**
* @file Contains plugin info and implementations not needed for rule evaluation.
*/
/**
* Implements a rules action.
*/
class RulesAction extends RulesAbstractPlugin implements RulesActionInterface {
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->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 {
protected $itemName = 'condition';
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));
rules_log('The condition %name evaluated to %bool', array('%name' => $this->elementName, '%bool' => $return ? 'TRUE' : 'FALSE'), RulesLog::INFO, $this);
return $this->negate ? !$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;
protected $itemName = 'rule';
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;
}
}
/**
* Get an iterator over all contained conditions. Note that this iterator also
* implements the ArrayAcces 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'));
}
/**
* Get an iterator over all contained actions. Note that this iterator also
* implements the ArrayAcces interface.
*
* @return RulesRecursiveElementIterator
*/
public function actions() {
return parent::getIterator();
}
/**
* Add a condition. Pass either an instance of the RulesConditionInterface
* or the arguments as needed by rules_condition().
*
* @return Rule
* Returns $this to support chained usage.
*/
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;
}
/**
* Rules may not provided 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);
}
/**
* Overriden 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 {
protected $itemName = 'reaction rule';
protected $events = array();
/**
* Returns the array of events associated with that Rule.
*/
public function &events() {
return $this->events;
}
/**
* Removes an event from the rule configuration.
*
* @param $event
* The name of the event to remove.
* @return RulesReactionRule
*/
public function removeEvent($event) {
if (($id = array_search($event, $this->events)) !== FALSE) {
unset($this->events[$id]);
}
return $this;
}
/**
* @return RulesReactionRule
*/
public function event($event) {
$this->events[] = $event;
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() {
$event_info = rules_fetch_data('event_info');
foreach ($this->events as $event) {
if (!empty($event_info[$event]['access callback']) && !call_user_func($event_info[$event]['access callback'], 'event', $event)) {
return FALSE;
}
}
return parent::access();
}
public function dependencies() {
$modules = array_flip(parent::dependencies());
$event_info = rules_fetch_data('event_info');
foreach ($this->events as $event) {
if (isset($event_info[$event]['module'])) {
$modules[$event_info[$event]['module']] = TRUE;
}
}
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.
$event_info = rules_fetch_data('event_info');
$events = array_intersect($this->events, array_keys($event_info));
foreach ($events as $event) {
$event_info[$event] += array('variables' => array());
if (isset($this->availableVariables)) {
$this->availableVariables = array_intersect_key($this->availableVariables, $event_info[$event]['variables']);
}
else {
$this->availableVariables = $event_info[$event]['variables'];
}
}
$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'));
}
protected function exportChildren($key = 'ON') {
$export[$key] = array_values($this->events);
return $export + parent::exportChildren();
}
protected function importChildren($export, $key = 'ON') {
$this->events = $export[$key];
parent::importChildren($export);
}
}
/**
* A logical AND.
*/
class RulesAnd extends RulesConditionContainer {
protected $itemName = 'and';
public function evaluate(RulesState $state) {
foreach ($this->children as $condition) {
if (!$condition->evaluate($state)) {
rules_log('AND evaluated to FALSE.');
return $this->negate;
}
}
rules_log('AND evaluated to 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 {
protected $itemName = 'or';
public function evaluate(RulesState $state) {
foreach ($this->children as $condition) {
if ($condition->evaluate($state)) {
rules_log('OR evaluated to TRUE.');
return !$this->negate;
}
}
rules_log('OR evaluated to FALSE.');
return $this->negate;
}
public function label() {
return !empty($this->label) ? $this->label : ($this->negate ? t('NOT OR') : t('OR'));
}
/**
* 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 {
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 {
protected $itemName = 'action set';
}
/**
* A set of rules to execute upon defined variables.
*/
class RulesRuleSet extends RulesActionContainer {
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 {
protected $itemName = 'event set';
// Event sets may recurse as we block recursions on rule-level.
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);
}
/**
* Cache event-sets per event 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) {
// Skip not defined events.
if (empty($events[$event])) {
continue;
}
// Create an event set if not yet done.
if (!isset($sets[$event])) {
$event_info = $events[$event] + array(
'variables' => isset($events[$event]['arguments']) ? $events[$event]['arguments'] : array(),
);
$sets[$event] = new RulesEventSet($event_info);
$sets[$event]->name = $event;
}
// If 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]->rule(clone $rule);
}
}
}
// Create cache items for all created sets.
foreach ($sets as $event => $set) {
$set->sortChildren();
$set->optimize();
// Allow modules to alter the cached event set.
drupal_alter('rules_event_set', $event, $set);
rules_set_cache('event_' . $event, $set);
}
// Cache a list of empty sets so we can use it to speed up later calls.
// See rules_get_event_set().
$empty_events = array_keys(array_diff_key($events, $sets));
variable_set('rules_empty_sets', array_flip($empty_events));
}
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;
}
}

View File

@@ -0,0 +1,364 @@
<?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;
}
/**
* Returns 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 suiting processor object, which gets invoked
* on evaluation time.
*
* @param $setting
* The processor settings which are to be prepared.
* @param $param_info
* The info about the parameter to prepare the processor for.
* @param $var_info
* 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 access to the processors is checked.
*
* @param $param_info
* If given, only processors valid for this parameter are returned.
*/
public static function processors($param_info = NULL, $access_check = TRUE, $hook = 'data_processor_info') {
static $items = array();
if (!isset($items[$hook]['all'])) {
$items[$hook]['all'] = rules_fetch_data($hook);
uasort($items[$hook]['all'], array(__CLASS__, '_item_sort'));
}
// 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 $state RulesState
* The rules evaluation state.
* @param $element RulesPlugin
* 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.
*/
public static function access() {
return TRUE;
}
/**
* Defines the processor form element.
*
* @param $settings
* The settings of the processor.
* @param $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 as input evaluators. Input evaluators are not listed
* in hook_rules_data_processor_info(). Instead they use
* hook_rules_evaluator_info() and get attached to input forms.
*/
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;
}
/**
* Overriden to prepare input evaluator processors. The setting is expected
* to be the input value to be evaluated later on and is replaced by the
* suiting processor.
*/
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;
}
/**
* Overriden 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 needed
* type.
*/
public static function evaluators($param_info = NULL, $access_check = TRUE) {
return parent::processors($param_info, $access_check, 'evaluator_info');
}
/**
* 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 evalution, e.g. to determine whether the input evaluator has
* been used. If this evaluator should be skipped just unset $this->setting.
*
* @param $text
* The text to evaluate later on.
* @param $variables
* An array of info about available variables.
* @param $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 $text
* The text to evaluate.
* @param $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
* The rules evaluation state.
*
* @return
* The evaluated text.
*/
abstract public function evaluate($text, $options, RulesState $state);
/**
* Provide some usage help for the evaluator.
*
* @param $variables
* An array of info about available variables.
* @param $param_info
* (optional) An array of information about the handled parameter value.
* For backward compatibility, this parameter is not required.
*
* @return
* A renderable array.
*/
public static function help($variables) {
return array();
}
}

View File

@@ -0,0 +1,770 @@
<?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.
*/
static protected $blocked = array();
/**
* The known variables.
*/
public $variables = array();
/**
* Holds info about the variables.
*/
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;
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 $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 $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 the info about to be saved variables form the given state into the
* existing state. Therefor we can aggregate saves from invoked components.
* Merged in saves are removed from the given state, but not mergable 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 $selector
* The selector string, e.g. "node:author:mail".
* @param $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 usign 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');
}
public function __wakeup() {
$this->save = new ArrayObject();
}
/**
* Restore 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.
*/
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 $ancestors
* Whether sub-type relationships for checking type compatibility should be
* taken into account. Defaults to TRUE.
*
* @return
* 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 matching data variables or properties for the given info and the to
* be configured parameter.
*
* @param $source
* Either an array of info about available variables or a entity metadata
* wrapper.
* @param $param_info
* The information array about the to be configured parameter.
* @param $prefix
* An optional prefix for the data selectors.
* @param $recursions
* The number of recursions used to go down the tree. Defaults to 2.
* @param $suggestions
* Whether possibilities to recurse are suggested as soon as the deepest
* level of recursions is reached. Defaults to TRUE.
*
* @return
* An array of info about matching variables or properties that match, keyed
* with the data selector.
*/
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.
if (isset($assertion['type']) && $var_info[$parts[0]]['type'] == 'entity') {
if (entity_get_info($assertion['type'])) {
$var_info[$parts[0]]['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 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 propertyinfo.
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 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 $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 serializiation.
*/
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;
}
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);
}
/**
* Interface that allows custom wrapper classes to declare that they are savable.
*/
interface RulesDataWrapperSavableInterface {
/**
* Save the currently wrapped data.
*/
public function save();
}

View File

@@ -0,0 +1,664 @@
<?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 dependend modules installed and upgraded, i.e. modules which provide Rules integration that has been used in your rules or rule sets. In addition those modules may need to implement some Rules specific update hooks for the conversion to properly work.') . ' ' .
t('After conversion, the old rules and rule sets will stay in the database until you manually delete them. That way you can make sure the conversion has gone right before you delete the old rules and rule sets.')
);
$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'));
}
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($element[$key])) {
$target->$key = $element[$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 $element
* The element to convert.
* @param $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 'breadcumb_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'];
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @file rules integration for the comment module
*
* @addtogroup rules
* @{
*/
/**
* Implementation of hook_rules_event_info().
*/
function rules_comment_event_info() {
$defaults = array(
'group' => t('comment'),
'module' => 'comment',
'access callback' => 'rules_comment_integration_access',
);
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');
}
}
/**
* @}
*/

View File

@@ -0,0 +1,397 @@
<?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;
}
if (isset($result)) {
// Ensure results are valid integer values if necessary.
$var_info = rules_array_key($element->providesVariables());
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;
}
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;
if ($type != 'integer') {
// Only support the rounding behavior option for integers.
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;
}
$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')));
if (empty($property_info['required'])) {
$element_info['parameter']['param_' . $property]['optional'] = 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: 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);
}
}

View File

@@ -0,0 +1,709 @@
<?php
/**
* @file General data related rules integration
*
* @addtogroup rules
* @{
*/
/**
* 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'),
'label' => t('Value to convert'),
'default mode' => 'selector',
),
// For to-integer conversion only.
'rounding_behavior' => array(
'type' => 'token',
'label' => t('Rounding behavior'),
'description' => t('The rounding behavior the conversion should use.'),
'options list' => 'rules_action_data_convert_rounding_behavior_options',
'restriction' => 'input',
'default value' => 'round',
'optional' => TRUE,
),
),
'provides' => array(
'conversion_result' => array(
'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'),
);
}
/**
* 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 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(),
);
}
/**
* Custom validate callback for entity create, add variable and data create
* action.
*/
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(
'+' => '( + )',
'-' => '( - )',
'*' => '( * )',
'/' => '( / )',
);
// Only show +/- in case a date has been selected.
if (($info = $element->getArgumentInfo('input_1')) && $info['type'] == 'date') {
unset($options['*']);
unset($options['/']);
}
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',
),
),
'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',
),
);
}
/**
* If the bundle is compared, add the metadata assertion so other elements
* can make use of properties specific to the bundle.
*/
function rules_condition_data_is_assertions($element) {
// Assert the bundle of entities, if its compared.
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');
}
}
/**
* @}
*/

View File

@@ -0,0 +1,159 @@
<?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 = entity_metadata_wrapper($element->settings['type']);
if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) {
$element_info['parameter']['value']['type'] = $property->type();
$element_info['parameter']['value']['options list'] = $property->optionsList() ? 'rules_action_entity_query_value_options_list' : FALSE;
}
}
}
$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($entity->is_new);
}
/**
* Condition: Entity has field.
*/
function rules_condition_entity_has_field($wrapper, $field_name, $settings, $state) {
return isset($wrapper->$field_name) || isset($entity->$field_name);
}
/**
* Condition: Entity is of type.
*/
function rules_condition_entity_is_of_type($wrapper, $type) {
return $wrapper->type() == $type;
}

View File

@@ -0,0 +1,401 @@
<?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_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 = entity_metadata_wrapper($element->settings['type']);
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;
}
/**
* 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',
),
);
}
/**
* 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) {
$options = array();
foreach (field_info_fields() as $field_name => $field) {
$options[$field_name] = $field_name;
}
return $options;
}
/**
* 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));
}
}
/**
* @}
*/

View File

@@ -0,0 +1,168 @@
<?php
/**
* @file Invokes events on behalf core modules. Usually this should be
* directly in the module providing rules integration instead.
*
* @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 http://drupal.org/node/1211946
* for details.
*/
/**
* Implements hook_entity_view().
*/
function rules_entity_view($entity, $type, $view_mode, $langcode) {
$entity_types = array(
'comment' => TRUE,
'node' => TRUE,
'user' => TRUE,
);
if (isset($entity_types[$type])) {
rules_invoke_event($type . '_view', $entity, $view_mode);
}
}
/**
* Implements hook_entity_presave().
*/
function rules_entity_presave($entity, $type) {
$entity_types = array(
'comment' => TRUE,
'node' => TRUE,
'taxonomy_term' => TRUE,
'taxonomy_vocabulary' => TRUE,
'user' => TRUE,
);
if (isset($entity_types[$type])) {
rules_invoke_event($type . '_presave', $entity);
}
}
/**
* Implements hook_entity_update().
*/
function rules_entity_update($entity, $type) {
$entity_types = array(
'comment' => TRUE,
'node' => TRUE,
'taxonomy_term' => TRUE,
'taxonomy_vocabulary' => TRUE,
'user' => TRUE,
);
if (isset($entity_types[$type])) {
rules_invoke_event($type . '_update', $entity);
}
}
/**
* Implements hook_entity_insert().
*/
function rules_entity_insert($entity, $type) {
$entity_types = array(
'comment' => TRUE,
'node' => TRUE,
'taxonomy_term' => TRUE,
'taxonomy_vocabulary' => TRUE,
'user' => TRUE,
);
if (isset($entity_types[$type])) {
rules_invoke_event($type . '_insert', $entity);
}
}
/**
* Implements hook_entity_delete().
*/
function rules_entity_delete($entity, $type) {
$entity_types = array(
'comment' => TRUE,
'node' => TRUE,
'taxonomy_term' => TRUE,
'taxonomy_vocabulary' => TRUE,
'user' => TRUE,
);
if (isset($entity_types[$type])) {
rules_invoke_event($type . '_delete', $entity);
}
}
/**
* 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;
}
/**
* @}
*/

View File

@@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains rules integration for the node module needed during evaluation.
*
* @addtogroup rules
* @{
*/
/**
* Condition: Check for selected content types
*/
function rules_condition_node_is_of_type($node, $types) {
return in_array($node->type, $types);
}
/**
* Condition: Check if the node is published
*/
function rules_condition_node_is_published($node) {
return $node->status == 1;
}
/**
* Condition: Check if the node is sticky
*/
function rules_condition_node_is_sticky($node) {
return $node->sticky == 1;
}
/**
* Condition: Check if the node is promoted to the frontpage
*/
function rules_condition_node_is_promoted($node) {
return $node->promote == 1;
}
/**
* @}
*/

View File

@@ -0,0 +1,173 @@
<?php
/**
* @file rules integration for the node module
*
* @addtogroup rules
* @{
*/
/**
* 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'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('created content')),
'access callback' => 'rules_node_integration_access',
),
'node_update' => array(
'label' => t('After updating existing content'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('updated content'), TRUE),
'access callback' => 'rules_node_integration_access',
),
'node_presave' => array(
'label' => t('Before saving content'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('saved content'), TRUE),
'access callback' => 'rules_node_integration_access',
),
'node_view' => array(
'label' => t('Content is viewed'),
'group' => t('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',
),
'node_delete' => array(
'label' => t('After deleting content'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('deleted content')),
'access callback' => 'rules_node_integration_access',
),
);
// 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_condition_info() on behalf of the node module.
*/
function rules_node_condition_info() {
$defaults = array(
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content')),
),
'group' => t('Node'),
'access callback' => 'rules_node_integration_access',
);
$items['node_is_of_type'] = $defaults + array(
'label' => t('Content is of type'),
'help' => t('Evaluates to TRUE if the given content is of one of the selected content types.'),
'base' => 'rules_condition_node_is_of_type',
);
$items['node_is_of_type']['parameter']['type'] = array(
'type' => 'list<text>',
'label' => t('Content types'),
'options list' => 'node_type_get_names',
'description' => t('The content type(s) to check for.'),
'restriction' => 'input',
);
$items['node_is_published'] = $defaults + array(
'label' => t('Content is published'),
'base' => 'rules_condition_node_is_published',
);
$items['node_is_sticky'] = $defaults + array(
'label' => t('Content is sticky'),
'base' => 'rules_condition_node_is_sticky',
);
$items['node_is_promoted'] = $defaults + array(
'label' => t('Content is promoted to frontpage'),
'base' => 'rules_condition_node_is_promoted',
);
return $items;
}
/**
* Provides the content type of a node as asserted metadata.
*/
function rules_condition_node_is_of_type_assertions($element) {
return array('node' => array('bundle' => $element->settings['type']));
}
/**
* Implements hook_rules_action_info() on behalf of the node module.
*/
function rules_node_action_info() {
$defaults = array(
'parameter' => array(
'node' => array('type' => 'node', 'label' => t('Content'), 'save' => TRUE),
),
'group' => t('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');
}
/**
* @}
*/

View File

@@ -0,0 +1,148 @@
<?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 by replacing non ASCII characters with the replacment character.
*
* Path cleaning may be adapted 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);
}
/**
* @}
*/

View File

@@ -0,0 +1,171 @@
<?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',
),
);
}
/**
* @}
*/

View File

@@ -0,0 +1,151 @@
<?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 {
public static function access() {
return user_access('use PHP for settings');
}
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;
}
}
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 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));
}
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 {
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;
}
public static function access() {
return user_access('use PHP for settings');
}
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));
}
/**
* Evalutes the given PHP code, with the given variables defined.
*
* @param $code
* The PHP code to run, with <?php ?>
* @param $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;
}
/**
* Evalutes the given PHP code, with the given variables defined. This is like
* rules_php_eval() but does return the returned data from the PHP code.
*
* @param $code
* The PHP code to run, without <?php ?>
* @param $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);
}
/**
* @}
*/

View File

@@ -0,0 +1,158 @@
<?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;
}
/**
* @}
*/

View File

@@ -0,0 +1,242 @@
<?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 mergable 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 right, consider "now".
*/
class RulesDateInputEvaluator extends RulesDataInputEvaluator {
const DATE_REGEX_LOOSE = '/^(\d{4})-?(\d{2})-?(\d{2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})?)?$/';
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;
}
}
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 to make
* sure URIs are absolute and path aliases get applied.
*/
class RulesURIInputEvaluator extends RulesDataInputEvaluator {
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;
}
}
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 {
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;
}
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 {
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;
}
public function process($value, $info, RulesState $state, RulesPlugin $element) {
$value = isset($this->processor) ? $this->processor->process($value, $info, $state, $element) : $value;
return $value + $this->setting['value'];
}
}
/**
* A custom wrapper class for vocabularies that is capable of loading vocabularies by machine name.
*/
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);
}
}
/**
* Overriden to permit machine names as values.
*/
public function validate($value) {
if (isset($value) && is_string($value)) {
return TRUE;
}
return parent::validate($value);
}
}

View File

@@ -0,0 +1,314 @@
<?php
/**
* @file Rules core integration providing data types and conditions and
* actions to invoke configured components.
*
* @addtogroup rules
* @{
*/
/**
* 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,
),
);
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 (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 RulesPluginFeaturesIntegrationInterace.
*/
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");
}
/**
* @}
*/

View File

@@ -0,0 +1,267 @@
<?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: 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 the current
// page execution unpredictly 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 {
$rids = implode(',', $roles);
// Avoid sending emails to members of two or more target role groups.
$result = db_query('SELECT DISTINCT u.mail FROM {users} u INNER JOIN {users_roles} r ON u.uid = r.uid WHERE r.rid IN ('. $rids .')');
}
// 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']) {
break;
}
}
if ($message['result']) {
$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().
*
* Set's 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'];
}
/**
* A class implementing a rules input evaluator processing tokens.
*/
class RulesTokenEvaluator extends RulesDataInputEvaluator {
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;
}
/**
* 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;
}
/**
* @}
*/

View File

@@ -0,0 +1,288 @@
<?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 an 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' => ('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',
),
'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',
),
);
}
/**
* 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,
),
);
}
/**
* @}
*/

View File

@@ -0,0 +1,100 @@
<?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',
);
$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');
}
}
/**
* @}
*/

View File

@@ -0,0 +1,102 @@
<?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;
}
/**
* @}
*/

View File

@@ -0,0 +1,236 @@
<?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');
}
/**
* Implementation of 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',
),
),
'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',
);
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');
}
/**
* @}
*/

View File

@@ -0,0 +1,972 @@
<?php
/**
* @file
* This file contains no working PHP code; it exists to provide additional
* documentation for doxygen as well as to document hooks in the standard
* Drupal manner.
*/
/**
* @defgroup rules Rules module integrations.
*
* Module integrations with the rules module.
*
* The Rules developer documentation describes how modules can integrate with
* rules: http://drupal.org/node/298486.
*/
/**
* @defgroup rules_hooks Rules' hooks
* @{
* Hooks that can be implemented by other modules in order to extend rules.
*/
/**
* Define rules compatible actions.
*
* This hook is required in order to add a new rules action. It should be
* placed into the file MODULENAME.rules.inc, which gets automatically included
* when the hook is invoked.
*
* @return
* An array of information about the module's provided rules actions.
* The array contains a sub-array for each action, with the action name as
* the key. Actions names may only contain lowercase alpha-numeric characters
* and underscores and should be prefixed with the providing module name.
* Possible attributes for each sub-array are:
* - label: The label of the action. Start capitalized. Required.
* - group: A group for this element, used for grouping the actions in the
* interface. Should start with a capital letter and be translated.
* Required.
* - parameter: (optional) An array describing all parameter of the action
* with the parameter's name as key. Each parameter has to be
* described by a sub-array with possible attributes as described
* afterwards, whereas the name of a parameter needs to be a lowercase,
* valid PHP variable name.
* - provides: (optional) An array describing the variables the action
* provides to the evaluation state with the variable name as key. Each
* variable has to be described by a sub-array with possible attributes as
* described afterwards, whereas the name of a parameter needs to be a
* lowercase, valid PHP variable name.
* - 'named parameter': (optional) If set to TRUE, the arguments will be
* passed as a single array with the parameter names as keys. This emulates
* named parameters in PHP and is in particular useful if the number of
* parameters can vary. Defaults to FALSE.
* - base: (optional) The base for action implementation callbacks to use
* instead of the action's name. Defaults to the action name.
* - callbacks: (optional) An array which allows to set specific function
* callbacks for the action. The default for each callback is the actions
* base appended by '_' and the callback name.
* - 'access callback': (optional) A callback which has to return whether the
* currently logged in user is allowed to configure this action. See
* rules_node_integration_access() for an example callback.
* Each 'parameter' array may contain the following properties:
* - label: The label of the parameter. Start capitalized. Required.
* - type: The rules data type of the parameter, which is to be passed to the
* action. All types declared in hook_rules_data_info() may be specified, as
* well as an array of possible types. Also lists and lists of a given type
* can be specified by using the notating list<integer> as introduced by
* the entity metadata module, see hook_entity_property_info(). The special
* keyword '*' can be used when all types should be allowed. Required.
* - bundles: (optional) An array of bundle names. When the specified type is
* set to a single entity type, this may be used to restrict the allowed
* bundles.
* - description: (optional) If necessary, a further description of the
* parameter.
* - options list: (optional) A callback that returns an array of possible
* values for this parameter. The callback has to return an array as used
* by hook_options_list(). For an example implementation see
* rules_data_action_type_options().
* - save: (optional) If this is set to TRUE, the parameter will be saved by
* rules when the rules evaluation ends. This is only supported for savable
* data types. If the action returns FALSE, saving is skipped.
* - optional: (optional) May be set to TRUE, when the parameter isn't
* required.
* - 'default value': (optional) The value to pass to the action, in case the
* parameter is optional and there is no specified value.
* - 'allow null': (optional) Usually Rules will not pass any NULL values as
* argument, but abort the evaluation if a NULL value is present. If set to
* TRUE, Rules will not abort and pass the NULL value through. Defaults to
* FALSE.
* - restriction: (optional) Restrict how the argument for this parameter may
* be provided. Supported values are 'selector' and 'input'.
* - default mode: (optional) Customize the default mode for providing the
* argument value for a parameter. Supported values are 'selector' and
* 'input'. The default depends on the required data type.
* - sanitize: (optional) Allows parameters of type 'text' to demand an
* already sanitized argument. If enabled, any user specified value won't be
* sanitized itself, but replacements applied by input evaluators are as
* well as values retrieved from selected data sources.
* - translatable: (optional) If set to TRUE, the provided argument value
* of the parameter is translatable via i18n String translation. This is
* applicable for textual parameters only, i.e. parameters of type 'text',
* 'token', 'list<text>' and 'list<token>'. Defaults to FALSE.
* - ui class: (optional) Allows overriding the UI class, which is used to
* generate the configuration UI of a parameter. Defaults to the UI class of
* the specified data type.
* - cleaning callback: (optional) A callback that input evaluators may use
* to clean inserted replacements; e.g. this is used by the token evaluator.
* - wrapped: (optional) Set this to TRUE in case the data should be passed
* wrapped. This only applies to wrapped data types, e.g. entities.
* Each 'provides' array may contain the following properties:
* - label: The label of the variable. Start capitalized. Required.
* - type: The rules data type of the variable. All types declared in
* hook_rules_data_info() may be specified. Types may be parametrized e.g.
* the types node<page> or list<integer> are valid.
* - save: (optional) If this is set to TRUE, the provided variable is saved
* by rules when the rules evaluation ends. Only possible for savable data
* types. Defaults to FALSE.
*
* The module has to provide an implementation for each action, being a
* function named as specified in the 'base' key or for the execution callback.
* All other possible callbacks are optional.
* Supported action callbacks by rules are defined and documented in the
* RulesPluginImplInterface. However any module may extend the action plugin
* based upon a defined interface using hook_rules_plugin_info(). All methods
* defined in those interfaces can be overridden by the action implementation.
* The callback implementations for those interfaces may reside in any file
* specified in hook_rules_file_info().
*
* @see hook_rules_file_info()
* @see rules_action_execution_callback()
* @see hook_rules_plugin_info()
* @see RulesPluginImplInterface
*/
function hook_rules_action_info() {
return array(
'mail_user' => array(
'label' => t('Send a mail to a user'),
'parameter' => array(
'user' => array('type' => 'user', 'label' => t('Recipient')),
),
'group' => t('System'),
'base' => 'rules_action_mail_user',
'callbacks' => array(
'validate' => 'rules_action_custom_validation',
'help' => 'rules_mail_help',
),
),
);
}
/**
* Specify files containing rules integration code.
*
* All files specified in that hook will be included when rules looks for
* existing callbacks for any plugin. Rules remembers which callback is found in
* which file and automatically includes the right file before it is executing
* a plugin method callback. The file yourmodule.rules.inc is added by default
* and need not be specified here.
* This allows you to add new include files only containing functions serving as
* plugin method callbacks in any file without having to care about file
* inclusion.
*
* @return
* An array of file names without the file ending which defaults to '.inc'.
*/
function hook_rules_file_info() {
return array('yourmodule.rules-eval');
}
/**
* The execution callback for an action.
*
* It should be placed in any file included by your module or in a file
* specified using hook_rules_file_info().
*
* @param
* The callback gets arguments passed as described as parameter in
* hook_rules_action_info() as well as an array containing the action's
* configuration settings.
* @return
* The action may return an array containg parameter or provided variables
* with their names as key. This is used update the value of a parameter or to
* provdide the value for a provided variable.
* Apart from that any parameters which have the key 'save' set to TRUE will
* be remembered to be saved by rules unless the action returns FALSE.
* Conditions have to return a boolean value in any case.
*
* @see hook_rules_action_info()
* @see hook_rules_file_info()
*/
function rules_action_execution_callback($node, $title, $settings) {
$node->title = $title;
return array('node' => $node);
}
/**
* Define rules conditions.
*
* This hook is required in order to add a new rules condition. It should be
* placed into the file MODULENAME.rules.inc, which gets automatically included
* when the hook is invoked.
*
* Adding conditions works exactly the same way as adding actions, with the
* exception that conditions can't provide variables and cannot save parameters.
* Thus the 'provides' attribute is not supported. Furthermore the condition
* implementation callback has to return a boolean value.
*
* @see hook_rules_action_info()
*/
function hook_rules_condition_info() {
return array(
'rules_condition_text_compare' => array(
'label' => t('Textual comparison'),
'parameter' => array(
'text1' => array('label' => t('Text 1'), 'type' => 'text'),
'text2' => array('label' => t('Text 2'), 'type' => 'text'),
),
'group' => t('Rules'),
),
);
}
/**
* Define rules events.
*
* This hook is required in order to add a new rules event. It should be
* placed into the file MODULENAME.rules.inc, which gets automatically included
* when the hook is invoked.
* The module has to invoke the event when it occurs using rules_invoke_event().
* This function call has to happen outside of MODULENAME.rules.inc,
* usually it's invoked directly from the providing module but wrapped by a
* module_exists('rules') check.
*
* @return
* An array of information about the module's provided rules events. The array
* contains a sub-array for each event, with the event name as the key. The
* name may only contain lower case alpha-numeric characters and underscores
* and should be prefixed with the providing module name. Possible attributes
* for each sub-array are:
* - label: The label of the event. Start capitalized. Required.
* - group: A group for this element, used for grouping the events in the
* interface. Should start with a capital letter and be translated.
* Required.
* - 'access callback': An callback, which has to return whether the
* currently logged in user is allowed to configure rules for this event.
* Access should be only granted, if the user at least may access all the
* variables provided by the event. Optional.
* - help: A help text for rules reaction on this event.
* - variables: An array describing all variables that are available for
* elements reaction on this event. Optional. Each variable has to be
* described by a sub-array with the possible attributes:
* - label: The label of the variable. Start capitalized. Required.
* - type: The rules data type of the variable. All types declared in
* hook_rules_data_info() or supported by hook_entity_property_info() may
* be specified.
* - bundle: (optional) If the type is an entity type, the bundle of the
* entity.
* - description: (optional) A description for the variable.
* - 'options list': (optional) A callback that returns an array of possible
* values for this variable as specified for entity properties at
* hook_entity_property_info().
* - 'skip save': If the variable is saved after the event has occurred
* anyway, set this to TRUE. So rules won't save the variable a second
* time. Optional, defaults to FALSE.
* - handler: A handler to load the actual variable value. This is useful
* for lazy loading variables. The handler gets all so far available
* variables passed in the order as defined. Optional. Also see
* http://drupal.org/node/884554.
* Note that for lazy-loading entities just the entity id may be passed
* as variable value, so a handler is not necessary in that case.
*
* @see rules_invoke_event()
*/
function hook_rules_event_info() {
$items = array(
'node_insert' => array(
'label' => t('After saving new content'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('created content')),
),
'node_update' => array(
'label' => t('After updating existing content'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('updated content'), TRUE),
),
'node_presave' => array(
'label' => t('Content is going to be saved'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('saved content'), TRUE),
),
'node_view' => array(
'label' => t('Content is going to be viewed'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('viewed content')) + array(
'view_mode' => array('type' => 'text', 'label' => t('view mode')),
),
),
'node_delete' => array(
'label' => t('After deleting content'),
'group' => t('Node'),
'variables' => rules_events_node_variables(t('deleted content')),
),
);
// Specify that on presave the node is saved anyway.
$items['node_presave']['variables']['node']['skip save'] = TRUE;
return $items;
}
/**
* Define rules data types.
*
* This hook is required in order to add a new rules data type. It should be
* placed into the file MODULENAME.rules.inc, which gets automatically included
* when the hook is invoked.
* Rules builds upon the entity metadata module, thus to improve the support of
* your data in rules, make it an entity if possible and provide metadata about
* its properties and CRUD functions by integrating with the entity metadata
* module.
* For a list of data types defined by rules see rules_rules_core_data_info().
*
*
* @return
* An array of information about the module's provided data types. The array
* contains a sub-array for each data type, with the data type name as the
* key. The name may only contain lower case alpha-numeric characters and
* underscores and should be prefixed with the providing module name. Possible
* attributes for each sub-array are:
* - label: The label of the data type. Start uncapitalized. Required.
* - parent: (optional) A parent type may be set to specify a sub-type
* relationship, which will be only used for checking compatible types. E.g.
* the 'entity' data type is parent of the 'node' data type, thus a node may
* be also used for any action needing an 'entity' parameter. Can be set to
* any known rules data type.
* - ui class: (optional) Specify a class that is used to generate the
* configuration UI to configure parameters of this type. The given class
* must extend RulesDataUI and may implement the
* RulesDataDirectInputFormInterface in order to allow the direct data input
* configuration mode. Defaults to RulesDataUI.
* - wrap: (optional) If set to TRUE, the data is wrapped internally using
* wrappers provided by the entity API module. This is required for entities
* and data structures to support selecting a property via the data selector
* and for intelligent saving.
* - is wrapped: (optional) In case the data wrapper is already wrapped when
* passed to Rules and Rules should not unwrap it when passing the data as
* argument, e.g. to an action, set this to TRUE. The default FALSE is fine
* in most cases.
* - wrapper class: (optional) Allows the specification of a custom wrapper
* class, which has to inherit from 'EntityMetadataWrapper'. If given Rules
* makes use of the class for wrapping the data of the given type. However
* note that if data is already wrapped when it is passed to Rules, the
* existing wrappers will be kept.
* For modules implementing identifiable data types being non-entites the
* class RulesIdentifiableDataWrapper is provided, which can be used as base
* for a custom wrapper class. See RulesIdentifiableDataWrapper for details.
* - property info: (optional) May be used for non-entity data structures to
* provide info about the data properties, such that data selectors via an
* entity metadata wrapper are supported. Specify an array as expected by
* the $info parameter of entity_metadata_wrapper().
* - creation callback: (optional) If 'property info' is given, an optional
* callback that makes use of the property info to create a new instance of
* this data type. Entities should use hook_entity_info() to specify the
* 'creation callback' instead, as utilized by the entity API module. See
* rules_action_data_create_array() for an example callback.
* - property defaults: (optional) May be used for non-entity data structures
* to to provide property info defaults for the data properties. Specify an
* array as expected by entity_metadata_wrapper().
* - group: (optional) A group for this element, used for grouping the data
* types in the interface. Should start with a capital letter and be
* translated.
* - token type: (optional) The type name as used by the token module.
* Defaults to the type name as used by rules. Use FALSE to let token ignore
* this type.
* - cleaning callback: (optional) A callback that input evaluators may use
* to clean inserted replacements; e.g. this is used by the token evaluator.
*
* @see entity_metadata_wrapper()
* @see hook_rules_data_info_alter()
* @see rules_rules_core_data_info()
*/
function hook_rules_data_info() {
return array(
'node' => array(
'label' => t('content'),
'parent' => 'entity',
'group' => t('Node'),
),
// Formatted text as used by in hook_entity_property_info() for text fields.
'text_formatted' => array(
'label' => t('formatted text'),
'ui class' => 'RulesDataUITextFormatted',
'wrap' => TRUE,
'property info' => entity_property_text_formatted_info(),
),
);
}
/**
* Defines rules plugins.
*
* A rules configuration may consist of elements being instances of any rules
* plugin. This hook can be used to define new or to extend rules plugins.
*
* @return
* An array of information about the module's provided rules plugins. The
* array contains a sub-array for each plugin, with the plugin name as the
* key. The name may only contain lower case alpha-numeric characters,
* underscores and spaces and should be prefixed with the providing module
* name. Possible attributes for
* each sub-array are:
* - label: A label for the plugin. Start capitalized. Required only for
* components (see below).
* - class: The implementation class. Has to extend the RulesPlugin class.
* - embeddable: A container class in which elements of those plugin may be
* embedded via the UI or FALSE to disallow embedding it via the UI. This
* has no implications on the API level though. Common classes that are
* used here are RulesConditionContainer and RulesActionContainer.
* - component: If set to TRUE, the rules admin UI will list elements of those
* plugin in the components UI and allows the creation of new components
* based upon this plugin. Optional.
* - extenders: This allows one to specify faces extenders, which may be used
* to dynamically implement interfaces. Optional. All extenders specified
* here are setup automatically by rules once the object is created. To
* specify set this to an array, where the keys are the implemented
* interfaces pointing to another array with the keys:
* - class: The class of the extender, implementing the FacesExtender
* and the specified interface. Either 'class' or 'methods' has to exist.
* - methods: An array of callbacks that implement the methods of the
* interface where the method names are the keys and the callback names
* the values. There has to be a callback for each defined method.
* - file: An optional array describing the file to include when a method
* of the interface is invoked. 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.
* - overrides: An optional array, which may be used to specify callbacks to
* override specific methods. For that the following keys are supported:
* - methods: As in the extenders array, but you may specify as many methods
* here as you like.
* - file: Optionally an array specifying a file to include for a method.
* For each method appearing in methods a file may be specified by using
* the method name as key and another array as value, which describes the
* file to include - looking like the file array supported by 'extenders'.
* - import keys: (optional) Embeddable plugins may specify an array of import
* keys, which the plugin make use for exporting. Defaults to the upper
* case plugin name, thus the key 'OR' in an export triggers the creation
* of the 'or' plugin. Note that only uppercase values are allowed, as
* lower case values are treated as action or condition exports.
*
* @see class RulesPlugin
* @see hook_rules_plugin_info_alter()
*/
function hook_rules_plugin_info() {
return array(
'or' => array(
'label' => t('Condition set (OR)'),
'class' => 'RulesOr',
'embeddable' => 'RulesConditionContainer',
'component' => TRUE,
'extenders' => array(
'RulesPluginUIInterface' => array(
'class' => 'RulesConditionContainerUI',
),
),
),
'rule' => array(
'class' => 'Rule',
'embeddable' => 'RulesRuleSet',
'extenders' => array(
'RulesPluginUIInterface' => array(
'class' => 'RulesRuleUI',
),
),
'import keys' => array('DO', 'IF'),
),
);
}
/**
* Declare provided rules input evaluators.
*
* The hook implementation should be placed into the file MODULENAME.rules.inc,
* which gets automatically included when the hook is invoked.
* For implementing an input evaluator a class has to be provided which
* extends the abstract RulesDataInputEvaluator class. Therefore the abstract
* methods prepare() and evaluate() have to be implemented, as well as access()
* and help() could be overridden in order to control access permissions or to
* provide some usage help.
*
* @return
* An array of information about the module's provided input evaluators. The
* array contains a sub-array for each evaluator, with the evaluator name as
* the key. The name may only contain lower case alpha-numeric characters and
* underscores and should be prefixed with the providing module name. Possible
* attributes for each sub-array are:
* - class: The implementation class, which has to extend the
* RulesDataInputEvaluator class. Required.
* - weight: A weight for controlling the evaluation order of multiple
* evaluators. Required.
* - type: Optionally, the data types for which the input evaluator should be
* used. Defaults to 'text'. Multiple data types may be specified using an
* array.
*
* @see class RulesDataInputEvaluator
* @see hook_rules_evaluator_info_alter()
*/
function hook_rules_evaluator_info() {
return array(
'token' => array(
'class' => 'RulesTokenEvaluator',
'type' => array('text', 'uri'),
'weight' => 0,
),
);
}
/**
* Declare provided rules data processors.
*
* The hook implementation should be placed into the file MODULENAME.rules.inc,
* which gets automatically included when the hook is invoked.
* For implementing a data processors a class has to be provided which
* extends the abstract RulesDataProcessor class. Therefore the abstract
* method process() has to be implemented, but also the methods form() and
* access() could be overridden in order to provide a configuration form or
* to control access permissions.
*
* @return
* An array of information about the module's provided data processors. The
* array contains a sub-array for each processor, with the processor name as
* the key. The name may only contain lower case alpha-numeric characters and
* underscores and should be prefixed with the providing module name, whereas
* 'select' is reserved as well.
* Possible attributes for each sub-array are:
* - class: The implementation class, which has to extend the
* RulesDataProcessor class. Required.
* - weight: A weight for controlling the processing order of multiple data
* processors. Required.
* - type: Optionally, the data types for which the data processor should be
* used. Defaults to 'text'. Multiple data types may be specified using an
* array.
*
* @see class RulesDataProcessor
* @see hook_rules_data_processor_info_alter()
*/
function hook_rules_data_processor_info() {
return array(
'date_offset' => array(
'class' => 'RulesDateOffsetProcessor',
'type' => 'date',
'weight' => -2,
),
);
}
/**
* Alter rules compatible actions.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $actions
* The items of all modules as returned from hook_rules_action_info().
*
* @see hook_rules_action_info().
*/
function hook_rules_action_info_alter(&$actions) {
// The rules action is more powerful, so hide the core action
unset($actions['rules_core_node_assign_owner_action']);
// We prefer handling saving by rules - not by the user.
unset($actions['rules_core_node_save_action']);
}
/**
* Alter rules conditions.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $conditions
* The items of all modules as returned from hook_rules_condition_info().
*
* @see hook_rules_condition_info()
*/
function hook_rules_condition_info_alter(&$conditions) {
// Change conditions.
}
/**
* Alter rules events.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $events
* The items of all modules as returned from hook_rules_event_info().
*
* @see hook_rules_event_info().
*/
function hook_rules_event_info_alter(&$events) {
// Change events.
}
/**
* Alter rules data types.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $data_info
* The items of all modules as returned from hook_rules_data_info().
*
* @see hook_rules_data_info()
*/
function hook_rules_data_info_alter(&$data_info) {
// Change data types.
}
/**
* Alter rules plugin info.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $plugin_info
* The items of all modules as returned from hook_rules_plugin_info().
*
* @see hook_rules_plugin_info()
*/
function hook_rules_plugin_info_alter(&$plugin_info) {
// Change plugin info.
}
/**
* Alter rules input evaluator info.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $evaluator_info
* The items of all modules as returned from hook_rules_evaluator_info().
*
* @see hook_rules_evaluator_info()
*/
function hook_rules_evaluator_info_alter(&$evaluator_info) {
// Change evaluator info.
}
/**
* Alter rules data_processor info.
*
* The implementation should be placed into the file MODULENAME.rules.inc, which
* gets automatically included when the hook is invoked.
*
* @param $processor_info
* The items of all modules as returned from hook_rules_data_processor_info().
*
* @see hook_rules_data_processor_info()
*/
function hook_rules_data_processor_info_alter(&$processor_info) {
// Change processor info.
}
/**
* Act on rules configuration being loaded from the database.
*
* This hook is invoked during rules configuration loading, which is handled
* by entity_load(), via classes RulesEntityController and EntityCRUDController.
*
* @param $configs
* An array of rules configurations being loaded, keyed by id.
*/
function hook_rules_config_load($configs) {
$result = db_query('SELECT id, foo FROM {mytable} WHERE id IN(:ids)', array(':ids' => array_keys($configs)));
foreach ($result as $record) {
$configs[$record->id]->foo = $record->foo;
}
}
/**
* Respond to creation of a new rules configuration.
*
* This hook is invoked after the rules configuration is inserted into the
* the database.
*
* @param RulesPlugin $config
* The rules configuration that is being created.
*/
function hook_rules_config_insert($config) {
db_insert('mytable')
->fields(array(
'nid' => $config->id,
'plugin' => $config->plugin,
))
->execute();
}
/**
* Act on a rules configuration being inserted or updated.
*
* This hook is invoked before the rules configuration is saved to the
* database.
*
* @param RulesPlugin $config
* The rules configuration that is being inserted or updated.
*/
function hook_rules_config_presave($config) {
if ($config->id && $config->module == 'yours') {
// Add custom condition.
$config->conditon(/* Your condition */);
}
}
/**
* Respond to updates to a rules configuration.
*
* This hook is invoked after the configuration has been updated in the
* database.
*
* @param RulesPlugin $config
* The rules configuration that is being updated.
*/
function hook_rules_config_update($config) {
db_update('mytable')
->fields(array('plugin' => $config->plugin))
->condition('id', $config->id)
->execute();
}
/**
* Respond to rules configuration deletion.
*
* This hook is invoked after the configuration has been removed from the
* database.
*
* @param RulesPlugin $config
* The rules configuration that is being deleted.
*/
function hook_rules_config_delete($config) {
db_delete('mytable')
->condition('id', $config->id)
->execute();
}
/**
* Respond to rules configuration execution.
*
* This hook is invoked right before the rules configuration is executed.
*
* @param RulesPlugin $config
* The rules configuration that is being executed.
*/
function hook_rules_config_execute($config) {
}
/**
* Define default rules configurations.
*
* This hook is invoked when rules configurations are loaded. The implementation
* should be placed into the file MODULENAME.rules_defaults.inc, which gets
* automatically included when the hook is invoked.
*
* @return
* An array of rules configurations with the configuration names as keys.
*
* @see hook_default_rules_configuration_alter()
* @see hook_rules_config_defaults_rebuild()
*/
function hook_default_rules_configuration() {
$rule = rules_reaction_rule();
$rule->label = 'example default rule';
$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;
return $configs;
}
/**
* Alter default rules configurations.
*
* The implementation should be placed into the file
* MODULENAME.rules_defaults.inc, which gets automatically included when the
* hook is invoked.
*
* @param $configs
* The default configurations of all modules as returned from
* hook_default_rules_configuration().
*
* @see hook_default_rules_configuration()
*/
function hook_default_rules_configuration_alter(&$configs) {
// Add custom condition.
$configs['foo']->condition('bar');
}
/**
* Act after rebuilding default configurations.
*
* This hook is invoked by the entity module after default rules configurations
* have been rebuilt; i.e. defaults have been saved to the database.
*
* @param $rules_configs
* The array of default rules configurations which have been inserted or
* updated, keyed by name.
* @param $originals
* An array of original rules configurations keyed by name; i.e. the rules
* configurations before the current defaults have been applied. For inserted
* rules configurations no original is available.
*
* @see hook_default_rules_configuration()
* @see entity_defaults_rebuild()
*/
function hook_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]);
}
}
}
/**
* Alter rules components before execution.
*
* This hooks allows altering rules components before they are cached for later
* re-use. Use this hook only for altering the component in order to prepare
* re-use through rules_invoke_component() or the provided condition/action.
* Note that this hook is only invoked for any components cached for execution,
* but not for components that are programmatically created and executed on the
* fly (without saving them).
*
* @param $plugin
* The name of the component plugin.
* @param $component
* The component that is to be cached.
*
* @see rules_invoke_component()
*/
function hook_rules_component_alter($plugin, RulesPlugin $component) {
}
/**
* Alters event sets.
*
* This hooks allows altering rules event sets, which contain all rules that are
* triggered upon a specific event. Rules internally caches all rules associated
* to an event in an event set, which is cached for fast evaluation. This hook
* is invoked just before any event set is cached, thus it allows altering of
* the to be executed rules without the changes to appear in the UI, e.g. to add
* a further condition to some rules.
*
* @param $event_name
* The name of the event.
* @param $event_set
* The event set that is to be cached.
*
* @see rules_invoke_event()
*/
function hook_rules_event_set_alter($event_name, RulesEventSet $event_set) {
}
/**
* D6 to D7 upgrade procedure hook for mapping action or condition names.
*
* If for a module the action or condition name changed since Drupal 6, this
* "hook" can be implemented in order to map to the new name of the action or
* condition.
*
* This is no real hook, but a callback that is invoked for each Drupal 6
* action or condition that is to be upgraded to Drupal 7. E.g. the function
* name called for the action "rules_action_set_node_title" would be
* "rules_action_set_node_title_upgrade_map_name".
*
* @param $element
* The element array of a configured condition or action which is to be
* upgraded.
* @return
* The name of the action or condition which should be used.
*/
function hook_rules_action_base_upgrade_map_name($element) {
return 'data_set';
}
/**
* D6 to D7 upgrade procedure hook for mapping action or condition configuration.
*
* During upgrading Drupal 6 rule configurations to Drupal 7 Rules is taking
* care of upgrading the configuration of all known parameters, which only works
* if the parameter name has not changed.
* If something changed, this callback can be used to properly apply the
* configruation of the Drupal 6 action ($element) to the Drupal 7 version
* ($target).
*
* This is no real hook, but a callback that is invoked for each Drupal 6
* action or condition that is to be upgraded to Drupal 7. E.g. the function
* name called for the action "rules_action_set_node_title" would be
* "rules_action_set_node_title_upgrade".
*
* @param $element
* The element array of a configured condition or action which is to be
* upgraded.
* @param $target
* The Drupal 7 version of the configured element.
*
* @see hook_rules_element_upgrade_alter()
*/
function hook_rules_action_base_upgrade($element, RulesPlugin $target) {
$target->settings['data:select'] = $element['#settings']['#argument map']['node'] . ':title';
$target->settings['value'] = $element['#settings']['title'];
}
/**
* D6 to D7 upgrade procedure hook for mapping action or condition configuration.
*
* A alter hook that is called after the action/condition specific callback for
* each element of a configuration that is upgraded.
*
* @param $element
* The element array of a configured condition or action which is to be
* upgraded.
* @param $target
* The Drupal 7 version of the configured element.
*
* @see hook_rules_action_base_upgrade()
*/
function hook_rules_element_upgrade_alter($element, $target) {
}
/**
* Allows modules to alter or to extend the provided Rules UI.
*
* Use this hook over the regular hook_menu_alter() as the Rules UI is re-used
* and embedded by modules. See rules_ui().
*
* @param $items
* The menu items to alter.
* @param $base_path
* The base path of the Rules UI.
* @param $base_count
* The count of the directories contained in the base path.
*/
function hook_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'),
);
}
/**
* @}
*/

View File

@@ -0,0 +1,72 @@
<?php
/**
* @file
* Provides Features integration for the Rules module, 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('RulesPluginFeaturesIntegrationInterace')) {
// Directly use __call() so we cann pass $export by reference.
$element->__call('features_export', array(&$export, &$pipe, $module_name));
}
}
}
return $pipe;
}
}
/**
* Default extension callback used as default for the abstract plugin class.
* Actions / conditions may override this with their own implementation, which
* actually does something.
*
* @see RulesPluginFeaturesIntegrationInterace
*/
function rules_features_abstract_default_features_export(&$export, &$pipe, $module_name = '', $element) {
}
/**
* 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 RulesPluginFeaturesIntegrationInterace {
/**
* Allows customizing the features export for a given rule element.
*/
function features_export(&$export, &$pipe, $module_name = '');
}

View File

@@ -0,0 +1,27 @@
name = Rules
description = React on events and conditionally evaluate actions.
package = Rules
core = 7.x
files[] = rules.features.inc
files[] = tests/rules.test
files[] = includes/faces.inc
files[] = includes/rules.core.inc
files[] = includes/rules.processor.inc
files[] = includes/rules.plugins.inc
files[] = includes/rules.state.inc
files[] = modules/php.eval.inc
files[] = modules/rules_core.eval.inc
files[] = modules/system.eval.inc
files[] = ui/ui.controller.inc
files[] = ui/ui.core.inc
files[] = ui/ui.data.inc
files[] = ui/ui.plugins.inc
dependencies[] = entity_token
dependencies[] = entity
; Information added by drupal.org packaging script on 2012-10-23
version = "7.x-2.2+5-dev"
core = "7.x"
project = "rules"
datestamp = "1350998486"

View File

@@ -0,0 +1,436 @@
<?php
/**
* @file Rules - Installation file.
*/
/**
* Implements hook_install().
*/
function rules_install() {
module_load_include('inc', 'rules', 'modules/events');
// Set the modules' weight to 20, see
// http://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_empty_sets');
variable_del('rules_debug');
}
/**
* 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,
),
'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'),
),
);
$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(
'id' => array('rules_config' => '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(
'id' => array('rules_config' => '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(
'id' => array('rules_config' => '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(
'id' => array('rules_config' => '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(
'id' => array('rules_config' => '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(
'id' => array('rules_config' => '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.',
));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
<?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");
module_load_include('inc', 'rules', 'modules/events');
}
/**
* 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() {
$return = array('data', 'entity', 'node', 'system', 'user', 'rules_core');
foreach (array('comment', 'taxonomy', 'php', 'path') as $module) {
if (module_exists($module)) {
$return[] = $module;
}
}
return $return;
}
/**
* 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() {
$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_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');
}

View File

@@ -0,0 +1,418 @@
<?php
/**
* @file Rules Admin UI
* 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'];
$conditions['event'] = $event;
$collapsed = FALSE;
}
$form['help'] = array(
'#type' => 'markup',
'#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(
'#type' => 'markup',
'#markup' => t('Components are stand-alone sets of Rules configuration that can be used by Rules and other modules on your site. Components are for example useful if you want to use the same conditions, actions or rules in multiple places, or call them from your custom module. You may also export each component separately. See <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="http://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', 0),
);
$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 = 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;
unset($form['settings']['help']);
unset($form['settings']['submit']);
$form['submit']['#value'] = t('Save');
$form_state += array('rules_config' => $rules_config);
$form['#validate'][] = 'rules_ui_edit_element_validate';
$form['#submit'][] = 'rules_ui_add_event_apply';
$form['#submit'][] = 'rules_ui_edit_element_submit';
return $form;
}
/**
* 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);
}

View File

@@ -0,0 +1,14 @@
name = Rules UI
description = Administrative interface for managing rules.
package = Rules
core = 7.x
files[] = rules_admin.module
files[] = rules_admin.inc
dependencies[] = rules
; Information added by drupal.org packaging script on 2012-10-23
version = "7.x-2.2+5-dev"
core = "7.x"
project = "rules"
datestamp = "1350998486"

View File

@@ -0,0 +1,134 @@
<?php
/**
* @file Rules Admin UI
*/
/**
* 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';
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* @file
* Internationalization integration based upon the entity API i18n stuff.
*/
/**
* Rules i18n integration controller.
*/
class RulesI18nStringController extends EntityDefaultI18nStringController {
/**
* Overriden 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;
}
/**
* Overriden 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 I18n String object wrapper, which register 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],
);
}
}
}
}
}

View File

@@ -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 2012-10-23
version = "7.x-2.2+5-dev"
core = "7.x"
project = "rules"
datestamp = "1350998486"

View File

@@ -0,0 +1,132 @@
<?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,
);
// For reaction-rules i18n generates the menu items, 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) {
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]);
}
}
}

View File

@@ -0,0 +1,196 @@
<?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 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 {
public static function access() {
return user_access('translate admin strings');
}
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;
}
public static function help($var_info, $param_info = array()) {
if (!empty($param_info['translatable'])) {
if ($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;
}
}
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* @file
* Rules i18n tests.
*/
/**
* Test the i18n integration.
*/
class RulesI18nTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Rules I18n',
'description' => 'Tests translating Rules configs.',
'group' => 'Rules',
'dependencies' => array('i18n_string'),
);
}
public 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 evaluted.
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.');
}
}

View File

@@ -0,0 +1,83 @@
<?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',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
),
),
);
return $table;
}

View File

@@ -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/config/[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/config/[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;
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @file
* An extended subclass for component filtering.
*/
class rules_scheduler_views_filter extends views_handler_filter_in_operator {
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;
}
}
}

View File

@@ -0,0 +1,140 @@
<?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="http://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(),
'%id' => $task['identifier'],
'%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 paramter 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;
}

View File

@@ -0,0 +1,20 @@
name = Rules Scheduler
description = Schedule the execution of Rules components using actions.
dependencies[] = rules
package = Rules
core = 7.x
files[] = rules_scheduler.admin.inc
files[] = rules_scheduler.module
files[] = rules_scheduler.install
files[] = rules_scheduler.rules.inc
files[] = rules_scheduler.test
files[] = includes/rules_scheduler.views_default.inc
files[] = includes/rules_scheduler.views.inc
files[] = includes/rules_scheduler_views_filter.inc
; Information added by drupal.org packaging script on 2012-10-23
version = "7.x-2.2+5-dev"
core = "7.x"
project = "rules"
datestamp = "1350998486"

View File

@@ -0,0 +1,159 @@
<?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,
),
'state' => array(
'type' => 'text',
'not null' => FALSE,
'serialize' => TRUE,
'description' => 'The whole, serialized evaluation state.',
),
'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'),
),
'unique key' => array(
'id' => array('config', 'identifier'),
),
);
return $schema;
}
/**
* 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,
),
'state' => array(
'type' => 'text',
'not null' => FALSE,
'serialize' => TRUE,
'description' => 'The whole, serialized evaluation state.',
),
'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'));
}
/**
* 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'];
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* @file
* Rules scheduler module.
*/
define('RULES_SCHEDULER_PATH', 'admin/config/workflow/rules/schedule');
/**
* Implements hook_cron().
*/
function rules_scheduler_cron() {
// Limit adding tasks to 1000 per cron run.
$result = db_select('rules_scheduler', 'r', array('fetch' => PDO::FETCH_ASSOC))
->fields('r')
->condition('date', time(), '<=')
->range(0, 1000)
->execute();
$queue = DrupalQueue::get('rules_scheduler_tasks');
foreach ($result as $task) {
// Add the task to the queue and remove the entry afterwards.
if ($queue->createItem($task)) {
db_delete('rules_scheduler')
->condition('tid', $task['tid'])
->execute();
$task_created = TRUE;
}
}
if (!empty($task_created)) {
// 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.
*/
function rules_scheduler_run_task(array $task) {
if ($component = rules_get_cache('comp_' . $task['config'])) {
$replacements = array('%label' => $component->label(), '%plugin' => $component->plugin());
$replacements['%identifier'] = $task['identifier'] ? $task['identifier'] : t('without identifier');
rules_log('Scheduled evaluation of %plugin %label, task %identifier.', $replacements, RulesLog::INFO, $component, TRUE);
$state = unserialize($task['state']);
$state->restoreBlocks();
// Finally evaluate the component with the given state.
$component->evaluate($state);
rules_log('Finished evaluation of %plugin %label, task %identifier.', $replacements, RulesLog::INFO, $component, FALSE);
$state->cleanUp();
}
}
/**
* 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;
}
/**
* Load a task by a given task ID.
*/
function rules_scheduler_task_load($tid) {
$result = db_select('rules_scheduler', 'r')
->fields('r')
->condition('tid', (int) $tid)
->execute();
return $result->fetchAssoc();
}
/**
* Delete a task by a given 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 $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: An rules evaluation state to use for scheduling.
* - identifier: User provided string to identify the task per scheduled
* configuration.
*/
function rules_scheduler_schedule_task($task) {
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);
}
/**
* Implements hook_rules_config_delete().
*/
function rules_scheduler_rules_config_delete($rules_config) {
// Delete all tasks scheduled for this config.
db_delete('rules_scheduler')
->condition('config', $rules_config->name)
->execute();
}
/**
* Implements hook_views_api().
*/
function rules_scheduler_views_api() {
return array(
'api' => '3.0-alpha1',
'path' => drupal_get_path('module', 'rules_scheduler') .'/includes',
);
}

View File

@@ -0,0 +1,214 @@
<?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'],
'state' => $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 to make 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('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();
}
/**
* Cancel 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.');
}
/**
* @}
*/

View File

@@ -0,0 +1,100 @@
<?php
/**
* @file
* Rules Scheduler tests.
*/
class RulesSchedulerTestCase extends DrupalWebTestCase {
static function getInfo() {
return array(
'name' => 'Rules Scheduler tests',
'description' => 'Test scheduling components.',
'group' => 'Rules',
);
}
function setUp() {
parent::setUp('rules_scheduler');
RulesLog::logger()->clear();
variable_set('rules_debug_log', 1);
}
/**
* 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.
*/
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.
drupal_cron_run();
$node = node_load($node->nid, NULL, TRUE);
$this->assertFalse($node->status, 'The component has been properly scheduled.');
RulesLog::logger()->checkLog();
}
/**
* Make sure recurion prevention is working fine for scheduled rule sets.
*/
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' => '',
'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.
drupal_cron_run();
$node = node_load($node->nid, NULL, TRUE);
$this->assertFalse($node->status, 'The component has been properly scheduled.');
$text1 = RulesLog::logger()->render();
$text2 = RulesTestCase::t('Not evaluating reaction rule %unlabeled to prevent recursion.', array('unlabeled' => $rule->name));
$this->assertTrue((strpos($text1, $text2) !== FALSE), "Scheduled recursion prevented.");
RulesLog::logger()->checkLog();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
name = "Rules Tests"
description = "Support module for the Rules tests."
package = Testing
core = 7.x
files[] = rules_test.rules.inc
files[] = rules_test.rules_defaults.inc
hidden = TRUE
; Information added by drupal.org packaging script on 2012-10-23
version = "7.x-2.2+5-dev"
core = "7.x"
project = "rules"
datestamp = "1350998486"

View File

@@ -0,0 +1,54 @@
<?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);
}
function rules_test_no_access($op) {
return $op == 'edit' ? FALSE : TRUE;
}

View File

@@ -0,0 +1,244 @@
<?php
/**
* @file Includes any rules integration provided by the module.
*/
/**
* 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'),
);
// 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 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');
}
/**
* 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 {
protected function extractIdentifier($data) {
return $data->nid;
}
protected function load($id) {
return node_load($id);
}
public function save() {
node_save($this->value());
}
}

View File

@@ -0,0 +1,121 @@
<?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';
$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,
"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" ] } } ]
}
}
]
}
}';
}

View File

@@ -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) {
}

View File

@@ -0,0 +1,195 @@
// 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
// overriden 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);
}
});
});
// Since jquery autocomplete by default strips html text by using .text()
// we need our own _renderItem function to display html content.
this.jqObject.data("autocomplete")._renderItem = function(ul, item) {
return $("<li></li>").data("item.autocomplete", item).append("<a>" + item.label + "</a>").appendTo(ul);
};
// Override close function
this.jqObject.data("autocomplete").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();
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");
};
/**
* Toogle the autcomplete 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);

View File

@@ -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);

View File

@@ -0,0 +1,196 @@
@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;
}
.rules-elements-table ul.rules-operations a {
background: none;
padding-left: 0px;
}
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;
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;
}
.rules-elements-table caption, .rules-overview-table caption {
font-size: 110%;
font-weight: bold;
padding-bottom: 0.5em;
text-align: left;
}
.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 whitepsace.
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;
}
.rules-debug-open-all {
position: relative;
float: right;
}
.rules-debug-log ul {
padding-left: 2em;
}
.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;
}
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;
}
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;
}

View File

@@ -0,0 +1,95 @@
/**
* JQuery UI style sheet 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;
-webkit-border-top-left-radius: 4px;
border-top-left-radius: 4px;
}
.ui-corner-tr {
-moz-border-radius-topright: 4px;
-webkit-border-top-right-radius: 4px;
border-top-right-radius: 4px;
}
.ui-corner-bl {
-moz-border-radius-bottomleft: 4px;
-webkit-border-bottom-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.ui-corner-br {
-moz-border-radius-bottomright: 4px;
-webkit-border-bottom-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.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;
-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;
}
.ui-corner-left {
-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-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;
}

View File

@@ -0,0 +1,316 @@
<?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 $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. Therefor we have to
// use this fugly 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', $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 a 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 $conditions
* An array of conditions as needed by rules_config_load_multiple().
* @param $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'),
);
if (!empty($options['base path'])) {
RulesPluginUI::$basePath = $options['base path'];
}
else if (!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 $additional_cols
* Additional columns to be added after the entity label column.
*/
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) {
$this->event_info += array($event_name => array('label' => t('Unknown event "!event_name"', array('!event_name' => $event_name))));
$events[] = check_plain($this->event_info[$event_name]['label']);
}
}
$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'))));
$row[] = l(t('translate'), RulesPluginUI::path($name, 'translate'), array('attributes' => array('class' => array('translate', 'action'))));
}
else {
$row[] = '';
$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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,521 @@
<?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);
}
/**
* 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 by making use of the label if an options list is available.
*
* Used for data UI classes implementing the
* RulesDataDirectInputFormInterface.
*
* In case an options list is available, the 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($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')),
);
}
}
}
/**
* UI for textual data.
*/
class RulesDataUIText extends RulesDataUI implements RulesDataDirectInputFormInterface {
public static function getDefaultMode() {
return 'input';
}
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',
);
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'),
'#rows' => 3,
);
return $form;
}
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 {
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 {
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;
}
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 {
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 {
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 boolean data.
*/
class RulesDataUIBoolean extends RulesDataUI implements RulesDataDirectInputFormInterface {
public static function getDefaultMode() {
return 'input';
}
public static function inputForm($name, $info, $settings, RulesPlugin $element) {
$settings += array($name => isset($info['default value']) ? $info['default value'] : NULL);
// Note: Due to the checkbox even optional parameter always receive a value.
$form[$name] = array(
'#type' => 'checkbox',
'#title' => check_plain($info['label']),
'#default_value' => $settings[$name],
);
return $form;
}
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 {
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 timpeicker is available.
return $form;
}
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 {
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;
}
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 {
public static function inputForm($name, $info, $settings, RulesPlugin $element) {
$form = parent::inputForm($name, $info, $settings, $element);
$form[$name]['#rows'] = 1;
$form[$name]['#description'] = t('You may enter relative URLs like %url as well as absolute URLs like %absolute-url.', array('%url' => 'user/login?destination=node', '%absolute-url' => 'http://drupal.org'));
return $form;
}
}
/**
* UI for lists of textual data.
*/
class RulesDataUIListText extends RulesDataUIText {
public static function getDefaultMode() {
return 'input';
}
/**
* @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;
}
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 {
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 {
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 {
public static function getDefaultMode() {
return 'selector';
}
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 {
public static function getDefaultMode() {
return 'input';
}
}
/**
* UI for taxonomy vocabularies.
*
* @see RulesTaxonomyVocabularyWrapper
*/
class RulesDataUITaxonomyVocabulary extends RulesDataUIEntity {
public static function getDefaultMode() {
return 'input';
}
public static function inputForm($name, $info, $settings, RulesPlugin $element) {
// Add an options list of all vocabularies if there is none yet.
if (!isset($info['options list'])) {
$info['options list'] = array('RulesDataUITaxonomyVocabulary', 'optionsList');
}
return parent::inputForm($name, $info, $settings, $element);
}
public static function optionsList() {
$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 {
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;
}
}

View File

@@ -0,0 +1,920 @@
<?php
/**
* @file Rules UI forms
*/
/**
* Ajax callback for reloading the whole form.
*/
function rules_ui_form_ajax_reload_form($form, $form_state) {
return $form;
}
/**
* Defines #ajax properties.
*/
function rules_ui_form_default_ajax($effect = 'slide') {
return array(
'callback' => 'rules_ui_form_ajax_reload_form',
'wrapper' => 'rules-form-wrapper',
'effect' => $effect,
'speed' => 'fast',
);
}
/**
* Submit handler for switching the parameter input mode.
*/
function rules_ui_parameter_replace_submit($form, &$form_state) {
if (isset($form_state['triggering_element'])) {
$name = $form_state['triggering_element']['#parameter'];
$form_state['parameter_mode'][$name] = $form_state['parameter_mode'][$name] == 'selector' ? 'input' : 'selector';
}
$form_state['rebuild'] = TRUE;
}
/**
* General form submit handler, that rebuilds the form
*/
function rules_form_submit_rebuild($form, &$form_state) {
$form_state['rebuild'] = TRUE;
}
/**
* Edit a rules configuration.
*/
function rules_ui_form_edit_rules_config($form, &$form_state, $rules_config, $base_path) {
RulesPluginUI::$basePath = $base_path;
$form_state += array('rules_element' => $rules_config);
// Add the rule configuration's form.
$rules_config->form($form, $form_state, array('show settings' => TRUE, 'button' => TRUE));
$form['#validate'] = array('rules_ui_form_rules_config_validate');
return $form;
}
/**
* General rules configuration form validation callback. Also populates the
* rules configuration with the form values.
*/
function rules_ui_form_rules_config_validate($form, &$form_state) {
$form_state['rules_element']->form_validate($form, $form_state);
}
/**
* Edit a rules configuration form submit callback.
*/
function rules_ui_form_edit_rules_config_submit($form, &$form_state) {
$form_state['rules_element']->form_submit($form, $form_state);
drupal_set_message(t('Your changes have been saved.'));
if (empty($form_state['redirect'])) {
$form_state['redirect'] = RulesPluginUI::defaultRedirect($form_state['rules_element']);
}
}
/**
* Clone a rules configuration form.
*/
function rules_ui_form_clone_rules_config($form, &$form_state, $rules_config, $base_path) {
RulesPluginUI::$basePath = $base_path;
$rules_config = clone $rules_config;
$rules_config->module = 'rules';
$rules_config->id = NULL;
$rules_config->name = '';
$rules_config->label .= ' (' . t('cloned') . ')';
$rules_config->status = ENTITY_CUSTOM;
$form['#validate'][] = 'rules_ui_form_rules_config_validate';
$form['#submit'][] = 'rules_ui_form_edit_rules_config_submit';
$form_state += array('rules_element' => $rules_config, 'op' => 'clone');
// Add the rule configuration's form.
$rules_config->form($form, $form_state, array('show settings' => TRUE, 'button' => TRUE, 'init' => TRUE));
// Open the settings fieldset so altering the name is easier.
$form['settings']['#collapsed'] = FALSE;
return $form;
}
/**
* A simple form just showing a textarea with the export.
*/
function rules_ui_form_export_rules_config($form, &$form_state, $rules_config, $base_path) {
$form['export'] = array(
'#type' => 'textarea',
'#title' => t('Export'),
'#description' => t('For importing copy the content of the text area and paste it into the import page.'),
'#rows' => 25,
'#default_value' => $rules_config->export(),
);
return $form;
}
/**
* Configuration form to directly execute a rules configuration.
*/
function rules_ui_form_execute_rules_config($form, &$form_state, $rules_config, $base_path) {
// Only components can be executed.
if (!($rules_config instanceof RulesTriggerableInterface)) {
RulesPluginUI::$basePath = $base_path;
// Create either the appropriate action or condition element.
$element = rules_plugin_factory($rules_config instanceof RulesActionInterface ? 'action' : 'condition', 'component_' . $rules_config->name);
$form['exec_help'] = array(
'#prefix' => '<p>',
'#markup' => t('This form allows you to manually trigger the execution of the @plugin "%label". If this component requires any parameters, input the suiting execution arguments below.', array('@plugin' => $rules_config->plugin(), '%label' => $rules_config->label())),
'#suffix' => '</p>',
);
$element->form($form, $form_state);
// For conditions hide the option to negate them.
if (isset($form['negate'])) {
$form['negate']['#access'] = FALSE;
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Execute'),
'#weight' => 20,
);
// Re-use the validation callback, which will also populate the action with
// the configuration settings in the form.
$form['#validate'] = array('rules_ui_form_rules_config_validate');
return $form;
}
drupal_not_found();
exit;
}
/**
* Submit callback for directly executing a component.
*/
function rules_ui_form_execute_rules_config_submit($form, &$form_state) {
$element = $form_state['rules_element'];
$result = $element->execute();
if ($element instanceof RulesActionInterface) {
drupal_set_message(t('Component %label has been executed.', array('%label' => $element->label())));
}
else {
drupal_set_message(t('Component %label evaluated to %result.', array('%label' => $element->label(), '%result' => $result ? 'true' : 'false')));
}
}
/**
* Gets the confirmation question for valid operations, or else FALSE.
*/
function rules_ui_confirm_operations($op, $rules_config) {
$vars = array('%plugin' => $rules_config->plugin(), '%label' => $rules_config->label());
switch ($op) {
case 'enable':
return array(t('Are you sure you want to enable the %plugin %label?', $vars), '');
case 'disable':
return array(t('Are you sure you want to disable the %plugin %label?', $vars), '');
case 'revert':
return array(t('Are you sure you want to revert the %plugin %label?', $vars), t('This action cannot be undone.'));
case 'delete':
return array(t('Are you sure you want to delete the %plugin %label?', $vars), t('This action cannot be undone.'));
default:
return FALSE;
}
}
/**
* Confirmation form for applying the operation to the config.
*/
function rules_ui_form_rules_config_confirm_op($form, &$form_state, $rules_config, $op, $base_path) {
if (list($confirm_question, $description) = rules_ui_confirm_operations($op, $rules_config)) {
RulesPluginUI::$basePath = $base_path;
$form_state += array('rules_config' => $rules_config, 'op' => $op);
return confirm_form($form, $confirm_question, $base_path, $description, t('Confirm'), t('Cancel'));
}
else {
drupal_not_found();
exit;
}
}
/**
* Applies the operation and returns the message to show to the user. Also the
* operation is logged to the watchdog. Note that the string is defined two
* times so that the translation extractor can find it.
*/
function rules_ui_confirm_operation_apply($op, $rules_config) {
$vars = array('%plugin' => $rules_config->plugin(), '%label' => $rules_config->label());
$edit_link = l(t('edit'), RulesPluginUI::path($rules_config->name));
switch ($op) {
case 'enable':
$rules_config->active = TRUE;
$rules_config->save();
watchdog('rules', 'Enabled %plugin %label.', $vars, WATCHDOG_NOTICE, $edit_link);
return t('Enabled %plugin %label.', $vars);
case 'disable':
$rules_config->active = FALSE;
$rules_config->save();
watchdog('rules', 'Disabled %plugin %label.', $vars, WATCHDOG_NOTICE, $edit_link);
return t('Disabled %plugin %label.', $vars);
case 'revert':
$rules_config->delete();
watchdog('rules', 'Reverted %plugin %label to the defaults.', $vars, WATCHDOG_NOTICE, $edit_link);
return t('Reverted %plugin %label to the defaults.', $vars);
case 'delete':
$rules_config->delete();
watchdog('rules', 'Deleted %plugin %label.', $vars);
return t('Deleted %plugin %label.', $vars);
}
}
/**
* Rule config deletion form submit callback.
*/
function rules_ui_form_rules_config_confirm_op_submit($form, &$form_state) {
if ($form_state['values']['confirm']) {
$msg = rules_ui_confirm_operation_apply($form_state['op'], $form_state['rules_config']);
drupal_set_message($msg);
}
}
/**
* Add a new element a rules configuration.
*/
function rules_ui_add_element($form, &$form_state, $rules_config, $plugin_name, RulesContainerPlugin $parent, $base_path) {
$cache = rules_get_cache();
if (!isset($cache['plugin_info'][$plugin_name]['class'])) {
drupal_not_found();
exit;
}
RulesPluginUI::$basePath = $base_path;
$plugin_is_abstract = in_array('RulesAbstractPlugin', class_parents($cache['plugin_info'][$plugin_name]['class']));
// In the first step create the element and in the second step show its edit
// form.
if ($plugin_is_abstract && !isset($form_state['rules_element'])) {
RulesPluginUI::formDefaults($form, $form_state);
$form_state += array('parent_element' => $parent, 'plugin' => $plugin_name);
$form['element_name'] = array(
'#type' => 'select',
'#title' => t('Select the %element to add', array('%element' => $plugin_name)),
'#options' => RulesPluginUI::getOptions($plugin_name),
'#ajax' => rules_ui_form_default_ajax() + array(
'trigger_as' => array('name' => 'continue'),
),
);
$form['continue'] = array(
'#type' => 'submit',
'#name' => 'continue',
'#value' => t('Continue'),
'#ajax' => rules_ui_form_default_ajax(),
);
}
elseif (!$plugin_is_abstract) {
// Create the initial, empty element.
$element = rules_plugin_factory($plugin_name);
// Always add the new element at the bottom, thus set an appropriate weight.
$iterator = $parent->getIterator();
if ($sibling = end($iterator)) {
$element->weight = $sibling->weight + 1;
}
$element->setParent($parent);
$form_state['rules_element'] = $element;
}
if (isset($form_state['rules_element'])) {
$form_state['rules_element']->form($form, $form_state, array('button' => TRUE, 'init' => TRUE));
$form['#validate'][] = 'rules_ui_edit_element_validate';
$form['#submit'][] = 'rules_ui_edit_element_submit';
}
return $form;
}
/**
* Add element submit callback.
* Used for "abstract plugins" to create the initial element object with the
* given implemenation name and rebuild the form.
*/
function rules_ui_add_element_submit($form, &$form_state) {
$element = rules_plugin_factory($form_state['plugin'], $form_state['values']['element_name']);
// Always add the new element at the bottom, thus set an appropriate weight.
$iterator = $form_state['parent_element']->getIterator();
if ($sibling = end($iterator)) {
$element->weight = $sibling->weight + 1;
}
// Clear the element settings so they won't be processed on serialization as
// there is nothing to be processed yet.
$element->settings = array();
$element->setParent($form_state['parent_element']);
$form_state['rules_element'] = $element;
$form_state['rebuild'] = TRUE;
}
/**
* Delete elements.
*/
function rules_ui_delete_element($form, &$form_state, $rules_config, $rules_element, $base_path) {
RulesPluginUI::$basePath = $base_path;
if (empty($form_state['rules_config'])) {
// Before modifying the rules config we have to clone it, so any
// modifications won't appear in the static cache of the loading controller.
$rules_config = clone $rules_config;
// Also get the element from the cloned config.
$rules_element = $rules_config->elementMap()->lookup($rules_element->elementId());
$form_state['rules_config'] = $rules_config;
$form_state['rules_element'] = $rules_element;
$form_state['element_parent'] = $rules_element->parentElement();
}
// Try deleting the element and warn the user if something breaks, but
// save the parent for determining the right redirect target on submit.
$removed_plugin = $form_state['rules_element']->plugin();
$rules_element->delete();
if (empty($rules_config->dirty) && empty($form_state['input'])) {
try {
$rules_config->integrityCheck();
}
catch (RulesIntegrityException $e) {
$args = array(
'@plugin' => $e->element->plugin(),
'%label' => $e->element->label(),
'@removed-plugin' => $removed_plugin,
'!url' => url(RulesPluginUI::path($form_state['rules_config']->name, 'edit', $e->element)),
);
drupal_set_message(t('Deleting this @removed-plugin would break your configuration as some of its provided variables are utilized by the @plugin <a href="!url">%label</a>.', $args), 'warning');
}
}
$confirm_question = t('Are you sure you want to delete the %element_plugin %element_name?', array('%element_plugin' => $rules_element->plugin(), '%element_name' => $rules_element->label(), '%plugin' => $rules_config->plugin(), '%label' => $rules_config->label()));
return confirm_form($form, $confirm_question, RulesPluginUI::path($rules_config->name), t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}
/**
* Rule config deletion form submit callback.
*/
function rules_ui_delete_element_submit($form, &$form_state) {
$rules_config = $form_state['rules_config'];
$rules_config->save();
if (empty($form_state['redirect'])) {
$form_state['redirect'] = RulesPluginUI::defaultRedirect($form_state['element_parent']);
}
}
/**
* Configure a rule element.
*/
function rules_ui_edit_element($form, &$form_state, $rules_config, $element, $base_path) {
RulesPluginUI::$basePath = $base_path;
$form_state += array('rules_element' => $element);
$form_state['rules_element']->form($form, $form_state, array('button' => TRUE));
return $form;
}
/**
* Validate the element configuration.
*/
function rules_ui_edit_element_validate($form, &$form_state) {
$form_state['rules_element']->form_validate($form, $form_state);
}
/**
* Submit the element configuration.
*/
function rules_ui_edit_element_submit($form, &$form_state) {
$form_state['rules_element']->form_submit($form, $form_state);
drupal_set_message(t('Your changes have been saved.'));
if (empty($form_state['redirect'])) {
$form_state['redirect'] = RulesPluginUI::defaultRedirect($form_state['rules_element']);
}
}
/**
* Add a new event.
*/
function rules_ui_add_event($form, &$form_state, RulesReactionRule $rules_config, $base_path) {
RulesPluginUI::$basePath = $base_path;
$form_state += array('rules_config' => $rules_config);
$events = array_diff_key(rules_fetch_data('event_info'), array_flip($rules_config->events()));
$form['help'] = array(
'#markup' => t('Select the event to add. However note that all added events need to provide all variables that should be available to your rule.'),
);
$form['event'] = array(
'#type' => 'select',
'#title' => t('React on event'),
'#options' => RulesPluginUI::getOptions('event', $events),
'#description' => t('Whenever the event occurs, rule evaluation is triggered.'),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Add'),
);
$form_state['redirect'] = RulesPluginUI::path($rules_config->name);
return $form;
}
/**
* Submit callback that just adds the selected event.
*
* @see rules_admin_add_reaction_rule()
*/
function rules_ui_add_event_apply($form, &$form_state) {
$form_state['rules_config']->event($form_state['values']['event']);
}
/**
* Submit the event configuration.
*/
function rules_ui_add_event_submit($form, &$form_state) {
rules_ui_add_event_apply($form, $form_state);
$rules_config = $form_state['rules_config'];
// Tell the user if this breaks something, but let him proceed.
if (empty($rules_config->dirty)) {
try {
$rules_config->integrityCheck();
}
catch (RulesIntegrityException $e) {
$warning = TRUE;
drupal_set_message(t('Added the event, but it does not provide all variables utilized.'), 'warning');
}
}
$rules_config->save();
if (!isset($warning)) {
$events = rules_fetch_data('event_info');
$label = $events[$form_state['values']['event']]['label'];
drupal_set_message(t('Added event %event.', array('%event' => $label)));
}
}
/**
* Form to remove a event from a rule.
*/
function rules_ui_remove_event($form, &$form_state, $rules_config, $event, $base_path) {
RulesPluginUI::$basePath = $base_path;
$form_state += array('rules_config' => $rules_config, 'rules_event' => $event);
$events = rules_fetch_data('event_info');
$form_state['event_label'] = $events[$event]['label'];
$confirm_question = t('Are you sure you want to remove the event?');
return confirm_form($form, $confirm_question, RulesPluginUI::path($rules_config->name), t('You are about to remove the event %event.', array('%event' => $form_state['event_label'])), t('Remove'), t('Cancel'));
}
/**
* Submit the event configuration.
*/
function rules_ui_remove_event_submit($form, &$form_state) {
$rules_config = $form_state['rules_config'];
$rules_config->removeEvent($form_state['rules_event']);
// Tell the user if this breaks something, but let him proceed.
if (empty($rules_config->dirty)) {
try {
$rules_config->integrityCheck();
}
catch (RulesIntegrityException $e) {
$warning = TRUE;
drupal_set_message(t('Removed the event, but it had provided some variables which are now missing.'), 'warning');
}
}
$rules_config->save();
if (!isset($warning)) {
drupal_set_message(t('Event %event has been removed.', array('%event' => $form_state['event_label'])));
}
$form_state['redirect'] = RulesPluginUI::path($rules_config->name);
}
/**
* Import form for rule configurations.
*/
function rules_ui_import_form($form, &$form_state, $base_path) {
RulesPluginUI::$basePath = $base_path;
RulesPluginUI::formDefaults($form, $form_state);
$form['import'] = array(
'#type' => 'textarea',
'#title' => t('Import'),
'#description' => t('Paste an exported Rules configuration here.'),
'#rows' => 20,
);
$form['overwrite'] = array(
'#title' => t('Overwrite'),
'#type' => 'checkbox',
'#description' => t('If checked, any existing configuration with the same identifier will be replaced by the import.'),
'#default_value' => FALSE,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Import'),
);
return $form;
}
/**
* Validation callback for the import form.
*/
function rules_ui_import_form_validate($form, &$form_state) {
if ($rules_config = rules_import($form_state['values']['import'], $error_msg)) {
// Store the successfully imported entity in $form_state.
$form_state['rules_config'] = $rules_config;
if (!$form_state['values']['overwrite']) {
// Check for existing entities with the same identifier.
if (rules_config_load($rules_config->name)) {
$vars = array('@entity' => t('Rules configuration'), '%label' => $rules_config->label());
form_set_error('import', t('Import of @entity %label failed, a @entity with the same machine name already exists. Check the overwrite option to replace it.', $vars));
}
}
try {
$rules_config->integrityCheck();
}
catch (RulesIntegrityException $e) {
form_set_error('import', t('Integrity check for the imported configuration failed. Error message: %message.', array('%message' => $e->getMessage())));
}
if (!user_access('bypass rules access') && !$rules_config->access()) {
form_set_error('import', t('You have insufficient access permissions for importing this Rules configuration.'));
}
}
else {
form_set_error('import', t('Import failed.'));
if ($error_msg) {
drupal_set_message($error_msg, 'error');
}
}
}
/**
* Submit callback for the import form.
*/
function rules_ui_import_form_submit($form, &$form_state) {
$rules_config = $form_state['rules_config'];
if ($existing_config = rules_config_load($rules_config->name)) {
// Copy DB id and remove the new indicator to overwrite the existing record.
$rules_config->id = $existing_config->id;
unset($rules_config->is_new);
}
$rules_config->save();
$vars = array('@entity' => t('Rules configuration'), '%label' => $rules_config->label());
watchdog('rules_config', 'Imported @entity %label.', $vars);
drupal_set_message(t('Imported @entity %label.', $vars));
$form_state['redirect'] = RulesPluginUI::$basePath;
}
/**
* FAPI process callback for the data selection widget.
* This finalises the auto completion callback path by appending the form build
* id.
*/
function rules_data_selection_process($element, &$form_state, $form) {
$element['#autocomplete_path'] .= '/' . $form['#build_id'];
$form_state['cache'] = TRUE;
return $element;
}
/**
* Autocomplete data selection results.
*/
function rules_ui_form_data_selection_auto_completion($parameter, $form_build_id, $string = '') {
// Get the form and its state from the cache to get the currently edited
// or created element.
$form_state = form_state_defaults();
$form = form_get_cache($form_build_id, $form_state);
if (!isset($form_state['rules_element'])) {
return;
}
$element = $form_state['rules_element'];
$params = $element->pluginParameterInfo();
$matches = array();
if (isset($params[$parameter])) {
$parts = explode(':', $string);
// Remove the last part as it might be unfinished.
$last_part = array_pop($parts);
$selector = implode(':', $parts);
// Start with the partly given selector or from scratch.
$result = array();
if ($selector && $wrapper = $element->applyDataSelector($selector)) {
$result = RulesData::matchingDataSelector($wrapper, $params[$parameter], $selector . ':', 0);
}
elseif (!$selector) {
$result = RulesData::matchingDataSelector($element->availableVariables(), $params[$parameter], '', 0);
}
foreach ($result as $selector => $info) {
// If we have an uncomplete last part, take it into account now.
$attributes = array();
if (!$last_part || strpos($selector, $string) === 0) {
$attributes['class'][] = 'rules-dsac-item';
$attributes['title'] = isset($info['description']) ? strip_tags($info['description']) : '';
if ($selector[strlen($selector) - 1] == ':') {
$attributes['class'][] = 'rules-dsac-group';
$text = check_plain($selector) . '... (' . check_plain($info['label']) . ')';
}
else {
$text = check_plain($selector) . ' (' . check_plain($info['label']) . ')';
}
$matches[$selector] = "<div" . drupal_attributes($attributes) . ">$text</div";
}
}
}
drupal_json_output($matches);
}
/**
* FAPI validation of an integer element. Copy of the private function
* _element_validate_integer().
*/
function rules_ui_element_integer_validate($element, &$form_state) {;
$value = $element['#value'];
if (isset($value) && $value !== '' && (!is_numeric($value) || intval($value) != $value)) {
form_error($element, t('%name must be an integer value.', array('%name' => isset($element['#title']) ? $element['#title'] : t('Element'))));
}
}
/**
* FAPI validation of a decimal element. Improved version of the private
* function _element_validate_number().
*/
function rules_ui_element_decimal_validate($element, &$form_state) {
// Substitute the decimal separator ",".
$value = strtr($element['#value'], ',', '.');
if ($value != '' && !is_numeric($value)) {
form_error($element, t('%name must be a number.', array('%name' => $element['#title'])));
}
elseif ($value != $element['#value']) {
form_set_value($element, $value, $form_state);
}
}
/**
* FAPI validation of a date element. Makes sure the specified date format is
* correct and converts date values specifiy a fixed (= non relative) date to
* a timestamp. Relative dates are handled by the date input evaluator.
*/
function rules_ui_element_date_validate($element, &$form_state) {
$value = $element['#value'];
if ($value == '' || (is_numeric($value) && intval($value) == $value)) {
// The value is a timestamp.
return;
}
elseif (is_string($value) && RulesDateInputEvaluator::gmstrtotime($value) === FALSE) {
form_error($element, t('Wrong date format. Specify the date in the format %format.', array('%format' => gmdate('Y-m-d H:i:s', time() + 86400))));
}
elseif (is_string($value) && RulesDateInputEvaluator::isFixedDateString($value)) {
// As the date string specifies a fixed format, we can convert it now.
$value = RulesDateInputEvaluator::gmstrtotime($value);
form_set_value($element, $value, $form_state);
}
}
/**
* FAPI process callback for the duration element type.
*/
function rules_ui_element_duration_process($element, &$form_state) {
$element['value'] = array(
'#type' => 'textfield',
'#size' => 8,
'#element_validate' => array('rules_ui_element_integer_validate'),
'#default_value' => $element['#default_value'],
'#required' => !empty($element['#required']),
);
$element['multiplier'] = array(
'#type' => 'select',
'#options' => rules_ui_element_duration_multipliers(),
'#default_value' => 1,
);
// Put the child elements in a container-inline div.
$element['value']['#prefix'] = '<div class="rules-duration container-inline">';
$element['multiplier']['#suffix'] = '</div>';
// Set an appropriate multiplier.
if (!empty($element['value']['#default_value'])) {
foreach (array_keys(rules_ui_element_duration_multipliers()) as $m) {
if ($element['value']['#default_value'] % $m == 0) {
$element['multiplier']['#default_value'] = $m;
}
}
// Divide value by the multiplier, so the display is correct.
$element['value']['#default_value'] /= $element['multiplier']['#default_value'];
}
return $element;
}
/**
* Defines possible duration multiplier.
*/
function rules_ui_element_duration_multipliers() {
return array(
1 => t('seconds'),
60 => t('minutes'),
3600 => t('hours'),
// Just use approximate numbers for days (might last 23h on DST change),
// months and years.
86400 => t('days'),
86400 * 30 => t('months'),
86400 * 30 * 12 => t('years'),
);
}
/**
* Helper function to determine the value for a rules duration form
* element.
*/
function rules_ui_element_duration_value($element, $input = FALSE) {
// This runs before child elements are processed, so we cannot calculate the
// value here. But we have to make sure the value is an array, so the form
// API is able to process the children to set their values in the array. Thus
// once the form API has finished processing the element, the value is an
// array containing the child element values. Then finally the after build
// callback converts it back to the numeric value and sets that.
return array();
}
/**
* FAPI after build callback for the duration parameter type form.
* Fixes up the form value by applying the multiplier.
*/
function rules_ui_element_duration_after_build($element, &$form_state) {
if ($element['value']['#value'] !== '') {
$element['#value'] = $element['value']['#value'] * $element['multiplier']['#value'];
form_set_value($element, $element['#value'], $form_state);
}
else {
$element['#value'] = NULL;
form_set_value($element, NULL, $form_state);
}
return $element;
}
/**
* FAPI after build callback to ensure empty form elements result in no value.
*/
function rules_ui_element_fix_empty_after_build($element, &$form_state) {
if (isset($element['#value']) && $element['#value'] === '') {
$element['#value'] = NULL;
form_set_value($element, NULL, $form_state);
}
// Work-a-round for the text_format element.
elseif ($element['#type'] == 'text_format' && !isset($element['value']['#value'])) {
form_set_value($element, NULL, $form_state);
}
return $element;
}
/**
* FAPI after build callback for specifying a list of values.
*
* Turns the textual value in an array by splitting the text in chunks using the
* delimiter set at $element['#delimiter'].
*/
function rules_ui_list_textarea_after_build($element, &$form_state) {
$element['#value'] = $element['#value'] ? explode($element['#delimiter'], $element['#value']) : array();
$element['#value'] = array_map('trim', $element['#value']);
form_set_value($element, $element['#value'], $form_state);
return $element;
}
/**
* FAPI pre render callback. Turns the value back to a string for rendering.
*
* @see rules_ui_list_textarea_after_build()
*/
function rules_ui_list_textarea_pre_render($element) {
$element['#value'] = implode($element['#delimiter'], $element['#value']);
return $element;
}
/**
* FAPI callback to validate a list of integers.
*/
function rules_ui_element_integer_list_validate($element, &$form_state) {
foreach ($element['#value'] as $value) {
if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) {
form_error($element, t('Each value must be an integer.'));
}
}
}
/**
* FAPI callback to validate a token.
*/
function rules_ui_element_token_validate($element) {
$value = $element['#value'];
if (isset($value) && $value !== '' && !entity_property_verify_data_type($value, 'token')) {
form_error($element, t('%name may only contain lowercase letters, numbers, and underscores and has to start with a letter.', array('%name' => isset($element['#title']) ? $element['#title'] : t('Element'))));
}
}
/**
* FAPI callback to validate a list of tokens.
*/
function rules_ui_element_token_list_validate($element, &$form_state) {
foreach ($element['#value'] as $value) {
if ($value !== '' && !entity_property_verify_data_type($value, 'token')) {
form_error($element, t('Each value may only contain lowercase letters, numbers, and underscores and has to start with a letter.'));
}
}
}
/**
* FAPI callback to validate a machine readable name.
*/
function rules_ui_element_machine_name_validate($element, &$form_state) {
if ($element['#value'] && !preg_match('!^[a-z0-9_]+$!', $element['#value'])) {
form_error($element, t('Machine-readable names must contain only lowercase letters, numbers, and underscores.'));
}
}
/**
* FAPI callback to validate the form for editing variable info.
* @see RulesPluginUI::getVariableForm()
*/
function rules_ui_element_variable_form_validate($elements, &$form_state) {
$names = array();
foreach (element_children($elements['items']) as $item_key) {
$element = &$elements['items'][$item_key];
if ($element['name']['#value'] || $element['type']['#value'] || $element['label']['#value']) {
foreach (array('name' => t('Machine name'), 'label' => t('Label'), 'type' => t('Data type')) as $key => $title) {
if (!$element[$key]['#value']) {
form_error($element[$key], t('!name field is required.', array('!name' => $title)));
}
}
if (isset($names[$element['name']['#value']])) {
form_error($element['name'], t('The machine-readable name %name is already taken.', array('%name' => $element['name']['#value'])));
}
$names[$element['name']['#value']] = TRUE;
}
}
}
/**
* Helper to sort elements by their 'weight' key.
*/
function rules_element_sort_helper($a, $b) {
$a += array('weight' => 0);
$b += array('weight' => 0);
if ($a['weight'] == $b['weight']) {
return 0;
}
return ($a['weight'] < $b['weight']) ? -1 : 1;
}
/**
* Form after build handler to set the static base path.
*
* @see RulesPluginUI::formDefaults()
*/
function rules_form_after_build_restore_base_path($form, &$form_state) {
if (isset($form_state['_rules_base_path'])) {
RulesPluginUI::$basePath = $form_state['_rules_base_path'];
}
return $form;
}
/**
* AJAX page callback to load tag suggestions.
*
* Largely copied from taxonomy_autocomplete().
*/
function rules_autocomplete_tags($tags_typed = '') {
// The user enters a comma-separated list of tags. We only autocomplete the
// last tag.
$tags_typed = drupal_explode_tags($tags_typed);
$tag_last = drupal_strtolower(array_pop($tags_typed));
$tag_matches = array();
if ($tag_last != '') {
$query = db_select('rules_tags', 'rt');
// Do not select already entered terms.
if (!empty($tags_typed)) {
$query->condition('rt.tag', $tags_typed, 'NOT IN');
}
// Select rows that match by tag name.
$tags_return = $query
->distinct()
->fields('rt', array('tag'))
->condition('rt.tag', '%' . db_like($tag_last) . '%', 'LIKE')
->groupBy('rt.tag')
->range(0, 10)
->execute()
->fetchCol('rt.tag');
$prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
foreach ($tags_return as $name) {
$n = $name;
// Tag names containing commas or quotes must be wrapped in quotes.
if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
$n = '"' . str_replace('"', '""', $name) . '"';
}
$tag_matches[$prefix . $n] = check_plain($name);
}
}
drupal_json_output($tag_matches);
}

View File

@@ -0,0 +1,234 @@
<?php
/**
* @file Contains UI for diverse plugins provided by Rules.
*/
/**
* Rule specific UI.
*/
class RulesRuleUI extends RulesActionContainerUI {
protected $rule, $conditions;
public function __construct(FacesExtendable $object) {
parent::__construct($object);
$this->rule = $object;
$this->conditions = $this->rule->conditionContainer();
}
public function form(&$form, &$form_state, $options = array()) {
$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.
*/
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()) {
$form['events'] = array(
'#type' => 'container',
'#weight' => -10,
'#access' => empty($options['init']),
);
$event_info = rules_fetch_data('event_info');
$form['events']['table'] = array(
'#theme' => 'table',
'#caption' => 'Events',
'#header' => array('Event', 'Operations'),
'#empty' => t('None'),
);
$form['events']['table']['#attributes']['class'][] = 'rules-elements-table';
foreach ($this->rule->events() as $event_name) {
$event_info += array($event_name => array('label' => t('Unknown event "!event_name"', array('!event_name' => $event_name))));
$form['events']['table']['#rows'][$event_name] = array(
check_plain($event_info[$event_name]['label']),
'<span class="rules_rule_event">' . l(t('delete'), RulesPluginUI::path($this->rule->name, 'delete/event/' . $event_name)) . '</span>',
);
}
// 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()) {
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,
);
}
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;
}
}

View File

@@ -0,0 +1,287 @@
<?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>";
}