feedback.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  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. ),
  62. // Disable Metatags (metatag) module's entity form additions.
  63. 'metatags' => FALSE,
  64. ),
  65. );
  66. return $return;
  67. }
  68. /**
  69. * Entity uri callback.
  70. */
  71. function feedback_uri($entry) {
  72. return array(
  73. 'path' => 'admin/reports/feedback/' . $entry->fid,
  74. );
  75. }
  76. /**
  77. * Implements hook_permission().
  78. */
  79. function feedback_permission() {
  80. return array(
  81. 'access feedback form' => array(
  82. 'title' => t('Access feedback form'),
  83. 'description' => t('Submit feedback messages.'),
  84. ),
  85. 'view feedback messages' => array(
  86. 'title' => t('View feedback messages'),
  87. 'description' => t('View, process, and delete submitted feedback messages.'),
  88. ),
  89. 'administer feedback' => array(
  90. 'title' => t('Administer feedback settings'),
  91. ),
  92. );
  93. }
  94. /**
  95. * Implements hook_menu().
  96. */
  97. function feedback_menu() {
  98. $items['admin/reports/feedback'] = array(
  99. 'title' => 'Feedback messages',
  100. 'description' => 'View feedback messages.',
  101. 'page callback' => 'drupal_get_form',
  102. 'page arguments' => array('feedback_admin_view_form'),
  103. 'access arguments' => array('view feedback messages'),
  104. 'file' => 'feedback.admin.inc',
  105. );
  106. $items['admin/reports/feedback/%feedback'] = array(
  107. 'title' => 'Feedback entry',
  108. 'page callback' => 'feedback_view',
  109. 'page arguments' => array(3),
  110. 'access arguments' => array('view feedback messages'),
  111. 'file' => 'feedback.admin.inc',
  112. );
  113. $items['admin/reports/feedback/%feedback/delete'] = array(
  114. 'title' => 'Delete feedback entry',
  115. 'page callback' => 'drupal_get_form',
  116. 'page arguments' => array('feedback_delete_confirm', 3),
  117. 'access arguments' => array('view feedback messages'),
  118. 'file' => 'feedback.admin.inc',
  119. );
  120. $items['admin/config/user-interface/feedback'] = array(
  121. 'title' => 'Feedback',
  122. 'description' => 'Administer feedback settings.',
  123. 'page callback' => 'drupal_get_form',
  124. 'page arguments' => array('feedback_admin_settings_form'),
  125. 'access arguments' => array('administer feedback'),
  126. 'file' => 'feedback.admin.inc',
  127. );
  128. $items['admin/config/user-interface/feedback/settings'] = array(
  129. 'title' => 'Settings',
  130. 'type' => MENU_DEFAULT_LOCAL_TASK,
  131. 'weight' => -10,
  132. );
  133. return $items;
  134. }
  135. /**
  136. * Implements hook_init().
  137. */
  138. function feedback_init() {
  139. if (user_access('access feedback form')) {
  140. $path = drupal_get_path('module', 'feedback');
  141. drupal_add_css($path . '/feedback.css');
  142. drupal_add_js($path . '/feedback.js');
  143. }
  144. }
  145. /**
  146. * Implements hook_page_build().
  147. */
  148. function feedback_page_build(&$page) {
  149. if (user_access('access feedback form') && !feedback_match_path(variable_get('feedback_excluded_paths', 'admin/reports/feedback'))) {
  150. $page['page_bottom']['feedback'] = array(
  151. '#theme' => 'feedback_form_display',
  152. '#title' => t('Feedback'),
  153. '#content' => drupal_get_form('feedback_form'),
  154. );
  155. }
  156. }
  157. /**
  158. * Check if the current path matches any pattern in a set of patterns.
  159. *
  160. * @param $patterns
  161. * String containing a set of patterns separated by \n, \r or \r\n.
  162. *
  163. * @return
  164. * Boolean value: TRUE if the current path or alias matches a pattern.
  165. */
  166. function feedback_match_path($patterns) {
  167. // Convert path to lowercase. This allows comparison of the same path
  168. // with different case. Ex: /Page, /page, /PAGE.
  169. $patterns = drupal_strtolower($patterns);
  170. // Convert the current path to lowercase.
  171. $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
  172. // Compare the lowercase internal and lowercase path alias (if any).
  173. $page_match = drupal_match_path($path, $patterns);
  174. if ($path != $_GET['q']) {
  175. $page_match = $page_match || drupal_match_path($_GET['q'], $patterns);
  176. }
  177. return $page_match;
  178. }
  179. /**
  180. * Form constructor for the feedback form.
  181. *
  182. * @see feedback_form_submit()
  183. * @ingroup forms
  184. */
  185. function feedback_form($form, &$form_state) {
  186. $form['#attributes']['class'] = array('feedback-form');
  187. // Store the path on which this form is displayed.
  188. if (!isset($form_state['inline']['location'])) {
  189. $form_state['inline']['location'] = $_GET['q'];
  190. }
  191. # changed value by type by hidden to be able to change location value in JS
  192. // $form['location'] = array(
  193. // '#type' => 'value',
  194. // '#value' => $form_state['inline']['location'],
  195. // );
  196. $form['location'] = array(
  197. '#type' => 'hidden',
  198. '#default_value' => $form_state['inline']['location'],
  199. );
  200. $form['help'] = array(
  201. '#prefix' => '<div class="feedback-help">',
  202. '#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.'),
  203. '#suffix' => '</div>',
  204. );
  205. if (user_access('view feedback messages')) {
  206. if (arg(0) != 'node') {
  207. $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location_masked' => feedback_mask_path($_GET['q'])));
  208. }
  209. else {
  210. $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location' => $_GET['q']));
  211. }
  212. if ($feedbacks) {
  213. $form['messages'] = array(
  214. '#prefix' => '<div class="feedback-messages">',
  215. '#suffix' => '</div>',
  216. );
  217. foreach ($feedbacks as $fid => $feedback) {
  218. $form['messages'][$fid]['#feedback'] = $feedback;
  219. $form['messages'][$fid]['submitted'] = array('#markup' => t('@feedback-author !feedback-date:', array('@feedback-author' => format_username($feedback), '!feedback-date' => format_date($feedback->timestamp, 'small'))));
  220. $form['messages'][$fid]['submitted']['#prefix'] = '<div class="feedback-submitted">';
  221. $form['messages'][$fid]['submitted']['#suffix'] = '</div>';
  222. $form['messages'][$fid]['body'] = feedback_format_message($feedback);
  223. $form['messages'][$fid]['body']['#prefix'] = '<div class="feedback-body">';
  224. $form['messages'][$fid]['body']['#suffix'] = '</div>';
  225. }
  226. }
  227. }
  228. $form['message'] = array(
  229. '#type' => 'textarea',
  230. '#attributes' => array('class' => array('feedback-message')),
  231. '#cols' => 20,
  232. '#title' => t('Message'),
  233. '#required' => TRUE,
  234. '#wysiwyg' => FALSE,
  235. );
  236. $entry = new stdClass();
  237. field_attach_form('feedback', $entry, $form, $form_state);
  238. $form['actions'] = array(
  239. '#type' => 'actions',
  240. // Without clearfix, the AJAX throbber wraps in an ugly way.
  241. // @todo Patch #type actions in core?
  242. '#attributes' => array('class' => array('clearfix')),
  243. );
  244. $form['actions']['submit'] = array(
  245. '#type' => 'submit',
  246. '#value' => t('Send feedback'),
  247. '#id' => 'feedback-submit',
  248. '#ajax' => array(
  249. 'wrapper' => 'feedback-form',
  250. 'callback' => 'feedback_form_ajax_callback',
  251. 'progress' => array(
  252. 'type' => 'throbber',
  253. 'message' => '',
  254. ),
  255. ),
  256. );
  257. return $form;
  258. }
  259. /**
  260. * Form submission handler for feedback_form().
  261. */
  262. function feedback_form_submit($form, &$form_state) {
  263. $entry = new stdClass();
  264. entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
  265. $entry->message = $form_state['values']['message'];
  266. $entry->location = $form_state['values']['location'];
  267. feedback_save($entry);
  268. drupal_set_message(t('Thanks for your feedback!'));
  269. }
  270. /**
  271. * AJAX callback for feedback_form() submissions.
  272. */
  273. function feedback_form_ajax_callback($form, &$form_state) {
  274. // If there was a form validation error, re-render the entire form.
  275. if (!$form_state['executed']) {
  276. return $form;
  277. }
  278. // Otherwise, return a fresh copy of the form, so the user may post additional
  279. // feedback.
  280. // Reset the static cache of drupal_html_id().
  281. // @see drupal_process_form()
  282. // @see drupal_html_id()
  283. $seen_ids = &drupal_static('drupal_html_id');
  284. $seen_ids = array();
  285. // Prevent the form from being processed again.
  286. // @see drupal_build_form()
  287. list($form, $new_form_state) = ajax_get_form();
  288. $new_form_state['input'] = array();
  289. drupal_process_form($form['#form_id'], $form, $new_form_state);
  290. // Return AJAX commands in order to output the special success message.
  291. // @see ajax_deliver()
  292. $build = array('#type' => 'ajax');
  293. $html = drupal_render($form);
  294. $build['#commands'][] = ajax_command_insert(NULL, $html);
  295. // A successful form submission normally means that there were no errors, so
  296. // we only render status messages.
  297. $messages = drupal_get_messages();
  298. $messages += array('status' => array());
  299. $messages = implode('<br />', $messages['status']);
  300. $html = '<div id="feedback-status-message">' . $messages . '</div>';
  301. $build['#commands'][] = ajax_command_append('#block-feedback-form', $html);
  302. return $build;
  303. }
  304. /**
  305. * Returns HTML for a feedback entry.
  306. *
  307. * @param $entry
  308. * A feedback object.
  309. */
  310. function feedback_format_message($entry) {
  311. $message = check_plain($entry->message);
  312. if (!empty($entry->useragent)) {
  313. if (module_exists('browscap')) {
  314. $browserinfo = browscap_get_browser($entry->useragent);
  315. // Browscap returns useragent but not always parent info.
  316. $browser = (isset($browserinfo['parent']) ? $browserinfo['parent'] . ' / ' . $browserinfo['platform'] : $browserinfo['useragent']);
  317. $message .= '<div class="browserinfo">(' . check_plain($browser) . ')</div>';
  318. }
  319. else {
  320. $message .= '<div class="browserinfo">(' . check_plain($entry->useragent) . ')</div>';
  321. }
  322. }
  323. $uri = entity_uri('feedback', $entry);
  324. if ($uri['path'] != $_GET['q']) {
  325. $links['view'] = array('title' => t('view'), 'href' => $uri['path']);
  326. }
  327. else {
  328. $links['delete'] = array('title' => t('delete'), 'href' => $uri['path'] . '/delete');
  329. }
  330. $elements['message'] = array('#markup' => $message);
  331. $elements['links'] = array('#theme' => 'links__feedback_message', '#links' => $links);
  332. return $elements;
  333. }
  334. /**
  335. * Loads a feedback entry from the database.
  336. *
  337. * @param $fid
  338. * Integer specifying the feedback ID to load.
  339. *
  340. * @return
  341. * A loaded feedback entry object upon successful load, or FALSE if the entry
  342. * cannot be loaded.
  343. *
  344. * @see feedback_load_multiple()
  345. */
  346. function feedback_load($fid) {
  347. $entries = feedback_load_multiple(array($fid));
  348. return (isset($entries[$fid]) ? $entries[$fid] : FALSE);
  349. }
  350. /**
  351. * Loads feedback entries from the database.
  352. *
  353. * @param $fids
  354. * An array of feedback entry IDs.
  355. * @param $conditions
  356. * An associative array of conditions on the {feedback} table, where the keys
  357. * are the database fields and the values are the values those fields
  358. * must have.
  359. *
  360. * @return
  361. * An array of feedback entry objects indexed by fid.
  362. *
  363. * @see hook_feedback_load()
  364. * @see feedback_load()
  365. * @see entity_load()
  366. * @see EntityFieldQuery
  367. */
  368. function feedback_load_multiple($fids = array(), $conditions = array()) {
  369. return entity_load('feedback', $fids, $conditions);
  370. }
  371. /**
  372. * Saves changes to a feedback entry or adds a new feedback entry.
  373. *
  374. * @param $entry
  375. * The feedback entry object to modify or add. If $entry->fid is omitted, a
  376. * new entry will be added.
  377. *
  378. * @see hook_feedback_insert()
  379. * @see hook_feedback_update()
  380. */
  381. function feedback_save($entry) {
  382. global $user;
  383. // Load the stored entity, if any.
  384. if (!empty($entry->fid) && !isset($entry->original)) {
  385. $entry->original = entity_load_unchanged('feedback', $entry->fid);
  386. }
  387. field_attach_presave('feedback', $entry);
  388. // Allow modules to alter the feedback entry before saving.
  389. module_invoke_all('feedback_presave', $entry);
  390. module_invoke_all('entity_presave', $entry, 'feedback');
  391. if (empty($entry->fid)) {
  392. $entry->message = trim($entry->message);
  393. $defaults = array(
  394. 'uid' => $user->uid,
  395. 'location_masked' => feedback_mask_path($entry->location),
  396. 'url' => url($entry->location, array('absolute' => TRUE)),
  397. 'timestamp' => REQUEST_TIME,
  398. 'useragent' => $_SERVER['HTTP_USER_AGENT'],
  399. );
  400. foreach ($defaults as $key => $default) {
  401. if (!isset($entry->$key)) {
  402. $entry->$key = $default;
  403. }
  404. }
  405. $status = drupal_write_record('feedback', $entry);
  406. field_attach_insert('feedback', $entry);
  407. module_invoke_all('feedback_insert', $entry);
  408. module_invoke_all('entity_insert', $entry, 'feedback');
  409. }
  410. else {
  411. $status = drupal_write_record('feedback', $entry, 'fid');
  412. field_attach_update('feedback', $entry);
  413. module_invoke_all('feedback_update', $entry);
  414. module_invoke_all('entity_update', $entry, 'feedback');
  415. }
  416. unset($entry->original);
  417. return $status;
  418. }
  419. /**
  420. * Deletes a feedback entry.
  421. *
  422. * @param $fid
  423. * A feedback entry ID.
  424. */
  425. function feedback_delete($fid) {
  426. feedback_delete_multiple(array($fid));
  427. }
  428. /**
  429. * Deletes multiple feedback entries.
  430. *
  431. * @param $fids
  432. * An array of feedback entry IDs.
  433. */
  434. function feedback_delete_multiple($fids) {
  435. if (!empty($fids)) {
  436. $entries = feedback_load_multiple($fids);
  437. foreach ($entries as $fid => $entry) {
  438. field_attach_delete('feedback', $entry);
  439. module_invoke_all('feedback_delete', $entry);
  440. module_invoke_all('entity_delete', $entry, 'feedback');
  441. }
  442. db_delete('feedback')
  443. ->condition('fid', $fids, 'IN')
  444. ->execute();
  445. }
  446. }
  447. /**
  448. * 'Mask' a path, i.e. replace all numeric arguments in a path with '%' placeholders.
  449. *
  450. * Please note that only numeric arguments with a preceding slash will be
  451. * replaced.
  452. *
  453. * @param $path
  454. * An internal Drupal path, f.e. 'user/123/edit'.
  455. * @return
  456. * A 'masked' path, for above example 'user/%/edit'.
  457. *
  458. * @todo Use the untranslated patch of menu_get_item() instead.
  459. */
  460. function feedback_mask_path($path) {
  461. return preg_replace('@/\d+@', '/%', $path);
  462. }
  463. /**
  464. * Implements hook_user_cancel().
  465. */
  466. function feedback_user_cancel($edit, $account, $method) {
  467. switch ($method) {
  468. case 'user_cancel_reassign':
  469. db_update('feedback')
  470. ->fields(array('uid' => 0))
  471. ->condition('uid', $account->uid)
  472. ->execute();
  473. break;
  474. }
  475. }
  476. /**
  477. * Implements hook_user_delete().
  478. */
  479. function feedback_user_delete($account) {
  480. $fids = db_query('SELECT fid FROM {feedback} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
  481. feedback_delete_multiple($fids);
  482. }
  483. /**
  484. * Implements hook_mollom_form_list().
  485. */
  486. function feedback_mollom_form_list() {
  487. $forms['feedback_form'] = array(
  488. 'title' => t('Feedback form'),
  489. 'entity' => 'feedback',
  490. 'report access' => array('view feedback messages'),
  491. );
  492. return $forms;
  493. }
  494. /**
  495. * Implements hook_mollom_form_info().
  496. */
  497. function feedback_mollom_form_info($form_id) {
  498. if ($form_id == 'feedback_form') {
  499. return array(
  500. 'mode' => MOLLOM_MODE_ANALYSIS,
  501. 'bypass access' => array('administer feedback'),
  502. 'elements' => array(
  503. 'message' => t('Message'),
  504. ),
  505. );
  506. }
  507. }
  508. /**
  509. * Implements hook_views_api();
  510. */
  511. function feedback_views_api() {
  512. return array(
  513. 'api' => 3.0,
  514. 'path' => drupal_get_path('module', 'feedback') . '/views',
  515. );
  516. }