Browse Source

merged feedback submodule

Bachir Soussi Chiadmi 9 years ago
parent
commit
88c1012c48
22 changed files with 3708 additions and 0 deletions
  1. 339 0
      sites/all/modules/contrib/users/feedback/LICENSE.txt
  2. 61 0
      sites/all/modules/contrib/users/feedback/README.txt
  3. 338 0
      sites/all/modules/contrib/users/feedback/feedback-353548-45.patch
  4. 30 0
      sites/all/modules/contrib/users/feedback/feedback-entry.tpl.php
  5. 13 0
      sites/all/modules/contrib/users/feedback/feedback-form-display.tpl.php
  6. 412 0
      sites/all/modules/contrib/users/feedback/feedback.admin.inc
  7. 16 0
      sites/all/modules/contrib/users/feedback/feedback.admin.js
  8. 133 0
      sites/all/modules/contrib/users/feedback/feedback.api.php
  9. 23 0
      sites/all/modules/contrib/users/feedback/feedback.controller.inc
  10. 72 0
      sites/all/modules/contrib/users/feedback/feedback.css
  11. 17 0
      sites/all/modules/contrib/users/feedback/feedback.info
  12. 116 0
      sites/all/modules/contrib/users/feedback/feedback.install
  13. 59 0
      sites/all/modules/contrib/users/feedback/feedback.js
  14. 822 0
      sites/all/modules/contrib/users/feedback/feedback.module
  15. 593 0
      sites/all/modules/contrib/users/feedback/feedback.module.orig
  16. BIN
      sites/all/modules/contrib/users/feedback/images/throbber.gif
  17. 159 0
      sites/all/modules/contrib/users/feedback/tests/feedback.test
  18. 184 0
      sites/all/modules/contrib/users/feedback/views/feedback.views.inc
  19. 250 0
      sites/all/modules/contrib/users/feedback/views/feedback.views_default.inc
  20. 45 0
      sites/all/modules/contrib/users/feedback/views/feedback_handler_field_feedback_link.inc
  21. 13 0
      sites/all/modules/contrib/users/feedback/views/feedback_handler_field_feedback_link_delete.inc
  22. 13 0
      sites/all/modules/contrib/users/feedback/views/feedback_handler_field_feedback_link_edit.inc

+ 339 - 0
sites/all/modules/contrib/users/feedback/LICENSE.txt

@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

+ 61 - 0
sites/all/modules/contrib/users/feedback/README.txt

@@ -0,0 +1,61 @@
+
+-- SUMMARY --
+
+Feedback allows visitors and users of a site to report issues for the currently
+displayed page, where issues could be data or theming related bugs, or also
+feature requests.
+
+Site administrators are then able to review and process those feedback messages.
+
+For a full description of the module, visit the project page:
+  http://drupal.org/project/feedback
+To submit bug reports and feature suggestions, or to track changes:
+  http://drupal.org/project/issues/feedback
+
+
+-- INTEGRATED MODULES --
+
+* BrowsCap (optional, record user agents)
+* Unfuddle Feedback (optional, Unfuddle ticketing integration)
+* Mollom (optional, spam prevention)
+* Views (optional)
+
+
+-- INSTALLATION --
+
+* Install as usual, see http://drupal.org/node/895232 for further information.
+
+
+-- CONFIGURATION --
+
+* Configure user permissions in Administration » People » Permissions »
+  Feedback.
+
+* Configure fields at Administration » Configuration » User interface »
+  Feedback.
+
+
+-- USAGE --
+
+* To view all feedback messages, go to Administration » Reports » Feedback
+  messages.
+
+
+-- CONTACT --
+
+Current maintainers:
+* Daniel F. Kudwien (sun) - http://drupal.org/user/54136
+* Jody Hamilton (Jody Lynn) - http://drupal.org/user/112500
+
+Note:
+Feedback module v1.x had a completely different purpose, but its features are
+provided by several other and much more mature modules now (i.e. Contact in
+Drupal core, Webform, CCK, aso.).
+
+This project has been sponsored by:
+* UNLEASHED MIND
+  Specialized in consulting and planning of Drupal powered sites, UNLEASHED
+  MIND offers installation, development, theming, customization, and hosting
+  to get you started. Visit http://www.unleashedmind.com for more information.
+* Zivtech
+

+ 338 - 0
sites/all/modules/contrib/users/feedback/feedback-353548-45.patch

