mediawiki-gateway 0.1.0
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.
- data/.gitignore +3 -0
- data/README +12 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/config/hosts.yml +17 -0
- data/doc/classes/MediaWiki.html +189 -0
- data/doc/classes/MediaWiki/Config.html +269 -0
- data/doc/classes/MediaWiki/Gateway.html +952 -0
- data/doc/created.rid +1 -0
- data/doc/files/README_txt.html +117 -0
- data/doc/files/media_wiki/config_rb.html +108 -0
- data/doc/files/media_wiki/gateway_rb.html +113 -0
- data/doc/files/media_wiki/utils_rb.html +101 -0
- data/doc/files/script/create_page_rb.html +115 -0
- data/doc/files/script/delete_book_rb.html +108 -0
- data/doc/files/script/export_xml_rb.html +114 -0
- data/doc/files/script/get_page_rb.html +114 -0
- data/doc/files/script/import_xml_rb.html +114 -0
- data/doc/files/script/undelete_page_rb.html +101 -0
- data/doc/files/script/upload_commons_rb.html +109 -0
- data/doc/files/script/upload_file_rb.html +115 -0
- data/doc/fr_class_index.html +29 -0
- data/doc/fr_file_index.html +38 -0
- data/doc/fr_method_index.html +49 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/media_wiki.rb +3 -0
- data/lib/media_wiki/config.rb +69 -0
- data/lib/media_wiki/gateway.rb +307 -0
- data/lib/media_wiki/utils.rb +18 -0
- data/mediawiki-gateway.gemspec +88 -0
- data/script/create_page.rb +14 -0
- data/script/delete_book.rb +14 -0
- data/script/export_xml.rb +14 -0
- data/script/get_page.rb +12 -0
- data/script/import_xml.rb +14 -0
- data/script/run_fake_media_wiki.rb +8 -0
- data/script/undelete_page.rb +15 -0
- data/script/upload_commons.rb +42 -0
- data/script/upload_file.rb +14 -0
- data/spec/fake_media_wiki/api_pages.rb +131 -0
- data/spec/fake_media_wiki/app.rb +262 -0
- data/spec/fake_media_wiki/query_handling.rb +112 -0
- data/spec/gateway_spec.old +535 -0
- data/spec/gateway_spec.rb +653 -0
- data/spec/import-test-data.xml +68 -0
- metadata +115 -0
data/doc/index.html
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
<?xml version="1.0" encoding="iso-8859-1"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
|
5
|
+
|
6
|
+
<!--
|
7
|
+
|
8
|
+
RDoc Documentation
|
9
|
+
|
10
|
+
-->
|
11
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
12
|
+
<head>
|
13
|
+
<title>RDoc Documentation</title>
|
14
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
15
|
+
</head>
|
16
|
+
<frameset rows="20%, 80%">
|
17
|
+
<frameset cols="25%,35%,45%">
|
18
|
+
<frame src="fr_file_index.html" title="Files" name="Files" />
|
19
|
+
<frame src="fr_class_index.html" name="Classes" />
|
20
|
+
<frame src="fr_method_index.html" name="Methods" />
|
21
|
+
</frameset>
|
22
|
+
<frame src="files/README_txt.html" name="docwin" />
|
23
|
+
</frameset>
|
24
|
+
</html>
|
data/doc/rdoc-style.css
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
|
2
|
+
body {
|
3
|
+
font-family: Verdana,Arial,Helvetica,sans-serif;
|
4
|
+
font-size: 90%;
|
5
|
+
margin: 0;
|
6
|
+
margin-left: 40px;
|
7
|
+
padding: 0;
|
8
|
+
background: white;
|
9
|
+
}
|
10
|
+
|
11
|
+
h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
|
12
|
+
h1 { font-size: 150%; }
|
13
|
+
h2,h3,h4 { margin-top: 1em; }
|
14
|
+
|
15
|
+
a { background: #eef; color: #039; text-decoration: none; }
|
16
|
+
a:hover { background: #039; color: #eef; }
|
17
|
+
|
18
|
+
/* Override the base stylesheet's Anchor inside a table cell */
|
19
|
+
td > a {
|
20
|
+
background: transparent;
|
21
|
+
color: #039;
|
22
|
+
text-decoration: none;
|
23
|
+
}
|
24
|
+
|
25
|
+
/* and inside a section title */
|
26
|
+
.section-title > a {
|
27
|
+
background: transparent;
|
28
|
+
color: #eee;
|
29
|
+
text-decoration: none;
|
30
|
+
}
|
31
|
+
|
32
|
+
/* === Structural elements =================================== */
|
33
|
+
|
34
|
+
div#index {
|
35
|
+
margin: 0;
|
36
|
+
margin-left: -40px;
|
37
|
+
padding: 0;
|
38
|
+
font-size: 90%;
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
div#index a {
|
43
|
+
margin-left: 0.7em;
|
44
|
+
}
|
45
|
+
|
46
|
+
div#index .section-bar {
|
47
|
+
margin-left: 0px;
|
48
|
+
padding-left: 0.7em;
|
49
|
+
background: #ccc;
|
50
|
+
font-size: small;
|
51
|
+
}
|
52
|
+
|
53
|
+
|
54
|
+
div#classHeader, div#fileHeader {
|
55
|
+
width: auto;
|
56
|
+
color: white;
|
57
|
+
padding: 0.5em 1.5em 0.5em 1.5em;
|
58
|
+
margin: 0;
|
59
|
+
margin-left: -40px;
|
60
|
+
border-bottom: 3px solid #006;
|
61
|
+
}
|
62
|
+
|
63
|
+
div#classHeader a, div#fileHeader a {
|
64
|
+
background: inherit;
|
65
|
+
color: white;
|
66
|
+
}
|
67
|
+
|
68
|
+
div#classHeader td, div#fileHeader td {
|
69
|
+
background: inherit;
|
70
|
+
color: white;
|
71
|
+
}
|
72
|
+
|
73
|
+
|
74
|
+
div#fileHeader {
|
75
|
+
background: #057;
|
76
|
+
}
|
77
|
+
|
78
|
+
div#classHeader {
|
79
|
+
background: #048;
|
80
|
+
}
|
81
|
+
|
82
|
+
|
83
|
+
.class-name-in-header {
|
84
|
+
font-size: 180%;
|
85
|
+
font-weight: bold;
|
86
|
+
}
|
87
|
+
|
88
|
+
|
89
|
+
div#bodyContent {
|
90
|
+
padding: 0 1.5em 0 1.5em;
|
91
|
+
}
|
92
|
+
|
93
|
+
div#description {
|
94
|
+
padding: 0.5em 1.5em;
|
95
|
+
background: #efefef;
|
96
|
+
border: 1px dotted #999;
|
97
|
+
}
|
98
|
+
|
99
|
+
div#description h1,h2,h3,h4,h5,h6 {
|
100
|
+
color: #125;;
|
101
|
+
background: transparent;
|
102
|
+
}
|
103
|
+
|
104
|
+
div#validator-badges {
|
105
|
+
text-align: center;
|
106
|
+
}
|
107
|
+
div#validator-badges img { border: 0; }
|
108
|
+
|
109
|
+
div#copyright {
|
110
|
+
color: #333;
|
111
|
+
background: #efefef;
|
112
|
+
font: 0.75em sans-serif;
|
113
|
+
margin-top: 5em;
|
114
|
+
margin-bottom: 0;
|
115
|
+
padding: 0.5em 2em;
|
116
|
+
}
|
117
|
+
|
118
|
+
|
119
|
+
/* === Classes =================================== */
|
120
|
+
|
121
|
+
table.header-table {
|
122
|
+
color: white;
|
123
|
+
font-size: small;
|
124
|
+
}
|
125
|
+
|
126
|
+
.type-note {
|
127
|
+
font-size: small;
|
128
|
+
color: #DEDEDE;
|
129
|
+
}
|
130
|
+
|
131
|
+
.xxsection-bar {
|
132
|
+
background: #eee;
|
133
|
+
color: #333;
|
134
|
+
padding: 3px;
|
135
|
+
}
|
136
|
+
|
137
|
+
.section-bar {
|
138
|
+
color: #333;
|
139
|
+
border-bottom: 1px solid #999;
|
140
|
+
margin-left: -20px;
|
141
|
+
}
|
142
|
+
|
143
|
+
|
144
|
+
.section-title {
|
145
|
+
background: #79a;
|
146
|
+
color: #eee;
|
147
|
+
padding: 3px;
|
148
|
+
margin-top: 2em;
|
149
|
+
margin-left: -30px;
|
150
|
+
border: 1px solid #999;
|
151
|
+
}
|
152
|
+
|
153
|
+
.top-aligned-row { vertical-align: top }
|
154
|
+
.bottom-aligned-row { vertical-align: bottom }
|
155
|
+
|
156
|
+
/* --- Context section classes ----------------------- */
|
157
|
+
|
158
|
+
.context-row { }
|
159
|
+
.context-item-name { font-family: monospace; font-weight: bold; color: black; }
|
160
|
+
.context-item-value { font-size: small; color: #448; }
|
161
|
+
.context-item-desc { color: #333; padding-left: 2em; }
|
162
|
+
|
163
|
+
/* --- Method classes -------------------------- */
|
164
|
+
.method-detail {
|
165
|
+
background: #efefef;
|
166
|
+
padding: 0;
|
167
|
+
margin-top: 0.5em;
|
168
|
+
margin-bottom: 1em;
|
169
|
+
border: 1px dotted #ccc;
|
170
|
+
}
|
171
|
+
.method-heading {
|
172
|
+
color: black;
|
173
|
+
background: #ccc;
|
174
|
+
border-bottom: 1px solid #666;
|
175
|
+
padding: 0.2em 0.5em 0 0.5em;
|
176
|
+
}
|
177
|
+
.method-signature { color: black; background: inherit; }
|
178
|
+
.method-name { font-weight: bold; }
|
179
|
+
.method-args { font-style: italic; }
|
180
|
+
.method-description { padding: 0 0.5em 0 0.5em; }
|
181
|
+
|
182
|
+
/* --- Source code sections -------------------- */
|
183
|
+
|
184
|
+
a.source-toggle { font-size: 90%; }
|
185
|
+
div.method-source-code {
|
186
|
+
background: #262626;
|
187
|
+
color: #ffdead;
|
188
|
+
margin: 1em;
|
189
|
+
padding: 0.5em;
|
190
|
+
border: 1px dashed #999;
|
191
|
+
overflow: hidden;
|
192
|
+
}
|
193
|
+
|
194
|
+
div.method-source-code pre { color: #ffdead; overflow: hidden; }
|
195
|
+
|
196
|
+
/* --- Ruby keyword styles --------------------- */
|
197
|
+
|
198
|
+
.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
|
199
|
+
|
200
|
+
.ruby-constant { color: #7fffd4; background: transparent; }
|
201
|
+
.ruby-keyword { color: #00ffff; background: transparent; }
|
202
|
+
.ruby-ivar { color: #eedd82; background: transparent; }
|
203
|
+
.ruby-operator { color: #00ffee; background: transparent; }
|
204
|
+
.ruby-identifier { color: #ffdead; background: transparent; }
|
205
|
+
.ruby-node { color: #ffa07a; background: transparent; }
|
206
|
+
.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
|
207
|
+
.ruby-regexp { color: #ffa07a; background: transparent; }
|
208
|
+
.ruby-value { color: #7fffd4; background: transparent; }
|
data/lib/media_wiki.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module MediaWiki
|
5
|
+
|
6
|
+
class Config
|
7
|
+
|
8
|
+
attr_reader :article, :desc, :file, :pw, :summary, :target, :url, :user
|
9
|
+
|
10
|
+
def initialize(args, type = "read")
|
11
|
+
@summary = "Automated edit via MediaWiki::Gateway"
|
12
|
+
@opts = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: [options]"
|
14
|
+
|
15
|
+
opts.on("-h", "--host HOST", "Use preconfigured HOST in config/hosts.yml") do |host_id|
|
16
|
+
yaml = YAML.load_file('config/hosts.yml')
|
17
|
+
if yaml.include? host_id
|
18
|
+
host = yaml[host_id]
|
19
|
+
@url = host['url']
|
20
|
+
@pw = host['pw']
|
21
|
+
@user = host['user']
|
22
|
+
else
|
23
|
+
raise "Host #{host_id} not found in config/hosts.yml"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if type == "upload"
|
28
|
+
opts.on("-d", "--description DESCRIPTION", "Description of file to upload") do |desc|
|
29
|
+
@desc = desc
|
30
|
+
end
|
31
|
+
opts.on("-t", "--target-file TARGET-FILE", "Target file name to upload to") do |target|
|
32
|
+
@target = target
|
33
|
+
end
|
34
|
+
else
|
35
|
+
opts.on("-a", "--article ARTICLE", "Name of article in Wiki") do |article|
|
36
|
+
@article = article
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on("-n", "--username USERNAME", "Username for login") do |user|
|
41
|
+
@user = user
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on("-p", "--password PASSWORD", "Password for login") do |pw|
|
45
|
+
@pw = pw
|
46
|
+
end
|
47
|
+
|
48
|
+
if type != "read"
|
49
|
+
opts.on("-s", "--summary SUMMARY", "Edit summary for this change") do |summary|
|
50
|
+
@summary = summary
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-u", "--url URL", "MediaWiki API URL") do |url|
|
55
|
+
@url = url
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@opts.parse!
|
59
|
+
abort("URL (-u) or valid host (-h) is mandatory.") unless @url
|
60
|
+
end
|
61
|
+
|
62
|
+
def abort(error)
|
63
|
+
puts "Error: #{error}\n\n#{@opts.to_s}"
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'logger'
|
3
|
+
require 'rest_client'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module MediaWiki
|
8
|
+
|
9
|
+
class Gateway
|
10
|
+
|
11
|
+
# Set up a MediaWiki::Gateway for a given MediaWiki installation
|
12
|
+
#
|
13
|
+
# [url] Path to API of target MediaWiki (eg. "http://en.wikipedia.org/w/api.php")
|
14
|
+
# [loglevel] Log level to use (optional, defaults to Logger::WARN)
|
15
|
+
def initialize(url, loglevel = Logger::WARN)
|
16
|
+
@log = Logger.new(STDERR)
|
17
|
+
@log.level = loglevel
|
18
|
+
@wiki_url = url
|
19
|
+
@headers = { "User-Agent" => "MediaWiki::Gateway/#{MediaWiki.version}" }
|
20
|
+
@cookies = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :base_url
|
24
|
+
|
25
|
+
# Login to MediaWiki
|
26
|
+
#
|
27
|
+
# [username] Username
|
28
|
+
# [password] Password
|
29
|
+
# [domain] Domain for authentication plugin logins (eg. LDAP), optional -- defaults to 'local' if not given
|
30
|
+
#
|
31
|
+
# Throws error if login fails
|
32
|
+
def login(username, password, domain = 'local')
|
33
|
+
form_data = {'action' => 'login', 'lgname' => username, 'lgpassword' => password, 'lgdomain' => domain}
|
34
|
+
make_api_request(form_data)
|
35
|
+
@password = password
|
36
|
+
@username = username
|
37
|
+
end
|
38
|
+
|
39
|
+
# Fetch MediaWiki page in MediaWiki format
|
40
|
+
#
|
41
|
+
# [page_title] Page title to fetch
|
42
|
+
#
|
43
|
+
# Returns nil if the page does not exist
|
44
|
+
def get(page_title)
|
45
|
+
form_data = {'action' => 'query', 'prop' => 'revisions', 'rvprop' => 'content', 'titles' => page_title}
|
46
|
+
page = make_api_request(form_data).elements["query/pages/page"]
|
47
|
+
if ! page or page.attributes["missing"]
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
page.elements["revisions/rev"].text
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Render a MediaWiki page as HTML
|
55
|
+
#
|
56
|
+
# [page_title] Page title to fetch
|
57
|
+
#
|
58
|
+
# Returns nil if the page does not exist
|
59
|
+
def render(page_title)
|
60
|
+
form_data = {'action' => 'parse', 'page' => page_title}
|
61
|
+
parsed = make_api_request(form_data).elements["parse"]
|
62
|
+
if parsed.attributes["revid"] != '0'
|
63
|
+
return parsed.elements["text"].text.gsub(/<!--(.|\s)*?-->/, '')
|
64
|
+
else
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create a new page, or overwrite an existing one
|
70
|
+
#
|
71
|
+
# [title] Page title to create or overwrite, string
|
72
|
+
# [content] Content for the page, string
|
73
|
+
# [options] Hash of additional options
|
74
|
+
#
|
75
|
+
# Options:
|
76
|
+
# * [overwrite] Allow overwriting existing pages
|
77
|
+
# * [summary] Edit summary for history, string
|
78
|
+
# * [token] Use this existing edit token instead requesting a new one (useful for bulk loads)
|
79
|
+
def create(title, content, options={})
|
80
|
+
form_data = {'action' => 'edit', 'title' => title, 'text' => content, 'summary' => (options[:summary] || ""), 'token' => get_token('edit', title)}
|
81
|
+
form_data['createonly'] = "" unless options[:overwrite]
|
82
|
+
make_api_request(form_data)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Delete one page. (MediaWiki API does not support deleting multiple pages at a time.)
|
86
|
+
#
|
87
|
+
# [title] Title of page to delete
|
88
|
+
def delete(title)
|
89
|
+
form_data = {'action' => 'delete', 'title' => title, 'token' => get_token('delete', title)}
|
90
|
+
make_api_request(form_data)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Undelete all revisions of one page.
|
94
|
+
#
|
95
|
+
# [title] Title of page to undelete
|
96
|
+
#
|
97
|
+
# Returns number of revisions undeleted.
|
98
|
+
def undelete(title)
|
99
|
+
token = get_undelete_token(title)
|
100
|
+
if token
|
101
|
+
form_data = {'action' => 'undelete', 'title' => title, 'token' => token }
|
102
|
+
xml = make_api_request(form_data)
|
103
|
+
xml.elements["undelete"].attributes["revisions"].to_i
|
104
|
+
else
|
105
|
+
0 # No revisions to undelete
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Get a list of matching page titles
|
110
|
+
#
|
111
|
+
# [key] Search key, matched as a prefix (^key.*). May contain or equal a namespace.
|
112
|
+
#
|
113
|
+
# Returns array of page titles (empty if no matches)
|
114
|
+
def list(key)
|
115
|
+
titles = []
|
116
|
+
apfrom = nil
|
117
|
+
key, namespace = key.split(":", 2).reverse
|
118
|
+
namespace = namespaces_by_prefix[namespace] || 0
|
119
|
+
begin
|
120
|
+
form_data =
|
121
|
+
{'action' => 'query',
|
122
|
+
'list' => 'allpages',
|
123
|
+
'apfrom' => apfrom,
|
124
|
+
'apprefix' => key,
|
125
|
+
'aplimit' => 500, # max allowed by API
|
126
|
+
'apnamespace' => namespace}
|
127
|
+
res = make_api_request(form_data)
|
128
|
+
apfrom = res.elements['query-continue'] ? res.elements['query-continue/allpages'].attributes['apfrom'] : nil
|
129
|
+
titles += REXML::XPath.match(res, "//p").map { |x| x.attributes["title"] }
|
130
|
+
end while apfrom
|
131
|
+
titles
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get a list of pages with matching content in given namespaces
|
135
|
+
#
|
136
|
+
# [key] Search key
|
137
|
+
# [namespaces] Array of namespace names to search (defaults to NS_MAIN only)
|
138
|
+
# [limit] Max number of hits to return
|
139
|
+
#
|
140
|
+
# Returns array of page titles (empty if no matches)
|
141
|
+
def search(key, namespaces=nil, limit=10)
|
142
|
+
titles = []
|
143
|
+
form_data = { 'action' => 'query',
|
144
|
+
'list' => 'search',
|
145
|
+
'srwhat' => 'text',
|
146
|
+
'srsearch' => key,
|
147
|
+
'srlimit' => limit}
|
148
|
+
if namespaces
|
149
|
+
namespaces = [ namespaces ] unless namespaces.kind_of? Array
|
150
|
+
form_data['srnamespace'] = namespaces.map! do |ns| namespaces_by_prefix[ns] end.join('|')
|
151
|
+
end
|
152
|
+
titles += REXML::XPath.match(make_api_request(form_data), "//p").map { |x| x.attributes["title"] }
|
153
|
+
end
|
154
|
+
|
155
|
+
# Upload file to MediaWiki
|
156
|
+
# Requires Mediawiki 1.16+
|
157
|
+
#
|
158
|
+
# [path] Path to file to upload
|
159
|
+
# [options] Hash of additional options
|
160
|
+
#
|
161
|
+
# Options:
|
162
|
+
# * [description] Description of this file
|
163
|
+
# * [target] Target filename, defaults to local name if not given
|
164
|
+
# * [summary] Edit summary for history
|
165
|
+
def upload(path, options={})
|
166
|
+
comment = (options[:summary] || "Uploaded by MediaWiki::Gateway")
|
167
|
+
file = File.new(path)
|
168
|
+
filename = (options[:target] || File.basename(path))
|
169
|
+
form_data = { 'action' => 'upload',
|
170
|
+
'filename' => filename,
|
171
|
+
'file' => file,
|
172
|
+
'token' => get_token('edit', filename),
|
173
|
+
'text' => (options[:description] || options[:summary]),
|
174
|
+
'comment' => comment}
|
175
|
+
make_api_request(form_data)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Imports a MediaWiki XML dump
|
179
|
+
#
|
180
|
+
# [xml] String or array of page names to fetch
|
181
|
+
#
|
182
|
+
# Returns XML array <api><import><page/><page/>...
|
183
|
+
# <page revisions="1"> (or more) means successfully imported
|
184
|
+
# <page revisions="0"> means duplicate, not imported
|
185
|
+
def import(xmlfile)
|
186
|
+
form_data = { "action" => "import",
|
187
|
+
"xml" => File.new(xmlfile),
|
188
|
+
"token" => get_token('import', 'Main Page'), # NB: dummy page name
|
189
|
+
"format" => 'xml' }
|
190
|
+
make_api_request(form_data)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Exports a page or set of pages
|
194
|
+
#
|
195
|
+
# [page_titles] String or array of page titles to fetch
|
196
|
+
#
|
197
|
+
# Returns MediaWiki XML dump
|
198
|
+
def export(page_titles)
|
199
|
+
form_data = {'action' => 'query', 'titles' => [page_titles].join('|'), 'export' => nil, 'exportnowrap' => nil}
|
200
|
+
return make_api_request(form_data)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Get a list of all known namespaces
|
204
|
+
#
|
205
|
+
# Returns array of namespaces (name => id)
|
206
|
+
def namespaces_by_prefix
|
207
|
+
form_data = { 'action' => 'query', 'meta' => 'siteinfo', 'siprop' => 'namespaces' }
|
208
|
+
res = make_api_request(form_data)
|
209
|
+
REXML::XPath.match(res, "//ns").inject(Hash.new) do |namespaces, namespace|
|
210
|
+
prefix = namespace.attributes["canonical"] || ""
|
211
|
+
namespaces[prefix] = namespace.attributes["id"].to_i
|
212
|
+
namespaces
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Get a list of all installed (and registered) extensions
|
217
|
+
#
|
218
|
+
# Returns array of extensions (name => version)
|
219
|
+
def extensions
|
220
|
+
form_data = { 'action' => 'query', 'meta' => 'siteinfo', 'siprop' => 'extensions' }
|
221
|
+
res = make_api_request(form_data)
|
222
|
+
REXML::XPath.match(res, "//ext").inject(Hash.new) do |extensions, extension|
|
223
|
+
name = extension.attributes["name"] || ""
|
224
|
+
extensions[name] = extension.attributes["version"]
|
225
|
+
extensions
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Execute Semantic Mediawiki query
|
230
|
+
#
|
231
|
+
# [query] Semantic Mediawiki query
|
232
|
+
# [params] Array of additional parameters or options, eg. mainlabel=Foo or ?Place (optional)
|
233
|
+
#
|
234
|
+
# Returns result as an HTML string
|
235
|
+
def semantic_query(query, params = [])
|
236
|
+
params << "format=list"
|
237
|
+
form_data = { 'action' => 'parse', 'prop' => 'text', 'text' => "{{#ask:#{query}|#{params.join('|')}}}" }
|
238
|
+
xml = make_api_request(form_data)
|
239
|
+
return xml.elements["parse/text"].text
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
# Fetch token (type 'delete', 'edit', 'import')
|
245
|
+
def get_token(type, page_titles)
|
246
|
+
form_data = {'action' => 'query', 'prop' => 'info', 'intoken' => type, 'titles' => page_titles}
|
247
|
+
res = make_api_request(form_data)
|
248
|
+
token = res.elements["query/pages/page"].attributes[type + "token"]
|
249
|
+
raise "User is not permitted to perform this operation: #{type}" if token.nil?
|
250
|
+
token
|
251
|
+
end
|
252
|
+
|
253
|
+
def get_undelete_token(page_titles)
|
254
|
+
form_data = {'action' => 'query', 'list' => 'deletedrevs', 'prop' => 'info', 'drprop' => 'token', 'titles' => page_titles}
|
255
|
+
res = make_api_request(form_data)
|
256
|
+
if res.elements["query/deletedrevs/page"]
|
257
|
+
token = res.elements["query/deletedrevs/page"].attributes["token"]
|
258
|
+
raise "User is not permitted to perform this operation: #{type}" if token.nil?
|
259
|
+
token
|
260
|
+
else
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Make generic request to API
|
266
|
+
#
|
267
|
+
# [form_data] hash or string of attributes to post
|
268
|
+
#
|
269
|
+
# Returns XML document
|
270
|
+
def make_api_request(form_data)
|
271
|
+
form_data['format'] = 'xml' if form_data.kind_of? Hash
|
272
|
+
@log.debug("REQ: #{form_data.inspect}, #{@cookies.inspect}")
|
273
|
+
RestClient.post(@wiki_url, form_data, @headers.merge({:cookies => @cookies})) do |response, &block|
|
274
|
+
# Check response for errors and return XML
|
275
|
+
raise "API error, bad response: #{response}" unless response.code >= 200 and response.code < 300
|
276
|
+
doc = get_response(response.dup)
|
277
|
+
if(form_data['action'] == 'login')
|
278
|
+
login_result = doc.elements["login"].attributes['result']
|
279
|
+
@cookies.merge!(response.cookies)
|
280
|
+
case login_result
|
281
|
+
when "Success" then # do nothing
|
282
|
+
when "NeedToken" then make_api_request(form_data.merge('lgtoken' => doc.elements["login"].attributes["token"]))
|
283
|
+
else raise "Login failed: " + login_result
|
284
|
+
end
|
285
|
+
end
|
286
|
+
return doc
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
|
291
|
+
# Get API XML response
|
292
|
+
# If there are errors, print and bail out
|
293
|
+
# Otherwise return XML root
|
294
|
+
def get_response(res)
|
295
|
+
doc = REXML::Document.new(res).root
|
296
|
+
@log.debug("RES: #{doc}")
|
297
|
+
raise "API error, response does not contain Mediawiki API XML: #{res}" unless [ "api", "mediawiki" ].include? doc.name
|
298
|
+
if doc.elements["error"]
|
299
|
+
code = doc.elements["error"].attributes["code"]
|
300
|
+
info = doc.elements["error"].attributes["info"]
|
301
|
+
raise "API error: code '#{code}', info '#{info}'"
|
302
|
+
end
|
303
|
+
doc
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
end
|