qbf.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  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.3 2008-10-03 17:40:40 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. *
  46. * It MUST be a single component path, without a "/", otherwise qbf_menu() will
  47. * need to be changed.
  48. *
  49. * @ingroup paths
  50. * @see qbf_menu()
  51. */
  52. define('QBF_PATH_MAIN', 'qbf');
  53. /**
  54. * The QBF autocomplete path for search fields
  55. * @ingroup paths
  56. */
  57. define('QBF_PATH_AC', 'qbf/ac');
  58. /**
  59. * Authorize use of QBF searches
  60. */
  61. define('QBF_PERM_QUERY', 'use QBF search functions');
  62. /**
  63. * Authorize QBF administration
  64. */
  65. define('QBF_PERM_ADMIN', 'administer QBF');
  66. /**
  67. * The name of the table used to store queries
  68. */
  69. define('QBF_TABLE_NAME', 'qbf_queries');
  70. /**
  71. * Notify owner about saved query deletions, variable name.
  72. */
  73. define('QBF_VAR_NOTIFY_DELETE', 'qbf_notify_delete');
  74. /**
  75. * Notify owner about saved query deletions, default value.
  76. */
  77. define('QBF_DEF_NOTIFY_DELETE', FALSE);
  78. /**
  79. * Transform a form array for QBF.
  80. *
  81. * This function obtains the form array using Forms API, and transforms it by
  82. * modifying widgets to other types where needed.
  83. *
  84. * Any additional parameter passed to the function is transmitted to the form
  85. * generating function.
  86. *
  87. * @ingroup forms
  88. * @param string $form_id
  89. * @return array
  90. */
  91. function qbf_transform_form($form_id) {
  92. $arArgs = func_get_args();
  93. //dsm(array('qtf' => $arArgs));
  94. // Fetch the basic form and rename it, passing it the caller's arguments
  95. $form = call_user_func_array('drupal_retrieve_form', $arArgs);
  96. $newFormId = "qbf_$form_id";
  97. // Only keep the children of the form and QBF properties on the form itself
  98. $elements = array();
  99. $newForm = array();
  100. $newForm['#qbf_source_form_id'] = $form_id;
  101. if (in_array('#qbf', element_properties($form)))
  102. {
  103. $newForm += $form['#qbf'];
  104. }
  105. foreach (element_children($form) as $key)
  106. {
  107. // dsm("Transforming $key, type " . $form[$key]['#type']);
  108. $newElement = _qbf_transform_element($key, $form[$key]);
  109. if (!is_null($newElement))
  110. {
  111. $newForm[$key] = $newElement;
  112. }
  113. }
  114. $newForm['#id'] = $newFormId;
  115. $newForm['#multistep'] = TRUE;
  116. // Do not set #redirect, even to FALSE (submit handlers)
  117. // $newForm['#redirect'] = FALSE;
  118. $newForm['#after_build'][] = 'qbf_after_build';
  119. $newForm['#submit'] = array('qbf_submit' => array());
  120. // dsm($newForm);
  121. return $newForm;
  122. }
  123. /**
  124. * Transform a form element for QBF.
  125. *
  126. * QBF-specific properties are:
  127. * - #qbf : array of properties
  128. * - #level: only within #qbf
  129. *
  130. * See QBF_* constants
  131. *
  132. * @ingroup forms
  133. * @param string $key
  134. * @param array $element
  135. * @return void
  136. */
  137. function _qbf_transform_element($key, $element) {
  138. // dsm(array('key' => $key, 'element' => $element));
  139. /**
  140. * List default type transformations applied to widget by FAPI.
  141. *
  142. * Types without a default transformation are not transformed
  143. */
  144. static $arDefaultTypeTransformations = array
  145. (
  146. 'button' => NULL,
  147. 'file' => NULL,
  148. // 'hidden' => NULL,
  149. 'markup' => NULL,
  150. 'password' => NULL,
  151. 'radio' => NULL,
  152. 'submit' => NULL,
  153. 'textarea' => 'textfield',
  154. // 'value' => 'value',
  155. );
  156. /**
  157. * List default property transformations applied to widget by FAPI property.
  158. *
  159. * Properties without a default transformation are not transformed
  160. */
  161. static $arDefaultPropertyTransformations = array
  162. (
  163. // Standard properties
  164. '#action' => NULL,
  165. '#after_build' => NULL,
  166. '#base' => NULL,
  167. '#button_type' => NULL,
  168. '#built' => NULL,
  169. '#description' => NULL,
  170. '#method' => NULL,
  171. '#parents' => NULL,
  172. '#redirect' => NULL,
  173. '#ref' => NULL,
  174. '#required' => NULL,
  175. '#rows' => NULL,
  176. '#submit' => NULL,
  177. '#tree' => NULL,
  178. '#validate' => NULL,
  179. );
  180. /**
  181. * List properties causing causing element removal.
  182. *
  183. * The key is the property name, the value is the one causing removal.
  184. */
  185. static $arKillerProperties = array
  186. (
  187. '#disabled' => TRUE,
  188. );
  189. // Transform type
  190. $sourceType = $element['#type'];
  191. // .. Default transformation
  192. $destType = array_key_exists($sourceType, $arDefaultTypeTransformations)
  193. ? $arDefaultTypeTransformations[$sourceType]
  194. : $sourceType;
  195. // .. Apply form-defined type override
  196. if (isset($element['#qbf']['#type']))
  197. {
  198. $destType = $element['#qbf']['#type'];
  199. }
  200. if (is_null($destType))
  201. {
  202. $ret = NULL;
  203. }
  204. else
  205. {
  206. $ret = $element;
  207. $ret['#type'] = $destType;
  208. if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
  209. {
  210. $ret = NULL;
  211. }
  212. else
  213. {
  214. foreach (element_properties($element) as $propertyName)
  215. {
  216. // Apply killer properties first to avoid useless work
  217. if (array_key_exists($propertyName, $arKillerProperties)
  218. && ($element[$propertyName] = $arKillerProperties[$propertyName]))
  219. {
  220. $ret = NULL;
  221. break;
  222. }
  223. // Now transform or copy remaining properties
  224. if (array_key_exists($propertyName, $arDefaultPropertyTransformations))
  225. {
  226. $ret[$propertyName] = $arDefaultPropertyTransformations[$propertyName];
  227. }
  228. else
  229. {
  230. $ret[$propertyName] = $element[$propertyName];
  231. }
  232. // And apply form-defined property overrides
  233. if ($propertyName == '#qbf')
  234. {
  235. foreach ($element[$propertyName] as $overrideName => $overrideValue)
  236. {
  237. $ret[$overrideName] = $overrideValue;
  238. }
  239. }
  240. }
  241. // Recursively transform children
  242. foreach (element_children($element) as $childName)
  243. {
  244. $child = _qbf_transform_element($childName, $element[$childName]);
  245. if (is_null($child))
  246. {
  247. unset($ret[$childName]);
  248. }
  249. else
  250. {
  251. $ret[$childName] = $child;
  252. }
  253. }
  254. }
  255. }
  256. //dsm(array('key' => $key, 'transformed element' => $ret));
  257. return $ret;
  258. }
  259. /**
  260. * Implement hook_perm().
  261. *
  262. * @ingroup hooks
  263. * @return array
  264. */
  265. function qbf_perm() {
  266. $ret = array
  267. (
  268. QBF_PERM_QUERY,
  269. );
  270. return $ret;
  271. }
  272. /**
  273. * Implement hook_forms().
  274. *
  275. * @ingroup forms
  276. * @ingroup hooks
  277. * @return array
  278. */
  279. function qbf_forms() {
  280. $hookName = 'qbf_register';
  281. foreach (module_implements($hookName) as $module)
  282. {
  283. foreach (module_invoke($module, $hookName) as $formName)
  284. {
  285. $forms["qbf_$formName"] = array
  286. (
  287. 'callback' => 'qbf_transform_form',
  288. 'callback arguments' => array($formName),
  289. );
  290. }
  291. }
  292. return $forms;
  293. }
  294. /**
  295. * Insert the query results at the bottom of the query form.
  296. *
  297. * @ingroup forms
  298. * @param array $form
  299. * @param array $form_values
  300. * @return array
  301. */
  302. function qbf_after_build($form, $form_values) {
  303. if (empty($form['#post']))
  304. {
  305. return $form;
  306. }
  307. // If #post is not emtpy, we are indeed querying
  308. $arQuery = _qbf_extract_query($form, $form_values);
  309. /* This function is called at the end of the form building process, which
  310. * means that child properties of #qbf have already been upgraded to element
  311. * properties. So we look for $form['#callback'] and not
  312. * $form['#qbf']['#callback']
  313. */
  314. if (isset($form['#callback']) && function_exists($function = $form['#callback']))
  315. {
  316. $results = $function($arQuery);
  317. }
  318. else
  319. {
  320. drupal_set_message(t('QBF: incorrect callback function for search'), 'error');
  321. }
  322. $form['qbf_query_results'] = array
  323. (
  324. '#type' => 'markup',
  325. '#value' => $results,
  326. '#weight' => 10,
  327. );
  328. return $form;
  329. }
  330. /**
  331. * Recursively build a query array from the form and its values
  332. *
  333. * In the current version, element names are supposed to be unique, even at
  334. * different levels in the tree.
  335. *
  336. * @ingroup forms
  337. * @param array $form
  338. * @param array $form_values
  339. */
  340. function _qbf_extract_query($form, $form_values) {
  341. $name = $form['#parents'][0];
  342. // Elements which are removed or display-only have no place in the query
  343. if (array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
  344. && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL)
  345. {
  346. $ret = array($name => $form_values[$name]);
  347. }
  348. else
  349. {
  350. $ret = array();
  351. }
  352. // QBF level is not inherited, so this loop is outside the "if" above
  353. foreach (element_children($form) as $childName)
  354. {
  355. $ret += _qbf_extract_query($form[$childName], $form_values);
  356. }
  357. return $ret;
  358. }
  359. /**
  360. * Provide an optional automatic mapping mechanism for query building.
  361. *
  362. * This function takes a partly built query map $arQueryMap, and a defaults
  363. * array to complete it in $arDefaults, and returns a fully built query array
  364. * ready to be used for querying.
  365. *
  366. * @param array $arQuery
  367. * @param array $arDefaults
  368. * @return array
  369. */
  370. function qbf_query_mapper($arQueryMap = array(), $arDefaults = array()) {
  371. $ret = array();
  372. foreach ($arQueryMap as $name => $value)
  373. {
  374. if (!is_array($value)) // accept NULL, empty strings...
  375. {
  376. $value = array();
  377. }
  378. $item = $value;
  379. foreach ($arDefaults as $defaultKey => $defaultValue)
  380. {
  381. if (!array_key_exists($defaultKey, $item))
  382. {
  383. $item[$defaultKey] = is_null($defaultValue)
  384. ? $name
  385. : $defaultValue;
  386. }
  387. // else if is already in $item, so we don't touch it
  388. }
  389. $ret[$name] = $item;
  390. }
  391. return $ret;
  392. }
  393. /**
  394. * Load a form_values array into a form used by QBF.
  395. *
  396. * This is typically useful when loading saved queries using qbf_load().
  397. * For other cases, the mechanisms built within FAPI should be used instead.
  398. *
  399. * @see qbf_load()
  400. *
  401. * @ingroup forms
  402. * @param array $form
  403. * @param array $form_values
  404. * @return array The modified form
  405. */
  406. function qbf_import_values($element, $form_values) {
  407. foreach (element_children($element) as $childName)
  408. {
  409. if (!empty($form_values[$childName]))
  410. {
  411. $element[$childName]['#qbf']['#default_value'] = $form_values[$childName];
  412. }
  413. $element[$childName] = qbf_import_values($element[$childName], $form_values);
  414. }
  415. return $element;
  416. }
  417. /**
  418. * Load a saved QBF query.
  419. *
  420. * @see qbf_import_values()
  421. *
  422. * @param int $qid
  423. * @return array A form_values array usable by qbf_import_values
  424. */
  425. function qbf_load($qid) {
  426. $sq = 'SELECT qq.qid, qq.uid, qq.query, qq.name '
  427. . 'FROM {%s} qq '
  428. . 'WHERE qq.qid = %d ';
  429. // db_rewrite_sql does not apply here until we add more advanced support for access control
  430. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  431. $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  432. if ($ret === NULL) // FALSE does not happen
  433. {
  434. $ret = NULL;
  435. }
  436. else
  437. {
  438. $ret->query = unserialize($ret->query);
  439. }
  440. return $ret;
  441. }
  442. /**
  443. * Submit handler for query save form.
  444. *
  445. * @ingroup forms
  446. * @param $form_id string
  447. * @param $form_values array
  448. * @return string
  449. */
  450. function qbf_submit($form_id, $form_values) {
  451. switch ($form_values['op'])
  452. {
  453. case t('Search'):
  454. $ret = FALSE;
  455. break;
  456. case t('Save query'):
  457. _qbf_save($form_id, $form_values);
  458. drupal_set_message(t('Your query was saved as "@name".',
  459. array('@name' => $form_values['save-name'])));
  460. global $user;
  461. $ret = "user/$user->uid/edit/job";;
  462. break;
  463. }
  464. //dsm(array('QS' => $form_values));
  465. return $ret;
  466. }
  467. /**
  468. * List queries owned by a given user.
  469. *
  470. * @param int $uid > 0
  471. * @return array
  472. */
  473. function qbf_get_queries_by_user($uid = NULL) {
  474. if (is_null($uid))
  475. {
  476. global $user;
  477. $uid = $user->uid;
  478. }
  479. $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query '
  480. . 'FROM {%s} qq '
  481. . 'WHERE qq.uid = %d '
  482. . 'ORDER BY qq.name ';
  483. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  484. $ret = array();
  485. while ($o = db_fetch_object($q))
  486. {
  487. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  488. }
  489. return $ret;
  490. }
  491. /**
  492. * Save a query and return its qid.
  493. *
  494. * @ingroup forms
  495. * @param $form_id string
  496. * @param $form_values array
  497. * @return int
  498. */
  499. function _qbf_save($form_id, $form_values) {
  500. global $user;
  501. if ($user->uid == 0)
  502. {
  503. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  504. drupal_set_message($warning, 'error');
  505. watchdog('qbf', $warning, WATCHDOG_WARNING);
  506. $ret = 0;
  507. }
  508. else
  509. {
  510. $sq = 'INSERT INTO {%s} (qid, uid, name, query) '
  511. ."VALUES (%d, %d, '%s', '%s' ) ";
  512. $ret = db_next_id('qbf_qid');
  513. $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $form_values['save-name'], serialize($form_values));
  514. }
  515. return $ret;
  516. }
  517. /**
  518. * Implement hook_menu().
  519. *
  520. * @param $may_cache boolean
  521. * @return array
  522. */
  523. function qbf_menu($may_cache) {
  524. $items = array();
  525. if ($may_cache)
  526. {
  527. }
  528. else
  529. {
  530. if ((arg(0) == QBF_PATH_MAIN) && is_numeric(arg(1)) && arg(1) > 0 && arg(2) == 'delete')
  531. {
  532. $qid = arg(1);
  533. $querorAccess = user_access(QBF_PERM_QUERY);
  534. $items[] = array
  535. (
  536. 'path' => QBF_PATH_MAIN . '/' . $qid . '/delete',
  537. 'type' => MENU_CALLBACK,
  538. 'access' => $querorAccess,
  539. 'callback' => '_qbf_query_delete',
  540. 'callback arguments' => array($qid),
  541. );
  542. }
  543. }
  544. return $items;
  545. }
  546. /**
  547. * Delete a query by qid
  548. *
  549. * $qid has been tested in qbf_menu() to be a positive integer, so it is a safe
  550. * number, but we still need to know more about it.
  551. *
  552. * @param $qid integer
  553. */
  554. function _qbf_query_delete($qid) {
  555. global $user;
  556. $query = qbf_load($qid);
  557. $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);
  558. $link = l($qid, QBF_PATH_MAIN . '/' . $qid . '/delete');
  559. // @todo Check safety, this seem dangerous
  560. $usArgs = func_get_args();
  561. $path = implode('/', array_slice($usArgs, 1));
  562. // only valid if valid query, and owner or admin
  563. if (isset($query->uid) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN)))
  564. {
  565. $sq = 'DELETE FROM %s WHERE qid = %d ';
  566. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  567. $message = t('Query @id "@name" has been deleted.', array
  568. (
  569. '@id' => $qid,
  570. '@name' => $query->name,
  571. ));
  572. drupal_set_message($message, 'status');
  573. watchdog('qbf', $message, WATCHDOG_NOTICE, $link);
  574. }
  575. else
  576. {
  577. $message = t('Failed attempt to delete query @qid. Administrators has been alerted.', array
  578. (
  579. '@qid' => $qid,
  580. ));
  581. drupal_set_message($message, 'error');
  582. watchdog('qbf', $message, WATCHDOG_ERROR, $link);
  583. }
  584. drupal_goto($path);
  585. }
  586. error_reporting($_qbf_er);