miketracy-wwmd 0.2.16 → 0.2.17

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 (60) hide show
  1. data/History.txt +21 -0
  2. data/{README → README.rdoc} +27 -2
  3. data/lib/wwmd.rb +4 -4
  4. data/lib/wwmd/class_extensions.rb +2 -0
  5. data/lib/wwmd/{mixins.rb → class_extensions/extensions_base.rb} +25 -121
  6. data/lib/wwmd/class_extensions/extensions_encoding.rb +79 -0
  7. data/lib/wwmd/{mixins_external.rb → class_extensions/extensions_external.rb} +0 -0
  8. data/lib/wwmd/class_extensions/extensions_nilclass.rb +11 -0
  9. data/lib/wwmd/{mixins_extends.rb → class_extensions/extensions_rbkb.rb} +0 -0
  10. data/lib/wwmd/{encoding.rb → class_extensions/mixins_string_encoding.rb} +6 -6
  11. data/lib/wwmd/page.rb +3 -245
  12. data/lib/wwmd/page/auth.rb +0 -166
  13. data/lib/wwmd/page/constants.rb +7 -4
  14. data/lib/wwmd/page/form.rb +0 -15
  15. data/lib/wwmd/page/form_array.rb +96 -74
  16. data/lib/wwmd/page/headers.rb +25 -21
  17. data/lib/wwmd/page/helpers.rb +30 -0
  18. data/lib/wwmd/{hpricot_html2text.rb → page/html2text_hpricot.rb} +1 -1
  19. data/lib/wwmd/{nokogiri_html2text.rb → page/html2text_nokogiri.rb} +0 -0
  20. data/lib/wwmd/page/inputs.rb +1 -1
  21. data/lib/wwmd/page/irb_helpers.rb +37 -13
  22. data/lib/wwmd/page/page.rb +238 -0
  23. data/lib/wwmd/page/parsing_convenience.rb +8 -3
  24. data/lib/wwmd/page/scrape.rb +15 -19
  25. data/lib/wwmd/page/spider.rb +11 -11
  26. data/lib/wwmd/urlparse.rb +20 -5
  27. data/lib/wwmd/viewstate.rb +9 -112
  28. data/lib/wwmd/viewstate/viewstate.rb +101 -0
  29. data/lib/wwmd/viewstate/viewstate_deserializer_methods.rb +36 -36
  30. data/lib/wwmd/viewstate/viewstate_types.rb +0 -4
  31. data/lib/wwmd/viewstate/viewstate_utils.rb +6 -1
  32. data/lib/wwmd/viewstate/vs_stubs.rb +22 -0
  33. data/lib/wwmd/viewstate/{vs_array.rb → vs_stubs/vs_array.rb} +3 -1
  34. data/lib/wwmd/viewstate/{vs_binary_serialized.rb → vs_stubs/vs_binary_serialized.rb} +3 -1
  35. data/lib/wwmd/viewstate/{vs_hashtable.rb → vs_stubs/vs_hashtable.rb} +3 -1
  36. data/lib/wwmd/viewstate/{vs_hybrid_dict.rb → vs_stubs/vs_hybrid_dict.rb} +3 -1
  37. data/lib/wwmd/viewstate/{vs_indexed_string.rb → vs_stubs/vs_indexed_string.rb} +1 -1
  38. data/lib/wwmd/viewstate/{vs_indexed_string_ref.rb → vs_stubs/vs_indexed_string_ref.rb} +3 -1
  39. data/lib/wwmd/viewstate/{vs_int_enum.rb → vs_stubs/vs_int_enum.rb} +3 -1
  40. data/lib/wwmd/viewstate/{vs_list.rb → vs_stubs/vs_list.rb} +2 -1
  41. data/lib/wwmd/viewstate/{vs_pair.rb → vs_stubs/vs_pair.rb} +3 -1
  42. data/lib/wwmd/viewstate/vs_stubs/vs_read_types.rb +11 -0
  43. data/lib/wwmd/viewstate/{vs_read_value.rb → vs_stubs/vs_read_value.rb} +3 -1
  44. data/lib/wwmd/viewstate/{vs_sparse_array.rb → vs_stubs/vs_sparse_array.rb} +3 -1
  45. data/lib/wwmd/viewstate/{vs_string.rb → vs_stubs/vs_string.rb} +2 -1
  46. data/lib/wwmd/viewstate/{vs_string_array.rb → vs_stubs/vs_string_array.rb} +4 -2
  47. data/lib/wwmd/viewstate/{vs_string_formatted.rb → vs_stubs/vs_string_formatted.rb} +4 -2
  48. data/lib/wwmd/viewstate/{viewstate_class_helpers.rb → vs_stubs/vs_stub_helpers.rb} +2 -1
  49. data/lib/wwmd/viewstate/{vs_triplet.rb → vs_stubs/vs_triplet.rb} +3 -1
  50. data/lib/wwmd/viewstate/{vs_type.rb → vs_stubs/vs_type.rb} +3 -1
  51. data/lib/wwmd/viewstate/{vs_unit.rb → vs_stubs/vs_unit.rb} +3 -1
  52. data/lib/wwmd/viewstate/{vs_value.rb → vs_stubs/vs_value.rb} +4 -2
  53. data/lib/wwmd/wwmd_config.rb +44 -36
  54. data/lib/wwmd/wwmd_puts.rb +9 -0
  55. data/lib/wwmd/wwmd_utils.rb +22 -24
  56. data/tasks/setup.rb +1 -1
  57. metadata +41 -35
  58. data/README.txt +0 -62
  59. data/lib/wwmd/viewstate/vs_read_types.rb +0 -11
  60. data/wwmd.gemspec +0 -0
