|
@@ -0,0 +1,517 @@
|
|
|
|
+<?php
|
|
|
|
+/**
|
|
|
|
+ * Query By Form
|
|
|
|
+ *
|
|
|
|
+ * This module allows node modules to add a query by form tab for their node
|
|
|
|
+ * types to the default search form
|
|
|
|
+ *
|
|
|
|
+ * @copyright 2008 Ouest Systemes Informatiques (OSInet)
|
|
|
|
+ * @author Frederic G. MARAND
|
|
|
|
+ * @version $Id: qbf.module,v 1.9 2008-09-02 08:25:24 marand Exp $
|
|
|
|
+ * @license CeCILL 2.0
|
|
|
|
+ * @package QBF
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 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
|
|
|
|
+ */
|
|
|
|
+define('QBF_PATH_MAIN', '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');
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 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.
|
|
|
|
+ *
|
|
|
|
+ * @param string $form_id
|
|
|
|
+ * @return array
|
|
|
|
+ */
|
|
|
|
+function qbf_transform_form($form_id)
|
|
|
|
+ {
|
|
|
|
+ $arArgs = func_get_args();
|
|
|
|
+//dsm(array('qtf' => $arArgs));
|
|
|
|
+ // Fetch the basic form and rename it, passing it the caller's arguments
|
|
|
|
+ $form = call_user_func_array('drupal_retrieve_form', $arArgs);
|
|
|
|
+ $newFormId = "qbf_$form_id";
|
|
|
|
+
|
|
|
|
+ // Only keep the children of the form and QBF properties on the form itself
|
|
|
|
+ $elements = array();
|
|
|
|
+ $newForm = array();
|
|
|
|
+ $newForm['#qbf_source_form_id'] = $form_id;
|
|
|
|
+ if (in_array('#qbf', element_properties($form)))
|
|
|
|
+ {
|
|
|
|
+ $newForm += $form['#qbf'];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ foreach (element_children($form) as $key)
|
|
|
|
+ {
|
|
|
|
+ // dsm("Transforming $key, type " . $form[$key]['#type']);
|
|
|
|
+ $newElement = _qbf_transform_element($key, $form[$key]);
|
|
|
|
+ if (!is_null($newElement))
|
|
|
|
+ {
|
|
|
|
+ $newForm[$key] = $newElement;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ $newForm['#id'] = $newFormId;
|
|
|
|
+ $newForm['#multistep'] = TRUE;
|
|
|
|
+ // Do not set #redirect, even to FALSE (submit handlers)
|
|
|
|
+ // $newForm['#redirect'] = FALSE;
|
|
|
|
+ $newForm['#after_build'][] = 'qbf_after_build';
|
|
|
|
+ $newForm['#submit'] = array('qbf_submit' => array());
|
|
|
|
+// dsm($newForm);
|
|
|
|
+ return $newForm;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Transform a form element for QBF.
|
|
|
|
+ *
|
|
|
|
+ * QBF-specific properties are:
|
|
|
|
+ * - #qbf : array of properties
|
|
|
|
+ * - #level: only within #qbf
|
|
|
|
+ *
|
|
|
|
+ * @see QBF_* constants
|
|
|
|
+ *
|
|
|
|
+ * @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 $arDefaultTypeTransformations = 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 $arDefaultPropertyTransformations = 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 $arKillerProperties = array
|
|
|
|
+ (
|
|
|
|
+ '#disabled' => TRUE,
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // Transform type
|
|
|
|
+ $sourceType = $element['#type'];
|
|
|
|
+ // .. Default transformation
|
|
|
|
+ $destType = array_key_exists($sourceType, $arDefaultTypeTransformations)
|
|
|
|
+ ? $arDefaultTypeTransformations[$sourceType]
|
|
|
|
+ : $sourceType;
|
|
|
|
+ // .. Apply form-defined type override
|
|
|
|
+ if (isset($element['#qbf']['#type']))
|
|
|
|
+ {
|
|
|
|
+ $destType = $element['#qbf']['#type'];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (is_null($destType))
|
|
|
|
+ {
|
|
|
|
+ $ret = NULL;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ $ret = $element;
|
|
|
|
+ $ret['#type'] = $destType;
|
|
|
|
+ if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
|
|
|
|
+ {
|
|
|
|
+ $ret = NULL;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ foreach (element_properties($element) as $propertyName)
|
|
|
|
+ {
|
|
|
|
+ // Apply killer properties first to avoid useless work
|
|
|
|
+ if (array_key_exists($propertyName, $arKillerProperties)
|
|
|
|
+ && ($element[$propertyName] = $arKillerProperties[$propertyName]))
|
|
|
|
+ {
|
|
|
|
+ $ret = NULL;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Now transform or copy remaining properties
|
|
|
|
+ if (array_key_exists($propertyName, $arDefaultPropertyTransformations))
|
|
|
|
+ {
|
|
|
|
+ $ret[$propertyName] = $arDefaultPropertyTransformations[$propertyName];
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ $ret[$propertyName] = $element[$propertyName];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // And apply form-defined property overrides
|
|
|
|
+ if ($propertyName == '#qbf')
|
|
|
|
+ {
|
|
|
|
+ foreach ($element[$propertyName] as $overrideName => $overrideValue)
|
|
|
|
+ {
|
|
|
|
+ $ret[$overrideName] = $overrideValue;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Recursively transform children
|
|
|
|
+ foreach (element_children($element) as $childName)
|
|
|
|
+ {
|
|
|
|
+ $child = _qbf_transform_element($childName, $element[$childName]);
|
|
|
|
+ if (is_null($child))
|
|
|
|
+ {
|
|
|
|
+ unset($ret[$childName]);
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ $ret[$childName] = $child;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //dsm(array('key' => $key, 'transformed element' => $ret));
|
|
|
|
+ return $ret;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Implement hook_perm().
|
|
|
|
+ *
|
|
|
|
+ * @return array
|
|
|
|
+ */
|
|
|
|
+function qbf_perm()
|
|
|
|
+ {
|
|
|
|
+ $ret = array
|
|
|
|
+ (
|
|
|
|
+ QBF_PERM_QUERY,
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ return $ret;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Implement hook_forms().
|
|
|
|
+ *
|
|
|
|
+ * @return array
|
|
|
|
+ */
|
|
|
|
+function qbf_forms()
|
|
|
|
+ {
|
|
|
|
+ $hookName = 'qbf_register';
|
|
|
|
+
|
|
|
|
+ foreach (module_implements($hookName) as $module)
|
|
|
|
+ {
|
|
|
|
+ foreach (module_invoke($module, $hookName) as $formName)
|
|
|
|
+ {
|
|
|
|
+ $forms["qbf_$formName"] = array
|
|
|
|
+ (
|
|
|
|
+ 'callback' => 'qbf_transform_form',
|
|
|
|
+ 'callback arguments' => array($formName),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return $forms;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Insert the query results at the bottom of the query form.
|
|
|
|
+ *
|
|
|
|
+ * @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 emtpy, we are indeed querying
|
|
|
|
+ $arQuery = _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($arQuery);
|
|
|
|
+ }
|
|
|
|
+ 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.
|
|
|
|
+ *
|
|
|
|
+ * @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 $childName)
|
|
|
|
+ {
|
|
|
|
+ $ret += _qbf_extract_query($form[$childName], $form_values);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return $ret;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Provide an optional automatic mapping mechanism for query building.
|
|
|
|
+ *
|
|
|
|
+ * This function takes a partly built query map $arQueryMap, and a defaults
|
|
|
|
+ * array to complete it in $arDefaults, and returns a fully built query array
|
|
|
|
+ * ready to be used for querying.
|
|
|
|
+ *
|
|
|
|
+ * @param array $arQuery
|
|
|
|
+ * @param array $arDefaults
|
|
|
|
+ * @return array
|
|
|
|
+ */
|
|
|
|
+function qbf_query_mapper($arQueryMap = array(), $arDefaults = array())
|
|
|
|
+ {
|
|
|
|
+ $ret = array();
|
|
|
|
+
|
|
|
|
+ foreach ($arQueryMap as $name => $value)
|
|
|
|
+ {
|
|
|
|
+ if (!is_array($value)) // accept NULL, empty strings...
|
|
|
|
+ {
|
|
|
|
+ $value = array();
|
|
|
|
+ }
|
|
|
|
+ $item = $value;
|
|
|
|
+
|
|
|
|
+ foreach ($arDefaults as $defaultKey => $defaultValue)
|
|
|
|
+ {
|
|
|
|
+ if (!array_key_exists($defaultKey, $item))
|
|
|
|
+ {
|
|
|
|
+ $item[$defaultKey] = is_null($defaultValue)
|
|
|
|
+ ? $name
|
|
|
|
+ : $defaultValue;
|
|
|
|
+ }
|
|
|
|
+ // 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_load().
|
|
|
|
+ * For other cases, the mechanisms built within FAPI should be used instead.
|
|
|
|
+ *
|
|
|
|
+ * @see qbf_load()
|
|
|
|
+ *
|
|
|
|
+ * @param array $form
|
|
|
|
+ * @param array $form_values
|
|
|
|
+ * @return array The modified form
|
|
|
|
+ */
|
|
|
|
+function qbf_import_values($element, $form_values)
|
|
|
|
+ {
|
|
|
|
+ foreach (element_children($element) as $childName)
|
|
|
|
+ {
|
|
|
|
+ if (!empty($form_values[$childName]))
|
|
|
|
+ {
|
|
|
|
+ $element[$childName]['#qbf']['#default_value'] = $form_values[$childName];
|
|
|
|
+ }
|
|
|
|
+ $element[$childName] = qbf_import_values($element[$childName], $form_values);
|
|
|
|
+ }
|
|
|
|
+ return $element;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Load a saved QBF query.
|
|
|
|
+ *
|
|
|
|
+ * @see qbf_import_values()
|
|
|
|
+ *
|
|
|
|
+ * @param int $qid
|
|
|
|
+ * @return array A form_values array usable by qbf_import_values
|
|
|
|
+ */
|
|
|
|
+function qbf_load($qid)
|
|
|
|
+ {
|
|
|
|
+ $sq = 'SELECT qq.qid, qq.uid, qq.query '
|
|
|
|
+ . '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
|
|
|
|
+ if ($ret === FALSE)
|
|
|
|
+ {
|
|
|
|
+ $ret = NULL;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ $ret->query = unserialize($ret->query);
|
|
|
|
+ }
|
|
|
|
+ return $ret;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+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)
|
|
|
|
+ {
|
|
|
|
+ $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query '
|
|
|
|
+ . '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.
|
|
|
|
+ *
|
|
|
|
+ * @global $user
|
|
|
|
+ * @param string $form_id
|
|
|
|
+ * @param array $form_values
|
|
|
|
+ * @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
|
|
|
|
+ {
|
|
|
|
+ $sq = 'INSERT INTO {%s} (qid, uid, name, query) '
|
|
|
|
+ . "VALUES (%d, %d, '%s', '%s' ) ";
|
|
|
|
+ $ret = db_next_id('qbf_qid');
|
|
|
|
+ $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $form_values['save-name'], serialize($form_values));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return $ret;
|
|
|
|
+ }
|