radiant-comments-extension 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +40 -0
  3. data/HELP_admin.markdown +52 -0
  4. data/HELP_designer.markdown +36 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +53 -0
  7. data/Rakefile +133 -0
  8. data/TODO +6 -0
  9. data/VERSION +1 -0
  10. data/app/controllers/admin/comments_controller.rb +130 -0
  11. data/app/controllers/comments_controller.rb +59 -0
  12. data/app/helpers/admin/comments_helper.rb +7 -0
  13. data/app/models/akismet_spam_filter.rb +37 -0
  14. data/app/models/comment.rb +121 -0
  15. data/app/models/comment_mailer.rb +24 -0
  16. data/app/models/mollom_spam_filter.rb +52 -0
  17. data/app/models/simple_spam_filter.rb +38 -0
  18. data/app/models/spam_filter.rb +43 -0
  19. data/app/views/admin/comments/_comment.rhtml +34 -0
  20. data/app/views/admin/comments/_form.rhtml +36 -0
  21. data/app/views/admin/comments/edit.rhtml +5 -0
  22. data/app/views/admin/comments/index.rhtml +55 -0
  23. data/app/views/admin/pages/_comments.rhtml +0 -0
  24. data/app/views/admin/pages/_edit_comments_enabled.rhtml +8 -0
  25. data/app/views/admin/pages/_index_head_view_comments.rhtml +1 -0
  26. data/app/views/admin/pages/_index_view_comments.rhtml +11 -0
  27. data/app/views/comment_mailer/comment_notification.rhtml +21 -0
  28. data/app/views/comments/_comment.rhtml +1 -0
  29. data/app/views/comments/_form.rhtml +23 -0
  30. data/app/views/comments/_new.rhtml +5 -0
  31. data/autotest/discover.rb +3 -0
  32. data/comments_extension.rb +81 -0
  33. data/cucumber.yml +1 -0
  34. data/db/migrate/001_create_comments.rb +29 -0
  35. data/db/migrate/002_create_snippets.rb +115 -0
  36. data/db/migrate/003_change_filter_id_from_integer_to_string.rb +10 -0
  37. data/db/migrate/004_add_approval_columns.rb +13 -0
  38. data/db/migrate/005_add_mollomid_column.rb +11 -0
  39. data/db/migrate/006_move_config_to_migrations.rb +22 -0
  40. data/db/migrate/007_add_preference_for_simple_spamcheck.rb +12 -0
  41. data/features/support/env.rb +16 -0
  42. data/features/support/paths.rb +16 -0
  43. data/lib/akismet.rb +134 -0
  44. data/lib/comment_page_extensions.rb +41 -0
  45. data/lib/comment_tags.rb +338 -0
  46. data/lib/mollom.rb +246 -0
  47. data/lib/radiant-comments-extension.rb +0 -0
  48. data/lib/tasks/comments_extension_tasks.rake +68 -0
  49. data/public/images/admin/accept.png +0 -0
  50. data/public/images/admin/comment_edit.png +0 -0
  51. data/public/images/admin/comments.png +0 -0
  52. data/public/images/admin/comments_delete.png +0 -0
  53. data/public/images/admin/delete.png +0 -0
  54. data/public/images/admin/email.png +0 -0
  55. data/public/images/admin/error.png +0 -0
  56. data/public/images/admin/link.png +0 -0
  57. data/public/images/admin/page_white_edit.png +0 -0
  58. data/public/images/admin/table_save.png +0 -0
  59. data/public/images/admin/tick.png +0 -0
  60. data/public/stylesheets/admin/comments.css +41 -0
  61. data/radiant-comments-extension.gemspec +133 -0
  62. data/spec/controllers/admin/comments_controller_spec.rb +57 -0
  63. data/spec/controllers/admin/comments_routing_spec.rb +43 -0
  64. data/spec/controllers/page_postback_spec.rb +51 -0
  65. data/spec/datasets/comments_dataset.rb +7 -0
  66. data/spec/models/akismet_spam_filter_spec.rb +61 -0
  67. data/spec/models/comment_spec.rb +148 -0
  68. data/spec/models/comment_tags_spec.rb +55 -0
  69. data/spec/models/mollom_spam_filter_spec.rb +103 -0
  70. data/spec/models/simple_spam_filter_spec.rb +44 -0
  71. data/spec/models/spam_filter_spec.rb +38 -0
  72. data/spec/spec.opts +6 -0
  73. data/spec/spec_helper.rb +36 -0
  74. data/test/fixtures/users.yml +6 -0
  75. data/test/integration/comment_enabling_test.rb +18 -0
  76. data/test/test_helper.rb +24 -0
  77. data/test/unit/comment_test.rb +52 -0
  78. metadata +177 -0
