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,98 @@
1
+ module MediaWiki
2
+
3
+ class Gateway
4
+
5
+ module Query
6
+
7
+ # Get a list of pages with matching content in given namespaces
8
+ #
9
+ # [key] Search key
10
+ # [namespaces] Array of namespace names to search (defaults to main only)
11
+ # [limit] Maximum number of hits to ask for (defaults to 500; note that Wikimedia Foundation wikis allow only 50 for normal users)
12
+ # [max_results] Maximum total number of results to return
13
+ # [options] Hash of additional options
14
+ #
15
+ # Returns array of page titles (empty if no matches)
16
+ def search(key, namespaces = nil, limit = @options[:limit], max_results = @options[:max_results], options = {})
17
+ titles, offset, form_data = [], 0, options.merge(
18
+ 'action' => 'query',
19
+ 'list' => 'search',
20
+ 'srwhat' => 'text',
21
+ 'srsearch' => key,
22
+ 'srlimit' => limit
23
+ )
24
+
25
+ if namespaces
26
+ form_data['srnamespace'] = Array(namespaces).map! { |ns|
27
+ namespaces_by_prefix[ns]
28
+ }.join('|')
29
+ end
30
+
31
+ begin
32
+ form_data['sroffset'] = offset if offset
33
+ form_data['srlimit'] = [limit, max_results - offset.to_i].min
34
+
35
+ res, offset = make_api_request(form_data, '//query-continue/search/@sroffset')
36
+
37
+ titles += REXML::XPath.match(res, '//p').map { |x| x.attributes['title'] }
38
+ end while offset && offset.to_i < max_results.to_i
39
+
40
+ titles
41
+ end
42
+
43
+ # Execute Semantic Mediawiki query
44
+ #
45
+ # [query] Semantic Mediawiki query
46
+ # [params] Array of additional parameters or options, eg. mainlabel=Foo or ?Place (optional)
47
+ # [options] Hash of additional options
48
+ #
49
+ # Returns result as an HTML string
50
+ def semantic_query(query, params = [], options = {})
51
+ unless smw_version = extensions['Semantic MediaWiki']
52
+ raise MediaWiki::Exception, 'Semantic MediaWiki extension not installed.'
53
+ end
54
+
55
+ if smw_version.to_f >= 1.7
56
+ send_request(options.merge(
57
+ 'action' => 'ask',
58
+ 'query' => "#{query}|#{params.join('|')}"
59
+ ))
60
+ else
61
+ send_request(options.merge(
62
+ 'action' => 'parse',
63
+ 'prop' => 'text',
64
+ 'text' => "{{#ask:#{query}|#{params.push('format=list').join('|')}}}"
65
+ )).elements['parse/text'].text
66
+ end
67
+ end
68
+
69
+ # Make a custom query
70
+ #
71
+ # [options] query options
72
+ #
73
+ # Returns the REXML::Element object as result
74
+ #
75
+ # Example:
76
+ # def creation_time(pagename)
77
+ # res = bot.custom_query(:prop => :revisions,
78
+ # :titles => pagename,
79
+ # :rvprop => :timestamp,
80
+ # :rvdir => :newer,
81
+ # :rvlimit => 1)
82
+ # timestr = res.get_elements('*/*/*/rev')[0].attribute('timestamp').to_s
83
+ # time.parse(timestr)
84
+ # end
85
+ #
86
+ def custom_query(options)
87
+ form_data = {}
88
+ options.each { |k, v| form_data[k.to_s] = v.to_s }
89
+ send_request(form_data.merge('action' => 'query')).elements['query']
90
+ end
91
+
92
+ end
93
+
94
+ include Query
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,101 @@
1
+ module MediaWiki
2
+
3
+ class Gateway
4
+
5
+ module Site
6
+
7
+ # Imports a MediaWiki XML dump
8
+ #
9
+ # [xml] String or array of page names to fetch
10
+ # [options] Hash of additional options
11
+ #
12
+ # Returns XML array <api><import><page/><page/>...
13
+ # <page revisions="1"> (or more) means successfully imported
14
+ # <page revisions="0"> means duplicate, not imported
15
+ def import(xmlfile, options = {})
16
+ send_request(options.merge(
17
+ 'action' => 'import',
18
+ 'xml' => File.new(xmlfile),
19
+ 'token' => get_token('import', 'Main Page'), # NB: dummy page name
20
+ 'format' => 'xml'
21
+ ))
22
+ end
23
+
24
+ # Exports a page or set of pages
25
+ #
26
+ # [page_titles] String or array of page titles to fetch
27
+ # [options] Hash of additional options
28
+ #
29
+ # Returns MediaWiki XML dump
30
+ def export(page_titles, options = {})
31
+ send_request(options.merge(
32
+ 'action' => 'query',
33
+ 'titles' => Array(page_titles).join('|'),
34
+ 'export' => nil,
35
+ 'exportnowrap' => nil
36
+ ))
37
+ end
38
+
39
+ # Get the wiki's siteinfo as a hash. See http://www.mediawiki.org/wiki/API:Siteinfo.
40
+ #
41
+ # [options] Hash of additional options
42
+ def siteinfo(options = {})
43
+ res = send_request(options.merge(
44
+ 'action' => 'query',
45
+ 'meta' => 'siteinfo'
46
+ ))
47
+
48
+ REXML::XPath.first(res, '//query/general')
49
+ .attributes.each_with_object({}) { |(k, v), h| h[k] = v }
50
+ end
51
+
52
+ # Get the wiki's MediaWiki version.
53
+ #
54
+ # [options] Hash of additional options passed to #siteinfo
55
+ def version(options = {})
56
+ siteinfo(options).fetch('generator', '').split.last
57
+ end
58
+
59
+ # Get a list of all known namespaces
60
+ #
61
+ # [options] Hash of additional options
62
+ #
63
+ # Returns array of namespaces (name => id)
64
+ def namespaces_by_prefix(options = {})
65
+ res = send_request(options.merge(
66
+ 'action' => 'query',
67
+ 'meta' => 'siteinfo',
68
+ 'siprop' => 'namespaces'
69
+ ))
70
+
71
+ REXML::XPath.match(res, '//ns').each_with_object({}) { |namespace, namespaces|
72
+ prefix = namespace.attributes['canonical'] || ''
73
+ namespaces[prefix] = namespace.attributes['id'].to_i
74
+ }
75
+ end
76
+
77
+ # Get a list of all installed (and registered) extensions
78
+ #
79
+ # [options] Hash of additional options
80
+ #
81
+ # Returns array of extensions (name => version)
82
+ def extensions(options = {})
83
+ res = send_request(options.merge(
84
+ 'action' => 'query',
85
+ 'meta' => 'siteinfo',
86
+ 'siprop' => 'extensions'
87
+ ))
88
+
89
+ REXML::XPath.match(res, '//ext').each_with_object({}) { |extension, extensions|
90
+ name = extension.attributes['name'] || ''
91
+ extensions[name] = extension.attributes['version']
92
+ }
93
+ end
94
+
95
+ end
96
+
97
+ include Site
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,182 @@
1
+ module MediaWiki
2
+
3
+ class Gateway
4
+
5
+ module Users
6
+
7
+ # Login to MediaWiki
8
+ #
9
+ # [username] Username
10
+ # [password] Password
11
+ # [domain] Domain for authentication plugin logins (eg. LDAP), optional -- defaults to 'local' if not given
12
+ # [options] Hash of additional options
13
+ #
14
+ # Throws MediaWiki::Unauthorized if login fails
15
+ def login(username, password, domain = 'local', options = {})
16
+ send_request(options.merge(
17
+ 'action' => 'login',
18
+ 'lgname' => username,
19
+ 'lgpassword' => password,
20
+ 'lgdomain' => domain
21
+ ))
22
+
23
+ @password = password
24
+ @username = username
25
+ end
26
+
27
+ # Get a list of users
28
+ #
29
+ # [options] Optional hash of options, eg. { 'augroup' => 'sysop' }. See http://www.mediawiki.org/wiki/API:Allusers
30
+ #
31
+ # Returns array of user names (empty if no matches)
32
+ def users(options = {})
33
+ iterate_query('allusers', '//u', 'name', 'aufrom', options.merge(
34
+ 'aulimit' => @options[:limit]
35
+ ))
36
+ end
37
+
38
+ # Get user contributions
39
+ #
40
+ # user: The user name
41
+ # count: Maximum number of contributions to retreive, or nil for all
42
+ # [options] Optional hash of options, eg. { 'ucnamespace' => 4 }. See http://www.mediawiki.org/wiki/API:Usercontribs
43
+ #
44
+ # Returns array of hashes containing the "item" attributes defined here: http://www.mediawiki.org/wiki/API:Usercontribs
45
+ def contributions(user, count = nil, options = {})
46
+ result = []
47
+
48
+ iterate_query('usercontribs', '//item', nil, 'uccontinue', options.merge(
49
+ 'ucuser' => user,
50
+ 'uclimit' => @options[:limit]
51
+ )) { |element|
52
+ result << hash = {}
53
+ element.attributes.each { |key, value| hash[key] = value }
54
+ }
55
+
56
+ count ? result.take(count) : result
57
+ end
58
+
59
+ # Sends e-mail to a user
60
+ #
61
+ # [user] Username to send mail to (name only: eg. 'Bob', not 'User:Bob')
62
+ # [subject] Subject of message
63
+ # [content] Content of message
64
+ # [options] Hash of additional options
65
+ #
66
+ # Will raise a 'noemail' APIError if the target user does not have a confirmed email address, see http://www.mediawiki.org/wiki/API:E-mail for details.
67
+ def email_user(user, subject, text, options = {})
68
+ res = send_request(options.merge(
69
+ 'action' => 'emailuser',
70
+ 'target' => user,
71
+ 'subject' => subject,
72
+ 'text' => text,
73
+ 'token' => get_token('email', "User:#{user}")
74
+ ))
75
+
76
+ res.elements['emailuser'].attributes['result'] == 'Success'
77
+ end
78
+
79
+ # Create a new account
80
+ #
81
+ # [options] is +Hash+ passed as query arguments. See https://www.mediawiki.org/wiki/API:Account_creation#Parameters for more information.
82
+ def create_account(options)
83
+ send_request(options.merge('action' => 'createaccount'))
84
+ end
85
+
86
+ # Sets options for currenlty logged in user
87
+ #
88
+ # [changes] a +Hash+ that will be transformed into an equal sign and pipe-separated key value parameter
89
+ # [optionname] a +String+ indicating which option to change (optional)
90
+ # [optionvalue] the new value for optionname - allows pipe characters (optional)
91
+ # [reset] a +Boolean+ indicating if all preferences should be reset to site defaults (optional)
92
+ # [options] Hash of additional options
93
+ def options(changes = {}, optionname = nil, optionvalue = nil, reset = false, options = {})
94
+ form_data = options.merge(
95
+ 'action' => 'options',
96
+ 'token' => get_options_token
97
+ )
98
+
99
+ if changes && !changes.empty?
100
+ form_data['change'] = changes.map { |key, value| "#{key}=#{value}" }.join('|')
101
+ end
102
+
103
+ if optionname && !optionname.empty?
104
+ form_data[optionname] = optionvalue
105
+ end
106
+
107
+ if reset
108
+ form_data['reset'] = true
109
+ end
110
+
111
+ send_request(form_data)
112
+ end
113
+
114
+ # Set groups for a user
115
+ #
116
+ # [user] Username of user to modify
117
+ # [groups_to_add] Groups to add user to, as an array or a string if a single group (optional)
118
+ # [groups_to_remove] Groups to remove user from, as an array or a string if a single group (optional)
119
+ # [options] Hash of additional options
120
+ def set_groups(user, groups_to_add = [], groups_to_remove = [], comment = '', options = {})
121
+ token = get_userrights_token(user)
122
+ userrights(user, token, groups_to_add, groups_to_remove, comment, options)
123
+ end
124
+
125
+ private
126
+
127
+ # User rights management (aka group assignment)
128
+ def get_userrights_token(user)
129
+ res = send_request(
130
+ 'action' => 'query',
131
+ 'list' => 'users',
132
+ 'ustoken' => 'userrights',
133
+ 'ususers' => user
134
+ )
135
+
136
+ token = res.elements['query/users/user'].attributes['userrightstoken']
137
+
138
+ @log.debug("RESPONSE: #{res.to_s}")
139
+
140
+ unless token
141
+ if res.elements['query/users/user'].attributes['missing']
142
+ raise APIError.new('invaliduser', "User '#{user}' was not found (get_userrights_token)")
143
+ else
144
+ raise Unauthorized.new("User '#{@username}' is not permitted to perform this operation: get_userrights_token")
145
+ end
146
+ end
147
+
148
+ token
149
+ end
150
+
151
+ def get_options_token
152
+ send_request('action' => 'tokens', 'type' => 'options')
153
+ .elements['tokens'].attributes['optionstoken']
154
+ end
155
+
156
+ def userrights(user, token, groups_to_add, groups_to_remove, reason, options = {})
157
+ # groups_to_add and groups_to_remove can be a string or an array. Turn them into MediaWiki's pipe-delimited list format.
158
+ if groups_to_add.is_a?(Array)
159
+ groups_to_add = groups_to_add.join('|')
160
+ end
161
+
162
+ if groups_to_remove.is_a?(Array)
163
+ groups_to_remove = groups_to_remove.join('|')
164
+ end
165
+
166
+ send_request(options.merge(
167
+ 'action' => 'userrights',
168
+ 'user' => user,
169
+ 'token' => token,
170
+ 'add' => groups_to_add,
171
+ 'remove' => groups_to_remove,
172
+ 'reason' => reason
173
+ ))
174
+ end
175
+
176
+ end
177
+
178
+ include Users
179
+
180
+ end
181
+
182
+ end
@@ -1,5 +1,6 @@
1
1
  module MediaWiki
