feedback.module 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. <?php
  2. /**
  3. * @file
  4. * Allows site visitors and users to report issues about this site.
  5. */
  6. /**
  7. * Open state (unprocessed) for feedback entries.
  8. */
  9. define('FEEDBACK_OPEN', 0);
  10. /**
  11. * Processed state for feedback entries.
  12. */
  13. define('FEEDBACK_PROCESSED', 1);
  14. /**
  15. * Implements hook_theme().
  16. */
  17. function feedback_theme() {
  18. return array(
  19. 'feedback_admin_view_form' => array(
  20. 'render element' => 'form',
  21. ),
  22. 'feedback_entry' => array(
  23. 'render element' => 'elements',
  24. 'template' => 'feedback-entry',
  25. 'file' => 'feedback.admin.inc',
  26. ),
  27. 'feedback_form_display' => array(
  28. 'template' => 'feedback-form-display',
  29. 'variables' => array('title' => NULL, 'content' => NULL),
  30. ),
  31. );
  32. }
  33. /**
  34. * Implements hook_entity_info().
  35. */
  36. function feedback_entity_info() {
  37. $return = array(
  38. 'feedback' => array(
  39. 'label' => t('Feedback'),
  40. 'controller class' => 'FeedbackController',
  41. 'base table' => 'feedback',
  42. 'uri callback' => 'feedback_uri',
  43. 'fieldable' => TRUE,
  44. 'entity keys' => array(
  45. 'id' => 'fid',
  46. ),
  47. 'bundles' => array(
  48. 'feedback' => array(
  49. 'label' => t('Feedback'),
  50. 'admin' => array(
  51. 'path' => 'admin/config/user-interface/feedback',
  52. 'access arguments' => array('administer feedback'),
  53. ),
  54. ),
  55. ),
  56. 'view modes' => array(
  57. 'full' => array(
  58. 'label' => t('Full feedback entry'),
  59. 'custom settings' => FALSE,
  60. ),
  61. 'teaser' => array(
  62. 'label' => t('Teaser'),
  63. 'custom settings' => FALSE,
  64. ),
  65. 'widget' => array(
  66. 'label' => t('Widget'),
  67. 'custom settings' => FALSE,
  68. ),
  69. ),
  70. // Disable Metatags (metatag) module's entity form additions.
  71. 'metatags' => FALSE,
  72. ),
  73. );
  74. return $return;
  75. }
  76. /**
  77. * Implements hook_entity_property_info().
  78. */
  79. function feedback_entity_property_info() {
  80. $info = array();
  81. $properties = &$info['feedback']['properties'];
  82. $properties['fid'] = array(
  83. 'label' => t('Feedback ID'),
  84. 'type' => 'integer',
  85. 'description' => t('The Feedback ID'),
  86. 'schema field' => 'fid',
  87. );
  88. $properties['status'] = array(
  89. 'label' => t("Status"),
  90. 'type' => 'integer',
  91. 'description' => t("0 for new, 1 for processed"),
  92. 'schema field' => 'status',
  93. );
  94. $properties['author'] = array(
  95. 'label' => t("Author"),
  96. 'type' => 'user',
  97. 'description' => t("The author of the feedback."),
  98. 'setter callback' => 'entity_property_verbatim_set',
  99. 'required' => TRUE,
  100. 'schema field' => 'uid',
  101. );
  102. $properties['message'] = array(
  103. 'label' => t("Title"),
  104. 'description' => t("The feedback message."),
  105. 'setter callback' => 'entity_property_verbatim_set',
  106. 'schema field' => 'message',
  107. 'required' => TRUE,
  108. );
  109. $properties['timestamp'] = array(
  110. 'label' => t("Date created"),
  111. 'type' => 'date',
  112. 'schema field' => 'timestamp',
  113. 'description' => t("The date the feedback was created."),
  114. );
  115. return $info;
  116. }
  117. /**
  118. * Implements hook_field_extra_fields().
  119. */
  120. function feedback_field_extra_fields() {
  121. $extras['feedback']['feedback']['form']['help'] = array(
  122. 'label' => t('Help'),
  123. 'description' => t('Feedback submission guidelines'),
  124. 'weight' => -10,
  125. );
  126. $extras['feedback']['feedback']['form']['messages'] = array(
  127. 'label' => t('Entries'),
  128. 'description' => t('Existing feedback entries for the current page'),
  129. 'weight' => -5,
  130. );
  131. $extras['feedback']['feedback']['form']['message'] = array(
  132. 'label' => t('Message'),
  133. 'description' => t('Feedback message form text field'),
  134. 'weight' => 0,
  135. );
  136. $extras['feedback']['feedback']['display']['location'] = array(
  137. 'label' => t('Location'),
  138. 'description' => t('The URL of the page the message was submitted on'),
  139. 'weight' => -15,
  140. );
  141. $extras['feedback']['feedback']['display']['date'] = array(
  142. 'label' => t('Date'),
  143. 'description' => t('The submission date of the message'),
  144. 'weight' => -10,
  145. );
  146. $extras['feedback']['feedback']['display']['user'] = array(
  147. 'label' => t('User'),
  148. 'description' => t('The name of the user who submitted the message'),
  149. 'weight' => -5,
  150. );
  151. $extras['feedback']['feedback']['display']['message'] = array(
  152. 'label' => t('Message'),
  153. 'description' => t('The main feedback message'),
  154. 'weight' => 0,
  155. );
  156. return $extras;
  157. }
  158. /**
  159. * Entity uri callback.
  160. */
  161. function feedback_uri($entry) {
  162. return array(
  163. 'path' => 'admin/reports/feedback/' . $entry->fid,
  164. );
  165. }
  166. /**
  167. * Implements hook_permission().
  168. */
  169. function feedback_permission() {
  170. return array(
  171. 'access feedback form' => array(
  172. 'title' => t('Access feedback form'),
  173. 'description' => t('Submit feedback messages.'),
  174. ),
  175. 'view feedback messages' => array(
  176. 'title' => t('View feedback messages'),
  177. 'description' => t('View, process, and delete submitted feedback messages.'),
  178. ),
  179. 'administer feedback' => array(
  180. 'title' => t('Administer feedback settings'),
  181. ),
  182. );
  183. }
  184. /**
  185. * Implements hook_menu().
  186. */
  187. function feedback_menu() {
  188. $items['admin/reports/feedback'] = array(
  189. 'title' => 'Feedback messages',
  190. 'description' => 'View feedback messages.',
  191. 'page callback' => 'drupal_get_form',
  192. 'page arguments' => array('feedback_admin_view_form'),
  193. 'access arguments' => array('view feedback messages'),
  194. 'file' => 'feedback.admin.inc',
  195. );
  196. $items['admin/reports/feedback/%feedback'] = array(
  197. 'title' => 'Feedback entry',
  198. 'page callback' => 'feedback_view',
  199. 'page arguments' => array(3),
  200. 'access arguments' => array('view feedback messages'),
  201. 'file' => 'feedback.admin.inc',
  202. );
  203. $items['admin/reports/feedback/%feedback/view'] = array(
  204. 'title' => 'View',
  205. 'type' => MENU_DEFAULT_LOCAL_TASK,
  206. 'weight' => -10,
  207. );
  208. $items['admin/reports/feedback/%feedback/edit'] = array(
  209. 'title' => 'Edit',
  210. 'page callback' => 'drupal_get_form',
  211. 'page arguments' => array('feedback_entry_form', 3),
  212. 'access arguments' => array('view feedback messages'),
  213. 'type' => MENU_LOCAL_TASK,
  214. 'file' => 'feedback.admin.inc',
  215. );
  216. $items['admin/reports/feedback/%feedback/delete'] = array(
  217. 'title' => 'Delete feedback entry',
  218. 'page callback' => 'drupal_get_form',
  219. 'page arguments' => array('feedback_delete_confirm', 3),
  220. 'access arguments' => array('view feedback messages'),
  221. 'type' => MENU_CALLBACK,
  222. 'file' => 'feedback.admin.inc',
  223. );
  224. $items['admin/config/user-interface/feedback'] = array(
  225. 'title' => 'Feedback',
  226. 'description' => 'Administer feedback settings.',
  227. 'page callback' => 'drupal_get_form',
  228. 'page arguments' => array('feedback_admin_settings_form'),
  229. 'access arguments' => array('administer feedback'),
  230. 'file' => 'feedback.admin.inc',
  231. );
  232. $items['admin/config/user-interface/feedback/settings'] = array(
  233. 'title' => 'Settings',
  234. 'type' => MENU_DEFAULT_LOCAL_TASK,
  235. 'weight' => -10,
  236. );
  237. return $items;
  238. }
  239. /**
  240. * Implements hook_page_build().
  241. */
  242. function feedback_page_build(&$page) {
  243. if (user_access('access feedback form') && !feedback_match_path(variable_get('feedback_excluded_paths', 'admin/reports/feedback'))) {
  244. $page['page_bottom']['feedback'] = array(
  245. '#theme' => 'feedback_form_display',
  246. '#title' => t('Feedback'),
  247. '#content' => drupal_get_form('feedback_form'),
  248. );
  249. $path = drupal_get_path('module', 'feedback');
  250. $page['page_bottom']['feedback']['#attached']['css'][] = $path . '/feedback.css';
  251. $page['page_bottom']['feedback']['#attached']['js'][] = $path . '/feedback.js';
  252. }
  253. }
  254. /**
  255. * Check if the current path matches any pattern in a set of patterns.
  256. *
  257. * @param $patterns
  258. * String containing a set of patterns separated by \n, \r or \r\n.
  259. *
  260. * @return
  261. * Boolean value: TRUE if the current path or alias matches a pattern.
  262. */
  263. function feedback_match_path($patterns) {
  264. // Convert path to lowercase. This allows comparison of the same path
  265. // with different case. Ex: /Page, /page, /PAGE.
  266. $patterns = drupal_strtolower($patterns);
  267. // Convert the current path to lowercase.
  268. $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
  269. // Compare the lowercase internal and lowercase path alias (if any).
  270. $page_match = drupal_match_path($path, $patterns);
  271. if ($path != $_GET['q']) {
  272. $page_match = $page_match || drupal_match_path($_GET['q'], $patterns);
  273. }
  274. return $page_match;
  275. }
  276. /**
  277. * Form constructor for the feedback form.
  278. *
  279. * @see feedback_form_submit()
  280. * @ingroup forms
  281. */
  282. function feedback_form($form, &$form_state) {
  283. $form['#attributes']['class'] = array('feedback-form');
  284. // Store the path on which this form is displayed.
  285. if (!isset($form_state['inline']['location'])) {
  286. $form_state['inline']['location'] = $_GET['q'];
  287. }
  288. $form['location'] = array(
  289. '#type' => 'value',
  290. '#value' => $form_state['inline']['location'],
  291. );
  292. $form['help'] = array(
  293. '#prefix' => '<div class="feedback-help">',
  294. '#markup' => t('If you experience a bug or would like to see an addition on the current page, feel free to leave us a message.'),
  295. '#suffix' => '</div>',
  296. );
  297. if (user_access('view feedback messages')) {
  298. if (arg(0) != 'node') {
  299. $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location_masked' => feedback_mask_path($_GET['q'])));
  300. }
  301. else {
  302. $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location' => $_GET['q']));
  303. }
  304. if ($feedbacks) {
  305. form_load_include($form_state, 'inc', 'feedback', 'feedback.admin');
  306. $form['messages'] = array(
  307. '#prefix' => '<div class="feedback-messages">',
  308. '#suffix' => '</div>',
  309. );
  310. foreach ($feedbacks as $fid => $feedback) {
  311. $form['messages'][$fid] = array(
  312. '#type' => 'container',
  313. '#attributes' => array('class' => array('feedback-entry')),
  314. '#feedback' => $feedback,
  315. );
  316. $form['messages'][$fid]['submitted'] = array(
  317. '#markup' => t('@feedback-author !feedback-date:', array(
  318. '@feedback-author' => format_username($feedback),
  319. '!feedback-date' => format_date($feedback->timestamp, 'small'),
  320. )),
  321. );
  322. $form['messages'][$fid]['submitted']['#prefix'] = '<div class="feedback-submitted">';
  323. $form['messages'][$fid]['submitted']['#suffix'] = '</div>';
  324. feedback_build_content($feedback, 'widget');
  325. $form['messages'][$fid]['body'] = $feedback->content;
  326. unset($feedback->content);
  327. $form['messages'][$fid]['body']['#prefix'] = '<div class="feedback-body">';
  328. $form['messages'][$fid]['body']['#suffix'] = '</div>';
  329. }
  330. }
  331. }
  332. $form['message'] = array(
  333. '#type' => 'textarea',
  334. '#attributes' => array('class' => array('feedback-message')),
  335. '#cols' => 20,
  336. '#title' => t('Message'),
  337. '#required' => TRUE,
  338. '#wysiwyg' => FALSE,
  339. );
  340. $entry = new stdClass();
  341. field_attach_form('feedback', $entry, $form, $form_state);
  342. $form['actions'] = array(
  343. '#type' => 'actions',
  344. // Without clearfix, the AJAX throbber wraps in an ugly way.
  345. // @todo Patch #type actions in core?
  346. '#attributes' => array('class' => array('clearfix')),
  347. );
  348. $form['actions']['submit'] = array(
  349. '#type' => 'submit',
  350. '#value' => t('Send feedback'),
  351. '#id' => 'feedback-submit',
  352. '#ajax' => array(
  353. 'wrapper' => 'feedback-form',
  354. 'callback' => 'feedback_form_ajax_callback',
  355. 'progress' => array(
  356. 'type' => 'throbber',
  357. 'message' => '',
  358. ),
  359. ),
  360. );
  361. return $form;
  362. }
  363. /**
  364. * Form submission handler for feedback_form().
  365. */
  366. function feedback_form_submit($form, &$form_state) {
  367. $entry = new stdClass();
  368. entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
  369. $entry->message = $form_state['values']['message'];
  370. $entry->location = $form_state['values']['location'];
  371. feedback_save($entry);
  372. drupal_set_message(t('Thanks for your feedback!'));
  373. }
  374. /**
  375. * AJAX callback for feedback_form() submissions.
  376. */
  377. function feedback_form_ajax_callback($form, &$form_state) {
  378. // If there was a form validation error, re-render the entire form.
  379. if (!$form_state['executed']) {
  380. return $form;
  381. }
  382. // Otherwise, return a fresh copy of the form, so the user may post additional
  383. // feedback.
  384. // Reset the static cache of drupal_html_id().
  385. // @see drupal_process_form()
  386. // @see drupal_html_id()
  387. $seen_ids = &drupal_static('drupal_html_id');
  388. $seen_ids = array();
  389. // Prevent the form from being processed again.
  390. // @see drupal_build_form()
  391. list($form, $new_form_state) = ajax_get_form();
  392. $new_form_state['input'] = array();
  393. drupal_process_form($form['#form_id'], $form, $new_form_state);
  394. // Return AJAX commands in order to output the special success message.
  395. // @see ajax_deliver()
  396. $build = array('#type' => 'ajax');
  397. $html = drupal_render($form);
  398. $build['#commands'][] = ajax_command_insert(NULL, $html);
  399. // A successful form submission normally means that there were no errors, so
  400. // we only render status messages.
  401. $messages = drupal_get_messages();
  402. $messages += array('status' => array());
  403. $messages = implode('<br />', $messages['status']);
  404. $html = '<div id="feedback-status-message">' . $messages . '</div>';
  405. $build['#commands'][] = ajax_command_append('#block-feedback-form', $html);
  406. return $build;
  407. }
  408. /**
  409. * Loads a feedback entry from the database.
  410. *
  411. * @param $fid
  412. * Integer specifying the feedback ID to load.
  413. *
  414. * @return
  415. * A loaded feedback entry object upon successful load, or FALSE if the entry
  416. * cannot be loaded.
  417. *
  418. * @see feedback_load_multiple()
  419. */
  420. function feedback_load($fid) {
  421. $entries = feedback_load_multiple(array($fid));
  422. return (isset($entries[$fid]) ? $entries[$fid] : FALSE);
  423. }
  424. /**
  425. * Loads feedback entries from the database.
  426. *
  427. * @param $fids
  428. * An array of feedback entry IDs.
  429. * @param $conditions
  430. * An associative array of conditions on the {feedback} table, where the keys
  431. * are the database fields and the values are the values those fields
  432. * must have.
  433. *
  434. * @return
  435. * An array of feedback entry objects indexed by fid.
  436. *
  437. * @see hook_feedback_load()
  438. * @see feedback_load()
  439. * @see entity_load()
  440. * @see EntityFieldQuery
  441. */
  442. function feedback_load_multiple($fids = array(), $conditions = array()) {
  443. return entity_load('feedback', $fids, $conditions);
  444. }
  445. /**
  446. * Saves changes to a feedback entry or adds a new feedback entry.
  447. *
  448. * @param $entry
  449. * The feedback entry object to modify or add. If $entry->fid is omitted, a
  450. * new entry will be added.
  451. *
  452. * @see hook_feedback_insert()
  453. * @see hook_feedback_update()
  454. */
  455. function feedback_save($entry) {
  456. global $user;
  457. // Load the stored entity, if any.
  458. if (!empty($entry->fid) && !isset($entry->original)) {
  459. $entry->original = entity_load_unchanged('feedback', $entry->fid);
  460. }
  461. field_attach_presave('feedback', $entry);
  462. // Allow modules to alter the feedback entry before saving.
  463. module_invoke_all('feedback_presave', $entry);
  464. module_invoke_all('entity_presave', $entry, 'feedback');
  465. if (empty($entry->fid)) {
  466. $entry->message = trim($entry->message);
  467. $defaults = array(
  468. 'uid' => $user->uid,
  469. 'location_masked' => feedback_mask_path($entry->location),
  470. 'url' => url($entry->location, array('absolute' => TRUE)),
  471. 'timestamp' => REQUEST_TIME,
  472. 'useragent' => $_SERVER['HTTP_USER_AGENT'],
  473. );
  474. foreach ($defaults as $key => $default) {
  475. if (!isset($entry->$key)) {
  476. $entry->$key = $default;
  477. }
  478. }
  479. $status = drupal_write_record('feedback', $entry);
  480. field_attach_insert('feedback', $entry);
  481. module_invoke_all('feedback_insert', $entry);
  482. module_invoke_all('entity_insert', $entry, 'feedback');
  483. }
  484. else {
  485. $status = drupal_write_record('feedback', $entry, 'fid');
  486. field_attach_update('feedback', $entry);
  487. module_invoke_all('feedback_update', $entry);
  488. module_invoke_all('entity_update', $entry, 'feedback');
  489. }
  490. unset($entry->original);
  491. return $status;
  492. }
  493. /**
  494. * Deletes a feedback entry.
  495. *
  496. * @param $fid
  497. * A feedback entry ID.
  498. */
  499. function feedback_delete($fid) {
  500. feedback_delete_multiple(array($fid));
  501. }
  502. /**
  503. * Deletes multiple feedback entries.
  504. *
  505. * @param $fids
  506. * An array of feedback entry IDs.
  507. */
  508. function feedback_delete_multiple($fids) {
  509. if (!empty($fids)) {
  510. $entries = feedback_load_multiple($fids);
  511. foreach ($entries as $fid => $entry) {
  512. field_attach_delete('feedback', $entry);
  513. module_invoke_all('feedback_delete', $entry);
  514. module_invoke_all('entity_delete', $entry, 'feedback');
  515. }
  516. db_delete('feedback')
  517. ->condition('fid', $fids, 'IN')
  518. ->execute();
  519. }
  520. }
  521. /**
  522. * 'Mask' a path, i.e. replace all numeric arguments in a path with '%' placeholders.
  523. *
  524. * Please note that only numeric arguments with a preceding slash will be
  525. * replaced.
  526. *
  527. * @param $path
  528. * An internal Drupal path, f.e. 'user/123/edit'.
  529. * @return
  530. * A 'masked' path, for above example 'user/%/edit'.
  531. *
  532. * @todo Use the untranslated patch of menu_get_item() instead.
  533. */
  534. function feedback_mask_path($path) {
  535. return preg_replace('@/\d+@', '/%', $path);
  536. }
  537. /**
  538. * Implements hook_user_cancel().
  539. */
  540. function feedback_user_cancel($edit, $account, $method) {
  541. switch ($method) {
  542. case 'user_cancel_reassign':
  543. db_update('feedback')
  544. ->fields(array('uid' => 0))
  545. ->condition('uid', $account->uid)
  546. ->execute();
  547. break;
  548. }
  549. }
  550. /**
  551. * Implements hook_user_delete().
  552. */
  553. function feedback_user_delete($account) {
  554. $fids = db_query('SELECT fid FROM {feedback} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
  555. feedback_delete_multiple($fids);
  556. }
  557. /**
  558. * Implements hook_mollom_form_list().
  559. */
  560. function feedback_mollom_form_list() {
  561. $forms['feedback_form'] = array(
  562. 'title' => t('Feedback form'),
  563. 'entity' => 'feedback',
  564. 'bundle' => 'feedback',
  565. 'entity delete multiple callback' => 'feedback_delete_multiple',
  566. 'delete form' => 'feedback_delete_confirm',
  567. 'delete form file' => array(
  568. 'name' => 'feedback.admin',
  569. ),
  570. 'report access' => array('view feedback messages'),
  571. );
  572. return $forms;
  573. }
  574. /**
  575. * Implements hook_mollom_form_info().
  576. */
  577. function feedback_mollom_form_info($form_id) {
  578. $form_info = array(
  579. 'mode' => MOLLOM_MODE_ANALYSIS,
  580. 'bypass access' => array('administer feedback'),
  581. 'elements' => array(
  582. 'message' => t('Message'),
  583. ),
  584. );
  585. mollom_form_info_add_fields($form_info, 'feedback', 'feedback');
  586. return $form_info;
  587. }
  588. /**
  589. * Implements hook_views_api();
  590. */
  591. function feedback_views_api() {
  592. return array(
  593. 'api' => 3.0,
  594. 'path' => drupal_get_path('module', 'feedback') . '/views',
  595. );
  596. }
  597. /**
  598. * Implements hook_feedback_insert().
  599. */
  600. function feedback_feedback_insert($entry) {
  601. // Trigger rule if Rules is enabled
  602. if (module_exists('rules')) {
  603. rules_invoke_event('feedback_insert', $entry);
  604. }
  605. }
  606. /**
  607. * Implements hook_feedback_update().
  608. */
  609. function feedback_feedback_update($entry) {
  610. // Trigger rule if Rules is enabled
  611. if (module_exists('rules')) {
  612. rules_invoke_event('feedback_update', $entry);
  613. }
  614. }