qbf.module 17 KB

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