2
- class << self
2
+
3
+ module Utils
3
4
 
4
5
  # Extract base name. If there are no subpages, return page name.
5
6
  #
@@ -7,7 +8,7 @@ module MediaWiki
7
8
  # get_base_name("Namespace:Foo/Bar/Baz") -> "Namespace:Foo"
8
9
  # get_base_name("Namespace:Foo") -> "Namespace:Foo"
9
10
  #
10
- # [title] Page name string in Wiki format
11
+ # [title] Page name string in Wiki format
11
12
  def get_base_name(title)
12
13
  title.split('/').first if title
13
14
  end
@@ -18,10 +19,9 @@ module MediaWiki
18
19
  # get_path_to_subpage("Namespace:Foo/Bar/Baz") -> "Namespace:Foo/Bar"
19
20
  # get_path_to_subpage("Namespace:Foo") -> nil
20
21
  #
21
- # [title] Page name string in Wiki format
22
+ # [title] Page name string in Wiki format
22
23
  def get_path_to_subpage(title)
23
- return nil unless title and title.include? '/'
24
- title.split(/\/([^\/]*)$/).first
24
+ title.split(/\/([^\/]*)$/).first if title && title.include?('/')
25
25
  end
26
26
 
27
27
  # Extract subpage name. If there is no hierarchy above, return page name.
