'. $explanation .' '. t('Below you can assign actions to run when certain comment-related operations happen. For example, you could promote a post to the front page when a comment is added.') .'
';
break;
case 'admin/build/actions/assign/node':
$output = ''. $explanation .' '. t('Below you can assign actions to run when certain post-related operations happen. For example, you could remove a post from the front page when the post is updated. Note that if you are running actions that modify the characteristics of a post (such as making a post sticky or removing a post from the front page), you will need to add the %node_save action to save the changes.', array('%node_save' => t('Save post'))) .'
';
break;
case 'admin/build/actions/assign/user':
$output = ''. $explanation .' '. t("Below you can assign actions to run when certain user-related operations happen. For example, you could block a user when the user's account is edited by assigning the %block_user action to the user %update operation.", array('%block_user' => t('Block user'), '%update' => t('update'))) .'
';
break;
case 'admin/build/actions/assign/cron':
$output = ''. t('Actions are functions that Drupal can execute when certain events happen, such as when a post is added or a user logs in. Below you can assign actions to run when cron runs.') .'
';
break;
}
return $output;
}
/**
* Implementation of hook_menu().
*/
function actions_menu() {
$items['admin/build/actions/assign'] = array(
'title' => 'Assign actions',
'description' => 'Tell Drupal when to execute actions.',
'page callback' => 'actions_assign',
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/actions/assign/node'] = array(
'title' => 'Content',
'page callback' => 'actions_assign',
'page arguments' => array('node'),
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/actions/assign/user'] = array(
'title' => 'User',
'page callback' => 'actions_assign',
'page arguments' => array('user'),
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/actions/assign/comment'] = array(
'title' => 'Comment',
'page callback' => 'actions_assign',
'page arguments' => array('comment'),
'access callback' => 'actions_access_check',
'access arguments' => array('comment'),
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/actions/assign/taxonomy'] = array(
'title' => 'Taxonomy',
'page callback' => 'actions_assign',
'page arguments' => array('taxonomy'),
'access callback' => 'actions_access_check',
'access arguments' => array('taxonomy'),
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/actions/assign/cron'] = array(
'title' => 'Cron',
'page callback' => 'actions_assign',
'page arguments' => array('cron'),
'type' => MENU_LOCAL_TASK,
);
// We want contributed modules to be able to describe
// their hooks and have actions assignable to them.
$hooks = module_invoke_all('hook_info');
foreach ($hooks as $module => $hook) {
if (in_array($module, array('node', 'comment', 'user', 'system', 'taxonomy'))) {
continue;
}
$info = db_result(db_query("SELECT info FROM {system} WHERE name = '%s'", $module));
$info = unserialize($info);
$nice_name = $info['name'];
$items["admin/build/actions/assign/$module"] = array(
'title' => $nice_name,
'page callback' => 'actions_assign',
'page arguments' => array($module),
'type' => MENU_LOCAL_TASK,
);
}
$items['admin/build/actions/assign/remove'] = array(
'title' => 'Unassign',
'description' => 'Remove an action assignment.',
'page callback' => 'drupal_get_form',
'page arguments' => array('actions_unassign'),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Access callback for menu system.
*/
function actions_access_check($module) {
return (module_exists($module) && user_access('administer actions'));
}
/**
* Get the actions that have already been defined for this
* type-hook-op combination.
*
* @param $type
* One of 'node', 'user', 'comment'.
* @param $hook
* The name of the hook for which actions have been assigned,
* e.g. 'nodeapi'.
* @param $op
* The hook operation for which the actions have been assigned,
* e.g., 'view'.
* @return
* An array of action descriptions keyed by action IDs.
*/
function _actions_get_hook_actions($hook, $op, $type = NULL) {
$actions = array();
if ($type) {
$result = db_query("SELECT h.aid, a.description FROM {actions_assignments} h LEFT JOIN {actions} a on a.aid = h.aid WHERE a.type = '%s' AND h.hook = '%s' AND h.op = '%s' ORDER BY h.weight", $type, $hook, $op);
}
else {
$result = db_query("SELECT h.aid, a.description FROM {actions_assignments} h LEFT JOIN {actions} a on a.aid = h.aid WHERE h.hook = '%s' AND h.op = '%s' ORDER BY h.weight", $hook, $op);
}
while ($action = db_fetch_object($result)) {
$actions[$action->aid] = $action->description;
}
return $actions;
}
/**
* Get the aids of actions to be executed for a hook-op combination.
*
* @param $hook
* The name of the hook being fired.
* @param $op
* The name of the operation being executed. Defaults to an empty string
* because some hooks (e.g., hook_cron()) do not have operations.
* @return
* An array of action IDs.
*/
function _actions_get_hook_aids($hook, $op = '') {
$aids = array();
$result = db_query("SELECT aa.aid, a.type FROM {actions_assignments} aa LEFT JOIN {actions} a ON aa.aid = a.aid WHERE aa.hook = '%s' AND aa.op = '%s' ORDER BY weight", $hook, $op);
while ($action = db_fetch_object($result)) {
$aids[$action->aid]['type'] = $action->type;
}
return $aids;
}
/**
* Create the form definition for assigning an action to a hook-op combination.
*
* @param $form_state
* Information about the current form.
* @param $hook
* The name of the hook, e.g., 'nodeapi'.
* @param $op
* The name of the hook operation, e.g., 'insert'.
* @param $description
* A plain English description of what this hook operation does.
* @return
*/
function actions_assign_form($form_state, $hook, $op, $description) {
$form['hook'] = array(
'#type' => 'hidden',
'#value' => $hook,
);
$form['operation'] = array(
'#type' => 'hidden',
'#value' => $op,
);
// All of these forms use the same #submit function.
$form['#submit'][] = 'actions_assign_form_submit';
$options = array();
$functions = array();
// Restrict the options list to actions that declare support for this hook-op combination.
foreach (actions_list() as $func => $metadata) {
if (isset($metadata['hooks']['any']) || (isset($metadata['hooks'][$hook]) && is_array($metadata['hooks'][$hook]) && (in_array($op, $metadata['hooks'][$hook])))) {
$functions[] = $func;
}
}
foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
if (in_array($action['callback'], $functions)) {
$options[$action['type']][$aid] = $action['description'];
}
}
$form[$op] = array(
'#type' => 'fieldset',
'#title' => $description,
'#theme' => 'actions_display'
);
// Retrieve actions that are already assigned to this hook-op combination.
$actions = _actions_get_hook_actions($hook, $op);
$form[$op]['assigned']['#type'] = 'value';
$form[$op]['assigned']['#value'] = array();
foreach ($actions as $aid => $description) {
$form[$op]['assigned']['#value'][$aid] = array(
'description' => $description,
'link' => l(t('remove'), "admin/build/actions/assign/remove/$hook/$op/". md5($aid))
);
}
$form[$op]['parent'] = array(
'#prefix' => "",
'#suffix' => '
',
);
// List possible actions that may be assigned.
if (count($options) != 0) {
array_unshift($options, t('Choose an action'));
$form[$op]['parent']['aid'] = array(
'#type' => 'select',
'#options' => $options,
);
$form[$op]['parent']['submit'] = array(
'#type' => 'submit',
'#value' => t('Assign')
);
}
else {
$form[$op]['none'] = array(
'#value' => t('No available actions support this context.')
);
}
return $form;
}
function actions_assign_form_submit($form, $form_state) {
$form_values = $form_state['values'];
if (!empty($form_values['aid'])) {
$aid = actions_function_lookup($form_values['aid']);
$weight = db_result(db_query("SELECT MAX(weight) FROM {actions_assignments} WHERE hook = '%s' AND op = '%s'", $form_values['hook'], $form_values['operation']));
db_query("INSERT INTO {actions_assignments} values ('%s', '%s', '%s', %d)", $form_values['hook'], $form_values['operation'], $aid, $weight + 1);
}
}
/**
* Implementation of hook_theme().
*/
function actions_theme() {
return array(
'actions_display' => array(
'arguments' => array('element')
),
);
}
/**
* Display actions assigned to this hook-op combination in a table.
*
* @param array $element
* The fieldset including all assigned actions.
* @return
* The rendered form with the table prepended.
*/
function theme_actions_display($element) {
$header = array();
$rows = array();
if (count($element['assigned']['#value'])) {
$header = array(array('data' => t('Name')), array('data' => t('Operation')));
$rows = array();
foreach ($element['assigned']['#value'] as $aid => $info) {
$rows[] = array(
$info['description'],
$info['link']
);
}
}
$output = theme('table', $header, $rows) . drupal_render($element);
return $output;
}
/**
* Implementation of hook_forms(). We reuse code by using the
* same assignment form definition for each node-op combination.
*/
function actions_forms() {
$hooks = module_invoke_all('hook_info');
foreach ($hooks as $module => $info) {
foreach ($hooks[$module] as $hook => $ops) {
foreach ($ops as $op => $description) {
$forms['actions_'. $hook .'_'. $op .'_assign_form'] = array('callback' => 'actions_assign_form');
}
}
}
return $forms;
}
/**
* Build the form that allows users to assign actions to hooks.
*
* @param $type
* Name of hook.
* @return
* HTML form.
*/
function actions_assign($type = NULL) {
// If no type is specified we default to node actions, since they
// are the most common.
if (!isset($type)) {
drupal_goto('admin/build/actions/assign/node');
}
if ($type == 'node') {
$type = 'nodeapi';
}
$output = '';
$hooks = module_invoke_all('hook_info');
foreach ($hooks as $module => $hook) {
if (isset($hook[$type])) {
foreach ($hook[$type] as $op => $description) {
$form_id = 'actions_'. $type .'_'. $op .'_assign_form';
$output .= drupal_get_form($form_id, $type, $op, $description['runs when']);
}
}
}
return $output;
}
/**
* Confirm removal of an assigned action.
*
* @param $hook
* @param $op
* @param $aid
* The action ID.
*/
function actions_unassign($form_state, $hook = NULL, $op = NULL, $aid = NULL) {
if (!($hook && $op && $aid)) {
drupal_goto('admin/build/actions/assign');
}
$form['hook'] = array(
'#type' => 'value',
'#value' => $hook,
);
$form['operation'] = array(
'#type' => 'value',
'#value' => $op,
);
$form['aid'] = array(
'#type' => 'value',
'#value' => $aid,
);
$action = actions_function_lookup($aid);
$actions = actions_get_all_actions();
$destination = 'admin/build/actions/assign/'. ($hook == 'nodeapi' ? 'node' : $hook);
return confirm_form($form,
t('Are you sure you want to remove the action %title?', array('%title' => $actions[$action]['description'])),
$destination,
t('You can assign it again later if you wish.'),
t('Remove'), t('Cancel')
);
}
function actions_unassign_submit($form, &$form_state) {
$form_values = $form_state['values'];
if ($form_values['confirm'] == 1) {
$aid = actions_function_lookup($form_values['aid']);
db_query("DELETE FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = '%s'", $form_values['hook'], $form_values['operation'], $aid);
$actions = actions_get_all_actions();
watchdog('actions', 'Action %action has been unassigned.', array('%action' => check_plain($actions[$aid]['description'])));
drupal_set_message(t('Action %action has been unassigned.', array('%action' => $actions[$aid]['description'])));
$hook = $form_values['hook'] == 'nodeapi' ? 'node' : $form_values['hook'];
$form_state['redirect'] = 'admin/build/actions/assign/'. $hook;
}
else {
drupal_goto('admin/build/actions');
}
}
/**
* When an action is called in a context that does not match its type,
* the object that the action expects must be retrieved. For example, when
* an action that works on users is called during the node hook, the
* user object is not available since the node hook doesn't pass it.
* So here we load the object the action expects.
*
* @param $type
* The type of action that is about to be called.
* @param $node
* The node that was passed via the nodeapi hook.
* @return
* The object expected by the action that is about to be called.
*/
function _actions_normalize_node_context($type, $node) {
switch ($type) {
// If an action that works on comments is being called in a node context,
// the action is expecting a comment object. But we do not know which comment
// to give it. The first? The most recent? All of them? So comment actions
// in a node context are not supported.
// An action that works on users is being called in a node context.
// Load the user object of the node's author.
case 'user':
return user_load(array('uid' => $node->uid));
}
}
/**
* Implementation of hook_nodeapi().
*/
function actions_nodeapi($node, $op, $a3, $a4) {
// Keep objects for reuse so that changes actions make to objects can persist.
static $objects;
// Prevent recursion by tracking which operations have already been called.
static $recursion;
// Support a subset of operations.
if (!in_array($op, array('view', 'update', 'presave', 'insert', 'delete')) || isset($recursion[$op])) {
return;
}
$recursion[$op] = TRUE;
$aids = _actions_get_hook_aids('nodeapi', $op);
if (!$aids) {
return;
}
$context = array(
'hook' => 'nodeapi',
'op' => $op,
);
// We need to get the expected object if the action's type is not 'node'.
// We keep the object in $objects so we can reuse it if we have multiple actions
// that make changes to an object.
foreach ($aids as $aid => $action_info) {
if ($action_info['type'] != 'node') {
if (!isset($objects[$action_info['type']])) {
$objects[$action_info['type']] = _actions_normalize_node_context($action_info['type'], $node);
}
// Since we know about the node, we pass that info along to the action.
$context['node'] = $node;
$result = actions_do($aid, $objects[$action_info['type']], $context, $a4, $a4);
}
else {
actions_do($aid, $node, $context, $a3, $a4);
}
}
}
/**
* When an action is called in a context that does not match its type,
* the object that the action expects must be retrieved. For example, when
* an action that works on nodes is called during the comment hook, the
* node object is not available since the comment hook doesn't pass it.
* So here we load the object the action expects.
*
* @param $type
* The type of action that is about to be called.
* @param $comment
* The comment that was passed via the comment hook.
* @return
* The object expected by the action that is about to be called.
*/
function _actions_normalize_comment_context($type, $comment) {
switch ($type) {
// An action that works with nodes is being called in a comment context.
case 'node':
return node_load(is_array($comment) ? $comment['nid'] : $comment->nid);
// An action that works on users is being called in a comment context.
case 'user':
return user_load(array('uid' => is_array($comment) ? $comment['uid'] : $comment->uid));
}
}
/**
* Implementation of hook_comment().
*/
function actions_comment($a1, $op) {
// Keep objects for reuse so that changes actions make to objects can persist.
static $objects;
// We support a subset of operations.
if (!in_array($op, array('insert', 'update', 'delete', 'view'))) {
return;
}
$aids = _actions_get_hook_aids('comment', $op);
$context = array(
'hook' => 'comment',
'op' => $op,
);
// We need to get the expected object if the action's type is not 'comment'.
// We keep the object in $objects so we can reuse it if we have multiple actions
// that make changes to an object.
foreach ($aids as $aid => $action_info) {
if ($action_info['type'] != 'comment') {
if (!isset($objects[$action_info['type']])) {
$objects[$action_info['type']] = _actions_normalize_comment_context($action_info['type'], $a1);
}
// Since we know about the comment, we pass it along to the action
// in case it wants to peek at it.
$context['comment'] = (object) $a1;
actions_do($aid, $objects[$action_info['type']], $context);
}
else {
actions_do($aid, (object) $a1, $context);
}
}
}
/**
* Implementation of hook_cron().
*/
function actions_cron() {
$aids = _actions_get_hook_aids('cron');
$context = array(
'hook' => 'cron',
'op' => '',
);
// Cron does not act on any specific object.
$object = NULL;
actions_do(array_keys($aids), $object, $context);
}
/**
* When an action is called in a context that does not match its type,
* the object that the action expects must be retrieved. For example, when
* an action that works on nodes is called during the user hook, the
* node object is not available since the user hook doesn't pass it.
* So here we load the object the action expects.
*
* @param $type
* The type of action that is about to be called.
* @param $account
* The account object that was passed via the user hook.
* @return
* The object expected by the action that is about to be called.
*/
function _actions_normalize_user_context($type, $account) {
switch ($type) {
// If an action that works on comments is being called in a user context,
// the action is expecting a comment object. But we have no way of
// determining the appropriate comment object to pass. So comment
// actions in a user context are not supported.
// An action that works with nodes is being called in a user context.
// If a single node is being viewed, return the node.
case 'node':
// If we are viewing an individual node, return the node.
if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
return node_load(array('nid' => arg(1)));
}
}
}
/**
* Implementation of hook_user().
*/
function actions_user($op, &$edit, &$account, $category = NULL) {
// Keep objects for reuse so that changes actions make to objects can persist.
static $objects;
// We support a subset of operations.
if (!in_array($op, array('login', 'logout', 'insert', 'update', 'delete', 'view'))) {
return;
}
$aids = _actions_get_hook_aids('user', $op);
$context = array(
'hook' => 'user',
'op' => $op,
'form_values' => &$edit,
);
foreach ($aids as $aid => $action_info) {
if ($action_info['type'] != 'user') {
if (!isset($objects[$action_info['type']])) {
$objects[$action_info['type']] = _actions_normalize_user_context($action_info['type'], $account);
}
$context['account'] = $account;
actions_do($aid, $objects[$action_info['type']], $context);
}
else {
actions_do($aid, $account, $context, $category);
}
}
}
/**
* Implementation of hook_taxonomy().
*/
function actions_taxonomy($op, $type, $array) {
if ($type != 'term') {
return;
}
$aids = _actions_get_hook_aids('taxonomy', $op);
$context = array(
'hook' => 'taxonomy',
'op' => $op
);
foreach ($aids as $aid => $action_info) {
actions_do($aid, (object) $array, $context);
}
}
/**
* Often we generate a select field of all actions. This function
* generates the options for that select.
*
* @param $type
* One of 'node', 'user', 'comment'.
* @return
* Array keyed by action ID.
*/
function actions_options($type = 'all') {
$options = array(t('Choose an action'));
foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
$options[$action['type']][$aid] = $action['description'];
}
if ($type == 'all') {
return $options;
}
else {
return $options[$type];
}
}