mediawiki-gateway 0.6.2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +22 -0
  3. data/ChangeLog +16 -0
  4. data/README.md +80 -21
  5. data/Rakefile +28 -34
  6. data/bin/mediawiki-gateway +203 -0
  7. data/lib/media_wiki.rb +4 -9
  8. data/lib/media_wiki/exception.rb +11 -8
  9. data/lib/media_wiki/fake_wiki.rb +636 -0
  10. data/lib/media_wiki/gateway.rb +105 -940
  11. data/lib/media_wiki/gateway/files.rb +173 -0
  12. data/lib/media_wiki/gateway/pages.rb +400 -0
  13. data/lib/media_wiki/gateway/query.rb +98 -0
  14. data/lib/media_wiki/gateway/site.rb +101 -0
  15. data/lib/media_wiki/gateway/users.rb +182 -0
  16. data/lib/media_wiki/utils.rb +47 -13
  17. data/lib/media_wiki/version.rb +27 -0
  18. data/lib/mediawiki-gateway.rb +1 -0
  19. data/spec/{import-test-data.xml → data/import.xml} +0 -0
  20. data/spec/media_wiki/gateway/files_spec.rb +34 -0
  21. data/spec/media_wiki/gateway/pages_spec.rb +390 -0
  22. data/spec/media_wiki/gateway/query_spec.rb +84 -0
  23. data/spec/media_wiki/gateway/site_spec.rb +122 -0
  24. data/spec/media_wiki/gateway/users_spec.rb +171 -0
  25. data/spec/media_wiki/gateway_spec.rb +129 -0
  26. data/spec/{live_gateway_spec.rb → media_wiki/live_gateway_spec.rb} +31 -35
  27. data/spec/{utils_spec.rb → media_wiki/utils_spec.rb} +41 -39
  28. data/spec/spec_helper.rb +17 -16
  29. metadata +77 -135
  30. data/.ruby-version +0 -1
  31. data/.rvmrc +0 -34
  32. data/Gemfile +0 -19
  33. data/Gemfile.lock +0 -77
  34. data/LICENSE +0 -21
  35. data/config/hosts.yml +0 -17
  36. data/lib/media_wiki/config.rb +0 -69
  37. data/mediawiki-gateway.gemspec +0 -113
  38. data/samples/README +0 -18
  39. data/samples/create_page.rb +0 -13
  40. data/samples/delete_batch.rb +0 -14
  41. data/samples/download_batch.rb +0 -15
  42. data/samples/email_user.rb +0 -14
  43. data/samples/export_xml.rb +0 -14
  44. data/samples/get_page.rb +0 -11
  45. data/samples/import_xml.rb +0 -14
  46. data/samples/run_fake_media_wiki.rb +0 -8
  47. data/samples/search_content.rb +0 -12
  48. data/samples/semantic_query.rb +0 -17
  49. data/samples/upload_commons.rb +0 -45
  50. data/samples/upload_file.rb +0 -13
  51. data/spec/fake_media_wiki/api_pages.rb +0 -135
  52. data/spec/fake_media_wiki/app.rb +0 -360
  53. data/spec/fake_media_wiki/query_handling.rb +0 -136
  54. data/spec/gateway_spec.rb +0 -888
