qbf.module 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  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.5 2008-10-05 18:27:13 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_load().
  401. * For other cases, the mechanisms built within FAPI should be used instead.
  402. *
  403. * @see qbf_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. * @see qbf_import_values()
  425. *
  426. * @param int $qid
  427. * @return array A form_values array usable by qbf_import_values
  428. */
  429. function qbf_load($qid) {
  430. $sq = 'SELECT qq.qid, qq.uid, qq.query, qq.name '
  431. . 'FROM {%s} qq '
  432. . 'WHERE qq.qid = %d ';
  433. // db_rewrite_sql does not apply here until we add more advanced support for access control
  434. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  435. $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  436. if ($ret === NULL) // FALSE does not happen
  437. {
  438. $ret = NULL;
  439. }
  440. else
  441. {
  442. $ret->query = unserialize($ret->query);
  443. //dsm($ret);
  444. }
  445. return $ret;
  446. }
  447. /**
  448. * Submit handler for query save form.
  449. *
  450. * @ingroup forms
  451. * @param $form_id string
  452. * @param $form_values array
  453. * @return string
  454. */
  455. function qbf_submit($form_id, $form_values) {
  456. switch ($form_values['op'])
  457. {
  458. case t('Search'):
  459. $ret = FALSE;
  460. break;
  461. case t('Save query'):
  462. _qbf_save($form_id, $form_values);
  463. drupal_set_message(t('Your query was saved as "@name".',
  464. array('@name' => $form_values['save-name'])));
  465. global $user;
  466. $ret = "user/$user->uid/edit/job";;
  467. break;
  468. }
  469. //dsm(array('QS' => $form_values));
  470. return $ret;
  471. }
  472. /**
  473. * List queries owned by a given user.
  474. *
  475. * @param int $uid > 0
  476. * @return array
  477. */
  478. function qbf_get_queries_by_user($uid = NULL) {
  479. if (is_null($uid))
  480. {
  481. global $user;
  482. $uid = $user->uid;
  483. }
  484. $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query '
  485. . 'FROM {%s} qq '
  486. . 'WHERE qq.uid = %d '
  487. . 'ORDER BY qq.name ';
  488. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  489. $ret = array();
  490. while ($o = db_fetch_object($q))
  491. {
  492. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  493. }
  494. return $ret;
  495. }
  496. /**
  497. * Save a query and return its qid.
  498. *
  499. * @ingroup forms
  500. * @param $form_id string
  501. * @param $form_values array
  502. * @return int
  503. */
  504. function _qbf_save($form_id, $form_values) {
  505. global $user;
  506. if ($user->uid == 0)
  507. {
  508. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  509. drupal_set_message($warning, 'error');
  510. watchdog('qbf', $warning, WATCHDOG_WARNING);
  511. $ret = 0;
  512. }
  513. else
  514. {
  515. $form = drupal_retrieve_form($form_id);
  516. drupal_prepare_form($form_id, $form);
  517. $name = $form_values['save-name'];
  518. $form_values = _qbf_extract_query($form, $form_values);
  519. $arValues = array();
  520. foreach ($form_values as $key => $value)
  521. {
  522. if (empty($value))
  523. {
  524. continue;
  525. }
  526. $arValues[$key] = $value;
  527. }
  528. $sq = 'INSERT INTO {%s} (qid, uid, name, query) '
  529. ."VALUES (%d, %d, '%s', '%s' ) ";
  530. $ret = db_next_id('qbf_qid');
  531. $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $name, serialize($arValues));
  532. }
  533. return $ret;
  534. }
  535. /**
  536. * Implement hook_menu().
  537. *
  538. * @param $may_cache boolean
  539. * @return array
  540. */
  541. function qbf_menu($may_cache) {
  542. $items = array();
  543. if ($may_cache)
  544. {
  545. $items[] = array
  546. (
  547. 'path' => QBF_PATH_SETTINGS,
  548. 'title' => t('Query-By-Form'),
  549. 'access' => user_access(QBF_PERM_ADMIN),
  550. 'callback' => 'drupal_get_form',
  551. 'callback arguments' => 'qbf_admin_settings',
  552. 'type' => MENU_NORMAL_ITEM,
  553. );
  554. }
  555. else
  556. {
  557. if ((arg(0) == QBF_PATH_MAIN) && is_numeric(arg(1)) && arg(1) > 0 && arg(2) == 'delete')
  558. {
  559. $qid = arg(1);
  560. $querorAccess = user_access(QBF_PERM_QUERY);
  561. $items[] = array
  562. (
  563. 'path' => QBF_PATH_MAIN . '/' . $qid . '/delete',
  564. 'type' => MENU_CALLBACK,
  565. 'access' => $querorAccess,
  566. 'callback' => '_qbf_query_delete',
  567. 'callback arguments' => array($qid),
  568. );
  569. }
  570. }
  571. return $items;
  572. }
  573. /**
  574. * Delete a query by qid
  575. *
  576. * $qid has been tested in qbf_menu() to be a positive integer, so it is a safe
  577. * number, but we still need to know more about it.
  578. *
  579. * @param $qid integer
  580. */
  581. function _qbf_query_delete($qid) {
  582. global $user;
  583. $query = qbf_load($qid);
  584. $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);
  585. $link = l($qid, QBF_PATH_MAIN . '/' . $qid . '/delete');
  586. // @todo Check safety, this seem dangerous
  587. //$usArgs = func_get_args();
  588. //$path = implode('/', array_slice($usArgs, 1));
  589. // only valid if valid query, and owner or admin
  590. if (isset($query->uid) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN)))
  591. {
  592. $sq = 'DELETE FROM %s WHERE qid = %d ';
  593. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  594. $message = t('Query @id "@name" has been deleted.', array
  595. (
  596. '@id' => $qid,
  597. '@name' => $query->name,
  598. ));
  599. drupal_set_message($message, 'status');
  600. watchdog('qbf', $message, WATCHDOG_NOTICE, $link);
  601. if (variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE) && $query->uid != $user->uid)
  602. {
  603. $ret = drupal_mail(__FUNCTION__, $user->mail, $message, $message, $user->mail);
  604. $account = user_load(array('uid' => $query->uid));
  605. drupal_set_message(t('User !link has been informed', array
  606. (
  607. '!link' => l($account->name, 'user/' . $query->uid),
  608. )));
  609. dsm($ret);
  610. }
  611. }
  612. else
  613. {
  614. $message = t('Failed attempt to delete query @qid. Administrators has been alerted.', array
  615. (
  616. '@qid' => $qid,
  617. ));
  618. drupal_set_message($message, 'error');
  619. watchdog('qbf', $message, WATCHDOG_ERROR, $link);
  620. }
  621. drupal_goto();
  622. }
  623. function qbf_admin_settings() {
  624. $form = array();
  625. $form[QBF_VAR_NOTIFY_DELETE] = array
  626. (
  627. '#type' => 'checkbox',
  628. '#default_value' => variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE),
  629. '#title' => t('Notify users when one of their saved searches has been deleted'),
  630. );
  631. return system_settings_form($form);
  632. }
  633. error_reporting($_qbf_er);