2007-06-29 18:06:51 +00:00
< ? php
2008-06-29 12:07:15 +00:00
// $Id$
2007-06-29 18:06:51 +00:00
2008-06-29 12:07:15 +00:00
/**
* @ file
* This is the actions engine for executing stored actions .
*/
/**
* Perform a given list of actions by executing their callback functions .
*
* Given the IDs of actions to perform , find out what the callbacks
* for the actions are by querying the database . Then call each callback
2008-08-12 06:57:45 +00:00
* using the function call $function ( $object , $context , $a1 , $a2 )
2008-06-29 12:07:15 +00:00
* where $function is the name of a function written in compliance with
* the action specification ; that is , foo ( $object , $context ) .
*
* @ param $action_ids
* The ID of the action to perform . Can be a single action ID or an array
* of IDs . IDs of instances will be numeric ; IDs of singletons will be
* function names .
* @ param $object
* Parameter that will be passed along to the callback . Typically the
* object that the action will act on ; a node , user or comment object .
* @ param $context
* Parameter that will be passed along to the callback . $context is a
* keyed array containing extra information about what is currently
* happening at the time of the call . Typically $context [ 'hook' ] and
* $context [ 'op' ] will tell which hook - op combination resulted in this
* call to actions_do () .
* @ param $a1
* Parameter that will be passed along to the callback .
* @ param $a2
* Parameter that will be passed along to the callback .
*
* @ return
* An associative array containing the result of the function that
* performs the action , keyed on action ID .
*/
function actions_do ( $action_ids , $object = NULL , $context = NULL , $a1 = NULL , $a2 = NULL ) {
2008-10-02 02:06:00 +00:00
// $stack tracks the number of recursive calls.
2008-06-29 12:07:15 +00:00
static $stack ;
$stack ++ ;
if ( $stack > variable_get ( 'actions_max_stack' , 35 )) {
watchdog ( 'actions' , 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.' , array (), WATCHDOG_ERROR );
return ;
}
$actions = array ();
$available_actions = actions_list ();
$result = array ();
if ( is_array ( $action_ids )) {
2008-10-10 08:49:51 +00:00
$conditions = array ();
2008-06-29 12:07:15 +00:00
foreach ( $action_ids as $action_id ) {
if ( is_numeric ( $action_id )) {
2008-10-10 08:49:51 +00:00
$conditions [] = $action_id ;
2008-06-29 12:07:15 +00:00
}
elseif ( isset ( $available_actions [ $action_id ])) {
$actions [ $action_id ] = $available_actions [ $action_id ];
}
}
// When we have action instances we must go to the database to
// retrieve instance data.
2008-10-10 08:49:51 +00:00
if ( ! empty ( $conditions )) {
$query = db_select ( 'actions' );
$query -> addField ( 'actions' , 'aid' );
$query -> addField ( 'actions' , 'type' );
$query -> addField ( 'actions' , 'callback' );
$query -> addField ( 'actions' , 'parameters' );
$query -> condition ( 'aid' , $conditions , 'IN' );
$result = $query -> execute ();
foreach ( $result as $action ) {
2008-06-29 12:07:15 +00:00
$actions [ $action -> aid ] = $action -> parameters ? unserialize ( $action -> parameters ) : array ();
$actions [ $action -> aid ][ 'callback' ] = $action -> callback ;
$actions [ $action -> aid ][ 'type' ] = $action -> type ;
}
}
// Fire actions, in no particular order.
foreach ( $actions as $action_id => $params ) {
if ( is_numeric ( $action_id )) { // Configurable actions need parameters.
$function = $params [ 'callback' ];
$context = array_merge ( $context , $params );
$result [ $action_id ] = $function ( $object , $context , $a1 , $a2 );
}
// Singleton action; $action_id is the function name.
else {
$result [ $action_id ] = $action_id ( $object , $context , $a1 , $a2 );
}
}
}
// Optimized execution of single action.
else {
// If it's a configurable action, retrieve stored parameters.
if ( is_numeric ( $action_ids )) {
2008-10-10 08:49:51 +00:00
$action = db_query ( " SELECT callback, parameters FROM { actions} WHERE aid = :aid " , array ( ':aid' => $action_ids )) -> fetchObject ();
2008-06-29 12:07:15 +00:00
$function = $action -> callback ;
$context = array_merge ( $context , unserialize ( $action -> parameters ));
$result [ $action_ids ] = $function ( $object , $context , $a1 , $a2 );
}
// Singleton action; $action_ids is the function name.
else {
$result [ $action_ids ] = $action_ids ( $object , $context , $a1 , $a2 );
}
}
2008-10-02 02:06:00 +00:00
$stack -- ;
2008-06-29 12:07:15 +00:00
return $result ;
}
/**
* Discover all action functions by invoking hook_action_info () .
*
* mymodule_action_info () {
* return array (
* 'mymodule_functiondescription_action' => array (
* 'type' => 'node' ,
* 'description' => t ( 'Save node' ),
* 'configurable' => FALSE ,
* 'hooks' => array (
* 'nodeapi' => array ( 'delete' , 'insert' , 'update' , 'view' ),
* 'comment' => array ( 'delete' , 'insert' , 'update' , 'view' ),
* )
* )
* );
* }
*
* The description is used in presenting possible actions to the user for
* configuration . The type is used to present these actions in a logical
* grouping and to denote context . Some types are 'node' , 'user' , 'comment' ,
* and 'system' . If an action is configurable it will provide form ,
* validation and submission functions . The hooks the action supports
* are declared in the 'hooks' array .
*
* @ param $reset
* Reset the action info static cache .
*
* @ return
* An associative array keyed on function name . The value of each key is
* an array containing information about the action , such as type of
* action and description of the action , e . g . ,
*
* @ code
* $actions [ 'node_publish_action' ] = array (
* 'type' => 'node' ,
* 'description' => t ( 'Publish post' ),
* 'configurable' => FALSE ,
* 'hooks' => array (
* 'nodeapi' => array ( 'presave' , 'insert' , 'update' , 'view' ),
* 'comment' => array ( 'delete' , 'insert' , 'update' , 'view' ),
* ),
* );
* @ endcode
*/
function actions_list ( $reset = FALSE ) {
static $actions ;
if ( ! isset ( $actions ) || $reset ) {
$actions = module_invoke_all ( 'action_info' );
drupal_alter ( 'action_info' , $actions );
}
// See module_implements for explanations of this cast.
return ( array ) $actions ;
}
/**
* Retrieve all action instances from the database .
*
* Compare with actions_list () which gathers actions by
* invoking hook_action_info () . The two are synchronized
* by visiting / admin / build / actions ( when actions . module is
* enabled ) which runs actions_synchronize () .
*
* @ return
* Associative array keyed by action ID . Each value is
* an associative array with keys 'callback' , 'description' ,
* 'type' and 'configurable' .
*/
function actions_get_all_actions () {
2008-10-10 08:49:51 +00:00
$actions = db_query ( " SELECT aid, type, callback, parameters, description FROM { actions} " ) -> fetchAllAssoc ( 'aid' , PDO :: FETCH_ASSOC );
foreach ( $actions as & $action ) {
$action [ 'configurable' ] = ( bool ) $action [ 'parameters' ];
unset ( $action [ 'parameters' ]);
unset ( $action [ 'aid' ]);
2007-06-29 18:06:51 +00:00
}
2008-06-29 12:07:15 +00:00
return $actions ;
}
/**
* Create an associative array keyed by md5 hashes of function names .
*
* Hashes are used to prevent actual function names from going out into
* HTML forms and coming back .
*
* @ param $actions
* An associative array with function names as keys and associative
* arrays with keys 'description' , 'type' , etc . as values . Generally
* the output of actions_list () or actions_get_all_actions () is given
* as input to this function .
*
* @ return
* An associative array keyed on md5 hash of function name . The value of
* each key is an associative array of function , description , and type
* for the action .
*/
function actions_actions_map ( $actions ) {
$actions_map = array ();
foreach ( $actions as $callback => $array ) {
$key = md5 ( $callback );
$actions_map [ $key ][ 'callback' ] = isset ( $array [ 'callback' ]) ? $array [ 'callback' ] : $callback ;
$actions_map [ $key ][ 'description' ] = $array [ 'description' ];
$actions_map [ $key ][ 'type' ] = $array [ 'type' ];
$actions_map [ $key ][ 'configurable' ] = $array [ 'configurable' ];
}
return $actions_map ;
}
/**
* Given an md5 hash of a function name , return the function name .
*
* Faster than actions_actions_map () when you only need the function name .
*
* @ param $hash
* MD5 hash of a function name
*
* @ return
* Function name
*/
function actions_function_lookup ( $hash ) {
$actions_list = actions_list ();
foreach ( $actions_list as $function => $array ) {
if ( md5 ( $function ) == $hash ) {
return $function ;
}
}
2007-06-29 18:06:51 +00:00
2008-06-29 12:07:15 +00:00
// Must be an instance; must check database.
2008-10-10 08:49:51 +00:00
return db_query ( " SELECT aid FROM { actions} WHERE MD5(aid) = :hash AND parameters <> '' " , array ( ':hash' => $hash )) -> fetchField ();
2008-06-29 12:07:15 +00:00
}
/**
* Synchronize actions that are provided by modules .
*
* They are synchronized with actions that are stored in the actions table .
* This is necessary so that actions that do not require configuration can
* receive action IDs . This is not necessarily the best approach ,
* but it is the most straightforward .
*/
function actions_synchronize ( $actions_in_code = array (), $delete_orphans = FALSE ) {
if ( ! $actions_in_code ) {
2008-09-15 16:07:47 +00:00
$actions_in_code = actions_list ( TRUE );
2008-06-29 12:07:15 +00:00
}
2008-10-10 08:49:51 +00:00
$actions_in_db = db_query ( " SELECT aid, callback, description FROM { actions} WHERE parameters = '' " ) -> fetchAllAssoc ( 'callback' , PDO :: FETCH_ASSOC );
2008-06-29 12:07:15 +00:00
// Go through all the actions provided by modules.
foreach ( $actions_in_code as $callback => $array ) {
// Ignore configurable actions since their instances get put in
// when the user adds the action.
if ( ! $array [ 'configurable' ]) {
// If we already have an action ID for this action, no need to assign aid.
if ( array_key_exists ( $callback , $actions_in_db )) {
unset ( $actions_in_db [ $callback ]);
}
else {
// This is a new singleton that we don't have an aid for; assign one.
2008-10-10 08:49:51 +00:00
db_insert ( 'actions' )
-> fields ( array (
'aid' => $callback ,
'type' => $array [ 'type' ],
'callback' => $callback ,
'parameters' => '' ,
'description' => $array [ 'description' ],
))
-> execute ();
2008-06-29 12:07:15 +00:00
watchdog ( 'actions' , " Action '%action' added. " , array ( '%action' => filter_xss_admin ( $array [ 'description' ])));
}
}
}
// Any actions that we have left in $actions_in_db are orphaned.
if ( $actions_in_db ) {
2008-09-26 16:13:37 +00:00
$orphaned = array_keys ( $actions_in_db );
2008-06-29 12:07:15 +00:00
if ( $delete_orphans ) {
2008-10-10 08:49:51 +00:00
$results = db_select ( 'actions' )
-> addField ( 'actions' , 'aid' )
-> addField ( 'actions' , 'description' )
-> condition ( 'callback' , $orphaned , 'IN' )
-> execute ();
foreach ( $results as $action ) {
2008-06-29 12:07:15 +00:00
actions_delete ( $action -> aid );
watchdog ( 'actions' , " Removed orphaned action '%action' from database. " , array ( '%action' => filter_xss_admin ( $action -> description )));
}
}
else {
2008-10-10 06:10:14 +00:00
$link = l ( t ( 'Remove orphaned actions' ), 'admin/settings/actions/orphan' );
2008-06-29 12:07:15 +00:00
$count = count ( $actions_in_db );
2008-09-26 16:13:37 +00:00
$orphans = implode ( ', ' , $orphaned );
2008-08-11 06:57:13 +00:00
watchdog ( 'actions' , format_plural ( $count , 'One orphaned action (%orphans) exists in the actions table. !link' , '@count orphaned actions (%orphans) exist in the actions table. !link' ), array ( '@count' => $count , '%orphans' => $orphans , '!link' => $link ), WATCHDOG_WARNING );
2008-06-29 12:07:15 +00:00
}
2007-08-29 14:57:50 +00:00
}
}
2008-06-29 12:07:15 +00:00
/**
* Save an action and its associated user - supplied parameter values to the database .
*
* @ param $function
* The name of the function to be called when this action is performed .
* @ param $params
* An associative array with parameter names as keys and parameter values
* as values .
* @ param $desc
* A user - supplied description of this particular action , e . g . , ' Send
* e - mail to Jim ' .
* @ param $aid
* The ID of this action . If omitted , a new action is created .
*
* @ return
* The ID of the action .
*/
function actions_save ( $function , $type , $params , $desc , $aid = NULL ) {
2008-10-10 08:49:51 +00:00
// aid is the callback for singleton actions so we need to keep a
// separate table for numeric aids.
if ( ! $aid ) {
2008-11-08 07:28:03 +00:00
$aid = db_insert ( 'actions_aid' ) -> useDefaults ( array ( 'aid' )) -> execute ();
2008-06-29 12:07:15 +00:00
}
2008-10-10 08:49:51 +00:00
db_merge ( 'actions' )
-> key ( array ( 'aid' => $aid ))
-> fields ( array (
'callback' => $function ,
'type' => $type ,
'parameters' => serialize ( $params ),
'description' => $desc ,
))
-> execute ();
watchdog ( 'actions' , 'Action %action saved.' , array ( '%action' => $desc ));
2008-06-29 12:07:15 +00:00
return $aid ;
}
/**
* Retrieve a single action from the database .
*
* @ param $aid
* integer The ID of the action to retrieve .
*
* @ return
* The appropriate action row from the database as an object .
*/
function actions_load ( $aid ) {
2008-10-10 08:49:51 +00:00
return db_query ( " SELECT aid, type, callback, parameters, description FROM { actions} WHERE aid = :aid " , array ( ':aid' => $aid )) -> fetchObject ();
2008-06-29 12:07:15 +00:00
}
/**
* Delete a single action from the database .
*
* @ param $aid
* integer The ID of the action to delete .
*/
function actions_delete ( $aid ) {
2008-10-10 08:49:51 +00:00
db_delete ( 'actions' )
-> condition ( 'aid' , $aid )
-> execute ();
2008-06-29 12:07:15 +00:00
module_invoke_all ( 'actions_delete' , $aid );
}