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
@@ -1,9 +1,4 @@
1
- require 'thread' # bizarre workaround for Rails 2.3.x/RubyGems incompatibility
2
- require File.dirname(__FILE__) + '/media_wiki/config'
3
- require File.dirname(__FILE__) + '/media_wiki/exception'
4
- require File.dirname(__FILE__) + '/media_wiki/utils'
5
- require File.dirname(__FILE__) + '/media_wiki/gateway'
6
-
7
- module MediaWiki
8
- VERSION = "0.6.2"
9
- end
1
+ require_relative 'media_wiki/version'
2
+ require_relative 'media_wiki/exception'
3
+ require_relative 'media_wiki/utils'
4
+ require_relative 'media_wiki/gateway'
@@ -1,26 +1,29 @@
1
1
  module MediaWiki
2
+
2
3
  # General exception occurred within MediaWiki::Gateway, and parent class for MediaWiki::APIError, MediaWiki::Unauthorized.
3
- class Exception < Exception
4
+ class Exception < ::Exception
4
5
  end
5
6
 
6
7
  # Wrapper for errors returned by MediaWiki API. Possible codes are defined in http://www.mediawiki.org/wiki/API:Errors_and_warnings.
7
8
  #
8
9
  # Warnings also throw errors with code 'warning', unless MediaWiki::Gateway#new was called with :ignorewarnings.
9
- class APIError < MediaWiki::Exception
10
+ class APIError < Exception
11
+
10
12
  attr_reader :code, :info, :message
11
-
13
+
12
14
  def initialize(code, info)
13
- @code = code
14
- @info = info
15
- @message = "API error: code '#{code}', info '#{info}'"
15
+ @code, @info, @message = code, info,
16
+ "API error: code '#{code}', info '#{info}'"
16
17
  end
17
18
 
18
19
  def to_s
19
- "#{self.class.to_s}: #{@message}"
20
+ "#{self.class}: #{@message}"
20
21
  end
22
+
21
23
  end
22
24
 
23
25
  # User is not authorized to perform this operation. Also thrown if MediaWiki::Gateway#login fails.
24
- class Unauthorized < MediaWiki::Exception
26
+ class Unauthorized < Exception
25
27
  end
28
+
26
29
  end
