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