despamilator 2.0.1 → 2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -0
- data/lib/despamilator.rb +38 -7
- data/lib/despamilator/filter.rb +39 -23
- data/lib/despamilator/filter/gtubs_test_filter.rb +4 -4
- data/lib/despamilator/filter/html_tags.rb +9 -7
- data/lib/despamilator/filter/ip_address_url.rb +6 -4
- data/lib/despamilator/filter/long_words.rb +7 -5
- data/lib/despamilator/filter/mixed_case.rb +21 -0
- data/lib/despamilator/filter/naughty_words.rb +5 -5
- data/lib/despamilator/filter/numbers_and_words.rb +19 -11
- data/lib/despamilator/filter/obfuscated_urls.rb +41 -0
- data/lib/despamilator/filter/prices.rb +19 -0
- data/lib/despamilator/filter/script_tag.rb +4 -4
- data/lib/despamilator/filter/shouting.rb +9 -6
- data/lib/despamilator/filter/spammy_tlds.rb +22 -0
- data/lib/despamilator/filter/square_brackets.rb +5 -5
- data/lib/despamilator/filter/trailing_number.rb +4 -4
- data/lib/despamilator/filter/unusual_characters.rb +5 -5
- data/lib/despamilator/filter/urls.rb +7 -9
- data/lib/despamilator/filter/very_long_domain_name.rb +27 -0
- data/lib/despamilator/filter/weird_punctuation.rb +44 -0
- data/lib/despamilator/subject.rb +30 -0
- data/lib/despamilator/subject/text.rb +32 -0
- data/lib/despamilator/version.rb +3 -0
- metadata +29 -75
- data/.rspec +0 -2
- data/.rvmrc +0 -1
- data/Gemfile +0 -12
- data/Gemfile.lock +0 -47
- data/Manifest.txt +0 -46
- data/PostInstall.txt +0 -1
- data/Rakefile +0 -39
- data/conf/unusual_characters.txt +0 -6674
- data/despamilator.gemspec +0 -38
- data/lib/despamilator/filter_base.rb +0 -82
- data/scripts/despamilator_score.rb +0 -25
- data/scripts/from_file.rb +0 -26
- data/spec/despamilator_spec.rb +0 -13
- data/spec/filter_base_spec.rb +0 -30
- data/spec/filters/gtubs_test_filter_spec.rb +0 -9
- data/spec/filters/html_tags_spec.rb +0 -129
- data/spec/filters/ip_address_url_spec.rb +0 -11
- data/spec/filters/long_words_spec.rb +0 -11
- data/spec/filters/naughty_words_spec.rb +0 -11
- data/spec/filters/numbers_and_words_spec.rb +0 -34
- data/spec/filters/script_tag_spec.rb +0 -22
- data/spec/filters/shouting_spec.rb +0 -45
- data/spec/filters/square_brackets_spec.rb +0 -11
- data/spec/filters/trailing_number_spec.rb +0 -10
- data/spec/filters/unusual_characters_spec.rb +0 -9
- data/spec/filters/urls_spec.rb +0 -11
- data/spec/helpers/corpus_helper.rb +0 -5
- data/spec/helpers/filter_helper.rb +0 -59
- data/spec/helpers/spec_helper.rb +0 -6
- data/tasks/test.rake +0 -6
data/despamilator.gemspec
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
|
3
|
-
Gem::Specification.new do |s|
|
4
|
-
s.name = %q{despamilator}
|
5
|
-
s.version = "2.0.1"
|
6
|
-
|
7
|
-
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
-
s.authors = ["Stephen Hardisty"]
|
9
|
-
s.date = %q{2011-08-11}
|
10
|
-
s.description = %q{Despamilator is a plugin based spam detector designed for use on your web forms borne out of two annoyances:
|
11
|
-
Spam being submitted in my web forms and CAPTCHAS being intrusive. Despamilator will apply
|
12
|
-
some commonly used heuristics from the world of anti-spam to help you decide whether your users are human or machine.}
|
13
|
-
s.email = ["moowahaha@hotmail.com"]
|
14
|
-
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "PostInstall.txt", "conf/unusual_characters.txt"]
|
15
|
-
s.files = [".rspec", ".rvmrc", "Gemfile", "Gemfile.lock", "History.txt", "Manifest.txt", "PostInstall.txt", "README.rdoc", "Rakefile", "conf/unusual_characters.txt", "despamilator.gemspec", "lib/despamilator.rb", "lib/despamilator/filter.rb", "lib/despamilator/filter/gtubs_test_filter.rb", "lib/despamilator/filter/html_tags.rb", "lib/despamilator/filter/ip_address_url.rb", "lib/despamilator/filter/long_words.rb", "lib/despamilator/filter/naughty_words.rb", "lib/despamilator/filter/numbers_and_words.rb", "lib/despamilator/filter/script_tag.rb", "lib/despamilator/filter/shouting.rb", "lib/despamilator/filter/square_brackets.rb", "lib/despamilator/filter/trailing_number.rb", "lib/despamilator/filter/unusual_characters.rb", "lib/despamilator/filter/urls.rb", "lib/despamilator/filter_base.rb", "scripts/despamilator_score.rb", "scripts/from_file.rb", "spec/despamilator_spec.rb", "spec/filter_base_spec.rb", "spec/filters/gtubs_test_filter_spec.rb", "spec/filters/html_tags_spec.rb", "spec/filters/ip_address_url_spec.rb", "spec/filters/long_words_spec.rb", "spec/filters/naughty_words_spec.rb", "spec/filters/numbers_and_words_spec.rb", "spec/filters/script_tag_spec.rb", "spec/filters/shouting_spec.rb", "spec/filters/square_brackets_spec.rb", "spec/filters/trailing_number_spec.rb", "spec/filters/unusual_characters_spec.rb", "spec/filters/urls_spec.rb", "spec/helpers/corpus_helper.rb", "spec/helpers/filter_helper.rb", "spec/helpers/spec_helper.rb", "tasks/test.rake"]
|
16
|
-
s.homepage = %q{http://github.com/moowahaha/despamilator}
|
17
|
-
s.post_install_message = %q{PostInstall.txt}
|
18
|
-
s.rdoc_options = ["--main", "README.rdoc"]
|
19
|
-
s.require_paths = ["lib"]
|
20
|
-
s.rubyforge_project = %q{despamilator}
|
21
|
-
s.rubygems_version = %q{1.5.2}
|
22
|
-
s.summary = %q{Despamilator is a plugin based spam detector designed for use on your web forms borne out of two annoyances: Spam being submitted in my web forms and CAPTCHAS being intrusive}
|
23
|
-
|
24
|
-
if s.respond_to? :specification_version then
|
25
|
-
s.specification_version = 3
|
26
|
-
|
27
|
-
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
28
|
-
s.add_development_dependency(%q<rubyforge>, [">= 2.0.4"])
|
29
|
-
s.add_development_dependency(%q<hoe>, [">= 2.7.0"])
|
30
|
-
else
|
31
|
-
s.add_dependency(%q<rubyforge>, [">= 2.0.4"])
|
32
|
-
s.add_dependency(%q<hoe>, [">= 2.7.0"])
|
33
|
-
end
|
34
|
-
else
|
35
|
-
s.add_dependency(%q<rubyforge>, [">= 2.0.4"])
|
36
|
-
s.add_dependency(%q<hoe>, [">= 2.7.0"])
|
37
|
-
end
|
38
|
-
end
|
@@ -1,82 +0,0 @@
|
|
1
|
-
class Despamilator
|
2
|
-
|
3
|
-
#This class is the base class of all the despamilator filters.
|
4
|
-
#
|
5
|
-
#== EXAMPLE:
|
6
|
-
#
|
7
|
-
#This example is to detect the letter "a". Put the code in
|
8
|
-
#lib/despamilator/filter/detect_letter_a.rb:
|
9
|
-
#
|
10
|
-
# require 'despamilator/filter_base'
|
11
|
-
#
|
12
|
-
# module DespamilatorFilter
|
13
|
-
#
|
14
|
-
# class DetectLetterA < Despamilator::FilterBase
|
15
|
-
#
|
16
|
-
# def name
|
17
|
-
# 'Detecting the letter A'
|
18
|
-
# end
|
19
|
-
#
|
20
|
-
# def description
|
21
|
-
# 'Detects the letter "a" in a string for no reason other than a demo'
|
22
|
-
# end
|
23
|
-
#
|
24
|
-
# def parse text
|
25
|
-
# if text.downcase.scan(/a/)
|
26
|
-
# # add 0.1 to the score of the text
|
27
|
-
# self.append_score = 0.1
|
28
|
-
# end
|
29
|
-
# end
|
30
|
-
# end
|
31
|
-
|
32
|
-
class FilterBase
|
33
|
-
attr_accessor :text, :score, :matches
|
34
|
-
|
35
|
-
# Constructor for the class.
|
36
|
-
|
37
|
-
def initialize
|
38
|
-
@matches = 0
|
39
|
-
@score = 0
|
40
|
-
@matched = false
|
41
|
-
end
|
42
|
-
|
43
|
-
# Returns the score the filter instance has calculated.
|
44
|
-
|
45
|
-
def score
|
46
|
-
# workaround for a bug in ruby 1.9.2's floats
|
47
|
-
sprintf("%.3f", @score).to_f
|
48
|
-
end
|
49
|
-
|
50
|
-
# The nice description of the filter. Usually no more than a sentence.
|
51
|
-
|
52
|
-
def description
|
53
|
-
raise "No description defined for #{self.class}"
|
54
|
-
end
|
55
|
-
|
56
|
-
# This method parses some text. The score is assigned to the same instance.
|
57
|
-
|
58
|
-
def parse text
|
59
|
-
raise "No parse defined for #{self.class}"
|
60
|
-
end
|
61
|
-
|
62
|
-
# The one or two word name for the filter.
|
63
|
-
|
64
|
-
def name
|
65
|
-
raise "No name defined for #{self.class}"
|
66
|
-
end
|
67
|
-
|
68
|
-
# Boolean. Whether or not the filter matched anything.
|
69
|
-
|
70
|
-
def matched?
|
71
|
-
@score > 0
|
72
|
-
end
|
73
|
-
|
74
|
-
protected
|
75
|
-
|
76
|
-
def append_score= new_score
|
77
|
-
@matches += 1
|
78
|
-
@score += new_score
|
79
|
-
end
|
80
|
-
|
81
|
-
end
|
82
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'despamilator')
|
4
|
-
require 'zlib'
|
5
|
-
|
6
|
-
file = ARGV[0] || raise("Usage: despamilator_spec.rb [filename]")
|
7
|
-
|
8
|
-
text = file =~ /\.gz$/i ? Zlib::GzipReader.open(file).read : File.open(file).read
|
9
|
-
|
10
|
-
puts "Testing:"
|
11
|
-
puts "========================"
|
12
|
-
puts text
|
13
|
-
puts "========================"
|
14
|
-
puts "\n"
|
15
|
-
|
16
|
-
dspam = Despamilator.new(text)
|
17
|
-
|
18
|
-
puts "Total Score: #{dspam.score}\n\n"
|
19
|
-
|
20
|
-
puts "Matched by..." unless dspam.matched_by.empty?
|
21
|
-
dspam.matched_by.each do |match|
|
22
|
-
puts "\tFilter: #{match.name}"
|
23
|
-
puts "\tScore: #{match.score}"
|
24
|
-
puts "\n"
|
25
|
-
end
|
data/scripts/from_file.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'cgi'
|
4
|
-
|
5
|
-
slots = {};
|
6
|
-
|
7
|
-
1.upto(10000) do |i|
|
8
|
-
slots[i] = true
|
9
|
-
end
|
10
|
-
|
11
|
-
dir = './spec/spam_corpus/'
|
12
|
-
|
13
|
-
Dir[dir + '*.gz'].each do |file|
|
14
|
-
slots.delete(file.scan(/\d+/).first.to_i)
|
15
|
-
end
|
16
|
-
|
17
|
-
slots = slots.keys.sort
|
18
|
-
|
19
|
-
File.open(ARGV[0] || raise).each do |line|
|
20
|
-
txt = dir + "#{slots.shift}.txt"
|
21
|
-
File.open(txt, 'w') do |fh|
|
22
|
-
fh.puts CGI.unescapeHTML(line)
|
23
|
-
end
|
24
|
-
|
25
|
-
`gzip #{txt}`
|
26
|
-
end
|
data/spec/despamilator_spec.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
describe Despamilator do
|
2
|
-
before :each do
|
3
|
-
@dspam = Despamilator.new('this text is absolutely fine')
|
4
|
-
end
|
5
|
-
|
6
|
-
it "should return a zero score for fine text" do
|
7
|
-
@dspam.score.should == 0
|
8
|
-
end
|
9
|
-
|
10
|
-
it "should return no matching filter for fine text" do
|
11
|
-
@dspam.matched_by.should be_empty
|
12
|
-
end
|
13
|
-
end
|
data/spec/filter_base_spec.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
require 'despamilator/filter_base'
|
2
|
-
|
3
|
-
class UnimplementedFilter < Despamilator::FilterBase
|
4
|
-
end
|
5
|
-
|
6
|
-
describe Despamilator::FilterBase do
|
7
|
-
describe "abstract method" do
|
8
|
-
|
9
|
-
before do
|
10
|
-
@filter = UnimplementedFilter.new
|
11
|
-
end
|
12
|
-
|
13
|
-
[
|
14
|
-
['name', 'No name defined for UnimplementedFilter'],
|
15
|
-
['description', 'No description defined for UnimplementedFilter'],
|
16
|
-
|
17
|
-
].each do |method, exception|
|
18
|
-
|
19
|
-
it "should throw an error when the child class has not implemented a '#{method}' method" do
|
20
|
-
-> {@filter.send(method)}.should raise_error(exception)
|
21
|
-
end
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
it "should throw an error when the child class has not implemented a 'parse' method" do
|
26
|
-
-> {@filter.parse('abc')}.should raise_error('No parse defined for UnimplementedFilter')
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
@@ -1,9 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::GtubsTestFilter do
|
2
|
-
the_name_should_be 'GTubs Test Filter'
|
3
|
-
the_description_should_be 'Detects the special test string (Despamilator.gtubs_test_string) and assigns a big score.'
|
4
|
-
|
5
|
-
despamilator_should_apply_the_filter_for(Despamilator.gtubs_test_string)
|
6
|
-
|
7
|
-
a_single_match_of(Despamilator.gtubs_test_string, should_score: 100)
|
8
|
-
a_multiple_match_of(Despamilator.gtubs_test_string, should_score: [100, 1.times])
|
9
|
-
end
|
@@ -1,129 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::HtmlTags do
|
2
|
-
|
3
|
-
the_name_should_be 'HTML tags'
|
4
|
-
the_description_should_be 'Detects HTML tags in text'
|
5
|
-
|
6
|
-
despamilator_should_apply_the_filter_for('<xmp>')
|
7
|
-
|
8
|
-
a_single_match_of('<xmp>', should_score: 0.6)
|
9
|
-
a_multiple_match_of('<h1></h1> <h2></h2>', should_score: [1.2, 2.times])
|
10
|
-
|
11
|
-
[
|
12
|
-
'!--',
|
13
|
-
'!DOCTYPE',
|
14
|
-
'a',
|
15
|
-
'abbr',
|
16
|
-
'acronym',
|
17
|
-
'address',
|
18
|
-
'applet',
|
19
|
-
'area',
|
20
|
-
'b',
|
21
|
-
'base',
|
22
|
-
'basefont',
|
23
|
-
'bdo',
|
24
|
-
'big',
|
25
|
-
'blockquote',
|
26
|
-
'body',
|
27
|
-
'br',
|
28
|
-
'button',
|
29
|
-
'caption',
|
30
|
-
'center',
|
31
|
-
'cite',
|
32
|
-
'code',
|
33
|
-
'col',
|
34
|
-
'colgroup',
|
35
|
-
'dd',
|
36
|
-
'del',
|
37
|
-
'dfn',
|
38
|
-
'dir',
|
39
|
-
'div',
|
40
|
-
'dl',
|
41
|
-
'dt',
|
42
|
-
'em',
|
43
|
-
'fieldset',
|
44
|
-
'font',
|
45
|
-
'form',
|
46
|
-
'frame',
|
47
|
-
'frameset',
|
48
|
-
'h1',
|
49
|
-
'h2',
|
50
|
-
'h3',
|
51
|
-
'h4',
|
52
|
-
'h5',
|
53
|
-
'h6',
|
54
|
-
'head',
|
55
|
-
'hr',
|
56
|
-
'html',
|
57
|
-
'i',
|
58
|
-
'iframe',
|
59
|
-
'img',
|
60
|
-
'input',
|
61
|
-
'ins',
|
62
|
-
'isindex',
|
63
|
-
'kbd',
|
64
|
-
'label',
|
65
|
-
'legend',
|
66
|
-
'li',
|
67
|
-
'link',
|
68
|
-
'map',
|
69
|
-
'menu',
|
70
|
-
'meta',
|
71
|
-
'noframes',
|
72
|
-
'noscript',
|
73
|
-
'object',
|
74
|
-
'ol',
|
75
|
-
'optgroup',
|
76
|
-
'option',
|
77
|
-
'p',
|
78
|
-
'param',
|
79
|
-
'pre',
|
80
|
-
'q',
|
81
|
-
's',
|
82
|
-
'samp',
|
83
|
-
'select',
|
84
|
-
'small',
|
85
|
-
'span',
|
86
|
-
'strike',
|
87
|
-
'strong',
|
88
|
-
'style',
|
89
|
-
'sub',
|
90
|
-
'sup',
|
91
|
-
'table',
|
92
|
-
'tbody',
|
93
|
-
'td',
|
94
|
-
'textarea',
|
95
|
-
'tfoot',
|
96
|
-
'th',
|
97
|
-
'thead',
|
98
|
-
'title',
|
99
|
-
'tr',
|
100
|
-
'tt',
|
101
|
-
'u',
|
102
|
-
'ul',
|
103
|
-
'var',
|
104
|
-
'xmp'
|
105
|
-
].each do |script_tag|
|
106
|
-
[script_tag.upcase, script_tag.downcase].each do |script_tag|
|
107
|
-
[
|
108
|
-
"<#{script_tag}>",
|
109
|
-
"<#{script_tag}/>",
|
110
|
-
"< #{script_tag} >",
|
111
|
-
"<#{script_tag} />",
|
112
|
-
"<\n#{script_tag}\n/>",
|
113
|
-
"<\n#{script_tag} >",
|
114
|
-
"<#{script_tag}\n/>",
|
115
|
-
"<\r#{script_tag}\r/>"
|
116
|
-
].each do |tag|
|
117
|
-
it "should detect '#{tag}'" do
|
118
|
-
dspam = DespamilatorFilter::HtmlTags.new
|
119
|
-
dspam.parse(tag)
|
120
|
-
dspam.score.should == 0.6
|
121
|
-
end
|
122
|
-
|
123
|
-
end
|
124
|
-
|
125
|
-
end
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
end
|
@@ -1,11 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::IPAddressURL do
|
2
|
-
|
3
|
-
the_name_should_be 'IP Address URL'
|
4
|
-
the_description_should_be 'Detects IP address URLs'
|
5
|
-
|
6
|
-
despamilator_should_apply_the_filter_for('http://12.34.56.78/')
|
7
|
-
|
8
|
-
a_single_match_of('http://12.34.56.78/', should_score: 0.5)
|
9
|
-
a_multiple_match_of('http://12.34.56.78/ http://98.76.54.32/', should_score: [0.5, 1.times])
|
10
|
-
|
11
|
-
end
|
@@ -1,11 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::LongWords do
|
2
|
-
|
3
|
-
the_name_should_be 'Long Words'
|
4
|
-
the_description_should_be 'Detects long and unbroken strings'
|
5
|
-
|
6
|
-
despamilator_should_apply_the_filter_for('honorificabilitudinitatibus')
|
7
|
-
|
8
|
-
a_single_match_of('honorificabilitudinitatibus', should_score: 0.1)
|
9
|
-
a_multiple_match_of('honorificabilitudinitatibus antidisestablishmentarianism', should_score: [0.2, 2.times])
|
10
|
-
|
11
|
-
end
|
@@ -1,11 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::NaughtyWords do
|
2
|
-
|
3
|
-
the_name_should_be 'Naughty Words'
|
4
|
-
the_description_should_be 'Detects cheeky words'
|
5
|
-
|
6
|
-
despamilator_should_apply_the_filter_for('bondage')
|
7
|
-
|
8
|
-
a_single_match_of('bondage', should_score: 0.1)
|
9
|
-
a_multiple_match_of('viagra penis', should_score: [0.2, 2.times])
|
10
|
-
|
11
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::NumbersAndWords do
|
2
|
-
|
3
|
-
the_name_should_be 'Numbers next to words'
|
4
|
-
the_description_should_be 'Detects unusual number/word combinations'
|
5
|
-
|
6
|
-
despamilator_should_apply_the_filter_for('X5T')
|
7
|
-
|
8
|
-
a_single_match_of('X5T', should_score: 0.1)
|
9
|
-
a_multiple_match_of('4g6hk', should_score: [0.2, 2.times])
|
10
|
-
|
11
|
-
describe 'exceptions' do
|
12
|
-
before :all do
|
13
|
-
@filter = DespamilatorFilter::NumbersAndWords.new
|
14
|
-
end
|
15
|
-
|
16
|
-
[1, 4, 10, 100000, '1,000,000', '1st', '2nd', '3rd', '4th', '5th', '6th', '10th', '122nd'].each do |number|
|
17
|
-
it "should return a blank for a #{number}" do
|
18
|
-
@filter.parse(number.to_s)
|
19
|
-
@filter.score.should == 0
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
[1, 2, 3, 4, 5, 6].each do |tag_no|
|
24
|
-
header_tag = "h#{tag_no}"
|
25
|
-
|
26
|
-
it "should ignore html header tag #{header_tag}" do
|
27
|
-
@filter.parse(header_tag)
|
28
|
-
@filter.score.should == 0
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
describe DespamilatorFilter::ScriptTag do
|
2
|
-
|
3
|
-
the_name_should_be 'Script tag'
|
4
|
-
the_description_should_be 'Searches for variations for the HTML script tag'
|
5
|
-
|
6
|
-
despamilator_should_apply_the_filter_for('<script>')
|
7
|
-
|
8
|
-
a_single_match_of('<script>', should_score: 1)
|
9
|
-
a_multiple_match_of('<script></script> <script></script>', should_score: [1, 1.times])
|
10
|
-
|
11
|
-
describe "detecting various script tags" do
|
12
|
-
['<script type="whatever">', '<script></script>', '</script>', '<script>', "<script\n>"].each do |script_tag|
|
13
|
-
[script_tag.upcase, script_tag.downcase].each do |script_tag|
|
14
|
-
it "should detect '#{script_tag}' of a script tag" do
|
15
|
-
dspam = Despamilator.new(script_tag)
|
16
|
-
dspam.score.should == 1
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
end
|