@@ -0,0 +1,636 @@
1
+ require 'sinatra/base'
2
+ require 'sham_rack'
3
+ require 'nokogiri'
4
+
5
+ module MediaWiki
6
+
7
+ # A simple Rack app that stubs out a web service, for testing.
8
+
9
+ module FakeWiki
10
+
11
+ class App < Sinatra::Base
12
+
13
+ set :show_exceptions, false
14
+ set :environment, :development
15
+
16
+ def initialize
17
+ reset
18
+ super
19
+ end
20
+
21
+ def reset
22
+ @sequence_id = 0
23
+
24
+ @users = {}
25
+ add_user('atlasmw', 'wombat', 'local', true)
26
+ add_user('nonadmin', 'sekrit', 'local', false)
27
+ add_user('ldapuser', 'ldappass', 'ldapdomain', false)
28
+
29
+ @pages = ApiPages.new
30
+ @pages.add('Main Page', 'Content')
31
+ @pages.add('Main 2', 'Content')
32
+ @pages.add('Empty', '')
33
+ @pages.add('Level/Level/Index', '{{#include:Foo}} {{#include:Bar}}')
34
+ @pages.add_namespace(100, "Book")
35
+ @pages.add('Book:Italy', 'Introduction')
36
+ @pages.add_namespace(200, "Sandbox")
37
+ @pages.add('Foopage', 'Content')
38
+ @pages.add('Redirect', '#REDIRECT', true)
39
+
40
+ @extensions = { 'FooExtension' => 'r1', 'BarExtension' => 'r2', 'Semantic MediaWiki' => '1.5' }
41
+
42
+ @logged_in_users = []
43
+ end
44
+
45
+ def next_id
46
+ @sequence_id += 1
47
+ end
48
+
49
+ def add_user(username, password, domain, is_admin)
50
+ @users[username] = {
51
+ :userid => next_id,
52
+ :username => username,
53
+ :password => password,
54
+ :domain => domain,
55
+ :is_admin => is_admin
56
+ }
57
+ end
58
+
59
+ def logged_in(username)
60
+ @logged_in_users.include?(username)
61
+ end
62
+
63
+ get "/w/api.php" do
64
+ handle_request if params[:action] == 'query'
65
+ end
66
+
67
+ post "/w/api.php" do
68
+ handle_request
69
+ end
70
+
71
+ def handle_request
72
+ begin
73
+ halt(503, "Maxlag exceeded") if params[:maxlag].to_i < 0
74
+
75
+ @token = ApiToken.new(params)
76
+ action = params[:action]
77
+ if respond_to?(action)
78
+ content_type "application/xml"
79
+ return send(action)
80
+ end
81
+
82
+ halt(404, "Page not found")
83
+ rescue ApiError => e
84
+ return api_error_response(e.code, e.message)
85
+ end
86
+ end
87
+
88
+ def import
89
+ @token.validate_admin
90
+
91
+ api_response do |_|
92
+ _.import do
93
+ _.page(nil, :title => "Main Page", :ns => 0, :revisions => 0)
94
+ _.page(nil, :title => "Template:Header", :ns => 10, :revisions => 1)
95
+ end
96
+ end
97
+ end
98
+
99
+ def validate_page_overwrite(current_page)
100
+ if current_page && params[:createonly]
101
+ raise ApiError.new("articleexists", "The article you tried to create has been created already")
102
+ end
103
+ end
104
+
105
+ def edit
106
+ @token.validate
107
+
108
+ title = params[:title]
109
+ current_page = @pages.get(title)
110
+ validate_page_overwrite(current_page)
111
+
112
+ new_page = @pages.add(title, params[:text])
113
+ page_info = {:result => "Success", :pageid => new_page[:pageid], :title => new_page[:title], :newrevid => new_page[:pageid]}
114
+ if current_page
115
+ page_info.merge!(:oldrevid => current_page[:pageid])
116
+ else
117
+ page_info.merge!(:new => "", :oldrevid => 0)
118
+ end
119
+
120
+ api_response do |_|
121
+ _.edit(nil, page_info)
122
+ end
123
+ end
124
+
125
+ def delete
126
+ @token.validate_admin
127
+
128
+ title = params[:title]
129
+ raise ApiError.new("missingtitle", "The page you requested doesn't exist") unless @pages.get(title)
130
+ @pages.delete(title)
131
+
132
+ api_response do |_|
133
+ _.delete(nil, {:title => title, :reason => "Default reason"})
134
+ end
135
+ end
136
+
137
+ def undelete
138
+ @token.validate_admin
139
+
140
+ title = params[:title]
141
+ revisions = @pages.undelete(title)
142
+ api_response do |_|
143
+ _.undelete(nil, {:title => title, :revisions => revisions})
144
+ end
145
+ end
146
+
147
+ def upload
148
+ @token.validate
149
+
150
+ filename = params[:filename]
151
+ @pages.add(filename, params[:file])
152
+ api_response do |_|
153
+ _.upload(nil, {:filename => filename, :result => "Success"})
154
+ end
155
+ end
156
+
157
+ def parse
158
+ page = @pages.get(params[:page])
159
+ api_response do |_|
160
+ _.parse({ :revid => page ? page[:pageid] : 0}) do
161
+ if params[:page] == "Foopage"
162
+ _.text!('Sample <B>HTML</B> content.' \
163
+ '<img width="150" height="150" class="thumbimage" src="http://upload.wikimedia.org/foo/Ruby_logo.svg" alt="Ruby logo.svg"/>' \
164
+ '<span class="editsection">[<a title="Edit section: Nomenclature" href="/w/index.php?title=Seat_of_local_government&amp;action=edit&amp;section=1">edit</a>]</span>' \
165
+ '<a title="Interpreted language" href="/wiki/Interpreted_language">interpreted language</a>'
166
+ )
167
+ else
168
+ _.text!('Sample <B>HTML</B> content.')
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ def action
175
+ [:userrights].each do |action_type|
176
+ return send(action_type)
177
+ end
178
+ halt(404, "Page not found")
179
+ end
180
+
181
+ def query
182
+ [:prop, :export, :list, :meta].each do |query_type|
183
+ return send(query_type) if params[query_type]
184
+ end
185
+ halt(404, "Page not found")
186
+ end
187
+
188
+ def prop
189
+ return get_revisions if params[:prop] == "revisions"
190
+ return get_undelete_token if params[:drprop] == 'token'
191
+ return get_token if params[:intoken]
192
+ return get_info if params[:prop] == "info"
193
+ end
194
+
195
+ def export
196
+ Nokogiri::XML::Builder.new do |_|
197
+ _.mediawiki do
198
+ requested_page_titles.each do |requested_title|
199
+ page = @pages.get(requested_title)
200
+ _.page do
201
+ _.title(page[:title])
202
+ _.id(page[:pageid])
203
+ _.revision do
204
+ _.id(page[:pageid])
205
+ _.text!(page[:content])
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end.to_xml
211
+ end
212
+
213
+ def list
214
+ list_type = params[:list].to_sym
215
+
216
+ # api.php?action=query&list=users&ususers=Bob&ustoken=userrights
217
+ if list_type == :users && params[:ustoken] && params[:ususers]
218
+ # This "list" is actually a request for a user rights token
219
+ return get_userrights_token(params[:ususers])
220
+ end
221
+
222
+ # This is a real list
223
+ return send(list_type) if respond_to?(list_type)
224
+ halt(404, "Page not found")
225
+ end
226
+
227
+ def allpages
228
+ api_response do |_|
229
+ _.query do
230
+ _.allpages do
231
+ prefix = params[:apprefix]
232
+ namespace = @pages.namespaces_by_id[params[:apnamespace].to_i]
233
+ prefix = "#{namespace}:#{prefix}" unless namespace.empty?
234
+ @pages.list(prefix).each do |key, page|
235
+ _.p(nil, { :title => page[:title], :ns => page[:namespace], :id => page[:pageid] })
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ def search
243
+ api_response do |_|
244
+ _.query do
245
+ _.search do
246
+ namespaces = params[:srnamespace] ? params[:srnamespace].split('|') : [ "0" ]
247
+ @pages.search(params[:srsearch], namespaces).first(params[:srlimit].to_i).each do |key, page|
248
+ _.p(nil, { :title => page[:title], :ns => page[:namespace], :id => page[:pageid] })
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ def meta
256
+ meta_type = params[:meta].to_sym
257
+ return send(meta_type) if respond_to?(meta_type)
258
+ halt(404, "Page not found")
259
+ end
260
+
261
+ def siteinfo
262
+ if siteinfo_type = params[:siprop]
263
+ return send(siteinfo_type) if respond_to?(siteinfo_type)
264
+ halt(404, "Page not found")
265
+ else
266
+ api_response do |_|
267
+ _.query do
268
+ _.general(generator: "MediaWiki #{MediaWiki::VERSION}")
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ def namespaces
275
+ api_response do |_|
276
+ _.query do
277
+ _.namespaces do
278
+ @pages.namespaces_by_prefix.each do |prefix, id|
279
+ attr = { :id => id }
280
+ attr[:canonical] = prefix unless prefix.empty?
281
+ _.ns(prefix, attr)
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ def extensions
289
+ api_response do |_|
290
+ _.query do
291
+ _.extensions do
292
+ @extensions.each do |name, version|
293
+ attr = { :version => version }
294
+ attr[:name] = name unless name.empty?
295
+ _.ext(name, attr)
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ def api_response(api_attr = {}, &block)
303
+ Nokogiri::XML::Builder.new do |_|
304
+ _.api(api_attr, &block)
305
+ end.to_xml
306
+ end
307
+
308
+ def api_error_response(code, info)
309
+ api_response do |_|
310
+ _.error(nil,
311
+ :code => code,
312
+ :info => info)
313
+ end
314
+ end
315
+
316
+ def query_pages
317
+ api_response do |_|
318
+ _.query do
319
+ _.pages do
320
+ requested_page_titles.each do |title|
321
+ yield(_, title, @pages.get(title))
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+
328
+ def get_info
329
+ query_pages do |_, title, page|
330
+ attributes = { :title => title, :ns => '0'}
331
+ if page.nil?
332
+ attributes[:missing] = ""
333
+ else
334
+ attributes[:redirect] = "" if page[:redirect]
335
+ end
336
+ _.page(nil, attributes)
337
+ end
338
+ end
339
+
340
+ def get_revisions
341
+ query_pages do |_, title, page|
342
+ if page.nil?
343
+ _.page(nil, { :title => title, :ns => '0', :missing => "" })
344
+ else
345
+ page = page.dup
346
+ content = page.delete(:content)
347
+ _.page(page.merge({ :ns => 0 })) do
348
+ _.revisions do
349
+ _.rev(content)
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ def user
357
+ username = request.cookies['login']
358
+ @users[username] if logged_in(username)
359
+ end
360
+
361
+ def requested_page_titles
362
+ params[:titles].split("|")
363
+ end
364
+
365
+ def tokens
366
+ @token.request(user)
367
+
368
+ api_response do |_|
369
+ _.tokens(:optionstoken => @token.optionstoken)
370
+ end
371
+ end
372
+
373
+ def get_token
374
+ token_str = @token.request(user)
375
+ query_pages do |_, title, page|
376
+ page = page ? page.dup : {}
377
+ page[params[:intoken] + "token"] = token_str if token_str
378
+ _.page(nil, page.merge({ :ns => 0 }))
379
+ end
380
+ end
381
+
382
+ def get_undelete_token
383
+ @token.set_type 'undelete'
384
+ token_str = @token.request(user)
385
+ api_response do |_|
386
+ _.query do
387
+ _.deletedrevs do
388
+ requested_page_titles.select {|title| ! @pages.get(title) }.each do |title|
389
+ _.page(nil, { :title => title, :token => token_str })
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ def get_userrights_token(username)
397
+ @token.set_type 'userrights'
398
+ token_str = @token.request(user)
399
+
400
+ user_to_manage = @users[username]
401
+
402
+ if user_to_manage
403
+ api_response do |_|
404
+ _.query do
405
+ _.users do
406
+ _.user(nil, { :name => user_to_manage[:username], :userrightstoken => token_str })
407
+ end
408
+ end
409
+ end
410
+ else
411
+ api_response do |_|
412
+ _.error(nil, { :code => 'nosuchuser', :info => "The user '#{params[:ususer].to_s}' does not exist"} )
413
+ end
414
+ end
415
+ end
416
+
417
+ def login
418
+ user = @users[params[:lgname]]
419
+ if user and user[:domain] == params[:lgdomain]
420
+ if params[:lgpassword] == user[:password]
421
+ @logged_in_users << user[:username]
422
+ response.set_cookie('login', user[:username])
423
+ result = { :result => "Success", :lguserid => "1", :lgusername => "Atlasmw"}
424
+ else
425
+ result = { :result => "WrongPass" }
426
+ end
427
+ else
428
+ result = { :result => "NotExists" }
429
+ end
430
+
431
+ api_response do |_|
432
+ _.login(nil, result)
433
+ end
434
+ end
435
+
436
+ def createaccount
437
+ api_response do |_|
438
+ @token.request(user)
439
+
440
+ if params[:token] && !params[:token].empty?
441
+ @token.validate_admin
442
+ add_user(params[:name], params[:password], 'local', false)
443
+ _.createaccount(:token => @token.createusertoken, :userid => @users.length, :username => params[:name], :result => 'success')
444
+ else
445
+ _.createaccount(:token => @token.createusertoken, :result => 'needtoken')
446
+ end
447
+ end
448
+ end
449
+
450
+ def options
451
+ api_response(:options => 'success')
452
+ end
453
+
454
+ def userrights
455
+ api_response do |_|
456
+ _.userrights({:user => params[:user]}) do
457
+ _.removed do
458
+ params[:remove].split('|').each do |removed_group|
459
+ _.group(removed_group)
460
+ end
461
+ end
462
+ _.added do
463
+ params[:add].split('|').each do |added_group|
464
+ _.group(added_group)
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end
470
+
471
+ end
472
+
473
+ class WikiPage
474
+
475
+ def initialize(options={})
476
+ options.each { |k, v| send("#{k}=", v) }
477
+ end
478
+
479
+ attr_accessor :content, :author
480
+
481
+ end
482
+
483
+ class ApiPages
484
+
485
+ def initialize
486
+ @page_id = 0
487
+ @pages = {}
488
+ @namespaces = { "" => 0 }
489
+ end
490
+
491
+ def add_namespace(id, prefix)
492
+ @namespaces[prefix] = id
493
+ end
494
+
495
+ def namespaces_by_prefix
496
+ @namespaces
497
+ end
498
+
499
+ def namespaces_by_id
500
+ @namespaces.invert
501
+ end
502
+
503
+ def add(title, content, redirect=false)
504
+ @page_id += 1
505
+ dummy, prefix = title.split(":", 2).reverse
506
+ @pages[title] = {
507
+ :pageid => @page_id,
508
+ :namespace => namespaces_by_prefix[prefix || ""],
509
+ :title => title,
510
+ :content => content,
511
+ :redirect => redirect
512
+ }
513
+ end
514
+
515
+ def get(title)
516
+ @pages[title]
517
+ end
518
+
519
+ def list(prefix)
520
+ @pages.select do |key, page|
521
+ key =~ /^#{prefix}/
522
+ end
523
+ end
524
+
525
+ def search(searchkey, namespaces)
526
+ raise ApiError.new("srparam-search", "empty search string is not allowed") if searchkey.empty?
527
+ @pages.select do |key, page|
528
+ page[:content] =~ /#{searchkey}/ and namespaces.include? page[:namespace].to_s
529
+ end
530
+ end
531
+
532
+ def delete(title)
533
+ @pages.delete(title)
534
+ end
535
+
536
+ def undelete(title)
537
+ if @pages[title]
538
+ 0
539
+ else
540
+ add(title, "Undeleted content")
541
+ 1
542
+ end
543
+ end
544
+ end
545
+
546
+ class ApiToken
547
+
548
+ ADMIN_TOKEN = "admin_token+\\"
549
+ REGULAR_TOKEN = "regular_token+\\"
550
+ BLANK_TOKEN = "+\\"
551
+
552
+ def initialize(params)
553
+ @token_str = params[:token]
554
+ @token_in = params[:intoken]
555
+ end
556
+
557
+ def set_type(type)
558
+ @token_in = type
559
+ end
560
+
561
+ def validate
562
+ unless @token_str
563
+ raise ApiError.new("notoken", "The token parameter must be set")
564
+ end
565
+ end
566
+
567
+ def validate_admin
568
+ validate
569
+ if @token_str != ADMIN_TOKEN
570
+ raise ApiError.new("badtoken", "Invalid token")
571
+ end
572
+ end
573
+
574
+ def request(user)
575
+ @user = user
576
+ respond_to?(requested_token_type) ? send(requested_token_type) : nil
577
+ end
578
+
579
+ def requested_token_type
580
+ "#{@token_in}token".to_sym
581
+ end
582
+
583
+ def importtoken
584
+ if @user && @user[:is_admin]
585
+ ADMIN_TOKEN
586
+ else
587
+ nil
588
+ end
589
+ end
590
+
591
+ alias_method :deletetoken, :importtoken
592
+ alias_method :undeletetoken, :importtoken
593
+ alias_method :userrightstoken, :importtoken
594
+ alias_method :createusertoken, :importtoken
595
+
596
+ def edittoken
597
+ if @user
598
+ REGULAR_TOKEN
599
+ else
600
+ BLANK_TOKEN
601
+ end
602
+ end
603
+
604
+ alias_method :optionstoken, :edittoken
605
+
606
+ end
607
+
608
+ class ApiError < StandardError
609
+
610
+ attr_reader :code, :message
611
+
612
+ def initialize(code, message)
613
+ @code = code
614
+ @message = message
615
+ end
616
+
617
+ end
618
+
619
+ module RSpecAdapter
620
+
621
+ ADDRESS = 'dummy-wiki.example'
622
+
623
+ def self.enhance(config, *args)
624
+ ShamRack.mount($fake_media_wiki = App.new!, ADDRESS)
625
+
626
+ config.before(*args) {
627
+ @gateway = Gateway.new("http://#{ADDRESS}/w/api.php")
628
+ $fake_media_wiki.reset
629
+ }
630
+ end
631
+
632
+ end
633
+
634
+ end
635
+
636
+ end