mediawiki-gateway 0.6.2 → 1.0.0.rc1

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 (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