miketracy-wwmd 0.2.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/History.txt +3 -0
  2. data/README +62 -0
  3. data/README.txt +62 -0
  4. data/Rakefile +34 -0
  5. data/examples/config_example.yaml +24 -0
  6. data/examples/wwmd_example.rb +73 -0
  7. data/lib/wwmd.rb +78 -0
  8. data/lib/wwmd/encoding.rb +40 -0
  9. data/lib/wwmd/form.rb +110 -0
  10. data/lib/wwmd/form_array.rb +273 -0
  11. data/lib/wwmd/guid.rb +155 -0
  12. data/lib/wwmd/hpricot_html2text.rb +76 -0
  13. data/lib/wwmd/mixins.rb +318 -0
  14. data/lib/wwmd/mixins_extends.rb +188 -0
  15. data/lib/wwmd/mixins_external.rb +18 -0
  16. data/lib/wwmd/nokogiri_html2text.rb +41 -0
  17. data/lib/wwmd/page.rb +414 -0
  18. data/lib/wwmd/page/auth.rb +183 -0
  19. data/lib/wwmd/page/config.rb +44 -0
  20. data/lib/wwmd/page/constants.rb +60 -0
  21. data/lib/wwmd/page/headers.rb +107 -0
  22. data/lib/wwmd/page/inputs.rb +47 -0
  23. data/lib/wwmd/page/irb_helpers.rb +90 -0
  24. data/lib/wwmd/page/scrape.rb +202 -0
  25. data/lib/wwmd/page/spider.rb +127 -0
  26. data/lib/wwmd/page/urlparse.rb +79 -0
  27. data/lib/wwmd/page/utils.rb +30 -0
  28. data/lib/wwmd/viewstate.rb +118 -0
  29. data/lib/wwmd/viewstate/viewstate_class_helpers.rb +35 -0
  30. data/lib/wwmd/viewstate/viewstate_deserializer_methods.rb +213 -0
  31. data/lib/wwmd/viewstate/viewstate_from_xml.rb +126 -0
  32. data/lib/wwmd/viewstate/viewstate_types.rb +51 -0
  33. data/lib/wwmd/viewstate/viewstate_utils.rb +157 -0
  34. data/lib/wwmd/viewstate/viewstate_yaml.rb +25 -0
  35. data/lib/wwmd/viewstate/vs_array.rb +36 -0
  36. data/lib/wwmd/viewstate/vs_binary_serialized.rb +28 -0
  37. data/lib/wwmd/viewstate/vs_hashtable.rb +40 -0
  38. data/lib/wwmd/viewstate/vs_hybrid_dict.rb +40 -0
  39. data/lib/wwmd/viewstate/vs_indexed_string.rb +6 -0
  40. data/lib/wwmd/viewstate/vs_indexed_string_ref.rb +22 -0
  41. data/lib/wwmd/viewstate/vs_int_enum.rb +25 -0
  42. data/lib/wwmd/viewstate/vs_list.rb +32 -0
  43. data/lib/wwmd/viewstate/vs_pair.rb +27 -0
  44. data/lib/wwmd/viewstate/vs_read_types.rb +11 -0
  45. data/lib/wwmd/viewstate/vs_read_value.rb +33 -0
  46. data/lib/wwmd/viewstate/vs_sparse_array.rb +56 -0
  47. data/lib/wwmd/viewstate/vs_string.rb +29 -0
  48. data/lib/wwmd/viewstate/vs_string_array.rb +37 -0
  49. data/lib/wwmd/viewstate/vs_string_formatted.rb +30 -0
  50. data/lib/wwmd/viewstate/vs_triplet.rb +29 -0
  51. data/lib/wwmd/viewstate/vs_type.rb +21 -0
  52. data/lib/wwmd/viewstate/vs_unit.rb +28 -0
  53. data/lib/wwmd/viewstate/vs_value.rb +33 -0
  54. data/spec/README +3 -0
  55. data/spec/form_array.spec +49 -0
  56. data/spec/spider_csrf_test.spec +28 -0
  57. data/spec/urlparse_test.spec +89 -0
  58. data/tasks/ann.rake +80 -0
  59. data/tasks/bones.rake +20 -0
  60. data/tasks/gem.rake +201 -0
  61. data/tasks/git.rake +40 -0
  62. data/tasks/notes.rake +27 -0
  63. data/tasks/post_load.rake +34 -0
  64. data/tasks/rdoc.rake +51 -0
  65. data/tasks/rubyforge.rake +55 -0
  66. data/tasks/setup.rb +292 -0
  67. data/tasks/spec.rake +54 -0
  68. data/tasks/test.rake +40 -0
  69. data/tasks/zentest.rake +36 -0
  70. metadata +164 -0
