lib/media_wiki/gateway.rb in mediawiki-gateway-0.6.0 vs lib/media_wiki/gateway.rb in mediawiki-gateway-0.6.1

- old
+ new

@@ -12,21 +12,22 @@ # Set up a MediaWiki::Gateway for a given MediaWiki installation # # [url] Path to API of target MediaWiki (eg. "http://en.wikipedia.org/w/api.php") # [options] Hash of options + # [http_options] Hash of options for RestClient::Request (via http_send) # # Options: # [:bot] When set to true, executes API queries with the bot parameter (see http://www.mediawiki.org/wiki/API:Edit#Parameters). Defaults to false. # [:ignorewarnings] Log API warnings and invalid page titles, instead throwing MediaWiki::APIError # [:limit] Maximum number of results returned per search (see http://www.mediawiki.org/wiki/API:Query_-_Lists#Limits), defaults to the MediaWiki default of 500. # [:logdevice] Log device to use. Defaults to STDERR # [:loglevel] Log level to use, defaults to Logger::WARN. Set to Logger::DEBUG to dump every request and response to the log. # [:maxlag] Maximum allowed server lag (see http://www.mediawiki.org/wiki/Manual:Maxlag_parameter), defaults to 5 seconds. # [:retry_count] Number of times to try before giving up if MediaWiki returns 503 Service Unavailable, defaults to 3 (original request plus two retries). # [:retry_delay] Seconds to wait before retry if MediaWiki returns 503 Service Unavailable, defaults to 10 seconds. - def initialize(url, options={}) + def initialize(url, options={}, http_options={}) default_options = { :bot => false, :limit => 500, :logdevice => STDERR, :loglevel => Logger::WARN, @@ -34,10 +35,11 @@ :retry_count => 3, :retry_delay => 10, :max_results => 500 } @options = default_options.merge(options) + @http_options = http_options @wiki_url = url @log = Logger.new(@options[:logdevice]) @log.level = @options[:loglevel] @headers = { "User-Agent" => "MediaWiki::Gateway/#{MediaWiki::VERSION}", "Accept-Encoding" => "gzip" } @cookies = {} @@ -48,43 +50,58 @@ # Login to MediaWiki # # [username] Username # [password] Password # [domain] Domain for authentication plugin logins (eg. LDAP), optional -- defaults to 'local' if not given + # [options] Hash of additional options # # Throws MediaWiki::Unauthorized if login fails - def login(username, password, domain = 'local') - form_data = {'action' => 'login', 'lgname' => username, 'lgpassword' => password, 'lgdomain' => domain} - make_api_request(form_data) + def login(username, password, domain = 'local', options = {}) + make_api_request(options.merge( + 'action' => 'login', + 'lgname' => username, + 'lgpassword' => password, + 'lgdomain' => domain + )) + @password = password @username = username end # Fetch MediaWiki page in MediaWiki format. Does not follow redirects. # # [page_title] Page title to fetch + # [options] Hash of additional options # # Returns content of page as string, nil if the page does not exist. - def get(page_title) - form_data = {'action' => 'query', 'prop' => 'revisions', 'rvprop' => 'content', 'titles' => page_title} - page = make_api_request(form_data).first.elements["query/pages/page"] - if valid_page? page - page.elements["revisions/rev"].text || "" - end + def get(page_title, options = {}) + page = make_api_request(options.merge( + 'action' => 'query', + 'prop' => 'revisions', + 'rvprop' => 'content', + 'titles' => page_title + )).first.elements['query/pages/page'] + + page.elements['revisions/rev'].text || '' if valid_page?(page) end # Fetch latest revision ID of a MediaWiki page. Does not follow redirects. # # [page_title] Page title to fetch + # [options] Hash of additional options # # Returns revision ID as a string, nil if the page does not exist. - def revision(page_title) - form_data = {'action' => 'query', 'prop' => 'revisions', 'rvprop' => 'ids', 'rvlimit' => 1, 'titles' => page_title} - page = make_api_request(form_data).first.elements["query/pages/page"] - if valid_page? page - page.elements["revisions/rev"].attributes["revid"] - end + def revision(page_title, options = {}) + page = make_api_request(options.merge( + 'action' => 'query', + 'prop' => 'revisions', + 'rvprop' => 'ids', + 'rvlimit' => 1, + 'titles' => page_title + )).first.elements['query/pages/page'] + + page.elements['revisions/rev'].attributes['revid'] if valid_page?(page) end # Render a MediaWiki page as HTML # # [page_title] Page title to fetch @@ -108,11 +125,11 @@ if parsed.attributes["revid"] != '0' rendered = parsed.elements["text"].text.gsub(/<!--(.|\s)*?-->/, '') # OPTIMIZE: unifiy the keys in +options+ like symbolize_keys! but w/o if options["linkbase"] or options[:linkbase] linkbase = options["linkbase"] || options[:linkbase] - rendered = rendered.gsub(/\shref="\/wiki\/([\w\(\)_\-\.%\d:,]*)"/, ' href="' + linkbase + '/wiki/\1"') + rendered = rendered.gsub(/\shref="\/wiki\/([\w\(\)\-\.%:,]*)"/, ' href="' + linkbase + '/wiki/\1"') end if options["noeditsections"] or options[:noeditsections] rendered = rendered.gsub(/<span class="editsection">\[.+\]<\/span>/, '') end if options["noimages"] or options[:noimages] @@ -238,25 +255,32 @@ end # Delete one page. (MediaWiki API does not support deleting multiple pages at a time.) # # [title] Title of page to delete - def delete(title) - form_data = {'action' => 'delete', 'title' => title, 'token' => get_token('delete', title)} - make_api_request(form_data) + # [options] Hash of additional options + def delete(title, options = {}) + make_api_request(options.merge( + 'action' => 'delete', + 'title' => title, + 'token' => get_token('delete', title) + )) end # Undelete all revisions of one page. # # [title] Title of page to undelete + # [options] Hash of additional options # # Returns number of revisions undeleted, or zero if nothing to undelete - def undelete(title) - token = get_undelete_token(title) - if token - form_data = {'action' => 'undelete', 'title' => title, 'token' => token } - make_api_request(form_data).first.elements["undelete"].attributes["revisions"].to_i + def undelete(title, options = {}) + if token = get_undelete_token(title) + make_api_request(options.merge( + 'action' => 'undelete', + 'title' => title, + 'token' => token + )).first.elements['undelete'].attributes['revisions'].to_i else 0 # No revisions to undelete end end @@ -265,123 +289,94 @@ # [key] Search key, matched as a prefix (^key.*). May contain or equal a namespace, defaults to main (namespace 0) if none given. # [options] Optional hash of additional options, eg. { 'apfilterredir' => 'nonredirects' }. See http://www.mediawiki.org/wiki/API:Allpages # # Returns array of page titles (empty if no matches) def list(key, options = {}) - titles = [] - apfrom = nil - key, namespace = key.split(":", 2).reverse + key, namespace = key.split(':', 2).reverse namespace = namespaces_by_prefix[namespace] || 0 - begin - form_data = options.merge( - {'action' => 'query', - 'list' => 'allpages', - 'apfrom' => apfrom, - 'apprefix' => key, - 'aplimit' => @options[:limit], - 'apnamespace' => namespace}) - res, apfrom = make_api_request(form_data, '//query-continue/allpages/@apfrom') - titles += REXML::XPath.match(res, "//p").map { |x| x.attributes["title"] } - end while apfrom - titles + + iterate_query('allpages', '//p', 'title', 'apfrom', options.merge( + 'list' => 'allpages', + 'apprefix' => key, + 'apnamespace' => namespace, + 'aplimit' => @options[:limit] + )) end # Get a list of pages that are members of a category # # [category] Name of the category # [options] Optional hash of additional options. See http://www.mediawiki.org/wiki/API:Categorymembers # # Returns array of page titles (empty if no matches) def category_members(category, options = {}) - titles = [] - apfrom = nil - begin - form_data = options.merge( - {'action' => 'query', - 'list' => 'categorymembers', - 'apfrom' => apfrom, - 'cmtitle' => category, - 'cmlimit' => @options[:limit]}) - res, apfrom = make_api_request(form_data, '//query-continue/categorymembers/@apfrom') - titles += REXML::XPath.match(res, "//cm").map { |x| x.attributes["title"] } - end while apfrom - titles + iterate_query('categorymembers', '//cm', 'title', 'cmcontinue', options.merge( + 'cmtitle' => category, + 'cmlimit' => @options[:limit] + )) end # Get a list of pages that link to a target page # # [title] Link target page # [filter] "all" links (default), "redirects" only, or "nonredirects" (plain links only) + # [options] Hash of additional options # # Returns array of page titles (empty if no matches) - def backlinks(title, filter = "all") - titles = [] - blcontinue = nil - begin - form_data = - {'action' => 'query', - 'list' => 'backlinks', - 'bltitle' => title, - 'blfilterredir' => filter, - 'bllimit' => @options[:limit] } - form_data['blcontinue'] = blcontinue if blcontinue - res, blcontinue = make_api_request(form_data, '//query-continue/backlinks/@blcontinue') - titles += REXML::XPath.match(res, "//bl").map { |x| x.attributes["title"] } - end while blcontinue - titles + def backlinks(title, filter = 'all', options = {}) + iterate_query('backlinks', '//bl', 'title', 'blcontinue', options.merge( + 'bltitle' => title, + 'blfilterredir' => filter, + 'bllimit' => @options[:limit] + )) end # Get a list of pages with matching content in given namespaces # # [key] Search key # [namespaces] Array of namespace names to search (defaults to main only) # [limit] Maximum number of hits to ask for (defaults to 500; note that Wikimedia Foundation wikis allow only 50 for normal users) # [max_results] Maximum total number of results to return + # [options] Hash of additional options # # Returns array of page titles (empty if no matches) - def search(key, namespaces=nil, limit=@options[:limit], max_results=@options[:max_results]) + def search(key, namespaces = nil, limit = @options[:limit], max_results = @options[:max_results], options = {}) titles = [] offset = 0 - in_progress = true - form_data = { 'action' => 'query', - 'list' => 'search', - 'srwhat' => 'text', + form_data = options.merge( + 'action' => 'query', + 'list' => 'search', + 'srwhat' => 'text', 'srsearch' => key, - 'srlimit' => limit - } + 'srlimit' => limit + ) + if namespaces namespaces = [ namespaces ] unless namespaces.kind_of? Array form_data['srnamespace'] = namespaces.map! do |ns| namespaces_by_prefix[ns] end.join('|') end + begin form_data['sroffset'] = offset if offset form_data['srlimit'] = [limit, max_results - offset.to_i].min res, offset = make_api_request(form_data, '//query-continue/search/@sroffset') titles += REXML::XPath.match(res, "//p").map { |x| x.attributes["title"] } end while offset && offset.to_i < max_results.to_i + titles end # Get a list of users # # [options] Optional hash of options, eg. { 'augroup' => 'sysop' }. See http://www.mediawiki.org/wiki/API:Allusers # # Returns array of user names (empty if no matches) def users(options = {}) - names = [] - aufrom = nil - begin - form_data = options.merge( - {'action' => 'query', - 'list' => 'allusers', - 'aufrom' => aufrom, - 'aulimit' => @options[:limit]}) - res, aufrom = make_api_request(form_data, '//query-continue/allusers/@aufrom') - names += REXML::XPath.match(res, "//u").map { |x| x.attributes["name"] } - end while aufrom - names + iterate_query('allusers', '//u', 'name', 'aufrom', options.merge( + 'aulimit' => @options[:limit] + )) end # Get user contributions # # user: The user name @@ -389,24 +384,20 @@ # [options] Optional hash of options, eg. { 'ucnamespace' => 4 }. See http://www.mediawiki.org/wiki/API:Usercontribs # # Returns array of hashes containing the "item" attributes defined here: http://www.mediawiki.org/wiki/API:Usercontribs def contributions(user, count = nil, options = {}) result = [] - ucstart = options[:ucstart] or nil - begin - limit = [count, @options[:limit]].compact.min - form_data = options.merge( - {'action' => 'query', - 'list' => 'usercontribs', - 'ucuser' => user, - 'ucstart' => ucstart, - 'uclimit' => limit}) - res, ucstart = make_api_request(form_data, '//query-continue/usercontribs/@ucstart') - result += REXML::XPath.match(res, "//item").map { |x| x.attributes.inject({}) { |hash, data| hash[data.first] = data.last; hash } } - break if count and result.size >= count - end while ucstart - result + + iterate_query('usercontribs', '//item', nil, 'uccontinue', options.merge( + 'ucuser' => user, + 'uclimit' => @options[:limit] + )) { |element| + result << hash = {} + element.attributes.each { |key, value| hash[key] = value } + } + + count ? result.take(count) : result end # Upload a file, or get the status of pending uploads. Several # methods are available: # @@ -488,53 +479,66 @@ # Get image list for given article[s]. Follows redirects. # # _article_or_pageid_ is the title or pageid of a single article # _imlimit_ is the maximum number of images to return (defaults to 200) + # _options_ is the hash of additional options # # Example: # images = mw.images('Gaborone') # _images_ would contain ['File:Gaborone at night.jpg', 'File:Gaborone2.png', ...] + def images(article_or_pageid, imlimit = 200, options = {}) + form_data = options.merge( + 'action' => 'query', + 'prop' => 'images', + 'imlimit' => imlimit, + 'redirects' => true + ) - def images(article_or_pageid, imlimit = 200) - form_data = {'action' => 'query', 'prop' => 'images', 'imlimit' => imlimit, 'redirects' => true} case article_or_pageid when Fixnum form_data['pageids'] = article_or_pageid else form_data['titles'] = article_or_pageid end - xml, dummy = make_api_request(form_data) + xml, _ = make_api_request(form_data) page = xml.elements["query/pages/page"] if valid_page? page if xml.elements["query/redirects/r"] # We're dealing with redirect here. images(page.attributes["pageid"].to_i, imlimit) else - imgs = REXML::XPath.match(page, "images/im").map { |x| x.attributes["title"] } + REXML::XPath.match(page, "images/im").map { |x| x.attributes["title"] } end else nil end end # Get list of interlanguage links for given article[s]. Follows redirects. Returns a hash like { 'id' => 'Yerusalem', 'en' => 'Jerusalem', ... } # # _article_or_pageid_ is the title or pageid of a single article # _lllimit_ is the maximum number of langlinks to return (defaults to 500, the maximum) + # _options_ is the hash of additional options # # Example: # langlinks = mw.langlinks('Jerusalem') - def langlinks(article_or_pageid, lllimit = 500) - form_data = {'action' => 'query', 'prop' => 'langlinks', 'lllimit' => lllimit, 'redirects' => true} + def langlinks(article_or_pageid, lllimit = 500, options = {}) + form_data = options.merge( + 'action' => 'query', + 'prop' => 'langlinks', + 'lllimit' => lllimit, + 'redirects' => true + ) + case article_or_pageid when Fixnum form_data['pageids'] = article_or_pageid else form_data['titles'] = article_or_pageid end - xml, dummy = make_api_request(form_data) + xml, _ = make_api_request(form_data) page = xml.elements["query/pages/page"] if valid_page? page if xml.elements["query/redirects/r"] # We're dealing with the redirect here. langlinks(page.attributes["pageid"].to_i, lllimit) @@ -602,11 +606,11 @@ form_data['pageids'] = file_name_or_page_id else form_data['titles'] = "File:#{file_name_or_page_id}" end - xml, dummy = make_api_request(form_data) + xml, _ = make_api_request(form_data) page = xml.elements["query/pages/page"] if valid_page? page if xml.elements["query/redirects/r"] # We're dealing with redirect here. image_info(page.attributes["pageid"].to_i, options) @@ -633,51 +637,70 @@ end # Imports a MediaWiki XML dump # # [xml] String or array of page names to fetch + # [options] Hash of additional options # # Returns XML array <api><import><page/><page/>... # <page revisions="1"> (or more) means successfully imported # <page revisions="0"> means duplicate, not imported - def import(xmlfile) - form_data = { "action" => "import", - "xml" => File.new(xmlfile), - "token" => get_token('import', 'Main Page'), # NB: dummy page name - "format" => 'xml' } - make_api_request(form_data) + def import(xmlfile, options = {}) + make_api_request(options.merge( + 'action' => 'import', + 'xml' => File.new(xmlfile), + 'token' => get_token('import', 'Main Page'), # NB: dummy page name + 'format' => 'xml' + )) end # Exports a page or set of pages # # [page_titles] String or array of page titles to fetch + # [options] Hash of additional options # # Returns MediaWiki XML dump - def export(page_titles) - form_data = {'action' => 'query', 'titles' => [page_titles].join('|'), 'export' => nil, 'exportnowrap' => nil} - make_api_request(form_data).first + def export(page_titles, options = {}) + make_api_request(options.merge( + 'action' => 'query', + 'titles' => Array(page_titles).join('|'), + 'export' => nil, + 'exportnowrap' => nil + )).first end # Get a list of all known namespaces # + # [options] Hash of additional options + # # Returns array of namespaces (name => id) - def namespaces_by_prefix - form_data = { 'action' => 'query', 'meta' => 'siteinfo', 'siprop' => 'namespaces' } - res = make_api_request(form_data) + def namespaces_by_prefix(options = {}) + res = make_api_request(options.merge( + 'action' => 'query', + 'meta' => 'siteinfo', + 'siprop' => 'namespaces' + )).first + REXML::XPath.match(res, "//ns").inject(Hash.new) do |namespaces, namespace| prefix = namespace.attributes["canonical"] || "" namespaces[prefix] = namespace.attributes["id"].to_i namespaces end end # Get a list of all installed (and registered) extensions # + # [options] Hash of additional options + # # Returns array of extensions (name => version) - def extensions - form_data = { 'action' => 'query', 'meta' => 'siteinfo', 'siprop' => 'extensions' } - res = make_api_request(form_data) + def extensions(options = {}) + res = make_api_request(options.merge( + 'action' => 'query', + 'meta' => 'siteinfo', + 'siprop' => 'extensions' + )).first + REXML::XPath.match(res, "//ext").inject(Hash.new) do |extensions, extension| name = extension.attributes["name"] || "" extensions[name] = extension.attributes["version"] extensions end @@ -686,59 +709,70 @@ # Sends e-mail to a user # # [user] Username to send mail to (name only: eg. 'Bob', not 'User:Bob') # [subject] Subject of message # [content] Content of message + # [options] Hash of additional options # # 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. - def email_user(user, subject, text) - form_data = { 'action' => 'emailuser', 'target' => user, 'subject' => subject, 'text' => text, 'token' => get_token('email', "User:" + user) } - res, dummy = make_api_request(form_data) + def email_user(user, subject, text, options = {}) + res = make_api_request(options.merge( + 'action' => 'emailuser', + 'target' => user, + 'subject' => subject, + 'text' => text, + 'token' => get_token('email', "User:#{user}") + )).first + res.elements['emailuser'].attributes['result'] == 'Success' end # Execute Semantic Mediawiki query # # [query] Semantic Mediawiki query # [params] Array of additional parameters or options, eg. mainlabel=Foo or ?Place (optional) + # [options] Hash of additional options # # Returns result as an HTML string - def semantic_query(query, params = []) - if extensions.include? 'Semantic MediaWiki' - smw_version = extensions['Semantic MediaWiki'].to_f - if smw_version >= 1.7 - form_data = { 'action' => 'ask', 'query' => "#{query}|#{params.join('|')}"} - xml, dummy = make_api_request(form_data) - return xml - else - params << "format=list" - form_data = { 'action' => 'parse', 'prop' => 'text', 'text' => "{{#ask:#{query}|#{params.join('|')}}}" } - xml, dummy = make_api_request(form_data) - return xml.elements["parse/text"].text - end - else - raise MediaWiki::Exception.new "Semantic MediaWiki extension not installed." - end + def semantic_query(query, params = [], options = {}) + unless smw_version = extensions['Semantic MediaWiki'] + raise MediaWiki::Exception, 'Semantic MediaWiki extension not installed.' + end + + if smw_version.to_f >= 1.7 + make_api_request(options.merge( + 'action' => 'ask', + 'query' => "#{query}|#{params.join('|')}" + )).first + else + make_api_request(options.merge( + 'action' => 'parse', + 'prop' => 'text', + 'text' => "{{#ask:#{query}|#{params.push('format=list').join('|')}}}" + )).first.elements['parse/text'].text + end end # Create a new account # # [options] is +Hash+ passed as query arguments. See https://www.mediawiki.org/wiki/API:Account_creation#Parameters for more information. def create_account(options) - form_data = options.merge({ 'action' => 'createaccount' }) - res, dummy = make_api_request(form_data) - res + make_api_request(options.merge('action' => 'createaccount')).first end # Sets options for currenlty logged in user # # [changes] a +Hash+ that will be transformed into an equal sign and pipe-separated key value parameter # [optionname] a +String+ indicating which option to change (optional) # [optionvalue] the new value for optionname - allows pipe characters (optional) # [reset] a +Boolean+ indicating if all preferences should be reset to site defaults (optional) - def options(changes = {}, optionname = nil, optionvalue = nil, reset = false) - form_data = { 'action' => 'options', 'token' => get_options_token } + # [options] Hash of additional options + def options(changes = {}, optionname = nil, optionvalue = nil, reset = false, options = {}) + form_data = options.merge( + 'action' => 'options', + 'token' => get_options_token + ) if changes.present? form_data['change'] = changes.map { |key, value| "#{key}=#{value}" }.join('|') end @@ -748,51 +782,59 @@ if reset form_data['reset'] = true end - res, dummy = make_api_request(form_data) - res + make_api_request(form_data).first end # Set groups for a user # # [user] Username of user to modify # [groups_to_add] Groups to add user to, as an array or a string if a single group (optional) # [groups_to_remove] Groups to remove user from, as an array or a string if a single group (optional) - def set_groups(user, groups_to_add = [], groups_to_remove = [], comment = '') + # [options] Hash of additional options + def set_groups(user, groups_to_add = [], groups_to_remove = [], comment = '', options = {}) token = get_userrights_token(user) - userrights(user, token, groups_to_add, groups_to_remove, comment) + userrights(user, token, groups_to_add, groups_to_remove, comment, options) end # Review current revision of an article (requires FlaggedRevisions extension, see http://www.mediawiki.org/wiki/Extension:FlaggedRevs) # # [title] Title of article to review # [flags] Hash of flags and values to set, eg. { "accuracy" => "1", "depth" => "2" } # [comment] Comment to add to review (optional) - def review(title, flags, comment = "Reviewed by MediaWiki::Gateway") + # [options] Hash of additional options + def review(title, flags, comment = "Reviewed by MediaWiki::Gateway", options = {}) raise APIError.new('missingtitle', "Article #{title} not found") unless revid = revision(title) - form_data = {'action' => 'review', 'revid' => revid, 'token' => get_token('edit', title), 'comment' => comment} - form_data.merge!( Hash[flags.map {|k,v| ["flag_#{k}", v]}] ) - res, dummy = make_api_request(form_data) - res + + form_data = options.merge( + 'action' => 'review', + 'revid' => revid, + 'token' => get_token('edit', title), + 'comment' => comment + ) + + flags.each { |k, v| form_data["flag_#{k}"] = v } + + make_api_request(form_data).first end private # Fetch token (type 'delete', 'edit', 'email', 'import', 'move', 'protect') def get_token(type, page_titles) form_data = {'action' => 'query', 'prop' => 'info', 'intoken' => type, 'titles' => page_titles} - res, dummy = make_api_request(form_data) + res, _ = make_api_request(form_data) token = res.elements["query/pages/page"].attributes[type + "token"] raise Unauthorized.new "User is not permitted to perform this operation: #{type}" if token.nil? token end def get_undelete_token(page_titles) form_data = {'action' => 'query', 'list' => 'deletedrevs', 'prop' => 'info', 'drprop' => 'token', 'titles' => page_titles} - res, dummy = make_api_request(form_data) + res, _ = make_api_request(form_data) if res.elements["query/deletedrevs/page"] token = res.elements["query/deletedrevs/page"].attributes["token"] raise Unauthorized.new "User is not permitted to perform this operation: #{type}" if token.nil? token else @@ -801,11 +843,11 @@ end # User rights management (aka group assignment) def get_userrights_token(user) form_data = {'action' => 'query', 'list' => 'users', 'ustoken' => 'userrights', 'ususers' => user} - res, dummy = make_api_request(form_data) + res, _ = make_api_request(form_data) token = res.elements["query/users/user"].attributes["userrightstoken"] @log.debug("RESPONSE: #{res.to_s}") if token.nil? if res.elements["query/users/user"].attributes["missing"] @@ -818,30 +860,32 @@ token end def get_options_token form_data = { 'action' => 'tokens', 'type' => 'options' } - res, dummy = make_api_request(form_data) + res, _ = make_api_request(form_data) res.elements['tokens'].attributes['optionstoken'] end - def userrights(user, token, groups_to_add, groups_to_remove, reason) + def userrights(user, token, groups_to_add, groups_to_remove, reason, options = {}) # groups_to_add and groups_to_remove can be a string or an array. Turn them into MediaWiki's pipe-delimited list format. if groups_to_add.is_a? Array groups_to_add = groups_to_add.join('|') end + if groups_to_remove.is_a? Array groups_to_remove = groups_to_remove.join('|') end - form_data = {'action' => 'userrights', 'user' => user, 'token' => token, - 'add' => groups_to_add, + make_api_request(options.merge( + 'action' => 'userrights', + 'user' => user, + 'token' => token, + 'add' => groups_to_add, 'remove' => groups_to_remove, 'reason' => reason - } - res, dummy = make_api_request(form_data) - res + )).first end # Make a custom query # @@ -865,11 +909,44 @@ options.each {|k,v| form_data[k.to_s] = v.to_s } form_data['action'] = 'query' make_api_request(form_data).first.elements['query'] end + # Iterate over query results + # + # [list] list name to query + # [res_xpath] XPath selector for results + # [attr] attribute name to extract, if any + # [param] parameter name to continue query + # [options] additional query options + # + # Yields each attribute value, or, if +attr+ is nil, each REXML::Element. + def iterate_query(list, res_xpath, attr, param, options, &block) + items, block = [], lambda { |item| items << item } unless block + attribute_names = %w[from continue].map { |name| + "name()='#{param[0, 2]}#{name}'" + } + + req_xpath = "//query-continue/#{list}/@*[#{attribute_names.join(' or ')}]" + res_xpath = "//query/#{list}/#{res_xpath}" unless res_xpath.start_with?('/') + + options, continue = options.merge('action' => 'query', 'list' => list), nil + + loop { + res, continue = make_api_request(options, req_xpath) + + REXML::XPath.match(res, res_xpath).each { |element| + block[attr ? element.attributes[attr] : element] + } + + continue ? options[param] = continue : break + } + + items + end + # Make generic request to API # # [form_data] hash or string of attributes to post # [continue_xpath] XPath selector for query continue parameter # [retry_count] Counter for retries @@ -910,37 +987,38 @@ if action == 'login' raise Unauthorized.new("Login failed: #{action_result}") elsif action == 'createaccount' raise Unauthorized.new("Account creation failed: #{action_result}") end - end + end end - - continue = (continue_xpath and doc.elements['query-continue']) ? REXML::XPath.first(doc, continue_xpath).value : nil + continue = (continue_xpath and doc.elements['query-continue']) ? REXML::XPath.first(doc, continue_xpath) : nil return [doc, continue] end end # Execute the HTTP request using either GET or POST as appropriate def http_send url, form_data, headers, &block + opts = @http_options.merge(:url => url, :headers => headers) + if form_data['action'] == 'query' log.debug("GET: #{form_data.inspect}, #{@cookies.inspect}") headers[:params] = form_data - RestClient.get url, headers, &block + RestClient::Request.execute(opts.update(:method => :get), &block) else log.debug("POST: #{form_data.inspect}, #{@cookies.inspect}") - RestClient.post url, form_data, headers, &block + RestClient::Request.execute(opts.update(:method => :post, :payload => form_data), &block) end end # Get API XML response # If there are errors or warnings, raise APIError # Otherwise return XML root def get_response(res) begin res = res.force_encoding("UTF-8") if res.respond_to?(:force_encoding) doc = REXML::Document.new(res).root - rescue REXML::ParseException => e + rescue REXML::ParseException raise MediaWiki::Exception.new "Response is not XML. Are you sure you are pointing to api.php?" end log.debug("RES: #{doc}") raise MediaWiki::Exception.new "Response does not contain Mediawiki API XML: #{res}" unless [ "api", "mediawiki" ].include? doc.name if doc.elements["error"]