@@ -0,0 +1,59 @@
1
+ class CommentsController < ApplicationController
2
+
3
+ no_login_required
4
+ skip_before_filter :verify_authenticity_token
5
+ before_filter :find_page
6
+ before_filter :set_host
7
+
8
+ def index
9
+ @page.selected_comment = @page.comments.find_by_id(flash[:selected_comment])
10
+ @page.request = request
11
+ render :text => @page.render
12
+ end
13
+
14
+ def create
15
+ comment = @page.comments.build(params[:comment])
16
+ comment.request = request
17
+ comment.request = @page.request = request
18
+ comment.save!
19
+
20
+ clear_single_page_cache(comment)
21
+ if Radiant::Config['comments.notification'] == "true"
22
+ if comment.approved? || Radiant::Config['comments.notify_unapproved'] == "true"
23
+ CommentMailer.deliver_comment_notification(comment)
24
+ end
25
+ end
26
+
27
+ flash[:selected_comment] = comment.id
28
+ redirect_to "#{@page.url}comments#comment-#{comment.id}"
29
+ rescue ActiveRecord::RecordInvalid
30
+ @page.last_comment = comment
31
+ render :text => @page.render
32
+ # rescue Comments::MollomUnsure
33
+ #flash, en render :text => @page.render
34
+ end
35
+
36
+ private
37
+
38
+ def find_page
39
+ url = params[:url]
40
+ url.shift if defined?(SiteLanguage) && SiteLanguage.count > 1
41
+ @page = Page.find_by_url(url.join("/"))
42
+ end
43
+
44
+ def set_host
45
+ CommentMailer.default_url_options[:host] = request.host_with_port
46
+ end
47
+
48
+ def clear_single_page_cache(comment)
49
+ if comment && comment.page
50
+ unless defined?(ResponseCache)
51
+ Radiant::Cache::EntityStore.new.purge(comment.page.url)
52
+ Radiant::Cache::MetaStore.new.purge(comment.page.url)
53
+ else
54
+ ResponseCache.instance.clear
55
+ end
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,7 @@
1
+ module Admin::CommentsHelper
2
+ def link_or_span_unless_current(text, url, options={})
3
+ link_to_unless_current(text,url, options) do
4
+ content_tag(:span, text)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ class AkismetSpamFilter < SpamFilter
2
+ def message
3
+ 'Protected by <a href="http://akismet.com/">Akismet</a>'
4
+ end
5
+
6
+ def configured?
7
+ !Radiant::Config['comments.akismet_key'].blank? &&
8
+ !Radiant::Config['comments.akismet_url'].blank?
9
+ end
10
+
11
+ def approved?(comment)
12
+ (akismet.valid? && ham?(comment)) || raise(SpamFilter::Spam)
13
+ rescue
14
+ # Spam and anything raised by Net::HTTP, e.g. Errno, Timeout stuff
15
+ false
16
+ end
17
+
18
+ def akismet
19
+ @akismet ||= Akismet.new(Radiant::Config['comments.akismet_key'], Radiant::Config['comments.akismet_url'])
20
+ end
21
+
22
+ private
23
+ def ham?(comment)
24
+ !akismet.commentCheck(
25
+ comment.author_ip, # remote IP
26
+ comment.user_agent, # user agent
27
+ comment.referrer, # http referer
28
+ comment.page.url, # permalink
29
+ 'comment', # comment type
30
+ comment.author, # author name
31
+ comment.author_email, # author email
32
+ comment.author_url, # author url
33
+ comment.content, # comment text
34
+ {} # other
35
+ )
36
+ end
37
+ end
@@ -0,0 +1,121 @@
1
+ require 'digest/md5'
2
+ class Comment < ActiveRecord::Base
3
+ belongs_to :page, :counter_cache => true
4
+
5
+ named_scope :unapproved, :conditions => {:approved_at => nil}
6
+ named_scope :approved, :conditions => 'approved_at IS NOT NULL'
7
+ named_scope :recent, :order => 'created_at DESC'
8
+
9
+ validate :check_for_spam
10
+ validates_presence_of :author, :author_email, :content
11
+
12
+ before_save :auto_approve
13
+ before_save :apply_filter
14
+ before_save :canonicalize_url
15
+
16
+ attr_accessor :valid_spam_answer, :spam_answer
17
+ attr_accessible :author, :author_email, :author_url, :filter_id, :content, :valid_spam_answer, :spam_answer
18
+
19
+ def self.per_page
20
+ count = Radiant::Config['comments.per_page'].to_i.abs
21
+ count > 0 ? count : 50
22
+ end
23
+
24
+ def self.spam_filter
25
+ @spam_filter ||= SpamFilter.select
26
+ end
27
+
28
+ def self.simple_spam_filter_enabled?
29
+ spam_filter == SimpleSpamFilter && spam_filter.required?
30
+ end
31
+
32
+ def request=(request)
33
+ self.author_ip = request.remote_ip
34
+ self.user_agent = request.env['HTTP_USER_AGENT']
35
+ self.referrer = request.env['HTTP_REFERER']
36
+ end
37
+
38
+ def auto_approve?
39
+ Radiant::Config['comments.auto_approve'] == "true" && spam_filter.approved?(self)
40
+ end
41
+
42
+ def unapproved?
43
+ !approved?
44
+ end
45
+
46
+ def approved?
47
+ !approved_at.nil?
48
+ end
49
+
50
+ def ap_status
51
+ if approved?
52
+ "approved"
53
+ else
54
+ "unapproved"
55
+ end
56
+ end
57
+
58
+ def approve!
59
+ self.update_attribute(:approved_at, Time.now)
60
+ end
61
+
62
+ def unapprove!
63
+ self.update_attribute(:approved_at, nil)
64
+ spam_filter.spam!(self)
65
+ end
66
+
67
+ def check_for_spam
68
+ spam_filter && spam_filter.valid?(self)
69
+ end
70
+
71
+ private
72
+ def spam_filter
73
+ self.class.spam_filter
74
+ end
75
+
76
+ def auto_approve
77
+ self.approved_at = Time.now if auto_approve?
78
+ end
79
+
80
+ def apply_filter
81
+ cleaner_type = defined?(COMMENT_SANITIZE_OPTION) ? COMMENT_SANITIZE_OPTION : Sanitize::Config::RELAXED
82
+ sanitized_content = Sanitize.clean(content, cleaner_type)
83
+ self.content_html = filter.filter(sanitized_content)
84
+ end
85
+
86
+ def canonicalize_url
87
+ self.author_url = CGI.escapeHTML(author_url =~ /\Ahttps?:\/\//i ? author_url : "http://#{author_url}") unless author_url.blank?
88
+ end
89
+
90
+ def filter
91
+ if filtering_enabled? && filter_from_form
92
+ filter_from_form
93
+ else
94
+ SimpleFilter.new
95
+ end
96
+ end
97
+
98
+ def filter_from_form
99
+ unless filter_id.blank?
100
+ TextFilter.descendants.find { |f| f.filter_name == filter_id }
101
+ else
102
+ nil
103
+ end
104
+ end
105
+
106
+ def filtering_enabled?
107
+ Radiant::Config['comments.filters_enabled'] == "true"
108
+ end
109
+
110
+ class SimpleFilter
111
+ include ERB::Util
112
+ include ActionView::Helpers::TextHelper
113
+ include ActionView::Helpers::TagHelper
114
+
115
+ def filter(content)
116
+ simple_format(escape_once(content))
117
+ end
118
+ end
119
+
120
+ class AntispamWarning < StandardError; end
121
+ end
@@ -0,0 +1,24 @@
1
+ class CommentMailer < ActionMailer::Base
2
+ def comment_notification(comment, sent_at = Time.now)
3
+ notify_creator_config = Radiant::Config['comments.notify_creator']
4
+ notify_updater_config = Radiant::Config['comments.notify_updater']
5
+ notification_to_config = Radiant::Config['comments.notification_to']
6
+
7
+ receivers = []
8
+ receivers << notification_to_config unless notification_to_config.blank?
9
+ receivers << comment.page.created_by.email unless notify_creator_config == "false"
10
+ if notify_updater_config == "true" && comment.page.updated_by != comment.page.created_by
11
+ receivers << comment.page.updated_by.email
12
+ end
13
+
14
+ page_url = root_url(:host => default_url_options[:host], :port => default_url_options[:port])[0..-2] + comment.page.url
15
+ site_name = Radiant::Config['comments.notification_site_name']
16
+
17
+ subject "[#{site_name}] New #{comment.ap_status} comment posted"
18
+ recipients receivers.join(',')
19
+ from Radiant::Config['comments.notification_from']
20
+ sent_on sent_at
21
+
22
+ body :site_name => site_name, :comment => comment, :page_url => page_url
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ class MollomSpamFilter < SpamFilter
2
+ def message
3
+ 'Protected by <a href="http://mollom.com">Mollom</a>'
4
+ end
5
+
6
+ def configured?
7
+ !Radiant::Config['comments.mollom_privatekey'].blank? &&
8
+ !Radiant::Config['comments.mollom_publickey'].blank?
9
+ end
10
+
11
+ def approved?(comment)
12
+ (mollom.key_ok? && ham?(comment)) || raise(SpamFilter::Spam)
13
+ rescue Mollom::Error, SpamFilter::Spam
14
+ false
15
+ end
16
+
17
+ def spam!(comment)
18
+ begin
19
+ if mollom.key_ok? and !comment.mollom_id.empty?
20
+ mollom.send_feedback :session_id => comment.mollom_id, :feedback => 'spam'
21
+ end
22
+ rescue Mollom::Error => e
23
+ raise Comment::AntispamWarning.new(e.to_s)
24
+ end
25
+ end
26
+
27
+ def mollom
28
+ @mollom ||= Mollom.new(:private_key => Radiant::Config['comments.mollom_privatekey'], :public_key => Radiant::Config['comments.mollom_publickey']).tap do |m|
29
+ unless Rails.cache.read('MOLLOM_SERVER_CACHE').blank?
30
+ m.server_list = YAML::load(Rails.cache.read('MOLLOM_SERVER_CACHE'))
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+ def ham?(comment)
37
+ response = mollom.check_content(
38
+ :author_name => comment.author, # author name
39
+ :author_mail => comment.author_email, # author email
40
+ :author_url => comment.author_url, # author url
41
+ :post_body => comment.content # comment text
42
+ )
43
+ comment.mollom_id = response.session_id
44
+ save_mollom_servers
45
+ response.ham?
46
+ end
47
+
48
+ def save_mollom_servers
49
+ Rails.cache.write('MOLLOM_SERVER_CACHE', mollom.server_list.to_yaml) if mollom.key_ok?
50
+ rescue Mollom::Error #TODO: something with this error...
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ # A simple challenge-response spam filter
2
+ class SimpleSpamFilter < SpamFilter
3
+ def message
4
+ if required?
5
+ 'Comments are protected from spam by a simple challenge/response field. For more robust spam filtering, try <a href="http://mollom.com">Mollom</a> or <a href="http://akismet.com/">Akismet</a>.'
6
+ else
7
+ 'You have 3 built-in options for spam protection although currently comments are not automatically protected. Install <a href="http://mollom.com">Mollom</a> or <a href="http://akismet.com/">Akismet</a> to protect against comment spam through an external service, or use the &lt;r:comments:spam_answer_tag /&gt;. Instructions may be found in the README.'
8
+ end
9
+ end
10
+
11
+ def configured?
12
+ true
13
+ end
14
+
15
+ # Instead of filtering at the approval stage, the simple spam filter requires
16
+ # the user to give the correct answer before saving the record.
17
+ def valid?(comment)
18
+ if !required? || comment.valid_spam_answer == hashed_spam_answer(comment)
19
+ true
20
+ else
21
+ comment.errors.add :spam_answer, "is not correct."
22
+ false
23
+ end
24
+ end
25
+
26
+ def approved?(comment)
27
+ true
28
+ end
29
+
30
+ def required?
31
+ Radiant::Config['comments.simple_spam_filter_required?']
32
+ end
33
+
34
+ private
35
+ def hashed_spam_answer(comment)
36
+ Digest::MD5.hexdigest(comment.spam_answer.to_s.to_slug)
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ unless Array.instance_methods.include?('without')
2
+ class Array
3
+ def without(object)
4
+ self.dup.tap do |new_array|
5
+ new_array.delete(object)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ class SpamFilter
12
+ include Simpleton
13
+
14
+ def message
15
+ raise NotImplementedError, 'spam filter subclasses should implement this method'
16
+ end
17
+
18
+ def select
19
+ # Make sure Simple filter comes last, as a fallback
20
+ filters = SpamFilter.descendants.without(SimpleSpamFilter) << SimpleSpamFilter
21
+ filters.find {|filter| filter.try(:configured?) }
22
+ end
23
+
24
+ def approved?(comment)
25
+ raise NotImplementedError, "spam filter subclasses should implement this method"
26
+ end
27
+
28
+ def spam!(comment)
29
+ # This is only implemented in filters that accept feedback like Mollom
30
+ end
31
+
32
+ def configured?
33
+ false
34
+ end
35
+
36
+ # By default, let comments save to the database. Then they can be approved
37
+ # manually or auto-approved by the filter.
38
+ def valid?(comment)
39
+ true
40
+ end
41
+
42
+ class Spam < ::StandardError; end
43
+ end
@@ -0,0 +1,34 @@
1
+ <tr id="<%= dom_id(comment) %>" class="comment<%= " approved" if comment.approved? %>">
2
+ <td class="content" <% if comment.content.size >= 70 -%>title="Click to toggle complete text"<% end-%>>
3
+ <blockquote class="short"><%= escape_once(truncate(comment.content, :length => 70)) %></blockquote>
4
+ <% if comment.content.size >= 70 %>
5
+ <blockquote class="expanded" style="display:none"><%= escape_once(comment.content) %></blockquote>
6
+ <% end %>
7
+ </td>
8
+ <td class="date">
9
+ <%= comment.created_at.strftime("%b %e, %Y at %I:%M%p") %>
10
+ </td>
11
+ <td class="author">
12
+ by <%= escape_once(comment.author) %>
13
+ <% unless comment.author_email.blank? %><%= mail_to(comment.author_email, image_tag("admin/email.png"))%><% end %>
14
+ <% unless comment.author_url.blank? %><%= link_to(image_tag("admin/link.png"), comment.author_url) %><% end %>
15
+ </td>
16
+ <% unless @page %>
17
+ <td class="page">
18
+ on
19
+ <%= link_to truncate(comment.page.title, :length => 40), comment.page.url, :class => 'view-page' %>
20
+ <%= link_to image_tag("admin/page_white_edit.png"),
21
+ edit_admin_page_path(comment.page), :title => "Edit page" %>
22
+ </td>
23
+ <% end %>
24
+ <td class="controls">
25
+ <%= link_to(image_tag('admin/accept.png'), approve_admin_comment_path(comment),
26
+ :title => "Approve comment", :method => :put) unless comment.approved? %>
27
+ <%= link_to(image_tag('admin/error.png'), unapprove_admin_comment_path(comment),
28
+ :method => :put, :title => "Unapprove comment") if comment.approved? %>
29
+ <%= link_to image_tag("admin/delete.png"), admin_comment_path(comment),
30
+ :method => :delete, :confirm => "Are you sure you want to delete this comment?", :title => "Delete comment" %>
31
+ <%= link_to image_tag("admin/comment_edit.png"), edit_admin_comment_path(comment),
32
+ :title => "Edit comment" %>
33
+ </td>
34
+ </tr>
@@ -0,0 +1,36 @@
1
+ <% if Comment.spam_filter == SimpleSpamFilter %>
2
+ <div style="display:none">
3
+ <%= f.hidden_field :spam_answer, :value => "hemidemisemiquaver" %>
4
+ <%= f.hidden_field :valid_spam_answer, :value => Digest::MD5.hexdigest("hemidemisemiquaver") %>
5
+ </div>
6
+ <% end %>
7
+ <div id="comment_form_container" class="form-area">
8
+ <div class="row">
9
+ <p>
10
+ <label for="comment_author">Author</label><br/>
11
+ <%= f.text_field :author %>
12
+ </p>
13
+ <p>
14
+ <label for="comment_author_url">URL</label><br/>
15
+ <%= f.text_field :author_url %>
16
+ </p>
17
+ </div>
18
+ <br style="clear: both" />
19
+ <p>
20
+ <label for="comment_author_email">Email</label><br/>
21
+ <%= f.text_field :author_email %>
22
+ </p>
23
+ <p>
24
+ <label for="comment_content">Comment</label><br />
25
+ <%= f.text_area :content, :rows => 10, :cols => 72, :class => "textarea"%>
26
+ </p>
27
+ <p>
28
+ <label for="comment_filter_id">Filter</label>
29
+ <%= f.select :filter_id, [['<none>', '']] + TextFilter.descendants.map { |s| s.filter_name }.sort %>
30
+ </p>
31
+ </div>
32
+ <p class="buttons">
33
+ <%= save_model_button(@comment) %>
34
+ or
35
+ <%= link_to "Cancel", :back %>
36
+ </p>