qbf.module 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338
  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.15 2009-03-24 13:01:58 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. * @return void
  266. */
  267. function _qbf_form_perform_submit($form, &$form_state)
  268. {
  269. $callback = $form_state['values']['qbf_query'];
  270. if (function_exists(($callback)))
  271. {
  272. $ar_query = _qbf_extract_query(NULL, $form, $form_state['values']);
  273. $form_state['qbf_results'] = $callback($ar_query);
  274. }
  275. $form_state['rebuild'] = TRUE;
  276. }
  277. /**
  278. * Validate handler for qbf_form, Perform search button.
  279. *
  280. * @param array $form
  281. * @param array $form_state
  282. * Can be passed by reference if changes are needed
  283. * @return void
  284. */
  285. function _qbf_form_perform_validate($form, $form_state)
  286. {
  287. // @todo validate searches: checkboxes sets needs at least one value checked, otherwise there won't be any result
  288. }
  289. /**
  290. * Submit handler for qbf_form, Save search button.
  291. *
  292. * @param array $form
  293. * @param array $form_state
  294. * Can be passed by reference if changes are needed
  295. * @return integer
  296. * The id of the saved query.
  297. */
  298. function _qbf_form_save_submit($form, $form_state)
  299. {
  300. $qid = _qbf_save($form_state['values']['form_id'], $form_state);
  301. drupal_set_message(t('Your query was saved as "@name".',
  302. array('@name' => $form_state['values']['qbf_save_name'])));
  303. global $user;
  304. $form_state['redirect'] = "user/$user->uid/edit/qbf";
  305. return $qid;
  306. }
  307. /**
  308. * Validate handler for qbf_form, Save search button.
  309. *
  310. * @param array $form
  311. * @param array $form_state
  312. * @return void
  313. */
  314. //function _qbf_form_save_validate($form, &$form_state)
  315. // {
  316. // // @todo validate saves. Check whether any validation is necessary.
  317. // }
  318. /**
  319. * Return the human-readable for a query type.
  320. *
  321. * @param string $query_type
  322. * @return string
  323. */
  324. function _qbf_get_name_from_type($query_type)
  325. {
  326. static $labels = array();
  327. if (empty($labels) || empty($labels[$query_type]))
  328. {
  329. $ar_forms = qbf_forms();
  330. foreach ($ar_forms as /* $qbf_form_id => */ $form_info)
  331. {
  332. if ($query_type == $form_info['callback arguments'][0]['form'])
  333. {
  334. $labels[$query_type] = $form_info['callback arguments'][0]['label'];
  335. break;
  336. }
  337. }
  338. }
  339. return $labels[$query_type];
  340. }
  341. /**
  342. * Delete a query by qid
  343. *
  344. * In the qbf/<qid>/delete case, $query has been tested for validity and access
  345. * in qbf_query_load(), so it is safe and accessible.
  346. *
  347. * Outside this context, the function can also be invoken with just a qid, and
  348. * the same check via qbf_query_load() will be performed.
  349. *
  350. * @param mixed $query
  351. * int or object
  352. */
  353. function _qbf_query_delete($query)
  354. {
  355. global $user;
  356. if (is_int($query))
  357. {
  358. $query = qbf_query_load($query);
  359. }
  360. if ($query) // access already checked in explicit or implicit qbf_query_load
  361. {
  362. $qid = $query->qid;
  363. $sq = 'DELETE FROM %s WHERE qid = %d ';
  364. db_query($sq, QBF_TABLE_NAME, $qid);
  365. $message = t('Query @id "@name" has been deleted.', array
  366. (
  367. '@id' => $qid,
  368. '@name' => $query->name,
  369. ));
  370. drupal_set_message($message, 'status');
  371. $link = l($qid, QBF_PATH_MAIN .'/'. $qid .'/delete');
  372. $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);
  373. watchdog('qbf', $message, NULL, WATCHDOG_NOTICE, $link);
  374. // access check: we only send the message to the query owner, so access is
  375. // granted without an additional check
  376. if ($notify /* && $query->uid != $user->uid */)
  377. {
  378. $owner = user_load(array('uid' => $query->uid));
  379. $account = user_load(array('uid' => $query->uid));
  380. $language = user_preferred_language($account);
  381. $params = array
  382. (
  383. 'query' => $query,
  384. 'owner' => $owner, // unused by default, but can be used in a hook_mail_alter() implementation
  385. 'deletor' => $user,
  386. 'language' => $language,
  387. );
  388. /* $ret = */ drupal_mail('qbf', __FUNCTION__, $user->mail, $language, $params, $user->mail);
  389. drupal_set_message(t('User !link has been informed', array
  390. (
  391. '!link' => l($account->name, 'user/'. $query->uid),
  392. )));
  393. // dsm(array("QQD, ret" => $ret));
  394. }
  395. }
  396. else {
  397. $message = t('Failed attempt to delete query @qid. Administrator has been alerted.', array
  398. (
  399. '@qid' => $qid,
  400. ));
  401. drupal_set_message($message, 'error');
  402. watchdog('qbf', $message, NULL, WATCHDOG_ERROR, $link);
  403. }
  404. drupal_goto();
  405. }
  406. /**
  407. * Main query page.
  408. *
  409. * This returns the query form if a valid query id or query type is specified,
  410. * or the list of available query types if several exisit, or jumps to the single
  411. * available query type if only one exists.
  412. *
  413. * @param object $query
  414. * Valid query, loaded by qbf_query_load().
  415. * @return string
  416. */
  417. function _qbf_query_form($query = NULL )
  418. {
  419. if (!empty($query))
  420. {
  421. $qbf_form_id = 'qbf_' . $query->type;
  422. $ret = drupal_get_form($qbf_form_id, $query);
  423. }
  424. else
  425. {
  426. $ar_forms = qbf_forms();
  427. $arRet = array();
  428. foreach ($ar_forms as $qbf_form_id => $form_info)
  429. {
  430. $form_id = $form_info['callback arguments'][0]['form'];
  431. $arRet[QBF_PATH_MAIN . "/$form_id"] = l($form_info['callback arguments'][0]['label'],
  432. QBF_PATH_MAIN . "/$form_id");
  433. }
  434. // If there is only one form type, no need to ask the user.
  435. if (count($arRet) == 1)
  436. {
  437. reset($arRet);
  438. drupal_goto(key($arRet));
  439. }
  440. else
  441. {
  442. $ret = theme('item_list', $arRet, t('Choose a query type'));
  443. }
  444. }
  445. return $ret;
  446. }
  447. /**
  448. * Save a query and return its qid.
  449. *
  450. * This is not a hook_save() implementation, hence the "_".
  451. *
  452. * @ingroup forms
  453. *
  454. * @param $form_id string
  455. * @param $form_state array
  456. * @return int
  457. */
  458. function _qbf_save($form_id, $form_state)
  459. {
  460. if (user_is_anonymous())
  461. {
  462. $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
  463. drupal_set_message($warning, 'error');
  464. watchdog('qbf', $warning, NULL, WATCHDOG_WARNING);
  465. $ret = 0;
  466. }
  467. else
  468. {
  469. // @FIXME check whether form_state is now needed. It wasn't in QBF for D5
  470. $form = drupal_retrieve_form($form_id, $form_state);
  471. // dsm($form, "retrieve");
  472. drupal_prepare_form($form_id, $form, $form_state);
  473. // dsm($form, "prepare");
  474. $name = $form_state['values']['qbf_save_name'];
  475. $type = $form_state['values']['qbf_save_type'];
  476. // dsm($form_state);
  477. $form_values = _qbf_extract_query(NULL, $form, $form_state['values']);
  478. // dsm($form_values);
  479. $ar_values = array();
  480. foreach ($form_values as $key => $value)
  481. {
  482. if (empty($value))
  483. {
  484. continue;
  485. }
  486. $ar_values[$key] = $value;
  487. }
  488. $query = new Qbf_Query($type, $name, $ar_values);
  489. $ret = $query->save();
  490. }
  491. return $ret;
  492. }
  493. /**
  494. * Transform a form element for QBF.
  495. *
  496. * QBF-specific properties are:
  497. * - #qbf : array of properties
  498. * - #level: only within #qbf
  499. *
  500. * See QBF_* constants
  501. *
  502. * @ingroup forms
  503. *
  504. * @param string $key
  505. * @param array $element
  506. * @param array $form_state
  507. * @param object $query
  508. * @return array
  509. */
  510. function _qbf_transform($key, $element, $form_state, $query)
  511. {
  512. // dsm(array('key' => $key, 'element' => $element));
  513. /**
  514. * List default type transformations applied to widget by FAPI.
  515. * Types without a default transformation are not transformed
  516. */
  517. static $ar_default_type_transformations = array
  518. (
  519. 'button' => NULL, // no content
  520. 'file' => NULL, // non-querable (yet ?)
  521. 'image_button' => NULL, // new in D6
  522. 'markup' => NULL, // no content
  523. 'password' => NULL, // forbidden
  524. 'radio' => NULL, // single radio is useless, unlike a set of them
  525. 'submit' => NULL, // no content
  526. 'textarea' => 'textfield', // reduce text for searches
  527. // Don't transform these:
  528. // 'checkbox' => NULL,
  529. // 'checkboxes' => NULL,
  530. // 'date' => NULL,
  531. // 'fieldset' => NULL, // useful visually
  532. // 'form' => NULL, // removing it would delete the whole shebang
  533. // 'hidden' => NULL, // non-querable visually, but may be useful
  534. // 'item' => NULL,
  535. // 'radios' => NULL,
  536. // 'select' => NULL,
  537. // 'textfield' => NULL,
  538. // 'value' => 'value',
  539. // 'weight' => NULL,
  540. );
  541. /**
  542. * List default property transformations applied to widget by FAPI property.
  543. *
  544. * Properties without a default transformation are not transformed
  545. */
  546. static $ar_default_property_transformations = array
  547. (
  548. // Standard properties
  549. '#action' => NULL,
  550. '#after_build' => NULL,
  551. // '#base' => NULL, // gone in D6
  552. '#button_type' => NULL,
  553. '#built' => NULL,
  554. '#description' => NULL,
  555. '#method' => NULL,
  556. '#parents' => NULL,
  557. '#redirect' => NULL,
  558. '#ref' => NULL,
  559. '#required' => NULL,
  560. '#rows' => NULL,
  561. // '#submit' => NULL,
  562. '#tree' => NULL,
  563. // '#validate' => NULL,
  564. );
  565. /**
  566. * List properties causing causing element removal.
  567. *
  568. * The key is the property name, the value is the one causing removal.
  569. */
  570. static $ar_killer_properties = array
  571. (
  572. '#disabled' => TRUE,
  573. );
  574. // Transform type
  575. $source_type = $element['#type'];
  576. // .. Default transformation
  577. $dest_type = array_key_exists($source_type, $ar_default_type_transformations)
  578. ? $ar_default_type_transformations[$source_type]
  579. : $source_type;
  580. // .. Apply form-defined type override
  581. if (isset($element['#qbf']['#type']))
  582. {
  583. $dest_type = $element['#qbf']['#type'];
  584. }
  585. if (is_null($dest_type))
  586. {
  587. $ret = NULL;
  588. }
  589. else
  590. {
  591. $ret = $element;
  592. $ret['#type'] = $dest_type;
  593. if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
  594. {
  595. $ret = NULL;
  596. }
  597. else
  598. {
  599. foreach (element_properties($element) as $property_name)
  600. {
  601. // Apply killer properties first to avoid useless work
  602. if (array_key_exists($property_name, $ar_killer_properties)
  603. && ($element[$property_name] = $ar_killer_properties[$property_name]))
  604. {
  605. $ret = NULL;
  606. break;
  607. }
  608. // Now transform or copy remaining properties
  609. if (array_key_exists($property_name, $ar_default_property_transformations))
  610. {
  611. $ret[$property_name] = $ar_default_property_transformations[$property_name];
  612. }
  613. else
  614. {
  615. $ret[$property_name] = $element[$property_name];
  616. }
  617. // And apply form-defined property overrides
  618. if ($property_name == '#qbf')
  619. {
  620. foreach ($element[$property_name] as $override_name => $override_value)
  621. {
  622. $ret[$override_name] = $override_value;
  623. }
  624. }
  625. }
  626. if (isset($form_state['values'][$key]))
  627. {
  628. $ret['#default_value'] = $form_state['values'][$key];
  629. }
  630. elseif (isset($query->query[$key]))
  631. {
  632. $ret['#default_value'] = $query->query[$key];
  633. }
  634. // Recursively transform children
  635. foreach (element_children($element) as $child_name)
  636. {
  637. $child = _qbf_transform($child_name, $element[$child_name], $form_state, $query);
  638. if (is_null($child))
  639. {
  640. unset($ret[$child_name]);
  641. }
  642. else
  643. {
  644. $ret[$child_name] = $child;
  645. }
  646. }
  647. }
  648. }
  649. //dsm(array('key' => $key, 'transformed element' => $ret));
  650. return $ret;
  651. }
  652. /**
  653. * Implement the former hook_settings().
  654. *
  655. * @return array
  656. */
  657. function qbf_admin_settings()
  658. {
  659. $form = array();
  660. $form['queries'] = array
  661. (
  662. '#type' => 'fieldset',
  663. '#title' => t('Queries'),
  664. '#collapsible' => TRUE,
  665. '#collapsed' => TRUE,
  666. );
  667. $form['queries'][QBF_VAR_MAX_QUERIES] = array
  668. (
  669. '#type' => 'select',
  670. '#title' => t('Maximum number of saved queries'),
  671. '#description' => t('The maximum number of queries a user allowed to perform queries may save.'),
  672. '#default_value' => variable_get(QBF_VAR_MAX_QUERIES, QBF_DEF_MAX_QUERIES),
  673. '#options' => array_combine($iota = range(1, 99), $iota),
  674. );
  675. $ar_options = Qbf_Query_Mode::get_options();
  676. $form['queries'][QBF_VAR_QUERY_MODE] = array
  677. (
  678. '#type' => 'radios',
  679. '#title' => t('Query mode'),
  680. '#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.'),
  681. '#options' => $ar_options,
  682. '#default_value' => variable_get(QBF_VAR_QUERY_MODE, Qbf_Query_Mode::CONTAINS),
  683. );
  684. $form['queries'][QBF_VAR_PROFILE_CATEGORY] = array
  685. (
  686. '#type' => 'textfield',
  687. '#title' => t('Name of profile category'),
  688. '#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.'),
  689. '#default_value' => variable_get(QBF_VAR_PROFILE_CATEGORY, QBF_DEF_PROFILE_CATEGORY),
  690. );
  691. $form['notifications'] = array
  692. (
  693. '#type' => 'fieldset',
  694. '#title' => t('Notifications'),
  695. '#collapsible' => TRUE,
  696. '#collapsed' => TRUE,
  697. );
  698. $form['notifications'][QBF_VAR_NOTIFY_DELETE] = array
  699. (
  700. '#type' => 'checkbox',
  701. '#default_value' => variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE),
  702. '#title' => t('Notify users when one of their saved searches has been deleted'),
  703. );
  704. return system_settings_form($form);
  705. }
  706. /**
  707. * The QBF form builder.
  708. *
  709. * @param array $form_state
  710. * @param array $query_info
  711. * The query structure array
  712. * @param string $qbf_form_id
  713. * The name of the QBF form
  714. * @param string $query
  715. * The saved query.
  716. */
  717. function qbf_form(&$form_state, $query_info, $qbf_form_id, $query = NULL)
  718. {
  719. $form_id = $query_info['form'];
  720. // Fetch the basic form and rename it, passing it the previous values
  721. $node = new stdClass();
  722. $form = $form_id($node, $form_state);
  723. $qbf_form = array();
  724. $qbf_form['#id'] = $qbf_form_id;
  725. $qbf_form['#qbf_source_form_id'] = $form_id;
  726. // On the form element itself, only keep the QBF properties, the handlers, and the children
  727. foreach (element_properties($form) as $key)
  728. {
  729. if (in_array($key, array('#qbf', '#validate', '#submit')))
  730. {
  731. $qbf_form[$key] = $form[$key];
  732. }
  733. }
  734. // Transform the children tree
  735. foreach (element_children($form) as $key)
  736. {
  737. $new_element = _qbf_transform($key, $form[$key], $form_state, $query);
  738. if (!is_null($new_element))
  739. {
  740. $qbf_form[$key] = $new_element;
  741. }
  742. }
  743. $qbf_form['qbf'] = array
  744. (
  745. '#type' => 'fieldset',
  746. '#title' => t('Query'),
  747. );
  748. if (isset($form_state['values']) && !empty($form_state['values']))
  749. {
  750. if (isset($form_state['qbf_results']))
  751. {
  752. $qbf_form['qbf']['qbf_results'] = array
  753. (
  754. '#type' => 'markup',
  755. '#prefix' => '<p>',
  756. '#value' => $form_state['qbf_results'],
  757. '#suffix' => '</p>',
  758. );
  759. }
  760. }
  761. /**
  762. * Offer query save feature only to logged-in users with query permission.
  763. * We know they have QBF_PERM_QUERY since it is needed to access this form,
  764. * from qbf_menu().
  765. */
  766. if (user_is_anonymous())
  767. {
  768. $qbf_form['qbf']['save'] = array
  769. (
  770. '#type' => 'markup',
  771. '#value' => t('<p>You need to be logged in order to save a query.</p>'),
  772. );
  773. }
  774. else
  775. {
  776. global $user;
  777. $ar_queries = qbf_get_queries_by_user($user->uid);
  778. $max_queries = variable_get(QBF_VAR_MAX_QUERIES, QBF_DEF_MAX_QUERIES);
  779. if (count($ar_queries) >= $max_queries)
  780. {
  781. $qbf_form['qbf']['save'] = array
  782. (
  783. '#type' => 'markup',
  784. '#value' => t('<p>You already have reached the maximum number of saved queries (@max). To save this query, first <a href="!queries">remove</a> at least one saved query.</p>',
  785. array
  786. (
  787. '@max' => $max_queries,
  788. '!queries' => url("user/$user->uid/edit/qbf"),
  789. )
  790. ),
  791. );
  792. }
  793. else
  794. {
  795. $qbf_form['qbf']['qbf_save_type'] = array
  796. (
  797. '#type' => 'hidden',
  798. '#value' => $query_info['form'],
  799. );
  800. $qbf_form['qbf']['qbf_save_name'] = array
  801. (
  802. '#title' => t('Name of query in your save list'),
  803. '#type' => 'textfield',
  804. '#required' => TRUE,
  805. '#default_value' => empty($query->name)
  806. ? t('@label - @time', array('@label' => $query_info['label'], '@time' => format_date(time(), 'large')))
  807. : $query->name,
  808. );
  809. $qbf_form['qbf']['qbf_save'] = array
  810. (
  811. '#submit' => array('_qbf_form_save_submit'),
  812. '#validate' => array('_qbf_form_save_validate'),
  813. '#type' => 'submit',
  814. '#value' => t('Save query'),
  815. '#weight' => 2,
  816. );
  817. }
  818. }
  819. $qbf_form['qbf']['qbf_query'] = array
  820. (
  821. '#type' => 'hidden',
  822. '#value' => $query_info['callback'],
  823. );
  824. $qbf_form['qbf']['qbf_perform'] = array
  825. (
  826. '#submit' => array('_qbf_form_perform_submit'),
  827. '#validate' => array('_qbf_form_perform_validate'),
  828. '#type' => 'submit',
  829. '#value' => t('Perform query'),
  830. '#weight' => 1,
  831. );
  832. return $qbf_form;
  833. }
  834. /**
  835. * Implement hook_forms().
  836. *
  837. * @link http://drupal.org/node/144132#hook-forms @endlink
  838. *
  839. * hook_qbf_register() returns an array of QBF-able node types, indexed by the
  840. * node type, with the following properties:
  841. * - form: the name of the hook_form() implementation (a $form_id)
  842. * - label: the human-readable type name under which the queries are saved by QBF
  843. * - callback: the function QBF must invoke to query the node type. It will
  844. * receive the query type and a filtered version of $form_state['values']
  845. * containing only valid node fields, and must return a themed grid of query
  846. * results, which will be displayed as a #markup FAPI element. In advanced
  847. * uses, a single callback can be used for several query types by using the
  848. * query type parameter to know what the values apply to.
  849. *
  850. * @ingroup forms
  851. * @ingroup hooks
  852. *
  853. * @param array $args
  854. * @return array
  855. */
  856. function qbf_forms(/* $args = NULL */)
  857. {
  858. static $forms = array();
  859. if (empty($forms))
  860. {
  861. $hook_name = 'qbf_register';
  862. // dsm(array("QBF_forms $qbf_form_id" => $args));
  863. // More efficient than using module_invoke_all: we avoid array-merging + re-looping
  864. foreach (module_implements($hook_name) as $module)
  865. {
  866. $arImplementations = module_invoke($module, $hook_name);
  867. // dsm($arImplementations);
  868. foreach ($arImplementations as /* $node_type => */ $query_info)
  869. {
  870. $qbf_form_id = 'qbf_' . $query_info['form'];
  871. $forms[$qbf_form_id] = array
  872. (
  873. 'callback' => 'qbf_form',
  874. 'callback arguments' => array($query_info, $qbf_form_id),
  875. );
  876. } // foreach implementation
  877. } // foreach module
  878. } // if empty
  879. return $forms;
  880. }
  881. /**
  882. * List queries owned by a given user.
  883. *
  884. * @param int $uid > 0
  885. * Defaults to current user
  886. * @return array
  887. */
  888. function qbf_get_queries_by_user($uid = NULL)
  889. {
  890. if (is_null($uid))
  891. {
  892. global $user;
  893. $uid = $user->uid;
  894. }
  895. $sq = 'SELECT qq.qid, qq.uid, qq.type, qq.name, qq.query, qq.updated '
  896. . 'FROM {%s} qq '
  897. . 'WHERE qq.uid = %d '
  898. . 'ORDER BY qq.type, qq.name ';
  899. // no db_rewrite_sql: this function is not in a menu callback, so it is up to
  900. // the caller to check access
  901. $q = db_query($sq, QBF_TABLE_NAME, $uid);
  902. $ret = array();
  903. while (is_object($o = db_fetch_object($q)))
  904. {
  905. $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
  906. }
  907. return $ret;
  908. }
  909. /**
  910. * Implement hook_mail().
  911. *
  912. * @param string $key
  913. * @param array $message
  914. * @param array $params
  915. * @return void
  916. */
  917. function qbf_mail($key, &$message, $params)
  918. {
  919. if ($key != '_qbf_query_delete')
  920. {
  921. return;
  922. }
  923. // dsm(array('QBF_mail key' => $key, 'message' => $message, 'params' => $params));
  924. $deletor_tokens = user_mail_tokens($params['deletor'], $params['language']->language);
  925. $tokens = array_merge($deletor_tokens, array
  926. (
  927. '!qname' => $params['query']->name,
  928. '!qid' => $params['query']->qid,
  929. ));
  930. $message['subject'] = t('Effacement d\'une recherche !site enregistrée', $tokens);
  931. $message['body'] = t("!date\n\nVotre recherche !qid: !qname\nsur le site !site vient d'être effacée par !username.", $tokens);
  932. }
  933. /**
  934. * Implement hook_menu().
  935. *
  936. * @return array
  937. */
  938. function qbf_menu()
  939. {
  940. $items = array();
  941. $items[QBF_PATH_SETTINGS] = array
  942. (
  943. 'title' => 'Query-By-Form',
  944. 'access arguments' => array(QBF_PERM_ADMIN),
  945. 'page callback' => 'drupal_get_form',
  946. 'page arguments' => array('qbf_admin_settings'),
  947. );
  948. $items[QBF_PATH_MAIN] = array
  949. (
  950. 'type' => MENU_CALLBACK,
  951. 'access arguments' => array(QBF_PERM_QUERY),
  952. 'page callback' => '_qbf_query_form',
  953. );
  954. $items[QBF_PATH_MAIN . '/%qbf_query'] = array
  955. (
  956. 'type' => MENU_CALLBACK,
  957. 'access arguments' => array(QBF_PERM_QUERY),
  958. 'page callback' => '_qbf_query_form',
  959. 'page arguments' => array(1),
  960. );
  961. $items[QBF_PATH_MAIN . '/%qbf_query/delete'] = array
  962. (
  963. 'type' => MENU_CALLBACK,
  964. 'access arguments' => array(QBF_PERM_QUERY),
  965. 'page callback' => '_qbf_query_delete',
  966. 'page arguments' => array(1),
  967. );
  968. return $items;
  969. }
  970. /**
  971. * Implement hook_perm().
  972. *
  973. * @todo D7: Format will change
  974. * @see http://drupal.org/node/224333#descriptions-permissions
  975. *
  976. * @ingroup hooks
  977. * @return array
  978. */
  979. function qbf_perm()
  980. {
  981. $ret = array
  982. (
  983. QBF_PERM_ADMIN,
  984. QBF_PERM_QUERY,
  985. );
  986. return $ret;
  987. }
  988. /**
  989. * Load a saved QBF query, or an empty query by type
  990. *
  991. * @link http://drupal.org/node/109153#load @endlink
  992. *
  993. * @param int $us_qid
  994. * @return array
  995. * A form_values array
  996. */
  997. function qbf_query_load($us_qid)
  998. {
  999. static $query = NULL;
  1000. // Only allow query loading by logged-in users
  1001. if (user_is_anonymous())
  1002. {
  1003. return FALSE;
  1004. }
  1005. // Filter out visibly invalid values
  1006. $qid = (is_numeric($us_qid) && ($us_qid > 0))
  1007. ? $us_qid
  1008. : 0;
  1009. // If this is not a saved query, it may be a QBF query type
  1010. if ($qid === 0)
  1011. {
  1012. $ar_forms = qbf_forms();
  1013. foreach ($ar_forms as /* $qbf_form_id => */ $form_info)
  1014. {
  1015. if ($us_qid === $form_info['callback arguments'][0]['form'])
  1016. {
  1017. $query = new Qbf_Query($us_qid);
  1018. break;
  1019. }
  1020. }
  1021. }
  1022. if (is_null($query) && $qid)
  1023. {
  1024. $sq = 'SELECT qq.qid, qq.uid, qq.type, qq.name, qq.query '
  1025. . 'FROM {%s} qq '
  1026. . 'WHERE qq.qid = %d ';
  1027. // db_rewrite_sql does not apply here: access control is further down
  1028. $q = db_query($sq, QBF_TABLE_NAME, $qid);
  1029. $query = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  1030. if ($query !== FALSE)
  1031. {
  1032. $query->query = unserialize($query->query);
  1033. // dsm($query);
  1034. }
  1035. }
  1036. global $user;
  1037. $ret = (isset($query) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN)))
  1038. ? $query
  1039. : FALSE;
  1040. return $ret;
  1041. }
  1042. /**
  1043. * Provide an optional automatic mapping mechanism for query building.
  1044. *
  1045. * This function takes a partly built query map $ar_queryMap, and a defaults
  1046. * array to complete it in $ar_defaults, and returns a fully built query array
  1047. * ready to be used for querying.
  1048. *
  1049. * @param array $ar_query_map
  1050. * @param array $ar_defaults
  1051. * @return array
  1052. */
  1053. function qbf_query_mapper($ar_query_map = array(), $ar_defaults = array())
  1054. {
  1055. $ret = array();
  1056. foreach ($ar_query_map as $name => $value)
  1057. {
  1058. // accept NULL, empty strings...
  1059. if (!is_array($value))
  1060. {
  1061. $value = array();
  1062. }
  1063. $item = $value;
  1064. foreach ($ar_defaults as $default_key => $default_value)
  1065. {
  1066. if (!array_key_exists($default_key, $item))
  1067. {
  1068. $item[$default_key] = is_null($default_value)
  1069. ? $name
  1070. : $default_value;
  1071. }
  1072. // else if is already in $item, so we don't touch it
  1073. }
  1074. $ret[$name] = $item;
  1075. }
  1076. return $ret;
  1077. }
  1078. /**
  1079. * Strip QBF properties from a form (or element) array.
  1080. *
  1081. * @param array &$element
  1082. * @return void
  1083. */
  1084. function qbf_strip_element(&$element)
  1085. {
  1086. foreach (element_propeties($element) as $key)
  1087. {
  1088. if (strpos($key, '#qbf') === 0)
  1089. {
  1090. unset($element[$key]);
  1091. }
  1092. }
  1093. foreach (element_children($element) as $key)
  1094. {
  1095. qbf_strip_element($element[$key]);
  1096. }
  1097. }
  1098. /**
  1099. * Implement hook_user().
  1100. *
  1101. * Display saved QBF searches as an account form category
  1102. *
  1103. * Edit and account could be passed by reference, but are currently not modified.
  1104. *
  1105. * @ingroup hooks
  1106. *
  1107. * @param string $op
  1108. * @param array &$edit
  1109. * @param array $account
  1110. * @param string $category
  1111. * @return array|void
  1112. */
  1113. function qbf_user($op, $edit, $account, $category = NULL)
  1114. {
  1115. $qbf_category = variable_get(QBF_VAR_PROFILE_CATEGORY, QBF_DEF_PROFILE_CATEGORY);
  1116. // dsm("hook user($op, edit, $account->uid = $account->name, $category)");
  1117. switch ($op)
  1118. {
  1119. case 'categories':
  1120. // dsm("hook user($op)");
  1121. $ret = array();
  1122. $ret[] = array
  1123. (
  1124. 'name' => 'qbf',
  1125. 'title' => $qbf_category,
  1126. 'weight' => 2,
  1127. );
  1128. break;
  1129. // case 'view':
  1130. // // Only allow field to QBF admins and own user
  1131. // if ($user->uid != $account->uid && !user_access(QBF_PERM_ADMIN))
  1132. // {
  1133. // return;
  1134. // }
  1135. //
  1136. // $account->content['queries'] = array
  1137. // (
  1138. // '#type' => 'user_profile_category',
  1139. // '#title' => t('Saved queries'),
  1140. // // '#class' => "qbf-user-$category",
  1141. // );
  1142. // $account->content['queries']['list'] = array
  1143. // (
  1144. // '#type' => 'user_profile_item',
  1145. // '#title' => t('List of searches'),
  1146. // '#value' => '<p>Would appear here</p>',
  1147. // );
  1148. // $none_message = ($account->uid == $user->uid)
  1149. // ? t('None yet. !newQuery', array('!newQuery' => $new_query_link))
  1150. // : t('None yet.');
  1151. // $saved = ($count > 0)
  1152. // ? format_plural($count, 'One saved query. ', '@count saved queries. ')
  1153. // . l(t('View/edit'), "user/$account->uid/edit/qbf")
  1154. // : $none_message;
  1155. // dsm($account->content);
  1156. // break;
  1157. case 'form':
  1158. // dsm("hook user($op, $account->uid = $account->name, $category)");
  1159. if ($category != 'qbf')
  1160. {
  1161. $ret = NULL;
  1162. break;
  1163. }
  1164. // No access control: it is already controlled by the
  1165. // "administer users" permission in user.module
  1166. $ar_queries = qbf_get_queries_by_user($account->uid);
  1167. $count = count($ar_queries);
  1168. $ar_header = array
  1169. (
  1170. t('Query type'),
  1171. t('Query title'),
  1172. t('Saved on'),
  1173. // t('# results'),
  1174. t('Actions'),
  1175. );
  1176. $ar_data = array();
  1177. foreach ($ar_queries as $query)
  1178. {
  1179. $ar_data[] = array
  1180. (
  1181. _qbf_get_name_from_type($query->type),
  1182. l($query->name, QBF_PATH_MAIN . "/$query->qid"),
  1183. format_date($query->updated, 'small'),
  1184. // t('n.a.'),
  1185. l(t('Delete'), QBF_PATH_MAIN ."/$query->qid/delete", array('query' => "destination=user/$account->uid/edit/qbf")),
  1186. );
  1187. }
  1188. $data = theme('table', $ar_header, $ar_data);
  1189. $max_count = variable_get(QBF_VAR_MAX_QUERIES, QBF_DEF_MAX_QUERIES);
  1190. if ($count < $max_count)
  1191. {
  1192. $new_query_link = l(t('Create new query'), QBF_PATH_MAIN);
  1193. $data .= format_plural($max_count - $count,
  1194. '<p>You may still save one more query.',
  1195. '<p>You may still save @count more queries.');
  1196. $data .= " $new_query_link.</p>";
  1197. }
  1198. else
  1199. {
  1200. $data .= t("<p>You have reached the maximum number of saved queries (@count).</p>",
  1201. array('@count' => $count));
  1202. }
  1203. /**
  1204. * A first-level form element is needed by contrib module profile_privacy,
  1205. * at least in version 5.x-1.2 and 6.x-1-2. This mimics the
  1206. * user_profile_category/user_profile_item scheme provided by profile.module.
  1207. * We do not use these types, in order to use the full display area width
  1208. * for the queries table.
  1209. */
  1210. $ret['qbf'] = array
  1211. (
  1212. '#type' => 'markup', // in profile, usually user_profile_category
  1213. '#title' => NULL,
  1214. );
  1215. $ret['qbf']['queries'] = array
  1216. (
  1217. '#type' => 'markup', // in profile, usually user_profile_item
  1218. '#value' => $data,
  1219. );
  1220. break;
  1221. }
  1222. return $ret;
  1223. }
  1224. error_reporting($_qbf_er);