qbf.module 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264
  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-2009 Ouest Systemes Informatiques (OSInet)
  10. * @author Frederic G. MARAND
  11. * @license Licensed under the CeCILL 2.0 and the General Public Licence version 2 or later
  12. * @package QBF
  13. */
  14. // $Id: qbf.module,v 1.9.4.12 2009-03-22 15:15:19 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. * Maximum number of queries a user may save
  76. *
  77. * @ingroup vars
  78. */
  79. define('QBF_VAR_MAX_QUERIES', 'qbf_max_queries');
  80. /**
  81. * Notify owner about saved query deletions, variable name.
  82. *
  83. * @ingroup vars
  84. */
  85. define('QBF_VAR_NOTIFY_DELETE', 'qbf_notify_delete');
  86. /**
  87. * Name of the profile category under which the list of saved queries will be
  88. * displayed.
  89. *
  90. * @ingroup vars
  91. *
  92. * @see qbf_admin_settings(), qbf_profile_alter()
  93. */
  94. define('QBF_VAR_PROFILE_CATEGORY', 'qbf_profile_category');
  95. /**
  96. * Querying mode: contains, starts, or equals
  97. *
  98. * @ingroup vars
  99. */
  100. define('QBF_VAR_QUERY_MODE', 'qbf_query_mode');
  101. /**
  102. * Default value for the max number of saved queries per user
  103. *
  104. * @ingroup vars
  105. *
  106. * See QBF_VAR_MAX_QUERIES
  107. */
  108. define('QBF_DEF_MAX_QUERIES', 5);
  109. /**
  110. * Notify owner about saved query deletions, default value.
  111. *
  112. * @ingroup vars
  113. */
  114. define('QBF_DEF_NOTIFY_DELETE', FALSE);
  115. /**
  116. * Default value for the profile category
  117. *
  118. * @ingroup vars
  119. *
  120. * See QBF_VAR_PROFILE_CATEGORY
  121. */
  122. define('QBF_DEF_PROFILE_CATEGORY', 'Saved queries');
  123. /**
  124. * Return #options properties.
  125. *
  126. * It has not been implemented as an abstract class with a values array, from
  127. * which all descendant class could reuse the getter method
  128. * get_options(), although this would be much cleaner, because the current
  129. * translation interface (potx.module) relies on t(), and it is impossible for
  130. * static properties to be initialized to values which are the result of a
  131. * function, like:
  132. * <code>protected $arOptions = array('foo' => t('foo'));</code>
  133. * which would be necessary in this case. The alternative solution of only
  134. * declaring the array keys and returning their translated version as a value
  135. * with t($key) in get_options() would also fail because the translation
  136. * template extractor would not be able to generate the declaration from a
  137. * t() call with variable content.
  138. *
  139. * @link http://drupal.org/project/potx
  140. */
  141. Interface Qbf_Optioned_Interface
  142. {
  143. static function get_options();
  144. }
  145. /**
  146. * Querying modes for QBF
  147. */
  148. class Qbf_Query_Mode implements Qbf_Optioned_Interface
  149. {
  150. /**
  151. * Query mode: contains
  152. */
  153. const CONTAINS = 'contains';
  154. /**
  155. * Query mode: starts with
  156. */
  157. const STARTS = 'starts';
  158. /**
  159. * Query mode: equals
  160. */
  161. const EQUALS = 'equals';
  162. /**
  163. * Returns the list of querying modes
  164. *
  165. * @ingroup classmethods
  166. * @return array
  167. */
  168. static function get_options() {
  169. return array
  170. (
  171. self::CONTAINS => t('Contains'),
  172. self::STARTS => t('Starts with'),
  173. self::EQUALS => t('Equals'),
  174. );
  175. }
  176. }
  177. /**
  178. * A class wrapper for saved QBF queries
  179. */
  180. class Qbf_Query
  181. {
  182. public $qid;
  183. public $uid;
  184. public $name;
  185. public $type;
  186. public $query;
  187. public $created;
  188. public $updated;
  189. /**
  190. * Constructor
  191. *
  192. * @param string $name
  193. * @param array $ar_values
  194. * @return void
  195. */
  196. public function __construct($type, $name = NULL, $ar_values = NULL)
  197. {
  198. global $user;
  199. $this->qid = 0; // will be autoset by the DB serial
  200. $this->uid = $user->uid;
  201. $this->type = $type;
  202. $this->name = $name;
  203. if (!empty($ar_values))
  204. {
  205. $this->query = serialize($ar_values);
  206. }
  207. $this->created = $this->updated = time();
  208. }
  209. /**
  210. * Save a named query to the DB, erasing previous homonym queries is any exists.
  211. *
  212. * @return int
  213. */
  214. public function save()
  215. {
  216. // Avoid duplicates
  217. if (!empty($this->name))
  218. {
  219. $sq = "DELETE FROM {%s} WHERE name = '%s' AND uid = '%d' ";
  220. db_query($sq, QBF_TABLE_NAME, $this->name, $this->uid);
  221. // $n = db_affected_rows(); // Know how many homonym queries we deleted
  222. }
  223. $ret = drupal_write_record(QBF_TABLE_NAME, $this); // no update param: we just deleted the previous version
  224. if ($ret) // has to be SAVED_NEW, by construction
  225. {
  226. $ret = $this->qid; // from serial
  227. }
  228. return $ret;
  229. }
  230. }
  231. /**
  232. * Recursively build a query array from the form and its values
  233. *
  234. * In the current version, element names are supposed to be unique, even at
  235. * different levels in the tree.
  236. *
  237. * @ingroup forms
  238. * @param array $form
  239. * @param array $form_values
  240. */
  241. function _qbf_extract_query($element_id, $form, $form_values)
  242. {
  243. // Elements which are unnamed (form), removed, or display-only have no place in the query
  244. if (!empty($element_id) && array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
  245. && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL)
  246. {
  247. $ret = array($element_id => $form_values[$element_id]);
  248. }
  249. else
  250. {
  251. $ret = array();
  252. }
  253. // QBF level is not inherited, so this loop is outside the "if" above
  254. foreach (element_children($form) as $child_name)
  255. {
  256. $ret += _qbf_extract_query($child_name, $form[$child_name], $form_values);
  257. }
  258. return $ret;
  259. }
  260. /**
  261. * Submit handler for qbf_form, Perform search button.
  262. *
  263. * @param array $form
  264. * @param array $form_state
  265. */
  266. function _qbf_form_perform_submit($form, &$form_state)
  267. {
  268. // dsm($form);
  269. // dsm($form_state);
  270. $callback = $form_state['values']['qbf_query'];
  271. if (function_exists(($callback)))
  272. {
  273. $ar_query = _qbf_extract_query(NULL, $form, $form_state['values']);
  274. $form_state['qbf_results'] = $callback($ar_query);
  275. }
  276. $form_state['rebuild'] = TRUE;
  277. }
  278. /**
  279. * Validate handler for qbf_form, Perform search button.
  280. *
  281. * @param array $form
  282. * @param array $form_state
  283. */
  284. //function _qbf_form_perform_validate($form, &$form_state)
  285. // {
  286. // // @todo validate searches: checkboxes sets needs at least one value checked, otherwise there won't be any result
  287. // }
  288. /**
  289. * Submit handler for qbf_form, Save search button.
  290. *
  291. * @param array $form
  292. * @param array $form_state
  293. * @return integer
  294. * The id of the saved query.
  295. */
  296. function _qbf_form_save_submit($form, &$form_state)
  297. {
  298. $qid = _qbf_save($form_state['values']['form_id'], $form_state);
  299. drupal_set_message(t('Your query was saved as "@name".',
  300. array('@name' => $form_state['values']['qbf_save_name'])));
  301. global $user;
  302. $form_state['redirect'] = "user/$user->uid/edit/qbf";
  303. return $qid;
  304. }
  305. /**
  306. * Validate handler for qbf_form, Save search button.
  307. *
  308. * @param array $form
  309. * @param array $form_state
  310. */
  311. //function _qbf_form_save_validate($form, &$form_state)
  312. // {
  313. // // @todo validate saves. Check whether any validation is necessary.
  314. // }
  315. /**
  316. * Return the human-readable for a query type.
  317. *
  318. * @param string $query_type
  319. * @return string
  320. */
  321. function _qbf_get_name_from_type($query_type)
  322. {
  323. static $labels = array();
  324. if (empty($labels) || empty($labels[$query_type]))
  325. {
  326. $ar_forms = qbf_forms();
  327. foreach ($ar_forms as /* $qbf_form_id => */ $form_info)
  328. {
  329. if ($query_type == $form_info['callback arguments'][0]['form'])
  330. {
  331. $labels[$query_type] = $form_info['callback arguments'][0]['label'];
  332. break;
  333. }
  334. }
  335. }
  336. return $labels[$query_type];
  337. }
  338. /**
  339. * Delete a query by qid
  340. *
  341. * In the qbf/<qid>/delete case, $query has been tested for validity and access
  342. * in qbf_query_load(), so it is safe and accessible.
  343. *
  344. * Outside this context, the function can also be invoken with just a qid, and
  345. * the same check via qbf_query_load() will be performed.
  346. *
  347. * @param mixed $query
  348. * int or object
  349. */
  350. function _qbf_query_delete($query)
  351. {
  352. global $user;
  353. if (is_int($query))
  354. {
  355. $query = qbf_query_load($query);
  356. }
  357. if ($query) // access already checked in explicit or implicit qbf_query_load
  358. {
  359. $qid = $query->qid;
  360. $sq = 'DELETE FROM %s WHERE qid = %d ';
  361. db_query($sq, QBF_TABLE_NAME, $qid);
  362. $message = t('Query @id "@name" has been deleted.', array
  363. (
  364. '@id' => $qid,
  365. '@name' => $query->name,
  366. ));
  367. drupal_set_message($message, 'status');
  368. $link = l($qid, QBF_PATH_MAIN .'/'. $qid .'/delete');
  369. $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);
  370. watchdog('qbf', $message, NULL, WATCHDOG_NOTICE, $link);
  371. // access check: we only send the message to the query owner, so access is
  372. // granted without an additional check
  373. if ($notify /* && $query->uid != $user->uid */)
  374. {
  375. $owner = user_load(array('uid' => $query->uid));
  376. $account = user_load(array('uid' => $query->uid));
  377. $language = user_preferred_language($account);
  378. $params = array
  379. (
  380. 'query' => $query,
  381. 'owner' => $owner, // unused by default, but can be used in a hook_mail_alter() implementation
  382. 'deletor' => $user,
  383. 'language' => $language,
  384. );
  385. /* $ret = */ drupal_mail('qbf', __FUNCTION__, $user->mail, $language, $params, $user->mail);
  386. drupal_set_message(t('User !link has been informed', array
  387. (
  388. '!link' => l($account->name, 'user/'. $query->uid),
  389. )));
  390. // dsm(array("QQD, ret" => $ret));
  391. }
  392. }
  393. else {
  394. $message = t('Failed attempt to delete query @qid. Administrator has been alerted.', array
  395. (
  396. '@qid' => $qid,
  397. ));
  398. drupal_set_message($message, 'error');
  399. watchdog('qbf', $message, NULL, WATCHDOG_ERROR, $link);
  400. }
  401. drupal_goto();
  402. }
  403. /**
  404. * Main query page.
  405. *
  406. * This returns the query form if a valid query id or query type is specified,
  407. * or the list of available query types if several exisit, or jumps to the single
  408. * available query type if only one exists.
  409. *
  410. * @param object $query
  411. * Valid query, loaded by qbf_query_load().
  412. * @return string
  413. */
  414. function _qbf_query_form($query = NULL )
  415. {
  416. if (!empty($query))
  417. {
  418. $qbf_form_id = 'qbf_' . $query->type;
  419. $ret = drupal_get_form($qbf_form_id, $query);
  420. }
  421. else
  422. {
  423. $ar_forms = qbf_forms();
  424. $arRet = array();
  425. foreach ($ar_forms as $qbf_form_id => $form_info)
  426. {
  427. $form_id = $form_info['callback arguments'][0]['form'];
  428. $arRet[QBF_PATH_MAIN . "/$form_id"] = l($form_info['callback arguments'][0]['label'],
  429. QBF_PATH_MAIN . "/$form_id");
  430. }
  431. // If there is only one form type, no need to ask the user.
  432. if (count($arRet) == 1)
  433. {
  434. reset($arRet);
  435. drupal_goto(key($arRet));
  436. }
  437. else
  438. {
  439. $ret = theme('item_list', $arRet, t('Choose a query type'));
  440. }
  441. }
  442. return $ret;
  443. }
  444. /**
  445. * Save a query and return its qid.
  446. *
  447. * This is not a hook_save() implementation, hence the "_".
  448. *
  449. * @ingroup forms
  450. *
  451. * @param $form_id string
  452. * @param $form_state array
  453. * @return int
  454. */
  455. function _qbf_save($form_id, $form_state)
  456. {
  457. if (user_is_anonymous())
  458. {
  459. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  460. drupal_set_message($warning, 'error');
  461. watchdog('qbf', $warning, NULL, WATCHDOG_WARNING);
  462. $ret = 0;
  463. }
  464. else
  465. {
  466. // @FIXME check whether form_state is now needed. It wasn't in QBF for D5
  467. $form = drupal_retrieve_form($form_id, $form_state);
  468. // dsm($form, "retrieve");
  469. drupal_prepare_form($form_id, $form, $form_state);
  470. // dsm($form, "prepare");
  471. $name = $form_state['values']['qbf_save_name'];
  472. $type = $form_state['values']['qbf_save_type'];
  473. // dsm($form_state);
  474. $form_values = _qbf_extract_query(NULL, $form, $form_state['values']);
  475. // dsm($form_values);
  476. $ar_values = array();
  477. foreach ($form_values as $key => $value)
  478. {
  479. if (empty($value))
  480. {
  481. continue;
  482. }
  483. $ar_values[$key] = $value;
  484. }
  485. $query = new Qbf_Query($type, $name, $ar_values);
  486. $ret = $query->save();
  487. }
  488. return $ret;
  489. }
  490. /**
  491. * Transform a form element for QBF.
  492. *
  493. * QBF-specific properties are:
  494. * - #qbf : array of properties
  495. * - #level: only within #qbf
  496. *
  497. * See QBF_* constants
  498. *
  499. * @ingroup forms
  500. * @param string $key
  501. * @param array $element
  502. * @return void
  503. */
  504. function _qbf_transform($key, $element, $form_state, $query)
  505. {
  506. // dsm(array('key' => $key, 'element' => $element));
  507. /**
  508. * List default type transformations applied to widget by FAPI.
  509. * Types without a default transformation are not transformed
  510. */
  511. static $ar_default_type_transformations = array
  512. (
  513. 'button' => NULL, // no content
  514. 'file' => NULL, // non-querable (yet ?)
  515. 'image_button' => NULL, // new in D6
  516. 'markup' => NULL, // no content
  517. 'password' => NULL, // forbidden
  518. 'radio' => NULL, // single radio is useless, unlike a set of them
  519. 'submit' => NULL, // no content
  520. 'textarea' => 'textfield', // reduce text for searches
  521. // Don't transform these:
  522. // 'checkbox' => NULL,
  523. // 'checkboxes' => NULL,
  524. // 'date' => NULL,
  525. // 'fieldset' => NULL, // useful visually
  526. // 'form' => NULL, // removing it would delete the whole shebang
  527. // 'hidden' => NULL, // non-querable visually, but may be useful
  528. // 'item' => NULL,
  529. // 'radios' => NULL,
  530. // 'select' => NULL,
  531. // 'textfield' => NULL,
  532. // 'value' => 'value',
  533. // 'weight' => NULL,
  534. );
  535. /**
  536. * List default property transformations applied to widget by FAPI property.
  537. *
  538. * Properties without a default transformation are not transformed
  539. */
  540. static $ar_default_property_transformations = array
  541. (
  542. // Standard properties
  543. '#action' => NULL,
  544. '#after_build' => NULL,
  545. // '#base' => NULL, // gone in D6
  546. '#button_type' => NULL,
  547. '#built' => NULL,
  548. '#description' => NULL,
  549. '#method' => NULL,
  550. '#parents' => NULL,
  551. '#redirect' => NULL,
  552. '#ref' => NULL,
  553. '#required' => NULL,
  554. '#rows' => NULL,
  555. '#submit' => NULL,
  556. '#tree' => NULL,
  557. '#validate' => NULL,
  558. );
  559. /**
  560. * List properties causing causing element removal.
  561. *
  562. * The key is the property name, the value is the one causing removal.
  563. */
  564. static $ar_killer_properties = array
  565. (
  566. '#disabled' => TRUE,
  567. );
  568. // Transform type
  569. $source_type = $element['#type'];
  570. // .. Default transformation
  571. $dest_type = array_key_exists($source_type, $ar_default_type_transformations)
  572. ? $ar_default_type_transformations[$source_type]
  573. : $source_type;
  574. // .. Apply form-defined type override
  575. if (isset($element['#qbf']['#type']))
  576. {
  577. $dest_type = $element['#qbf']['#type'];
  578. }
  579. if (is_null($dest_type))
  580. {
  581. $ret = NULL;
  582. }
  583. else
  584. {
  585. $ret = $element;
  586. $ret['#type'] = $dest_type;
  587. if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
  588. {
  589. $ret = NULL;
  590. }
  591. else
  592. {
  593. foreach (element_properties($element) as $property_name)
  594. {
  595. // Apply killer properties first to avoid useless work
  596. if (array_key_exists($property_name, $ar_killer_properties)
  597. && ($element[$property_name] = $ar_killer_properties[$property_name]))
  598. {
  599. $ret = NULL;
  600. break;
  601. }
  602. // Now transform or copy remaining properties
  603. if (array_key_exists($property_name, $ar_default_property_transformations))
  604. {
  605. $ret[$property_name] = $ar_default_property_transformations[$property_name];
  606. }
  607. else
  608. {
  609. $ret[$property_name] = $element[$property_name];
  610. }
  611. // And apply form-defined property overrides
  612. if ($property_name == '#qbf')
  613. {
  614. foreach ($element[$property_name] as $override_name => $override_value)
  615. {
  616. $ret[$override_name] = $override_value;
  617. }
  618. }
  619. }
  620. if (isset($form_state['values'][$key]))
  621. {
  622. $ret['#default_value'] = $form_state['values'][$key];
  623. }
  624. elseif (isset($query->query[$key]))
  625. {
  626. $ret['#default_value'] = $query->query[$key];
  627. }
  628. // Recursively transform children
  629. foreach (element_children($element) as $child_name)
  630. {
  631. $child = _qbf_transform($child_name, $element[$child_name], $form_state, $query);
  632. if (is_null($child))
  633. {
  634. unset($ret[$child_name]);
  635. }
  636. else
  637. {
  638. $ret[$child_name] = $child;
  639. }
  640. }
  641. }
  642. }
  643. //dsm(array('key' => $key, 'transformed element' => $ret));
  644. return $ret;
  645. }
  646. /**
  647. * Implement the former hook_settings().
  648. *
  649. * @return array
  650. */
  651. function qbf_admin_settings()
  652. {
  653. $form = array();
  654. $form['queries'] = array
  655. (
  656. '#type' => 'fieldset',
  657. '#title' => t('Queries'),
  658. '#collapsible' => TRUE,
  659. '#collapsed' => TRUE,
  660. );
  661. $form['queries'][QBF_VAR_MAX_QUERIES] = array
  662. (
  663. '#type' => 'select',
  664. '#title' => t('Maximum number of saved queries'),
  665. '#description' => t('The maximum number of queries a user allowed to perform queries may save.'),
  666. '#default_value' => variable_get(QBF_VAR_MAX_QUERIES, QBF_DEF_MAX_QUERIES),
  667. '#options' => array_combine($iota = range(1, 99), $iota),
  668. );
  669. $ar_options = Qbf_Query_Mode::get_options();
  670. $form['queries'][QBF_VAR_QUERY_MODE] = array
  671. (
  672. '#type' => 'radios',
  673. '#title' => t('Query mode'),
  674. '#description' => t('This defines the way QBF will search string fields. "Contains" returns more results and is slowest, "Equals" returns less results and is fastest, while "Starts with" balances results and speed. Even when using "Equals" mode, case is ignored.'),
  675. '#options' => $ar_options,
  676. '#default_value' => variable_get(QBF_VAR_QUERY_MODE, Qbf_Query_Mode::CONTAINS),
  677. );
  678. $form['queries'][QBF_VAR_PROFILE_CATEGORY] = array
  679. (
  680. '#type' => 'textfield',
  681. '#title' => t('Name of profile category'),
  682. '#description' => t('Choose a title for the section of the user profiles where the list of search queries will be displayed. It may match an existing profile category.'),
  683. '#default_value' => variable_get(QBF_VAR_PROFILE_CATEGORY, QBF_DEF_PROFILE_CATEGORY),
  684. );
  685. $form['notifications'] = array
  686. (
  687. '#type' => 'fieldset',
  688. '#title' => t('Notifications'),
  689. '#collapsible' => TRUE,
  690. '#collapsed' => TRUE,
  691. );
  692. $form['notifications'][QBF_VAR_NOTIFY_DELETE] = array
  693. (
  694. '#type' => 'checkbox',
  695. '#default_value' => variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE),
  696. '#title' => t('Notify users when one of their saved searches has been deleted'),
  697. );
  698. return system_settings_form($form);
  699. }
  700. /**
  701. * The QBF form builder.
  702. *
  703. * @param array $form_state
  704. * @param array $query_info
  705. * The query structure array
  706. * @param string $qbf_form_id
  707. * The name of the QBF form
  708. * @param string $query
  709. * The saved query.
  710. */
  711. function qbf_form(&$form_state, $query_info, $qbf_form_id, $query = NULL)
  712. {
  713. $form_id = $query_info['form'];
  714. // Fetch the basic form and rename it, passing it the previous values
  715. $node = new stdClass();
  716. $form = $form_id($node, $form_state);
  717. $qbf_form = array();
  718. $qbf_form['#qbf_source_form_id'] = $form_id;
  719. // On the form element itself, only keep the QBF properties and the children
  720. if (in_array('#qbf', element_properties($form)))
  721. {
  722. $qbf_form += $form['#qbf'];
  723. }
  724. foreach (element_children($form) as $key)
  725. {
  726. $new_element = _qbf_transform($key, $form[$key], $form_state, $query);
  727. if (!is_null($new_element))
  728. {
  729. $qbf_form[$key] = $new_element;
  730. }
  731. }
  732. $qbf_form['#id'] = $qbf_form_id;
  733. $qbf_form['qbf'] = array
  734. (
  735. '#type' => 'fieldset',
  736. '#title' => t('Query'),
  737. );
  738. if (isset($form_state['values']) && !empty($form_state['values']))
  739. {
  740. if (isset($form_state['qbf_results']))
  741. {
  742. $qbf_form['qbf']['qbf_results'] = array
  743. (
  744. '#type' => 'markup',
  745. '#prefix' => '<p>',
  746. '#value' => $form_state['qbf_results'],
  747. '#suffix' => '</p>',
  748. );
  749. }
  750. }
  751. $qbf_form['qbf']['qbf_save_type'] = array
  752. (
  753. '#type' => 'hidden',
  754. '#value' => $query_info['form'],
  755. );
  756. $qbf_form['qbf']['qbf_query'] = array
  757. (
  758. '#type' => 'hidden',
  759. '#value' => $query_info['callback'],
  760. );
  761. $qbf_form['qbf']['qbf_save_name'] = array
  762. (
  763. '#title' => t('Name of query in your save list'),
  764. '#type' => 'textfield',
  765. '#required' => TRUE,
  766. '#default_value' => empty($query->name)
  767. ? t('@label - @time', array('@label' => $query_info['label'], '@time' => format_date(time(), 'large')))
  768. : $query->name,
  769. );
  770. $qbf_form['qbf']['qbf_perform'] = array
  771. (
  772. '#submit' => array('_qbf_form_perform_submit'),
  773. '#validate' => array('_qbf_form_perform_validate'),
  774. '#type' => 'submit',
  775. '#value' => t('Perform query'),
  776. );
  777. $qbf_form['qbf']['qbf_save'] = array
  778. (
  779. '#submit' => array('_qbf_form_save_submit'),
  780. '#validate' => array('_qbf_form_save_validate'),
  781. '#type' => 'submit',
  782. '#value' => t('Save query'),
  783. );
  784. return $qbf_form;
  785. }
  786. /**
  787. * Implement hook_forms().
  788. *
  789. * @link http://drupal.org/node/144132#hook-forms @endlink
  790. *
  791. * hook_qbf_register() returns an array of QBF-able node types, indexed by the
  792. * node type, with the following properties:
  793. * - form: the name of the hook_form() implementation (a $form_id)
  794. * - label: the human-readable type name under which the queries are saved by QBF
  795. * - callback: the function QBF must invoke to query the node type. It will
  796. * receive the query type and a filtered version of $form_state['values']
  797. * containing only valid node fields, and must return a themed grid of query
  798. * results, which will be displayed as a #markup FAPI element. In advanced
  799. * uses, a single callback can be used for several query types by using the
  800. * query type parameter to know what the values apply to.
  801. *
  802. * @ingroup forms
  803. * @ingroup hooks
  804. *
  805. * @param array $args
  806. * @return array
  807. */
  808. function qbf_forms(/* $args = NULL */)
  809. {
  810. static $forms = array();
  811. if (empty($forms))
  812. {
  813. $hook_name = 'qbf_register';
  814. // dsm(array("QBF_forms $qbf_form_id" => $args));
  815. // More efficient than using module_invoke_all: we avoid array-merging + re-looping
  816. foreach (module_implements($hook_name) as $module)
  817. {
  818. $arImplementations = module_invoke($module, $hook_name);
  819. // dsm($arImplementations);
  820. foreach ($arImplementations as /* $node_type => */ $query_info)
  821. {
  822. $qbf_form_id = 'qbf_' . $query_info['form'];
  823. $forms[$qbf_form_id] = array
  824. (
  825. 'callback' => 'qbf_form',
  826. 'callback arguments' => array($query_info, $qbf_form_id),
  827. );
  828. } // foreach implementation
  829. } // foreach module
  830. } // if empty
  831. return $forms;
  832. }
  833. /**
  834. * List queries owned by a given user.
  835. *
  836. * @param int $uid > 0
  837. * @return array
  838. */
  839. function qbf_get_queries_by_user($uid = NULL)
  840. {
  841. if (is_null($uid))
  842. {
  843. global $user;
  844. $uid = $user->uid;
  845. }
  846. $sq = 'SELECT qq.qid, qq.uid, qq.type, qq.name, qq.query, qq.updated '
  847. . 'FROM {%s} qq '
  848. . 'WHERE qq.uid = %d '
  849. . 'ORDER BY qq.type, qq.name ';
  850. // no db_rewrite_sql: this function is not in a menu callback, so it is up to
  851. // the caller to check access
  852. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  853. $ret = array();
  854. while (is_object($o = db_fetch_object($q)))
  855. {
  856. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  857. }
  858. return $ret;
  859. }
  860. /**
  861. * Implement hook_mail().
  862. *
  863. * @param string $key
  864. * @param array $message
  865. * @param array $params
  866. * @return void
  867. */
  868. function qbf_mail($key, &$message, $params)
  869. {
  870. // dsm(array('QBF_mail key' => $key, 'message' => $message, 'params' => $params));
  871. $deletor_tokens = user_mail_tokens($params['deletor'], $params['language']->language);
  872. $tokens = array_merge($deletor_tokens, array
  873. (
  874. '!qname' => $params['query']->name,
  875. '!qid' => $params['query']->qid,
  876. ));
  877. $message['subject'] = t('Effacement d\'une recherche !site enregistrée', $tokens);
  878. $message['body'] = t("!date\n\nVotre recherche !qid: !qname\nsur le site !site vient d'être effacée par !username.", $tokens);
  879. }
  880. /**
  881. * Implement hook_menu().
  882. *
  883. * @return array
  884. */
  885. function qbf_menu()
  886. {
  887. $items = array();
  888. $items[QBF_PATH_SETTINGS] = array
  889. (
  890. 'title' => 'Query-By-Form',
  891. 'access arguments' => array(QBF_PERM_ADMIN),
  892. 'page callback' => 'drupal_get_form',
  893. 'page arguments' => array('qbf_admin_settings'),
  894. );
  895. $items[QBF_PATH_MAIN] = array
  896. (
  897. 'type' => MENU_CALLBACK,
  898. 'access arguments' => array(QBF_PERM_QUERY),
  899. 'page callback' => '_qbf_query_form',
  900. );
  901. $items[QBF_PATH_MAIN . '/%qbf_query'] = array
  902. (
  903. 'type' => MENU_CALLBACK,
  904. 'access arguments' => array(QBF_PERM_QUERY),
  905. 'page callback' => '_qbf_query_form',
  906. 'page arguments' => array(1),
  907. );
  908. $items[QBF_PATH_MAIN . '/%qbf_query/delete'] = array
  909. (
  910. 'type' => MENU_CALLBACK,
  911. 'access arguments' => array(QBF_PERM_QUERY),
  912. 'page callback' => '_qbf_query_delete',
  913. 'page arguments' => array(1),
  914. );
  915. return $items;
  916. }
  917. /**
  918. * Implement hook_perm().
  919. *
  920. * @todo D7: Format will change
  921. * @see http://drupal.org/node/224333#descriptions-permissions
  922. *
  923. * @ingroup hooks
  924. * @return array
  925. */
  926. function qbf_perm()
  927. {
  928. $ret = array
  929. (
  930. QBF_PERM_ADMIN,
  931. QBF_PERM_QUERY,
  932. );
  933. return $ret;
  934. }
  935. /**
  936. * Load a saved QBF query, or an empty query by type
  937. *
  938. * @link http://drupal.org/node/109153#load @endlink
  939. *
  940. * @param int $us_qid
  941. * @return array
  942. * A form_values array
  943. */
  944. function qbf_query_load($us_qid)
  945. {
  946. static $query = NULL;
  947. // Only allow query loading by logged-in users
  948. if (user_is_anonymous())
  949. {
  950. return FALSE;
  951. }
  952. // Filter out visibly invalid values
  953. $qid = (is_numeric($us_qid) && ($us_qid > 0))
  954. ? $us_qid
  955. : 0;
  956. // If this is not a saved query, it may be a QBF query type
  957. if ($qid === 0)
  958. {
  959. $ar_forms = qbf_forms();
  960. foreach ($ar_forms as /* $qbf_form_id => */ $form_info)
  961. {
  962. if ($us_qid === $form_info['callback arguments'][0]['form'])
  963. {
  964. $query = new Qbf_Query($us_qid);
  965. break;
  966. }
  967. }
  968. }
  969. if (is_null($query) && $qid)
  970. {
  971. $sq = 'SELECT qq.qid, qq.uid, qq.type, qq.name, qq.query '
  972. . 'FROM {%s} qq '
  973. . 'WHERE qq.qid = %d ';
  974. // db_rewrite_sql does not apply here: access control is further down
  975. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  976. $query = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  977. if ($query !== FALSE)
  978. {
  979. $query->query = unserialize($query->query);
  980. // dsm($query);
  981. }
  982. }
  983. global $user;
  984. $ret = (isset($query) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN)))
  985. ? $query
  986. : FALSE;
  987. return $ret;
  988. }
  989. /**
  990. * Provide an optional automatic mapping mechanism for query building.
  991. *
  992. * This function takes a partly built query map $ar_queryMap, and a defaults
  993. * array to complete it in $ar_defaults, and returns a fully built query array
  994. * ready to be used for querying.
  995. *
  996. * @param array $ar_query_map
  997. * @param array $ar_defaults
  998. * @return array
  999. */
  1000. function qbf_query_mapper($ar_query_map = array(), $ar_defaults = array())
  1001. {
  1002. $ret = array();
  1003. foreach ($ar_query_map as $name => $value)
  1004. {
  1005. // accept NULL, empty strings...
  1006. if (!is_array($value))
  1007. {
  1008. $value = array();
  1009. }
  1010. $item = $value;
  1011. foreach ($ar_defaults as $default_key => $default_value)
  1012. {
  1013. if (!array_key_exists($default_key, $item))
  1014. {
  1015. $item[$default_key] = is_null($default_value)
  1016. ? $name
  1017. : $default_value;
  1018. }
  1019. // else if is already in $item, so we don't touch it
  1020. }
  1021. $ret[$name] = $item;
  1022. }
  1023. return $ret;
  1024. }
  1025. /**
  1026. * Implement hook_user().
  1027. *
  1028. * Display saved QBF searches as an account form category
  1029. *
  1030. * Edit and account could be passed by reference, but are currently not modified.
  1031. *
  1032. * @ingroup hooks
  1033. *
  1034. * @param string $op
  1035. * @param array &$edit
  1036. * @param array $account
  1037. * @param string $category
  1038. * @return array|void
  1039. */
  1040. function qbf_user($op, $edit, $account, $category = NULL)
  1041. {
  1042. $qbf_category = variable_get(QBF_VAR_PROFILE_CATEGORY, QBF_DEF_PROFILE_CATEGORY);
  1043. // dsm("hook user($op, edit, $account->uid = $account->name, $category)");
  1044. switch ($op)
  1045. {
  1046. case 'categories':
  1047. // dsm("hook user($op)");
  1048. $ret = array();
  1049. $ret[] = array
  1050. (
  1051. 'name' => 'qbf',
  1052. 'title' => $qbf_category,
  1053. 'weight' => 2,
  1054. );
  1055. break;
  1056. // case 'view':
  1057. // // Only allow field to QBF admins and own user
  1058. // if ($user->uid != $account->uid && !user_access(QBF_PERM_ADMIN))
  1059. // {
  1060. // return;
  1061. // }
  1062. //
  1063. // $account->content['queries'] = array
  1064. // (
  1065. // '#type' => 'user_profile_category',
  1066. // '#title' => t('Saved queries'),
  1067. // // '#class' => "qbf-user-$category",
  1068. // );
  1069. // $account->content['queries']['list'] = array
  1070. // (
  1071. // '#type' => 'user_profile_item',
  1072. // '#title' => t('List of searches'),
  1073. // '#value' => '<p>Would appear here</p>',
  1074. // );
  1075. // $none_message = ($account->uid == $user->uid)
  1076. // ? t('None yet. !newQuery', array('!newQuery' => $new_query_link))
  1077. // : t('None yet.');
  1078. // $saved = ($count > 0)
  1079. // ? format_plural($count, 'One saved query. ', '@count saved queries. ')
  1080. // . l(t('View/edit'), "user/$account->uid/edit/qbf")
  1081. // : $none_message;
  1082. // dsm($account->content);
  1083. // break;
  1084. case 'form':
  1085. // dsm("hook user($op, $account->uid = $account->name, $category)");
  1086. if ($category != 'qbf')
  1087. {
  1088. $ret = NULL;
  1089. break;
  1090. }
  1091. // No access control: it is already controlled by the
  1092. // "administer users" permission in user.module
  1093. $ar_queries = qbf_get_queries_by_user($account->uid);
  1094. $count = count($ar_queries);
  1095. $ar_header = array
  1096. (
  1097. t('Query type'),
  1098. t('Query title'),
  1099. t('Saved on'),
  1100. // t('# results'),
  1101. t('Actions'),
  1102. );
  1103. $ar_data = array();
  1104. foreach ($ar_queries as $query)
  1105. {
  1106. $ar_data[] = array
  1107. (
  1108. _qbf_get_name_from_type($query->type),
  1109. l($query->name, QBF_PATH_MAIN . "/$query->qid"),
  1110. format_date($query->updated, 'small'),
  1111. // t('n.a.'),
  1112. l(t('Delete'), QBF_PATH_MAIN ."/$query->qid/delete", array('query' => "destination=user/$account->uid/edit/qbf")),
  1113. );
  1114. }
  1115. $data = theme('table', $ar_header, $ar_data);
  1116. $max_count = variable_get(QBF_VAR_MAX_QUERIES, QBF_DEF_MAX_QUERIES);
  1117. if ($count < $max_count)
  1118. {
  1119. $new_query_link = l(t('Create new query'), QBF_PATH_MAIN);
  1120. $data .= format_plural($max_count - $count,
  1121. '<p>You may still save one more query.',
  1122. '<p>You may still save @count more queries.');
  1123. $data .= " $new_query_link.</p>";
  1124. }
  1125. else
  1126. {
  1127. $data .= t("<p>You have reached the maximum number of saved queries (@count).</p>",
  1128. array('@count' => $count));
  1129. }
  1130. /**
  1131. * A first-level form element is needed by contrib module profile_privacy,
  1132. * at least in version 5.x-1.2 and 6.x-1-2. This mimics the
  1133. * user_profile_category/user_profile_item scheme provided by profile.module.
  1134. * We do not use these types, in order to use the full display area width
  1135. * for the queries table.
  1136. */
  1137. $ret['qbf'] = array
  1138. (
  1139. '#type' => 'markup', // in profile, usually user_profile_category
  1140. '#title' => NULL,
  1141. );
  1142. $ret['qbf']['queries'] = array
  1143. (
  1144. '#type' => 'markup', // in profile, usually user_profile_item
  1145. '#value' => $data,
  1146. );
  1147. break;
  1148. }
  1149. return $ret;
  1150. }
  1151. error_reporting($_qbf_er);