@@ -0,0 +1,338 @@
+diff --git a/feedback.module b/feedback.module
+index 9763575..ef5b24a 100644
+--- a/feedback.module
++++ b/feedback.module
+@@ -534,6 +534,235 @@ function feedback_user_delete($account) {
+ }
+ 
+ /**
++ * Implements hook_action_info().
++ */
++function feedback_action_info() {
++  return array(
++    'feedback_process_action' => array(
++      'label' => t('Process entry'),
++      'type' => 'feedback',
++      'configurable' => FALSE,
++      'behavior' => array('changes_property'),
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++    'feedback_open_action' => array(
++      'label' => t('Open entry'),
++      'type' => 'feedback',
++      'configurable' => FALSE,
++      'behavior' => array('changes_property'),
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++    'feedback_delete_action' => array(
++      'label' => t('Delete entry'),
++      'type' => 'feedback',
++      'configurable' => FALSE,
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++    'feedback_send_email_action' => array(
++      'label' => t('Send e-mail of feedback'),
++      'type' => 'feedback',
++      'configurable' => TRUE,
++      'triggers' => array('feedback_insert', 'feedback_update'),
++    ),
++  );
++}
++
++/**
++* Implements hook_trigger_info().
++*/
++function feedback_trigger_info() {
++  return array(
++    'feedback' => array(
++      'feedback_insert' => array(
++        'label' => t('New feedback is added.'),
++      ),
++      'feedback_update' => array(
++        'label' => t('Feedback is marked processed or open.'),
++      ),
++    ),
++  );
++}
++
++/**
++ * Implements hook_feedback_insert().
++ */
++function feedback_feedback_insert($entry) {
++  $aids = trigger_get_assigned_actions('feedback_insert');
++  if (!$aids) {
++    return;
++  }
++
++  $context = array(
++    'group' => 'feedback',
++    'hook' => 'feedback_insert',
++  );
++
++  foreach ($aids as $aid => $info) {
++    actions_do($aid, $entry, $context);
++  }
++}
++
++/**
++ * Implements hook_feedback_update().
++ */
++function feedback_feedback_update($entry) {
++  $aids = trigger_get_assigned_actions('feedback_update');
++  if (!$aids) {
++    return;
++  }
++
++  $context = array(
++    'group' => 'feedback',
++    'hook' => 'feedback_update',
++  );
++
++  foreach ($aids as $aid => $info) {
++    actions_do($aid, $entry, $context);
++  }
++}
++
++/**
++ * Sets the status of an entry to processed.
++ *
++ * @ingroup actions
++ */
++function feedback_process_action($entry, $context) {
++  $entry->status = FEEDBACK_PROCESSED;
++  drupal_write_record('feedback', $entry, 'fid');
++}
++
++/**
++ * Sets the status of an entry to open.
++ *
++ * @ingroup actions
++ */
++function feedback_open_action($entry, $context) {
++  $entry->status = FEEDBACK_OPEN;
++  drupal_write_record('feedback', $entry, 'fid');
++}
++
++/**
++ * Deletes a feedback entry.
++ *
++ * @ingroup actions
++ */
++function feedback_delete_action($entry, $context) {
++  feedback_delete($entry->fid);
++}
++
++/**
++ * Return a form definition so the Feedback send email action can be configured.
++ *
++ * @param $context
++ *   Default values (if we are editing an existing action instance).
++ * @return
++ *   Form definition.
++ */
++function feedback_send_email_action_form($context = array()) {
++  // Set default values for form.
++  $context += array(
++   'recipients' => '',
++   'subject' => '',
++   'message' => '',
++  );
++
++  $form['recipients'] = array(
++    '#type' => 'textarea',
++    '#title' => t('Recipients'),
++    '#default_value' => $context['recipients'],
++    '#description' => t("Example: 'webmaster@example.com' or 'dev@example.com,support@example.com'. To specify multiple recipients, separate each e-mail address with a comma."),
++    '#required' => TRUE,
++  );
++  $form['subject'] = array(
++    '#type' => 'textfield',
++    '#title' => t('Subject'),
++    '#default_value' => $context['subject'],
++    '#maxlength' => '254',
++    '#description' => t('The subject of the message.'),
++    '#required' => TRUE,
++  );
++  $form['message'] = array(
++    '#type' => 'textarea',
++    '#title' => t('Message'),
++    '#default_value' => $context['message'],
++    '#cols' => '80',
++    '#rows' => '20',
++    '#description' => t('The message that should be sent. You may include the following variables: !uid, !username, !usermail, !useragent (of the user who gave the feedback), !site_name, !status, !message, !url, !date.'),
++    '#required' => TRUE,
++  );
++  return $form;
++}
++
++/**
++ * Validate the send e-mail action form submission.
++ */
++function feedback_send_email_action_validate($form, &$form_state) {
++  if (empty($form_state['values']['recipients'])) {
++    form_set_error('recipients', t('You must enter one or more recipients.'));
++  }
++  else {
++    $recipients = explode(',', $form_state['values']['recipients']);
++    foreach ($recipients as $recipient) {
++      if (!valid_email_address(trim($recipient))) {
++        form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
++      }
++    }
++  }
++}
++
++/**
++ * Process the send e-mail action form submission.
++ */
++function feedback_send_email_action_submit($form, $form_state) {
++  // Process the HTML form to store configuration. The keyed array that
++  // we return will be serialized to the database.
++  $params = array(
++    'recipients' => $form_state['values']['recipients'],
++    'subject'    => $form_state['values']['subject'],
++    'message'    => $form_state['values']['message'],
++  );
++  return $params;
++}
++
++/**
++ * Implements the feedback send e-mail action.
++ */
++function feedback_send_email_action($entry, $context) {
++  $account = user_load($entry->uid);
++  $from = variable_get('site_name', 'Drupal') . ' <' . variable_get('site_mail', '') . '>';
++  $params = array('entry' => $entry, 'account' => $account, 'context' => $context);
++  // Send the e-mail to the recipients using the site default language.
++  drupal_mail('feedback', 'send_email_action', $context['recipients'], language_default(), $params, $from);
++  watchdog('feedback', 'Feedback information sent to %recipients', array('%recipients' => $context['recipients']));
++}
++
++/**
++ * Implements hook_mail().
++ */
++function feedback_mail($key, &$message, $params) {
++  $language = $message['language'];
++  $entry = $params['entry'];
++  $account = $params['account'];
++  $context = $params['context'];
++  $variables = array(
++    '!site_name' => variable_get('site_name', 'Drupal'),
++    '!uid' => $account->uid,
++    '!username' => $account->name ? $account->name : t('Anonymous'),
++    '!usermail' => $account->mail ? $account->mail : t('unknown'),
++    '!status' => $entry->status ? t('Processed') : t('Open'),
++    '!message' => $entry->message,
++    '!url' => url($entry->location, array('absolute' => TRUE, 'language' => $language)),
++    '!useragent' => $entry->useragent,
++    '!date' => format_date($entry->timestamp, 'small', '', NULL, $language->language),
++  );
++  $subject = strtr($context['subject'], $variables);
++  $body = strtr($context['message'], $variables);
++  $message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
++  $message['body'][] = drupal_html_to_text($body);
++}
++
++
++/**
+  * Implements hook_mollom_form_list().
+  */
+ function feedback_mollom_form_list() {
+diff --git a/tests/feedback.test b/tests/feedback.test
+index 4d4244c..09ce922 100644
+--- a/tests/feedback.test
++++ b/tests/feedback.test
+@@ -20,8 +20,7 @@ class FeedbackTestCase extends DrupalWebTestCase {
+   }
+ 
+   function setUp() {
+-    // @todo Remove soft-dependency on Block.
+-    parent::setUp(array('block', 'feedback'));
++    parent::setUp(array('feedback', 'trigger'));
+ 
+     $this->admin_user = $this->drupalCreateUser(array('access feedback form', 'view feedback messages', 'administer feedback'));
+     $this->web_user = $this->drupalCreateUser(array('access feedback form'));
+@@ -49,7 +48,7 @@ class FeedbackTestCase extends DrupalWebTestCase {
+     $edit = array(
+       'feedback-messages[0][1]' => TRUE,
+     );
+-    $this->drupalPost(NULL, $edit, t('Submit'));
++    $this->drupalPost(NULL, $edit, t('Update'));
+     $this->assertFieldByName('feedback-messages[1][1]', 1, t('Processed message found.'));
+   }
+ 
+@@ -89,4 +88,72 @@ class FeedbackTestCase extends DrupalWebTestCase {
+     $this->assertNoLinkByHref('admin/reports/feedback/1/delete');
+     $this->assertNoRaw(check_plain($message), t('Message not found.'));
+   }
+-}
++
++  /**
++   * Test the feedback triggers and actions.
++   */
++  function testFeedbackEmailAction() {
++    $test_user = $this->drupalCreateUser(array('administer actions', 'administer feedback', 'access feedback form'));
++    $this->drupalLogin($test_user);
++    $this->assignFeedbackEmailAction('feedback_insert', $test_user->mail);
++
++    // Insert a feedback entry.
++    $message = $this->randomString();
++    $edit = array(
++      'message' => $message,
++    );
++    $this->drupalPost('node', $edit, t('Send feedback'));
++    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $message);
++
++    $this->assignFeedbackEmailAction('feedback_update', $test_user->mail);
++
++    // Update a feedback entry.
++    $fid = db_query("SELECT fid FROM {feedback} LIMIT 0,1")->fetchField();
++    $entry = feedback_load($fid);
++    feedback_save($entry);
++    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $entry->message);
++  }
++
++  /**
++   * Assigns a system_send_email_action to the passed-in trigger.
++   *
++   * @param $trigger
++   *   For example, 'user_login'
++   */
++  function assignFeedbackEmailAction($trigger, $mail) {
++    $form_name = "trigger_{$trigger}_assign_form";
++    $form_html_id = strtr($form_name, '_', '-');
++
++    $edit = array(
++      'actions_label' => $trigger . "_feedback_send_email_action",
++      'recipients' => $mail,
++      'subject' => $this->randomName(),
++      'message' => '!message',
++    );
++
++    $hash = drupal_hash_base64('feedback_send_email_action');
++    $this->drupalPost("admin/config/system/actions/configure/$hash", $edit, t('Save'));
++    $this->assertText(t('The action has been successfully saved.'));
++
++    // Now we have to find out the action ID of what we created.
++    $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback AND label = :label', array(':callback' => 'feedback_send_email_action', ':label' => $edit['actions_label']))->fetchField();
++
++    $edit = array('aid' => drupal_hash_base64($aid));
++    $this->drupalPost('admin/structure/trigger/feedback', $edit, t('Assign'), array(), array(), $form_html_id);
++  }
++
++
++  /**
++   * Asserts correct token replacement for the given trigger and account.
++   *
++   * @param $account
++   *   The user account which triggered the action.
++   * @param $email_depth
++   *   Number of emails to scan, starting with most recent.
++   */
++  function assertFeedbackEmailTokenReplacement($mail, $message, $email_depth = 1) {
++    $this->verboseEmail($email_depth);
++    $this->assertMailString('body', $message, $email_depth);
++    $this->assertMail('to', $mail, t('Mail sent to correct destination'));
++  }
++}
+\ No newline at end of file

+ 30 - 0
sites/all/modules/contrib/users/feedback/feedback-entry.tpl.php

@@ -0,0 +1,30 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Default theme implementation to present a feedback entry.
+ *
+ * @see template_preprocess_feedback_entry()
+ */
+?>
+<div class="feedback-entry"<?php print $attributes; ?>>
+  <div class="field">
+    <div class="field-label"><?php print t('Location'); ?>:&nbsp;</div>
+    <?php print $location; ?>
+  </div>
+  <div class="field">
+    <div class="field-label"><?php print t('Date'); ?>:&nbsp;</div>
+    <?php print $date; ?>
+  </div>
+  <div class="field">
+    <div class="field-label"><?php print t('User'); ?>:&nbsp;</div>
+    <?php print $account; ?>
+  </div>
+  <div class="field">
+    <div class="field-label"><?php print t('Message'); ?>:&nbsp;</div>
+    <?php print render($message); ?>
+  </div>
+
+  <?php print render($content); ?>
+</div>

+ 13 - 0
sites/all/modules/contrib/users/feedback/feedback-form-display.tpl.php

@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Default theme implementation to present the feedback form.
+ *
+ * @see template_preprocess_feedback_form_display()
+ */
+?>
+<div id="block-feedback-form">
+  <h2><span class="feedback-link"><?php print $title; ?></span></h2>
+  <div class="content"><?php print drupal_render($content); ?></div>
+</div>

+ 412 - 0
sites/all/modules/contrib/users/feedback/feedback.admin.inc

@@ -0,0 +1,412 @@
+<?php
+
+/**
+ * @file
+ * Administrative functionality for Feedback module.
+ */
+
+/**
+ * Build a (sortable) form containing a checkbox for each feedback entry.
+ */
+function feedback_admin_view_form($form, &$form_state) {
+  $form['#attached']['js'][] = drupal_get_path('module', 'feedback') . '/feedback.admin.js';
+
+  $status_headings = array(
+    FEEDBACK_OPEN => t('Open feedback messages'),
+    FEEDBACK_PROCESSED => t('Processed feedback messages'),
+  );
+  $form['#feedback_header'] = array(
+    array(),
+    array('data' => t('Location'), 'field' => 'f.location_masked', 'sort' => 'asc'),
+    array('data' => t('Date'), 'field' => 'f.timestamp'),
+    array('data' => t('User'), 'field' => 'u.name'),
+    t('Message'),
+    t('Operations'),
+  );
+  // Hack to prevent pager_query() from issuing PHP notices.
+  if (!isset($_GET['page'])) {
+    $_GET['page'] = '';
+  }
+  if (count(explode(',', $_GET['page'])) < 2) {
+    $_GET['page'] .= ',0';
+  }
+
+  $form['feedback-messages'] = array('#tree' => TRUE);
+  $query = db_select('feedback', 'f')->extend('PagerDefault')->extend('TableSort');
+  $query->join('users', 'u', 'f.uid = u.uid');
+  $query->fields('f')
+    ->limit(50);
+  foreach (array(FEEDBACK_OPEN, FEEDBACK_PROCESSED) as $status) {
+    $status_query = clone $query;
+    $fids = $status_query->element($status)
+      ->condition('f.status', $status)
+      ->execute()->fetchCol();
+    $form['feedback-messages'][$status] = array(
+      '#type' => 'fieldset',
+      '#title' => $status_headings[$status],
+      '#collapsible' => TRUE,
+      '#collapsed' => $status,
+      '#attributes' => array('class' => array('feedback-messages')),
+    );
+    if (!empty($fids)) {
+      $entries = feedback_load_multiple($fids);
+      foreach ($entries as $fid => $entry) {
+        $form['feedback-messages'][$status][$fid] = array(
+          '#type' => 'checkbox',
+          '#return_value' => FEEDBACK_PROCESSED,
+          '#default_value' => FALSE,
+        );
+        $form['feedback-messages'][$status][$fid]['location'] = array('#markup' => l(truncate_utf8($entry->location, 32, FALSE, TRUE), $entry->url));
+        $form['feedback-messages'][$status][$fid]['date'] = array('#markup' => format_date($entry->timestamp, 'small'));
+        $form['feedback-messages'][$status][$fid]['user'] = array('#markup' => check_plain(format_username($entry)));
+        $form['feedback-messages'][$status][$fid]['message'] = feedback_build_content($entry, 'teaser');
+        $form['feedback-messages'][$status][$fid]['operations'] = array(
+          '#theme' => 'links',
+          '#links' => array(
+            'edit' => array(
+              'title' => t('edit'),
+              'href' => "admin/reports/feedback/$fid/edit"
+            ),
+            'delete' => array(
+              'title' => t('delete'),
+              'href' => "admin/reports/feedback/$fid/delete"
+            ),
+          ),
+          '#attributes' => array(),
+        );
+      }
+    }
+  }
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update'),
+    // Hide the submit button, if there are no entries at all.
+    '#access' => !empty($entries),
+  );
+  return $form;
+}
+
+/**
+ * Output a sortable table containing all feedback entries.
+ */
+function theme_feedback_admin_view_form($variables) {
+  $form = $variables['form'];
+  $output = '';
+  foreach (element_children($form['feedback-messages']) as $status) {
+    $item = &$form['feedback-messages'][$status];
+    if (!isset($item['#type']) || $item['#type'] != 'fieldset') {
+      continue;
+    }
+    // Build the table.
+    $rows = array();
+    foreach (element_children($item) as $element_entry) {
+      $entry = &$item[$element_entry];
+      // Render the data first.
+      $rows[] = array(
+        0,
+        drupal_render($entry['location']),
+        drupal_render($entry['date']),
+        drupal_render($entry['user']),
+        drupal_render($entry['message']),
+        drupal_render($entry['operations']),
+      );
+      // Render the checkbox.
+      $rows[count($rows) - 1][0] = drupal_render($entry);
+    }
+    if (empty($rows)) {
+      $rows[] = array(array('data' => t('No feedback entries available.'), 'colspan' => 6));
+    }
+    // Inject the table.
+    $item['messages'] = array(
+      '#markup' => theme('table', array('header' => $form['#feedback_header'], 'rows' => $rows)),
+      '#suffix' => theme('pager', array('element' => $status)),
+      '#weight' => -1,
+    );
+    // Render the fieldset.
+    $output .= drupal_render($item);
+  }
+  // Render internal FAPI and potential extra form elements.
+  $output .= drupal_render_children($form);
+  return $output;
+}
+
+/**
+ * Form submit callback for admin view form.
+ */
+function feedback_admin_view_form_submit($form, &$form_state) {
+  $update = array();
+  // Determine feedback entries to update.
+  foreach ($form_state['values']['feedback-messages'] as $status => $values) {
+    $values = array_filter($values);
+    if (!empty($values)) {
+      $entries = feedback_load_multiple(array_keys($values));
+      foreach ($entries as $fid => $entry) {
+        $entry->status = ($status == FEEDBACK_OPEN ? FEEDBACK_PROCESSED : FEEDBACK_OPEN);
+        feedback_save($entry);
+      }
+    }
+  }
+}
+
+/**
+ * Form builder; Feedback entry edit form.
+ *
+ * @ingroup forms
+ */
+function feedback_entry_form($form, &$form_state, $entry) {
+  $form['#fid'] = $entry->fid;
+
+  $form['location'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Location'),
+    '#required' => TRUE,
+    '#default_value' => $entry->location,
+  );
+
+  $account = user_load($entry->uid);
+  $form['user'] = array(
+    '#title' => t('User'),
+    '#type' => 'item',
+    '#markup' => theme('username', array('account' => $account)),
+  );
+
+  $form['status'] = array(
+    '#title' => t('Processed'),
+    '#type' => 'radios',
+    '#options' => array(
+       FEEDBACK_OPEN => 'Open',
+       FEEDBACK_PROCESSED => 'Processed',
+    ),
+    '#default_value' => $entry->status,
+  );
+
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Message'),
+    '#required' => TRUE,
+    '#wysiwyg' => FALSE,
+    '#default_value' => $entry->message,
+  );
+
+  field_attach_form('feedback', $entry, $form, $form_state);
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+  );
+  $form['actions']['delete'] = array(
+    '#type' => 'submit',
+    '#value' => t('Delete'),
+    '#submit' => array('feedback_entry_form_delete_submit'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submit callback for entry edit form.
+ *
+ * @todo Duplicates feedback_form_submit(). Add all default entity properties
+ *   as #type 'value' to feedback_form() and merge the two submit handlers.
+ */
+function feedback_entry_form_submit(&$form, &$form_state) {
+  $entry = feedback_load($form['#fid']);
+  entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
+  $entry->message = $form_state['values']['message'];
+  $entry->location = $form_state['values']['location'];
+  $entry->location_masked = feedback_mask_path($entry->location);
+  $entry->url = url($entry->location, array('absolute' => TRUE));
+  $entry->status = $form_state['values']['status'];
+  feedback_save($entry);
+  drupal_set_message(t('The entry has been updated.'));
+}
+
+/**
+ * Button submit function: handle the 'Delete' button on the feedback entry edit form.
+ */
+function feedback_entry_form_delete_submit($form, &$form_state) {
+  $destination = array();
+  if (isset($_GET['destination'])) {
+    $destination = drupal_get_destination();
+    unset($_GET['destination']);
+  }
+  $fid = $form['#fid'];
+  $form_state['redirect'] = array('admin/reports/feedback/' . $fid . '/delete', array('query' => $destination));
+}
+
+/**
+ * Form builder; The general feedback settings form.
+ *
+ * @ingroup forms
+ */
+function feedback_admin_settings_form($form, &$form_state) {
+  $form['feedback_excluded_paths'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Paths to exclude from feedback display'),
+    '#default_value' => variable_get('feedback_excluded_paths', 'admin/reports/feedback'),
+    '#description' => t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", array('%blog' => 'blog', '%blog-wildcard' => 'blog/*', '%front' => '<front>')),
+  );
+  return system_settings_form($form);
+}
+
+/**
+ * Generate a render array for viewing a feedback entry.
+ *
+ * @param $entry
+ *   A feedback entry object.
+ * @param $view_mode
+ *   View mode, e.g. 'full', 'teaser'...
+ * @param $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ *
+ * @return
+ *   An array as expected by drupal_render().
+ *
+ * @todo This is an API function; move into feedback.module.
+ */
+function feedback_view($entry, $view_mode = 'full', $langcode = NULL) {
+  if (!isset($langcode)) {
+    $langcode = $GLOBALS['language_content']->language;
+  }
+
+  // Populate $entry->content with a render() array.
+  feedback_build_content($entry, $view_mode, $langcode);
+
+  $build = $entry->content;
+  unset($entry->content);
+
+  $build += array(
+    '#theme' => 'feedback_entry',
+    '#feedback' => $entry,
+    '#view_mode' => $view_mode,
+    '#language' => $langcode,
+  );
+
+  // Allow modules to modify the structured entry.
+  $type = 'feedback';
+  drupal_alter(array('feedback_view', 'entity_view'), $build, $type);
+
+  return $build;
+}
+
+/**
+ * Builds a structured array representing the feedback entry's content.
+ *
+ * @param $entry
+ *   A feedback entry object.
+ * @param $view_mode
+ *   View mode, e.g. 'full', 'teaser'...
+ * @param $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ *
+ * @todo This is an API function; move into feedback.module.
+ */
+function feedback_build_content($entry, $view_mode = 'full', $langcode = NULL) {
+  if (!isset($langcode)) {
+    $langcode = $GLOBALS['language_content']->language;
+  }
+  // Remove previously built content, if exists.
+  $entry->content = array();
+
+  // Allow modules to change the view mode.
+  $context = array(
+    'entity_type' => 'feedback',
+    'entity' => $entry,
+    'langcode' => $langcode,
+  );
+  drupal_alter('entity_view_mode', $view_mode, $context);
+
+  $entry->content['message'] = array(
+    '#markup' => check_plain($entry->message),
+  );
+  if (!empty($entry->useragent)) {
+    $entry->content['browser'] = array(
+      '#theme' => 'container',
+      '#attributes' => array('class' => array('browserinfo', 'description')),
+    );
+    if (module_exists('browscap')) {
+      $browserinfo = browscap_get_browser($entry->useragent);
+      // Browscap returns useragent but not always parent info.
+      $browser = (isset($browserinfo['parent']) ? $browserinfo['parent'] . ' / ' . $browserinfo['platform'] : $browserinfo['useragent']);
+      $entry->content['browser']['#markup'] = check_plain($browser);
+    }
+    else {
+      $entry->content['browser']['#markup'] = check_plain($entry->useragent);
+    }
+  }
+
+  // Build fields content.
+  field_attach_prepare_view('feedback', array($entry->fid => $entry), $view_mode, $langcode);
+  entity_prepare_view('feedback', array($entry->fid => $entry), $langcode);
+  $entry->content += field_attach_view('feedback', $entry, $view_mode, $langcode);
+
+  $entry->content['links'] = array(
+    '#theme' => 'links__feedback',
+    '#pre_render' => array('drupal_pre_render_links'),
+    '#attributes' => array('class' => array('links', 'inline')),
+  );
+  $uri = entity_uri('feedback', $entry);
+  if ($uri['path'] != $_GET['q'] && arg(0) != 'admin') {
+    $entry->content['links']['#links']['view'] = array('title' => t('view'), 'href' => $uri['path']);
+  }
+
+  // Allow modules to make their own additions to the entry.
+  module_invoke_all('feedback_view', $entry, $view_mode, $langcode);
+  module_invoke_all('entity_view', $entry, 'feedback', $view_mode, $langcode);
+}
+
+/**
+ * Process variables for feedback-entry.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $entry
+ *
+ * @see feedback-entry.tpl.php
+ */
+function template_preprocess_feedback_entry(&$variables) {
+  foreach (element_children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+  $entry = $variables['elements']['#feedback'];
+
+  // Preprocess fields.
+  field_attach_preprocess('feedback', $entry, $variables['elements'], $variables);
+
+  $variables['location'] = l($entry->location, $entry->url);
+  $variables['date'] = format_date($entry->timestamp, 'small');
+  $variables['account'] = check_plain(format_username($entry));
+}
+
+/**
+ * Form constructor for the feedback delete confirmation form.
+ *
+ * @param $entry
+ *   The feedback entry to delete.
+ *
+ * @see feedback_delete_confirm_submit()
+ * @ingroup forms
+ */
+function feedback_delete_confirm($form, &$form_state, $entry) {
+  $form['fid'] = array('#type' => 'value', '#value' => $entry->fid);
+  $output = confirm_form($form,
+    t('Are you sure you want to delete the feedback entry?'),
+    'admin/reports/feedback',
+    NULL,
+    t('Delete'));
+  return $output;
+}
+
+/**
+ * Process feedback_delete_confirm() form submissions.
+ */
+function feedback_delete_confirm_submit($form, &$form_state) {
+  feedback_delete($form_state['values']['fid']);
+  drupal_set_message(t('The feedback entry was deleted.'));
+
+  $form_state['redirect'] = 'admin/reports/feedback';
+}

+ 16 - 0
sites/all/modules/contrib/users/feedback/feedback.admin.js

@@ -0,0 +1,16 @@
+(function ($) {
+
+/**
+ * Attach auto-submit to admin view form.
+ */
+Drupal.behaviors.feedbackAdminForm = {
+  attach: function (context) {
+    $('#feedback-admin-view-form', context).once('feedback', function () {
+      $(this).find('fieldset.feedback-messages :input[type="checkbox"]').click(function () {
+        this.form.submit();
+      });
+    });
+  }
+};
+
+})(jQuery);

+ 133 - 0
sites/all/modules/contrib/users/feedback/feedback.api.php

@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Feedback module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Act on an array of feedback entry objects when loaded from the database.
+ *
+ * @param $entries
+ *   An array of feedback entry objects, indexed by fid.
+ */
+function hook_feedback_load($entries) {
+  $result = db_query('SELECT * FROM {my_table} WHERE fid IN (:fids)', array(':fids' => array_keys($entries)));
+  foreach ($result as $record) {
+    $entries[$record->fid]->foo = $result->foo;
+  }
+}
+
+/**
+ * Act on a feedback entry before it is saved.
+ *
+ * Modules implementing this hook can act on the feedback entry object before it
+ * is inserted or updated.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ */
+function hook_feedback_presave($entry) {
+  $entry->foo = 'bar';
+}
+
+/**
+ * Respond to creation of a new feedback entry.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ *
+ * @see hook_feedback_update()
+ */
+function hook_feedback_insert($entry) {
+  db_insert('mytable')
+    ->fields(array(
+      'fid' => $entry->fid,
+      'extra' => $entry->extra,
+    ))
+    ->execute();
+}
+
+/**
+ * Respond to updates to a feedback entry.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ *
+ * @see hook_feedback_insert()
+ */
+function hook_feedback_update($entry) {
+  db_update('mytable')
+    ->fields(array('extra' => $entry->extra))
+    ->condition('fid', $entry->fid)
+    ->execute();
+}
+
+/**
+ * Respond to deletion of a feedback entry.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ *
+ * @see feedback_delete_multiple()
+ */
+function hook_feedback_delete($entry) {
+  db_delete('mytable')
+    ->condition('fid', $entry->fid)
+    ->execute();
+}
+
+/**
+ * The feedback entry is being displayed.
+ *
+ * The module should format its custom additions for display and add them to the
+ * $entry->content array.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ * @param $view_mode
+ *   View mode, e.g. 'full'.
+ * @param $langcode
+ *   The language code used for rendering.
+ *
+ * @see hook_feedback_view_alter()
+ * @see hook_entity_view()
+ */
+function hook_feedback_view($entry, $view_mode, $langcode) {
+  $entry->content['foo'] = array(
+    '#markup' => t('Bar'),
+  );
+}
+
+/**
+ * The feedback entry was built; the module may modify the structured content.
+ *
+ * This hook is called after the content has been assembled in a structured
+ * array and may be used for doing processing which requires that the complete
+ * content structure has been built.
+ *
+ * @param $build
+ *   A renderable array representing the feedback entry.
+ *
+ * @see feedback_view()
+ * @see hook_entity_view_alter()
+ */
+function hook_feedback_view_alter(&$build) {
+  // Check for the existence of a field added by another module.
+  if (isset($build['an_additional_field'])) {
+    // Change its weight.
+    $build['an_additional_field']['#weight'] = -10;
+  }
+
+  // Add a #post_render callback to act on the rendered HTML of the entry.
+  $build['#post_render'][] = 'my_module_feedback_post_render';
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */

+ 23 - 0
sites/all/modules/contrib/users/feedback/feedback.controller.inc

@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Entity controller class.
+ */
+
+/**
+ * Controller class for feedback entries.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for feedback objects.
+ */
+class FeedbackController extends DrupalDefaultEntityController {
+
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    $query = parent::buildQuery($ids, $conditions, $revision_id);
+    // Specify an additional field from the user table.
+    $query->innerJoin('users', 'u', 'base.uid = u.uid');
+    $query->fields('u', array('name'));
+    return $query;
+  }
+}

+ 72 - 0
sites/all/modules/contrib/users/feedback/feedback.css

@@ -0,0 +1,72 @@
+
+/**
+ * Styles for the feedback form.
+ */
+/* Reset commonly set styles */
+#block-feedback-form,
+#block-feedback-form .feedback-link,
+#block-feedback-form .feedback-link *,
+#block-feedback-form .content,
+#block-feedback-form form {
+  float: none;
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font-weight: normal;
+  color: inherit;
+}
+
+#block-feedback-form {
+  display: none;
+  position: fixed;
+  bottom: 60px;
+  right: 20px;
+  overflow: hidden;
+  z-index: 10;
+}
+/* IE6 seems to be unable to handle fixed */
+* html #block-feedback-form {
+  position: absolute;
+}
+#block-feedback-form .feedback-link {
+  padding: 0.3em 0;
+  text-align: right;
+  font-size: 12px;
+}
+#block-feedback-form .feedback-link * {
+  display: inline;
+  font-size: 12px;
+}
+#block-feedback-form form {
+  border: 1px solid #ccc;
+  padding: 6px;
+  background-color: #fff;
+  opacity: 0.9;
+  max-width: 300px;
+}
+/* IE6 doesn't support max-width. */
+* html #block-feedback-form form {
+  width: 300px;
+}
+#block-feedback-form .feedback-help {
+  margin: 0 0 0.5em;
+  font-size: 10px;
+  line-height: normal;
+}
+#block-feedback-form .feedback-message {
+  height: 10ex;
+}
+#block-feedback-form .ajax-progress .throbber {
+  background: transparent url(images/throbber.gif) no-repeat left center;
+  height: 16px;
+  width: 16px;
+}
+
+/**
+ * Styles for existing feedback messages.
+ */
+#block-feedback-form .feedback-submitted {
+  margin-top: 0.2em;
+  font-size: 10px;
+}