@@ -0,0 +1,173 @@
1
+ module MediaWiki
2
+
3
+ class Gateway
4
+
5
+ module Files
6
+
7
+ # Upload a file, or get the status of pending uploads. Several
8
+ # methods are available:
9
+ #
10
+ # * Upload file contents directly.
11
+ # * Have the MediaWiki server fetch a file from a URL, using the
12
+ # 'url' parameter
13
+ #
14
+ # Requires Mediawiki 1.16+
15
+ #
16
+ # Arguments:
17
+ # * [path] Path to file to upload. Set to nil if uploading from URL.
18
+ # * [options] Hash of additional options
19
+ #
20
+ # Note that queries using session keys must be done in the same login
21
+ # session as the query that originally returned the key (i.e. do not
22
+ # log out and then log back in).
23
+ #
24
+ # Options:
25
+ # * 'filename' - Target filename (defaults to local name if not given), options[:target] is alias for this.
26
+ # * 'comment' - Upload comment. Also used as the initial page text for new files if 'text' is not specified.
27
+ # * 'text' - Initial page text for new files
28
+ # * 'watch' - Watch the page
29
+ # * 'ignorewarnings' - Ignore any warnings
30
+ # * 'url' - Url to fetch the file from. Set path to nil if you want to use this.
31
+ #
32
+ # Deprecated but still supported options:
33
+ # * :description - Description of this file. Used as 'text'.
34
+ # * :target - Target filename, same as 'filename'.
35
+ # * :summary - Edit summary for history. Used as 'comment'. Also used as 'text' if neither it or :description is specified.
36
+ #
37
+ # Examples:
38
+ # mw.upload('/path/to/local/file.jpg', 'filename' => 'RemoteFile.jpg')
39
+ # mw.upload(nil, 'filename' => 'RemoteFile2.jpg', 'url' => 'http://remote.com/server/file.jpg')
40
+ #
41
+ def upload(path, options = {})
42
+ if options[:description]
43
+ options['text'] = options.delete(:description)
44
+ end
45
+
46
+ if options[:target]
47
+ options['filename'] = options.delete(:target)
48
+ end
49
+
50
+ if options[:summary]
51
+ options['text'] ||= options[:summary]
52
+ options['comment'] = options.delete(:summary)
53
+ end
54
+
55
+ options['comment'] ||= 'Uploaded by MediaWiki::Gateway'
56
+
57
+ options['file'] = File.new(path) if path
58
+
59
+ full_name = path || options['url']
60
+ options['filename'] ||= File.basename(full_name) if full_name
61
+
62
+ unless options['file'] || options['url'] || options['sessionkey']
63
+ raise ArgumentError,
64
+ "One of the 'file', 'url' or 'sessionkey' options must be specified!"
65
+ end
66
+
67
+ send_request(options.merge(
68
+ 'action' => 'upload',
69
+ 'token' => get_token('edit', options['filename'])
70
+ ))
71
+ end
72
+
73
+ # Get image list for given article[s]. Follows redirects.
74
+ #
75
+ # _article_or_pageid_ is the title or pageid of a single article
76
+ # _imlimit_ is the maximum number of images to return (defaults to 200)
77
+ # _options_ is the hash of additional options
78
+ #
79
+ # Example:
80
+ # images = mw.images('Gaborone')
81
+ # _images_ would contain ['File:Gaborone at night.jpg', 'File:Gaborone2.png', ...]
82
+ def images(article_or_pageid, imlimit = 200, options = {})
83
+ form_data = options.merge(
84
+ 'action' => 'query',
85
+ 'prop' => 'images',
86
+ 'imlimit' => imlimit,
87
+ 'redirects' => true
88
+ )
89
+
90
+ form_data[article_or_pageid.is_a?(Fixnum) ?
91
+ 'pageids' : 'titles'] = article_or_pageid
92
+
93
+ xml = send_request(form_data)
94
+
95
+ if valid_page?(page = xml.elements['query/pages/page'])
96
+ if xml.elements['query/redirects/r']
97
+ # We're dealing with redirect here.
98
+ images(page.attributes['pageid'].to_i, imlimit)
99
+ else
100
+ REXML::XPath.match(page, 'images/im').map { |x| x.attributes['title'] }
101
+ end
102
+ end
103
+ end
104
+
105
+ # Requests image info from MediaWiki. Follows redirects.
106
+ #
107
+ # _file_name_or_page_id_ should be either:
108
+ # * a file name (String) you want info about without File: prefix.
109
+ # * or a Fixnum page id you of the file.
110
+ #
111
+ # _options_ is +Hash+ passed as query arguments. See
112
+ # http://www.mediawiki.org/wiki/API:Query_-_Properties#imageinfo_.2F_ii
113
+ # for more information.
114
+ #
115
+ # options['iiprop'] should be either a string of properties joined by
116
+ # '|' or an +Array+ (or more precisely something that responds to #join).
117
+ #
118
+ # +Hash+ like object is returned where keys are image properties.
119
+ #
120
+ # Example:
121
+ # mw.image_info(
122
+ # 'Trooper.jpg', 'iiprop' => ['timestamp', 'user']
123
+ # ).each do |key, value|
124
+ # puts "#{key.inspect} => #{value.inspect}"
125
+ # end
126
+ #
127
+ # Output:
128
+ # "timestamp" => "2009-10-31T12:59:11Z"
129
+ # "user" => "Valdas"
130
+ #
131
+ def image_info(file_name_or_page_id, options = {})
132
+ if options['iiprop'].respond_to?(:join)
133
+ options['iiprop'] = options['iiprop'].join('|')
134
+ end
135
+
136
+ form_data = options.merge(
137
+ 'action' => 'query',
138
+ 'prop' => 'imageinfo',
139
+ 'redirects' => true
140
+ )
141
+
142
+ file_name_or_page_id.is_a?(Fixnum) ?
143
+ form_data['pageids'] = file_name_or_page_id :
144
+ form_data['titles'] = "File:#{file_name_or_page_id}"
145
+
146
+ xml = send_request(form_data)
147
+
148
+ if valid_page?(page = xml.elements['query/pages/page'])
149
+ if xml.elements['query/redirects/r']
150
+ # We're dealing with redirect here.
151
+ image_info(page.attributes['pageid'].to_i, options)
152
+ else
153
+ page.elements['imageinfo/ii'].attributes
154
+ end
155
+ end
156
+ end
157
+
158
+ # Download _file_name_ (without "File:" or "Image:" prefix). Returns file contents. All options are passed to
159
+ # #image_info however options['iiprop'] is forced to url. You can still
160
+ # set other options to control what file you want to download.
161
+ def download(file_name, options = {})
162
+ if attributes = image_info(file_name, options.merge('iiprop' => 'url'))
163
+ RestClient.get(attributes['url'])
164
+ end
165
+ end
166
+
167
+ end
168
+
169
+ include Files
170
+
171
+ end
172
+
173
+ end
@@ -0,0 +1,400 @@
1
+ module MediaWiki
2
+
3
+ class Gateway
4
+
5
+ module Pages
6
+
7
+ # Fetch MediaWiki page in MediaWiki format. Does not follow redirects.
8
+ #
9
+ # [page_title] Page title to fetch
10
+ # [options] Hash of additional options
11
+ #
12
+ # Returns content of page as string, nil if the page does not exist.
13
+ def get(page_title, options = {})
14
+ page = send_request(options.merge(
15
+ 'action' => 'query',
16
+ 'prop' => 'revisions',
17
+ 'rvprop' => 'content',
18
+ 'titles' => page_title
19
+ )).elements['query/pages/page']
20
+
21
+ page.elements['revisions/rev'].text || '' if valid_page?(page)
22
+ end
23
+
24
+ # Fetch latest revision ID of a MediaWiki page. Does not follow redirects.
25
+ #
26
+ # [page_title] Page title to fetch
27
+ # [options] Hash of additional options
28
+ #
29
+ # Returns revision ID as a string, nil if the page does not exist.
30
+ def revision(page_title, options = {})
31
+ page = send_request(options.merge(
32
+ 'action' => 'query',
33
+ 'prop' => 'revisions',
34
+ 'rvprop' => 'ids',
35
+ 'rvlimit' => 1,
36
+ 'titles' => page_title
37
+ )).elements['query/pages/page']
38
+
39
+ page.elements['revisions/rev'].attributes['revid'] if valid_page?(page)
40
+ end
41
+
42
+ # Render a MediaWiki page as HTML
43
+ #
44
+ # [page_title] Page title to fetch
45
+ # [options] Hash of additional options
46
+ #
47
+ # Options:
48
+ # * [:linkbase] supply a String to prefix all internal (relative) links with. '/wiki/' is assumed to be the base of a relative link
49
+ # * [:noeditsections] strips all edit-links if set to +true+
50
+ # * [:noimages] strips all +img+ tags from the rendered text if set to +true+
51
+ #
52
+ # Returns rendered page as string, or nil if the page does not exist
53
+ def render(page_title, options = {})
54
+ form_data = { 'action' => 'parse', 'page' => page_title }
55
+
56
+ validate_options(options, %w[linkbase noeditsections noimages])
57
+
58
+ rendered, parsed = nil, send_request(form_data).elements['parse']
59
+
60
+ if parsed.attributes['revid'] != '0'
61
+ rendered = parsed.elements['text'].text.gsub(/<!--(.|\s)*?-->/, '')
62
+
63
+ # OPTIMIZE: unifiy the keys in +options+ like symbolize_keys! but w/o
64
+ if linkbase = options['linkbase'] || options[:linkbase]
65
+ rendered = rendered.gsub(/\shref="\/wiki\/([\w\(\)\-\.%:,]*)"/, ' href="' + linkbase + '/wiki/\1"')
66
+ end
67
+
68
+ if options['noeditsections'] || options[:noeditsections]
69
+ rendered = rendered.gsub(/<span class="editsection">\[.+\]<\/span>/, '')
70
+ end
71
+
72
+ if options['noimages'] || options[:noimages]
73
+ rendered = rendered.gsub(/<img.*\/>/, '')
74
+ end
75
+ end
76
+
77
+ rendered
78
+ end
79
+
80
+ # Create a new page, or overwrite an existing one
81
+ #
82
+ # [title] Page title to create or overwrite, string
83
+ # [content] Content for the page, string
84
+ # [options] Hash of additional options
85
+ #
86
+ # Options:
87
+ # * [:overwrite] Allow overwriting existing pages
88
+ # * [:summary] Edit summary for history, string
89
+ # * [:token] Use this existing edit token instead requesting a new one (useful for bulk loads)
90
+ # * [:minor] Mark this edit as "minor" if true, mark this edit as "major" if false, leave major/minor status by default if not specified
91
+ # * [:notminor] Mark this edit as "major" if true
92
+ # * [:bot] Set the bot parameter (see http://www.mediawiki.org/wiki/API:Edit#Parameters). Defaults to false.
93
+ def create(title, content, options = {})
94
+ form_data = {
95
+ 'action' => 'edit',
96
+ 'title' => title,
97
+ 'text' => content,
98
+ 'summary' => options[:summary] || '',
99
+ 'token' => get_token('edit', title)
100
+ }
101
+
102
+ if @options[:bot] || options[:bot]
103
+ form_data.update('bot' => '1', 'assert' => 'bot')
104
+ end
105
+
106
+ form_data['minor'] = '1' if options[:minor]
107
+ form_data['notminor'] = '1' if options[:minor] == false || options[:notminor]
108
+ form_data['createonly'] = '' unless options[:overwrite]
109
+ form_data['section'] = options[:section].to_s if options[:section]
110
+
111
+ send_request(form_data)
112
+ end
113
+
114
+ # Edit page
115
+ #
116
+ # Same options as create, but always overwrites existing pages (and creates them if they don't exist already).
117
+ def edit(title, content, options = {})
118
+ create(title, content, { overwrite: true }.merge(options))
119
+ end
120
+
121
+ # Protect/unprotect a page
122
+ #
123
+ # Arguments:
124
+ # * [title] Page title to protect, string
125
+ # * [protections] Protections to apply, hash or array of hashes
126
+ #
127
+ # Protections:
128
+ # * [:action] (required) The action to protect, string
129
+ # * [:group] (required) The group allowed to perform the action, string
130
+ # * [:expiry] The protection expiry as a GNU timestamp, string
131
+ #
132
+ # * [options] Hash of additional options
133
+ #
134
+ # Options:
135
+ # * [:cascade] Protect pages included in this page, boolean
136
+ # * [:reason] Reason for protection, string
137
+ #
138
+ # Examples:
139
+ # 1. mw.protect('Main Page', {:action => 'edit', :group => 'all'}, {:cascade => true})
140
+ # 2. prt = [{:action => 'move', :group => 'sysop', :expiry => 'never'},
141
+ # {:action => 'edit', :group => 'autoconfirmed', :expiry => 'next Monday 16:04:57'}]
142
+ # mw.protect('Main Page', prt, {:reason => 'awesomeness'})
143
+ #
144
+ def protect(title, protections, options = {})
145
+ case protections
146
+ when Array
147
+ # ok
148
+ when Hash
149
+ protections = [protections]
150
+ else
151
+ raise ArgumentError, "Invalid type '#{protections.class}' for protections"
152
+ end
153
+
154
+ valid_prt_options = %w[action group expiry]
155
+ required_prt_options = %w[action group]
156
+
157
+ p, e = [], []
158
+
159
+ protections.each { |prt|
160
+ existing_prt_options = []
161
+
162
+ prt.each_key { |opt|
163
+ if valid_prt_options.include?(opt.to_s)
164
+ existing_prt_options << opt.to_s
165
+ else
166
+ raise ArgumentError, "Unknown option '#{opt}' for protections"
167
+ end
168
+ }
169
+
170
+ required_prt_options.each { |opt|
171
+ unless existing_prt_options.include?(opt)
172
+ raise ArgumentError, "Missing required option '#{opt}' for protections"
173
+ end
174
+ }
175
+
176
+ p << "#{prt[:action]}=#{prt[:group]}"
177
+ e << (prt.key?(:expiry) ? prt[:expiry].to_s : 'never')
178
+ }
179
+
180
+ validate_options(options, %w[cascade reason])
181
+
182
+ form_data = {
183
+ 'action' => 'protect',
184
+ 'title' => title,
185
+ 'token' => get_token('protect', title),
186
+ 'protections' => p.join('|'),
187
+ 'expiry' => e.join('|')
188
+ }
189
+
190
+ form_data['cascade'] = '' if options[:cascade] == true
191
+ form_data['reason'] = options[:reason].to_s if options[:reason]
192
+
193
+ send_request(form_data)
194
+ end
195
+
196
+ # Move a page to a new title
197
+ #
198
+ # [from] Old page name
199
+ # [to] New page name
200
+ # [options] Hash of additional options
201
+ #
202
+ # Options:
203
+ # * [:movesubpages] Move associated subpages
204
+ # * [:movetalk] Move associated talkpages
205
+ # * [:noredirect] Do not create a redirect page from old name. Requires the 'suppressredirect' user right, otherwise MW will silently ignore the option and create the redirect anyway.
206
+ # * [:reason] Reason for move
207
+ # * [:watch] Add page and any redirect to watchlist
208
+ # * [:unwatch] Remove page and any redirect from watchlist
209
+ def move(from, to, options = {})
210
+ validate_options(options, %w[movesubpages movetalk noredirect reason watch unwatch])
211
+
212
+ send_request(options.merge(
213
+ 'action' => 'move',
214
+ 'from' => from,
215
+ 'to' => to,
216
+ 'token' => get_token('move', from)
217
+ ))
218
+ end
219
+
220
+ # Delete one page. (MediaWiki API does not support deleting multiple pages at a time.)
221
+ #
222
+ # [title] Title of page to delete
223
+ # [options] Hash of additional options
224
+ def delete(title, options = {})
225
+ send_request(options.merge(
226
+ 'action' => 'delete',
227
+ 'title' => title,
228
+ 'token' => get_token('delete', title)
229
+ ))
230
+ end
231
+
232
+ # Undelete all revisions of one page.
233
+ #
234
+ # [title] Title of page to undelete
235
+ # [options] Hash of additional options
236
+ #
237
+ # Returns number of revisions undeleted, or zero if nothing to undelete
238
+ def undelete(title, options = {})
239
+ if token = get_undelete_token(title)
240
+ send_request(options.merge(
241
+ 'action' => 'undelete',
242
+ 'title' => title,
243
+ 'token' => token
244
+ )).elements['undelete'].attributes['revisions'].to_i
245
+ else
246
+ 0 # No revisions to undelete
247
+ end
248
+ end
249
+
250
+ # Get a list of matching page titles in a namespace
251
+ #
252
+ # [key] Search key, matched as a prefix (^key.*). May contain or equal a namespace, defaults to main (namespace 0) if none given.
253
+ # [options] Optional hash of additional options, eg. { 'apfilterredir' => 'nonredirects' }. See http://www.mediawiki.org/wiki/API:Allpages
254
+ #
255
+ # Returns array of page titles (empty if no matches)
256
+ def list(key, options = {})
257
+ key, namespace = key.split(':', 2).reverse
258
+ namespace = namespaces_by_prefix[namespace] || 0
259
+
260
+ iterate_query('allpages', '//p', 'title', 'apfrom', options.merge(
261
+ 'list' => 'allpages',
262
+ 'apprefix' => key,
263
+ 'apnamespace' => namespace,
264
+ 'aplimit' => @options[:limit]
265
+ ))
266
+ end
267
+
268
+ # Get a list of pages that are members of a category
269
+ #
270
+ # [category] Name of the category
271
+ # [options] Optional hash of additional options. See http://www.mediawiki.org/wiki/API:Categorymembers
272
+ #
273
+ # Returns array of page titles (empty if no matches)
274
+ def category_members(category, options = {})
275
+ iterate_query('categorymembers', '//cm', 'title', 'cmcontinue', options.merge(
276
+ 'cmtitle' => category,
277
+ 'cmlimit' => @options[:limit]
278
+ ))
279
+ end
280
+
281
+ # Get a list of pages that link to a target page
282
+ #
283
+ # [title] Link target page
284
+ # [filter] 'all' links (default), 'redirects' only, or 'nonredirects' (plain links only)
285
+ # [options] Hash of additional options
286
+ #
287
+ # Returns array of page titles (empty if no matches)
288
+ def backlinks(title, filter = 'all', options = {})
289
+ iterate_query('backlinks', '//bl', 'title', 'blcontinue', options.merge(
290
+ 'bltitle' => title,
291
+ 'blfilterredir' => filter,
292
+ 'bllimit' => @options[:limit]
293
+ ))
294
+ end
295
+
296
+ # Checks if page is a redirect.
297
+ #
298
+ # [page_title] Page title to fetch
299
+ #
300
+ # Returns true if the page is a redirect, false if it is not or the page does not exist.
301
+ def redirect?(page_title)
302
+ page = send_request(
303
+ 'action' => 'query',
304
+ 'prop' => 'info',
305
+ 'titles' => page_title
306
+ ).elements['query/pages/page']
307
+
308
+ !!(valid_page?(page) && page.attributes['redirect'])
309
+ end
310
+
311
+ # Get list of interlanguage links for given article[s]. Follows redirects. Returns a hash like { 'id' => 'Yerusalem', 'en' => 'Jerusalem', ... }
312
+ #
313
+ # _article_or_pageid_ is the title or pageid of a single article
314
+ # _lllimit_ is the maximum number of langlinks to return (defaults to 500, the maximum)
315
+ # _options_ is the hash of additional options
316
+ #
317
+ # Example:
318
+ # langlinks = mw.langlinks('Jerusalem')
319
+ def langlinks(article_or_pageid, lllimit = 500, options = {})
320
+ form_data = options.merge(
321
+ 'action' => 'query',
322
+ 'prop' => 'langlinks',
323
+ 'lllimit' => lllimit,
324
+ 'redirects' => true
325
+ )
326
+
327
+ form_data[article_or_pageid.is_a?(Fixnum) ?
328
+ 'pageids' : 'titles'] = article_or_pageid
329
+
330
+ xml = send_request(form_data)
331
+
332
+ if valid_page?(page = xml.elements['query/pages/page'])
333
+ if xml.elements['query/redirects/r']
334
+ # We're dealing with the redirect here.
335
+ langlinks(page.attributes['pageid'].to_i, lllimit)
336
+ elsif langl = REXML::XPath.match(page, 'langlinks/ll')
337
+ langl.each_with_object({}) { |ll, links|
338
+ links[ll.attributes['lang']] = ll.children[0].to_s
339
+ }
340
+ end
341
+ end
342
+ end
343
+
344
+ # Convenience wrapper for _langlinks_ returning the title in language _lang_ (ISO code) for a given article of pageid, if it exists, via the interlanguage link
345
+ #
346
+ # Example:
347
+ #
348
+ # langlink = mw.langlink_for_lang('Tycho Brahe', 'de')
349
+ def langlink_for_lang(article_or_pageid, lang)
350
+ langlinks(article_or_pageid)[lang]
351
+ end
352
+
353
+ # Review current revision of an article (requires FlaggedRevisions extension, see http://www.mediawiki.org/wiki/Extension:FlaggedRevs)
354
+ #
355
+ # [title] Title of article to review
356
+ # [flags] Hash of flags and values to set, eg. { 'accuracy' => '1', 'depth' => '2' }
357
+ # [comment] Comment to add to review (optional)
358
+ # [options] Hash of additional options
359
+ def review(title, flags, comment = 'Reviewed by MediaWiki::Gateway', options = {})
360
+ raise APIError.new('missingtitle', "Article #{title} not found") unless revid = revision(title)
361
+
362
+ form_data = options.merge(
363
+ 'action' => 'review',
364
+ 'revid' => revid,
365
+ 'token' => get_token('edit', title),
366
+ 'comment' => comment
367
+ )
368
+
369
+ flags.each { |k, v| form_data["flag_#{k}"] = v }
370
+
371
+ send_request(form_data)
372
+ end
373
+
374
+ private
375
+
376
+ def get_undelete_token(page_titles)
377
+ res = send_request(
378
+ 'action' => 'query',
379
+ 'list' => 'deletedrevs',
380
+ 'prop' => 'info',
381
+ 'drprop' => 'token',
382
+ 'titles' => page_titles
383
+ )
384
+
385
+ if res.elements['query/deletedrevs/page']
386
+ unless token = res.elements['query/deletedrevs/page'].attributes['token']
387
+ raise Unauthorized.new("User is not permitted to perform this operation: #{type}")
388
+ end
389
+
390
+ token
391
+ end
392
+ end
393
+
394
+ end
395
+
396
+ include Pages
397
+
398
+ end
399
+
400
+ end