qbf.module 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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.2 2008-10-03 13:48:59 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. * @ingroup hooks
  250. * @return array
  251. */
  252. function qbf_perm() {
  253. $ret = array
  254. (
  255. QBF_PERM_QUERY,
  256. );
  257. return $ret;
  258. }
  259. /**
  260. * Implement hook_forms().
  261. *
  262. * @ingroup forms
  263. * @ingroup hooks
  264. * @return array
  265. */
  266. function qbf_forms() {
  267. $hookName = 'qbf_register';
  268. foreach (module_implements($hookName) as $module)
  269. {
  270. foreach (module_invoke($module, $hookName) as $formName)
  271. {
  272. $forms["qbf_$formName"] = array
  273. (
  274. 'callback' => 'qbf_transform_form',
  275. 'callback arguments' => array($formName),
  276. );
  277. }
  278. }
  279. return $forms;
  280. }
  281. /**
  282. * Insert the query results at the bottom of the query form.
  283. *
  284. * @ingroup forms
  285. * @param array $form
  286. * @param array $form_values
  287. * @return array
  288. */
  289. function qbf_after_build($form, $form_values) {
  290. if (empty($form['#post']))
  291. {
  292. return $form;
  293. }
  294. // If #post is not emtpy, we are indeed querying
  295. $arQuery = _qbf_extract_query($form, $form_values);
  296. /* This function is called at the end of the form building process, which
  297. * means that child properties of #qbf have already been upgraded to element
  298. * properties. So we look for $form['#callback'] and not
  299. * $form['#qbf']['#callback']
  300. */
  301. if (isset($form['#callback']) && function_exists($function = $form['#callback']))
  302. {
  303. $results = $function($arQuery);
  304. }
  305. else
  306. {
  307. drupal_set_message(t('QBF: incorrect callback function for search'), 'error');
  308. }
  309. $form['qbf_query_results'] = array
  310. (
  311. '#type' => 'markup',
  312. '#value' => $results,
  313. '#weight' => 10,
  314. );
  315. return $form;
  316. }
  317. /**
  318. * Recursively build a query array from the form and its values
  319. *
  320. * In the current version, element names are supposed to be unique, even at
  321. * different levels in the tree.
  322. *
  323. * @ingroup forms
  324. * @param array $form
  325. * @param array $form_values
  326. */
  327. function _qbf_extract_query($form, $form_values) {
  328. $name = $form['#parents'][0];
  329. // Elements which are removed or display-only have no place in the query
  330. if (array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
  331. && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL)
  332. {
  333. $ret = array($name => $form_values[$name]);
  334. }
  335. else
  336. {
  337. $ret = array();
  338. }
  339. // QBF level is not inherited, so this loop is outside the "if" above
  340. foreach (element_children($form) as $childName)
  341. {
  342. $ret += _qbf_extract_query($form[$childName], $form_values);
  343. }
  344. return $ret;
  345. }
  346. /**
  347. * Provide an optional automatic mapping mechanism for query building.
  348. *
  349. * This function takes a partly built query map $arQueryMap, and a defaults
  350. * array to complete it in $arDefaults, and returns a fully built query array
  351. * ready to be used for querying.
  352. *
  353. * @param array $arQuery
  354. * @param array $arDefaults
  355. * @return array
  356. */
  357. function qbf_query_mapper($arQueryMap = array(), $arDefaults = array()) {
  358. $ret = array();
  359. foreach ($arQueryMap as $name => $value)
  360. {
  361. if (!is_array($value)) // accept NULL, empty strings...
  362. {
  363. $value = array();
  364. }
  365. $item = $value;
  366. foreach ($arDefaults as $defaultKey => $defaultValue)
  367. {
  368. if (!array_key_exists($defaultKey, $item))
  369. {
  370. $item[$defaultKey] = is_null($defaultValue)
  371. ? $name
  372. : $defaultValue;
  373. }
  374. // else if is already in $item, so we don't touch it
  375. }
  376. $ret[$name] = $item;
  377. }
  378. return $ret;
  379. }
  380. /**
  381. * Load a form_values array into a form used by QBF.
  382. *
  383. * This is typically useful when loading saved queries using qbf_load().
  384. * For other cases, the mechanisms built within FAPI should be used instead.
  385. *
  386. * @see qbf_load()
  387. *
  388. * @ingroup forms
  389. * @param array $form
  390. * @param array $form_values
  391. * @return array The modified form
  392. */
  393. function qbf_import_values($element, $form_values) {
  394. foreach (element_children($element) as $childName)
  395. {
  396. if (!empty($form_values[$childName]))
  397. {
  398. $element[$childName]['#qbf']['#default_value'] = $form_values[$childName];
  399. }
  400. $element[$childName] = qbf_import_values($element[$childName], $form_values);
  401. }
  402. return $element;
  403. }
  404. /**
  405. * Load a saved QBF query.
  406. *
  407. * @see qbf_import_values()
  408. *
  409. * @param int $qid
  410. * @return array A form_values array usable by qbf_import_values
  411. */
  412. function qbf_load($qid) {
  413. $sq = 'SELECT qq.qid, qq.uid, qq.query '
  414. . 'FROM {%s} qq '
  415. . 'WHERE qq.qid = %d ';
  416. // db_rewrite_sql does not apply here until we add more advanced support for access control
  417. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  418. $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  419. if ($ret === FALSE)
  420. {
  421. $ret = NULL;
  422. }
  423. else
  424. {
  425. $ret->query = unserialize($ret->query);
  426. }
  427. return $ret;
  428. }
  429. /**
  430. * Submit handler for query save form.
  431. *
  432. * @ingroup forms
  433. * @param $form_id string
  434. * @param $form_values array
  435. * @return string
  436. */
  437. function qbf_submit($form_id, $form_values) {
  438. switch ($form_values['op'])
  439. {
  440. case t('Search'):
  441. $ret = FALSE;
  442. break;
  443. case t('Save query'):
  444. _qbf_save($form_id, $form_values);
  445. drupal_set_message(t('Your query was saved as "@name".',
  446. array('@name' => $form_values['save-name'])));
  447. global $user;
  448. $ret = "user/$user->uid/edit/job";;
  449. break;
  450. }
  451. //dsm(array('QS' => $form_values));
  452. return $ret;
  453. }
  454. /**
  455. * List queries owned by a given user.
  456. *
  457. * @param int $uid > 0
  458. * @return array
  459. */
  460. function qbf_get_queries_by_user($uid = NULL) {
  461. if (is_null($uid))
  462. {
  463. global $user;
  464. $uid = $user->uid;
  465. }
  466. $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query '
  467. . 'FROM {%s} qq '
  468. . 'WHERE qq.uid = %d '
  469. . 'ORDER BY qq.name ';
  470. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  471. $ret = array();
  472. while ($o = db_fetch_object($q))
  473. {
  474. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  475. }
  476. return $ret;
  477. }
  478. /**
  479. * Save a query and return its qid.
  480. *
  481. * @ingroup forms
  482. * @param $form_id string
  483. * @param $form_values array
  484. * @return int
  485. */
  486. function _qbf_save($form_id, $form_values) {
  487. global $user;
  488. if ($user->uid == 0)
  489. {
  490. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  491. drupal_set_message($warning, 'error');
  492. watchdog('qbf', $warning, WATCHDOG_WARNING);
  493. $ret = 0;
  494. }
  495. else
  496. {
  497. $sq = 'INSERT INTO {%s} (qid, uid, name, query) '
  498. ."VALUES (%d, %d, '%s', '%s' ) ";
  499. $ret = db_next_id('qbf_qid');
  500. $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $form_values['save-name'], serialize($form_values));
  501. }
  502. return $ret;
  503. }
  504. error_reporting($_qbf_er);