'image_button', '#title' => t('My button'), '#return_value' => 'my_data', '#src' => 'my/image/path.jpg', ); */ /* TODO Check node access before emailing content Modules like Organic Groups and Project Issue send the same content as an email notifications to many users. They should now be using the new 3rd parameter to node_access() to check access on the content before emailing it. Note that db_rewrite_sql() provodes no protection because the recipient is not the logged in user who is receiving the content. */ /* TODO hook_user('view') The return value of hook_user('view') has changed, to match the process that nodes use for rendering. Modules should add their custom HTML to $account->content element. Further, this HTML should be in the form that drupal_render() recognizes. */ /* TODO Node previews and adding form fields to the node form. There is a subtle but important difference in the way node previews (and other such operations) are carried out when adding or editing a node. With the new Forms API, the node form is handled as a multi-step form. When the node form is previewed, all the form values are submitted, and the form is rebuilt with those form values put into $form['#node']. Thus, form elements that are added to the node form will lose any user input unless they set their '#default_value' elements using this embedded node object. */ /* TODO New user_mail_tokens() method may be useful. user.module now provides a user_mail_tokens() function to return an array of the tokens available for the email notification messages it sends when accounts are created, activated, blocked, etc. Contributed modules that wish to make use of the same tokens for their own needs are encouraged to use this function. */ /* TODO There is a new hook_watchdog in core. This means that contributed modules can implement hook_watchdog to log Drupal events to custom destinations. Two core modules are included, dblog.module (formerly known as watchdog.module), and syslog.module. Other modules in contrib include an emaillog.module, included in the logging_alerts module. See syslog or emaillog for an example on how to implement hook_watchdog. function example_watchdog($log = array()) { if ($log['severity'] == WATCHDOG_ALERT) { mysms_send($log['user']->uid, $log['type'], $log['message'], $log['variables'], $log['severity'], $log['referer'], $log['ip'], format_date($log['timestamp'])); } } */ /* TODO Implement the hook_theme registry. Combine all theme registry entries into one hook_theme function in each corresponding module file. function qbf_theme() { return array( ); }; */ /* TODO An argument for replacements has been added to format_plural(), escaping and/or theming the values just as done with t().*/ /* TODO $form['#base'] is gone In FormAPI, many forms with different form_ids can share the same validate, submit, and theme handlers. This can be done by manually populating the $form['#submit'], $form['#validate'], and $form['#theme'] elements with the proper function names. */ /** * Saved error reporting level. * * QBF module is supposed to pass parsing at E_ALL|E_STRICT, but other modules * may not be so strict, so we save the level at the start of the module and * restore it at the end of the module. */ global $_qbf_er; $_qbf_er = error_reporting(E_ALL | E_STRICT); /** * Remove this element from the generated form */ define('QBF_LEVEL_REMOVE', 0); /** * This element is only for display in the generated form: do not include it * in the query vector. */ define('QBF_LEVEL_DISPLAY', 1); /** * Include this element in the generated form and in the query vector, but do * not mark it as required. */ define('QBF_LEVEL_OPTIONAL', 2); /** * Include this element in the generated form and in the query vector, and * mark it as required. */ define('QBF_LEVEL_REQUIRED', 3); /** * The main QBF path. * * It MUST be a single component path, without a "/", otherwise qbf_menu() will * need to be changed. * * @ingroup paths * @see qbf_menu() */ define('QBF_PATH_MAIN', 'qbf'); /** * The QBF autocomplete path for search fields * @ingroup paths */ define('QBF_PATH_AC', 'qbf/ac'); /** * The path to the QBF settings page */ define('QBF_PATH_SETTINGS', 'admin/settings/qbf'); /** * Authorize use of QBF searches */ define('QBF_PERM_QUERY', 'use QBF search functions'); /** * Authorize QBF administration */ define('QBF_PERM_ADMIN', 'administer QBF'); /** * The name of the table used to store queries */ define('QBF_TABLE_NAME', 'qbf_queries'); /** * Notify owner about saved query deletions, variable name. */ define('QBF_VAR_NOTIFY_DELETE', 'qbf_notify_delete'); /** * Notify owner about saved query deletions, default value. */ define('QBF_DEF_NOTIFY_DELETE', FALSE); /** * Transform a form array for QBF. * * This function obtains the form array using Forms API, and transforms it by * modifying widgets to other types where needed. * * Any additional parameter passed to the function is transmitted to the form * generating function. * * @ingroup forms * @param string $form_id * @return array */ function qbf_transform_form($form_id) { // @todo GW: function qbf_transform_form(&$form_state, $form_id) { $ar_args = func_get_args(); //dsm(array('qtf' => $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) { /* function qbf_submit($form, &$form_state) { /* @TODO GW The 'op' element in the form values is deprecated. Each button can have #validate and #submit functions associated with it. Thus, there should be one button that submits the form and which invokes the normal form_id_validate and form_id_submit handlers. Any additional buttons which need to invoke different validate or submit functionality should have button-specific functions. switch ($form_state['values']['op']) { */ switch ($form_values['op']) { case t('Search'): $ret = FALSE; break; case t('Save query'): _qbf_save($form_id, $form_values); //@TODO GW _qbf_save($form_state['values']['form_id'], $form_state['values']); drupal_set_message(t('Your query was saved as "@name".', array('@name' => $form_values['save-name']))); //@TODO GW _qbf_save($form_state['values']['form_id'], $form_state['values']); global $user; $ret = "user/$user->uid/edit/job";; break; } //dsm(array('QS' => $form_values)); return $ret; /* @todo GW //dsm(array('QS' => $form_state['values'])); $form_state['redirect'] = $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 { /* @TODO GW drupal_retrieve_form() now accepts a form_state parameter. $form = drupal_retrieve_form($form_id, $form_state); */ $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() { /* TODO Non menu code that was placed in hook_menu under the '!$may_cache' block so that it could be run during initialization, should now be moved to hook_init. Previously we called hook_init twice, once early in the bootstrap process, second just after the bootstrap has finished. The first instance is now called boot instead of init. In Drupal 6, there are now two hooks that can be used by modules to execute code at the beginning of a page request. hook_boot() replaces hook_boot() in Drupal 5 and runs on each page request, even for cached pages. hook_boot() now only runs for non-cached pages and thus can be used for code that was previously placed in hook_menu() with $may_cache = FALSE: Dynamic menu items under a '!$may_cache' block can often be simplified to remove references to arg(n) and use of '%' to check conditions. See http://drupal.org/node/103114. The title and description arguments should not have strings wrapped in t(), because translation of these happen in a later stage in the menu system. */ $items = array(); $admin_access = array(QBF_PERM_ADMIN); $queror_access = array(QBF_PERM_QUERY); $items[QBF_PATH_SETTINGS] = array ( 'title' => t('Query-By-Form'), 'access arguments' => $admin_access, 'page callback' => 'drupal_get_form', 'page arguments' => array('qbf_admin_settings'), 'type' => MENU_NORMAL_ITEM, ); $items[QBF_PATH_MAIN . '/%qbf_query/delete'] = array ( 'type' => MENU_CALLBACK, 'access arguments' => $queror_access, 'page callback' => '_qbf_query_delete', 'page arguments' => array(1), ); 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); /* @todo GW // $ret = drupal_mail(__FUNCTION__, $user->mail, $message, $message, $user->mail); $ret = /* TODO Create a hook_mail($key, &$message, $params) function to generate $ret = the message body when called by drupal_mail. $ret = $account = array(); // Set this as needed $ret = $language = user_preferred_language($account); $ret = $object = array(); // Replace this as needed $ret = $context['subject'] = $subject; $ret = $context['body'] = $body; $ret = $params = array('account' => $account, 'object' => $object, 'context' => $context); $ret = drupal_mail('qbf', __FUNCTION__, $user->mail, $language, $params, $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);