+ 17 - 0
sites/all/modules/contrib/users/feedback/feedback.info

@@ -0,0 +1,17 @@
+name = Feedback
+description = Allows site visitors and users to report issues about this site.
+package = Development
+core = 7.x
+configure = admin/config/user-interface/feedback
+files[] = feedback.controller.inc
+files[] = views/feedback_handler_field_feedback_link.inc
+files[] = views/feedback_handler_field_feedback_link_delete.inc
+files[] = views/feedback_handler_field_feedback_link_edit.inc
+files[] = tests/feedback.test
+
+; Information added by drupal.org packaging script on 2013-03-04
+version = "7.x-2.x-dev"
+core = "7.x"
+project = "feedback"
+datestamp = "1362358407"
+

+ 116 - 0
sites/all/modules/contrib/users/feedback/feedback.install

@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @file
+ * Installation functions for Feedback module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function feedback_schema() {
+  $schema['feedback'] = array(
+    'description' => 'Stores all feedback messages.',
+    'fields' => array(
+      'fid' => array(
+        'description' => 'Feedback message ID.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'uid' => array(
+        'description' => 'User ID of the feedback message author.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'status' => array(
+        'description' => 'Feedback status (0 = new, 1 = processed).',
+        'type' => 'int',
+        'size' => 'tiny',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'message' => array(
+        'description' => 'The feedback message.',
+        'type' => 'text',
+        'size' => 'big',
+        'not null' => TRUE,
+      ),
+      'location' => array(
+        'description' => 'System path of the originating page.',
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'location_masked' => array(
+        'description' => 'Untranslated system path of the originating page.',
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'url' => array(
+        'description' => 'Absolute URL of the originating page.',
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'useragent' => array(
+        'description' => 'User agent of the feedback message author.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      // @todo Rename to created. Also add 'changed'.
+      'timestamp' => array(
+        'description' => 'UNIX timestamp of the feedback message creation date.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('fid'),
+    'indexes' => array(
+      'location' => array(array('location', 32)),
+      'location_masked' => array(array('location_masked', 32)),
+    ),
+  );
+  return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function feedback_uninstall() {
+  db_delete('variable')
+    ->condition('name', 'feedback_%', 'LIKE')
+    ->execute();
+}
+
+/**
+ * Change fid into type serial field.
+ */
+function feedback_update_6100() {
+  db_drop_primary_key('feedback');
+  db_change_field('feedback', 'fid', 'fid', array(
+    'type' => 'serial',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+  ), array(
+    'primary key' => array('fid'),
+  ));
+}
+
+/**
+ * Add column for absolute URL.
+ */
+function feedback_update_6101() {
+  db_add_field('feedback', 'url', array(
+    'type' => 'text',
+    'not null' => TRUE,
+  ));
+  // Set 'url' to the value of 'location' for all existing entries.
+  db_update('feedback')
+    ->expression('url', 'location')
+    ->execute();
+}
+

+ 59 - 0
sites/all/modules/contrib/users/feedback/feedback.js

@@ -0,0 +1,59 @@
+(function ($) {
+
+/**
+ * Attach collapse behavior to the feedback form block.
+ */
+Drupal.behaviors.feedbackForm = {
+  attach: function (context) {
+    $('#block-feedback-form', context).once('feedback', function () {
+      var $block = $(this);
+      $block.find('span.feedback-link')
+        .prepend('<span id="feedback-form-toggle">[ + ]</span> ')
+        .css('cursor', 'pointer')
+        .toggle(function () {
+            Drupal.feedbackFormToggle($block, false);
+          },
+          function() {
+            Drupal.feedbackFormToggle($block, true);
+          }
+        );
+      $block.find('form').hide();
+      $block.show();
+    });
+  }
+};
+
+/**
+ * Re-collapse the feedback form after every successful form submission.
+ */
+Drupal.behaviors.feedbackFormSubmit = {
+  attach: function (context) {
+    var $context = $(context);
+    if (!$context.is('#feedback-status-message')) {
+      return;
+    }
+    // Collapse the form.
+    $('#block-feedback-form .feedback-link').click();
+    // Blend out and remove status message.
+    window.setTimeout(function () {
+      $context.fadeOut('slow', function () {
+        $context.remove();
+      });
+    }, 3000);
+  }
+};
+
+/**
+ * Collapse or uncollapse the feedback form block.
+ */
+Drupal.feedbackFormToggle = function ($block, enable) {
+  $block.find('form').slideToggle('medium');
+  if (enable) {
+    $('#feedback-form-toggle', $block).html('[ + ]');
+  }
+  else {
+    $('#feedback-form-toggle', $block).html('[ &minus; ]');
+  }
+};
+
+})(jQuery);

+ 822 - 0
sites/all/modules/contrib/users/feedback/feedback.module

@@ -0,0 +1,822 @@
+<?php
+
+/**
+ * @file
+ * Allows site visitors and users to report issues about this site.
+ */
+
+/**
+ * Open state (unprocessed) for feedback entries.
+ */
+define('FEEDBACK_OPEN', 0);
+
+/**
+ * Processed state for feedback entries.
+ */
+define('FEEDBACK_PROCESSED', 1);
+
+/**
+ * Implements hook_theme().
+ */
+function feedback_theme() {
+  return array(
+    'feedback_admin_view_form' => array(
+      'render element' => 'form',
+    ),
+    'feedback_entry' => array(
+      'render element' => 'elements',
+      'template' => 'feedback-entry',
+      'file' => 'feedback.admin.inc',
+    ),
+    'feedback_form_display' => array(
+      'template' => 'feedback-form-display',
+      'variables' => array('title' => NULL, 'content' => NULL),
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function feedback_entity_info() {
+  $return = array(
+    'feedback' => array(
+      'label' => t('Feedback'),
+      'controller class' => 'FeedbackController',
+      'base table' => 'feedback',
+      'uri callback' => 'feedback_uri',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'fid',
+      ),
+      'bundles' => array(
+        'feedback' => array(
+          'label' => t('Feedback'),
+          'admin' => array(
+            'path' => 'admin/config/user-interface/feedback',
+            'access arguments' => array('administer feedback'),
+          ),
+        ),
+      ),
+      'view modes' => array(
+        'full' => array(
+          'label' => t('Full feedback entry'),
+          'custom settings' => FALSE,
+        ),
+        'teaser' => array(
+          'label' => t('Teaser'),
+          'custom settings' => FALSE,
+        ),
+        'widget' => array(
+          'label' => t('Widget'),
+          'custom settings' => FALSE,
+        ),
+      ),
+      // Disable Metatags (metatag) module's entity form additions.
+      'metatags' => FALSE,
+    ),
+  );
+
+  return $return;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function feedback_field_extra_fields() {
+  $extras['feedback']['feedback']['form']['help'] = array(
+    'label' => t('Help'),
+    'description' => t('Feedback submission guidelines'),
+    'weight' => -10,
+  );
+  $extras['feedback']['feedback']['form']['messages'] = array(
+    'label' => t('Entries'),
+    'description' => t('Existing feedback entries for the current page'),
+    'weight' => -5,
+  );
+  $extras['feedback']['feedback']['form']['message'] = array(
+    'label' => t('Message'),
+    'description' => t('Feedback message form text field'),
+    'weight' => 0,
+  );
+
+  $extras['feedback']['feedback']['display']['location'] = array(
+    'label' => t('Location'),
+    'description' => t('The URL of the page the message was submitted on'),
+    'weight' => -15,
+  );
+  $extras['feedback']['feedback']['display']['date'] = array(
+    'label' => t('Date'),
+    'description' => t('The submission date of the message'),
+    'weight' => -10,
+  );
+  $extras['feedback']['feedback']['display']['user'] = array(
+    'label' => t('User'),
+    'description' => t('The name of the user who submitted the message'),
+    'weight' => -5,
+  );
+  $extras['feedback']['feedback']['display']['message'] = array(
+    'label' => t('Message'),
+    'description' => t('The main feedback message'),
+    'weight' => 0,
+  );
+  return $extras;
+}
+
+/**
+ * Entity uri callback.
+ */
+function feedback_uri($entry) {
+  return array(
+    'path' => 'admin/reports/feedback/' . $entry->fid,
+  );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function feedback_permission() {
+  return array(
+    'access feedback form' => array(
+      'title' => t('Access feedback form'),
+      'description' => t('Submit feedback messages.'),
+    ),
+    'view feedback messages' => array(
+      'title' => t('View feedback messages'),
+      'description' => t('View, process, and delete submitted feedback messages.'),
+    ),
+    'administer feedback' => array(
+      'title' => t('Administer feedback settings'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function feedback_menu() {
+  $items['admin/reports/feedback'] = array(
+    'title' => 'Feedback messages',
+    'description' => 'View feedback messages.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_view_form'),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback'] = array(
+    'title' => 'Feedback entry',
+    'page callback' => 'feedback_view',
+    'page arguments' => array(3),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['admin/reports/feedback/%feedback/edit'] = array(
+    'title' => 'Edit',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_entry_form', 3),
+    'access arguments' => array('view feedback messages'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback/delete'] = array(
+    'title' => 'Delete feedback entry',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_delete_confirm', 3),
+    'access arguments' => array('view feedback messages'),
+    'type' => MENU_CALLBACK,
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback'] = array(
+    'title' => 'Feedback',
+    'description' => 'Administer feedback settings.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_settings_form'),
+    'access arguments' => array('administer feedback'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback/settings'] = array(
+    'title' => 'Settings',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_page_build().
+ */
+function feedback_page_build(&$page) {
+  if (user_access('access feedback form') && !feedback_match_path(variable_get('feedback_excluded_paths', 'admin/reports/feedback'))) {
+    $page['page_bottom']['feedback'] = array(
+      '#theme' => 'feedback_form_display',
+      '#title' => t('Feedback'),
+      '#content' => drupal_get_form('feedback_form'),
+    );
+    $path = drupal_get_path('module', 'feedback');
+    $page['page_bottom']['feedback']['#attached']['css'][] = $path . '/feedback.css';
+    $page['page_bottom']['feedback']['#attached']['js'][] = $path . '/feedback.js';
+  }
+}
+
+/**
+ * Check if the current path matches any pattern in a set of patterns.
+ *
+ * @param $patterns
+ *   String containing a set of patterns separated by \n, \r or \r\n.
+ *
+ * @return
+ *   Boolean value: TRUE if the current path or alias matches a pattern.
+ */
+function feedback_match_path($patterns) {
+  // Convert path to lowercase. This allows comparison of the same path
+  // with different case. Ex: /Page, /page, /PAGE.
+  $patterns = drupal_strtolower($patterns);
+
+  // Convert the current path to lowercase.
+  $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
+
+  // Compare the lowercase internal and lowercase path alias (if any).
+  $page_match = drupal_match_path($path, $patterns);
+  if ($path != $_GET['q']) {
+    $page_match = $page_match || drupal_match_path($_GET['q'], $patterns);
+  }
+
+  return $page_match;
+}
+
+/**
+ * Form constructor for the feedback form.
+ *
+ * @see feedback_form_submit()
+ * @ingroup forms
+ */
+function feedback_form($form, &$form_state) {
+  $form['#attributes']['class'] = array('feedback-form');
+
+  // Store the path on which this form is displayed.
+  if (!isset($form_state['inline']['location'])) {
+    $form_state['inline']['location'] = $_GET['q'];
+  }
+  $form['location'] = array(
+    '#type' => 'value',
+    '#value' => $form_state['inline']['location'],
+  );
+
+  $form['help'] = array(
+    '#prefix' => '<div class="feedback-help">',
+    '#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.'),
+    '#suffix' => '</div>',
+  );
+  if (user_access('view feedback messages')) {
+    if (arg(0) != 'node') {
+      $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location_masked' => feedback_mask_path($_GET['q'])));
+    }
+    else {
+      $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location' => $_GET['q']));
+    }
+    if ($feedbacks) {
+      form_load_include($form_state, 'inc', 'feedback', 'feedback.admin');
+      $form['messages'] = array(
+        '#prefix' => '<div class="feedback-messages">',
+        '#suffix' => '</div>',
+      );
+      foreach ($feedbacks as $fid => $feedback) {
+        $form['messages'][$fid]['#feedback'] = $feedback;
+        $form['messages'][$fid]['submitted'] = array('#markup' => t('@feedback-author !feedback-date:', array('@feedback-author' => format_username($feedback), '!feedback-date' => format_date($feedback->timestamp, 'small'))));
+        $form['messages'][$fid]['submitted']['#prefix'] = '<div class="feedback-submitted">';
+        $form['messages'][$fid]['submitted']['#suffix'] = '</div>';
+        $form['messages'][$fid]['body'] = feedback_build_content($feedback, 'widget');
+        $form['messages'][$fid]['body']['#prefix'] = '<div class="feedback-body">';
+        $form['messages'][$fid]['body']['#suffix'] = '</div>';
+      }
+    }
+  }
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#attributes' => array('class' => array('feedback-message')),
+    '#cols' => 20,
+    '#title' => t('Message'),
+    '#required' => TRUE,
+    '#wysiwyg' => FALSE,
+  );
+
+  $entry = new stdClass();
+  field_attach_form('feedback', $entry, $form, $form_state);
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+    // Without clearfix, the AJAX throbber wraps in an ugly way.
+    // @todo Patch #type actions in core?
+    '#attributes' => array('class' => array('clearfix')),
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Send feedback'),
+    '#id' => 'feedback-submit',
+    '#ajax' => array(
+      'wrapper' => 'feedback-form',
+      'callback' => 'feedback_form_ajax_callback',
+      'progress' => array(
+        'type' => 'throbber',
+        'message' => '',
+      ),
+    ),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submission handler for feedback_form().
+ */
+function feedback_form_submit($form, &$form_state) {
+  $entry = new stdClass();
+  entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
+  $entry->message = $form_state['values']['message'];
+  $entry->location = $form_state['values']['location'];
+  feedback_save($entry);
+
+  drupal_set_message(t('Thanks for your feedback!'));
+}
+
+/**
+ * AJAX callback for feedback_form() submissions.
+ */
+function feedback_form_ajax_callback($form, &$form_state) {
+  // If there was a form validation error, re-render the entire form.
+  if (!$form_state['executed']) {
+    return $form;
+  }
+
+  // Otherwise, return a fresh copy of the form, so the user may post additional
+  // feedback.
+  // Reset the static cache of drupal_html_id().
+  // @see drupal_process_form()
+  // @see drupal_html_id()
+  $seen_ids = &drupal_static('drupal_html_id');
+  $seen_ids = array();
+
+  // Prevent the form from being processed again.
+  // @see drupal_build_form()
+  list($form, $new_form_state) = ajax_get_form();
+  $new_form_state['input'] = array();
+  drupal_process_form($form['#form_id'], $form, $new_form_state);
+
+  // Return AJAX commands in order to output the special success message.
+  // @see ajax_deliver()
+  $build = array('#type' => 'ajax');
+  $html = drupal_render($form);
+  $build['#commands'][] = ajax_command_insert(NULL, $html);
+
+  // A successful form submission normally means that there were no errors, so
+  // we only render status messages.
+  $messages = drupal_get_messages();
+  $messages += array('status' => array());
+  $messages = implode('<br />', $messages['status']);
+  $html = '<div id="feedback-status-message">' . $messages . '</div>';
+  $build['#commands'][] = ajax_command_append('#block-feedback-form', $html);
+  return $build;
+}
+
+/**
+ * Loads a feedback entry from the database.
+ *
+ * @param $fid
+ *   Integer specifying the feedback ID to load.
+ *
+ * @return
+ *   A loaded feedback entry object upon successful load, or FALSE if the entry
+ *   cannot be loaded.
+ *
+ * @see feedback_load_multiple()
+ */
+function feedback_load($fid) {
+  $entries = feedback_load_multiple(array($fid));
+  return (isset($entries[$fid]) ? $entries[$fid] : FALSE);
+}
+
+/**
+ * Loads feedback entries from the database.
+ *
+ * @param $fids
+ *   An array of feedback entry IDs.
+ * @param $conditions
+ *   An associative array of conditions on the {feedback} table, where the keys
+ *   are the database fields and the values are the values those fields
+ *   must have.
+ *
+ * @return
+ *   An array of feedback entry objects indexed by fid.
+ *
+ * @see hook_feedback_load()
+ * @see feedback_load()
+ * @see entity_load()
+ * @see EntityFieldQuery
+ */
+function feedback_load_multiple($fids = array(), $conditions = array()) {
+  return entity_load('feedback', $fids, $conditions);
+}
+
+/**
+ * Saves changes to a feedback entry or adds a new feedback entry.
+ *
+ * @param $entry
+ *   The feedback entry object to modify or add. If $entry->fid is omitted, a
+ *   new entry will be added.
+ *
+ * @see hook_feedback_insert()
+ * @see hook_feedback_update()
+ */
+function feedback_save($entry) {
+  global $user;
+
+  // Load the stored entity, if any.
+  if (!empty($entry->fid) && !isset($entry->original)) {
+    $entry->original = entity_load_unchanged('feedback', $entry->fid);
+  }
+
+  field_attach_presave('feedback', $entry);
+
+  // Allow modules to alter the feedback entry before saving.
+  module_invoke_all('feedback_presave', $entry);
+  module_invoke_all('entity_presave', $entry, 'feedback');
+
+  if (empty($entry->fid)) {
+    $entry->message = trim($entry->message);
+
+    $defaults = array(
+      'uid' => $user->uid,
+      'location_masked' => feedback_mask_path($entry->location),
+      'url' => url($entry->location, array('absolute' => TRUE)),
+      'timestamp' => REQUEST_TIME,
+      'useragent' => $_SERVER['HTTP_USER_AGENT'],
+    );
+    foreach ($defaults as $key => $default) {
+      if (!isset($entry->$key)) {
+        $entry->$key = $default;
+      }
+    }
+
+    $status = drupal_write_record('feedback', $entry);
+    field_attach_insert('feedback', $entry);
+    module_invoke_all('feedback_insert', $entry);
+    module_invoke_all('entity_insert', $entry, 'feedback');
+  }
+  else {
+    $status = drupal_write_record('feedback', $entry, 'fid');
+
+    field_attach_update('feedback', $entry);
+    module_invoke_all('feedback_update', $entry);
+    module_invoke_all('entity_update', $entry, 'feedback');
+  }
+  unset($entry->original);
+
+  return $status;
+}
+
+/**
+ * Deletes a feedback entry.
+ *
+ * @param $fid
+ *   A feedback entry ID.
+ */
+function feedback_delete($fid) {
+  feedback_delete_multiple(array($fid));
+}
+
+/**
+ * Deletes multiple feedback entries.
+ *
+ * @param $fids
+ *   An array of feedback entry IDs.
+ */
+function feedback_delete_multiple($fids) {
+  if (!empty($fids)) {
+    $entries = feedback_load_multiple($fids);
+    foreach ($entries as $fid => $entry) {
+      field_attach_delete('feedback', $entry);
+      module_invoke_all('feedback_delete', $entry);
+      module_invoke_all('entity_delete', $entry, 'feedback');
+    }
+    db_delete('feedback')
+      ->condition('fid', $fids, 'IN')
+      ->execute();
+  }
+}
+
+/**
+ * 'Mask' a path, i.e. replace all numeric arguments in a path with '%' placeholders.
+ *
+ * Please note that only numeric arguments with a preceding slash will be
+ * replaced.
+ *
+ * @param $path
+ *   An internal Drupal path, f.e. 'user/123/edit'.
+ * @return
+ *   A 'masked' path, for above example 'user/%/edit'.
+ *
+ * @todo Use the untranslated patch of menu_get_item() instead.
+ */
+function feedback_mask_path($path) {
+  return preg_replace('@/\d+@', '/%', $path);
+}
+
+/**
+ * Implements hook_user_cancel().
+ */
+function feedback_user_cancel($edit, $account, $method) {
+  switch ($method) {
+    case 'user_cancel_reassign':
+      db_update('feedback')
+        ->fields(array('uid' => 0))
+        ->condition('uid', $account->uid)
+        ->execute();
+      break;
+  }
+}
+
+/**
+ * Implements hook_user_delete().
+ */
+function feedback_user_delete($account) {
+  $fids = db_query('SELECT fid FROM {feedback} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
+  feedback_delete_multiple($fids);
+}
+
+/**
+ * Implements hook_action_info().
+ */
+function feedback_action_info() {
+  return array(
+    'feedback_process_action' => array(
+      'label' => t('Process entry'),
+      'type' => 'feedback',
+      'configurable' => FALSE,
+      'behavior' => array('changes_property'),
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+    'feedback_open_action' => array(
+      'label' => t('Open entry'),
+      'type' => 'feedback',
+      'configurable' => FALSE,
+      'behavior' => array('changes_property'),
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+    'feedback_delete_action' => array(
+      'label' => t('Delete entry'),
+      'type' => 'feedback',
+      'configurable' => FALSE,
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+    'feedback_send_email_action' => array(
+      'label' => t('Send e-mail of feedback'),
+      'type' => 'feedback',
+      'configurable' => TRUE,
+      'triggers' => array('feedback_insert', 'feedback_update'),
+    ),
+  );
+}
+
+/**
+* Implements hook_trigger_info().
+*/
+function feedback_trigger_info() {
+  return array(
+    'feedback' => array(
+      'feedback_insert' => array(
+        'label' => t('New feedback is added.'),
+      ),
+      'feedback_update' => array(
+        'label' => t('Feedback is marked processed or open.'),
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_feedback_insert().
+ */
+function feedback_feedback_insert($entry) {
+  $aids = trigger_get_assigned_actions('feedback_insert');
+  if (!$aids) {
+    return;
+  }
+
+  $context = array(
+    'group' => 'feedback',
+    'hook' => 'feedback_insert',
+  );
+
+  foreach ($aids as $aid => $info) {
+    actions_do($aid, $entry, $context);
+  }
+}
+
+/**
+ * Implements hook_feedback_update().
+ */
+function feedback_feedback_update($entry) {
+  $aids = trigger_get_assigned_actions('feedback_update');
+  if (!$aids) {
+    return;
+  }
+
+  $context = array(
+    'group' => 'feedback',
+    'hook' => 'feedback_update',
+  );
+
+  foreach ($aids as $aid => $info) {
+    actions_do($aid, $entry, $context);
+  }
+}
+
+/**
+ * Sets the status of an entry to processed.
+ *
+ * @ingroup actions
+ */
+function feedback_process_action($entry, $context) {
+  $entry->status = FEEDBACK_PROCESSED;
+  drupal_write_record('feedback', $entry, 'fid');
+}
+
+/**
+ * Sets the status of an entry to open.
+ *
+ * @ingroup actions
+ */
+function feedback_open_action($entry, $context) {
+  $entry->status = FEEDBACK_OPEN;
+  drupal_write_record('feedback', $entry, 'fid');
+}
+
+/**
+ * Deletes a feedback entry.
+ *
+ * @ingroup actions
+ */
+function feedback_delete_action($entry, $context) {
+  feedback_delete($entry->fid);
+}
+
+/**
+ * Return a form definition so the Feedback send email action can be configured.
+ *
+ * @param $context
+ *   Default values (if we are editing an existing action instance).
+ * @return
+ *   Form definition.
+ */
+function feedback_send_email_action_form($context = array()) {
+  // Set default values for form.
+  $context += array(
+   'recipients' => '',
+   'subject' => '',
+   'message' => '',
+  );
+
+  $form['recipients'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Recipients'),
+    '#default_value' => $context['recipients'],
+    '#description' => t("Example: 'webmaster@example.com' or 'dev@example.com,support@example.com'. To specify multiple recipients, separate each e-mail address with a comma."),
+    '#required' => TRUE,
+  );
+  $form['subject'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Subject'),
+    '#default_value' => $context['subject'],
+    '#maxlength' => '254',
+    '#description' => t('The subject of the message.'),
+    '#required' => TRUE,
+  );
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Message'),
+    '#default_value' => $context['message'],
+    '#cols' => '80',
+    '#rows' => '20',
+    '#description' => t('The message that should be sent. You may include the following variables: !uid, !username, !usermail, !useragent (of the user who gave the feedback), !site_name, !status, !message, !url, !date.'),
+    '#required' => TRUE,
+  );
+  return $form;
+}
+
+/**
+ * Validate the send e-mail action form submission.
+ */
+function feedback_send_email_action_validate($form, &$form_state) {
+  if (empty($form_state['values']['recipients'])) {
+    form_set_error('recipients', t('You must enter one or more recipients.'));
+  }
+  else {
+    $recipients = explode(',', $form_state['values']['recipients']);
+    foreach ($recipients as $recipient) {
+      if (!valid_email_address(trim($recipient))) {
+        form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
+      }
+    }
+  }
+}
+
+/**
+ * Process the send e-mail action form submission.
+ */
+function feedback_send_email_action_submit($form, $form_state) {
+  // Process the HTML form to store configuration. The keyed array that
+  // we return will be serialized to the database.
+  $params = array(
+    'recipients' => $form_state['values']['recipients'],
+    'subject'    => $form_state['values']['subject'],
+    'message'    => $form_state['values']['message'],
+  );
+  return $params;
+}
+
+/**
+ * Implements the feedback send e-mail action.
+ */
+function feedback_send_email_action($entry, $context) {
+  $account = user_load($entry->uid);
+  $from = variable_get('site_name', 'Drupal') . ' <' . variable_get('site_mail', '') . '>';
+  $params = array('entry' => $entry, 'account' => $account, 'context' => $context);
+  // Send the e-mail to the recipients using the site default language.
+  drupal_mail('feedback', 'send_email_action', $context['recipients'], language_default(), $params, $from);
+  watchdog('feedback', 'Feedback information sent to %recipients', array('%recipients' => $context['recipients']));
+}
+
+/**
+ * Implements hook_mail().
+ */
+function feedback_mail($key, &$message, $params) {
+  $language = $message['language'];
+  $entry = $params['entry'];
+  $account = $params['account'];
+  $context = $params['context'];
+  $variables = array(
+    '!site_name' => variable_get('site_name', 'Drupal'),
+    '!uid' => $account->uid,
+    '!username' => $account->name ? $account->name : t('Anonymous'),
+    '!usermail' => $account->mail ? $account->mail : t('unknown'),
+    '!status' => $entry->status ? t('Processed') : t('Open'),
+    '!message' => $entry->message,
+    '!url' => url($entry->location, array('absolute' => TRUE, 'language' => $language)),
+    '!useragent' => $entry->useragent,
+    '!date' => format_date($entry->timestamp, 'small', '', NULL, $language->language),
+  );
+  $subject = strtr($context['subject'], $variables);
+  $body = strtr($context['message'], $variables);
+  $message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
+  $message['body'][] = drupal_html_to_text($body);
+}
+
+
+/**
+ * Implements hook_mollom_form_list().
+ */
+function feedback_mollom_form_list() {
+  $forms['feedback_form'] = array(
+    'title' => t('Feedback form'),
+    'entity' => 'feedback',
+    'bundle' => 'feedback',
+    'entity delete multiple callback' => 'feedback_delete_multiple',
+    'delete form' => 'feedback_delete_confirm',
+    'delete form file' => array(
+      'name' => 'feedback.admin',
+    ),
+    'report access' => array('view feedback messages'),
+  );
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function feedback_mollom_form_info($form_id) {
+  $form_info = array(
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer feedback'),
+    'elements' => array(
+      'message' => t('Message'),
+    ),
+  );
+  mollom_form_info_add_fields($form_info, 'feedback', 'feedback');
+  return $form_info;
+}
+
+/**
+ * Implements hook_views_api();
+ */
+function feedback_views_api() {
+  return array(
+    'api' => 3.0,
+    'path' => drupal_get_path('module', 'feedback') . '/views',
+  );
+}

+ 593 - 0
sites/all/modules/contrib/users/feedback/feedback.module.orig

@@ -0,0 +1,593 @@
+<?php
+
+/**
+ * @file
+ * Allows site visitors and users to report issues about this site.
+ */
+
+/**
+ * Open state (unprocessed) for feedback entries.
+ */
+define('FEEDBACK_OPEN', 0);
+
+/**
+ * Processed state for feedback entries.
+ */
+define('FEEDBACK_PROCESSED', 1);
+
+/**
+ * Implements hook_theme().
+ */
+function feedback_theme() {
+  return array(
+    'feedback_admin_view_form' => array(
+      'render element' => 'form',
+    ),
+    'feedback_entry' => array(
+      'render element' => 'elements',
+      'template' => 'feedback-entry',
+      'file' => 'feedback.admin.inc',
+    ),
+    'feedback_form_display' => array(
+      'template' => 'feedback-form-display',
+      'variables' => array('title' => NULL, 'content' => NULL),
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function feedback_entity_info() {
+  $return = array(
+    'feedback' => array(
+      'label' => t('Feedback'),
+      'controller class' => 'FeedbackController',
+      'base table' => 'feedback',
+      'uri callback' => 'feedback_uri',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'fid',
+      ),
+      'bundles' => array(
+        'feedback' => array(
+          'label' => t('Feedback'),
+          'admin' => array(
+            'path' => 'admin/config/user-interface/feedback',
+            'access arguments' => array('administer feedback'),
+          ),
+        ),
+      ),
+      'view modes' => array(
+        'full' => array(
+          'label' => t('Full feedback entry'),
+          'custom settings' => FALSE,
+        ),
+        'teaser' => array(
+          'label' => t('Teaser'),
+          'custom settings' => FALSE,
+        ),
+        'widget' => array(
+          'label' => t('Widget'),
+          'custom settings' => FALSE,
+        ),
+      ),
+      // Disable Metatags (metatag) module's entity form additions.
+      'metatags' => FALSE,
+    ),
+  );
+
+  return $return;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function feedback_field_extra_fields() {
+  $extras['feedback']['feedback']['form']['help'] = array(
+    'label' => t('Help'),
+    'description' => t('Feedback submission guidelines'),
+    'weight' => -10,
+  );
+  $extras['feedback']['feedback']['form']['messages'] = array(
+    'label' => t('Entries'),
+    'description' => t('Existing feedback entries for the current page'),
+    'weight' => -5,
+  );
+  $extras['feedback']['feedback']['form']['message'] = array(
+    'label' => t('Message'),
+    'description' => t('Feedback message form text field'),
+    'weight' => 0,
+  );
+
+  $extras['feedback']['feedback']['display']['location'] = array(
+    'label' => t('Location'),
+    'description' => t('The URL of the page the message was submitted on'),
+    'weight' => -15,
+  );
+  $extras['feedback']['feedback']['display']['date'] = array(
+    'label' => t('Date'),
+    'description' => t('The submission date of the message'),
+    'weight' => -10,
+  );
+  $extras['feedback']['feedback']['display']['user'] = array(
+    'label' => t('User'),
+    'description' => t('The name of the user who submitted the message'),
+    'weight' => -5,
+  );
+  $extras['feedback']['feedback']['display']['message'] = array(
+    'label' => t('Message'),
+    'description' => t('The main feedback message'),
+    'weight' => 0,
+  );
+  return $extras;
+}
+
+/**
+ * Entity uri callback.
+ */
+function feedback_uri($entry) {
+  return array(
+    'path' => 'admin/reports/feedback/' . $entry->fid,
+  );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function feedback_permission() {
+  return array(
+    'access feedback form' => array(
+      'title' => t('Access feedback form'),
+      'description' => t('Submit feedback messages.'),
+    ),
+    'view feedback messages' => array(
+      'title' => t('View feedback messages'),
+      'description' => t('View, process, and delete submitted feedback messages.'),
+    ),
+    'administer feedback' => array(
+      'title' => t('Administer feedback settings'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function feedback_menu() {
+  $items['admin/reports/feedback'] = array(
+    'title' => 'Feedback messages',
+    'description' => 'View feedback messages.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_view_form'),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback'] = array(
+    'title' => 'Feedback entry',
+    'page callback' => 'feedback_view',
+    'page arguments' => array(3),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['admin/reports/feedback/%feedback/edit'] = array(
+    'title' => 'Edit',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_entry_form', 3),
+    'access arguments' => array('view feedback messages'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/reports/feedback/%feedback/delete'] = array(
+    'title' => 'Delete feedback entry',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_delete_confirm', 3),
+    'access arguments' => array('view feedback messages'),
+    'type' => MENU_CALLBACK,
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback'] = array(
+    'title' => 'Feedback',
+    'description' => 'Administer feedback settings.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_settings_form'),
+    'access arguments' => array('administer feedback'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback/settings'] = array(
+    'title' => 'Settings',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_page_build().
+ */
+function feedback_page_build(&$page) {
+  if (user_access('access feedback form') && !feedback_match_path(variable_get('feedback_excluded_paths', 'admin/reports/feedback'))) {
+    $page['page_bottom']['feedback'] = array(
+      '#theme' => 'feedback_form_display',
+      '#title' => t('Feedback'),
+      '#content' => drupal_get_form('feedback_form'),
+    );
+    $path = drupal_get_path('module', 'feedback');
+    $page['page_bottom']['feedback']['#attached']['css'][] = $path . '/feedback.css';
+    $page['page_bottom']['feedback']['#attached']['js'][] = $path . '/feedback.js';
+  }
+}
+
+/**
+ * Check if the current path matches any pattern in a set of patterns.
+ *
+ * @param $patterns
+ *   String containing a set of patterns separated by \n, \r or \r\n.
+ *
+ * @return
+ *   Boolean value: TRUE if the current path or alias matches a pattern.
+ */
+function feedback_match_path($patterns) {
+  // Convert path to lowercase. This allows comparison of the same path
+  // with different case. Ex: /Page, /page, /PAGE.
+  $patterns = drupal_strtolower($patterns);
+
+  // Convert the current path to lowercase.
+  $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
+
+  // Compare the lowercase internal and lowercase path alias (if any).
+  $page_match = drupal_match_path($path, $patterns);
+  if ($path != $_GET['q']) {
+    $page_match = $page_match || drupal_match_path($_GET['q'], $patterns);
+  }
+
+  return $page_match;
+}
+
+/**
+ * Form constructor for the feedback form.
+ *
+ * @see feedback_form_submit()
+ * @ingroup forms
+ */
+function feedback_form($form, &$form_state) {
+  $form['#attributes']['class'] = array('feedback-form');
+
+  // Store the path on which this form is displayed.
+  if (!isset($form_state['inline']['location'])) {
+    $form_state['inline']['location'] = $_GET['q'];
+  }
+  $form['location'] = array(
+    '#type' => 'value',
+    '#value' => $form_state['inline']['location'],
+  );
+
+  $form['help'] = array(
+    '#prefix' => '<div class="feedback-help">',
+    '#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.'),
+    '#suffix' => '</div>',
+  );
+  if (user_access('view feedback messages')) {
+    if (arg(0) != 'node') {
+      $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location_masked' => feedback_mask_path($_GET['q'])));
+    }
+    else {
+      $feedbacks = feedback_load_multiple(array(), array('status' => FEEDBACK_OPEN, 'location' => $_GET['q']));
+    }
+    if ($feedbacks) {
+      form_load_include($form_state, 'inc', 'feedback', 'feedback.admin');
+      $form['messages'] = array(
+        '#prefix' => '<div class="feedback-messages">',
+        '#suffix' => '</div>',
+      );
+      foreach ($feedbacks as $fid => $feedback) {
+        $form['messages'][$fid]['#feedback'] = $feedback;
+        $form['messages'][$fid]['submitted'] = array('#markup' => t('@feedback-author !feedback-date:', array('@feedback-author' => format_username($feedback), '!feedback-date' => format_date($feedback->timestamp, 'small'))));
+        $form['messages'][$fid]['submitted']['#prefix'] = '<div class="feedback-submitted">';
+        $form['messages'][$fid]['submitted']['#suffix'] = '</div>';
+        $form['messages'][$fid]['body'] = feedback_build_content($feedback, 'widget');
+        $form['messages'][$fid]['body']['#prefix'] = '<div class="feedback-body">';
+        $form['messages'][$fid]['body']['#suffix'] = '</div>';
+      }
+    }
+  }
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#attributes' => array('class' => array('feedback-message')),
+    '#cols' => 20,
+    '#title' => t('Message'),
+    '#required' => TRUE,
+    '#wysiwyg' => FALSE,
+  );
+
+  $entry = new stdClass();
+  field_attach_form('feedback', $entry, $form, $form_state);
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+    // Without clearfix, the AJAX throbber wraps in an ugly way.
+    // @todo Patch #type actions in core?
+    '#attributes' => array('class' => array('clearfix')),
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Send feedback'),
+    '#id' => 'feedback-submit',
+    '#ajax' => array(
+      'wrapper' => 'feedback-form',
+      'callback' => 'feedback_form_ajax_callback',
+      'progress' => array(
+        'type' => 'throbber',
+        'message' => '',
+      ),
+    ),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submission handler for feedback_form().
+ */
+function feedback_form_submit($form, &$form_state) {
+  $entry = new stdClass();
+  entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
+  $entry->message = $form_state['values']['message'];
+  $entry->location = $form_state['values']['location'];
+  feedback_save($entry);
+
+  drupal_set_message(t('Thanks for your feedback!'));
+}
+
+/**
+ * AJAX callback for feedback_form() submissions.
+ */
+function feedback_form_ajax_callback($form, &$form_state) {
+  // If there was a form validation error, re-render the entire form.
+  if (!$form_state['executed']) {
+    return $form;
+  }
+
+  // Otherwise, return a fresh copy of the form, so the user may post additional
+  // feedback.
+  // Reset the static cache of drupal_html_id().
+  // @see drupal_process_form()
+  // @see drupal_html_id()
+  $seen_ids = &drupal_static('drupal_html_id');
+  $seen_ids = array();
+
+  // Prevent the form from being processed again.
+  // @see drupal_build_form()
+  list($form, $new_form_state) = ajax_get_form();
+  $new_form_state['input'] = array();
+  drupal_process_form($form['#form_id'], $form, $new_form_state);
+
+  // Return AJAX commands in order to output the special success message.
+  // @see ajax_deliver()
+  $build = array('#type' => 'ajax');
+  $html = drupal_render($form);
+  $build['#commands'][] = ajax_command_insert(NULL, $html);
+
+  // A successful form submission normally means that there were no errors, so
+  // we only render status messages.
+  $messages = drupal_get_messages();
+  $messages += array('status' => array());
+  $messages = implode('<br />', $messages['status']);
+  $html = '<div id="feedback-status-message">' . $messages . '</div>';
+  $build['#commands'][] = ajax_command_append('#block-feedback-form', $html);
+  return $build;
+}
+
+/**
+ * Loads a feedback entry from the database.
+ *
+ * @param $fid
+ *   Integer specifying the feedback ID to load.
+ *
+ * @return
+ *   A loaded feedback entry object upon successful load, or FALSE if the entry
+ *   cannot be loaded.
+ *
+ * @see feedback_load_multiple()
+ */
+function feedback_load($fid) {
+  $entries = feedback_load_multiple(array($fid));
+  return (isset($entries[$fid]) ? $entries[$fid] : FALSE);
+}
+
+/**
+ * Loads feedback entries from the database.
+ *
+ * @param $fids
+ *   An array of feedback entry IDs.
+ * @param $conditions
+ *   An associative array of conditions on the {feedback} table, where the keys
+ *   are the database fields and the values are the values those fields
+ *   must have.
+ *
+ * @return
+ *   An array of feedback entry objects indexed by fid.
+ *
+ * @see hook_feedback_load()
+ * @see feedback_load()
+ * @see entity_load()
+ * @see EntityFieldQuery
+ */
+function feedback_load_multiple($fids = array(), $conditions = array()) {
+  return entity_load('feedback', $fids, $conditions);
+}
+
+/**
+ * Saves changes to a feedback entry or adds a new feedback entry.
+ *
+ * @param $entry
+ *   The feedback entry object to modify or add. If $entry->fid is omitted, a
+ *   new entry will be added.
+ *
+ * @see hook_feedback_insert()
+ * @see hook_feedback_update()
+ */
+function feedback_save($entry) {
+  global $user;
+
+  // Load the stored entity, if any.
+  if (!empty($entry->fid) && !isset($entry->original)) {
+    $entry->original = entity_load_unchanged('feedback', $entry->fid);
+  }
+
+  field_attach_presave('feedback', $entry);
+
+  // Allow modules to alter the feedback entry before saving.
+  module_invoke_all('feedback_presave', $entry);
+  module_invoke_all('entity_presave', $entry, 'feedback');
+
+  if (empty($entry->fid)) {
+    $entry->message = trim($entry->message);
+
+    $defaults = array(
+      'uid' => $user->uid,
+      'location_masked' => feedback_mask_path($entry->location),
+      'url' => url($entry->location, array('absolute' => TRUE)),
+      'timestamp' => REQUEST_TIME,
+      'useragent' => $_SERVER['HTTP_USER_AGENT'],
+    );
+    foreach ($defaults as $key => $default) {
+      if (!isset($entry->$key)) {
+        $entry->$key = $default;
+      }
+    }
+
+    $status = drupal_write_record('feedback', $entry);
+    field_attach_insert('feedback', $entry);
+    module_invoke_all('feedback_insert', $entry);
+    module_invoke_all('entity_insert', $entry, 'feedback');
+  }
+  else {
+    $status = drupal_write_record('feedback', $entry, 'fid');
+
+    field_attach_update('feedback', $entry);
+    module_invoke_all('feedback_update', $entry);
+    module_invoke_all('entity_update', $entry, 'feedback');
+  }
+  unset($entry->original);
+
+  return $status;
+}
+
+/**
+ * Deletes a feedback entry.
+ *
+ * @param $fid
+ *   A feedback entry ID.
+ */
+function feedback_delete($fid) {
+  feedback_delete_multiple(array($fid));
+}
+
+/**
+ * Deletes multiple feedback entries.
+ *
+ * @param $fids
+ *   An array of feedback entry IDs.
+ */
+function feedback_delete_multiple($fids) {
+  if (!empty($fids)) {
+    $entries = feedback_load_multiple($fids);
+    foreach ($entries as $fid => $entry) {
+      field_attach_delete('feedback', $entry);
+      module_invoke_all('feedback_delete', $entry);
+      module_invoke_all('entity_delete', $entry, 'feedback');
+    }
+    db_delete('feedback')
+      ->condition('fid', $fids, 'IN')
+      ->execute();
+  }
+}
+
+/**
+ * 'Mask' a path, i.e. replace all numeric arguments in a path with '%' placeholders.
+ *
+ * Please note that only numeric arguments with a preceding slash will be
+ * replaced.
+ *
+ * @param $path
+ *   An internal Drupal path, f.e. 'user/123/edit'.
+ * @return
+ *   A 'masked' path, for above example 'user/%/edit'.
+ *
+ * @todo Use the untranslated patch of menu_get_item() instead.
+ */
+function feedback_mask_path($path) {
+  return preg_replace('@/\d+@', '/%', $path);
+}
+
+/**
+ * Implements hook_user_cancel().
+ */
+function feedback_user_cancel($edit, $account, $method) {
+  switch ($method) {
+    case 'user_cancel_reassign':
+      db_update('feedback')
+        ->fields(array('uid' => 0))
+        ->condition('uid', $account->uid)
+        ->execute();
+      break;
+  }
+}
+
+/**
+ * Implements hook_user_delete().
+ */
+function feedback_user_delete($account) {
+  $fids = db_query('SELECT fid FROM {feedback} WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
+  feedback_delete_multiple($fids);
+}
+
+/**
+ * Implements hook_mollom_form_list().
+ */
+function feedback_mollom_form_list() {
+  $forms['feedback_form'] = array(
+    'title' => t('Feedback form'),
+    'entity' => 'feedback',
+    'bundle' => 'feedback',
+    'entity delete multiple callback' => 'feedback_delete_multiple',
+    'delete form' => 'feedback_delete_confirm',
+    'delete form file' => array(
+      'name' => 'feedback.admin',
+    ),
+    'report access' => array('view feedback messages'),
+  );
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function feedback_mollom_form_info($form_id) {
+  $form_info = array(
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer feedback'),
+    'elements' => array(
+      'message' => t('Message'),
+    ),
+  );
+  mollom_form_info_add_fields($form_info, 'feedback', 'feedback');
+  return $form_info;
+}
+
+/**
+ * Implements hook_views_api();
+ */
+function feedback_views_api() {
+  return array(
+    'api' => 3.0,
+    'path' => drupal_get_path('module', 'feedback') . '/views',
+  );
+}

BIN
sites/all/modules/contrib/users/feedback/images/throbber.gif


+ 159 - 0
sites/all/modules/contrib/users/feedback/tests/feedback.test

@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * @file
+ * Tests for Feedback module.
+ */
+
+/**
+ * Test basic Feedback functionality.
+ */
+class FeedbackTestCase extends DrupalWebTestCase {
+  protected $profile = 'testing';
+
+  public static function getInfo() {
+    return array(
+     'name' => 'Feedback functionality',
+     'description' => 'Tests basic Feedback module functionality.',
+     'group' => 'Feedback',
+    );
+  }
+
+  function setUp() {
+    parent::setUp(array('feedback', 'trigger'));
+
+    $this->admin_user = $this->drupalCreateUser(array('access feedback form', 'view feedback messages', 'administer feedback'));
+    $this->web_user = $this->drupalCreateUser(array('access feedback form'));
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Test a basic feedback message.
+   */
+  function testFeedbackMessage() {
+    $message = $this->randomString();
+    $edit = array(
+      'message' => $message,
+    );
+    $this->drupalPost('node', $edit, t('Send feedback'));
+
+    // Verify the message was recorded.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet('admin/reports/feedback');
+    $this->assertRaw(check_plain($message), t('Message found.'));
+    $this->assertText('node', t('Originating system path found.'));
+    $this->assertLinkByHref(url('node', array('absolute' => TRUE)), 0, t('Originating absolute URL found.'));
+
+    // Verify that we can process the message.
+    $edit = array(
+      'feedback-messages[0][1]' => TRUE,
+    );
+    $this->drupalPost(NULL, $edit, t('Update'));
+    $this->assertFieldByName('feedback-messages[1][1]', 1, t('Processed message found.'));
+  }
+
+  /**
+   * Test visibility settings.
+   */
+   function testFeedbackVisibility() {
+     $this->drupalLogin($this->admin_user);
+     $this->drupalGet('user');
+     $this->assertRaw('<span class="feedback-link">' . t('Feedback') . '</span>', t('Feedback form shown.'));
+     $edit = array(
+       'feedback_excluded_paths' => 'user*',
+     );
+     $this->drupalPost('admin/config/user-interface/feedback', $edit, t('Save configuration'));
+     $this->drupalGet('user');
+     $this->assertNoRaw('<span class="feedback-link">' . t('Feedback') . '</span>', t('Feedback form not shown.'));
+   }
+
+ /**
+  * Test feedback deletion.
+  */
+  function testFeedbackDelete() {
+    $this->drupalLogin($this->admin_user);
+    $message = $this->randomString();
+    $edit = array(
+      'message' => $message,
+    );
+    $this->drupalPost('node', $edit, t('Send feedback'));
+
+    // Verify a delete link is shown.
+    $this->drupalGet('admin/reports/feedback');
+    $this->assertLinkByHref('admin/reports/feedback/1/delete');
+
+    // Verify deletion.
+    $this->drupalPost('admin/reports/feedback/1/delete', array(), t('Delete'));
+    $this->assertRaw(t('The feedback entry was deleted'), t('Feedback deletion message shown.'));
+    $this->assertNoLinkByHref('admin/reports/feedback/1/delete');
+    $this->assertNoRaw(check_plain($message), t('Message not found.'));
+  }
+
+  /**
+   * Test the feedback triggers and actions.
+   */
+  function testFeedbackEmailAction() {
+    $test_user = $this->drupalCreateUser(array('administer actions', 'administer feedback', 'access feedback form'));
+    $this->drupalLogin($test_user);
+    $this->assignFeedbackEmailAction('feedback_insert', $test_user->mail);
+
+    // Insert a feedback entry.
+    $message = $this->randomString();
+    $edit = array(
+      'message' => $message,
+    );
+    $this->drupalPost('node', $edit, t('Send feedback'));
+    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $message);
+
+    $this->assignFeedbackEmailAction('feedback_update', $test_user->mail);
+
+    // Update a feedback entry.
+    $fid = db_query("SELECT fid FROM {feedback} LIMIT 0,1")->fetchField();
+    $entry = feedback_load($fid);
+    feedback_save($entry);
+    $this->assertFeedbackEmailTokenReplacement($test_user->mail, $entry->message);
+  }
+
+  /**
+   * Assigns a system_send_email_action to the passed-in trigger.
+   *
+   * @param $trigger
+   *   For example, 'user_login'
+   */
+  function assignFeedbackEmailAction($trigger, $mail) {
+    $form_name = "trigger_{$trigger}_assign_form";
+    $form_html_id = strtr($form_name, '_', '-');
+
+    $edit = array(
+      'actions_label' => $trigger . "_feedback_send_email_action",
+      'recipients' => $mail,
+      'subject' => $this->randomName(),
+      'message' => '!message',
+    );
+
+    $hash = drupal_hash_base64('feedback_send_email_action');
+    $this->drupalPost("admin/config/system/actions/configure/$hash", $edit, t('Save'));
+    $this->assertText(t('The action has been successfully saved.'));
+
+    // Now we have to find out the action ID of what we created.
+    $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback AND label = :label', array(':callback' => 'feedback_send_email_action', ':label' => $edit['actions_label']))->fetchField();
+
+    $edit = array('aid' => drupal_hash_base64($aid));
+    $this->drupalPost('admin/structure/trigger/feedback', $edit, t('Assign'), array(), array(), $form_html_id);
+  }
+
+
+  /**
+   * Asserts correct token replacement for the given trigger and account.
+   *
+   * @param $account
+   *   The user account which triggered the action.
+   * @param $email_depth
+   *   Number of emails to scan, starting with most recent.
+   */
+  function assertFeedbackEmailTokenReplacement($mail, $message, $email_depth = 1) {
+    $this->verboseEmail($email_depth);
+    $this->assertMailString('body', $message, $email_depth);
+    $this->assertMail('to', $mail, t('Mail sent to correct destination'));
+  }
+}

+ 184 - 0
sites/all/modules/contrib/users/feedback/views/feedback.views.inc

@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * @file
+ * Provides views integration.
+ */
+
+/**
+ * Implements hook_views_data().
+ */
+function feedback_views_data() {
+  $data['feedback']['table']['group'] = t('Feedback');
+  $data['feedback']['table']['base'] = array(
+    'field' => 'fid',
+    'title' => t('Feedback'),
+    'help' => t('Feedback messages submitted to the site.'),
+  );
+  $data['feedback']['table']['join']['users'] = array(
+    'left_field' => 'uid',
+    'field' => 'uid',
+  );
+  $data['feedback']['fid'] = array(
+    'title' => t('Feedback ID'),
+    'help' => t('The primary identifier for a feedback message.'),
+    'field' => array(
+      'handler' => 'views_handler_field_numeric',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_numeric',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_numeric',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort_numeric',
+    ),
+  );
+  $data['feedback']['view_entry'] = array(
+    'field' => array(
+      'title' => t('Link'),
+      'help' => t('Provide a simple link to the feedback entry.'),
+      'handler' => 'feedback_handler_field_feedback_link',
+    ),
+  );
+  $data['feedback']['view_entry_delete'] = array(
+    'field' => array(
+      'title' => t('Delete entry'),
+      'help' => t('Provide a simple link to delete the feedback entry.'),
+      'handler' => 'feedback_handler_field_feedback_link_delete',
+    ),
+  );
+  $data['feedback']['view_entry_edit'] = array(
+    'field' => array(
+      'title' => t('Edit entry'),
+      'help' => t('Provide a simple link to the edit feedback entry.'),
+      'handler' => 'feedback_handler_field_feedback_link_edit',
+    ),
+  );
+  $data['feedback']['uid'] = array(
+    'title' => t('User Id'),
+    'help' => t('The user id of the author of a feedback message.'),
+    'relationship' => array(
+      'base' => 'users',
+      'base field' => 'uid',
+      'handler' => 'views_handler_relationship',
+      'label' => t('User'),
+    ),
+  );
+  $data['feedback']['status'] = array(
+    'title' => t('Status'),
+    'help' => t('The status of a feedback message.'),
+    'field' => array(
+      'handler' => 'views_handler_field_boolean',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_boolean_operator',
+      'label' => t('Feedback status is processed'),
+      'type' => 'yes-no',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_boolean',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+  $data['feedback']['message'] = array(
+    'title' => t('Message'),
+    'help' => t('The actual feedback message.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+  $data['feedback']['location'] = array(
+    'title' => t('Location'),
+    'help' => t('The internal Drupal path of the page the feedback message was submitted on.'),
+    'field' => array(
+      'handler' => 'views_handler_field_url',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+  $data['feedback']['location_masked'] = array(
+    'title' => t('Location Masked'),
+    'help' => t('The masked Drupal path of the page the feedback message was submitted on.'),
+    'field' => array(
+      'handler' => 'views_handler_field_url',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+  $data['feedback']['url'] = array(
+    'title' => t('URL'),
+    'help' => t('The absolute URL of the page the feedback message was submitted on.'),
+    'field' => array(
+      'handler' => 'views_handler_field_url',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+  $data['feedback']['useragent'] = array(
+    'title' => t('User agent'),
+    'help' => t('The user agent of the feedback message author.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+  $data['feedback']['timestamp'] = array(
+    'title' => t('Timestamp'),
+    'help' => t('The UNIX timestamp when the feedback message was created.'),
+    'field' => array(
+      'click sortable' => TRUE,
+      'handler' => 'views_handler_field_date',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_date',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_date',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort_date',
+    ),
+  );
+  return $data;
+}

+ 250 - 0
sites/all/modules/contrib/users/feedback/views/feedback.views_default.inc

@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * @file
+ * Contains default views on behalf of the feedback module.
+ */
+
+/**
+ * Implements hook_views_default_views().
+ */
+function feedback_views_default_views() {
+  $view = new view();
+  $view->name = 'feedback_messages';
+  $view->description = 'Override the default feedback messages report.';
+  $view->tag = 'feedback';
+  $view->base_table = 'feedback';
+  $view->human_name = '';
+  $view->core = 0;
+  $view->api_version = '3.0';
+  $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+  /* Display: Defaults */
+  $handler = $view->new_display('default', 'Defaults', 'default');
+  $handler->display->display_options['title'] = 'Feedback Messages';
+  $handler->display->display_options['use_more_always'] = FALSE;
+  $handler->display->display_options['access']['type'] = 'perm';
+  $handler->display->display_options['access']['perm'] = 'view feedback messages';
+  $handler->display->display_options['cache']['type'] = 'none';
+  $handler->display->display_options['query']['type'] = 'views_query';
+  $handler->display->display_options['exposed_form']['type'] = 'basic';
+  $handler->display->display_options['pager']['type'] = 'full';
+  $handler->display->display_options['pager']['options']['items_per_page'] = 50;
+  $handler->display->display_options['style_plugin'] = 'table';
+  $handler->display->display_options['style_options']['columns'] = array(
+    'location' => 'location',
+    'timestamp' => 'timestamp',
+    'name' => 'name',
+    'message' => 'message',
+    'useragent' => 'message',
+    'view_entry' => 'message',
+    'view_entry_delete' => 'message',
+    'view_entry_edit' => 'message',
+  );
+  $handler->display->display_options['style_options']['default'] = '-1';
+  $handler->display->display_options['style_options']['info'] = array(
+    'location' => array(
+      'sortable' => 1,
+      'default_sort_order' => 'asc',
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+    'timestamp' => array(
+      'sortable' => 1,
+      'default_sort_order' => 'asc',
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+    'name' => array(
+      'sortable' => 1,
+      'default_sort_order' => 'asc',
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+    'message' => array(
+      'align' => '',
+      'separator' => ' ',
+      'empty_column' => 0,
+    ),
+    'useragent' => array(
+      'sortable' => 0,
+      'default_sort_order' => 'asc',
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+    'view_entry' => array(
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+    'view_entry_delete' => array(
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+    'view_entry_edit' => array(
+      'align' => '',
+      'separator' => '',
+      'empty_column' => 0,
+    ),
+  );
+  /* Header: Global: Text area */
+  $handler->display->display_options['header']['text']['id'] = 'text';
+  $handler->display->display_options['header']['text']['table'] = 'views';
+  $handler->display->display_options['header']['text']['field'] = 'area';
+  $handler->display->display_options['header']['text']['content'] = 'Open Feedback Messages';
+  $handler->display->display_options['header']['text']['format'] = 'full_html';
+  /* No results behavior: Global: Text area */
+  $handler->display->display_options['empty']['area']['id'] = 'area';
+  $handler->display->display_options['empty']['area']['table'] = 'views';
+  $handler->display->display_options['empty']['area']['field'] = 'area';
+  $handler->display->display_options['empty']['area']['empty'] = TRUE;
+  $handler->display->display_options['empty']['area']['content'] = 'There are no open feedback entries.';
+  $handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+  /* Relationship: Feedback: User Id */
+  $handler->display->display_options['relationships']['uid']['id'] = 'uid';
+  $handler->display->display_options['relationships']['uid']['table'] = 'feedback';
+  $handler->display->display_options['relationships']['uid']['field'] = 'uid';
+  /* Field: Feedback: Location */
+  $handler->display->display_options['fields']['location']['id'] = 'location';
+  $handler->display->display_options['fields']['location']['table'] = 'feedback';
+  $handler->display->display_options['fields']['location']['field'] = 'location';
+  /* Field: Feedback: Timestamp */
+  $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp';
+  $handler->display->display_options['fields']['timestamp']['table'] = 'feedback';
+  $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp';
+  $handler->display->display_options['fields']['timestamp']['label'] = 'Date';
+  $handler->display->display_options['fields']['timestamp']['date_format'] = 'short';
+  /* Field: User: Name */
+  $handler->display->display_options['fields']['name']['id'] = 'name';
+  $handler->display->display_options['fields']['name']['table'] = 'users';
+  $handler->display->display_options['fields']['name']['field'] = 'name';
+  $handler->display->display_options['fields']['name']['relationship'] = 'uid';
+  $handler->display->display_options['fields']['name']['label'] = 'User';
+  /* Field: Feedback: Message */
+  $handler->display->display_options['fields']['message']['id'] = 'message';
+  $handler->display->display_options['fields']['message']['table'] = 'feedback';
+  $handler->display->display_options['fields']['message']['field'] = 'message';
+  $handler->display->display_options['fields']['message']['element_type'] = 'div';
+  /* Field: Feedback: User agent */
+  $handler->display->display_options['fields']['useragent']['id'] = 'useragent';
+  $handler->display->display_options['fields']['useragent']['table'] = 'feedback';
+  $handler->display->display_options['fields']['useragent']['field'] = 'useragent';
+  $handler->display->display_options['fields']['useragent']['label'] = '';
+  $handler->display->display_options['fields']['useragent']['element_type'] = 'div';
+  /* Field: Feedback: Link */
+  $handler->display->display_options['fields']['view_entry']['id'] = 'view_entry';
+  $handler->display->display_options['fields']['view_entry']['table'] = 'feedback';
+  $handler->display->display_options['fields']['view_entry']['field'] = 'view_entry';
+  $handler->display->display_options['fields']['view_entry']['label'] = '';
+  $handler->display->display_options['fields']['view_entry']['element_label_colon'] = FALSE;
+  /* Field: Feedback: Edit entry */
+  $handler->display->display_options['fields']['view_entry_edit']['id'] = 'view_entry_edit';
+  $handler->display->display_options['fields']['view_entry_edit']['table'] = 'feedback';
+  $handler->display->display_options['fields']['view_entry_edit']['field'] = 'view_entry_edit';
+  $handler->display->display_options['fields']['view_entry_edit']['label'] = '';
+  $handler->display->display_options['fields']['view_entry_edit']['element_label_colon'] = FALSE;
+  /* Field: Feedback: Delete entry */
+  $handler->display->display_options['fields']['view_entry_delete']['id'] = 'view_entry_delete';
+  $handler->display->display_options['fields']['view_entry_delete']['table'] = 'feedback';
+  $handler->display->display_options['fields']['view_entry_delete']['field'] = 'view_entry_delete';
+  $handler->display->display_options['fields']['view_entry_delete']['label'] = '';
+  $handler->display->display_options['fields']['view_entry_delete']['element_label_colon'] = FALSE;
+  /* Sort criterion: Feedback: Timestamp */
+  $handler->display->display_options['sorts']['timestamp']['id'] = 'timestamp';
+  $handler->display->display_options['sorts']['timestamp']['table'] = 'feedback';
+  $handler->display->display_options['sorts']['timestamp']['field'] = 'timestamp';
+  /* Filter criterion: Feedback: Status */
+  $handler->display->display_options['filters']['status']['id'] = 'status';
+  $handler->display->display_options['filters']['status']['table'] = 'feedback';
+  $handler->display->display_options['filters']['status']['field'] = 'status';
+  $handler->display->display_options['filters']['status']['value'] = '0';
+  $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE;
+
+  /* Display: Open Entries */
+  $handler = $view->new_display('page', 'Open Entries', 'page_1');
+  $handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+  $handler->display->display_options['defaults']['header'] = FALSE;
+  /* Header: Global: Text area */
+  $handler->display->display_options['header']['text']['id'] = 'text';
+  $handler->display->display_options['header']['text']['table'] = 'views';
+  $handler->display->display_options['header']['text']['field'] = 'area';
+  $handler->display->display_options['header']['text']['empty'] = TRUE;
+  $handler->display->display_options['header']['text']['content'] = '<h3>Open Feedback Messages</h3>';
+  $handler->display->display_options['header']['text']['format'] = 'full_html';
+  $handler->display->display_options['path'] = 'admin/reports/feedback';
+  $handler->display->display_options['menu']['type'] = 'normal';
+  $handler->display->display_options['menu']['title'] = 'Feedback messages';
+  $handler->display->display_options['menu']['description'] = 'View feedback messages.';
+  $handler->display->display_options['menu']['weight'] = '0';
+  $handler->display->display_options['menu']['name'] = 'management';
+
+  /* Display: Processed Entries */
+  $handler = $view->new_display('attachment', 'Processed Entries', 'attachment_1');
+  $handler->display->display_options['defaults']['title'] = FALSE;
+  $handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+  $handler->display->display_options['pager']['type'] = 'some';
+  $handler->display->display_options['defaults']['header'] = FALSE;
+  /* Header: Global: Text area */
+  $handler->display->display_options['header']['text']['id'] = 'text';
+  $handler->display->display_options['header']['text']['table'] = 'views';
+  $handler->display->display_options['header']['text']['field'] = 'area';
+  $handler->display->display_options['header']['text']['empty'] = TRUE;
+  $handler->display->display_options['header']['text']['content'] = '<h3>Processed Feedback Messages</h3>';
+  $handler->display->display_options['header']['text']['format'] = 'full_html';
+  $handler->display->display_options['defaults']['empty'] = FALSE;
+  /* No results behavior: Global: Text area */
+  $handler->display->display_options['empty']['area']['id'] = 'area';
+  $handler->display->display_options['empty']['area']['table'] = 'views';
+  $handler->display->display_options['empty']['area']['field'] = 'area';
+  $handler->display->display_options['empty']['area']['empty'] = TRUE;
+  $handler->display->display_options['empty']['area']['content'] = 'There are no processed feedback entries.';
+  $handler->display->display_options['empty']['area']['format'] = 'filtered_html';
+  $handler->display->display_options['defaults']['filter_groups'] = FALSE;
+  $handler->display->display_options['defaults']['filters'] = FALSE;
+  /* Filter criterion: Feedback: Status */
+  $handler->display->display_options['filters']['status']['id'] = 'status';
+  $handler->display->display_options['filters']['status']['table'] = 'feedback';
+  $handler->display->display_options['filters']['status']['field'] = 'status';
+  $handler->display->display_options['filters']['status']['value'] = '1';
+  $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE;
+  $handler->display->display_options['displays'] = array(
+    'page_1' => 'page_1',
+    'default' => 0,
+  );
+  $handler->display->display_options['attachment_position'] = 'after';
+  $handler->display->display_options['render_pager'] = TRUE;
+  $translatables['feedback_messages'] = array(
+    t('Defaults'),
+    t('Feedback Messages'),
+    t('more'),
+    t('Apply'),
+    t('Reset'),
+    t('Sort by'),
+    t('Asc'),
+    t('Desc'),
+    t('Items per page'),
+    t('- All -'),
+    t('Offset'),
+    t('« first'),
+    t('‹ previous'),
+    t('next ›'),
+    t('last »'),
+    t('Open Feedback Messages'),
+    t('There are no processed feedback entries.'),
+    t('There are no open feedback entries.'),
+    t('User'),
+    t('Location'),
+    t('Date'),
+    t('Message'),
+    t('Page'),
+    t('Attachment'),
+    t('Processed Feedback Messages'),
+  );
+  $views[$view->name] = $view;
+  return $views;
+}

+ 45 - 0
sites/all/modules/contrib/users/feedback/views/feedback_handler_field_feedback_link.inc

@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Field handler to present a link to a feedback entry.
+ */
+class feedback_handler_field_feedback_link extends views_handler_field {
+  function construct() {
+    parent::construct();
+    $this->additional_fields['fid'] = 'fid';
+  }
+
+  function option_definition() {
+    $options = parent::option_definition();
+
+    $options['text'] = array('default' => '', 'translatable' => TRUE);
+
+    return $options;
+  }
+
+  function options_form(&$form, &$form_state) {
+    $form['text'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Text to display'),
+      '#default_value' => $this->options['text'],
+    );
+    parent::options_form($form, $form_state);
+  }
+
+  function query() {
+    $this->ensure_my_table();
+    $this->add_additional_fields();
+  }
+
+  function render($values) {
+    $value = $this->get_value($values, 'fid');
+    return $this->render_link($this->sanitize_value($value), $values);
+  }
+
+  function render_link($data, $values) {
+    $this->options['alter']['make_link'] = TRUE;
+    $this->options['alter']['path'] = "admin/reports/feedback/$data";
+    $text = !empty($this->options['text']) ? $this->options['text'] : t('view');
+    return $text;
+  }
+}

+ 13 - 0
sites/all/modules/contrib/users/feedback/views/feedback_handler_field_feedback_link_delete.inc

@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * Field handler to present a link to delete a feedback entry.
+ */
+class feedback_handler_field_feedback_link_delete extends feedback_handler_field_feedback_link {
+  function render_link($data, $values) {
+    $this->options['alter']['make_link'] = TRUE;
+    $this->options['alter']['path'] = "admin/reports/feedback/$data/delete";
+    $text = !empty($this->options['text']) ? $this->options['text'] : t('delete');
+    return $text;
+  }
+}

+ 13 - 0
sites/all/modules/contrib/users/feedback/views/feedback_handler_field_feedback_link_edit.inc

@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * Field handler to present a link to edit a feedback entry.
+ */
+class feedback_handler_field_feedback_link_edit extends feedback_handler_field_feedback_link {
+  function render_link($data, $values) {
+    $this->options['alter']['make_link'] = TRUE;
+    $this->options['alter']['path'] = "admin/reports/feedback/$data/edit";
+    $text = !empty($this->options['text']) ? $this->options['text'] : t('edit');
+    return $text;
+  }
+}