qbf.module 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. /**
  3. * Query By Form
  4. *
  5. * This module allows node modules to add a query by form tab for their node
  6. * types to the default search form
  7. *
  8. * @copyright 2008 Ouest Systemes Informatiques (OSInet)
  9. * @author Frederic G. MARAND
  10. * @version $Id: qbf.module,v 1.9 2008-09-02 08:25:24 marand Exp $
  11. * @license CeCILL 2.0
  12. * @package QBF
  13. */
  14. /**
  15. * Remove this element from the generated form
  16. */
  17. define('QBF_LEVEL_REMOVE', 0);
  18. /**
  19. * This element is only for display in the generated form: do not include it
  20. * in the query vector.
  21. */
  22. define('QBF_LEVEL_DISPLAY', 1);
  23. /**
  24. * Include this element in the generated form and in the query vector, but do
  25. * not mark it as required.
  26. */
  27. define('QBF_LEVEL_OPTIONAL', 2);
  28. /**
  29. * Include this element in the generated form and in the query vector, and
  30. * mark it as required.
  31. */
  32. define('QBF_LEVEL_REQUIRED', 3);
  33. /**
  34. * The main QBF path
  35. */
  36. define('QBF_PATH_MAIN', 'qbf');
  37. /**
  38. * Authorize use of QBF searches
  39. */
  40. define('QBF_PERM_QUERY', 'use QBF search functions');
  41. /**
  42. * Authorize QBF administration
  43. */
  44. define('QBF_PERM_ADMIN', 'administer QBF');
  45. /**
  46. * The name of the table used to store queries
  47. */
  48. define('QBF_TABLE_NAME', 'qbf_queries');
  49. /**
  50. * Transform a form array for QBF.
  51. *
  52. * This function obtains the form array using Forms API, and transforms it by
  53. * modifying widgets to other types where needed.
  54. *
  55. * Any additional parameter passed to the function is transmitted to the form
  56. * generating function.
  57. *
  58. * @param string $form_id
  59. * @return array
  60. */
  61. function qbf_transform_form($form_id)
  62. {
  63. $arArgs = func_get_args();
  64. //dsm(array('qtf' => $arArgs));
  65. // Fetch the basic form and rename it, passing it the caller's arguments
  66. $form = call_user_func_array('drupal_retrieve_form', $arArgs);
  67. $newFormId = "qbf_$form_id";
  68. // Only keep the children of the form and QBF properties on the form itself
  69. $elements = array();
  70. $newForm = array();
  71. $newForm['#qbf_source_form_id'] = $form_id;
  72. if (in_array('#qbf', element_properties($form)))
  73. {
  74. $newForm += $form['#qbf'];
  75. }
  76. foreach (element_children($form) as $key)
  77. {
  78. // dsm("Transforming $key, type " . $form[$key]['#type']);
  79. $newElement = _qbf_transform_element($key, $form[$key]);
  80. if (!is_null($newElement))
  81. {
  82. $newForm[$key] = $newElement;
  83. }
  84. }
  85. $newForm['#id'] = $newFormId;
  86. $newForm['#multistep'] = TRUE;
  87. // Do not set #redirect, even to FALSE (submit handlers)
  88. // $newForm['#redirect'] = FALSE;
  89. $newForm['#after_build'][] = 'qbf_after_build';
  90. $newForm['#submit'] = array('qbf_submit' => array());
  91. // dsm($newForm);
  92. return $newForm;
  93. }
  94. /**
  95. * Transform a form element for QBF.
  96. *
  97. * QBF-specific properties are:
  98. * - #qbf : array of properties
  99. * - #level: only within #qbf
  100. *
  101. * @see QBF_* constants
  102. *
  103. * @param string $key
  104. * @param array $element
  105. * @return void
  106. */
  107. function _qbf_transform_element($key, $element)
  108. {
  109. // dsm(array('key' => $key, 'element' => $element));
  110. /**
  111. * List default type transformations applied to widget by FAPI.
  112. *
  113. * Types without a default transformation are not transformed
  114. */
  115. static $arDefaultTypeTransformations = array
  116. (
  117. 'button' => NULL,
  118. 'file' => NULL,
  119. // 'hidden' => NULL,
  120. 'markup' => NULL,
  121. 'password' => NULL,
  122. 'radio' => NULL,
  123. 'submit' => NULL,
  124. 'textarea' => 'textfield',
  125. // 'value' => 'value',
  126. );
  127. /**
  128. * List default property transformations applied to widget by FAPI property.
  129. *
  130. * Properties without a default transformation are not transformed
  131. */
  132. static $arDefaultPropertyTransformations = array
  133. (
  134. // Standard properties
  135. '#action' => NULL,
  136. '#after_build' => NULL,
  137. '#base' => NULL,
  138. '#button_type' => NULL,
  139. '#built' => NULL,
  140. '#description' => NULL,
  141. '#method' => NULL,
  142. '#parents' => NULL,
  143. '#redirect' => NULL,
  144. '#ref' => NULL,
  145. '#required' => NULL,
  146. '#rows' => NULL,
  147. '#submit' => NULL,
  148. '#tree' => NULL,
  149. '#validate' => NULL,
  150. );
  151. /**
  152. * List properties causing causing element removal.
  153. *
  154. * The key is the property name, the value is the one causing removal.
  155. */
  156. static $arKillerProperties = array
  157. (
  158. '#disabled' => TRUE,
  159. );
  160. // Transform type
  161. $sourceType = $element['#type'];
  162. // .. Default transformation
  163. $destType = array_key_exists($sourceType, $arDefaultTypeTransformations)
  164. ? $arDefaultTypeTransformations[$sourceType]
  165. : $sourceType;
  166. // .. Apply form-defined type override
  167. if (isset($element['#qbf']['#type']))
  168. {
  169. $destType = $element['#qbf']['#type'];
  170. }
  171. if (is_null($destType))
  172. {
  173. $ret = NULL;
  174. }
  175. else
  176. {
  177. $ret = $element;
  178. $ret['#type'] = $destType;
  179. if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
  180. {
  181. $ret = NULL;
  182. }
  183. else
  184. {
  185. foreach (element_properties($element) as $propertyName)
  186. {
  187. // Apply killer properties first to avoid useless work
  188. if (array_key_exists($propertyName, $arKillerProperties)
  189. && ($element[$propertyName] = $arKillerProperties[$propertyName]))
  190. {
  191. $ret = NULL;
  192. break;
  193. }
  194. // Now transform or copy remaining properties
  195. if (array_key_exists($propertyName, $arDefaultPropertyTransformations))
  196. {
  197. $ret[$propertyName] = $arDefaultPropertyTransformations[$propertyName];
  198. }
  199. else
  200. {
  201. $ret[$propertyName] = $element[$propertyName];
  202. }
  203. // And apply form-defined property overrides
  204. if ($propertyName == '#qbf')
  205. {
  206. foreach ($element[$propertyName] as $overrideName => $overrideValue)
  207. {
  208. $ret[$overrideName] = $overrideValue;
  209. }
  210. }
  211. }
  212. // Recursively transform children
  213. foreach (element_children($element) as $childName)
  214. {
  215. $child = _qbf_transform_element($childName, $element[$childName]);
  216. if (is_null($child))
  217. {
  218. unset($ret[$childName]);
  219. }
  220. else
  221. {
  222. $ret[$childName] = $child;
  223. }
  224. }
  225. }
  226. }
  227. //dsm(array('key' => $key, 'transformed element' => $ret));
  228. return $ret;
  229. }
  230. /**
  231. * Implement hook_perm().
  232. *
  233. * @return array
  234. */
  235. function qbf_perm()
  236. {
  237. $ret = array
  238. (
  239. QBF_PERM_QUERY,
  240. );
  241. return $ret;
  242. }
  243. /**
  244. * Implement hook_forms().
  245. *
  246. * @return array
  247. */
  248. function qbf_forms()
  249. {
  250. $hookName = 'qbf_register';
  251. foreach (module_implements($hookName) as $module)
  252. {
  253. foreach (module_invoke($module, $hookName) as $formName)
  254. {
  255. $forms["qbf_$formName"] = array
  256. (
  257. 'callback' => 'qbf_transform_form',
  258. 'callback arguments' => array($formName),
  259. );
  260. }
  261. }
  262. return $forms;
  263. }
  264. /**
  265. * Insert the query results at the bottom of the query form.
  266. *
  267. * @param array $form
  268. * @param array $form_values
  269. * @return array
  270. */
  271. function qbf_after_build($form, $form_values)
  272. {
  273. if (empty($form['#post']))
  274. {
  275. return $form;
  276. }
  277. // If #post is not emtpy, we are indeed querying
  278. $arQuery = _qbf_extract_query($form, $form_values);
  279. /* This function is called at the end of the form building process, which
  280. * means that child properties of #qbf have already been upgraded to element
  281. * properties. So we look for $form['#callback'] and not
  282. * $form['#qbf']['#callback']
  283. */
  284. if (isset($form['#callback']) && function_exists($function = $form['#callback']))
  285. {
  286. $results = $function($arQuery);
  287. }
  288. else
  289. {
  290. drupal_set_message(t('QBF: incorrect callback function for search'), 'error');
  291. }
  292. $form['qbf_query_results'] = array
  293. (
  294. '#type' => 'markup',
  295. '#value' => $results,
  296. '#weight' => 10,
  297. );
  298. return $form;
  299. }
  300. /**
  301. * Recursively build a query array from the form and its values
  302. *
  303. * In the current version, element names are supposed to be unique, even at
  304. * different levels in the tree.
  305. *
  306. * @param array $form
  307. * @param array $form_values
  308. */
  309. function _qbf_extract_query($form, $form_values)
  310. {
  311. $name = $form['#parents'][0];
  312. // Elements which are removed or display-only have no place in the query
  313. if (array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
  314. && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL)
  315. {
  316. $ret = array($name => $form_values[$name]);
  317. }
  318. else
  319. {
  320. $ret = array();
  321. }
  322. // QBF level is not inherited, so this loop is outside the "if" above
  323. foreach (element_children($form) as $childName)
  324. {
  325. $ret += _qbf_extract_query($form[$childName], $form_values);
  326. }
  327. return $ret;
  328. }
  329. /**
  330. * Provide an optional automatic mapping mechanism for query building.
  331. *
  332. * This function takes a partly built query map $arQueryMap, and a defaults
  333. * array to complete it in $arDefaults, and returns a fully built query array
  334. * ready to be used for querying.
  335. *
  336. * @param array $arQuery
  337. * @param array $arDefaults
  338. * @return array
  339. */
  340. function qbf_query_mapper($arQueryMap = array(), $arDefaults = array())
  341. {
  342. $ret = array();
  343. foreach ($arQueryMap as $name => $value)
  344. {
  345. if (!is_array($value)) // accept NULL, empty strings...
  346. {
  347. $value = array();
  348. }
  349. $item = $value;
  350. foreach ($arDefaults as $defaultKey => $defaultValue)
  351. {
  352. if (!array_key_exists($defaultKey, $item))
  353. {
  354. $item[$defaultKey] = is_null($defaultValue)
  355. ? $name
  356. : $defaultValue;
  357. }
  358. // else if is already in $item, so we don't touch it
  359. }
  360. $ret[$name] = $item;
  361. }
  362. return $ret;
  363. }
  364. /**
  365. * Load a form_values array into a form used by QBF.
  366. *
  367. * This is typically useful when loading saved queries using qbf_load().
  368. * For other cases, the mechanisms built within FAPI should be used instead.
  369. *
  370. * @see qbf_load()
  371. *
  372. * @param array $form
  373. * @param array $form_values
  374. * @return array The modified form
  375. */
  376. function qbf_import_values($element, $form_values)
  377. {
  378. foreach (element_children($element) as $childName)
  379. {
  380. if (!empty($form_values[$childName]))
  381. {
  382. $element[$childName]['#qbf']['#default_value'] = $form_values[$childName];
  383. }
  384. $element[$childName] = qbf_import_values($element[$childName], $form_values);
  385. }
  386. return $element;
  387. }
  388. /**
  389. * Load a saved QBF query.
  390. *
  391. * @see qbf_import_values()
  392. *
  393. * @param int $qid
  394. * @return array A form_values array usable by qbf_import_values
  395. */
  396. function qbf_load($qid)
  397. {
  398. $sq = 'SELECT qq.qid, qq.uid, qq.query '
  399. . 'FROM {%s} qq '
  400. . 'WHERE qq.qid = %d ';
  401. // db_rewrite_sql does not apply here until we add more advanced support for access control
  402. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  403. $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  404. if ($ret === FALSE)
  405. {
  406. $ret = NULL;
  407. }
  408. else
  409. {
  410. $ret->query = unserialize($ret->query);
  411. }
  412. return $ret;
  413. }
  414. function qbf_submit($form_id, $form_values)
  415. {
  416. switch ($form_values['op'])
  417. {
  418. case t('Search'):
  419. $ret = FALSE;
  420. break;
  421. case t('Save query'):
  422. _qbf_save($form_id, $form_values);
  423. drupal_set_message(t('Your query was saved as "@name".',
  424. array('@name' => $form_values['save-name'])));
  425. global $user;
  426. $ret = "user/$user->uid/edit/job";;
  427. break;
  428. }
  429. //dsm(array('QS' => $form_values));
  430. return $ret;
  431. }
  432. /**
  433. * List queries owned by a given user.
  434. *
  435. * @param int $uid > 0
  436. * @return array
  437. */
  438. function qbf_get_queries_by_user($uid)
  439. {
  440. $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query '
  441. . 'FROM {%s} qq '
  442. . 'WHERE qq.uid = %d '
  443. . 'ORDER BY qq.name ';
  444. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  445. $ret = array();
  446. while ($o = db_fetch_object($q))
  447. {
  448. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  449. }
  450. return $ret;
  451. }
  452. /**
  453. * Save a query and return its qid.
  454. *
  455. * @global $user
  456. * @param string $form_id
  457. * @param array $form_values
  458. * @return int
  459. */
  460. function _qbf_save($form_id, $form_values)
  461. {
  462. global $user;
  463. if ($user->uid == 0)
  464. {
  465. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  466. drupal_set_message($warning, 'error');
  467. watchdog('qbf', $warning, WATCHDOG_WARNING);
  468. $ret = 0;
  469. }
  470. else
  471. {
  472. $sq = 'INSERT INTO {%s} (qid, uid, name, query) '
  473. . "VALUES (%d, %d, '%s', '%s' ) ";
  474. $ret = db_next_id('qbf_qid');
  475. $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $form_values['save-name'], serialize($form_values));
  476. }
  477. return $ret;
  478. }