qbf.module 17 KB

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