radiant-comments-extension 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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>