rubot-base 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1182 @@
1
+ # :main:rubot/base.rb
2
+ # :title:Rubot Documentation
3
+ #
4
+ # = Rubot Base Library for Ruby
5
+ # Author:: Konstantin Haase
6
+ # Requires:: Ruby, Hpricot (>= 0.5), eruby, HTML Entities for Ruby (>= 4.0), Rubygems (optional)
7
+ # License:: MIT-License
8
+ #
9
+ # This is a library for creating bots for MediaWiki projects (i.e. Wikipedia).
10
+ # It can be either used directly or through an adapter library (for handling monitoring, caching and so on).
11
+ #
12
+ # == Status Quo
13
+ # This libary is working quite smooth now but it's not as feature-rich as I want it to be.
14
+ #
15
+ # Heavy improvements to be expected, stay in touch. Some more documentation will follow.
16
+ #
17
+ # == Ruby 1.9
18
+ # Rubot now works with Ruby 1.9. However, hpricot for 1.9 seems to be broken somehow (at least the
19
+ # version from the Ubuntu repo). I'll keep an eye on that one.
20
+ #
21
+ # == Installation
22
+ # At the moment, there is no official release, so you have to use the development version.
23
+ # You can download it using bazaar (http://bazaar-vcs.org)
24
+ #
25
+ # bzr branch http://freifunk-halle.de/~konstantin/rubot
26
+ #
27
+ # <b>First offical version will be released, when uploads are fixed. Enjoy!</b>
28
+ #
29
+ # == Known Bugs
30
+ # * Uploads don't work, yet
31
+ #
32
+ # == TODO
33
+ # * Download files
34
+ #
35
+ # == License
36
+ # Copyright (c) 2008 Konstantin Haase
37
+ #
38
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
39
+ # of this software and associated documentation files (the "Software"), to
40
+ # deal in the Software without restriction, including without limitation the
41
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
42
+ # sell copies of the Software, and to permit persons to whom the Software is
43
+ # furnished to do so, subject to the following conditions:
44
+ #
45
+ # The above copyright notice and this permission notice shall be included in
46
+ # all copies or substantial portions of the Software.
47
+ #
48
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
51
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
52
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
53
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
54
+ # IN THE SOFTWARE.
55
+
56
+ # Tries to execute the given block and returns true if no exception is raised,
57
+ # returns false otherwise. Takes as optional argument the exception to rescue.
58
+ def try(exception = Exception)
59
+ begin
60
+ yield
61
+ true
62
+ rescue exception
63
+ false
64
+ end
65
+ end
66
+
67
+ # :stopdoc:
68
+ try(LoadError) { require 'rubygems' }
69
+ # :startdoc:
70
+
71
+ require 'rubot/mime.rb'
72
+ require 'net/http'
73
+ require 'net/https'
74
+ require 'uri'
75
+ require 'hpricot'
76
+ require 'erb'
77
+ require 'logger'
78
+ require 'htmlentities'
79
+ require 'yaml'
80
+
81
+ module Rubot
82
+
83
+ # Base for all Rubot errors
84
+ class Error < StandardError; end
85
+
86
+ # Mixin for all post relatedt errors
87
+ module PostError; end
88
+
89
+ # Raised if not logged in but has to do some register user only stuff.
90
+ # (Mainly changing interface language / time format)
91
+ class NotLoggedInError < Error; end
92
+
93
+ # Error raised if login fails
94
+ class LoginError < Error; include PostError; end
95
+
96
+ # Error raised, when some page related errors occur.
97
+ class PageError < Error; end
98
+
99
+ # Raised if writing to a page failed.
100
+ class WritePageError < PageError; include PostError; end
101
+
102
+ # Raised if moving a page failed.
103
+ class MovePageError < PageError; include PostError; end
104
+
105
+ # Error directly from MediaWiki API
106
+ class ApiError < Error; end
107
+
108
+ # Server access
109
+ #
110
+ # Examples:
111
+ # Rubot[:en] # same as Rubot[:en, :default] unless Rubot.default_family was changed
112
+ # Rubot[:de, :wikipedia]
113
+ def Rubot.[](iw, family = Rubot.default_family)
114
+ Server.server[family.to_sym][iw.to_sym] rescue nil
115
+ end
116
+
117
+ # Default family for wikis. See Rubot.default_family=
118
+ def Rubot.default_family
119
+ @@default_family ||= :default
120
+ @@default_family
121
+ end
122
+
123
+ # Sets default family
124
+ # Comes in very handy when handling a lot of wikis.
125
+ # Example:
126
+ #
127
+ # Rubot :en, :host => 'localhost'
128
+ # Rubot :en, :host => 'en.somewiki.tld', :wiki_family => :somewiki
129
+ # Rubot[:en] # should be local wiki
130
+ # Rubot.new(:de, :host => 'localhost').family # should be :default
131
+ # Rubot.default_family = :somewiki
132
+ # Rubot[:en] # should be wiki at somewiki.tld
133
+ # Rubot.new(:de, :host => 'de.somewiki.tld').family # should be :somewiki
134
+ # Rubot.default_family = :default
135
+ # Rubot[:de] # should be local wiki
136
+ #
137
+ # If you want to connect to multiple wikis Rubot.mass_connect should be very useful, too.
138
+ def Rubot.default_family=(family)
139
+ @@default_family = family
140
+ end
141
+
142
+ # Loops through server.
143
+ def Rubot.each_server
144
+ Server.server.each { |f,s| s.each { |iw, server| yield server } }
145
+ end
146
+
147
+ # Hash-like each_key
148
+ def Rubot.each_key
149
+ Server.server.each { |family,server| server.each_key { |interwiki| yield interwiki, family } }
150
+ end
151
+
152
+ # Loops through wiki families.
153
+ def Rubot.each_family
154
+ Server.server.each { |family, server| yield family, server }
155
+ end
156
+
157
+ # Array containing all servers.
158
+ def Rubot.server
159
+ (Server.server.collect { |k,v| v.values }).flatten
160
+ end
161
+
162
+ # Same as Rubot::Server.new
163
+ def Rubot.new(interwiki, options = {})
164
+ Server.new(interwiki, options)
165
+ end
166
+
167
+ # Used for "magic" values like in Rubot.mass_connect
168
+ #
169
+ # Examples:
170
+ #
171
+ # # 'a string'
172
+ # Rubot.value 'a string', :en
173
+ # Rubot.value { :en => 'a string', :fr => 'another string' }, :en
174
+ # Rubot.value lambda { |v| 'a string' if v == :en }, :en
175
+ def Rubot.value(var, *keys)
176
+ case var
177
+ when Proc
178
+ var.call *keys
179
+ when Hash
180
+ var[*keys]
181
+ when Array
182
+ keys.length == 1 and keys[0].is_a?(Integer) ? var[] : var
183
+ else
184
+ var
185
+ end
186
+ end
187
+
188
+ # The server class. It handels all the communication with the website.
189
+ # See Server.new
190
+ class Server
191
+
192
+ @@server ||= {}
193
+ @@default_adapter ||= nil
194
+ @@coder ||= HTMLEntities.new
195
+ @@english_namespaces ||= [ :main, :talk,
196
+ :user, :user_talk,
197
+ :project, :project_talk,
198
+ :image, :image_talk,
199
+ :mediawiki, :mediawiki_talk,
200
+ :template, :template_talk,
201
+ :help, :help_talk,
202
+ :category, :category_talk ]
203
+
204
+ # Returns hash of wikis
205
+ def self.server
206
+ @@server
207
+ end
208
+
209
+ attr_accessor :cookies, :preferences, :adapter; :use_api
210
+ attr_reader :interwiki, :log, :family
211
+
212
+
213
+ # Creats new Wiki object.
214
+ # Takes Interwiki name (for handling multiple Wikis) and an optional hash of options.
215
+ #
216
+ # Note: You can even use Rubot.new
217
+ #
218
+ # Examples:
219
+ #
220
+ # # MediaWiki on http://localhost/index.php
221
+ # wiki = Rubot::Server.new :en, :host => 'localhost'
222
+ #
223
+ # # Wikipedia - en and de
224
+ # wiki_en = Rubot::Server.new :en, :host => 'en.wikipedia.org',
225
+ # :path => '/w/index.php'
226
+ # wiki_de = Rubot::Server.new :de, :host => 'de.wikipedia.org',
227
+ # :path => '/w/index.php'
228
+ #
229
+ # # Wikipedia, smooth way for loading a couple of wikis
230
+ # # You can access those via Rubot[:en] etc.
231
+ # ['en', 'de', 'fr', 'se'].each do |iw|
232
+ # Rubot.new iw, :host => iw+'.wikipedia.org', :path => '/w/index.php'
233
+ # end
234
+ #
235
+ # # With login
236
+ # Rubot::Server.new :xy, :host => 'somehost',
237
+ # :username => 'WikiAdmin',
238
+ # :password => 'qwerty'
239
+ # Options:
240
+ # :host wiki host, has to be set!
241
+ # :port port (default is 80 or 443, depending on ssl)
242
+ # :ssl wheter to use ssl (default is false)
243
+ # :path path to index.php (default is '/index.php')
244
+ # :http_user set http user if wiki is protectet by basic authentication
245
+ # :http_pass set http password if wiki is protectet by basic authentication
246
+ # :proxy_host proxy host, will only be used if set (has to be http proxy!)
247
+ # :proxy_port proxy port, will only be used if set, :proxy_host has to be set
248
+ # in order to use this!
249
+ # :proxy_user proxy user, will only be used if set, :proxy_host has to be set
250
+ # in order to use this!
251
+ # :proxy_pass proxy password, will only be used if set, :proxy_host and
252
+ # :proxy_user has to be set in order to use this!
253
+ # :cookies Hash of preset cookies, I don't think you'll need that one!
254
+ # :username Name of your bot. Has to be an existing wiki user. If not set bot
255
+ # won't try to login and work anonymous. (It's recommended to have a
256
+ # dedicated account for the bot)
257
+ # :password Guess what! Only to be used when :username is set.
258
+ # :identify_as_bot Whether the Bot should identdfy himself as such or should pretent
259
+ # to be a web browser. (default is true)
260
+ # :auto_login Login when calling new (only if :username is set, default is true)
261
+ # :user_agent User agent to be sent to server (default depends on :identify_as_bot)
262
+ # :logger Logger to be used
263
+ # :delay Seconds to wait between to edits. When working on Wikimedia projects
264
+ # this must not be less than 12. It should be at least 1, else MediaWiki
265
+ # flood control or something will prevent edits. Default is 1.
266
+ # :wiki_family groups wikis in families (like pywiki does) for handling multiple
267
+ # "interwiki clouds", if not given, wiki will be added to Rubot.default_family
268
+ # :use_api Whether to use api.php instead of index.php. EXPERIMENTEL, but could increase
269
+ # both speed and stability. (default is false)
270
+ # :gzip Whether or not to use gzip compression for HTTP. (default is true)
271
+ def initialize(interwiki, options = {})
272
+
273
+ # Basic settings
274
+ @interwiki = interwiki.to_sym
275
+ @options = { :host => nil, :port => nil,
276
+ :ssl => false, :path => nil,
277
+ :http_user => nil, :http_pass => nil,
278
+ :proxy_host => nil, :proxy_port => nil,
279
+ :proxy_user => nil, :proxy_pass => nil,
280
+ :logger => nil, :cookies => {},
281
+ :username => nil, :password => nil,
282
+ :identify_as_bot => true, :auto_login => true,
283
+ :user_agent => nil, :delay => 1,
284
+ :wiki_family => Rubot.default_family, :dont_mutter => false,
285
+ :use_api => false, :gzip => true }.merge options.keys_to_sym
286
+ @preferences = { 'wpUserLanguage' => 'en', 'wpDate' => "ISO 8601",
287
+ 'wpOpexternaleditor' => nil, 'wpOppreviewonfirs' => nil }
288
+ @cookies = @options[:cookies].dup
289
+ @logged_in = false
290
+ @checked = false
291
+ @family = @options[:wiki_family].to_sym
292
+ @adapter = @@default_adapter
293
+
294
+ # Default path depends on :use_api
295
+ @options[:path] ||= use_api? ? '/api.php' : '/index.php'
296
+
297
+ # Logging
298
+ if @adapter
299
+ @log = @adapter.log
300
+ else
301
+ @log = @options.delete :logger
302
+ unless @log
303
+ @log = Logger.new(STDOUT)
304
+ @log.level = Logger::INFO
305
+ end
306
+ end
307
+
308
+ # Default port
309
+ unless @options[:port]
310
+ @options[:port] = 80 unless @options[:ssl]
311
+ @options[:port] = 443 if @options[:ssl]
312
+ end
313
+
314
+ # HTTP connection (via proxy if :proxy_host given)
315
+ @log.debug iw.to_s+': Creating HTTP connection'
316
+ @http = Net::HTTP::Proxy(@options[:proxy_host], @options[:proxy_port], @options[:proxy_user],
317
+ @options[:proxy_pass]).new(@options[:host], @options[:port])
318
+ @http.basic_auth(@options[:http_user], @options[:http_pass]) if @options[:http_user]
319
+
320
+ # identify as bot? sets user agent
321
+ unless @options[:user_agent]
322
+ if @options[:identify_as_bot]
323
+ @options[:user_agent] = "Mozilla/5.0 (compatible; Rubot; #{RUBY_PLATFORM}; en)"
324
+ else
325
+ @options[:user_agent] = case rand(4)
326
+ # Firefox on Ubuntu
327
+ when 0
328
+ "Mozilla/5.0 (X11; U; Linux i686; en;"+
329
+ " rv:1.8.1.12)Gecko/20080207 Ubuntu/7"+
330
+ ".10 (gutsy) Firefox/2.0.0.12"
331
+ # Safari on OSX
332
+ when 1
333
+ "Mozilla/5.0 (Macintosh; U; PPC Mac O"+
334
+ "S X; en-en) AppleWebKit/523.12.2 (KH"+
335
+ "TML, like Gecko) Version/3.0.4 Safar"+
336
+ "i/523.12.2"
337
+ # Opera on Windows
338
+ when 2
339
+ "Opera/9.10 (Windows NT 5.0; U; en)"
340
+ # Lynx
341
+ when 3
342
+ "Lynx/2.8.4rel.1 libwww-FM/2.14 SSL-M"+
343
+ "M/1.4.1 OpenSSL/0.9.6c"
344
+ end
345
+ end
346
+ end
347
+
348
+ @log.debug iw.to_s+': Initialized'
349
+
350
+ # login attemp, only if :username given (handled by login)
351
+ login if @options[:auto_login]
352
+
353
+ # For Rubot::Server.server
354
+ @@server[@family] ||= {}
355
+ @@server[@family][@interwiki] = self
356
+
357
+ end
358
+
359
+ # Whether or not api.php is used.
360
+ def use_api?
361
+ @options[:use_api]
362
+ end
363
+
364
+ # Send HTTP request.
365
+ # This is mainly for internal useage.
366
+ # Does all the cookie handling.
367
+ # Takes a hash with options.
368
+ #
369
+ # Options:
370
+ # :method :get, :post or :head (default is :get)
371
+ # :path Path to be requestet (default is set when calling new)
372
+ # :path_values Takes a hash of Data that will be added to the Path
373
+ # :data Data that will be transmitted
374
+ # :headers some additional header (might be overwritten, though)
375
+ # :follow_redirects whether to follow redirects (default is true)
376
+ # :follow_limit max. redirects to follow (default is 10)
377
+ # :body body to be sent (post only, if given :data will be ignored)
378
+ # :retries number of retries if request fails (NOT YET IMPLEMENTED)
379
+ # :gzip whether or not to use gzip compression for this request
380
+
381
+ def request(params = {})
382
+
383
+ # basic settings
384
+ params = { :method => :get, :path => @options[:path],
385
+ :path_values => {}, :data => {},
386
+ :headers => {}, :follow_redirects => true,
387
+ :follow_limit => 10, :body => nil,
388
+ :retries => 2, :gzip => @options[:gzip] }.merge(params.keys_to_sym!)
389
+
390
+ # catching if something went wrong with the path
391
+ params[:path] = @options[:path] unless params[:path]
392
+
393
+ # redirect handling
394
+ raise Error, 'HTTP redirect too deep' if params[:follow_limit] == 0
395
+
396
+ # headers, cookie handling
397
+ params[:headers].merge!({ 'Cookie' => (@cookies.collect do |key,value|
398
+ key.for_url+'='+value.to_s.for_url+';'
399
+ end).join(' '),
400
+ 'User-Agent' => @options[:user_agent] })
401
+ params[:headers]['Accept-Encoding'] = "gzip,deflate" if params[:gzip]
402
+ if params[:method] == :post
403
+ params[:headers]['Content-Type'] ||= 'application/x-www-form-urlencoded'
404
+ else
405
+ params[:path_values].merge! params[:data]
406
+ params[:data] = {}
407
+ end
408
+
409
+ # adding parameters to path
410
+ params[:path] += (case
411
+ when (params[:path_values].empty? or params[:path] =~ /(\?|&)$/)
412
+ ''
413
+ when params[:path] =~ /\?.+$/
414
+ '&'
415
+ else
416
+ '?'
417
+ end) + params[:path_values].to_url_data
418
+ params[:path_values] = {}
419
+
420
+ # handling data
421
+ data = (params[:body] or params[:data].to_url_data) if params[:method] == :post
422
+
423
+ # now do the request
424
+ @log.debug iw.to_s+": Sending request (#{params[:method]}, #{params[:path]})"
425
+ case params[:method]
426
+ when :get
427
+ response = @http.request_get params[:path], params[:headers]
428
+ when :post
429
+ response = @http.request_post params[:path], data, params[:headers]
430
+ when :head
431
+ response = @http.request_head params[:path], params[:headers]
432
+ else
433
+ raise ArgumentError, ':method must be :get, :post or :head'
434
+ end
435
+
436
+ # cookie handling, again
437
+ if response['set-cookie']
438
+ response.get_fields('set-cookie').each do |field|
439
+ field.scan(/([^; ]*)=([^;]*)/).each do |cookie|
440
+ unless ['path', 'expires'].include? cookie[0]
441
+ @log.debug iw.to_s+": Setting cookie #{cookie[0]} = #{cookie[1]}"
442
+ @cookies[cookie[0]] = cookie[1]
443
+ end
444
+ end
445
+ end
446
+ end
447
+
448
+ # Handle redirects / errors / gzip
449
+ case response
450
+ when Net::HTTPSuccess
451
+ body = response.body
452
+ if body and response['Content-Encoding'] == "gzip"
453
+ zstream = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
454
+ body = zstream.inflate body
455
+ zstream.finish
456
+ zstream.close
457
+ end
458
+ {:response => response, :request => params, :body => body }
459
+ when Net::HTTPRedirection
460
+ uri = URI.parse(response['location'])
461
+ path = uri.path
462
+ path += '?'+uri.query if uri.query
463
+ @log.debug iw.to_s+": Following redirect #{params[:path]} to #{path}"
464
+ request params.merge(:path => path, :follow_limit => params[:follow_limit]-1)
465
+ else
466
+ response.error!
467
+ end
468
+
469
+ end
470
+
471
+ # Does a request via api.php
472
+ #
473
+ # First parameter takes path_values, second parameter takes a hash like Server.request.
474
+ # If the third parameter (force) is true no error will be raised, even if use_api?.
475
+ #
476
+ # Note: If use_api? is false, force is true and no path is given Rubot tries to figure
477
+ # out the api path on it's own (simply replacing 'index.php' by 'api.php')
478
+ #
479
+ # The value for :action is by default set to "query"
480
+ #
481
+ # Thus
482
+ # Rubot[:en].api_request :action => 'help'
483
+ # is the same as
484
+ # Rubot[:en].api_request {}, :path_values => { :action => 'help' }
485
+ def api_request(path_values = {}, params = {}, force = false)
486
+ params.keys_to_sym!
487
+ unless use_api?
488
+ raise Error, "Trying to do some MediaWiki API request without using MediaWik API. Try :use_api => true" unless force
489
+ params[:path] ||= @options[:path].sub 'index.php', 'api.php'
490
+ end
491
+ params[:path_values].keys_to_sym!
492
+ params[:path_values] ||= {}
493
+ params[:path_values].merge!(path_values.keys_to_sym.merge({:format=>'yaml'}))
494
+ params[:path_values][:action] ||= 'query'
495
+ result = request(params)
496
+ begin
497
+ data = YAML::load result[:body]
498
+ rescue ArgumentError
499
+ begin
500
+ data = YAML::load result[:body][/---.*\n \*: >/m][0..-8]
501
+ rescue NoMethodError
502
+ if result[:body] =~ /&lt;error code=&quot;(.*)&quot; info=&quot;(.*)&quot;/
503
+ data = {'error' => { 'code' => $1, 'info' => $2 } }
504
+ else
505
+ raise NoMethodError, $!
506
+ end
507
+ end
508
+ end
509
+ data ||= {}
510
+ raise ApiError, "#{data["error"]["code"]}: #{data["error"]["info"] or 'no message'} (#{result[:request][:path]})" if data["error"]
511
+ data
512
+ end
513
+
514
+ # Does a request and returns Hpricot object of body (DRY)
515
+ def hpricot(params = {})
516
+ Hpricot(request(params)[:body])
517
+ end
518
+
519
+ # Loads form data, see request and Rubot::Form.new
520
+ def get_form(handle = :form, params = {})
521
+ @log.debug iw.to_s+': Loading form data'
522
+ page = request(params)
523
+ Form.new page[:body], handle, page[:request][:path]
524
+ end
525
+
526
+ # Submits a form, takes optional request parameters if not given by request_data
527
+ # (i.e. :follow_redirects), for options see request
528
+ def submit_form(form, default_params = {})
529
+ @log.debug iw.to_s+': Sending form data'
530
+ request form.request_data.merge(default_params)
531
+ end
532
+
533
+ # MediaWiki Version. If parameter is true version number will be transformed to be comparable.
534
+ def mw_version(transform = false)
535
+ unless @version
536
+ if use_api?
537
+ @version = api(:meta => 'siteinfo')['query']['general']['generator'][/\d+\.\d+.\d(alpha|beta)?/]
538
+ else
539
+ @version = 'unknown'
540
+ doc = hpricot :path_values => {:title => 'Special:Version' }
541
+ (doc / :script).each { |script| version = $1 if script.inner_html =~ /var wgVersion = "(\w+)";/ }
542
+ if @version == 'unknown'
543
+ (doc / "#bodyContent tr" ).each do |tr|
544
+ try NoMethodError do
545
+ if (tr / :td)[0].at('a').inner_html == 'MediaWiki' and (tr / :td)[1].inner_html =~ /(\d+\.\d+.\d(alpha|beta)?)/
546
+ @version = $1
547
+ break
548
+ end
549
+ end
550
+ end
551
+ end
552
+ if @version == 'unknown'
553
+ (doc / "#bodyContent li" ).each do |li|
554
+ try NoMethodError do
555
+ if li.inner_html.gsub(/<[^>]*>/, '') =~ /MediaWiki: (\d+\.\d+.\d(alpha|beta)?)/
556
+ @version = $1
557
+ break
558
+ end
559
+ end
560
+ end
561
+ end
562
+ end
563
+ end
564
+ transform ? @version.gsub(/\.(\d)\./, '.0\1.').gsub(/\.(\d)\D*$/, '.0\1') : @version
565
+ end
566
+
567
+ # Try to login. False will prevent login from calling check_preferences.
568
+ def login(check = true)
569
+ @logged_in = false
570
+ if @options[:username]
571
+ @log.info iw.to_s+': Trying to login as '+@options[:username]
572
+ if use_api?
573
+ data = api :action => :login, :lgname => @options[:username], :lgpassword => @options[:password]
574
+ if data['login']['result'] != 'Success'
575
+ @log.warn "#{iw}: Login failed: #{data['result']['login']}"
576
+ raise LoginError, "failed to login (#{data['result']['login']})"
577
+ else
578
+ @logged_in == true
579
+ end
580
+ else
581
+ form = get_form "form[@name='userlogin']", :path_values => {:title => 'Special:Userlogin' }
582
+ form['wpName'] = @options[:username]
583
+ form['wpPassword'] = @options[:password]
584
+ form['wpRemember'] = '1'
585
+ form.submit = 'wpLoginattempt'
586
+ page = submit_form form
587
+ doc = Hpricot page[:body]
588
+ if doc.at('.errorbox')
589
+ @log.warn iw.to_s+': Login failed:'+doc.at('.errorbox').to_s.gsub(/<[^>]*>/, '').gsub(/\n|\t/, ' ').squeeze
590
+ raise LoginError, "failed to login"
591
+ else
592
+ @logged_in = true
593
+ check_preferences if check
594
+ end
595
+ end
596
+ end
597
+ @log.info iw.to_s+': Logged in!' if @logged_in
598
+ end
599
+
600
+ # Returns true if logged in, else false.
601
+ def logged_in?
602
+ try { @logged_in = (not [0, nil].include? api(:meta => 'userinfo')['query']['userinfo']['id']) }
603
+ @logged_in
604
+ end
605
+
606
+ # Checks preferences.
607
+ def check_preferences(force = false)
608
+ if use_api?
609
+ @log.debug iw.to_s+': Not checking preferences (using API).'
610
+ @checked = true
611
+ else
612
+ @log.warn iw.to_s+': Not checking preferences, need to login first!' unless logged_in?
613
+ if logged_in? and (!@checked or force)
614
+ @log.debug iw.to_s+': Checking preferences'
615
+ changes = []
616
+ form = get_form "form", :path_values => {:title => 'Special:Preferences' }
617
+ @preferences.each do |key,value|
618
+ if form[key] != value
619
+ form[key] = value
620
+ if value
621
+ changes.push "'#{key}' = '#{value}'"
622
+ else
623
+ changes.push "remove '#{key}'"
624
+ end
625
+ end
626
+ end
627
+ unless changes.empty?
628
+ @log.info iw.to_s+": Changing some preferences (#{changes.join(', ')})"
629
+ form.submit = 'wpSaveprefs'
630
+ submit_form form
631
+ end
632
+ @checked = true
633
+ end
634
+ end
635
+ end
636
+
637
+ # Reads contents of a page
638
+ def read_page(page)
639
+ @log.info iw.to_s+": Reading page '#{page}'"
640
+ if use_api?
641
+ result = api :prop => 'revisions', :titles => page, :rvprop => 'content'
642
+ try(NoMethodError) { return result['query']['pages'][0]['revisions'][0]['*'] }
643
+ "\n"
644
+ else
645
+ @@coder.decode(
646
+ hpricot(:path_values => {:title => page, :action => :edit, :internaledit => true}).at('#wpTextbox1').inner_html )
647
+ end
648
+ end
649
+
650
+ # Writes to a page
651
+ def write_page(page, text, summary, minor = false)
652
+ login unless logged_in?
653
+ if (Time.now - last_edit) < @options[:delay]
654
+ @log.debug iw.to_s+": Delay - editing another page is not yet permitted."
655
+ sleep((@options[:delay] - (Time.now - last_edit)).ceil)
656
+ end
657
+ if use_api?
658
+ raise NotImplementedError, "Writing pages with MediaWiki's build-in API is not yet possible."
659
+ else
660
+ @log.warn iw.to_s+": Writing to page '#{page}' without being logged in!" unless logged_in? or @options[:dont_mutter]
661
+ @log.debug iw.to_s+": Reading page '#{page}'"
662
+ form = get_form '#editform', :path_values => {:title => page, :action => :edit, :internaledit => true}
663
+ @log.info iw.to_s+": Writing page '#{page}'"
664
+ form['wpTextbox1'] = text
665
+ form['wpSummary'] = summary
666
+ form['wpMinoredit'] = "1" if minor
667
+ form.submit = 'wpSave'
668
+ page = submit_form form
669
+ doc = Hpricot page[:body]
670
+ if doc.at('.errorbox')
671
+ @log.error iw.to_s+": Failed to write to page '#{page}':"+
672
+ doc.at('.errorbox').to_s.gsub(/<[^>]*>/, '').gsub(/\n|\t/, ' ').squeeze
673
+ raise WritePageError, "Failed to write to page '#{page}'."
674
+ end
675
+ end
676
+ last_edit = Time.now
677
+ page
678
+ end
679
+
680
+ # Gets last edit time.
681
+ def last_edit
682
+ if @adapter and @adapter.respond_to? :last_edit
683
+ @adapter.last_edit self
684
+ else
685
+ @last_edit ||= Time.at 0
686
+ @last_edit
687
+ end
688
+ end
689
+
690
+ # Sets last edit time.
691
+ def last_edit=(time)
692
+ if @adapter and @adapter.respond_to? :set_last_edit
693
+ @adapter.set_last_edit self, time
694
+ else
695
+ @last_edit = time
696
+ end
697
+ end
698
+
699
+ # Gets page history. Offset is for recursion.
700
+ def page_history(page, offset = false)
701
+ raise NotImplementedError, "Reading page history with MediaWiki's build-in API is not yet possible." if use_api?
702
+ @log.info iw.to_s+": Reading page history for #{page}" unless offset
703
+ history = {}
704
+ if logged_in?
705
+ check_preferences
706
+ path_values = { :title => page, :action => :history }
707
+ path_values[:offset] = offset if offset
708
+ doc = hpricot :path_values => path_values
709
+ (doc / '#pagehistory li').each do |element|
710
+ id = element.at("input[@name='oldid']")[:value].to_i if element.at("input[@name='oldid']")
711
+ history[id] = {:user => element.at('.history-user').at('a').inner_html,
712
+ :oldid => id,
713
+ :comment => element.at('.comment'),
714
+ :time => Time.local(*(ParseDate.parsedate element.inner_html[/20\d\d-\d\d-\d\dT\d\d:\d\d:\d\d/])) }
715
+ end
716
+ (doc / 'a[@href]').each do |a|
717
+ if a.inner_html['next'] and a[:href] =~ /offset=(\d*)&amp;action=history/
718
+ history.merge! page_history(page, $1)
719
+ break
720
+ end
721
+ end
722
+ else
723
+ @log.error iw.to_s+": Will only read history, if logged in (cannot parse time otherwise)"
724
+ raise NotLoggedInError, "cannot read time format, need to log in."
725
+ end
726
+ offset ? history : history.values.sort_by { |a| a[:time] }
727
+ end
728
+
729
+ # Moves a page
730
+ def move(from, to, reason, keep_redirect = true)
731
+ raise NotImplementedError, "Moving pages with MediaWiki's build-in API is not yet possible." if use_api?
732
+ @log.info iw.to_s+": Moving page '#{from}' to '#{to}'"
733
+ login unless logged_in?
734
+ if (Time.now - last_edit) < @options[:delay]
735
+ @log.debug iw.to_s+": Delay - editing another page is not yet permitted."
736
+ sleep((@options[:delay] - (Time.now - last_edit)).ceil)
737
+ end
738
+ form = get_form "#movepage", :path_values => { :title => "Special:Movepage/#{from}"}
739
+ form['wpNewTitle'] = to
740
+ form['wpReason'] = reason
741
+ form.submit = 'wpMove'
742
+ page = submit_form form
743
+ doc = Hpricot page[:body]
744
+ if doc.at('.errorbox')
745
+ @log.error iw.to_s+": Failed to move page '#{from}' to '#{to}':"+
746
+ doc.at('.errorbox').to_s.gsub(/<[^>]*>/, '').gsub(/\n|\t/, ' ').squeeze
747
+ raise MovePageError, "Failed to move page '#{from}' to '#{to}'."
748
+ end
749
+ last_edit = Time.now
750
+ page
751
+ end
752
+
753
+ # Lists recent changes. If called first time, will list the last 500 changes, when called again will only
754
+ # list new changes (up to 500). The options given will be added directly to the path, so you can pass all
755
+ # you can pass to Special:Recentchanges in MediaWiki (from, limit, hideminor, days, etc). Note that limit
756
+ # cannot be more than 500 and days cannot be more than 30. All hide parameters take 0 or 1, from will be
757
+ # set automaticly depending on last request of recent_changes. However, if you overwrite it, it has to be
758
+ # in the format of YYMMDDhhmmss.
759
+ #
760
+ # Returns Hash with Arrays :edits, :deletes, :moves, :uploads, :protections and :user_rights.
761
+ #
762
+ # For more advanced handling of recent changes, use base/utils or Adapter.
763
+ def recent_changes(path_values = {})
764
+ raise NotImplementedError, "Reading recent changes with MediaWiki's build-in API is not yet possible." if use_api?
765
+ @log.info iw.to_s+": Reading recent changes"
766
+ check_preferences
767
+ @rc_from ||= ''
768
+ doc = hpricot :path_values => { :title => 'Special:Recentchanges', :from => @rc_from,
769
+ :limit => 500, :hidebots => 0,
770
+ :hideminor => 0, :hideanons => 0,
771
+ :hideliu => 0, :hidepatrolled => 0,
772
+ :hidemyself => 0, :days => 30 }.merge(path_values.keys_to_sym)
773
+ (doc / :script).each do |script|
774
+ if script.inner_html =~ /var wgUserLanguage = "(\w+)";/ and $1 != 'en'
775
+ @log.error iw.to_s+": Will only read recent changes, if interface language is english (can be solved be simply logging in!)"
776
+ raise NotLoggedInError, "cannot read language '#{$1}', need to log in."
777
+ return nil
778
+ end
779
+ end
780
+ (doc / 'a[@href]').each do |a|
781
+ if a[:href] =~ /from=(\d\d\d\d\d\d\d\d\d\d\d\d\d\d)/
782
+ @rc_from = $1
783
+ break
784
+ end
785
+ end
786
+ edits, deletes, moves, uploads, protections, user_rights = [], [], [], [], [], []
787
+ (doc / '.special li').each do |element|
788
+ case element.at('a').inner_html.downcase
789
+ when "diff", "hist"
790
+ edits.push( { :page => element.at('a')[:title],
791
+ :comment => ((element.at('.comment').inner_html[1..-2] if element.at('.comment')) or ""),
792
+ :user => ((element / :a).detect { |a| a.inner_html == 'Talk' })[:title][/[^:]*$/],
793
+ :new => (element.inner_html[0..5] == "(diff)"),
794
+ :minor => (element / '.minor' ).empty?,
795
+ :bot => (element / '.bot' ).empty?,
796
+ :oldid => ($1.to_i if element.at('a')[:href] =~ /oldid=(\d*)$/) })
797
+ when "deletion log"
798
+ deletes.push( { :user => ((element / :a).detect { |a| a.inner_html == 'Talk' })[:title][/[^:]*$/],
799
+ :comment => ((element.at('.comment').inner_html[1..-2] if element.at('.comment')) or ""),
800
+ :page => element.at('.comment a').inner_html })
801
+ when "move log"
802
+ moves.push( { :user => ((element / :a).detect { |a| a.inner_html == 'Talk' })[:title][/[^:]*$/],
803
+ :comment => ((element.at('.comment').inner_html[1..-2] if element.at('.comment')) or ""),
804
+ :from => (element / '.comment a')[0].inner_html,
805
+ :to => (element / '.comment a')[1].inner_html })
806
+ when "upload log"
807
+ uploads.push( { :user => ((element / :a).detect { |a| a.inner_html == 'Talk' })[:title][/[^:]*$/],
808
+ :comment => ((element.at('.comment').inner_html[1..-2] if element.at('.comment')) or ""),
809
+ :file => element.at('.comment a').inner_html })
810
+ when "protection log"
811
+ protections.push( { :user => ((element / :a).detect { |a| a.inner_html == 'Talk' })[:title][/[^:]*$/],
812
+ :comment => ((element.at('.comment').inner_html[1..-2] if element.at('.comment')) or ""),
813
+ :page => element.at('.comment a').inner_html,
814
+ :rules => element.at('.comment').inner_html[/\[.*\]\)/][1..-3].split(':').collect_hash do |rule|
815
+ {$1 => $2} if rule =~ /^(.*)=(.*)$/
816
+ end })
817
+ when "user rights log"
818
+ user_rights.push( { :user_1 => ((element / :a).detect { |a| a.inner_html == 'Talk' })[:title][/[^:]*$/],
819
+ :user_2 => ((element.at('.comment a').inner_html[/[^:]*$/] if element.at('.comment a')) or ""),
820
+ :comment => ((element.at('.comment').inner_html[1..-2] if element.at('.comment')) or "") })
821
+ end
822
+ end
823
+ { :edits => edits, :deletes => deletes,
824
+ :moves => moves, :uploads => uploads,
825
+ :protections => protections, :user_rights => user_rights }
826
+ end
827
+
828
+ # Uploads data
829
+ def upload(file_name, summary, data)
830
+ raise NotImplementedError, "Uploading files with MediaWiki's build-in API is not yet possible." if use_api?
831
+ @log.info iw.to_s+": Uploading file '#{file_name}'"
832
+ @log.warn "UPLOADS DON'T WORK RIGHT NOW! But have a try..."
833
+ form = get_form '#upload', :path_values => { :title => 'Special:Upload' }
834
+ form['wpDestFile'] = file_name
835
+ form['wpUploadFile'] = data
836
+ form['wpUploadDescription'] = summary
837
+ form['wpIgnoreWarning'] = 'true'
838
+ form.submit = 'wpUpload'
839
+ form.set_file_name 'wpUploadFile', file_name
840
+ form.set_mime_type 'wpUploadFile', MIME_TYPES[file_name[/[^\.]*$/]]
841
+ submit_form form
842
+ end
843
+
844
+ # Array of pages in a given Category
845
+ def category_pages(category, from = nil)
846
+ raise NotImplementedError, "Reading categories with MediaWiki's build-in API is not yet possible." if use_api?
847
+ @log.info iw.to_s+": Reading page list '#{category}'."
848
+ doc = hpricot :path_values => { :title => "Category:#{category}", :from => from }
849
+ (doc / :script).each do |script|
850
+ if script.inner_html =~ /var wgUserLanguage = "(\w+)";/ and $1 != 'en'
851
+ @log.error iw.to_s+": Will only read recent changes, if interface language is english (can be solved be simply logging in!)"
852
+ raise NotLoggedInError, "cannot read language '#{$1}', need to log in."
853
+ return nil
854
+ end
855
+ end
856
+ from = nil
857
+ from = @@coder.decode($1) if (doc / "a[@href]").detect { |a| a.inner_html['next'] and a[:href] =~ /from=(.*)$/ }
858
+ ((doc / 'li a').collect { |e| e[:title] if e[:title] == e.inner_html } +
859
+ (from ? [URI::decode(from).gsub(/[\+_]/, ' ')]+category_pages(category, from) : [])).compact.uniq.sort
860
+ end
861
+
862
+ # Localized namespaces.
863
+ #
864
+ # Examples:
865
+ # Rubot[:de].local_namespace(:Talk) # Could be something like "Diskussion"
866
+ # Rubot[:en].local_namespace(:Main) # Should be "(Main)"
867
+ def local_namespace(ns = :all, force_reload = false)
868
+ raise NotImplementedError, "Loading namespaces with MediaWiki's build-in API is not yet possible." if use_api?
869
+ if force_reload or not defined?(@ns)
870
+ @log.debug iw.to_s+": Loading localized namespaces."
871
+ elements = (hpricot(:path_values => { :title => 'Special:Search' }) / '#powersearch span')
872
+ elements = (hpricot(:path_values => { :title => 'Special:Search' }) / '#powersearch label') if elements.empty?
873
+ @ns = elements.collect_hash do |element|
874
+ { element.at('input')[:name][2..-1].to_i => @@coder.decode(element.inner_html.gsub(/<[^>]*>/, '')).gsub(/\302|\240/, " ").strip }
875
+ end
876
+ end
877
+ ns == :all ? @ns : @ns[ns_index(ns)].to_s
878
+ end
879
+
880
+ # Returns prefix like it's to be used in links
881
+ #
882
+ # Examples:
883
+ # Rubot[:en].namespace(:main) # => ''
884
+ # Rubot[:en].namespace(:talk) # => 'Talk:'
885
+ # Rubot[:de].namespace(:image) # => 'Bild:'
886
+ def namespace(ns)
887
+ lns(ns) =~ /^\(.*\)$/ ? '' : "#{lns(ns)}:"
888
+ end
889
+
890
+ # Returns namespace number
891
+ def namespace_index(ns)
892
+ case
893
+ when ns.is_a?(Integer)
894
+ ns
895
+ when ns =~ /^ns(\d*)$/
896
+ $1.to_i
897
+ when @@english_namespaces.include?(ns)
898
+ @@english_namespaces.index ns
899
+ when lns.include?(ns)
900
+ lns.index ns
901
+ when @@english_namespaces.include?(ns.to_s.downcase.to_sym)
902
+ @@english_namespaces.index ns.to_s.downcase.to_sym
903
+ else
904
+ (lns.detect { |key,value| value.to_s.downcase == ns.to_s.downcase })[0]
905
+ end
906
+ end
907
+
908
+ # Returns all pages in the given namespace.
909
+ def pages_in_namespace(ns = :main, from = '!')
910
+ raise NotImplementedError, "Listing pages in a namespace with MediaWiki's build-in API is not yet possible." if use_api?
911
+ doc = hpricot :path_values => { :title => "Special:Allpages", :namespace => ns_index(ns), :from => from }
912
+ new_from = nil
913
+ ary = ((doc / "td a").collect do |a|
914
+ if a[:title] == n(ns)+a.inner_html
915
+ new_from = a.inner_html
916
+ @@coder.decode a[:title]
917
+ end
918
+ end).compact.sort
919
+ ary += pages_in_namespace(ns, new_from) if new_from and new_from != from
920
+ ary.uniq.sort
921
+ end
922
+
923
+ # Returns all pages in all namespaces
924
+ def all_pages
925
+ (lns(:all).values.collect { |ns| pages_in_namespace ns }).flatten
926
+ end
927
+
928
+ # Sets log (should be a logger object).
929
+ def log=(logger)
930
+ if @adapter
931
+ @log = @adapter.log
932
+ log.warn("Could not set log for #{interwiki.to_s}, since logging is controlled by adapter.") unless logger == @log
933
+ else
934
+ @log = logger
935
+ end
936
+ end
937
+
938
+ # shortform
939
+ alias iw interwiki
940
+ alias ns_index namespace_index
941
+ alias lns local_namespace
942
+ alias n namespace
943
+ alias pages_in_category category_pages
944
+ alias api api_request
945
+
946
+ end
947
+
948
+
949
+
950
+ # Class for handling html forms, does not realy depend on Rubot stuff, so if you can use the result of
951
+ # request_data (which is a hash to be understood by Server.request), feel free to do so. Make sure
952
+ # to load hpricot, uri and mime/types.
953
+ class Form
954
+
955
+ include Enumerable
956
+
957
+ attr_accessor :method, :path, :force, :multipart, :boundary
958
+ attr_reader :submit
959
+
960
+ # Greps Formular for given source (handle should be css-like or XPath)
961
+ def initialize(source, handle, default_path)
962
+ @doc = Hpricot(source)
963
+ @form = @doc.at(handle)
964
+ @method = (@form[:method] || 'get').downcase.to_sym
965
+ @submit = nil
966
+ @force = false
967
+ @elements = {}
968
+ @@coder ||= HTMLEntities.new
969
+ @path = @@coder.decode((@form[:action] || default_path))
970
+ @multipart = false
971
+ @boundary = '349832898984244898448024464570528145'
972
+ (@form / :input).each do |element|
973
+ ignore = false
974
+ case (element[:type] || 'text').downcase.to_sym
975
+ when :text, :password, :hidden
976
+ @elements[element[:name]] = { :mode => :text, :type => element[:type], :value => element[:value] }
977
+ when :checkbox
978
+ @elements[element[:name]] = { :mode => :select, :type => element[:type],
979
+ :possible => [nil, element[:value]], :value => nil }
980
+ @elements[element[:name]][:value] = element[:value] if element[:checked]
981
+ when :radio
982
+ @elements[element[:name]] = { :possible => [] } unless @elements[element[:name]]
983
+ @elements[element[:name]][:mode] = :select
984
+ @elements[element[:name]][:type] = element[:type]
985
+ @elements[element[:name]][:value] = element[:value] if element[:checked]
986
+ @elements[element[:name]][:possible].push(element[:value])
987
+ when :submit, :image
988
+ @submit = element[:name] unless @submit
989
+ @elements[element[:name]] = { :mode => :submit, :type => element[:type],
990
+ :value => (element[:value] or element[:src]) }
991
+ when :file
992
+ @multipart = true
993
+ @method = :post
994
+ @elements[element[:name]] = { :mode => :file, :type => element[:type],
995
+ :value => '', :mime_type => 'text/plain',
996
+ :file_name => element[:name] }
997
+ when :reset, :button
998
+ ignore = true
999
+ end
1000
+ @elements[element[:name]][:disabled] = (element[:readonly] or element[:disabled]) != nil unless ignore
1001
+ end
1002
+ (@form / :textarea).each do |element|
1003
+ @elements[element[:name]] = { :mode => :text, :type => 'textarea', :value => element.inner_html,
1004
+ :disabled => (element[:readonly] or element[:disabled]) != nil }
1005
+ end
1006
+ (@form / :select).each do |element|
1007
+ @elements[element[:name]] = { :mode => :select, :type => 'select', :value => nil, :possible => [],
1008
+ :disabled => (element[:readonly] or element[:disabled]) != nil }
1009
+ (element / :option).each do |option|
1010
+ @elements[element[:name]][:possible].push((option[:value] or option.inner_html))
1011
+ @elements[element[:name]][:value] = (option[:value] or option.inner_html) if option[:selected]
1012
+ end
1013
+ end
1014
+ end
1015
+
1016
+ # sets submit field to be used
1017
+ def submit=(name)
1018
+ @submit = name if @elements[name] and @elements[name][:mode] == :submit
1019
+ end
1020
+
1021
+ # sets value of field +element+, if force is true even set if it wouldn't be possible
1022
+ def set_value(element, value, force = @force)
1023
+ @elements[element] = { :mode => :text, :type => nil } if !@elements[element] and force
1024
+ @elements[element][:value] = value if force or (!@elements[element][:disabled] and
1025
+ (@elements[element][:mode] == :text or
1026
+ @elements[element][:mode] == :file or
1027
+ (@elements[element][:mode] == :select and
1028
+ @elements[element][:possible].include? value)))
1029
+ end
1030
+
1031
+ # returns mime type of field
1032
+ def get_mime_type(element)
1033
+ @elements[element] ? @elements[element][:mime_type] : nil
1034
+ end
1035
+
1036
+ # sets mime type of field
1037
+ def set_mime_type(element, mime_type)
1038
+ @elements[element][:mime_type] = mime_type if @elements[element] and @elements[element][:mode] == :file
1039
+ end
1040
+
1041
+ # returns file name of field
1042
+ def get_file_name(element)
1043
+ @elements[element] ? @elements[element][:file_name] : nil
1044
+ end
1045
+
1046
+ # sets mime type of field
1047
+ def set_file_name(element, file_name)
1048
+ @elements[element][:file_name] = file_name if @elements[element] and @elements[element][:mode] == :file
1049
+ end
1050
+
1051
+ # returns value of given field
1052
+ def [](element)
1053
+ @elements[element] ? @elements[element][:value] : nil
1054
+ end
1055
+
1056
+ # alias for set_value
1057
+ def []=(element, value)
1058
+ set_value(element, value, @force)
1059
+ end
1060
+
1061
+ # loops through elements and passes name and value of fields
1062
+ def each(&block) # :yield: name, value
1063
+ @elements.each { |key, value| block.call key, value[:value] }
1064
+ end
1065
+
1066
+ # Hash-like
1067
+ def each_value(&block)
1068
+ @elements.each_value { |value| block.call value }
1069
+ end
1070
+
1071
+ # Hash-like
1072
+ def each_key(&block)
1073
+ @elements.each_key { |key| block.call key }
1074
+ end
1075
+
1076
+ # returns true if element is readonly
1077
+ def readonly? element
1078
+ @elements[element][:disabled]
1079
+ end
1080
+
1081
+ # returns false if element is readonly
1082
+ def writable? element
1083
+ not readonly? element
1084
+ end
1085
+
1086
+ # Data for request
1087
+ def request_data
1088
+ if @multipart
1089
+ data = []
1090
+ @elements.each do |name,element|
1091
+ unless element[:mode] == :select and element[:value] == nil
1092
+ if element[:mode] == :file
1093
+ data.push "Content-Disposition: form-data; name=\"#{name.for_url}\"; filename=\"#{element[:file_name]}\"\r\n" +
1094
+ "Content-Transfer-Encoding: binary\r\n" + "Content-Type: #{element[:mime_type]}\r\n" +
1095
+ "\r\n#{element[:value]}\r\n"
1096
+ elsif element[:mode] != :submit or @submit == name
1097
+ data.push "Content-Disposition: form-data; name=\"#{name.for_url}\"\r\n" +
1098
+ "\r\n#{element[:value]}\r\n"
1099
+ end
1100
+ end
1101
+ end
1102
+ data.push "Content-Disposition: form-data; name=\"submit\"\r\n\r\n#{@submit}\r\n"
1103
+ { :body => data.collect {|e| "--#{@boundary}\r\n#{e}"}.join('') + "--#{@boundary}--\r\n",
1104
+ :headers => {"Content-Type" => "multipart/form-data; boundary=" + @boundary},
1105
+ :method => @method,
1106
+ :path => @path }
1107
+ else
1108
+ data = {}
1109
+ @elements.each do |name,element|
1110
+ data[name] = element[:value] if (element[:mode] == :submit and @submit == name) or
1111
+ (element[:mode] == :select and element[:value] != nil) or
1112
+ element[:mode] == :text
1113
+ end
1114
+ data[:submit] = @submit
1115
+ { :method => @method, :path => @path, :data => data }
1116
+ end
1117
+ end
1118
+
1119
+ end
1120
+
1121
+ end
1122
+
1123
+
1124
+
1125
+ # Monkey patching
1126
+ class Object
1127
+ def for_url
1128
+ ERB::Util.url_encode(self.to_s)
1129
+ end
1130
+ end
1131
+
1132
+
1133
+ # Monkey patching
1134
+ class Array
1135
+
1136
+ def collect_hash
1137
+ result = {}
1138
+ self.each do |value|
1139
+ subhash = yield value
1140
+ result.merge!(subhash) if subhash.is_a? Hash
1141
+ end
1142
+ result
1143
+ end
1144
+
1145
+ def for_all?(&block)
1146
+ result = true
1147
+ self.each { |value| result = (result and block.call value) }
1148
+ result
1149
+ end
1150
+
1151
+ def exists?(&block)
1152
+ result = false
1153
+ self.each { |value| result = (result or block.call value) }
1154
+ result
1155
+ end
1156
+
1157
+ end
1158
+
1159
+
1160
+ # Monkey patching
1161
+ class Hash
1162
+ def to_url_data
1163
+ (self.collect { |key,value| key.to_s.for_url+'='+value.to_s.for_url }).join('&')
1164
+ end
1165
+
1166
+ def keys_to_sym
1167
+ self.to_a.collect_hash { |a| { a[0].to_sym => a[1] } }
1168
+ end
1169
+
1170
+ def keys_to_sym!
1171
+ self.each_key { |key| self[key.to_sym] = self.delete key }
1172
+ end
1173
+
1174
+ def collect_hash
1175
+ result = {}
1176
+ self.each do |key,value|
1177
+ subhash = yield key, value
1178
+ result.merge!(subhash) if subhash.is_a? Hash
1179
+ end
1180
+ result
1181
+ end
1182
+ end