@@ -1,30 +1,6 @@
1
- =begin rdoc
2
- This is where we do all the undocumented auth stuff. NTLM is here and
3
- hooked in.
4
-
5
- WWMDNTLM is an incredibly naive NTLM implementation (used to get
6
- around NTLM for one project ahwile back
7
- =end
8
-
9
1
  module WWMD
10
2
  class Page
11
3
 
12
- #:stopdoc:
13
-
14
- #:section: Authentication helpers
15
-
16
- # check if this request requires NTLM
17
- def ntlm?
18
- return false if self.code != 401
19
- count = 0
20
- self.header_data.each do |i|
21
- if i[0] =~ /www-authenticate/i
22
- count += 1 if (i[1] == "Negotiate" || i[1] == "NTLM")
23
- end
24
- end
25
- return (count > 0)
26
- end
27
-
28
4
  # does this request have an authenticate header?
29
5
  def auth?
30
6
  return false if self.code != 401
@@ -37,147 +13,5 @@ module WWMD
37
13
  return (count > 0)
38
14
  end
39
15
 
40
- # not sure why this is here
41
- def ntlm_perform(exp=nil)#:nodoc:
42
- self.perform
43
- return (self.code == exp)
44
- end
45
-
46
- # perform a get usig NTLM
47
- def ntlm_get(url=nil?,debug=false)
48
- self.clear_header('Authorization')
49
- nobj = WWMDNTLM.new(self.opts)
50
- self.url = @urlparse.parse(self.opts[:base_url],url) if not url.nil?
51
- self.perform
52
- return "This request does not appear to require NTLM" if not self.ntlm?
53
- self.headers['Authorization'] = nobj.type_1_msg
54
- self.perform
55
- type2 = self.header_data.get_value('WWW-Authenticate')
56
- nonce = nobj.get_nonce(type2)
57
- type3 = nobj.type_3_msg(nonce)
58
- self.headers['Authorization'] = type3
59
- self.perform
60
- self.clear_header('Authorization')
61
- return self.code
62
- end
63
-
64
- #:startdoc:
65
-
66
- end
67
-
68
- class WWMDNTLM#:nodoc:
69
- attr_accessor :hostname
70
- attr_accessor :domain
71
- attr_accessor :username
72
- attr_accessor :password
73
- attr_accessor :opts
74
- attr_accessor :negotiate_flags
75
- attr_accessor :debug
76
-
77
- def initialize(opts,debug=false)
78
- @opts = opts
79
- @hostname = self.opts[:hostname]
80
- @domain = self.opts[:domain]
81
- @username = self.opts[:username]
82
- @password = self.opts[:password]
83
- @hostname = "LOCALHOST" if self.hostname.nil?
84
- @negotiate_flags = 0x00002201.to_l32
85
- @debug = debug
86
- end
87
-
88
- def type_1_msg
89
- # do not add domain for now here as it doesn't seem to be needed
90
- # it does need to be set for type3 messages
91
- host_len = self.hostname.size
92
- host_off = 0x20
93
- # if self.domain.nil?
94
- if true
95
- dom_off = 0
96
- dom_len = 0
97
- else
98
- dom_off = (host_off + hostname.size)
99
- dom_len = self.domain.size
100
- end
101
- msg = ""
102
- msg << "NTLMSSP\x00" # signature[8]
103
- msg << 0x01.to_l32 # type[4]
104
- msg << self.negotiate_flags # NegotiateFlags[4]
105
- msg << dom_len.to_l16 # domain string length[2]
106
- msg << dom_len.to_l16 # domain string length[2]
107
- msg << dom_off.to_l32 # domain string offset[4]
108
- msg << host_len.to_l16 # host string length[2]
109
- msg << host_len.to_l16 # host string length[2]
110
- msg << host_off.to_l32 # host string offset[4]
111
- # msg << self.domain if not self.domain.nil? # domain[var]
112
- msg << self.hostname # host name[var]
113
- return "NTLM " + msg.b64e
114
- end
115
-
116
- def get_nonce(t2msg)
117
- # Signature[8]
118
- # MessageType[4]
119
- # TargetNameFields[8]
120
- # NegotiateFlags[4]
121
- # ServerChallenge[8]
122
- # Reserved[8] ! 0x00
123
- # TargetInfoFields[8]
124
- # Version[8]
125
- # Payload[var]
126
- msg = t2msg.split[1].b64d
127
- return msg[24..31]
128
- end
129
-
130
- def type_3_msg(nonce)
131
- hlen = 0x40
132
- poff = hlen
133
- domain = self.domain.to_utf16
134
- username = self.username.to_utf16
135
- hostname = self.hostname.to_utf16
136
- lmresp = NTLM.lm_response(self.opts[:password],nonce,:lmhash)
137
- ntresp = NTLM.lm_response(self.opts[:password],nonce,:nthash)
138
- msg = ""
139
- msg << "NTLMSSP\x00" # Signature[8]
140
- msg << 0x03.to_l32 # MessageType[4]
141
- # LmChallengeResonseFields[8]
142
- msg << lmresp.size.to_l16 # LmChallengeResponseLen[2]
143
- msg << lmresp.size.to_l16 # LmChallengeResponseMaxLen[2]
144
- msg << poff.to_l32 # LmChallengeResponseBufferOffset[4]
145
- poff += lmresp.size
146
- # msg << 0x40.to_l32 # LmChallengeResponseBufferOffset[4]
147
- # NtChallengeResponseFields[8]
148
- msg << ntresp.size.to_l16 # NtChallengeResponseLen[2]
149
- msg << ntresp.size.to_l16 # NtChallengeResponseMaxLen[2]
150
- # msg << 0x58.to_l32 # NtChallengeResponseBufferOffset[4]
151
- msg << poff.to_l32 # NtChallengeResponseBufferOffset[4]
152
- poff += ntresp.size
153
- # DomainNameFields[8]
154
- msg << domain.size.to_l16 # DomainNameLen[2]
155
- msg << domain.size.to_l16 # DomainNameMaxLen[2]
156
- msg << poff.to_l32 # DomainNameBufferOffset[4]
157
- poff += domain.size
158
- # UserNameFields[8]
159
- msg << username.size.to_l16 # UserNameLen[2]
160
- msg << username.size.to_l16 # UserNameMaxLen[2]
161
- msg << poff.to_l32 # UserNameBufferOffset[4]
162
- poff += username.size
163
- # WorkstationFields[8]
164
- msg << hostname.size.to_l16 # WorkstationLen[2]
165
- msg << hostname.size.to_l16 # WorkstationMaxLen[2]
166
- msg << poff.to_l32 # WorkstationBufferOffset[4]
167
- poff += hostname.size
168
- # EncryptedRandomSessionKeyFields[8]
169
- msg << 0x00.to_l16 # EncryptedRandomSessionKeyLen[2]
170
- msg << 0x00.to_l16 # EncryptedRandomSessionKeyMaxLen[2]
171
- msg << 0x00.to_l32 # EncryptedRandomSessionKeyBufferOffset[4]
172
- msg << self.negotiate_flags # NegotiateFlags[4]
173
- # Version[8] (optional do not add)
174
- # Payload[var]
175
- msg << lmresp # LmChallenge[var]
176
- msg << ntresp # NtChallenge[var]
177
- msg << domain # DomainName[var]
178
- msg << username # UserName[var]
179
- msg << hostname # Workstation[var]
180
- return "NTLM " + msg.b64e
181
- end
182
16
  end
183
17
  end
@@ -18,10 +18,11 @@ module WWMD
18
18
  ESCAPE = {
19
19
  :url => /[^a-zA-Z0-9\-_%]/,
20
20
  :nalnum => /[^a-zA-Z0-9]/,
21
- :xss => /[^a-zA-Z0-9=?&()']/,
21
+ :xss => /[^a-zA-Z0-9=?()']/,
22
22
  :ltgt => /[<>]/,
23
23
  :all => /.*/,
24
- :b64 => /[=+\/]/,
24
+ # :b64 => /[=+\/]/,
25
+ :b64 => /[^a-zA-Z0-9]/,
25
26
  :none => :none,
26
27
  :default => :default,
27
28
  }
@@ -33,11 +34,13 @@ module WWMD
33
34
  :ie7 => "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
34
35
  :ie8 => "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)",
35
36
  :opera => "Opera/9.20 (Windows NT 6.0; U; en)",
36
- :safari => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_11; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.22"
37
+ :safari => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_11; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.22",
38
+ :safari4 => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17",
39
+ :wwmd => "Mozilla/5.0 (compatible; WWMD #{WWMD::VERSION}; o_hai)"
37
40
  }
38
41
 
39
42
  DEFAULT_HEADERS = {
40
- "User-Agent" => UA[:moz3],
43
+ "User-Agent" => UA[:wwmd],
41
44
  "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
42
45
  "Accept-Language" => "en-US,en;q=0.8,en-au;q=0.6,en-us;q=0.4,en;q=0.2",
43
46
  "Accept-Encoding" => "gzip,deflate",
@@ -40,21 +40,6 @@ module WWMD
40
40
  return self.get_attribute("action")
41
41
  end
42
42
 
43
- def report
44
- puts "action = #{self.action}"
45
- self.fields.each { |field| puts field.to_text }
46
- return nil
47
- end
48
-
49
- alias_method :show, :report
50
-
51
- def to_form_array
52
- FormArray.new(self.fields)
53
- end
54
-
55
- def to_array
56
- self.to_form_array
57
- end
58
43
  end
59
44
 
60
45
  class Field < Form
@@ -6,35 +6,48 @@ Accessing this either as a hash or an array (but => won't work)
6
6
 
7
7
  Some of the methods in here are kept for backward compat before the refactor
8
8
  and now everything in this array should be accessed with []= and []
9
+
10
+ Set :action and take a block. Page#submit_form should take this and do the
11
+ right thing.
9
12
  =end
10
13
 
11
14
  module WWMD
12
15
  class FormArray < Array
16
+ attr_accessor :action
17
+ attr_accessor :delimiter
18
+ attr_accessor :equals
19
+
20
+ def initialize(fields=nil,action=nil,&block)
21
+ set_fields(fields)
22
+ @delimiter = "&"
23
+ @equals = "="
24
+ @action = action
25
+ instance_eval(&block) if block_given?
26
+ end
13
27
 
14
- def initialize(fields=nil)
15
- if not fields.nil?
16
- # this first one is an array of field objects
17
- if fields.class == Array
18
- fields.each do |f|
19
- name = f['name']
20
- if self.name_exists(name)
21
- if f['type'] == "hidden"
22
- self.set name,f.get_value
23
- elsif f['type'] == "checkbox" and f.to_html.grep(/checked/) != ''
24
- self[name] = f.get_value
25
- end
26
- else
27
- self << [ f['name'],f.get_value ]
28
+ def set_fields(fields=nil)
29
+ return nil if fields.nil?
30
+ # this first one is an array of field objects
31
+ if fields.class == Array
32
+ fields.each do |f|
33
+ name = f['name']
34
+ if self.name_exists(name)
35
+ if f['type'] == "hidden"
36
+ self.set name,f.get_value
37
+ elsif f['type'] == "checkbox" and f.to_html.grep(/checked/) != ''
38
+ self[name] = f.get_value
28
39
  end
29
- end
30
- elsif fields.class == Hash
31
- fields.each_pair { |k,v| self[k] = v }
32
- elsif fields.class == String
33
- fields.split("&").each do |f|
34
- k,v = f.split("=",2)
35
- self[k] = v
40
+ else
41
+ self << [ f['name'],f.get_value ]
36
42
  end
37
43
  end
44
+ elsif fields.class == Hash
45
+ fields.each_pair { |k,v| self[k] = v }
46
+ elsif fields.class == String
47
+ fields.split(@delimiter).each do |f|
48
+ k,v = f.split(@equals,2)
49
+ self[k] = v
50
+ end
38
51
  end
39
52
  end
40
53
 
@@ -43,6 +56,7 @@ module WWMD
43
56
  def clone
44
57
  ret = self.class.new
45
58
  self.each { |r| ret << r.clone }
59
+ ret.action = self.action
46
60
  return ret
47
61
  end
48
62
 
@@ -64,14 +78,6 @@ module WWMD
64
78
  self << [key,value]
65
79
  end
66
80
 
67
- def clear_viewstate
68
- self.each { |k,v|
69
- self[k] = "" if k == "__VIEWSTATE"
70
- }
71
- end
72
-
73
- alias_method :extend!, :add #:nodoc (this is here for backward compat)
74
-
75
81
  # key = Fixnum set value at index key
76
82
  # key = String find key named string and set value
77
83
  def set_value!(key,value)
@@ -87,6 +93,8 @@ module WWMD
87
93
  return [key,value]
88
94
  end
89
95
 
96
+ # get a value using its index
97
+ # override Array#[]
90
98
  alias_method :old_get, :[]#:nodoc:
91
99
  def [](*args)
92
100
  if args.first.class == Fixnum
@@ -133,6 +141,10 @@ module WWMD
133
141
 
134
142
  alias_method :get, :get_value
135
143
 
144
+ def keys
145
+ self.map { |k,v| k }
146
+ end
147
+
136
148
  def setall!(value)
137
149
  self.each_index { |i| self.set_value!(i,value) }
138
150
  end
@@ -176,39 +188,38 @@ module WWMD
176
188
 
177
189
  alias_method :unescape_all, :unescape_all!#:nodoc:
178
190
 
179
- # convert form into a post parameters string
180
- def to_post
181
- ret = []
182
- self.each do |i|
183
- ret.push(i.join("="))
184
- end
185
- ret.join("&")
191
+ # remove form elements with null values
192
+ def remove_nulls!
193
+ self.delete_if { |x| x[1].to_s.empty? || x[1].nil? }
186
194
  end
187
195
 
188
- # convert form into a get parameters string
189
- #
190
- # pass me a base to get a full url to pass to Page.get
191
- def to_get(base="")
192
- ret = []
193
- self.each do |i|
194
- ret.push(i.join("="))
195
- end
196
- ret = ret.join("&")
197
- return base.clip + "?" + ret.to_s
196
+ alias_method :squeeze!, :remove_nulls!
197
+
198
+ # remove form elements with null keys (for housekeeping returns)
199
+ def remove_null_keys!
200
+ self.delete_if { |x,y| x.to_s.empty? || x.nil? }
198
201
  end
199
202
 
200
- # IRB: puts the form in human readable format
201
- # if you <tt>form.show(true)</tt> it will show unescaped values
202
- def show(unescape=false)
203
- if unescape
204
- self.each_index { |i| puts i.to_s + " :: " + self[i][0].to_s + " = " + self[i][1].to_s.unescape }
205
- else
206
- self.each_index { |i| puts i.to_s + " :: " + self[i][0].to_s + " = " + self[i][1].to_s }
207
- end
208
- return nil
203
+ alias_method :squeeze_keys!, :remove_null_keys!
204
+
205
+ ## viewstate
206
+
207
+ # clear viewstate variables
208
+ def clear_viewstate
209
+ self.each { |k,v|
210
+ self[k] = "" if k =~ /^__/
211
+ }
212
+ end
213
+
214
+ # remove viewstate variables
215
+ def rm_viewstate
216
+ # my least favorite ruby idiom
217
+ self.replace(self.map { |k,v| [k,v] if not k =~ /^__/ }.reject { |x| x.nil? })
209
218
  end
210
219
 
211
- # meh
220
+ alias_method :extend!, :add #:nodoc (this is here for backward compat)
221
+
222
+ # add viewstate stuff
212
223
  def add_viewstate#:nodoc:
213
224
  self.insert(0,[ "__VIEWSTATE","" ])
214
225
  self.insert(0,[ "__EVENTARGUMENT","" ])
@@ -217,31 +228,40 @@ module WWMD
217
228
  return nil
218
229
  end
219
230
 
220
- # alias_method, :add_state, :add_viewstate#:nodoc:
231
+ ## conversions
221
232
 
222
- # remove form elements with null values
223
- def remove_nulls!
224
- self.delete_if { |x| x[1].to_s.empty? || x[1].nil? }
233
+ # convert form into a post parameters string
234
+ def to_post
235
+ ret = []
236
+ self.each do |i|
237
+ ret << i.join(@equals)
238
+ end
239
+ ret.join(@delimiter)
225
240
  end
226
241
 
227
- alias_method :squeeze!, :remove_nulls!
228
-
229
- # remove form elements with null keys (for housekeeping returns)
230
- def remove_null_keys!
231
- self.delete_if { |x,y| x.to_s.empty? || x.nil? }
242
+ # convert form into a get parameters string
243
+ #
244
+ # pass me a base to get a full url to pass to Page.get
245
+ def to_get(base="")
246
+ ret = []
247
+ self.each do |i|
248
+ ret << i.join(@equals)
249
+ end
250
+ ret = ret.join(@delimiter)
251
+ return base.clip + "?" + ret.to_s
232
252
  end
233
253
 
234
- alias_method :squeeze_keys!, :remove_null_keys!
254
+ ## parsing convenience
235
255
 
236
256
  # dump a web page containing a csrf example of the current FormArray
237
- def to_csrf(action,unescval=false)
257
+ def to_csrf(action=nil,unescval=false)
258
+ action = self.action if not action
238
259
  ret = ""
239
260
  ret << "<html><body>\n"
240
261
  ret << "<form method='post' id='wwmdtest' name='wwmdtest' action='#{action}'>\n"
241
262
  self.each do |key,val|
242
263
  val = val.unescape.gsub(/'/) { %q[\'] } if unescval
243
- ret << "<input name='#{key.to_s.unescape}' type='hidden' value='#{val}' />\n"
244
- # ret << "<input name='#{key.to_s.unescape}' type='hidden' value='#{val.to_s.unescape.gsub(/'/,"\\'")}' />\n"
264
+ ret << "<input name='#{key.to_s.unescape}' type='hidden' value='#{val.to_s.unescape}' />\n"
245
265
  end
246
266
  ret << "</form>\n"
247
267
  ret << "<script>document.wwmdtest.submit()</script>\n"
@@ -249,17 +269,14 @@ module WWMD
249
269
  return ret
250
270
  end
251
271
 
252
- def keys
253
- self.map { |k,v| k }
254
- end
255
-
272
+ # add markers for burp intruder to form
256
273
  def burpify #:nodoc:
257
274
  ret = self.clone
258
275
  ret.each_index do |i|
259
276
  next if ret[i][0] =~ /^__/
260
277
  ret.set_value!(i,"#{ret.get_value(i)}" + "\302\247" + "\302\247")
261
278
  end
262
- system("echo '#{ret.to_post}' | pbcopy")
279
+ ret.to_post.pbcopy
263
280
  return ret
264
281
  end
265
282
 
@@ -269,5 +286,10 @@ module WWMD
269
286
  end
270
287
  alias_method :fp, :fingerprint #:nodoc:
271
288
 
289
+ def from_array(arr)
290
+ self.clear
291
+ arr.each { |k,v| self[k] = v }
292
+ end
293
+
272
294
  end
273
295
  end