acts_as_sanitiled 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ 1.0.0 (10-14-09)
2
+ Initial butchering of defunkt's work.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2006,2009 Chris Wanstrath, Gabe da Silveira
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,141 @@
1
+ = Acts as Sanitiled
2
+
3
+ This plugin, based on Chris Wanstrath's venerable acts_as_textiled, extends the automatic textiling functionality to sanitization as well using as its basis Ryan Grove's powerful yet simple Sanitize gem.
4
+
5
+ The reasoning behind this approach is simple. Filtering input before it is saved to the database (as xss_terminate and many other popular plugins do) often fails to preserve user intent. On the other hand, filtering output at the template level is error prone, and you are begging to get pwned. Short of some sort of taint mode (which Rails 3 will have!), I believe the method employed by acts_as_textiled is the next best thing: you get safe output by default, but input is never corrupted.
6
+
7
+ == Requirements
8
+
9
+ * Sanitize >1.1.0 (prior versions had a whitespace issue)
10
+ * RedCloth (for Textile support)
11
+ * ActiveRecord (tested on 2.3.4)
12
+
13
+ == Changes from acts_as_textiled
14
+
15
+ acts_as_sanitiled mostly maintains the API, but one noticeable difference is that it needs to expose the Sanitize config. Therefore acts_as_textiled use of a hash to provide per-column RedCloth configuration had to be replaced with Sanitize config. RedCloth options can still be passed as an array that applies to all fields listed.
16
+
17
+ The other big change is that acts_as_sanitiled uses Sanitize which outputs utf8 rather than HTML entities. For my own purposes this is preferable anyway, but it might give someone a few headaches getting encoding issues. My advice: take your lumps now and figure out your encoding pipelines.
18
+
19
+ == Usage
20
+
21
+ class Story < ActiveRecord::Base
22
+ acts_as_sanitiled :body_text, :description
23
+ end
24
+
25
+ >> story = Story.find(3)
26
+ => #<Story:0x245fed8 ... >
27
+
28
+ >> story.description
29
+ => "<p>This is <strong>cool</strong>.</p>"
30
+
31
+ >> story.description(:source)
32
+ => "This is *cool*."
33
+
34
+ >> story.description(:plain)
35
+ => "This is cool."
36
+
37
+ >> story.description = "I _know_!"
38
+ => "I _know_!"
39
+
40
+ >> story.save
41
+ => true
42
+
43
+ >> story.description
44
+ => "<p>I <em>know</em>!</p>"
45
+
46
+ >> story.textiled = false
47
+ => false
48
+
49
+ >> story.description
50
+ => "I _know_!"
51
+
52
+ >> story.textiled = true
53
+ => true
54
+
55
+ >> story.description
56
+ => "<p>I <em>know</em>!</p>"
57
+
58
+ == Different Modes
59
+
60
+ Sanitize supports a detailed configuration hash describing what HTML is allowed (among
61
+ other things). This can be passed at the end of the declaration. See the Sanitize docs
62
+ for more information.
63
+
64
+ class Story < ActiveRecord::Base
65
+ acts_as_sanitiled :body_text, :elements => ['em','strong','div'], :attributes => {'div' => ['class','id']}
66
+ end
67
+
68
+ RedCloth supports different modes, such as :lite_mode. To use a mode on
69
+ a specific attribute simply pass one or more options in an array after the field names. Like so:
70
+
71
+ class Story < ActiveRecord::Base
72
+ acts_as_sanitiled :body_text, :description, [ :lite_mode ]
73
+ end
74
+
75
+ Of course you can combine them as well:
76
+
77
+ class Story < ActiveRecord::Base
78
+ acts_as_sanitiled :body_text, :description, [ :lite_mode ], :elements => ['a'], :add_attributes => {'a' => {'rel' => 'nofollow'}}
79
+ end
80
+
81
+ Suppose you want to sanitize but not textilize:
82
+
83
+ class Story < ActiveRecord::Base
84
+ acts_as_sanitized :body_text, :elements => ['br', 'p']
85
+ end
86
+
87
+ Or vice-versa:
88
+
89
+ class Story < ActiveRecord::Base
90
+ acts_as_textilized :body_text, [ :lite_mode ]
91
+ end
92
+
93
+ Get it? Now let's say you have an admin tool and you want the text to be displayed
94
+ in the text boxes / fields as plaintext. Do you have to change all your views?
95
+
96
+ Hell no.
97
+
98
+ == form_for
99
+
100
+ Are you using form_for? If you are, you don't have to change any code at all.
101
+
102
+ <% form_for :story, @story do |f| %>
103
+ Description: <br/> <%= f.text_field :description %>
104
+ <% end %>
105
+
106
+ You'll see the Textile plaintext in the text field. It Just Works.
107
+
108
+ == form tags
109
+
110
+ If you're being a bit unconvential, no worries. You can still get at your
111
+ raw Textile like so:
112
+
113
+ Description: <br/> <%= text_field_tag :description, @story.description(:source) %>
114
+
115
+ And there's always object.textiled = false, as demo'd above.
116
+
117
+ == Pre-fetching
118
+
119
+ acts_as_sanitiled locally caches rendered HTML once the attribute in question has
120
+ been requested. Obviously this doesn't bode well for marshalling or caching.
121
+
122
+ If you need to force your object to build and cache HTML for all textiled attributes,
123
+ call the +textilize+ method on your object.
124
+
125
+ If you're real crazy you can even do something like this:
126
+
127
+ class Story < ActiveRecord::Base
128
+ acts_as_sanitiled :body_text, :description
129
+
130
+ def after_find
131
+ textilize
132
+ end
133
+ end
134
+
135
+ All your Textile will now be ready to go in spiffy HTML format. But you probably
136
+ won't need to do this.
137
+
138
+ Enjoy.
139
+
140
+ * By Chris Wanstrath [ chris[at]ozmm[dot]org ]
141
+ * Butchered and Sanitized by Gabe da Silveira [ gabe[at]websaviour[dot]com ]
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "acts_as_sanitiled"
8
+ gem.summary = %Q{Automatically textiles and/or sanitizes ActiveRecord columns}
9
+ gem.description = %Q{A modernized version of Chris Wansthrath's venerable acts_as_textiled. It automatically textiles and then sanitizes columns to your specification. Ryan Grove's excellent Sanitize gem with nokogiri provides the backend for speedy and robust filtering of your output in order to: restrict Textile to a subset of HTML, guarantee well-formedness, and of course prevent XSS.}
10
+ gem.email = "gabe@websaviour.com"
11
+ gem.homepage = "http://github.com/dasil003/acts_as_sanitiled"
12
+ gem.authors = ["Gabe da Silveira"]
13
+
14
+ gem.add_dependency('nokogiri', '~> 1.3.3')
15
+ gem.add_dependency('sanitize', '~> 1.1.0')
16
+ gem.add_dependency('RedCloth')
17
+
18
+ gem.add_development_dependency "bacon"
19
+ gem.add_development_dependency "activesupport"
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
24
+ end
25
+
26
+ require 'rake/testtask'
27
+ Rake::TestTask.new(:spec) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.verbose = true
31
+ end
32
+
33
+ begin
34
+ require 'rcov/rcovtask'
35
+ Rcov::RcovTask.new do |spec|
36
+ spec.libs << 'spec'
37
+ spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.verbose = true
39
+ end
40
+ rescue LoadError
41
+ task :rcov do
42
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
43
+ end
44
+ end
45
+
46
+ task :spec => :check_dependencies
47
+
48
+ task :default => :spec
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ if File.exist?('VERSION')
53
+ version = File.read('VERSION')
54
+ else
55
+ version = ""
56
+ end
57
+
58
+ rdoc.rdoc_dir = 'rdoc'
59
+ rdoc.title = "acts_as_sanitiled #{version}"
60
+ rdoc.rdoc_files.include('README*')
61
+ rdoc.rdoc_files.include('lib/**/*.rb')
62
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,111 @@
1
+ require 'rubygems'
2
+ require 'sanitize'
3
+ require 'RedCloth'
4
+
5
+ module ActsAsSanitiled #:nodoc: all
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_textiled(*attributes)
12
+ raise "only acts_as_sanitized or acts_as_sanitiled can take an options hash" if attributes.last.is_a?(Hash)
13
+
14
+ attributes << {:skip_sanitize => true}
15
+ acts_as_sanitiled(*attributes)
16
+ end
17
+
18
+ def acts_as_sanitized(*attributes)
19
+ options = attributes.last.is_a?(Hash) ? attributes.pop : {}
20
+ options[:skip_textile] = true
21
+ attributes << options
22
+ acts_as_sanitiled(*attributes)
23
+ end
24
+
25
+ def acts_as_sanitiled(*attributes)
26
+ @textiled_attributes ||= []
27
+
28
+ @textiled_unicode = String.new.respond_to? :chars
29
+
30
+ options = attributes.last.is_a?(Hash) ? attributes.pop : {}
31
+ skip_textile = options.delete(:skip_textile)
32
+ skip_sanitize = options.delete(:skip_sanitize)
33
+
34
+ raise 'Both textile and sanitize were skipped' if skip_textile && skip_sanitize
35
+
36
+ sanitize_options = options.empty? ? Sanitize::Config::RELAXED : options
37
+ red_cloth_options = attributes.last && attributes.last.is_a?(Array) ? attributes.pop : []
38
+
39
+ raise 'No attributes were specified to filter' if attributes.empty?
40
+
41
+ type_options = %w( plain source )
42
+
43
+ attributes.each do |attribute|
44
+ define_method(attribute) do |*type|
45
+ type = type.first
46
+
47
+ if type.nil? && self[attribute]
48
+ if textiled[attribute.to_s].nil?
49
+ string = self[attribute]
50
+ string = RedCloth.new(string, red_cloth_options).to_html unless skip_textile
51
+ string = Sanitize.clean(string, sanitize_options) unless skip_sanitize
52
+ textiled[attribute.to_s] = string
53
+ end
54
+ textiled[attribute.to_s]
55
+ elsif type.nil? && self[attribute].nil?
56
+ nil
57
+ elsif type_options.include?(type.to_s)
58
+ send("#{attribute}_#{type}")
59
+ else
60
+ raise "I don't understand the `#{type}' option. Try #{type_options.join(' or ')}."
61
+ end
62
+ end
63
+
64
+ define_method("#{attribute}_plain", proc { strip_html(__send__(attribute)) if __send__(attribute) } )
65
+ define_method("#{attribute}_source", proc { __send__("#{attribute}_before_type_cast") } )
66
+
67
+ @textiled_attributes << attribute
68
+ end
69
+
70
+ include ActsAsSanitiled::InstanceMethods
71
+ end
72
+
73
+ def textiled_attributes
74
+ Array(@textiled_attributes)
75
+ end
76
+ end
77
+
78
+ module InstanceMethods
79
+ def textiled
80
+ textiled? ? (@textiled ||= {}) : @attributes.dup
81
+ end
82
+
83
+ def textiled?
84
+ @is_textiled != false
85
+ end
86
+
87
+ def textiled=(bool)
88
+ @is_textiled = !!bool
89
+ end
90
+
91
+ def textilize
92
+ self.class.textiled_attributes.each { |attr| __send__(attr) }
93
+ end
94
+
95
+ def reload
96
+ textiled.clear
97
+ super
98
+ end
99
+
100
+ def write_attribute(attr_name, value)
101
+ textiled[attr_name.to_s] = nil
102
+ super
103
+ end
104
+
105
+ private
106
+ def strip_html(html)
107
+ html.gsub!(%r{</p>\n<p>}, "</p>\n\n<p>") # Workaround RedCloth 4.2.x issue
108
+ Nokogiri::HTML::DocumentFragment.parse(html).inner_text
109
+ end
110
+ end
111
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ ActiveRecord::Base.send(:include, ActsAsSanitiled)
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'New object with textiled description' do
4
+ before do
5
+ @story = Story.new
6
+ end
7
+
8
+ it "should have nil description" do
9
+ @story.description.should.be.nil
10
+ end
11
+
12
+ it "should have nil description source" do
13
+ @story.description_source.should.be.nil
14
+ end
15
+
16
+ it "should have nil description plain" do
17
+ @story.description_plain.should.be.nil
18
+ end
19
+ end
20
+
21
+ describe 'A standard textiled object' do
22
+ before do
23
+ @desc_textile = '_why announces __Sandbox__'
24
+ @body_textile = <<EOF
25
+ First line
26
+ Second line with *bold*
27
+
28
+ Second paragraph with special char(TM), <a href="javascript:alert('shit')">XSS attribute</a>,
29
+ script>script tag</script>, and <b>unclosed tag.
30
+ EOF
31
+
32
+ @story = Story.new(
33
+ :title => 'The Thrilling Freaky-Freaky Sandbox Hack',
34
+ :description => @desc_textile,
35
+ :body => @body_textile)
36
+
37
+ @desc_html = '_why announces <i>Sandbox</i>'
38
+ @desc_plain = '_why announces Sandbox'
39
+
40
+ @body_html = "<p>First line<br />\nSecond line with <strong>bold</strong></p>\n<p>Second paragraph with special char\342\204\242, <a>XSS attribute</a>,<br />\nscript&gt;script tag, and <b>unclosed tag.</b></p>"
41
+ @body_plain = "First line\nSecond line with bold\n\nSecond paragraph with special char™, XSS attribute,\nscript>script tag, and unclosed tag."
42
+ end
43
+
44
+ it "should properly textilize and strip html" do
45
+ @story.description.should.equal @desc_html
46
+ @story.description(:source).should.equal @desc_textile
47
+ @story.description(:plain).should.equal @desc_plain
48
+
49
+ @story.body.should.equal @body_html
50
+ @story.body(:source).should.equal @body_textile
51
+ @story.body(:plain).should.equal @body_plain
52
+ end
53
+
54
+ it "should raise when given a non-sensical option" do
55
+ proc{ @story.description(:cassadaga) }.should.raise
56
+ end
57
+
58
+ it "should pick up changes to attributes" do
59
+ @story.description.should.equal @desc_html
60
+
61
+ @story.description = "**IRb** is simple"
62
+ @story.description.should.equal "<b>IRb</b> is simple"
63
+ @story.description(:plain).should.equal "IRb is simple"
64
+ end
65
+
66
+ it "should be able to toggle whether textile is active or not" do
67
+ @story.description.should.equal @desc_html
68
+ @story.textiled = false
69
+ @story.description.should.equal @desc_textile
70
+ end
71
+
72
+ it "should be able to do on-demand textile caching" do
73
+ @story.textiled.size.should.equal 0
74
+ @story.textilize
75
+ @story.textiled.size.should.equal 2
76
+ @story.description.should.equal @desc_html
77
+ end
78
+
79
+ it "should clear textiled hash on reload" do
80
+ @story.textilize
81
+ @story.textiled.size.should.equal 2
82
+ @story.reload
83
+ @story.textiled.size.should.equal 0
84
+ end
85
+ end
86
+
87
+ describe 'An object with one textiled and one sanitized field' do
88
+ before do
89
+ @author = Author.new(
90
+ :name => '<b>King *George*</p>',
91
+ :bio => '*Bold* but with <script>')
92
+ end
93
+
94
+ it "should sanitize but not textilize name" do
95
+ @author.name.should.equal '<b>King *George*</b>'
96
+ end
97
+
98
+ it "should textilize but not sanitize bio" do
99
+ @author.bio.should.equal '<p><strong>Bold</strong> but with <script></p>'
100
+ end
101
+ end
102
+
103
+ describe 'Defining fields on an ActiveRecord object' do
104
+ it "should not allow both skip_textile and skip_sanitize" do
105
+ proc do
106
+ class Foo < ActiveRecord::Base
107
+ acts_as_sanitiled :body, :skip_sanitize => true, :skip_textile => true
108
+ end
109
+ end.should.raise
110
+ end
111
+
112
+ it "should not allow options hash on acts_as_textiled" do
113
+ proc do
114
+ class Foo < ActiveRecord::Base
115
+ acts_as_textiled :body, :option => :is_verboten
116
+ end
117
+ end.should.raise
118
+ end
119
+ end
@@ -0,0 +1,43 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'acts_as_sanitiled'
5
+ require 'bacon'
6
+ require 'active_support'
7
+
8
+ class ActiveRecord
9
+ class Base
10
+ attr_reader :attributes
11
+
12
+ def initialize(attributes = {})
13
+ @attributes = attributes.dup.stringify_keys
14
+ end
15
+
16
+ def method_missing(name, *args)
17
+ if name.to_s[%r{=}]
18
+ @attributes[key = name.to_s.sub('=','')] = value = args.first
19
+ write_attribute key, value
20
+ else
21
+ self[name.to_s]
22
+ end
23
+ end
24
+
25
+ def [](value)
26
+ @attributes[value.to_s.sub('_before_type_cast', '')]
27
+ end
28
+ end
29
+ end unless defined? ActiveRecord
30
+
31
+ ActiveRecord::Base.send(:include, ActsAsSanitiled)
32
+
33
+ class Story < ActiveRecord::Base
34
+ acts_as_sanitiled :body
35
+ acts_as_sanitiled :description, [:lite_mode]
36
+ end
37
+
38
+ class Author < ActiveRecord::Base
39
+ acts_as_sanitized :name, :elements => ['b','em']
40
+ acts_as_textiled :bio
41
+ end
42
+
43
+ Bacon.summary_on_exit
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_sanitiled
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabe da Silveira
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nokogiri
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 1.3.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: sanitize
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: RedCloth
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: bacon
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ type: :development
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ description: "A modernized version of Chris Wansthrath's venerable acts_as_textiled. It automatically textiles and then sanitizes columns to your specification. Ryan Grove's excellent Sanitize gem with nokogiri provides the backend for speedy and robust filtering of your output in order to: restrict Textile to a subset of HTML, guarantee well-formedness, and of course prevent XSS."
66
+ email: gabe@websaviour.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files:
72
+ - LICENSE
73
+ - README.rdoc
74
+ files:
75
+ - CHANGELOG
76
+ - LICENSE
77
+ - README.rdoc
78
+ - Rakefile
79
+ - VERSION
80
+ - lib/acts_as_sanitiled.rb
81
+ - rails/init.rb
82
+ - spec/sanitiled_spec.rb
83
+ - spec/spec_helper.rb
84
+ has_rdoc: true
85
+ homepage: http://github.com/dasil003/acts_as_sanitiled
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options:
90
+ - --charset=UTF-8
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: "0"
98
+ version:
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: "0"
104
+ version:
105
+ requirements: []
106
+
107
+ rubyforge_project:
108
+ rubygems_version: 1.3.5
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Automatically textiles and/or sanitizes ActiveRecord columns
112
+ test_files:
113
+ - spec/sanitiled_spec.rb
114
+ - spec/spec_helper.rb