qbf.module 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  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.10 2008-10-22 13:02:16 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_ADMIN,
  256. QBF_PERM_QUERY,
  257. );
  258. return $ret;
  259. }
  260. /**
  261. * Implement hook_forms().
  262. *
  263. * @ingroup forms
  264. * @ingroup hooks
  265. * @return array
  266. */
  267. function qbf_forms() {
  268. $hook_name = 'qbf_register';
  269. foreach (module_implements($hook_name) as $module) {
  270. foreach (module_invoke($module, $hook_name) as $form_name) {
  271. $forms["qbf_$form_name"] = array
  272. (
  273. 'callback' => 'qbf_transform_form',
  274. 'callback arguments' => array($form_name),
  275. );
  276. }
  277. }
  278. return $forms;
  279. }
  280. /**
  281. * Insert the query results at the bottom of the query form.
  282. *
  283. * @ingroup forms
  284. * @param array $form
  285. * @param array $form_values
  286. * @return array
  287. */
  288. function qbf_after_build($form, $form_values) {
  289. if (empty($form['#post'])) {
  290. return $form;
  291. }
  292. // If #post is not empty, we are indeed querying
  293. $ar_query = _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. $results = $function($ar_query);
  301. }
  302. else {
  303. drupal_set_message(t('QBF: incorrect callback function for search'), 'error');
  304. }
  305. $form['qbf_query_results'] = array
  306. (
  307. '#type' => 'markup',
  308. '#value' => $results,
  309. '#weight' => 10,
  310. );
  311. return $form;
  312. }
  313. /**
  314. * Recursively build a query array from the form and its values
  315. *
  316. * In the current version, element names are supposed to be unique, even at
  317. * different levels in the tree.
  318. *
  319. * @ingroup forms
  320. * @param array $form
  321. * @param array $form_values
  322. */
  323. function _qbf_extract_query($form, $form_values) {
  324. $name = $form['#parents'][0];
  325. // Elements which are removed or display-only have no place in the query
  326. if (array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
  327. && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL) {
  328. $ret = array($name => $form_values[$name]);
  329. }
  330. else {
  331. $ret = array();
  332. }
  333. // QBF level is not inherited, so this loop is outside the "if" above
  334. foreach (element_children($form) as $child_name) {
  335. $ret += _qbf_extract_query($form[$child_name], $form_values);
  336. }
  337. return $ret;
  338. }
  339. /**
  340. * Provide an optional automatic mapping mechanism for query building.
  341. *
  342. * This function takes a partly built query map $ar_queryMap, and a defaults
  343. * array to complete it in $ar_defaults, and returns a fully built query array
  344. * ready to be used for querying.
  345. *
  346. * @param array $ar_query
  347. * @param array $ar_defaults
  348. * @return array
  349. */
  350. function qbf_query_mapper($ar_queryMap = array(), $ar_defaults = array()) {
  351. $ret = array();
  352. foreach ($ar_queryMap as $name => $value) {
  353. // accept NULL, empty strings...
  354. if (!is_array($value)) {
  355. $value = array();
  356. }
  357. $item = $value;
  358. foreach ($ar_defaults as $default_key => $default_value) {
  359. if (!array_key_exists($default_key, $item)) {
  360. $item[$default_key] = is_null($default_value)
  361. ? $name
  362. : $default_value;
  363. }
  364. // else if is already in $item, so we don't touch it
  365. }
  366. $ret[$name] = $item;
  367. }
  368. return $ret;
  369. }
  370. /**
  371. * Load a form_values array into a form used by QBF.
  372. *
  373. * This is typically useful when loading saved queries using qbf_query_load().
  374. * For other cases, the mechanisms built within FAPI should be used instead.
  375. *
  376. * @see qbf_query_load()
  377. *
  378. * @ingroup forms
  379. * @param array $form
  380. * @param array $form_values
  381. * @return array The modified form
  382. */
  383. function qbf_import_values($element, $form_values) {
  384. foreach (element_children($element) as $child_name) {
  385. if (!empty($form_values[$child_name])) {
  386. $element[$child_name]['#qbf']['#default_value'] = $form_values[$child_name];
  387. }
  388. $element[$child_name] = qbf_import_values($element[$child_name], $form_values);
  389. }
  390. return $element;
  391. }
  392. /**
  393. * Load a saved QBF query.
  394. *
  395. * It is not named qbf_load() although this would seem more natural, because a
  396. * hook_load() exists and this is not an implementation of this hook.
  397. *
  398. * @see qbf_import_values()
  399. *
  400. * @param int $qid
  401. * @return array A form_values array usable by qbf_import_values
  402. */
  403. function qbf_query_load($qid) {
  404. $sq = 'SELECT qq.qid, qq.uid, qq.query, qq.name '
  405. .'FROM {%s} qq '
  406. .'WHERE qq.qid = %d ';
  407. // db_rewrite_sql does not apply here until we add more advanced support for access control
  408. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  409. $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  410. // FALSE does not happen
  411. if ($ret === NULL) {
  412. $ret = NULL;
  413. }
  414. else {
  415. $ret->query = unserialize($ret->query);
  416. //dsm($ret);
  417. }
  418. return $ret;
  419. }
  420. /**
  421. * Submit handler for query save form.
  422. *
  423. * @ingroup forms
  424. * @param $form_id string
  425. * @param $form_values array
  426. * @return string
  427. */
  428. function qbf_submit($form_id, $form_values) {
  429. switch ($form_values['op']) {
  430. case t('Search'):
  431. $ret = FALSE;
  432. break;
  433. case t('Save query'):
  434. _qbf_save($form_id, $form_values);
  435. drupal_set_message(t('Your query was saved as "@name".',
  436. array('@name' => $form_values['save-name'])));
  437. global $user;
  438. $ret = "user/$user->uid/edit/job";;
  439. break;
  440. }
  441. //dsm(array('QS' => $form_values));
  442. return $ret;
  443. }
  444. /**
  445. * List queries owned by a given user.
  446. *
  447. * @param int $uid > 0
  448. * @return array
  449. */
  450. function qbf_get_queries_by_user($uid = NULL) {
  451. if (is_null($uid)) {
  452. global $user;
  453. $uid = $user->uid;
  454. }
  455. $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query, qq.updated '
  456. .'FROM {%s} qq '
  457. .'WHERE qq.uid = %d '
  458. .'ORDER BY qq.name ';
  459. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  460. $ret = array();
  461. while ($o = db_fetch_object($q)) {
  462. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  463. }
  464. return $ret;
  465. }
  466. /**
  467. * Save a query and return its qid.
  468. *
  469. * @ingroup forms
  470. * @param $form_id string
  471. * @param $form_values array
  472. * @return int
  473. */
  474. function _qbf_save($form_id, $form_values) {
  475. global $user;
  476. if ($user->uid == 0) {
  477. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  478. drupal_set_message($warning, 'error');
  479. watchdog('qbf', $warning, WATCHDOG_WARNING);
  480. $ret = 0;
  481. }
  482. else {
  483. $form = drupal_retrieve_form($form_id);
  484. drupal_prepare_form($form_id, $form);
  485. $name = $form_values['save-name'];
  486. $form_values = _qbf_extract_query($form, $form_values);
  487. $ar_values = array();
  488. foreach ($form_values as $key => $value) {
  489. if (empty($value)) {
  490. continue;
  491. }
  492. $ar_values[$key] = $value;
  493. }
  494. // Avoid duplicates
  495. if (!empty($name)) {
  496. $sq = "DELETE FROM {%s} WHERE name = '%s'";
  497. db_query($sq, QBF_TABLE_NAME, $name);
  498. }
  499. $sq = 'INSERT INTO {%s} (qid, uid, name, query, updated) '
  500. ."VALUES (%d, %d, '%s', '%s', '%d' ) ";
  501. $ret = db_next_id('qbf_qid');
  502. $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $name, serialize($ar_values), time());
  503. }
  504. return $ret;
  505. }
  506. /**
  507. * Implement hook_menu().
  508. *
  509. * @param $may_cache boolean
  510. * @return array
  511. */
  512. function qbf_menu($may_cache) {
  513. $items = array();
  514. if ($may_cache) {
  515. $admin_access = user_access(QBF_PERM_ADMIN);
  516. $items[] = array
  517. (
  518. 'path' => QBF_PATH_SETTINGS,
  519. 'title' => t('Query-By-Form'),
  520. 'access' => $admin_access,
  521. 'callback' => 'drupal_get_form',
  522. 'callback arguments' => 'qbf_admin_settings',
  523. 'type' => MENU_NORMAL_ITEM,
  524. );
  525. }
  526. else {
  527. if ((arg(0) == QBF_PATH_MAIN) && is_numeric(arg(1)) && arg(1) > 0 && arg(2) == 'delete') {
  528. $qid = arg(1);
  529. $queror_access = user_access(QBF_PERM_QUERY);
  530. $items[] = array
  531. (
  532. 'path' => QBF_PATH_MAIN .'/'. $qid .'/delete',
  533. 'type' => MENU_CALLBACK,
  534. 'access' => $queror_access,
  535. 'callback' => '_qbf_query_delete',
  536. 'callback arguments' => array($qid),
  537. );
  538. }
  539. }
  540. return $items;
  541. }
  542. /**
  543. * Delete a query by qid
  544. *
  545. * $qid has been tested in qbf_menu() to be a positive integer, so it is a safe
  546. * number, but we still need to know more about it.
  547. *
  548. * @param $qid integer
  549. */
  550. function _qbf_query_delete($qid) {
  551. global $user;
  552. $query = qbf_query_load($qid);
  553. $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);
  554. $link = l($qid, QBF_PATH_MAIN .'/'. $qid .'/delete');
  555. // only valid if valid query, and owner or admin
  556. if (isset($query->uid) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN))) {
  557. $sq = 'DELETE FROM %s WHERE qid = %d ';
  558. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  559. $message = t('Query @id "@name" has been deleted.', array
  560. (
  561. '@id' => $qid,
  562. '@name' => $query->name,
  563. ));
  564. drupal_set_message($message, 'status');
  565. watchdog('qbf', $message, WATCHDOG_NOTICE, $link);
  566. if (variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE) && $query->uid != $user->uid) {
  567. $ret = drupal_mail(__FUNCTION__, $user->mail, $message, $message, $user->mail);
  568. $account = user_load(array('uid' => $query->uid));
  569. drupal_set_message(t('User !link has been informed', array
  570. (
  571. '!link' => l($account->name, 'user/'. $query->uid),
  572. )));
  573. dsm($ret);
  574. }
  575. }
  576. else {
  577. $message = t('Failed attempt to delete query @qid. Administrator 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();
  585. }
  586. /**
  587. * Implement the former hook_settings().
  588. *
  589. * @return array
  590. */
  591. function qbf_admin_settings() {
  592. $form = array();
  593. $form[QBF_VAR_NOTIFY_DELETE] = array
  594. (
  595. '#type' => 'checkbox',
  596. '#default_value' => variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE),
  597. '#title' => t('Notify users when one of their saved searches has been deleted'),
  598. );
  599. return system_settings_form($form);
  600. }
  601. error_reporting($_qbf_er);