@@ -30,7 +30,7 @@ module MediaWiki
30
30
  # get_subpage("Namespace:Foo/Bar/Baz") -> "Baz"
31
31
  # get_subpage("Namespace:Foo") -> "Namespace:Foo"
32
32
  #
33
- # [title] Page name string in Wiki format
33
+ # [title] Page name string in Wiki format
34
34
  def get_subpage(title)
35
35
  title.split('/').last if title
36
36
  end
@@ -42,24 +42,58 @@ module MediaWiki
42
42
  def uri_to_wiki(uri)
43
43
  upcase_first_char(CGI.unescape(uri).tr('_', ' ').tr('#<>[]|{}', '')) if uri
44
44
  end
45
-
45
+
46
46
  # Convert a Wiki page name ("Getting there & away") to URI-safe format ("Getting_there_%26_away"),
47
47
  # taking care not to mangle slashes or colons
48
48
  # [wiki] Page name string in Wiki format
49
49
  def wiki_to_uri(wiki)
50
- wiki.to_s.split('/').map {|chunk| CGI.escape(CGI.unescape(chunk).tr(' ', '_')) }.join('/').gsub('%3A', ':') if wiki
50
+ wiki.to_s.split('/').map { |chunk|
51
+ CGI.escape(CGI.unescape(chunk).tr(' ', '_'))
52
+ }.join('/').gsub('%3A', ':') if wiki
51
53
  end