@@ -0,0 +1,202 @@
1
+ module WWMD
2
+ LINKS_REGEXP = [
3
+ /window\.open\s*\(([^\)]+)/i,
4
+ /open_window\s*\(([^\)]+)/i,
5
+ /window\.location\s*=\s*(['"][^'"]+['"])/i,
6
+ /.*location.href\s*=\s*(['"][^'"]+['"])/i,
7
+ /document.forms.*action\s*=\s*(['"][^'"]+['"])/i,
8
+ /Ajax\.Request\s*\((['"][^'"]+['"])/i,
9
+ ]
10
+
11
+ AJAX_REGEXP = [
12
+ /Ajax\.Request\s*\((['"][^'"]+['"])/i,
13
+ ]
14
+
15
+ SRC_REGEXP = [
16
+ /src=\s*(['"][^'"]+['"])/i
17
+ ]
18
+
19
+ # NOT_URL_CHAR = "[^0-9a-zA-Z\:\/\+\\-\%\#]"
20
+
21
+ class Scrape
22
+
23
+ attr_accessor :debug
24
+ attr_accessor :warn
25
+ attr_accessor :links # links found on page
26
+ attr_accessor :jlinks # links to javascript includes
27
+
28
+ attr_reader :hdoc
29
+
30
+ @debug = false
31
+ @warn = true
32
+
33
+ # create a new scrape object using passed HTML
34
+ def initialize(page='<>')
35
+ @page = page
36
+ @hdoc = HDOC.parse(@page)
37
+ @links = Array.new
38
+ @debug = false
39
+ @warn = true
40
+ end
41
+
42
+ # reset this scrape object (called by WWMD::Page)
43
+ def reset(page)
44
+ @page = page
45
+ @hdoc = HDOC.parse(@page)
46
+ @links = Array.new
47
+ end
48
+
49
+ # scan the passed string for the configured regular expressions
50
+ # and return them as an array
51
+ def urls_from_regexp(content,re,split=0)
52
+ ret = []
53
+ scrape = content.scan(re)
54
+ scrape.each do |url|
55
+ # cheat and take split string(,)[split]
56
+ add = url.to_s.split(',')[split].gsub(/['"]/, '')
57
+ next if (add == '' || add.nil?)
58
+ ret << add
59
+ end
60
+ return ret
61
+ end
62
+
63
+ # xpath search for tags and return the passed attribute
64
+ # urls_from_xpath("//a","href")
65
+ def urls_from_xpath(xpath,attr)
66
+ ret = []
67
+ @hdoc.search(xpath).each do |elem|
68
+ url = elem[attr]
69
+ next if url.empty?
70
+ ret << url.strip
71
+ end
72
+ return ret
73
+ end
74
+
75
+ # <b>NEED</b> to move this to external configuration
76
+ #
77
+ # list of urls we don't care to store in our links list
78
+ def reject_links
79
+ putw "WARN: override reject_links in helper script" if @warn
80
+ default_reject_links
81
+ end
82
+
83
+ # default reject links (override using reject_links in helper script)
84
+ def default_reject_links
85
+ @links.reject! do |url|
86
+ url.nil? ||
87
+ url.extname == ".css" ||
88
+ url.extname == ".pdf" ||
89
+ url =~ /javascript:/i ||
90
+ url =~ /mailto:/i ||
91
+ url =~ /[\[\]]/ ||
92
+ url =~ /^#/
93
+ end
94
+ end
95
+
96
+ # define an urls_from_helper method in your task specific script
97
+ def urls_from_helper
98
+ putw "WARN: Please set an urls_from_helper override in your helper script" if @warn
99
+ return nil
100
+ end
101
+
102
+ # use xpath searches to get
103
+ # * //a href
104
+ # * //area href
105
+ # * //frame src
106
+ # * //iframe src
107
+ # * //form action
108
+ # * //meta refresh content urls
109
+ # then get //script tags and regexp out links in javascript function calls
110
+ # from elem.inner_html
111
+ def for_links(reject=true)
112
+ self.urls_from_xpath("//a","href").each { |url| @links << url }; # get <a href=""> elements
113
+ self.urls_from_xpath("//area","href").each { |url| @links << url }; # get <area href=""> elements
114
+ self.urls_from_xpath("//frame","src").each { |url| @links << url }; # get <frame src=""> elements
115
+ self.urls_from_xpath("//iframe","src").each { |url| @links << url }; # get <iframe src=""> elements
116
+ self.urls_from_xpath("//form","action").each { |url| @links << url }; # get <form action=""> elements
117
+
118
+ # <meta> refresh
119
+ @hdoc.search("//meta").each do |meta|
120
+ next if meta['http-equiv'] != "refresh"
121
+ next if not (content = meta['content'].split(/=/)[1])
122
+ @links << content.strip
123
+ end
124
+
125
+ # add urls from onclick handlers
126
+ @hdoc.search("*[@onclick]").each do |onclick|
127
+ LINKS_REGEXP.each do |re|
128
+ self.urls_from_regexp(onclick['onclick'],re).each do |url|
129
+ @links << url
130
+ end
131
+ end
132
+ end
133
+
134
+ # add urls_from_regexp (limit to <script> tags (elem.inner_html))
135
+ @hdoc.search("//script").each do |scr|
136
+ LINKS_REGEXP.each do |re|
137
+ self.urls_from_regexp(scr.inner_html,re).each { |url| @links << url }
138
+ end
139
+ end
140
+
141
+ # re-define urls_from_helper in what you mix in
142
+ begin
143
+ self.urls_from_helper
144
+ end
145
+
146
+ self.reject_links; # reject links we don't care about
147
+ return @links
148
+ end
149
+
150
+ # scrape the page for <script src=""> tags
151
+ def for_javascript_links
152
+ urls = []
153
+ @hdoc.search("//script[@src]").each { |tag| urls << tag['src'] }
154
+ urls.reject! { |url| File.extname(url).clip != ".js" }
155
+ return urls
156
+ end
157
+
158
+ # scan page for comment fields
159
+ def for_comments
160
+ @page.scan(/\<!\s*--(.*?)--\s*\>/m).map { |x| x.to_s }
161
+ end
162
+
163
+ # scrape the page for a meta refresh tag and return the url from the contents attribute or nil
164
+ def for_meta_refresh
165
+ has_mr = @hdoc.search("//meta").map { |x| x['http-equiv'] }.include?('Refresh')
166
+ if has_mr
167
+ urls = @hdoc.search("//meta[@content]").map { |x| x['content'].split(";",2)[1] }
168
+ if urls.size > 1
169
+ STDERR.puts "PARSE ERROR: more than one meta refresh tag"
170
+ return "ERR"
171
+ end
172
+ k,v = urls.first.split("=",2)
173
+ if k.upcase.strip != "URL"
174
+ STDERR.puts "PARSE ERROR: content attribute of meta refresh does not contain url"
175
+ return "ERR"
176
+ end
177
+ return v.strip
178
+ else
179
+ return nil
180
+ end
181
+ end
182
+
183
+ # scrape the page for a script tag that contains a bare location.href tag (to redirect the page)
184
+ def for_javascript_redirect
185
+ redirs = []
186
+ @hdoc.search("//script").each do |scr|
187
+ scr.inner_html.scan(/.*location.href\s*=\s*['"]([^'"]+)['"]/i).each { |x| redirs += x }
188
+ end
189
+ if redirs.size > 1
190
+ STDERR.puts "PARSE ERROR: more than one javascript redirect"
191
+ return "ERR"
192
+ end
193
+ return redirs.first if not redirs.empty?
194
+ return nil
195
+ end
196
+
197
+ # renamed class variable (for backward compat)
198
+ def warnings#:nodoc:
199
+ return @warn
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,127 @@
1
+ module WWMD
2
+ # when a WWMD::Page object is created, it created its own WWMD::Spider object
3
+ # which can be accessed using <tt>page.spider.method</tt>. The <tt>page.set_data</tt>
4
+ # method calls <tt>page.spider.add</tt> with the current url and a list of scraped
5
+ # links from the page. This class doesn't do any real heavy lifting.
6
+ #
7
+ # a simple spider can be written just by recursing through page.spider.next until
8
+ # it's empty.
9
+ class Spider
10
+
11
+ attr_accessor :queued
12
+ attr_accessor :visited
13
+ attr_accessor :bypass
14
+ attr_accessor :local_only
15
+ attr_reader :opts
16
+ attr_accessor :ignore
17
+ attr_accessor :csrf_token
18
+
19
+ DEFAULT_IGNORE = [
20
+ /logoff/i,
21
+ /logout/i,
22
+ ]
23
+
24
+ # pass me opts and an array of regexps to ignore
25
+ # we have a set of sane(ish) defaults here
26
+ def initialize(opts={},ignore=nil)
27
+ @opts = opts
28
+ @visited = []
29
+ @queued = []
30
+ @bypass = []
31
+ @local_only = true
32
+ @csrf_token = nil
33
+ if !opts[:spider_local_only].nil?
34
+ @local_only = opts[:spider_local_only]
35
+ end
36
+ @ignore = ignore || DEFAULT_IGNORE
37
+ end
38
+
39
+ # push an url onto the queue
40
+ def push_url(url)
41
+ return false if _check_ignore(url)
42
+ url = _de_csrf(url)
43
+ if @local_only
44
+ return false if !(url =~ /#{@opts[:base_url]}/)
45
+ end
46
+ @bypass.each { |b| return true if (url =~ b) }
47
+ @queued.push(url) if (@visited.find { |v| v == url }.nil? and @queued.find { |q| q == url }.nil?)
48
+ return true
49
+ end
50
+
51
+ # skip items in the queue
52
+ def skip(tim=1)
53
+ tim.times { |i| @queued.shift }
54
+ return true
55
+ end
56
+
57
+ # get the next url in the queue
58
+ def get_next
59
+ return queued.shift
60
+ end
61
+
62
+ alias_method :next, :get_next
63
+
64
+ # more elements in the queue?
65
+ def next?
66
+ return !queued.empty?
67
+ end
68
+
69
+ # get the last ul we visited? this doesn't look right
70
+ def get_last(url)
71
+ tmp = @visited.reject { |v| v =~ /#{url}/ }
72
+ return tmp[-1]
73
+ end
74
+
75
+ # show the visited list (or the entry in the list at [id])
76
+ def show_visited(id=nil)
77
+ if id.nil?
78
+ @visited.each_index { |i| putx i.to_s + " :: " + @visited[i].to_s }
79
+ return nil
80
+ else
81
+ return @visited[id]
82
+ end
83
+ end
84
+
85
+ alias_method :v, :show_visited
86
+
87
+ # return the current queue (or the entry in the queue at [id]
88
+ def show_queue(id=nil)
89
+ if id.nil?
90
+ @queued.each_index { |i| putx i.to_s + " :: " + @queued[i].to_s }
91
+ return nil
92
+ else
93
+ return @queued[id]
94
+ end
95
+ end
96
+
97
+ alias_method :q, :show_queue
98
+
99
+ # add url to queue
100
+ def add(url='',links=[])
101
+ @visited.push(_de_csrf(url))
102
+ links.each { |l| self.push_url l }
103
+ return nil
104
+ end
105
+
106
+ # set up the ignore list
107
+ # ignore list is an array of regexp objects
108
+ # remember to set this up before calling any Page methods
109
+ def set_ignore(arr)
110
+ @ignore = arr
111
+ end
112
+
113
+ def _de_csrf(url)
114
+ return url if @csrf_token.nil?
115
+ act,params = url.clopa
116
+ form = params.to_form
117
+ return url if !form.has_key?(@csrf_token)
118
+ form[@csrf_token] = ''
119
+ url = act + form.to_get
120
+ end
121
+
122
+ def _check_ignore(url)
123
+ @ignore.each { |x| return true if (url =~ x) }
124
+ return false
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,79 @@
1
+ module WWMD
2
+ # yay for experiments in re-inventing the wheel
3
+ class URLParse
4
+ HANDLERS = [:https,:http,:ftp,:file]
5
+ attr_reader :proto,:location,:path,:script,:rpath,:params,:base_url,:fqpath
6
+
7
+ def initialize()
8
+ # nothing to see here, move along
9
+ end
10
+
11
+ def parse(*args)
12
+ if args.size == 1
13
+ base = ""
14
+ actual = args.shift
15
+ else
16
+ base = args.shift
17
+ actual = args.shift
18
+ end
19
+ @proto = @location = @path = @script = @rpath = nil
20
+ @base = base.to_s
21
+ @actual = actual
22
+ if self.has_proto?
23
+ @base = @actual
24
+ @actual = ""
25
+ end
26
+ # does this work for http://location/? probably not
27
+ @base += "/" if (!@base.has_ext? || @base.split("/").size == 3)
28
+ @rpath = make_me_path.join("/")
29
+ @params = @rpath.clop
30
+ @path = "/" + @rpath
31
+ if @rpath.has_ext?
32
+ @path = "/" + @rpath.dirname
33
+ @script = @rpath.basename.clip
34
+ end
35
+ @script = "" if @script.nil?
36
+ @base_url = @proto + "://" + @location
37
+ @fqpath = @path + @script
38
+ self
39
+ end
40
+
41
+ def make_me_path
42
+ @proto,tpath = @base.split(":",2)
43
+ tpath ||= ""
44
+ if @actual.empty?
45
+ a_path = tpath.split("/").reject { |x| x.empty? }
46
+ else
47
+ a_path = tpath.dirname.split("/").reject { |x| x.empty? }
48
+ end
49
+ @location = a_path.shift
50
+ a_path = [] if (@actual =~ (/^\//))
51
+ b_path = @actual.split("/").reject { |x| x.empty? }
52
+ a_path.pop if (a_path[-1] =~ /^\?/).kind_of?(Fixnum) && !b_path.empty?
53
+ c_path = (a_path + @actual.split("/").reject { |x| x.empty? }).flatten
54
+ d_path = []
55
+ c_path.each do |x|
56
+ (d_path.pop;next) if x == ".."
57
+ next if x == "."
58
+ d_path << x
59
+ end
60
+ return d_path
61
+ end
62
+
63
+ def has_proto?
64
+ return true if HANDLERS.include?(@actual.split(":").first.downcase.to_sym)
65
+ return false
66
+ end
67
+
68
+ def to_s
69
+ return "#{@proto}://#{@location}/#{rpath}"
70
+ end
71
+ end
72
+ end
73
+
74
+ class String
75
+ def has_ext? #:nodoc:
76
+ return false if self.basename.split(".",2)[1].empty?
77
+ return true
78
+ end
79
+ end
@@ -0,0 +1,30 @@
1
+ module WWMD
2
+ class WWMDUtils
3
+
4
+ def self.header_array_from_file(filename)
5
+ ret = Hash.new
6
+ File.readlines(filename).each do |line|
7
+ a = line.chomp.split(/\t/,2)
8
+ ret[a[0]] = a[1]
9
+ end
10
+ return ret
11
+ end
12
+
13
+ def self.ranstr(len=8,digits=false)
14
+ chars = ("a".."z").to_a
15
+ chars += ("0".."9").to_a if digits
16
+ ret = ""
17
+ 1.upto(len) { |i| ret << chars[rand(chars.size-1)] }
18
+ return ret
19
+ end
20
+
21
+ def self.rannum(len=8,hex=false)
22
+ chars = ("0".."9").to_a
23
+ chars += ("A".."F").to_a if hex
24
+ ret = ""
25
+ 1.upto(len) { |i| ret << chars[rand(chars.size-1)] }
26
+ return ret
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,118 @@
1
+ require 'wwmd/viewstate/viewstate_utils'
2
+ module WWMD
3
+ class ViewState < ViewStateUtils
4
+ end
5
+ end
6
+ require 'rubygems'
7
+ require 'nokogiri'
8
+ require 'htmlentities'
9
+ require 'rexml/document'
10
+ require 'wwmd/mixins'
11
+ require 'wwmd/mixins_extends'
12
+ require 'wwmd/viewstate/viewstate_types'
13
+ require 'wwmd/viewstate/viewstate_class_helpers'
14
+ require 'wwmd/viewstate/viewstate_yaml'
15
+ require 'wwmd/viewstate/viewstate_deserializer_methods'
16
+ require 'wwmd/viewstate/viewstate_from_xml'
17
+ require 'wwmd/viewstate/vs_read_value'
18
+ require 'wwmd/viewstate/vs_read_types'
19
+ require 'wwmd/viewstate/vs_value'
20
+ require 'wwmd/viewstate/vs_array'
21
+ require 'wwmd/viewstate/vs_binary_serialized'
22
+ require 'wwmd/viewstate/vs_int_enum'
23
+ require 'wwmd/viewstate/vs_hashtable'
24
+ require 'wwmd/viewstate/vs_hybrid_dict'
25
+ require 'wwmd/viewstate/vs_list'
26
+ require 'wwmd/viewstate/vs_pair'
27
+ require 'wwmd/viewstate/vs_sparse_array'
28
+ require 'wwmd/viewstate/vs_string'
29
+ require 'wwmd/viewstate/vs_string_array'
30
+ require 'wwmd/viewstate/vs_string_formatted'
31
+ require 'wwmd/viewstate/vs_triplet'
32
+ require 'wwmd/viewstate/vs_type'
33
+ require 'wwmd/viewstate/vs_unit'
34
+ require 'wwmd/viewstate/vs_indexed_string'
35
+ require 'wwmd/viewstate/vs_indexed_string_ref'
36
+ module WWMD
37
+ class ViewState
38
+ attr_accessor :b64
39
+ attr_accessor :obj_queue
40
+ attr_accessor :mac
41
+ attr_accessor :debug
42
+ attr_reader :raw
43
+ attr_reader :stack
44
+ attr_reader :bufarr
45
+ attr_reader :magic
46
+ attr_reader :size
47
+ attr_reader :indexed_strings
48
+ attr_reader :last_offset
49
+ attr_reader :xml
50
+
51
+ def initialize(b64=nil)
52
+ @b64 = b64
53
+ @raw = ""
54
+ @stack = ""
55
+ @obj_queue = []
56
+ @bufarr = []
57
+ @size = 0
58
+ @indexed_strings = []
59
+ @mac = nil
60
+ @debug = false
61
+ end
62
+
63
+ # mac_enabled?
64
+ def mac_enabled?
65
+ return !@mac.nil?
66
+ end
67
+
68
+ # deserialize
69
+ def deserialize(b64=nil)
70
+ @obj_queue = []
71
+ @b64 = b64 if b64
72
+ @raw = @b64.b64d
73
+ @bufarr = @raw.scan(/./m)
74
+ @size = @bufarr.size
75
+ raise "Invalid ViewState" if not self.magic?
76
+ @obj_queue << self.deserialize_value
77
+ if @bufarr.size == 20 then
78
+ @mac = bufarr.slice!(0..19).join("")
79
+ dlog(0x00,"MAC = #{@mac.hexify}")
80
+ end
81
+ raise "Error Parsing Viewstate (left: #{@buffarr.size})" if not @bufarr.size == 0
82
+ return !self.raw.nil?
83
+ end
84
+
85
+ def serialize(objs=nil,version=2)
86
+ @obj_queue = objs if objs
87
+ @stack << "\xFF\x01"
88
+ @stack << @obj_queue.first.serialize
89
+ @stack << @mac if @mac
90
+ return !self.stack.nil?
91
+ end
92
+
93
+ def to_xml
94
+ @xml = REXML::Document.new()
95
+ header = REXML::Element.new("ViewState")
96
+ header.add_attribute("version", @magic.b64e)
97
+ header.add_attribute("version_string", @magic.hexify)
98
+ header.add_element(@obj_queue.first.to_xml)
99
+ if self.mac_enabled?
100
+ max = REXML::Element.new("Mac")
101
+ max.add_attribute("encoding","hexify")
102
+ max.add_text(@mac.hexify)
103
+ header.add_element(max)
104
+ end
105
+ @xml.add_element(header)
106
+ @xml
107
+ end
108
+
109
+ def from_yaml(yaml)
110
+ @obj_queue = YAML.load(yaml)
111
+ end
112
+
113
+ def to_yaml
114
+ @obj_queue.to_yaml
115
+ end
116
+
117
+ end
118
+ end