'Conditional actions', 'description' => 'Administer the predicates setup to automate your store.', 'page callback' => 'ca_admin', 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', ); $items['admin/settings/ca/overview'] = array( 'title' => 'Overview', 'weight' => 0, 'type' => MENU_DEFAULT_LOCAL_TASK, ); /* $items['admin/settings/ca/convert'] = array( 'title' => 'Convert configurations', 'description' => 'Translate Workflow-ng configurations into Conditional Actions predicates.', 'page callback' => 'ca_conversion_page', 'access arguments' => array('administer conditional actions'), 'type' => MENU_LOCAL_TASK, 'file' => 'ca.admin.inc', ); */ $items['admin/settings/ca/add'] = array( 'title' => 'Add a predicate', 'description' => 'Allows an administrator to create a new predicate.', 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_predicate_meta_form', '0'), 'access arguments' => array('administer conditional actions'), 'type' => MENU_LOCAL_TASK, 'weight' => 5, 'file' => 'ca.admin.inc', ); $items['admin/settings/ca/%/edit'] = array( 'title' => 'Edit predicate', 'description' => "Edit a predicate's meta data, conditions, and actions.", 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_predicate_meta_form', 3), 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/settings/ca/%/edit/meta'] = array( 'title' => 'Meta data', 'description' => 'Edit the meta data for a predicate like title, trigger, etc.', 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_predicate_meta_form', 3), 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/settings/ca/%/edit/conditions'] = array( 'title' => 'Conditions', 'description' => 'Edit the conditions for a predicate.', 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_conditions_form', 3), 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', 'type' => MENU_LOCAL_TASK, 'weight' => -5, ); $items['admin/settings/ca/%/edit/actions'] = array( 'title' => 'Actions', 'description' => 'Edit the actions for a predicate.', 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_actions_form', 3), 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 0, ); $items['admin/settings/ca/%/reset'] = array( 'title' => 'Reset a predicate', 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_predicate_delete_form', 3), 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/settings/ca/%/delete'] = array( 'title' => 'Delete a predicate', 'page callback' => 'drupal_get_form', 'page arguments' => array('ca_predicate_delete_form', 3), 'access arguments' => array('administer conditional actions'), 'file' => 'ca.admin.inc', 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_perm(). */ function ca_perm() { return array('administer conditional actions'); } /******************************************************************************* * Module and Helper Functions ******************************************************************************/ /** * Pulls a trigger and evaluates any predicates associated with that trigger. * * @param ... * Accepts a variable number of arguments. The first should always be the * string name of the trigger to pull with any additional arguments being * the arguments expected by the trigger and used for evaluation. * @return * TRUE or FALSE indicating if at least one predicate was evaluated. */ function ca_pull_trigger() { $args = func_get_args(); $trigger = array_shift($args); // Load the data for the specified trigger. $trigger_data = ca_load_trigger($trigger); // Fail if the specified trigger doesn't exist. if (!$trigger_data) { return FALSE; } // Load any predicates associated with the trigger. $predicates = ca_load_trigger_predicates($trigger); // Fail if we didn't find any predicates. if (!$predicates || count($predicates) == 0) { return FALSE; } // Prepare the arguments for evaluation. $arguments = ca_parse_trigger_args($trigger_data, $args); // Fail if we didn't receive the right type of or enough arguments. if (!$arguments) { return FALSE; } // Loop through the predicates and evaluate them one by one. foreach ($predicates as $pid => $predicate) { // If all of a predicate's conditions evaluate to TRUE... if (ca_evaluate_conditions($predicate, $arguments)) { // Then perform its actions. ca_perform_actions($predicate, $arguments); } } return TRUE; } /** * Parses the argument array into a CA friendly array for the trigger. * * @param $trigger * The name of the trigger for which we are parsing the arguments. * @param $args * An array of arguments to check against the expected arguments. * @return * The array of arguments keyed according to the trigger's argument names. */ function ca_parse_trigger_args($trigger, $args) { // Fail if we didn't receive enough arguments for this trigger. if (count($args) < count($trigger['#arguments'])) { return FALSE; } // Load all the entity information. $entities = module_invoke_all('ca_entity'); // Loop through the expected arguments. foreach ($trigger['#arguments'] as $key => $value) { // Grab for comparison the next argument passed to the trigger. $arg = array_shift($args); // Check the type and fail if it is incorrect. if (gettype($arg) != $entities[$value['#entity']]['#type']) { return FALSE; } // Add the entity to the arguments array along with its meta data. $arguments[$key] = array( '#entity' => $value['#entity'], '#title' => $value['#title'], '#data' => $arg, ); } return $arguments; } /** * Loads predicates based on the specified parameters. * * @param $trigger * The name of the trigger for which to search when loading the predicates. * @param $all * FALSE by default, specifies whether we want to load all possible predicates * or only those that are active (status > 0). * @return * An array of predicates. */ function ca_load_trigger_predicates($trigger, $all = FALSE) { // Trigger actions can pull other triggers. Lock predicates when they are // marked for evaluation so they can't be evaluated again. static $locked = array(); // Load all the module defined predicates. $predicates = module_invoke_all('ca_predicate'); // Loop through the module defined predicates to prepare the data - unsets // inactive predicates if $all == FALSE and adds a default weight if need be. foreach ($predicates as $key => $value) { // Unset the predicate if it doesn't use the specified trigger. if ($value['#trigger'] != $trigger) { unset($predicates[$key]); continue; } if (!$all && $value['#status'] <= 0) { unset($predicates[$key]); } else { // Prevent predicates from being evaluated more than once in a page load. if (in_array($key, $locked)) { unset($predicates[$key]); } else { $locked[$key] = $key; } if (!isset($value['#weight'])) { $predicates[$key]['#weight'] = 0; } } } // Load and loop through the predicates from the database for this trigger. $result = db_query("SELECT * FROM {ca_predicates} WHERE ca_trigger = '%s'", $trigger); while ($data = db_fetch_array($result)) { // Module defined predicates have string IDs. When a user modifies one of // these, we unset the module defined predicate and reconsider adding it in. if (!is_numeric($data['pid'])) { unset($predicates[$data['pid']]); unset($locked[$data['pid']]); } // Add predicates from the database to our return array if $all == TRUE or // if the predicate is active. if ($all || $data['status'] > 0) { // Prevent predicates from being evaluated more than once in a page load. if (!in_array($data['pid'], $locked)) { $predicates[$data['pid']] = ca_prepare_db_predicate($data); $locked[$key] = $data['pid']; } } } uasort($predicates, 'ca_weight_sort'); return $predicates; } /** * Prepares predicate data from the database into a full predicate array. * * @param $data * An array of data representing a row in the predicates table. * @return * A predicate array. */ function ca_prepare_db_predicate($data) { $predicate = array(); foreach ($data as $key => $value) { switch ($key) { // Condition and action data needs to be unserialized. case 'conditions': case 'actions': $predicate['#'. $key] = unserialize($value); break; case 'ca_trigger': $predicate['#trigger'] = $value; break; default: $predicate['#'. $key] = $value; break; } } return $predicate; } /** * Evaluates a predicate's conditions. * * @param $predicate * A fully loaded predicate array. * @param $arguments * The array of parsed arguments for the trigger. * @return * TRUE or FALSE indicating the success or failure of the evaluation. */ function ca_evaluate_conditions($predicate, $arguments) { // Automatically pass if there are no conditions. if (count($predicate['#conditions']) == 0) { return TRUE; } // Load the data for the conditions as defined by modules. $condition_data = module_invoke_all('ca_condition'); // Recurse through the predicate's conditions for evaluation. $result = _ca_evaluate_conditions_tree($predicate['#conditions'], $arguments, $condition_data); if (is_null($result)) { $result = FALSE; } return $result; } // Recursively evaluates conditions to accommodate nested logical groups. function _ca_evaluate_conditions_tree($condition, $arguments, $condition_data) { if (isset($condition['#operator']) && is_array($condition['#conditions'])) { foreach ($condition['#conditions'] as $sub_condition) { $result = _ca_evaluate_conditions_tree($sub_condition, $arguments, $condition_data); // Invalid conditions return NULL. Skip it and go to the next one. if (is_null($result)) { continue; } // Save the processors! Apply Boolean shortcutting if we can. if ($condition['#operator'] == 'OR' && $result) { return TRUE; } elseif ($condition['#operator'] == 'AND' && !$result) { return FALSE; } } return $result; } else { return _ca_evaluate_condition($condition, $arguments, $condition_data); } } // Evaluates a single condition. function _ca_evaluate_condition($condition, $arguments, $condition_data) { $args = array(); // Make sure the condition tree is sane. if (!is_array($condition_data[$condition['#name']])) { return NULL; } // Get the callback function for the current condition. $callback = $condition_data[$condition['#name']]['#callback']; // Skip this condition if the function does not exist. if (!function_exists($callback)) { return NULL; } // Loop through the expected arguments for a condition. foreach ($condition_data[$condition['#name']]['#arguments'] as $key => $value) { // Using the argument map for the condition on this predicate, fetch the // argument that was passed to the trigger that matches to the argument // needed to evaluate this condition. if (isset($condition['#argument_map'][$key])) { $args[] = $arguments[$condition['#argument_map'][$key]]['#data']; } else { // Skip this condition of the predicate didn't map the arguments needed. return NULL; } } // Add the condition settings to the argument list. $args[] = $condition['#settings']; // Call the condition's function with the appropriate arguments. $result = call_user_func_array($callback, $args); // If the negate operator is TRUE, then switch the result! if ($condition['#settings']['negate']) { $result = !$result; } return $result; } /** * Performs a predicate's actions in order, preserving changes to the arguments. * * @param $predicate * A fully loaded predicate array. * @param $arguments * The array of parsed arguments for the trigger. */ function ca_perform_actions($predicate, $arguments) { // Exit now if we don't have any actions. if (count($predicate['#actions']) == 0) { return; } // Set the actions' weight if necessary and sort actions by their weight. for ($i = 0; $i < count($predicate['#actions']); $i++) { if (!isset($predicate['#actions'][$i]['#weight'])) { $predicate['#actions'][$i]['#weight'] = 0; } } usort($predicate['#actions'], 'ca_weight_sort'); // Load the data for the actions as defined by modules. $action_data = module_invoke_all('ca_action'); foreach ($predicate['#actions'] as $action) { $args = array(); // Get the callback function for the current action. $callback = $action_data[$action['#name']]['#callback']; // Do not perform the action if the function does not exist. if (!function_exists($callback)) { continue; } // Loop through the expected arguments for a condition. foreach ($action_data[$action['#name']]['#arguments'] as $key => $value) { // Using the argument map for the action on this predicate, fetch the // argument that was passed to the trigger that matches to the argument // needed to perform this action. if (isset($action['#argument_map'][$key])) { // Adding the arguments as references so action functions can update the // arguments here when they make changes to the argument data. $args[] = &$arguments[$action['#argument_map'][$key]]['#data']; } else { // Skip this action of the predicate didn't map the arguments needed. continue 2; } } // Add the condition settings to the argument list. $args[] = is_array($action['#settings']) ? $action['#settings'] : array(); // Call the action's function with the appropriate arguments. call_user_func_array($callback, $args); } } /** * Loads triggers defined in modules via hook_ca_trigger(). * * @param $trigger * Defaults to 'all'; may instead be the name of the trigger to load. * @return * An array of data for the specified trigger or an array of all the trigger * data if 'all' was specified. Returns FALSE if a specified trigger is * non-existent. */ function ca_load_trigger($trigger = 'all') { static $triggers; // Load the triggers defined in enabled modules to a cached variable. if (empty($triggers)) { $triggers = module_invoke_all('ca_trigger'); } // Return the whole array if the trigger specified is 'all'. if ($trigger === 'all') { return $triggers; } // If the specified trigger is non-existent, return FALSE. if (!isset($triggers[$trigger])) { return FALSE; } return $triggers[$trigger]; } /** * Returns an array of arguments received by a trigger formatted for use as FAPI * select element options. * * @param $trigger * The name of the trigger to load arguments for. * @param $entity * The name of the entity type we must restrict our returned arguments to. * @return * An array of arguments in FAPI options format. */ function ca_load_trigger_arguments($trigger, $entity) { static $arguments = array(); // Load the arguments if we can't find a cached version. if (!isset($arguments[$trigger][$entity])) { $arguments[$trigger][$entity] = array(); // Load the trigger data. $trigger_data = ca_load_trigger($trigger); // Add the trigger's arguments to the options array if the entity matches. foreach ((array) $trigger_data['#arguments'] as $key => $value) { if ($value['#entity'] == $entity) { $arguments[$trigger][$entity][$key] = $value['#title']; } } } return $arguments[$trigger][$entity]; } /** * Loads conditions defined in modules via hook_ca_condition(). * * @param $condition * Defaults to 'all'; may instead be the name of the condition to load. * @return * An array of data for the specified condition or an array of all the * condition data if 'all' was specified. Returns FALSE if a specified * condition is non-existent. */ function ca_load_condition($condition = 'all') { static $conditions; // Load the conditions defined in enabled modules to a cached variable. if (empty($conditions)) { $conditions = module_invoke_all('ca_condition'); } // Return the whole array if the trigger specified is 'all'. if ($condition === 'all') { return $conditions; } // If the specified trigger is non-existent, return FALSE. if (!isset($conditions[$condition])) { return FALSE; } return $conditions[$condition]; } /** * Returns an array of conditions available for the specified trigger. * * @param $trigger * The name of a trigger to find conditions for; if left empty, the function * returns conditions for the previously specified trigger. * @return * A nested array of names/titles for the conditions available for the * trigger; in the format required by select elements in FAPI. */ function ca_load_trigger_conditions($trigger = '') { static $options = array(); if (!empty($trigger)) { // Load the specified trigger. $trigger = ca_load_trigger($trigger); $trigger_entities = array(); // Organize trigger arguments by entity. foreach ($trigger['#arguments'] as $argument) { $trigger_entities[$argument['#entity']] = $argument; } // Load and loop through all the conditions defined by modules. $conditions = ca_load_condition(); foreach ($conditions as $name => $condition) { // Check through each argument needed for the condition. foreach ($condition['#arguments'] as $argument) { $entity = $argument['#entity']; // If the condition requires an entity the trigger doesn't provide, // then skip to the next condition. if (!$trigger_entities[$entity]) { continue 2; } } // Getting this far means that all of the condition's arguments have // the same entity types as the trigger's. Add it to the options, // and group them by category for usability. $options[$condition['#category']][$name] = $condition['#title']; } // Alphabetically sort the groups and their options. foreach ($options as $group => $conditions) { asort($conditions); $options[$group] = $conditions; } ksort($options); } return $options; } /** * Loads actions defined in modules via hook_ca_action(). * * @param $action * Defaults to 'all'; may instead be the name of the action to load. * @return * An array of data for the specified action or an array of all the action * data if 'all' was specified. Returns FALSE if a specified action is * non-existent. */ function ca_load_action($action = 'all') { static $actions; // Load the actions defined in enabled modules to a cached variable. if (empty($actions)) { $actions = module_invoke_all('ca_action'); } // Return the whole array if the trigger specified is 'all'. if ($action === 'all') { return $actions; } // If the specified trigger is non-existent, return FALSE. if (!isset($actions[$action])) { return FALSE; } return $actions[$action]; } /** * Returns an array of actions available for the specified trigger. * * @param $trigger * The name of a trigger to find actions for; if left empty, the function * returns conditions for the previously specified trigger. * @return * A nested array of names/titles for the actions available for the trigger; * in the format required by select elements in FAPI. */ function ca_load_trigger_actions($trigger = '') { static $options = array(); if (!empty($trigger)) { // Load the specified trigger. $trigger = ca_load_trigger($trigger); $trigger_entities = array(); // Organize trigger arguments by entity. foreach ($trigger['#arguments'] as $argument) { $trigger_entities[$argument['#entity']] = $argument; } // Load and loop through all the actions defined by modules. $actions = ca_load_action(); foreach ($actions as $name => $action) { // Check through each argument needed for the condition. foreach ($action['#arguments'] as $argument) { $entity = $argument['#entity']; // If the condition requires an entity the trigger doesn't provide, // then skip to the next condition. if (!$trigger_entities[$entity]) { continue 2; } } // Getting this far means that all of the condition's arguments have // the same entity types as the trigger's. Add it to the options, // and group them by category for usability. $options[$action['#category']][$name] = $action['#title']; } // Alphabetically sort the groups and their options. foreach ($options as $group => $actions) { asort($actions); $options[$group] = $actions; } ksort($options); } return $options; } // Returns a default conditions array for use on new predicates or in the UI. function ca_new_conditions() { return array( '#operator' => 'AND', '#conditions' => array( array( '#operator' => 'AND', '#conditions' => array(), ), ), ); } /** * Adds a new condition group to a predicate. * * @param $pid * The ID of the predicate to add the condition group to. * @return * An array representing the full, updated predicate. */ function ca_add_condition_group($pid) { // Load the predicate. $predicate = ca_load_predicate($pid); // Add the condition group to the conditions array in the appropriate place. if (empty($predicate['#conditions'])) { $predicate['#conditions'] = ca_new_conditions(); } else { $predicate['#conditions']['#conditions'][] = array( '#operator' => 'AND', '#conditions' => array(), ); } // Save the changes. ca_save_predicate($predicate); return $predicate; } /** * Removes a condition group from a predicate. * * @param $pid * The ID of the predicate to remove the condition group from. * @param $group_key * The key of the condition group to remove. * @return * An array representing the full, updated predicate. */ function ca_remove_condition_group($pid, $group_key) { // Load the predicate as it is now. $predicate = ca_load_predicate($pid); // Update, save, and return the predicate. unset($predicate['#conditions']['#conditions'][$group_key]); ca_save_predicate($predicate); return $predicate; } /** * Adds a new condition to a predicate. * * @param $pid * The ID of the predicate to add the condition to. * @param $name * The name of the condition to add. * @param $group_key * The key of the condition group we're adding the condition to. * @param $mark_expanded * If set to TRUE marks the condition so that it will be expanded next time it * displays in the UI so the user can adjust its settings. * @return * An array representing the full, updated predicate. */ function ca_add_condition($pid, $name, $group_key, $mark_expanded = TRUE) { // Load the predicate. $predicate = ca_load_predicate($pid); // Load the condition we want to add to the predicate. $data = ca_load_condition($name); // If the condition exists... if ($data) { // Build the condition array. $condition = array( '#name' => $name, '#title' => $data['#title'], '#argument_map' => array(), '#settings' => array(), ); // Mark it for expansion in the form if specified. if ($mark_expanded) { $condition['#expanded'] = TRUE; } // Add the condition to the predicate. $predicate['#conditions']['#conditions'][$group_key]['#conditions'][] = $condition; } ca_save_predicate($predicate); return $predicate; } /** * Removes a condition from a predicate. * * @param $pid * The ID of the predicate to remove the condition group from. * @param $group_key * The key of the condition group the condition is in. * @param $cond_key * The key of the condition to remove. * @return * An array representing the full, updated predicate. */ function ca_remove_condition($pid, $group_key, $cond_key) { // Load the predicate as it is now. $predicate = ca_load_predicate($pid); // Update, save, and return the predicate. unset($predicate['#conditions']['#conditions'][$group_key]['#conditions'][$cond_key]); ca_save_predicate($predicate); return $predicate; } /** * Adds a new action to a predicate. * * @param $pid * The ID of the predicate to add the action to. * @param $name * The name of the action to add. * @return * An array representing the full, updated predicate. */ function ca_add_action($pid, $name) { // Load the predicate as it is now. $predicate = ca_load_predicate($pid); // Load the action. $action = ca_load_action($name); // Abort if it did not exist. if (empty($action)) { return $predicate; } // Add the name to the action array so it's saved in the predicate. $action['#name'] = $name; // Add the action to the predicate's actions array. $predicate['#actions'][] = $action; // Save and return the updated predicate. ca_save_predicate($predicate); return $predicate; } /** * Removes an action from a predicate. * * @param $pid * The ID of the predicate to remove the action from. * @param $index * The index of the action to remove. * @return * An array representing the full, updated predicate. */ function ca_remove_action($pid, $index) { $actions = array(); // Load the predicate as it is now. $predicate = ca_load_predicate($pid); // Build a new actions array, leaving out the one marked for removal. foreach ($predicate['#actions'] as $key => $action) { if ($key != $index) { $actions[] = $action; } } // Update, save, and return the predicate. $predicate['#actions'] = $actions; ca_save_predicate($predicate); return $predicate; } /** * Loads a predicate by its ID. * * @param $pid * The ID of the predicate to load. * @return * A fully loaded predicate array. */ function ca_load_predicate($pid) { $predicate = array(); // First attempt to load the predicate from the database. $result = db_query("SELECT * FROM {ca_predicates} WHERE pid = '%s'", $pid); if ($predicate = db_fetch_array($result)) { $predicate = ca_prepare_db_predicate($predicate); } else { // Otherwise look for it in the module defined predicates. $predicates = module_invoke_all('ca_predicate'); if (!empty($predicates[$pid])) { $predicate = $predicates[$pid]; } } // Add the pid to the predicate so it can be resaved later. if (!empty($predicate)) { $predicate['#pid'] = $pid; } return $predicate; } /** * Saves a predicate array to the database. * * @param $predicate * A fully loaded predicate array. */ function ca_save_predicate($predicate) { // Check to see if the predicate has been previously saved to the database. $result = db_result(db_query("SELECT COUNT(*) FROM {ca_predicates} WHERE pid = '%s'", $predicate['#pid'])); if (!$result) { // If not, then insert it. db_query("INSERT INTO {ca_predicates} (pid, title, description, class, status, weight, uid, ca_trigger, conditions, actions, created, modified) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s', '%s', %d, %d)", $predicate['#pid'], $predicate['#title'], $predicate['#description'], $predicate['#class'], $predicate['#status'], $predicate['#weight'], $predicate['#uid'], $predicate['#trigger'], serialize($predicate['#conditions']), serialize($predicate['#actions']), time(), time()); } else { // Otherwise, update it. db_query("UPDATE {ca_predicates} SET title = '%s', description = '%s', class = '%s', status = %d, weight = %d, uid = %d, ca_trigger = '%s', conditions = '%s', actions = '%s', modified = %d WHERE pid = '%s'", $predicate['#title'], $predicate['#description'], $predicate['#class'], $predicate['#status'], $predicate['#weight'], $predicate['#uid'], $predicate['#trigger'], serialize($predicate['#conditions']), serialize($predicate['#actions']), time(), $predicate['#pid']); } } /** * Deletes a predicate from the database. * * @param $pid * The ID of the predicate to delete. */ function ca_delete_predicate($pid) { db_query("DELETE FROM {ca_predicates} WHERE pid = '%s'", $pid); } // Compares two conditional action arrays to sort them by #weight. function ca_weight_sort($a, $b) { if ($a['#weight'] == $b['#weight']) { return 0; } return ($a['#weight'] > $b['#weight']) ? 1 : -1; } // Quickie test function. :) function capm($var) { drupal_set_message('
'. print_r($var, TRUE) .'
'); }