'Finder AHAH', 'page callback' => 'finder_ahah', 'access arguments' => array('use finder'), 'type' => MENU_CALLBACK, ); $finders = finder_load_multiple(NULL, array(), TRUE); if (is_array($finders)) { foreach ($finders as $finder) { $items[$finder->path] = array( 'title' => $finder->title, 'page callback' => 'finder_page', 'page arguments' => array($finder->finder_id), 'access arguments' => array('use finder'), 'type' => MENU_CALLBACK, 'description' => $finder->description, ); } } $admin_item = array( 'file' => 'finder.admin.inc', 'file path' => finder_inc_path(), ); $items['admin/build/finder'] = $admin_item + array( 'title' => t('Finder'), 'page callback' => 'finder_admin_list', 'access arguments' => array('administer finder'), 'weight' => 0, 'type' => MENU_NORMAL_ITEM, 'description' => t("Finders are configurable forms to allow users to find objects in the system."), ); $items['admin/build/finder/list'] = $admin_item + array( 'title' => t('Finder list'), 'page callback' => 'finder_admin_list_redirect', 'access arguments' => array('administer finder'), 'weight' => 1, 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/build/finder/import'] = $admin_item + array( 'title' => t('Finder import'), 'page callback' => 'finder_admin_import', 'access arguments' => array('administer finder'), 'weight' => 2, 'type' => MENU_LOCAL_TASK, ); $items['admin/build/finder/%finder/edit'] = $admin_item + array( 'title' => t('Edit finder'), 'page callback' => 'drupal_get_form', 'page arguments' => array('finder_admin_edit', 3), 'access callback' => 'finder_menu_allow_finder_tabs', 'access arguments' => array(3), 'weight' => 3, 'type' => MENU_LOCAL_TASK, ); $items['admin/build/finder/%finder/delete'] = $admin_item + array( 'title' => t('Delete finder'), 'page callback' => 'drupal_get_form', 'page arguments' => array('finder_admin_delete', 3), 'access callback' => 'finder_menu_allow_finder_tabs', 'access arguments' => array(3), 'weight' => 4, 'type' => MENU_LOCAL_TASK, ); $items['admin/build/finder/%finder/export'] = $admin_item + array( 'title' => t('Export finder'), 'page callback' => 'finder_admin_export', 'page arguments' => array(3), 'access callback' => 'finder_menu_allow_finder_tabs', 'access arguments' => array(3), 'weight' => 5, 'type' => MENU_LOCAL_TASK, ); $items['admin/build/finder/%finder/edit/%/edit'] = $admin_item + array( 'title' => t('Edit element'), 'page callback' => 'drupal_get_form', 'page arguments' => array('finder_admin_element_edit', 3, 5), 'access callback' => 'finder_menu_allow_finder_element_tabs', 'access arguments' => array(3), 'weight' => 6, 'type' => MENU_LOCAL_TASK, ); $items['admin/build/finder/%finder/edit/%/delete'] = $admin_item + array( 'title' => t('Delete element'), 'page callback' => 'drupal_get_form', 'page arguments' => array('finder_admin_element_delete', 3, 5), 'access callback' => 'finder_menu_allow_finder_element_tabs', 'access arguments' => array(3), 'weight' => 7, 'type' => MENU_LOCAL_TASK, ); return $items; } /** * Determine whether to show edit/delete tabs for finder. * * @see finder_menu */ function finder_menu_allow_finder_tabs($finder) { if (is_numeric(arg(3)) && !$finder->settings['programmatic']) { return user_access('administer finder'); } return FALSE; } /** * Determine whether to show edit/delete tabs for finder element. * * @see finder_menu */ function finder_menu_allow_finder_element_tabs($finder) { return (is_numeric(arg(5)) && finder_menu_allow_finder_tabs($finder)); } /** * Implementation of hook_perm(). * * @see hook_perm */ function finder_perm() { return array('administer finder', 'administer finder PHP settings', 'use finder'); } /** * Implementation of hook_theme(). * * @see hook_theme */ function finder_theme() { return array( 'finder_admin_edit_elements_table' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'form' => NULL, ), ), 'finder_admin_links' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'finder' => NULL, ), ), 'finder_links' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'finder' => NULL, ), ), 'finder_page' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'finder' => NULL, ), ), 'finder_block' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'finder' => NULL, ), ), 'finder_view' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'finder' => NULL, 'display' => NULL, 'output_array' => NULL, ), ), 'finder_results' => array( 'file' => 'finder.theme.inc', 'path' => finder_inc_path(), 'arguments' => array( 'results' => NULL, 'finder' => NULL, 'keywords' => NULL, 'pager' => NULL, 'params' => NULL, 'form_state' => NULL, 'no_results' => NULL, ), ), ); } /** * Implementation of hook_forms(). * * @see hook_forms() */ function finder_forms($form_id, $args) { if (strpos($form_id, 'finder_form_') === 0) { $forms[$form_id] = array( 'callback' => 'finder_form', 'callback arguments' => $args, ); } return $forms; } /** * Invoke hook_finderapi(). * * @param &$object * The finder or finder flement. * @param $op * The operation, indicates where/when this is being invoked. * @param $a3, $a4 * Arguments to pass to the hook implementation. * @return * The returned value of the invoked hooks. */ function finder_invoke_finderapi(&$object, $op, $a3 = NULL, $a4 = NULL) { $return = array(); foreach (module_implements('finderapi') as $name) { $function = $name .'_finderapi'; $result = $function($object, $op, $a3, $a4); if (isset($result) && is_array($result)) { $return = array_merge($return, $result); } elseif (isset($result)) { $return[] = $result; } } return $return; } /** * Load objects from the database. * * This is intended as a reverse 'drupal_write_record' based on the code from * node_load_multiple() in Drupal 7. It can be used to read records from any * table assuming they are keyed by a serial field named {table}_id. Also * supports serialized fields. * * @param $load * The name of the table, and thus the type of object to load. * @param $ids * An array of IDs, if selecting by ID. * @param $conditions * An array of conditions on the table in the form 'field' => $value. * @param $reset * Whether to reset the internal cache for this type of $load object. * @return * An array of loaded objects indexed by ID. */ function finder_load_objects($load, $ids = NULL, $conditions = array(), $reset = FALSE) { static $object_cache = array(); if ($reset) { $object_cache[$load] = array(); } if (!isset($object_cache[$load])) { $object_cache[$load] = array(); } $objects = array(); $id_key = $load .'_id'; // Create a new variable which is either a prepared version of the $ids // array for later comparison with the finder cache, or FALSE if no $ids were // passed. The $ids array is reduced as items are loaded from cache, and we // need to know if it's empty for this reason to avoid querying the database // when all requested objects are loaded from cache. $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; // Load any available objects from the internal cache. if ($object_cache[$load]) { if (!empty($ids)) { $objects += array_intersect_key($object_cache[$load], $passed_ids); // If any objects were loaded, remove them from the $ids still to load. $ids = array_keys(array_diff_key($passed_ids, $objects)); } // If loading objects only by conditions, fetch all available objects from // the cache. objects which don't match are removed later. elseif ($conditions) { $objects = $object_cache[$load]; } } // Exclude any objects loaded from cache if they don't match $conditions. // This ensures the same behavior whether loading from memory or database. if (!empty($conditions)) { foreach ($objects as $object) { $object_values = (array) $object; if (array_diff_assoc($conditions, $object_values)) { unset($objects[$object->$id_key]); } } } // Load objects from the database. This is the case if there are // any $ids left to load, if $conditions was passed without $ids, // or if $ids and $conditions were intentionally left blank. if ((!empty($ids) || ($conditions && !$passed_ids)) || ($ids === NULL && $conditions === array())) { $query = array(); // build query $query['from'] = '{'. $load .'}'; if (!empty($ids)) { $query['wheres'][] = $id_key .' IN ('. db_placeholders($ids) .')'; $query['arguments'] = is_array($query['arguments']) ? $query['arguments'] + $ids : $ids; } if (!empty($conditions)) { $object_schema = drupal_get_schema($load); if (!empty($object_schema)) { foreach ($conditions as $field => $value) { $type = $object_schema['fields'][$field]['type']; $query['wheres'][] = $field ." = ". db_type_placeholder($type); $query['arguments'][] = $value; } } } if ($load == 'finder_element') { $query['orders'][] = 'weight'; } $queried_objects = finder_query($query); } // Pass all objects loaded from the database through the finder type specific // callbacks and hook_finderapi(), then add them to the internal cache. if (!empty($queried_objects)) { foreach ($queried_objects as $q_key => $q_object) { // unserialize settings if (isset($q_object->settings)) { $queried_objects[$q_key]->settings = (array)unserialize($q_object->settings); } // add elements if object is a finder. // this code is here in lieu of a hook_finderapi ($op='finder_load') implementation. if ($load == 'finder') { $queried_objects[$q_key]->elements = finder_element_load_multiple(array(), array($id_key => $q_object->$id_key)); if (!empty($queried_objects[$q_key]->elements)) { foreach ($queried_objects[$q_key]->elements as $position => $element) { $queried_objects[$q_key]->elements_index[$element->finder_element_id] = $position; } } finder_load_base_handler($queried_objects[$q_key]); finder_load_element_handler($queried_objects[$q_key]); finder_load_links($queried_objects[$q_key]); } // invoke finderapi so modules can make changes finder_invoke_finderapi($queried_objects[$q_key], $load .'_load'); } $objects += $queried_objects; // Add objects to the cache. //$object_cache[$load] += $queried_objects; foreach ($queried_objects as $queried_object) { $object_cache[$load][$queried_object->$id_key] = $queried_object; } } // Ensure that the returned array is ordered the same as the original $ids // array if this was passed in and remove any invalid ids. if ($passed_ids) { // Remove any invalid ids from the array. $passed_ids = array_intersect_key($passed_ids, $objects); foreach ($objects as $object) { $passed_ids[$object->$id_key] = $object; } $objects = $passed_ids; } return $objects; } /** * Load a finder object from the database. * * @param $finder_id * The finder ID. * @param $reset * Whether to reset the internal cache for finder objects. * @return * The loaded finder object, or FALSE on failure. */ function finder_load($finder_id, $reset = FALSE) { $finders = finder_load_multiple(array($finder_id), array(), $reset); return $finders ? $finders[$finder_id] : FALSE; } /** * Load finder objects from the database. * * @param $ids * An array of finder IDs if selecting by IDs. * @param $conditions * An array of conditions on the table in the form 'field' => $value. * @param $reset * Whether to reset the internal cache for finder objects. * @return * An array of loaded finder objects indexed by ID. */ function finder_load_multiple($ids = NULL, $conditions = array(), $reset = FALSE) { return finder_load_objects('finder', $ids, $conditions, $reset); } /** * Save changes to a finder or add a new finder. * * @param &$finder * The finder object. */ function finder_save(&$finder) { finder_invoke_finderapi($finder, 'finder_presave'); $update = array(); $op = 'finder_insert'; if (!empty($finder->finder_id)) { $update[] = 'finder_id'; $op = 'finder_update'; } drupal_write_record('finder', $finder, $update); finder_invoke_finderapi($finder, $op); } /** * Delete a finder and it's finder elements. * * @param $finder_id * The finder ID. */ function finder_delete($finder_id) { $finder = finder_load($finder_id); db_query("DELETE FROM {finder_element} WHERE finder_id = %d", $finder_id); db_query('DELETE FROM {finder} WHERE finder_id = %d', $finder_id); finder_invoke_finderapi($finder, 'finder_delete'); watchdog('finder', 'Finder %title deleted.', array('%title' => $finder->title)); drupal_set_message(t('Finder %title has been deleted.', array('%title' => $finder->title))); } /** * Load a finder element object from the database. * * @param $finder_element_id * The finder element ID. * @param $reset * Whether to reset the internal cache for finder element objects. * @return * The loaded finder element object, or FALSE on failure. */ function finder_element_load($finder_element_id) { $finder_elements = finder_element_load_multiple(array($finder_element_id), array(), $reset); return $finder_elements ? $finder_elements[$finder_element_id] : FALSE; } /** * Load finder element objects from the database. * * @param $ids * An array of finder element IDs if selecting by IDs. * @param $conditions * An array of conditions on the table in the form 'field' => $value. * @param $reset * Whether to reset the internal cache for finder element objects. * @return * An array of loaded finder element objects indexed by ID. */ function finder_element_load_multiple($ids = NULL, $conditions = array(), $reset = FALSE) { return finder_load_objects('finder_element', $ids, $conditions, $reset); } /** * Save changes to a finder element or add a new finder element. * * @param &$finder_element * The finder element object. */ function finder_element_save(&$finder_element) { finder_invoke_finderapi($finder_element, 'finder_element_presave'); $update = array(); $op = 'finder_element_insert'; if (!empty($finder_element->finder_element_id)) { $update[] = 'finder_element_id'; $op = 'finder_element_update'; } drupal_write_record('finder_element', $finder_element, $update); finder_invoke_finderapi($finder_element, $op); } /** * Delete a finder element. * * @param $finder_element_id * The finder element ID. */ function finder_element_delete($finder_element_id) { $finder_element = finder_element_load($finder_element_id); db_query("DELETE FROM {finder_element} WHERE finder_element_id = %d", $finder_element_id); finder_invoke_finderapi($finder_element, 'finder_element_delete'); watchdog('finder', 'Finder element %title deleted.', array('%title' => $finder_element->title)); drupal_set_message(t('Finder element %title has been deleted.', array('%title' => $finder_element->title))); } /** * Write a finder into the database as a new finder. * * @param $old_finder * The finder object to clone. * @return * The new finder object. */ function finder_clone($old_finder) { $finder = drupal_clone($old_finder); finder_invoke_finderapi($finder, 'finder_clone'); unset($finder->finder_id); finder_save($finder); foreach ($finder->elements as $key => $finder_element) { unset($finder_element->finder_element_id); $finder_element->finder_id = $finder->finder_id; finder_element_save($finder_element); $finder->elements[$key] = $finder_element; } return $finder; } /** * Menu callback; view a finder page. * * @param $finder_id * The finder ID. * @return * Themed output of a finder page. */ function finder_page($finder_id) { $finder = finder_load($finder_id); finder_invoke_finderapi($finder, 'finder_page'); return theme('finder_page', $finder); } /** * Generate display of a given finder. * * @param $finder * The finder object to generate the output for. * @param $display * The type of display ('page', 'block', or 'ahah'). * @return * Themed output of a finder. */ function finder_view($finder, $display) { finder_inc('form'); finder_invoke_finderapi($finder, 'finder_view', $display); $output = array(); $finder->finder_view_build_id = 'finder-'. $display .'-'. $finder->finder_id .'-wrapper'; $finder->finder_view_build_display = $display; // Always get the form in order to populate the form_state in case there are results we need to present. // The form building function will not spend resources building elements if it doesn't need to. // to do: this non form_on_page get_form may only be needed when hiding url args? $form = drupal_get_form('finder_form_'. $finder->finder_id, $finder); if ($finder->settings['advanced']['show_admin_links'] && user_access('administer finder')) { $output['admin_links'] = theme('finder_admin_links', $finder); } if ($display != 'page' || ($display == 'page' && $finder->settings['form_on_page'])) { $output['form'] = $form; } if ($finder->settings['advanced']['show_links']) { $output['links'] = theme('finder_links', $finder); } if ($display != 'block') { $output['results'] = finder_results($finder); } $rendered = ''; $rendered .= ($display == 'ahah') ? '' : '
'; $rendered .= theme('finder_view', $finder, $display, $output); $rendered .= ($display == 'ahah') ? '' : '
'; return $rendered; } /** * Menu callback; get finder ahah output. * * @param $finder_id * The finder ID. * @param $path * URL encoded path substitute. * @return * Finder ahah output in JSON format. */ function finder_ahah($finder_id, $path) { if ($finder_id) { $finder = finder_load($finder_id); if ($finder) { // fix the path for any scripts that might call $_GET['q'] $_GET['q'] = urldecode($path); // force the json'd finder output to hide_args $finder->settings['advanced']['hide_args'] = 1; drupal_json(array('status' => TRUE, 'data' => finder_view($finder, 'ahah'))); exit; } } drupal_json(array('data' => '')); exit; } /** * Create finder results output. * * @param $finder * The finder object. * @return * Themed output of finder results. */ function finder_results($finder) { $output = ''; $finder_form_state = finder_form_state($finder->finder_id); finder_invoke_finderapi($finder, 'finder_results', $finder_form_state); if (($finder_form_state && $finder_form_state['storage']['finished']) || $finder->settings['advanced']['filter']) { // I can't remember what this is for... foreach ($_REQUEST as $k => $v) { unset($_REQUEST[$k]); } $keywords = array(); $pager = &$finder->settings['advanced']['pager']; foreach ($finder->elements as $element) { $match = &$element->settings['advanced']['match']; $keyword = array(); if (!is_null($finder_form_state['values'][$element->finder_element_id])) { $keyword = (array)$finder_form_state['values'][$element->finder_element_id]; if (!empty($element->settings['advanced']['delimit'])) { foreach ($keyword as $k => $v) { unset($keyword[$k]); $exploded = explode($element->settings['advanced']['delimit'], $v); foreach ($exploded as $e) { $keyword[] = trim($e); } } } } $keywords[$element->finder_element_id] = $keyword; } $goto = &$finder->settings['advanced']['goto']; if ($_GET['go'] || ($finder_form_state['clicked_button']['#name'] == 'go' && $finder_form_state['storage']['finished']) || $goto == 'always') { $finder->go = TRUE; } $result = finder_find($finder, $keywords, 'results', $match, $pager); if ($finder->settings['advanced']['hide_args']) { if ($pager && !isset($finder_form_state['storage']['pager_token'])) { $token = drupal_get_token(); $finder_form_state['storage']['pager_token'] = $token; $_SESSION['finder'][$token] = $finder_form_state; } } $base_module = &$finder->base_handler['#module']; if (($finder->go && count($result)) || ($goto == 'best' && count($result) === 1)) { drupal_alter('finder_goto', $result, $finder); $current_result = current($result); module_invoke($base_module, 'finder_goto', $finder, $current_result); } $results = module_invoke($base_module, 'finder_result', $finder, $keywords, $result, $finder_form_state); $params = array(); if (!empty($finder_form_state['storage']['pager_token'])) { $params['finder'] = $finder_form_state['storage']['pager_token']; } if (!empty($finder->settings['advanced']['no_results']['no_results'])) { $variables = array( 'finder' => $finder, 'keywords' => $keywords, 'form_state' => $finder_form_state, ); $no_results = finder_eval($finder->settings['advanced']['no_results']['no_results'], $variables); } else { // Default text. $no_results = t('There are no results to display'); } $output .= theme('finder_results', $results, $finder, $keywords, $pager, $params, $finder_form_state, $no_results); } return $output; } /** * Implementation of hook_block(). * * @see hook_block() */ function finder_block($op = 'list', $delta = 0, $edit = array()) { if ($op == 'list') { $finders = finder_load_multiple(NULL, array('block' => 1)); $blocks = array(); foreach ($finders as $finder) { $blocks['finder_'. $finder->finder_id] = array( 'info' => t('Finder') .': '. $finder->title, 'cache' => BLOCK_NO_CACHE, ); } return $blocks; } else if ($op == 'view' && user_access('use finder')) { $finder_id = str_replace('finder_', '', $delta); $finder = finder_load($finder_id); if ($finder) { finder_invoke_finderapi($finder, 'finder_block'); $block = array( 'subject' => $finder->title, 'content' => theme('finder_block', $finder), ); return $block; } } } /** * Get a list of choices for form or results. * * This will invoke the base handler module's hook_finder_find() * implementation. It also allows for hooks to alter the keywords, finder, and * choices/results. * * @param $finder * The finder object. * @param $keywords * An array keyed by finder_element_id, where the values are any * str/num/bool/null or an array of such values to be OR'd together. * @param $mode * 'choices' or 'results' depending on what we are fetching. * @param $match * The match method, see finder_match_operator(). * @param $pager * Used to limit choices or results per page. * @param $finder_element_id * If $mode is 'choices', this is the finder element id to get choices for. * @param $reset * Reset the cached return value for this set of parameters. * @return * An array of choices/results. * @see hook_finder_find() */ function finder_find($finder, $keywords, $mode = 'choices', $match = 'e', $pager = 0, $finder_element_id = NULL, $reset = FALSE) { static $finder_find_cache = array(); // For a 'choices' find we need a main element to focus our query around // normally calling finder_find() you would not specify the $finder_element_id // but it would be interpreted as the index of the last $keywords element // though some modules may need to specify a main element other than the last // so that parameter is available. This value is best left 'NULL' for 'results'. if ($mode == 'choices' && is_null($finder_element_id)) { // no $finder_element_id was passed as the current element we're doing // so let's assume the last array key in $keywords is current finder_element_id $finder_element_id = end(array_keys($keywords)); } // Create an ID using the function params so we can cache the return value. $id = ($mode == 'choices' ? 'e'. $finder_element_id : 'f'. $finder->finder_id) .'|'; $id_length = strlen($id); $cache_id = ($match != 'e' ? $match : '') . ($pager ? $pager . (isset($_GET['page']) ? '.'. $_GET['page'] : '') : '') . serialize($keywords); $cache_id_length = strlen($cache_id); if (($cache_id_length + $id_length) > 255) { // For ID's that (will) exceed 255 we will try to represent them a unique way and pray for the best :/ $cache_id = md5($cache_id) . $cache_id_length . substr($cache_id, 0, 223 - strlen($cache_id_length) - $id_length); // 223 = 255 - 32 } $cache_id = $id . $cache_id; if (isset($finder_find_cache[$cache_id])) { // Use the static data. $options = $finder_find_cache[$cache_id]; } else if ($finder->settings['advanced']['cache_finder_find'] && !$reset && ($cache = cache_get($cache_id, 'cache_finder_find')) && !empty($cache->data)) { // Use the cached data. $options = $cache->data; } else { // Calculate the values from the database. // Figure out which module is the base module (the module that tells us the options) $module = &$finder->base_handler['#module']; // Allow other modules to intefere with the keywords. drupal_alter('finder_find_keywords', $keywords, $finder, $finder_element_id, $mode, $match, $pager); // Allow other modules to react to and alter the finder. finder_invoke_finderapi($finder, 'finder_find', $mode, $finder_element_id); $options = module_invoke($module, 'finder_find', $finder, $finder_element_id, $keywords, $mode, $match, $pager); // If mode is choices, we want to conduct some extra processing on this list. if ($mode == 'choices') { $options = finder_find_choices($finder, $finder_element_id, $options, $keywords[$finder_element_id], $match); } // Allow other modules to intefere with the options. drupal_alter('finder_find_options', $options, $finder, $finder_element_id, $keywords, $mode, $match, $pager); // Add the resulting $options to the drupal cache. if ($finder->settings['advanced']['cache_finder_find']) { cache_set($cache_id, $options, 'cache_finder_find', time() + $finder->settings['advanced']['cache_finder_find']); } // Add the resulting $options to the static internal load cache. $finder_find_cache[$cache_id] = $options; } return $options; } /** * Build basic finder query arrays. * * This is a helper function that can be leveraged by basic hook_finder_find() * implementations. It takes care of the common tasks that need to be done to * build the $query array correctly, and allows the base handler modules to * focus on their specialties. * * @param $query * The query array to modify. * @param $finder * The finder object. * @param $finder_element_id * If $mode is 'choices', this is the finder element id to get choices for. * @param $keywords * An array keyed by finder_element_id, where the values are any * str/num/bool/null or an array of such values to be OR'd together. * @param $mode * 'choices' or 'results' depending on what we are fetching. * @param $match * The match method, see finder_match_operator(). * @param $pager * Used to limit choices or results per page. * @param $join_ons * Join information in the form of an array where the key is the table string * as returned when a value returned from hook_finder_fields() is put through * finder_split_info() and the value is an array where the keys are the names * of real tables to join, and the values are the 'ON condition' that follows * the ON keyword in SQL. * @param $base_table * The name of the base table to query. * @param $base_field * The primary key of the base table. * @return * The modified query array. */ function finder_find_query($query, $finder, $finder_element_id, $keywords, $mode, $match, $pager, $join_ons, $base_table, $base_field) { $options = array(); $element_match = finder_match_operator($match); $query['from'] = '{'. $base_table .'} '. $base_table; $query['selects'][] = '"'. $base_table .'" AS base_table'; if ($mode == 'results') { $query['selects'][] = $base_table .'.'. $base_field; $query['selects'][] = '"'. $base_field .'" AS base_field'; $query['groups'][] = $base_table .'.'. $base_field; } foreach ($keywords as $feid => $keyword_array) { $element = &finder_element($finder, $feid); $fields[$feid] = &$element->settings['choices']['field']; foreach ($fields[$feid] as $key => $field) { $field_info[$feid][$key] = finder_split_field($field); } $sort[$feid] = &$element->settings['choices']['sort']; if ($mode == 'choices' && $feid == $finder_element_id && $element->settings['choices']['per_result']) { $query['selects'][] = $base_table .'.'. $base_field; $query['selects'][] = '"'. $base_field .'" AS base_field'; $query['groups'][] = $base_table .'.'. $base_field; } foreach ($fields[$feid] as $key => $field) { $field_alias = 'finder_element_'. $feid .'_'. $field_info[$feid][$key]['table'] .'_'. $field_info[$feid][$key]['field']; $query['selects'][] = $field .' AS '. $field_alias; if ($mode == 'choices' && $feid == $finder_element_id) { $query['groups'][] = $field_alias; } } // join tables if needed foreach ($fields[$feid] as $key => $field) { if (in_array($field_info[$feid][$key]['table'], array_keys($join_ons))) { $join_on = $join_ons[$field_info[$feid][$key]['table']]; foreach ($join_on as $table => $on) { $query['joins'][] = 'INNER JOIN {'. $table .'} '. $table .' ON '. $on; } } } $operator = $element->settings['advanced']['value_combination'] ? 'AND' : 'OR'; // Restrict by keywords on field. $keyword_array = (array)$keyword_array; $keyword_array = array_filter($keyword_array, 'finder_empty_keyword'); if (!empty($keyword_array)) { $results_match = finder_match_operator($element->settings['advanced']['match']); $query['wheres']['keywords']['#operator'] = $operator; foreach ($keyword_array as $keyword) { if ($feid == $finder_element_id) { foreach ($fields[$feid] as $key => $field) { $query['wheres']['keywords'][] = $field . finder_placeholder($element_match, $field_info[$feid][$key]['table'], $field_info[$feid][$key]['field']); $query['arguments'][] = $keyword; } } else { $query['wheres']['restrictions'][$feid]['keywords']['#operator'] = $operator; foreach ($fields[$feid] as $key => $field) { $query['wheres']['restrictions'][$feid]['keywords'][] = $field . finder_placeholder($results_match, $field_info[$feid][$key]['table'], $field_info[$feid][$key]['field']); $query['arguments'][] = $keyword; } } } } } // provide info for db_rewrite_sql $query['primary_table'] = $base_table; $query['primary_field'] = $base_field; // ensure there are no duplicate joins if (!empty($query['joins'])) { $query['joins'] = array_unique($query['joins']); } // for additional elements wheres group if (!empty($query['wheres']['restrictions'])) { $query['wheres']['restrictions']['#operator'] = $finder->settings['advanced']['element_combination'] ? 'OR' : 'AND'; } // if this is a choices list add a sort if there is only one field if ($mode == 'choices' && $sort[$finder_element_id] && count($fields[$finder_element_id]) === 1) { $query['orders'][] = end($fields[$finder_element_id]); } if ($pager) { $query['limit'] = $pager; $query['pager'] = TRUE; } // Restrict to one result if $finder->go is TRUE. if ($mode == 'results' && isset($finder->go) && $finder->go) { $query['limit'] = 1; $query['pager'] = TRUE; } return $query; } /* * A very basic query builder to perform select queries. * * Takes a $query array which determines how to build the SQL, whether and how * to execute it, and what to return. * * @param $query * The query array. See in-code comments for expected values. * @return * The built $query array if $query['execute'] is FALSE, or the array of * query results if $query['execute'] is TRUE, or FALSE on failure. */ function finder_query($query) { // Allow modules to modify this query. drupal_alter('finder_query', $query); // Prepare 'selects'. // Expecting empty or array('field1', 'alias.field2', etc..). if (!empty($query['selects'])) { $selects = "SELECT ". implode(', ', $query['selects']); } else { $selects = "SELECT *"; } // Prepare 'from' // expecting string like "{tablename} tablealias". if ($selects && $query['from']) { $from = " FROM ". $query['from']; } elseif ($selects) { drupal_set_message(t("No 'from' given in finder_query()"), "error"); return FALSE; } else { $from = ''; } // Prepare joins. // Expecting array("LEFT JOIN {table} alias ON alias.field1 = x.field2", "INNER JOIN {table} alias ON alias.field1 = x.field2", etc..). if (!empty($query['joins'])) { $joins = " ". implode(' ', $query['joins']); } else { $joins = ''; } // Prepare wheres. // See finder_wheres() for expected values. if (!empty($query['wheres'])) { $wheres = " WHERE ". finder_wheres($query['wheres']); } else { $wheres = ''; } // Prepare groups. // Expecting array('a field', 'another field', etc...). if (!empty($query['groups'])) { $groups = " GROUP BY ". implode(', ', $query['groups']); } else { $groups = ''; } // Prepare orders. // Expecting array("field1 ASC", "alias.field2 DESC", etc..). if (!empty($query['orders'])) { $orders = " ORDER BY ". implode(', ', $query['orders']); } else { $orders = ''; } // Build the query string. $query['sql'] = $selects . $from . $joins . $wheres . $groups . $orders; // Rewrite if required information is given. if ($query['primary_table'] && $query['primary_field']) { $query['sql'] = db_rewrite_sql($query['sql'], $query['primary_table'], $query['primary_field'], $query['arguments']); } // Do a pager query if ($query['pager']) { $query['limit'] = $query['limit'] ? $query['limit'] : 10; $query['element'] = $query['element'] ? $query['element'] : 0; $query['count_sql'] = $query['count_sql'] ? $query['count_sql'] : "SELECT COUNT(*) ". $from . $joins . $wheres; $query['query_function'] = 'pager_query'; $query['query_function_arguments'] = array( 'query' => $query['sql'], 'limit' => $query['limit'], 'element' => $query['element'], 'count_query' => $query['count_sql'], 'arguments' => $query['arguments'], ); } // Do a range query elseif ($query['range']) { $query['from'] = $query['from'] ? $query['from'] : 0; $query['count'] = $query['count'] ? $query['count'] : 10; $query['query_function'] = 'db_query_range'; $query['query_function_arguments'] = array( 'query' => $query['sql'], 'arguments' => $query['arguments'], 'from' => $query['from'], 'count' => $query['count'], ); } // Do a normal query. else { $query['query_function'] = 'db_query'; $query['query_function_arguments'] = array( 'query' => $query['sql'], 'arguments' => $query['arguments'], ); } // Allow modules to modify the built query. drupal_alter('finder_query_built', $query); // If not executing a query just return the query object here. if ($query['execute'] === FALSE) { return $query; } if ($query['query_function']) { $result = call_user_func_array($query['query_function'], $query['query_function_arguments']); } // process results $db_function = isset($query['db_function']) ? $query['db_function'] : 'db_fetch_object'; $results = array(); while ($row = $db_function($result)) { $results[] = $row; } return $results; } /* * Recursively process SQL 'wheres'. * * @param $wheres * The wheres array. For example: * array( * '#operator' => 'OR', * "foo = 'something'", * "bar = 'another'", * array( * '#operator' => 'AND', * "baz IN (1,2,3)", * "baz NOT NULL", * ), * ); * @return * The where string. For example: * WHERE foo = 'something' * OR bar = 'another' * OR (baz IN (1,2,3) AND baz NOT NULL) */ function finder_wheres($wheres) { $operator = $wheres['#operator'] ? $wheres['#operator'] : 'AND'; unset($wheres['#operator']); foreach ($wheres as $key => $where) { if (is_array($where)) { $finder_wheres = finder_wheres($where); if ($finder_wheres) { $wheres[$key] = '('. $finder_wheres .')'; } else { unset($wheres[$key]); } } } return implode(' '. $operator .' ', $wheres); } /** * Get a list of possible element types. * * @return * An array of element handlers from hook implementations. * @see hook_finder_element_handlers() */ function finder_element_handlers() { static $element_handlers; if (empty($element_handlers)) { $element_handlers = module_invoke_all('finder_element_handlers'); } return $element_handlers; } /** * Attach element handler data to the finder. * * @param &$finder * The finder object. */ function finder_load_element_handler(&$finder) { $element_handlers = finder_element_handlers(); if (!empty($finder->elements)) { foreach ($finder->elements as $key => $element) { $finder->elements[$key]->element_handler = $element_handlers[$element->element]; } } } /** * Get a list of findable Drupal objects. * * @return * An array of base handlers from hook implementations. * @see hook_finder_base_handlers() */ function finder_base_handlers() { static $base_handlers; if (empty($base_handlers)) { $base_handlers = module_invoke_all('finder_base_handlers'); } return $base_handlers; } /** * Attach base handler data to the finder. * * @param &$finder * The finder object. */ function finder_load_base_handler(&$finder) { $base_handlers = finder_base_handlers(); $finder->base_handler = $base_handlers[$finder->base]; } /** * Load a module include file according to finder's naming convention. * * Finder's naming convention suggests includes be put in a directory called * 'includes' within the module's directory, and named like so: * module-name.inc-string.inc * * @param $inc_string * If the file is finder.foo.inc then the $inc_string to specify is 'foo'. * @param $module * The name of the module. */ function finder_inc($inc_string, $module = 'finder') { return module_load_include('inc', $module, 'includes/'. $module .'.'. $inc_string); } /** * Returns the path to a module's includes directory according to finder's * naming convention. * * Finder's naming convention suggests includes be put in a directory called * 'includes' within the module's directory. * * @param $module * The name of the module. */ function finder_inc_path($module = 'finder') { static $inc_path; if (empty($inc_path[$module])) { $inc_path[$module] = drupal_get_path('module', $module) .'/includes'; } return $inc_path[$module]; } /** * Turns string placeholders to other types in keyword queries, if required. * * By default finder's queries use a string placeholder ('%s') with no regard * for pgsql peculiarities when creating an SQL condition for searches. By * putting such conditions through this function, this is a common place to do * string manipulation on the condition to ensure compatibility with the * database. * * @param $match * The SQL condition string. * @param $table * The name of the table this condition is from. * @param $field * The name of the field this condition is against. * @return * The modified SQL condition string. */ function finder_placeholder($match, $table, $field) { global $db_type; $object_schema = drupal_get_schema($table); $type = $object_schema['fields'][$field]['type']; $placeholder = db_type_placeholder($type); if ($placeholder != "'%s'") { if (strpos($match, 'LIKE') === FALSE) { $match = str_replace('%s', $placeholder, $match); $match = str_replace("'", "", $match); } else if ($db_type == 'pgsql') { // It is also assumed that $match contains 'LIKE' here. $match = '::text'. str_replace('LIKE', 'ILIKE', $match); } } return $match; } /** * Return info about a field. * * Finder stores information about fields as "table-name.field-name", this is * just a simple function to split the field string into the two parts for * convenience. * * @param $field * A field value as given in the array keys returned from * hook_finder_fields() implementations. * @return * An array with keys 'field' and 'table'. */ function finder_split_field($field) { $field_parts = explode('.', $field); $field_info['field'] = $field_parts[1]; $field_info['table'] = $field_parts[0]; return $field_info; } /** * Get data about finder match methods. * * @param $match * The match method if a specific operator is needed. * @return * A specific operator string if $match is set. Otherwise returns the full * array of data about all match methods including operators and * descriptions. */ function finder_match_operator($match = NULL) { static $operators; if (empty($operators)) { // Operators use abbreviated key names because they need to be tiny in cache ID's. $operators = array( 'c' => array( 'operator' => " LIKE '%%%s%%'", 'description' => t('Contains - Results contain the submitted values.'), ), 'e' => array( 'operator' => " = '%s'", 'description' => t('Equals - Results must match the submitted values exactly.'), ), 'sw' => array( 'operator' => " LIKE '%s%%'", 'description' => t('Starts with - Results must start with the submitted values.'), ), 'ew' => array( 'operator' => " LIKE '%%%s'", 'description' => t('Ends with - Results must end with the submitted values.'), ), 'lt' => array( 'operator' => " < '%s'", 'description' => t('Less than - Results must be less than the submitted values.'), ), 'lte' => array( 'operator' => " <= '%s'", 'description' => t('Less than or equals - Results must be less than or equal to the submitted values.'), ), 'gt' => array( 'operator' => " > '%s'", 'description' => t('Greater than - Results must be greater than the submitted values.'), ), 'gte' => array( 'operator' => " >= '%s'", 'description' => t('Greater than or equals - Results must be greater than or equal to the submitted values.'), ), 'ne' => array( 'operator' => " != '%s'", 'description' => t('Not equals - Results must not match the submitted values.'), ), ); drupal_alter('finder_match_operators', $operators); } if (!is_null($match)) { return $operators[$match]['operator']; } return $operators; } /** * Get an element from a finder. * * Finder stores it's elements in an indexed array, as well as tracking an * array that maps finder element IDs to the index position. This can be * awkward to use when dealing with a particular element's settings especially * when the element variable needs to be a reference to an element in the * finder variable. This function conveniently allows us to pull a finder * element into a reference. If called by reference, for example, * $element = &finder_element($finder, $finder_element_id); * this function will return a reference to a finder element from the supplied * finder as identified by the supplied finder element id, if not called by * reference it will return a copy of the element. * * @param &$finder * The finder object from which to get the element. * @param &$finder_element_id * The ID of the finder element required. * @return * The finder element, or reference to the finder element. */ function &finder_element(&$finder, &$finder_element_id) { $key = &$finder->elements_index[$finder_element_id]; return $finder->elements[$key]; } /** * Attach 'links' data and 'admin links' data to the finder. * * @param &$finder * The finder object. */ function finder_load_links(&$finder) { // create admin links $finder->admin_links = array(); $finder->admin_links[$finder->path] = t('View "Path"'); if (!$finder->settings['programmatic']) { $finder->admin_links['admin/build/finder/'. $finder->finder_id .'/edit'] = t('Edit'); } // create links $finder->links = array(); } /** * Build finder code string recursively. */ function finder_export($var, $iteration = 0){ $tab = ''; for ($i = 0; $i < $iteration; $i++) { $tab = $tab ." "; } $iteration++; if (is_object($var)) { $var = (array)$var; $var['#_finder_object'] = '1'; } if (is_array($var)) { $empty = empty($var); $code = "array(". ($empty ? '' : "\n"); foreach ($var as $key => $value) { $out = $tab ." '". $key ."' => ". finder_export($value, $iteration) .",\n"; drupal_alter('finder_export', $out, $tab, $key, $value, $iteration); $code .= $out; } $code .= ($empty ? '' : $tab) .")"; return $code; } else { if (is_string($var)) { return "'". addslashes($var) ."'"; } elseif (is_numeric($var)) { return $var; } elseif (is_bool($var)) { return ($var ? 'TRUE' : 'FALSE'); } else { return 'NULL'; } } } /** * Evaluate and return decoded string. */ function finder_import($string) { $array = eval('return '. $string .';'); $return = finder_import_objects($array); return $return; } /** * Recursively converts arrays back into objects. */ function finder_import_objects($array) { foreach ($array as $k => $v) { if (is_array($v)) { $array[$k] = finder_import_objects($v); } if (is_string($v)) { $array[$k] = stripslashes($v); } } if ($array['#_finder_object']) { unset($array['#_finder_object']); $array = (object)$array; } return $array; } /** * Postprocessing for returned finder_find options when mode is choices. * * TO DO: some of the added properties here could probably just be "selected" * in the query. */ function finder_find_choices($finder, $finder_element_id, $options, $keywords, $match) { if ($options) { $element = &finder_element($finder, $finder_element_id); $fields = &$element->settings['choices']['field']; foreach ($fields as $key => $field) { $field_info[$key] = finder_split_field($field); $field_names[$key] = 'finder_element_' . $finder_element_id .'_' . $field_info[$key]['table'] .'_' . $field_info[$key]['field']; } $new_options = array(); foreach ($options as $option) { if (count($fields) === 1) { $option->field_name = end($field_names); $option->display_field = $option->field_name; if ($element->settings['choices']['per_result']) { $option->field_name = $option->base_field; } $new_options[] = $option; } elseif (count($fields) > 1) { $operator = str_replace( '%%', '%', str_replace("'", "", finder_match_operator($match)) ); $matching_names = array(); if (is_null($keywords)) { $matching_names = $field_names; } else { foreach ($field_names as $field_name) { $expression = str_replace('%s', $keywords, $operator); if (strpos($expression, 'LIKE') !== FALSE) { $expression = str_replace(' LIKE ', '', $expression); // Added strtolower() because some people were reporting // case-sensitivity even with the i modifier. $like_matches = preg_grep( "/^" . str_replace( '%', '(.*?)', preg_quote(strtolower($expression)) ) ."$/si", array(strtolower($option->$field_name)) ); if (!empty($like_matches)) { $matching_names[] = $field_name; } } elseif (eval('return '. $option->$field_name . $expression .';')) { $matching_names[] = $field_name; } } } if (count($matching_names) === 1) { $option->field_name = end($matching_names); $option->display_field = $option->field_name; $new_options[] = $option; } elseif (!empty($matching_names)) { foreach ($matching_names as $matching_name) { $new_option = drupal_clone($option); $new_option->field_name = $matching_name; $new_option->display_field = $new_option->field_name; $new_options[] = $new_option; } } } } return $new_options; } return $options; } /** * Evaluate a string of PHP code. * * This is a wrapper around PHP's eval(). It uses output buffering to capture * both returned value and printed text. Allows to use variables with the given * code. * Using this wrapper also ensures that the PHP code which is evaluated can not * overwrite any variables in the calling code, unlike a regular eval() call. * In other words, we evaluate the code with independent variable scope. * * @param $code * The code to evaluate. * @param $variables * Variables to import to local variable scope. * @return * A string containing the printed output of the code, followed by the * returned output of the code. */ function finder_eval($code, $variables = array()) { global $theme_path, $theme_info, $conf; // Store current theme path. $old_theme_path = $theme_path; // Restore theme_path to the theme, as long as drupal_eval() executes, // so code evaluted will not see the caller module as the current theme. // If theme info is not initialized get the path from theme_default. if (!isset($theme_info)) { $theme_path = drupal_get_path('theme', $conf['theme_default']); } else { $theme_path = dirname($theme_info->filename); } extract($variables); ob_start(); print eval('?>'. $code); $output = ob_get_contents(); ob_end_clean(); // Recover original theme path. $theme_path = $old_theme_path; return $output; } /** * Modify a PHP setting element. * * Used for security reasons to prevent an unauthorized user editing the * field. Also makes variables available for the PHP input. * * @param $element * The original array for the element. * @param $variables * Array where keys are variable names (without the $) to make available in * the PHP, and the values are descriptions already passed through t(). */ function finder_php_setting($element, $variables = array()) { if (user_access('administer finder PHP settings')) { $var_list = array(); foreach ($variables as $variable => $description) { $var_list[] = '$'. $variable .' - '. $description; } if (!empty($var_list)) { $element['#description'] = (isset($element['#description']) ? $element['#description'] : '') .'
' . t('Available variables') .':' . theme('item_list', $var_list) . '
'; } } else { $element['#disabled'] = TRUE; $element['#prefix'] = '
' . t("You don't have permission to modify !setting.", array('!setting' => $element['#title'])) .'
' . (isset($element['#prefix']) ? $element['#prefix'] : ''); $element['#value'] = $element['#default_value']; } return $element; } /** * Callback for array_filter to remove empty keywords. */ function finder_empty_keyword($keyword) { if ($keyword !== '' && $keyword !== NULL) { return TRUE; } return FALSE; } // TO DO: add multifield element sorts into finder_find_choices. // TO DO: reduce duplicate choices returned from queries - distinct doesn't // work because nid's are selected too. - but it doesn't hurt to add // it to prevent superfluous rows??