$ar_args)); // Fetch the basic form and rename it, passing it the caller's arguments $form = call_user_func_array('drupal_retrieve_form', $ar_args); $new_form_id = "qbf_$form_id"; // Only keep the children of the form and QBF properties on the form itself $elements = array(); $new_form = array(); $new_form['#qbf_source_form_id'] = $form_id; if (in_array('#qbf', element_properties($form))) { $new_form += $form['#qbf']; } foreach (element_children($form) as $key) { // dsm("Transforming $key, type " . $form[$key]['#type']); $new_element = _qbf_transform_element($key, $form[$key]); if (!is_null($new_element)) { $new_form[$key] = $new_element; } } $new_form['#id'] = $new_form_id; $new_form['#multistep'] = TRUE; // Do not set #redirect, even to FALSE (submit handlers) // $new_form['#redirect'] = FALSE; $new_form['#after_build'][] = 'qbf_after_build'; $new_form['#submit'] = array('qbf_submit' => array()); // dsm($new_form); return $new_form; } /** * Transform a form element for QBF. * * QBF-specific properties are: * - #qbf : array of properties * - #level: only within #qbf * * See QBF_* constants * * @ingroup forms * @param string $key * @param array $element * @return void */ function _qbf_transform_element($key, $element) { // dsm(array('key' => $key, 'element' => $element)); /** * List default type transformations applied to widget by FAPI. * * Types without a default transformation are not transformed */ static $ar_default_type_transformations = array ( 'button' => NULL, 'file' => NULL, // 'hidden' => NULL, 'markup' => NULL, 'password' => NULL, 'radio' => NULL, 'submit' => NULL, 'textarea' => 'textfield', // 'value' => 'value', ); /** * List default property transformations applied to widget by FAPI property. * * Properties without a default transformation are not transformed */ static $ar_default_property_transformations = array ( // Standard properties '#action' => NULL, '#after_build' => NULL, '#base' => NULL, '#button_type' => NULL, '#built' => NULL, '#description' => NULL, '#method' => NULL, '#parents' => NULL, '#redirect' => NULL, '#ref' => NULL, '#required' => NULL, '#rows' => NULL, '#submit' => NULL, '#tree' => NULL, '#validate' => NULL, ); /** * List properties causing causing element removal. * * The key is the property name, the value is the one causing removal. */ static $ar_killer_properties = array ( '#disabled' => TRUE, ); // Transform type $source_type = $element['#type']; // .. Default transformation $dest_type = array_key_exists($source_type, $ar_default_type_transformations) ? $ar_default_type_transformations[$source_type] : $source_type; // .. Apply form-defined type override if (isset($element['#qbf']['#type'])) { $dest_type = $element['#qbf']['#type']; } if (is_null($dest_type)) { $ret = NULL; } else { $ret = $element; $ret['#type'] = $dest_type; if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE) { $ret = NULL; } else { foreach (element_properties($element) as $property_name) { // Apply killer properties first to avoid useless work if (array_key_exists($property_name, $ar_killer_properties) && ($element[$property_name] = $ar_killer_properties[$property_name])) { $ret = NULL; break; } // Now transform or copy remaining properties if (array_key_exists($property_name, $ar_default_property_transformations)) { $ret[$property_name] = $ar_default_property_transformations[$property_name]; } else { $ret[$property_name] = $element[$property_name]; } // And apply form-defined property overrides if ($property_name == '#qbf') { foreach ($element[$property_name] as $override_name => $override_value) { $ret[$override_name] = $override_value; } } } // Recursively transform children foreach (element_children($element) as $child_name) { $child = _qbf_transform_element($child_name, $element[$child_name]); if (is_null($child)) { unset($ret[$child_name]); } else { $ret[$child_name] = $child; } } } } //dsm(array('key' => $key, 'transformed element' => $ret)); return $ret; } /** * Implement hook_perm(). * * @ingroup hooks * @return array */ function qbf_perm() { $ret = array ( QBF_PERM_ADMIN, QBF_PERM_QUERY, ); return $ret; } /** * Implement hook_forms(). * * @ingroup forms * @ingroup hooks * @return array */ function qbf_forms() { $hook_name = 'qbf_register'; foreach (module_implements($hook_name) as $module) { foreach (module_invoke($module, $hook_name) as $form_name) { $forms["qbf_$form_name"] = array ( 'callback' => 'qbf_transform_form', 'callback arguments' => array($form_name), ); } } return $forms; } /** * Insert the query results at the bottom of the query form. * * @ingroup forms * @param array $form * @param array $form_values * @return array */ function qbf_after_build($form, $form_values) { if (empty($form['#post'])) { return $form; } // If #post is not empty, we are indeed querying $ar_query = _qbf_extract_query($form, $form_values); /* This function is called at the end of the form building process, which * means that child properties of #qbf have already been upgraded to element * properties. So we look for $form['#callback'] and not * $form['#qbf']['#callback'] */ if (isset($form['#callback']) && function_exists($function = $form['#callback'])) { $results = $function($ar_query); } else { drupal_set_message(t('QBF: incorrect callback function for search'), 'error'); } $form['qbf_query_results'] = array ( '#type' => 'markup', '#value' => $results, '#weight' => 10, ); return $form; } /** * Recursively build a query array from the form and its values * * In the current version, element names are supposed to be unique, even at * different levels in the tree. * * @ingroup forms * @param array $form * @param array $form_values */ function _qbf_extract_query($form, $form_values) { $name = $form['#parents'][0]; // Elements which are removed or display-only have no place in the query if (array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf']) && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL) { $ret = array($name => $form_values[$name]); } else { $ret = array(); } // QBF level is not inherited, so this loop is outside the "if" above foreach (element_children($form) as $child_name) { $ret += _qbf_extract_query($form[$child_name], $form_values); } return $ret; } /** * Provide an optional automatic mapping mechanism for query building. * * This function takes a partly built query map $ar_queryMap, and a defaults * array to complete it in $ar_defaults, and returns a fully built query array * ready to be used for querying. * * @param array $ar_query * @param array $ar_defaults * @return array */ function qbf_query_mapper($ar_queryMap = array(), $ar_defaults = array()) { $ret = array(); foreach ($ar_queryMap as $name => $value) { // accept NULL, empty strings... if (!is_array($value)) { $value = array(); } $item = $value; foreach ($ar_defaults as $default_key => $default_value) { if (!array_key_exists($default_key, $item)) { $item[$default_key] = is_null($default_value) ? $name : $default_value; } // else if is already in $item, so we don't touch it } $ret[$name] = $item; } return $ret; } /** * Load a form_values array into a form used by QBF. * * This is typically useful when loading saved queries using qbf_query_load(). * For other cases, the mechanisms built within FAPI should be used instead. * * @see qbf_query_load() * * @ingroup forms * @param array $form * @param array $form_values * @return array The modified form */ function qbf_import_values($element, $form_values) { foreach (element_children($element) as $child_name) { if (!empty($form_values[$child_name])) { $element[$child_name]['#qbf']['#default_value'] = $form_values[$child_name]; } $element[$child_name] = qbf_import_values($element[$child_name], $form_values); } return $element; } /** * Load a saved QBF query. * * It is not named qbf_load() although this would seem more natural, because a * hook_load() exists and this is not an implementation of this hook. * * @see qbf_import_values() * * @param int $qid * @return array A form_values array usable by qbf_import_values */ function qbf_query_load($qid) { $sq = 'SELECT qq.qid, qq.uid, qq.query, qq.name ' .'FROM {%s} qq ' .'WHERE qq.qid = %d '; // db_rewrite_sql does not apply here until we add more advanced support for access control $q = db_query($sq, QBF_TABLE_NAME, $qid); $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key // FALSE does not happen if ($ret === NULL) { $ret = NULL; } else { $ret->query = unserialize($ret->query); //dsm($ret); } return $ret; } /** * Submit handler for query save form. * * @ingroup forms * @param $form_id string * @param $form_values array * @return string */ function qbf_submit($form_id, $form_values) { switch ($form_values['op']) { case t('Search'): $ret = FALSE; break; case t('Save query'): _qbf_save($form_id, $form_values); drupal_set_message(t('Your query was saved as "@name".', array('@name' => $form_values['save-name']))); global $user; $ret = "user/$user->uid/edit/job";; break; } //dsm(array('QS' => $form_values)); return $ret; } /** * List queries owned by a given user. * * @param int $uid > 0 * @return array */ function qbf_get_queries_by_user($uid = NULL) { if (is_null($uid)) { global $user; $uid = $user->uid; } $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query, qq.updated ' .'FROM {%s} qq ' .'WHERE qq.uid = %d ' .'ORDER BY qq.name '; $q = db_query($sq, QBF_TABLE_NAME, $uid); $ret = array(); while ($o = db_fetch_object($q)) { $ret[$o->qid] = $o; // qid is the PK, so it is present and unique } return $ret; } /** * Save a query and return its qid. * * @ingroup forms * @param $form_id string * @param $form_values array * @return int */ function _qbf_save($form_id, $form_values) { global $user; if ($user->uid == 0) { $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.'); drupal_set_message($warning, 'error'); watchdog('qbf', $warning, WATCHDOG_WARNING); $ret = 0; } else { $form = drupal_retrieve_form($form_id); drupal_prepare_form($form_id, $form); $name = $form_values['save-name']; $form_values = _qbf_extract_query($form, $form_values); $ar_values = array(); foreach ($form_values as $key => $value) { if (empty($value)) { continue; } $ar_values[$key] = $value; } // Avoid duplicates if (!empty($name)) { $sq = "DELETE FROM {%s} WHERE name = '%s'"; db_query($sq, QBF_TABLE_NAME, $name); } $sq = 'INSERT INTO {%s} (qid, uid, name, query, updated) ' ."VALUES (%d, %d, '%s', '%s', '%d' ) "; $ret = db_next_id('qbf_qid'); $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $name, serialize($ar_values), time()); } return $ret; } /** * Implement hook_menu(). * * @param $may_cache boolean * @return array */ function qbf_menu($may_cache) { $items = array(); if ($may_cache) { $admin_access = user_access(QBF_PERM_ADMIN); $items[] = array ( 'path' => QBF_PATH_SETTINGS, 'title' => t('Query-By-Form'), 'access' => $admin_access, 'callback' => 'drupal_get_form', 'callback arguments' => 'qbf_admin_settings', 'type' => MENU_NORMAL_ITEM, ); } else { if ((arg(0) == QBF_PATH_MAIN) && is_numeric(arg(1)) && arg(1) > 0 && arg(2) == 'delete') { $qid = arg(1); $queror_access = user_access(QBF_PERM_QUERY); $items[] = array ( 'path' => QBF_PATH_MAIN .'/'. $qid .'/delete', 'type' => MENU_CALLBACK, 'access' => $queror_access, 'callback' => '_qbf_query_delete', 'callback arguments' => array($qid), ); } } return $items; } /** * Delete a query by qid * * $qid has been tested in qbf_menu() to be a positive integer, so it is a safe * number, but we still need to know more about it. * * @param $qid integer */ function _qbf_query_delete($qid) { global $user; $query = qbf_query_load($qid); $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE); $link = l($qid, QBF_PATH_MAIN .'/'. $qid .'/delete'); // only valid if valid query, and owner or admin if (isset($query->uid) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN))) { $sq = 'DELETE FROM %s WHERE qid = %d '; $q = db_query($sq, QBF_TABLE_NAME, $qid); $message = t('Query @id "@name" has been deleted.', array ( '@id' => $qid, '@name' => $query->name, )); drupal_set_message($message, 'status'); watchdog('qbf', $message, WATCHDOG_NOTICE, $link); if (variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE) && $query->uid != $user->uid) { $ret = drupal_mail(__FUNCTION__, $user->mail, $message, $message, $user->mail); $account = user_load(array('uid' => $query->uid)); drupal_set_message(t('User !link has been informed', array ( '!link' => l($account->name, 'user/'. $query->uid), ))); dsm($ret); } } else { $message = t('Failed attempt to delete query @qid. Administrator has been alerted.', array ( '@qid' => $qid, )); drupal_set_message($message, 'error'); watchdog('qbf', $message, WATCHDOG_ERROR, $link); } drupal_goto(); } /** * Implement the former hook_settings(). * * @return array */ function qbf_admin_settings() { $form = array(); $form[QBF_VAR_NOTIFY_DELETE] = array ( '#type' => 'checkbox', '#default_value' => variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE), '#title' => t('Notify users when one of their saved searches has been deleted'), ); return system_settings_form($form); } error_reporting($_qbf_er);