52
54
 
53
55
  # Return current version of MediaWiki::Gateway
54
56
  def version
55
57
  MediaWiki::VERSION
56
58
  end
57
-
59
+
58
60
  private
59
-
60
- def upcase_first_char(str)
61
- [ ActiveSupport::Multibyte::Chars.new(str.mb_chars.slice(0,1)).upcase.to_s, str.mb_chars.slice(1..-1) ].join
61
+
62
+ NO_UNICODE_SUPPORT = begin
63
+ require 'unicode'
64
+ rescue LoadError
65
+ begin
66
+ require 'active_support/core_ext/string/multibyte'
67
+ rescue LoadError
68
+ warn 'mediawiki-gateway: For better Unicode support,' <<
69
+ " install the `unicode' or `activesupport' gem."
70
+
71
+ def upcase_first_char(str)
72
+ first, rest = str.slice(0, 1), str.slice(1..-1)
73
+ [first.upcase, rest].join
74
+ end
75
+
76
+ 'No Unicode support'
77
+ else
78
+ def upcase_first_char(str)
79
+ mb_str = str.mb_chars
80
+ first, rest = mb_str.slice(0, 1), mb_str.slice(1..-1)
81
+ [ActiveSupport::Multibyte::Chars.new(first).upcase.to_s, rest].join
82
+ end
83
+
84
+ nil
85
+ end
86
+ else
87
+ def upcase_first_char(str)
88
+ first, rest = str.slice(0, 1), str.slice(1..-1)
89
+ [Unicode.upcase(first), rest].join
90
+ end
91
+
92
+ nil
62
93
  end
94
+
63
95
  end
64
-
96
+
97
+ extend Utils
98
+
65
99
  end