qbf.module 13 KB

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