'. $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]; } }