Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
8714 lines (8123 sloc) 344 KB
/**
* @(#)Wiki.java 0.35 20/05/2018
* Copyright (C) 2007 - 2018 MER-C and contributors
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version. Additionally
* this file is subject to the "Classpath" exception.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.wikipedia;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.file.*;
import java.text.Normalizer;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.*;
import java.util.zip.GZIPInputStream;
import javax.security.auth.login.*;
/**
* This is a somewhat sketchy bot framework for editing MediaWiki wikis.
* Requires JDK 1.8 or greater. Uses the <a
* href="https://mediawiki.org/wiki/API:Main_page">MediaWiki API</a> for most
* operations. It is recommended that the server runs the latest version
* of MediaWiki (1.31), otherwise some functions may not work. This framework
* requires no dependencies outside the core JDK and does not implement any
* functionality added by MediaWiki extensions.
* <p>
* Extended documentation is available
* <a href="https://github.com/MER-C/wiki-java/wiki/Extended-documentation">here</a>.
* All wikilinks are relative to the English Wikipedia and all timestamps are in
* your wiki's time zone.
* </p>
* Please file bug reports <a href="https://en.wikipedia.org/wiki/User_talk:MER-C">here</a>
* or at the <a href="https://github.com/MER-C/wiki-java/issues">Github issue
* tracker</a>.
*
* @author MER-C and contributors
* @version 0.35
*/
public class Wiki implements Comparable<Wiki>
{
// NAMESPACES
/**
* Denotes the namespace of images and media, such that there is no
* description page. Uses the "Media:" prefix.
* @see #FILE_NAMESPACE
* @since 0.03
*/
public static final int MEDIA_NAMESPACE = -2;
/**
* Denotes the namespace of pages with the "Special:" prefix. Note
* that many methods dealing with special pages may spew due to
* raw content not being available.
* @since 0.03
*/
public static final int SPECIAL_NAMESPACE = -1;
/**
* Denotes the main namespace, with no prefix.
* @since 0.03
*/
public static final int MAIN_NAMESPACE = 0;
/**
* Denotes the namespace for talk pages relating to the main
* namespace, denoted by the prefix "Talk:".
* @since 0.03
*/
public static final int TALK_NAMESPACE = 1;
/**
* Denotes the namespace for user pages, given the prefix "User:".
* @since 0.03
*/
public static final int USER_NAMESPACE = 2;
/**
* Denotes the namespace for user talk pages, given the prefix
* "User talk:".
* @since 0.03
*/
public static final int USER_TALK_NAMESPACE = 3;
/**
* Denotes the namespace for pages relating to the project,
* with prefix "Project:". It also goes by the name of whatever
* the project name was.
* @since 0.03
*/
public static final int PROJECT_NAMESPACE = 4;
/**
* Denotes the namespace for talk pages relating to project
* pages, with prefix "Project talk:". It also goes by the name
* of whatever the project name was, + "talk:".
* @since 0.03
*/
public static final int PROJECT_TALK_NAMESPACE = 5;
/**
* Denotes the namespace for file description pages. Has the prefix
* "File:". Do not create these directly, use upload() instead.
* @see #MEDIA_NAMESPACE
* @since 0.25
*/
public static final int FILE_NAMESPACE = 6;
/**
* Denotes talk pages for file description pages. Has the prefix
* "File talk:".
* @since 0.25
*/
public static final int FILE_TALK_NAMESPACE = 7;
/**
* Denotes the namespace for (wiki) system messages, given the prefix
* "MediaWiki:".
* @since 0.03
*/
public static final int MEDIAWIKI_NAMESPACE = 8;
/**
* Denotes the namespace for talk pages relating to system messages,
* given the prefix "MediaWiki talk:".
* @since 0.03
*/
public static final int MEDIAWIKI_TALK_NAMESPACE = 9;
/**
* Denotes the namespace for templates, given the prefix "Template:".
* @since 0.03
*/
public static final int TEMPLATE_NAMESPACE = 10;
/**
* Denotes the namespace for talk pages regarding templates, given
* the prefix "Template talk:".
* @since 0.03
*/
public static final int TEMPLATE_TALK_NAMESPACE = 11;
/**
* Denotes the namespace for help pages, given the prefix "Help:".
* @since 0.03
*/
public static final int HELP_NAMESPACE = 12;
/**
* Denotes the namespace for talk pages regarding help pages, given
* the prefix "Help talk:".
* @since 0.03
*/
public static final int HELP_TALK_NAMESPACE = 13;
/**
* Denotes the namespace for category description pages. Has the
* prefix "Category:".
* @since 0.03
*/
public static final int CATEGORY_NAMESPACE = 14;
/**
* Denotes the namespace for talk pages regarding categories. Has the
* prefix "Category talk:".
* @since 0.03
*/
public static final int CATEGORY_TALK_NAMESPACE = 15;
/**
* Denotes all namespaces.
* @since 0.03
*/
public static final int ALL_NAMESPACES = 0x09f91102;
// LOG TYPES
/**
* Denotes all logs.
* @since 0.06
*/
public static final String ALL_LOGS = "";
/**
* Denotes the user creation log.
* @since 0.06
*/
public static final String USER_CREATION_LOG = "newusers";
/**
* Denotes the upload log.
* @since 0.06
*/
public static final String UPLOAD_LOG = "upload";
/**
* Denotes the deletion log.
* @since 0.06
*/
public static final String DELETION_LOG = "delete";
/**
* Denotes the move log.
* @since 0.06
*/
public static final String MOVE_LOG = "move";
/**
* Denotes the block log.
* @since 0.06
*/
public static final String BLOCK_LOG = "block";
/**
* Denotes the protection log.
* @since 0.06
*/
public static final String PROTECTION_LOG = "protect";
/**
* Denotes the user rights log.
* @since 0.06
*/
public static final String USER_RIGHTS_LOG = "rights";
/**
* Denotes the user renaming log.
* @since 0.06
*/
public static final String USER_RENAME_LOG = "renameuser";
/**
* Denotes the page importation log.
* @since 0.08
*/
public static final String IMPORT_LOG = "import";
/**
* Denotes the edit patrol log.
* @since 0.08
*/
public static final String PATROL_LOG = "patrol";
/**
* Denotes the page creation log.
* @since 0.36
*/
public static final String PAGE_CREATION_LOG = "create";
// PROTECTION LEVELS
/**
* Denotes a non-protected page.
* @since 0.09
*/
public static final String NO_PROTECTION = "all";
/**
* Denotes semi-protection (only autoconfirmed users can perform a
* action).
* @since 0.09
*/
public static final String SEMI_PROTECTION = "autoconfirmed";
/**
* Denotes full protection (only admins can perfom a particular action).
* @since 0.09
*/
public static final String FULL_PROTECTION = "sysop";
// ASSERTION MODES
/**
* Use no assertions.
* @see #setAssertionMode
* @since 0.11
*/
public static final int ASSERT_NONE = 0;
/**
* Assert that we are logged in. This is checked every action.
* @see #setAssertionMode
* @since 0.30
*/
public static final int ASSERT_USER = 1;
/**
* Assert that we have a bot flag. This is checked every action.
* @see #setAssertionMode
* @since 0.11
*/
public static final int ASSERT_BOT = 2;
/**
* Assert that we have no new messages. Not defined officially, but
* some bots have this. This is checked intermittently.
* @see #setAssertionMode
* @since 0.11
*/
public static final int ASSERT_NO_MESSAGES = 4;
/**
* Assert that we have a sysop flag. This is checked intermittently.
* @see #setAssertionMode
* @since 0.30
*/
public static final int ASSERT_SYSOP = 8;
// REVISION OPTIONS
/**
* In {@link org.wikipedia.Wiki.Revision#diff(long) Revision.diff()},
* denotes the next revision.
* @see org.wikipedia.Wiki.Revision#diff(long)
* @since 0.21
*/
public static final long NEXT_REVISION = -1L;
/**
* In {@link org.wikipedia.Wiki.Revision#diff(long) Revision.diff()},
* denotes the current revision.
* @see org.wikipedia.Wiki.Revision#diff(long)
* @since 0.21
*/
public static final long CURRENT_REVISION = -2L;
/**
* In {@link org.wikipedia.Wiki.Revision#diff(long) Revision.diff()},
* denotes the previous revision.
* @see org.wikipedia.Wiki.Revision#diff(long)
* @since 0.21
*/
public static final long PREVIOUS_REVISION = -3L;
/**
* The list of options the user can specify for his/her gender.
* @see User#getGender()
* @since 0.24
*/
public enum Gender
{
// These names come from the MW API so we can use valueOf() and
// toString() without any fidgets whatsoever. Java naming conventions
// aren't worth another 20 lines of code.
/**
* The user self-identifies as a male.
* @since 0.24
*/
male,
/**
* The user self-identifies as a female.
* @since 0.24
*/
female,
/**
* The user has not specified a gender in preferences.
* @since 0.24
*/
unknown;
}
private static final String version = "0.35";
// fundamental URL strings
private final String protocol, domain, scriptPath;
private String base, articleUrl;
/**
* Stores default HTTP parameters for API calls. Contains {@linkplain
* #setMaxLag(int) maxlag}, {@linkplain #setResolveRedirects(boolean) redirect
* resolution} and {@linkplain #setAssertionMode(int) user and bot assertions}
* when wanted by default. Add stuff to this map if you want to add parameters
* to every API call.
* @see #makeApiCall(Map, Map, String)
*/
protected ConcurrentHashMap<String, String> defaultApiParams;
/**
* URL entrypoint for the MediaWiki API. (Needs to be accessible to
* subclasses.)
* @see #initVars()
* @see #getApiUrl()
* @see <a href="https://mediawiki.org/wiki/Manual:Api.php">MediaWiki
* documentation</a>
*/
protected String apiUrl;
// wiki properties
private boolean siteinfofetched = false;
private boolean wgCapitalLinks = true;
private String mwVersion;
private ZoneId timezone = ZoneOffset.UTC;
private Locale locale = Locale.ENGLISH;
private List<String> extensions = Collections.emptyList();
private LinkedHashMap<String, Integer> namespaces = null;
private ArrayList<Integer> ns_subpages = null;
// user management
private final CookieManager cookies;
private User user;
private int statuscounter = 0;
// watchlist cache
private List<String> watchlist = null;
// preferences
private int max = 500;
private int slowmax = 50;
private int throttle = 10000;
private int maxlag = 5;
private int assertion = ASSERT_NONE; // assertion mode
private int statusinterval = 100; // status check
private int querylimit = Integer.MAX_VALUE;
private String useragent = "Wiki.java/" + version + " (https://github.com/MER-C/wiki-java/)";
private boolean zipped = true;
private boolean markminor = false, markbot = false;
private boolean resolveredirect = false;
private Level loglevel = Level.ALL;
private static final Logger logger = Logger.getLogger("wiki");
// Store time when the last throttled action was executed
private long lastThrottleActionTime = 0;
// retry count
private final int maxtries = 2;
// time to open a connection
private static final int CONNECTION_CONNECT_TIMEOUT_MSEC = 30000; // 30 seconds
// time for the read to take place. (needs to be longer, some connections are slow
// and the data volume is large!)
private static final int CONNECTION_READ_TIMEOUT_MSEC = 180000; // 180 seconds
// log2(upload chunk size). Default = 22 => upload size = 4 MB. Disable
// chunked uploads by setting a large value here (50 = 1 PB will do).
// Stuff you actually upload must be no larger than 2 GB.
private static final int LOG2_CHUNK_SIZE = 22;
// CONSTRUCTORS AND CONFIGURATION
/**
* Creates a new connection to a wiki with <a
* href="https://mediawiki.org/wiki/Manual:$wgScriptPath"><var>
* $wgScriptPath</var></a> set to <var>scriptPath</var> and via the
* specified protocol.
*
* @param domain the wiki domain name
* @param scriptPath the script path
* @param protocol a protocol e.g. "http://", "https://" or "file:///"
* @since 0.31
*/
protected Wiki(String domain, String scriptPath, String protocol)
{
this.domain = Objects.requireNonNull(domain);
this.scriptPath = Objects.requireNonNull(scriptPath);
this.protocol = Objects.requireNonNull(protocol);
defaultApiParams = new ConcurrentHashMap<>();
defaultApiParams.put("format", "xml");
defaultApiParams.put("maxlag", String.valueOf(maxlag));
logger.setLevel(loglevel);
logger.log(Level.CONFIG, "[{0}] Using Wiki.java {1}", new Object[] { domain, version });
cookies = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
}
/**
* Creates a new connection to a wiki via HTTPS. Depending on the settings
* of the wiki, you may need to call {@link Wiki#getSiteInfo()} on the
* returned object after this in order for some functionality to work
* correctly.
*
* @param domain the wiki domain name e.g. en.wikipedia.org (defaults to
* en.wikipedia.org)
* @return the created wiki
* @since 0.34
*/
public static Wiki createInstance(String domain)
{
return createInstance(domain, "/w", "https://");
}
/**
* Creates a new connection to a wiki with <a
* href="https://mediawiki.org/wiki/Manual:$wgScriptPath"><var>
* $wgScriptPath</var></a> set to <var>scriptPath</var> and via the
* specified protocol. Depending on the settings of the wiki, you may need
* to call {@link Wiki#getSiteInfo()} on the returned object after this in
* order for some functionality to work correctly.
*
* <p>All factory methods in subclasses must call {@link #initVars()}.
*
* @param domain the wiki domain name
* @param scriptPath the script path
* @param protocol a protocol e.g. "http://", "https://" or "file:///"
* @return the constructed Wiki object
* @since 0.34
*/
public static Wiki createInstance(String domain, String scriptPath, String protocol)
{
// Don't put network requests here. Servlets cannot afford to make
// unnecessary network requests in initialization.
Wiki wiki = new Wiki(domain, scriptPath, protocol);
wiki.initVars(); // construct URL bases
return wiki;
}
/**
* Edit this if you need to change the API and human interface url
* configuration of the wiki. One example use is to change the port number.
*
* <p>Contributed by Tedder
* @since 0.24
*/
protected void initVars()
{
base = protocol + domain + scriptPath + "/index.php";
apiUrl = protocol + domain + scriptPath + "/api.php";
articleUrl = protocol + domain + "/wiki/";
}
/**
* Gets the domain of the wiki as supplied on construction.
* @return the domain of the wiki
* @since 0.06
*/
public final String getDomain()
{
return domain;
}
/**
* Gets the <a href="https://mediawiki.org/wiki/Manual:$wgScriptPath"><var>
* $wgScriptPath</var></a> variable as supplied on construction.
* @return the script path of the wiki
* @since 0.14
*/
public final String getScriptPath()
{
return scriptPath;
}
/**
* Gets the protocol used to access this MediaWiki instance, as supplied
* on construction.
* @return (see above)
* @since 0.35
*/
public final String getProtocol()
{
return protocol;
}
/**
* Determines whether this wiki is equal to another object based on the
* protocol (not case sensitive), domain (not case sensitive) and
* scriptPath (case sensitive). A return value of {@code true} means two
* Wiki objects point to the same instance of MediaWiki.
* @param obj the object to compare
* @return whether the wikis point to the same instance of MediaWiki
*/
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof Wiki))
return false;
Wiki other = (Wiki)obj;
return domain.equalsIgnoreCase(other.domain)
&& scriptPath.equals(other.scriptPath)
&& protocol.equalsIgnoreCase(other.protocol);
}
/**
* Returns a hash code of this object based on the protocol, domain and
* scriptpath.
* @return a hash code
*/
@Override
public int hashCode()
{
// English locale used here for reproducability and so network requests
// are not required
int hc = domain.toLowerCase(Locale.ENGLISH).hashCode();
hc = 127 * hc + scriptPath.hashCode();
hc = 127 * hc + protocol.toLowerCase(Locale.ENGLISH).hashCode();
return hc;
}
/**
* Allows wikis to be sorted based on their domain (case insensitive), then
* their script path (case sensitive). If 0 is returned, it is reasonable
* both Wikis point to the same instance of MediaWiki.
* @param other the wiki to compare to
* @return -1 if this wiki is alphabetically before the other, 1 if after
* and 0 if they are likely to be the same instance of MediaWiki
* @since 0.35
*/
@Override
public int compareTo(Wiki other)
{
int result = domain.compareToIgnoreCase(other.domain);
if (result == 0)
result = scriptPath.compareTo(other.scriptPath);
return result;
}
/**
* Gets the URL of index.php.
* @return (see above)
* @see <a href="https://mediawiki.org/wiki/Manual:Parameters_to_index.php">
* MediaWiki documentation</a>
* @since 0.35
*/
public String getIndexPhpUrl()
{
return base;
}
/**
* Gets the URL of api.php.
* @return (see above)
* @see <a href="https://mediawiki.org/wiki/Manual:Api.php">MediaWiki
* documentation</a>
* @since 0.36
*/
public String getApiUrl()
{
return apiUrl;
}
/**
* Gets the editing throttle.
* @return the throttle value in milliseconds
* @see #setThrottle
* @since 0.09
*/
public int getThrottle()
{
return throttle;
}
/**
* Sets the throttle, which limits most write requests to no more than one
* per wiki instance in the given time across all threads. (As a
* consequence, all throttled methods are thread safe.) Read requests are
* not throttled or restricted in any way. Default is 10 seconds.
*
* @param throttle the new throttle value in milliseconds
* @see #getThrottle
* @since 0.09
*/
public void setThrottle(int throttle)
{
this.throttle = throttle;
log(Level.CONFIG, "setThrottle", "Throttle set to " + throttle + " milliseconds");
}
/**
* Gets various properties of the wiki and sets the bot framework up to use
* them. The return value is cached. This method is thread safe. Returns:
* <ul>
* <li><b>usingcapitallinks</b>: (Boolean) whether a wiki forces upper case
* for the title. Example: en.wikipedia = true, en.wiktionary = false.
* Default = true. See <a href="https://mediawiki.org/wiki/Manual:$wgCapitalLinks">
* <var>$wgCapitalLinks</var></a>
* <li><b>scriptpath</b>: (String) the <a
* href="https://mediawiki.org/wiki/Manual:$wgScriptPath"><var>
* $wgScriptPath</var> wiki variable</a>. Default = {@code /w}.
* <li><b>version</b>: (String) the MediaWiki version used for this wiki
* <li><b>timezone</b>: (ZoneId) the timezone the wiki is in, default = UTC
* <li><b>locale</b>: (Locale) the locale of the wiki
* </ul>
*
* @return (see above)
* @since 0.30
* @throws IOException if a network error occurs
* @deprecated This method is likely going to get renamed with the return
* type changed to void once I finish cleaning up the site info caching
* mechanism. Use the specialized methods instead.
*/
@Deprecated
public synchronized Map<String, Object> getSiteInfo() throws IOException
{
Map<String, Object> siteinfo = new HashMap<>();
if (!siteinfofetched)
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("meta", "siteinfo");
getparams.put("siprop", "namespaces|namespacealiases|general|extensions");
String line = makeApiCall(getparams, null, "getSiteInfo");
// general site info
String bits = line.substring(line.indexOf("<general "), line.indexOf("</general>"));
wgCapitalLinks = parseAttribute(bits, "case", 0).equals("first-letter");
timezone = ZoneId.of(parseAttribute(bits, "timezone", 0));
mwVersion = parseAttribute(bits, "generator", 0);
locale = new Locale(parseAttribute(bits, "lang", 0));
// parse extensions
bits = line.substring(line.indexOf("<extensions>"), line.indexOf("</extensions>"));
extensions = new ArrayList<>();
String[] unparsed = bits.split("<ext ");
for (int i = 1; i < unparsed.length; i++)
extensions.add(parseAttribute(unparsed[i], "name", 0));
// populate namespace cache
namespaces = new LinkedHashMap<>(30);
ns_subpages = new ArrayList<>(30);
// xml form: <ns id="-2" canonical="Media" ... >Media</ns> or <ns id="0" ... />
String[] items = line.split("<ns ");
for (int i = 1; i < items.length; i++)
{
int ns = Integer.parseInt(parseAttribute(items[i], "id", 0));
// parse localized namespace name
// must be before parsing canonical namespace so that
// namespaceIdentifier always returns the localized name
int b = items[i].indexOf('>') + 1;
int c = items[i].indexOf("</ns>");
if (c < 0)
namespaces.put("", ns);
else
namespaces.put(normalize(decode(items[i].substring(b, c))), ns);
String canonicalnamespace = parseAttribute(items[i], "canonical", 0);
if (canonicalnamespace != null)
namespaces.put(canonicalnamespace, ns);
// does this namespace support subpages?
if (items[i].contains("subpages=\"\""))
ns_subpages.add(ns);
}
siteinfofetched = true;
log(Level.INFO, "getSiteInfo", "Successfully retrieved site info for " + getDomain());
}
siteinfo.put("usingcapitallinks", wgCapitalLinks);
siteinfo.put("scriptpath", scriptPath);
siteinfo.put("timezone", timezone);
siteinfo.put("version", mwVersion);
siteinfo.put("locale", locale);
siteinfo.put("extensions", extensions);
return siteinfo;
}
/**
* Gets the version of MediaWiki this wiki runs e.g. 1.20wmf5 (54b4fcb).
* See [[Special:Version]] on your wiki.
* @return (see above)
* @throws UncheckedIOException if the site info cache has not been
* populated and a network error occurred when populating it
* @since 0.14
* @see <a href="https://gerrit.wikimedia.org/">MediaWiki Git</a>
*/
public String version()
{
ensureNamespaceCache();
return mwVersion;
}
/**
* Detects whether a wiki forces upper case for the first character in a
* title. Example: en.wikipedia = true, en.wiktionary = false.
* @return (see above)
* @throws UncheckedIOException if the site info cache has not been
* populated and a network error occurred when populating it
* @see <a href="https://mediawiki.org/wiki/Manual:$wgCapitalLinks">MediaWiki
* documentation</a>
* @since 0.30
*/
public boolean usesCapitalLinks()
{
ensureNamespaceCache();
return wgCapitalLinks;
}
/**
* Returns the list of extensions installed on this wiki.
* @return (see above)
* @throws UncheckedIOException if the site info cache has not been
* populated and a network error occurred when populating it
* @see <a href="https://www.mediawiki.org/wiki/Manual:Extensions">MediaWiki
* documentation</a>
* @since 0.35
*/
public List<String> installedExtensions()
{
ensureNamespaceCache();
return new ArrayList<>(extensions);
}
/**
* Gets the timezone of this wiki
* @return (see above)
* @throws UncheckedIOException if the site info cache has not been
* populated and a network error occurred when populating it
* @since 0.35
*/
public ZoneId timezone()
{
ensureNamespaceCache();
return timezone;
}
/**
* Gets the locale of this wiki.
* @return (see above)
* @throws UncheckedIOException if the site info cache has not been
* populated and a network error occurred when populating it
* @since 0.35
*/
public Locale locale()
{
ensureNamespaceCache();
return locale;
}
/**
* Sets the user agent HTTP header to be used for requests. Default is
* <samp>"Wiki.java " + version</samp>.
* @param useragent the new user agent
* @since 0.22
*/
public void setUserAgent(String useragent)
{
this.useragent = useragent;
}
/**
* Gets the user agent HTTP header to be used for requests. Default is
* <samp>"Wiki.java " + version</samp>.
* @return useragent the user agent
* @since 0.22
*/
public String getUserAgent()
{
return useragent;
}
/**
* Enables/disables GZip compression for GET requests. Default: true.
* @param zipped whether we use GZip compression
* @since 0.23
*/
public void setUsingCompressedRequests(boolean zipped)
{
this.zipped = zipped;
}
/**
* Checks whether we are using GZip compression for GET requests.
* Default: true.
* @return (see above)
* @since 0.23
*/
public boolean isUsingCompressedRequests()
{
return zipped;
}
/**
* Checks whether API action=query dependencies automatically resolve
* redirects (default = false).
* @return (see above)
* @since 0.27
*/
public boolean isResolvingRedirects()
{
return resolveredirect;
}
/**
* Sets whether API action=query dependencies automatically resolve
* redirects (default = false).
* @param b (see above)
* @since 0.27
*/
public void setResolveRedirects(boolean b)
{
resolveredirect = b;
if (b)
defaultApiParams.put("redirects", "1");
else
defaultApiParams.remove("redirects");
}
/**
* Sets whether edits are marked as bot by default (may be overridden).
* Default = false. Works only if one has the required permissions.
* @param markbot (see above)
* @since 0.26
*/
public void setMarkBot(boolean markbot)
{
this.markbot = markbot;
}
/**
* Are edits are marked as bot by default?
* @return whether edits are marked as bot by default
* @since 0.26
*/
public boolean isMarkBot()
{
return markbot;
}
/**
* Sets whether edits are marked as minor by default (may be overridden).
* Default = false.
* @param minor (see above)
* @since 0.26
*/
public void setMarkMinor(boolean minor)
{
this.markminor = minor;
}
/**
* Are edits are marked as minor by default?
* @return whether edits are marked as minor by default
* @since 0.26
*/
public boolean isMarkMinor()
{
return markminor;
}
/**
* Returns the maximum number of results returned when querying the API.
* Default = Integer.MAX_VALUE
* @return see above
* @since 0.34
*/
public int getQueryLimit()
{
return querylimit;
}
/**
* Sets the maximum number of results returned when querying the API.
* Useful for operating in constrained environments (e.g. web servers)
* or queries for which results are sorted by relevance (e.g. search).
*
* @param limit the desired maximum number of results to retrieve
* @throws IllegalArgumentException if <var>limit</var> is not a positive
* integer
* @since 0.34
*/
public void setQueryLimit(int limit)
{
if (limit < 1)
throw new IllegalArgumentException("Query limit must be a positive integer.");
querylimit = limit;
}
/**
* Returns a string representation of this Wiki.
* @return a string representation of this Wiki.
* @since 0.10
*/
@Override
public String toString()
{
// domain
StringBuilder buffer = new StringBuilder("Wiki[url=");
buffer.append(protocol);
buffer.append(domain);
buffer.append(scriptPath);
// user
buffer.append(",user=");
buffer.append(user != null ? user.toString() : "null");
buffer.append(",");
// throttle mechanisms
buffer.append("throttle=");
buffer.append(throttle);
buffer.append(",maxlag=");
buffer.append(maxlag);
buffer.append(",assertionMode=");
buffer.append(assertion);
buffer.append(",statusCheckInterval=");
buffer.append(statusinterval);
buffer.append(",cookies=");
buffer.append(cookies);
buffer.append("]");
return buffer.toString();
}
/**
* Gets the maxlag parameter.
* @return the current maxlag, in seconds
* @see #setMaxLag
* @see #getCurrentDatabaseLag
* @see <a href="https://mediawiki.org/wiki/Manual:Maxlag_parameter">
* MediaWiki documentation</a>
* @since 0.11
*/
public int getMaxLag()
{
return maxlag;
}
/**
* Sets the maxlag parameter. A value of less than 0s disables this
* mechanism. Default is 5s.
* @param lag the desired maxlag in seconds
* @see #getMaxLag
* @see #getCurrentDatabaseLag
* @see <a href="https://mediawiki.org/wiki/Manual:Maxlag_parameter">
* MediaWiki documentation</a>
* @since 0.11
*/
public void setMaxLag(int lag)
{
maxlag = lag;
log(Level.CONFIG, "setMaxLag", "Setting maximum allowable database lag to " + lag);
if (maxlag >= 0)
defaultApiParams.put("maxlag", String.valueOf(maxlag));
else
defaultApiParams.remove("maxlag");
}
/**
* Gets the assertion mode. Assertion modes are bitmasks.
* @return the current assertion mode
* @see #setAssertionMode
* @since 0.11
*/
public int getAssertionMode()
{
return assertion;
}
/**
* Sets the assertion mode. Do this AFTER logging in, otherwise the login
* will fail. Assertion modes are bitmasks. Default is {@link #ASSERT_NONE}.
* @param mode an assertion mode
* @see #getAssertionMode
* @since 0.11
*/
public void setAssertionMode(int mode)
{
assertion = mode;
log(Level.CONFIG, "setAssertionMode", "Set assertion mode to " + mode);
if ((assertion & ASSERT_BOT) == ASSERT_BOT)
defaultApiParams.put("assert", "bot");
else if ((assertion & ASSERT_USER) == ASSERT_USER)
defaultApiParams.put("assert", "user");
else
defaultApiParams.remove("assert");
}
/**
* Gets the number of actions (edit, move, block, delete, etc) between
* status checks. A status check is where we update user rights, block
* status and check for new messages (if the appropriate assertion mode
* is set).
*
* @return the number of edits between status checks
* @see #setStatusCheckInterval
* @since 0.18
*/
public int getStatusCheckInterval()
{
return statusinterval;
}
/**
* Sets the number of actions (edit, move, block, delete, etc) between
* status checks. A status check is where we update user rights, block
* status and check for new messages (if the appropriate assertion mode
* is set). Default is 100.
*
* @param interval the number of edits between status checks
* @see #getStatusCheckInterval
* @since 0.18
*/
public void setStatusCheckInterval(int interval)
{
statusinterval = interval;
log(Level.CONFIG, "setStatusCheckInterval", "Status check interval set to " + interval);
}
/**
* Set the logging level used by the internal logger.
* @param loglevel one of the levels specified in java.util.logging.LEVEL
* @since 0.31
*/
public void setLogLevel(Level loglevel)
{
this.loglevel = loglevel;
logger.setLevel(loglevel);
}
// META STUFF
/**
* Logs in to the wiki. This method is thread-safe.
*
* @param username a username
* @param password a password, as a {@code char[]} for security
* reasons. Overwritten once the password is used.
* @throws IOException if a network error occurs
* @throws FailedLoginException if the login failed due to an incorrect
* username or password, the requirement for an interactive login (not
* supported, use [[Special:BotPasswords]]) or some other reason
* @see #logout
* @see <a href="https://mediawiki.org/wiki/API:Login">MediaWiki
* documentation</a>
*/
public synchronized void login(String username, char[] password) throws IOException, FailedLoginException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "login");
Map<String, Object> postparams = new HashMap<>();
postparams.put("lgname", username);
postparams.put("lgpassword", new String(password));
postparams.put("lgtoken", getToken("login"));
String line = makeApiCall(getparams, postparams, "login");
Arrays.fill(password, '0');
// check for success
if (line.contains("result=\"Success\""))
{
user = getUser(parseAttribute(line, "lgusername", 0));
boolean apihighlimit = user.isAllowedTo("apihighlimits");
if (apihighlimit)
{
max = 5000;
slowmax = 500;
}
log(Level.INFO, "login", "Successfully logged in as " + username + ", highLimit = " + apihighlimit);
}
else if (line.contains("result=\"Failed\""))
throw new FailedLoginException("Login failed: " + parseAttribute(line, "reason", 0));
// interactive login or bot password required
else if (line.contains("result=\"Aborted\""))
throw new FailedLoginException("Login failed: you need to use a bot password, see [[Special:Botpasswords]].");
else
throw new AssertionError("Unreachable!");
}
/**
* Logs in to the wiki. This method is thread-safe.
*
* @param username a username
* @param password a string with the password
* @throws IOException if a network error occurs
* @throws FailedLoginException if the login failed due to an incorrect
* username or password, the requirement for an interactive login (not
* supported, use [[Special:Botpasswords]]) or some other reason
* @see #logout
*/
public synchronized void login(String username, String password) throws IOException, FailedLoginException
{
login(username, password.toCharArray());
}
/**
* Logs out of the wiki. This method is thread safe (so that we don't log
* out during an edit). All operations are conducted offline, so you can
* serialize this Wiki first.
* @see #login
* @see #logoutServerSide
*/
public synchronized void logout()
{
cookies.getCookieStore().removeAll();
user = null;
max = 500;
slowmax = 50;
log(Level.INFO, "logout", "Logged out");
}
/**
* Logs out of the wiki and destroys the session on the server. You will
* need to log in again instead of just reading in a serialized wiki.
* Equivalent to [[Special:Userlogout]]. This method is thread safe
* (so that we don't log out during an edit). WARNING: kills all
* concurrent sessions - if you are logged in with a browser this will log
* you out there as well.
*
* @throws IOException if a network error occurs
* @since 0.14
* @see #login
* @see #logout
* @see <a href="https://mediawiki.org/wiki/API:Logout">MediaWiki
* documentation</a>
*/
public synchronized void logoutServerSide() throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "logout");
makeApiCall(getparams, null, "logoutServerSide");
logout(); // destroy local cookies
}
/**
* Determines whether the current user has new messages. (A human would
* notice a yellow bar at the top of the page).
* @return whether the user has new messages
* @throws IOException if a network error occurs
* @since 0.11
*/
public boolean hasNewMessages() throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("meta", "userinfo");
getparams.put("uiprop", "hasmsg");
return makeApiCall(getparams, null, "hasNewMessages").contains("messages=\"\"");
}
/**
* Determines the current database replication lag.
* @return the current database replication lag
* @throws IOException if a network error occurs
* @see #setMaxLag
* @see #getMaxLag
* @see <a href="https://mediawiki.org/wiki/Manual:Maxlag_parameter">
* MediaWiki documentation</a>
* @since 0.10
*/
public int getCurrentDatabaseLag() throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("meta", "siteinfo");
getparams.put("siprop", "dbrepllag");
String line = makeApiCall(getparams, null, "getCurrentDatabaseLag");
String lag = parseAttribute(line, "lag", 0);
log(Level.INFO, "getCurrentDatabaseLag", "Current database replication lag is " + lag + " seconds");
return Integer.parseInt(lag);
}
/**
* Fetches some site statistics, namely the number of articles, pages,
* files, edits, users and admins. Equivalent to [[Special:Statistics]].
*
* @return a map containing the stats. Use "articles", "pages", "files"
* "edits", "users", "activeusers", "admins" or "jobs" to retrieve the
* respective value
* @throws IOException if a network error occurs
* @since 0.14
*/
public Map<String, Integer> getSiteStatistics() throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("meta", "siteinfo");
getparams.put("siprop", "statistics");
String text = makeApiCall(getparams, null, "getSiteStatistics");
Map<String, Integer> ret = new HashMap<>(20);
ret.put("pages", Integer.parseInt(parseAttribute(text, "pages", 0)));
ret.put("articles", Integer.parseInt(parseAttribute(text, "articles", 0)));
ret.put("files", Integer.parseInt(parseAttribute(text, "images", 0)));
ret.put("users", Integer.parseInt(parseAttribute(text, "users", 0)));
ret.put("activeusers", Integer.parseInt(parseAttribute(text, "activeusers", 0)));
ret.put("admins", Integer.parseInt(parseAttribute(text, "admins", 0)));
ret.put("jobs", Integer.parseInt(parseAttribute(text, "jobs", 0))); // job queue length
return ret;
}
/**
* Renders the specified wiki markup as HTML by passing it to the MediaWiki
* parser through the API.
*
* @param markup the markup to parse
* @return the parsed markup as HTML
* @throws IOException if a network error occurs
* @since 0.13
*/
public String parse(String markup) throws IOException
{
Map<String, Object> content = new HashMap<>();
content.put("text", markup);
return parse(content, -1, false);
}
/**
* Parses wikitext, revisions or pages. Deleted pages and revisions to
* deleted pages are not allowed if you don't have the rights to view them.
*
* <p>
* The returned HTML does not include "edit" links. Hyperlinks are
* rewritten from useless relative links to other wiki pages to full URLs.
* References to resources using protocol relative URLs are rewritten to
* use {@linkplain #getProtocol() this wiki's protocol}.
*
* <p>
* <b>Warnings</b>:
* <ul>
* <li>The parameters to this method will be changed when the time comes
* for JDK11 refactoring to accept Map.Entry instead. I also haven't
* decided how many more boolean parameters to add, and what format
* they will take.
* </ul>
*
* @param content a Map following the same scheme as specified by
* {@link #diff(Map, int, Map, int)}
* @param section parse only this section (optional, use -1 to skip)
* @param nolimitreport do not include the HTML comment detailing limits
* @return the parsed wikitext
* @throws NoSuchElementException or IllegalArgumentException if no content
* was supplied for parsing
* @throws SecurityException if you pass a RevisionDeleted revision and
* lack the necessary privileges
* @throws IOException if a network error occurs
* @see #parse(String)
* @see #getRenderedText(String)
* @see Wiki.Revision#getRenderedText()
* @see <a href="https://mediawiki.org/wiki/API:Parsing_wikitext">MediaWiki
* documentation</a>
* @since 0.35
*/
public String parse(Map<String, Object> content, int section, boolean nolimitreport) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "parse");
getparams.put("prop", "text");
if (nolimitreport)
getparams.put("disablelimitreport", "1");
getparams.put("disableeditsection", "1");
Map<String, Object> postparams = new HashMap<>();
Map.Entry<String, Object> entry = content.entrySet().iterator().next();
Object value = entry.getValue();
switch (entry.getKey())
{
case "title":
getparams.put("page", normalize((String)value));
break;
case "revid":
getparams.put("oldid", value.toString());
break;
case "revision":
getparams.put("oldid", String.valueOf(((Revision)value).getID()));
break;
case "text":
postparams.put("text", value);
break;
default:
throw new IllegalArgumentException("No content was specified to parse!");
}
if (section >= 0)
getparams.put("section", String.valueOf(section));
String response = makeApiCall(getparams, postparams, "parse");
if (response.contains("error code=\""))
// Bad section numbers, revids, deleted pages should all end up here.
// FIXME: makeHTTPRequest() swallows the API error "missingtitle"
// (deleted pages) to throw an UnknownError instead.
return null;
int y = response.indexOf('>', response.indexOf("<text")) + 1;
int z = response.indexOf("</text>");
// Rewrite URLs to replace useless relative links and make images work on
// locally saved copies of wiki pages.
String html = decode(response.substring(y, z));
html = html.replace("href=\"/wiki", "href=\"" + protocol + domain + "/wiki");
html = html.replace(" src=\"//", " src=\"" + protocol); // a little fragile for my liking, but will do
return html;
}
/**
* Same as {@link #parse(String)}, but also strips out unwanted
* crap. This might be useful to subclasses.
*
* @param in the string to parse
* @return that string without the crap
* @throws IOException if a network error occurs
* @since 0.14
* @deprecated parse now has a parameter that disables the parser report
*/
@Deprecated
protected String parseAndCleanup(String in) throws IOException
{
String output = parse(in);
output = output.replace("<p>", "").replace("</p>", ""); // remove paragraph tags
output = output.replace("\n", ""); // remove new lines
// strip out the parser report, which comes at the end
int a = output.indexOf("<!--");
return output.substring(0, a);
}
/**
* Fetches a random page in the specified namespaces. Equivalent to
* [[Special:Random]].
*
* @param ns the namespaces to fetch random pages from. If not present,
* fetch pages from {@link #MAIN_NAMESPACE}. Allows {@link #ALL_NAMESPACES}
* for obvious effect. Invalid namespaces, {@link #SPECIAL_NAMESPACE} and
* {@link #MEDIA_NAMESPACE} are ignored; if only these namespaces are
* provided, this is equivalent to {@link #ALL_NAMESPACES}.
* @return the title of the page
* @throws IOException if a network error occurs
* @since 0.13
* @see <a href="https://mediawiki.org/wiki/API:Random">MediaWiki
* documentation</a>
*/
public String random(int... ns) throws IOException
{
if (ns.length == 0)
ns = new int[] { MAIN_NAMESPACE };
// no bulk queries here because they are deterministic
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("list", "random");
if (ns[0] != ALL_NAMESPACES)
getparams.put("rnnamespace", constructNamespaceString(ns));
String line = makeApiCall(getparams, null, "random");
return parseAttribute(line, "title", 0);
}
/**
* Fetches edit and other types of tokens.
* @param type one of "csrf", "patrol", "rollback", "userrights", "watch"
* or "login"
* @return the token
* @throws IOException if a network error occurs
* @since 0.32
*/
public String getToken(String type) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("meta", "tokens");
getparams.put("type", type);
String content = makeApiCall(getparams, null, "getToken");
return parseAttribute(content, type + "token", 0);
}
// PAGE METHODS
/**
* Returns the corresponding talk page to this page.
*
* @param title the page title
* @return the name of the talk page corresponding to <var>title</var>
* or "" if we cannot recognise it
* @throws IllegalArgumentException if given title is in a talk namespace
* or we try to retrieve the talk page of a Special: or Media: page.
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @since 0.10
*/
public String getTalkPage(String title)
{
// It is convention that talk namespaces are the original namespace + 1
// and are odd integers.
int namespace = namespace(title);
if (namespace % 2 == 1)
throw new IllegalArgumentException("Cannot fetch talk page of a talk page!");
if (namespace < 0)
throw new IllegalArgumentException("Special: and Media: pages do not have talk pages!");
if (namespace != MAIN_NAMESPACE) // remove the namespace
title = title.substring(title.indexOf(':') + 1);
return namespaceIdentifier(namespace + 1) + ":" + title;
}
/**
* If a namespace supports subpages, return the top-most page --
* {@code getRootPage("Talk:Aaa/Bbb/Ccc")} returns "Talk:Aaa" if the talk
* namespace supports subpages, "Talk:Aaa/Bbb/Ccc" if it doesn't. See
* also the <a href="https://mediawiki.org/wiki/Help:Magic_words">magic word</a>
* <kbd>{{ROOTPAGENAME}}</kbd>, though that removes the namespace prefix.
*
* @param page a page
* @return (see above)
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @since 0.33
*/
public String getRootPage(String page)
{
if (supportsSubpages(namespace(page)) && page.contains("/"))
return page.substring(0, page.indexOf('/'));
else
return page;
}
/**
* If a namespace supports subpages, return the top-most page --
* {@code getParentPage("Talk:Aaa/Bbb/Ccc")} returns "Talk:Aaa/Bbb" if
* the talk namespace supports subpages, "Talk:Aaa/Bbb/Ccc" if it doesn't.
* See also the <a href="https://mediawiki.org/wiki/Help:Magic_words">magic
* word</a> <kbd>{{BASEPAGENAME}}</kbd>, though that removes the namespace
* prefix.
*
* @param page a page
* @return (see above)
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @since 0.33
*/
public String getParentPage(String page)
{
if (supportsSubpages(namespace(page)) && page.contains("/"))
return page.substring(0, page.lastIndexOf('/'));
else
return page;
}
/**
* Returns a URL to the human readable version of <var>page</var>. Example:
* https://en.wikipedia.org/wiki/Create_a_page
* @param page a title
* @return (see above)
* @since 0.35
*/
public String getPageUrl(String page)
{
try
{
page = normalize(page).replace(' ', '_');
return articleUrl + URLEncoder.encode(page, "UTF-8");
}
catch (IOException ex)
{
throw new UncheckedIOException(ex); // seriously?
}
}
/**
* Removes the namespace identifier from a page title. Equivalent to the
* <a href="https://mediawiki.org/wiki/Help:Magic_words">magic word</a>
* <kbd>{{PAGENAME}}</kbd>.
*
* @param page a page
* @return (see above)
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @see #namespace(String)
* @see #namespaceIdentifier(int)
* @since 0.35
*/
public String removeNamespace(String page)
{
if (namespace(page) == 0)
return page;
return page.substring(page.indexOf(':') + 1);
}
/**
* Gets miscellaneous page info.
* @param page the page to get info for
* @return see {@link #getPageInfo(String[]) }
* @throws IOException if a network error occurs
* @since 0.28
*/
public Map<String, Object> getPageInfo(String page) throws IOException
{
return getPageInfo(new String[] { page })[0];
}
/**
* Gets miscellaneous page info. Returns:
* <ul>
* <li><b>inputpagename</b>: (String) the page name supplied to this method
* <li><b>pagename</b>: (String) the normalized page name
* <li><b>displaytitle</b>: (String) the title of the page that is actually
* displayed. Example: "iPod"
* <li><b>protection</b>: (Map) the {@link #protect(String, Map, String)
* protection state} of the page. Does not cover implied protection
* levels (e.g. MediaWiki namespace).
* <li><b>exists</b>: (Boolean) whether the page exists
* <li><b>lastpurged</b>: (OffsetDateTime) when the page was last purged or
* <code>null</code> if the page does not exist
* <li><b>lastrevid</b>: (Long) the revid of the top revision or -1L if the
* page does not exist
* <li><b>size</b>: (Integer) the size of the page or -1 if the page does
* not exist
* <li><b>pageid</b>: (Long) the id of the page or -1 if the page does not
* exist
* <li><b>timestamp</b>: (OffsetDateTime) when this method was called
* <li><b>watchers</b>: (Integer) number of watchers, may be restricted
* </ul>
*
* <p>Note: <code>intestactions=X</code> is deliberately not implemented
* because it lowers the number of pages per network request by N, where N
* is the number of actions tested. Furthermore, it doesn't give the reason
* why if any given action is disallowed.
*
* @param pages the pages to get info for.
* @return (see above), or {@code null} for Special and Media pages.
* The Maps will come out in the same order as the processed array.
* @throws IOException if a network error occurs
* @since 0.23
*/
public Map<String, Object>[] getPageInfo(String[] pages) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("prop", "info");
getparams.put("inprop", "protection|displaytitle|watchers");
Map<String, Object> postparams = new HashMap<>();
Map<String, Map<String, Object>> metamap = new HashMap<>();
// copy because redirect resolver overwrites
String[] pages2 = Arrays.copyOf(pages, pages.length);
for (String temp : constructTitleString(pages))
{
postparams.put("titles", temp);
String line = makeApiCall(getparams, postparams, "getPageInfo");
if (resolveredirect)
resolveRedirectParser(pages2, line);
// form: <page pageid="239098" ns="0" title="BitTorrent" ... >
// <protection />
// </page>
for (int j = line.indexOf("<page "); j > 0; j = line.indexOf("<page ", ++j))
{
int x = line.indexOf("</page>", j);
String item = line.substring(j, x);
Map<String, Object> tempmap = new HashMap<>(15);
// does the page exist?
String parsedtitle = parseAttribute(item, "title", 0);
tempmap.put("pagename", parsedtitle);
boolean exists = !item.contains("missing=\"\"");
tempmap.put("exists", exists);
if (exists)
{
tempmap.put("lastpurged", OffsetDateTime.parse(parseAttribute(item, "touched", 0)));
tempmap.put("lastrevid", Long.parseLong(parseAttribute(item, "lastrevid", 0)));
tempmap.put("size", Integer.parseInt(parseAttribute(item, "length", 0)));
tempmap.put("pageid", Long.parseLong(parseAttribute(item, "pageid", 0)));
}
else
{
tempmap.put("lastedited", null);
tempmap.put("lastrevid", -1L);
tempmap.put("size", -1);
tempmap.put("pageid", -1);
}
// parse protection level
// expected form: <pr type="edit" level="sysop" expiry="infinity" cascade="" />
Map<String, Object> protectionstate = new HashMap<>();
for (int z = item.indexOf("<pr "); z > 0; z = item.indexOf("<pr ", ++z))
{
String type = parseAttribute(item, "type", z);
String level = parseAttribute(item, "level", z);
protectionstate.put(type, level);
//if (level != NO_PROTECTION)
String expiry = parseAttribute(item, "expiry", z);
if (expiry.equals("infinity"))
protectionstate.put(type + "expiry", null);
else
protectionstate.put(type + "expiry", OffsetDateTime.parse(expiry));
// protected via cascade
if (item.contains("source=\""))
protectionstate.put("cascadesource", parseAttribute(item, "source", z));
}
// MediaWiki namespace
if (namespace(parsedtitle) == MEDIAWIKI_NAMESPACE)
{
protectionstate.put("edit", FULL_PROTECTION);
protectionstate.put("move", FULL_PROTECTION);
if (!exists)
protectionstate.put("create", FULL_PROTECTION);
}
protectionstate.put("cascade", item.contains("cascade=\"\""));
tempmap.put("protection", protectionstate);
tempmap.put("displaytitle", parseAttribute(item, "displaytitle", 0));
tempmap.put("timestamp", OffsetDateTime.now(timezone));
// number of watchers
if (item.contains("watchers=\""))
tempmap.put("watchers", Integer.parseInt(parseAttribute(item, "watchers", 0)));
metamap.put(parsedtitle, tempmap);
}
}
Map<String, Object>[] info = new HashMap[pages.length];
// Reorder. Make a new HashMap so that inputpagename remains unique.
for (int i = 0; i < pages2.length; i++)
{
Map<String, Object> tempmap = metamap.get(normalize(pages2[i]));
if (tempmap != null)
{
info[i] = new HashMap<>(tempmap);
info[i].put("inputpagename", pages[i]);
}
}
log(Level.INFO, "getPageInfo", "Successfully retrieved page info for " + Arrays.toString(pages));
return info;
}
/**
* Fills namespace cache.
* @throws UncheckedIOException if a network error occurs (unchecked for
* lambda friendliness, very rare since this should only be called once
* per session)
* @since 0.32
*/
private void ensureNamespaceCache()
{
try
{
if (namespaces == null)
getSiteInfo();
}
catch (IOException ex)
{
throw new UncheckedIOException(ex);
}
}
/**
* Returns the namespace a page is in. There is no need to override this to
* add custom namespaces, though you may want to define static fields e.g.
* {@code public static final int PORTAL_NAMESPACE = 100;} for the Portal
* namespace on the English Wikipedia.
*
* @param title any valid page name
* @return an integer representing the namespace of <var>title</var>
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @see #namespaceIdentifier(int)
* @since 0.03
*/
public int namespace(String title)
{
ensureNamespaceCache();
// perform a limited normalization
if (title.startsWith(":"))
title = title.substring(1);
if (!title.contains(":"))
return MAIN_NAMESPACE;
title = title.replace("_", " ");
String namespace = title.substring(0, 1).toUpperCase(locale) + title.substring(1, title.indexOf(':'));
return namespaces.getOrDefault(namespace, MAIN_NAMESPACE);
}
/**
* For a given namespace denoted as an integer, fetch the corresponding
* identification string e.g. {@code namespaceIdentifier(1)} should
* return "Talk" on en.wp. (This does the exact opposite to {@link
* #namespace(String)}). Strings returned are always localized.
*
* @param namespace an integer corresponding to a namespace. If it does not
* correspond to a namespace, we assume you mean the main namespace (i.e.
* return "").
* @return the identifier of the namespace
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @see #namespace(String)
* @since 0.25
*/
public String namespaceIdentifier(int namespace)
{
ensureNamespaceCache();
// anything we cannot identify is assumed to be in the main namespace
if (!namespaces.containsValue(namespace))
return "";
for (Map.Entry<String, Integer> entry : namespaces.entrySet())
if (entry.getValue().equals(namespace))
return entry.getKey();
throw new AssertionError("Unreachable.");
}
/**
* Gets the namespaces used by this wiki.
* @return a map containing e.g. {"Media" : -2, "Special" : -1, ...}.
* Changes in this map do not propagate back to this Wiki object.
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @since 0.28
*/
public LinkedHashMap<String, Integer> getNamespaces()
{
ensureNamespaceCache();
return new LinkedHashMap<>(namespaces);
}
/**
* Returns true if the given namespace allows subpages.
* @param ns a namespace number
* @return (see above)
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @throws IllegalArgumentException if the give namespace does not exist
* @since 0.33
*/
public boolean supportsSubpages(int ns)
{
ensureNamespaceCache();
if (namespaces.containsValue(ns))
return ns_subpages.contains(ns);
throw new IllegalArgumentException("Invalid namespace " + ns);
}
/**
* Determines whether a series of pages exist.
* @param titles the titles to check.
* @return whether the pages exist, in the same order as the processed array
* @throws IOException or UncheckedIOException if a network error occurs
* @since 0.10
*/
public boolean[] exists(String[] titles) throws IOException
{
boolean[] ret = new boolean[titles.length];
Map<String, Object>[] info = getPageInfo(titles);
for (int i = 0; i < titles.length; i++)
ret[i] = (Boolean)info[i].get("exists");
return ret;
}
/**
* Gets the raw wikicode for a page. WARNING: does not support special
* pages. Check [[User talk:MER-C/Wiki.java#Special page equivalents]]
* for fetching the contents of special pages. Use {@link #getImage(String,
* File)} to fetch an image.
*
* @param title the title of the page.
* @return the raw wikicode of a page, or {@code null} if the page doesn't exist
* @throws UnsupportedOperationException if you try to retrieve the text of
* a Special: or Media: page
* @throws IOException or UncheckedIOException if a network error occurs
* @see #edit
*/
public String getPageText(String title) throws IOException
{
return getPageText(new String[] { title })[0];
}
/**
* Gets the raw wikicode for a set of pages. WARNING: does not support
* special pages. Check [[User talk:MER-C/Wiki.java#Special page equivalents]]
* for fetching the contents of special pages. Use {@link #getImage(String,
* File)} to fetch an image. If a page doesn't exist, the corresponding
* return value is {@code null}.
*
* @param titles a list of titles
* @return the raw wikicode of those titles, in the same order as the input
* array
* @throws UnsupportedOperationException if you try to retrieve the text of
* a Special: or Media: page
* @throws IOException or UncheckedIOException if a network error occurs
* @since 0.32
* @see #edit
*/
public String[] getPageText(String[] titles) throws IOException
{
return getText(titles, null, -1);
}
/**
* Gets the wikitext of a list of titles or revisions. If a page or
* revision doesn't exist or is deleted, return {@code null}.
* RevisionDeleted revisions are not allowed.
*
* @param titles a list of titles (use null to skip, overrides revids)
* @param revids a list of revids (use null to skip)
* @param section a section number. This section number must exist in all
* titles or revids otherwise you will get vast swathes of your results
* being erroneously null. Optional, use -1 to skip.
* @return the raw wikicode of those titles, in the same order as the input
* array
* @throws IOException or UncheckedIOException if a network error occurs
* @since 0.35
*/
public String[] getText(String[] titles, long[] revids, int section) throws IOException
{
// determine what type of request we have. Cannot mix the two.
// FIXME: XML bleeding to return results for lists of pages
boolean isrevisions;
int count = 0;
String[] titles2 = null;
if (titles != null)
{
// validate titles
for (String title : titles)
if (namespace(title) < 0)
throw new UnsupportedOperationException("Cannot retrieve \"" + title + "\": namespace < 0.");
isrevisions = false;
count = titles.length;
// copy because redirect resolver overwrites
titles2 = Arrays.copyOf(titles, count);
}
else if (revids != null)
{
isrevisions = true;
count = revids.length;
}
else
throw new IllegalArgumentException("Either titles or revids must be specified!");
if (count == 0)
return new String[0];
Map<String, String> pageTexts = new HashMap<>(2 * count);
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("prop", "revisions");
getparams.put("rvprop", "ids|content");
if (section >= 0)
getparams.put("rvsection", String.valueOf(section));
Map<String, Object> postparams = new HashMap<>();
List<String> chunks = isrevisions ? constructRevisionString(revids) : constructTitleString(titles);
for (String chunk : chunks)
{
postparams.put(isrevisions ? "revids" : "titles", chunk);
String temp = makeApiCall(getparams, postparams, "getText");
String[] results = temp.split(isrevisions ? "<rev " : "<page ");
if (!isrevisions && resolveredirect)
resolveRedirectParser(titles2, results[0]);
// skip first element to remove front crud
for (int i = 1; i < results.length; i++)
{
// determine existance, then locate and extract content
String key = parseAttribute(results[i], isrevisions ? "revid" : "title", 0);
if (!results[i].contains("missing=\"\"") && !results[i].contains("texthidden=\"\""))
{
int x = results[i].indexOf("<rev ", i);
int y = results[i].indexOf('>', x) + 1;
// this </rev> tag is not present for empty pages/revisions
int z = results[i].indexOf("</rev>", y);
// store result for later
String text = (z < 0) ? "" : decode(results[i].substring(y, z));
pageTexts.put(key, text);
}
}
}
// returned array is in the same order as input array
String[] ret = new String[count];
for (int i = 0; i < count; i++)
{
String key = isrevisions ? String.valueOf(revids[i]) : normalize(titles2[i]);
ret[i] = pageTexts.get(key);
}
log(Level.INFO, "getPageText", "Successfully retrieved text of " + count + (isrevisions ? " revisions." : " pages."));
return ret;
}
/**
* Gets the text of a specific section. Useful for section editing.
* @param title the title of the relevant page
* @param section the section number of the section to retrieve text for
* @return the text of the given section, or {@code null} if the page doesn't
* have that many sections
* @throws IOException if a network error occurs
* @throws IllegalArgumentException if {@code section < 0}
* @since 0.24
*/
public String getSectionText(String title, int section) throws IOException
{
if (section < 0)
throw new IllegalArgumentException("Section numbers must be positive!");
return getText(new String[] { title }, null, section)[0];
}
/**
* Gets the contents of a page, rendered in HTML (as opposed to
* wikitext). WARNING: only supports special pages in certain
* circumstances, for example {@code getRenderedText("Special:Recentchanges")}
* returns the 50 most recent change to the wiki in pretty-print HTML. You
* should test any use of this method on-wiki through the text
* <kbd>{{Special:Specialpage}}</kbd>. Use {@link #getImage(String, File)}
* to fetch an image. Be aware of any transclusion limits, as outlined at
* [[Wikipedia:Template limits]].
*
* @param title the title of the page
* @return the rendered contents of that page
* @throws IOException if a network error occurs
* @since 0.10
*/
public String getRenderedText(String title) throws IOException
{
Map<String, Object> content = new HashMap<>();
if (namespace(title) == SPECIAL_NAMESPACE)
// not guaranteed to succeed...
content.put("text", "{{:" + title + "}}");
else
content.put("title", title);
return parse(content, -1, false);
}
/**
* Edits a page by setting its text to the supplied value. This method is
* {@linkplain #setThrottle(int) throttled}. The edit will be marked bot if
* {@link #isMarkBot()} is {@code true} and minor if {@link #isMarkMinor()}
* is {@code true}.
*
* @param text the text of the page
* @param title the title of the page
* @param summary the edit summary. See [[Help:Edit summary]]. Summaries
* longer than 200 characters are truncated server-side.
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialException if page is protected and we can't edit it
* @throws UnsupportedOperationException if you try to edit a Special: or a
* Media: page
* @throws ConcurrentModificationException if an edit conflict occurs
* @see #getPageText
*/
public void edit(String title, String text, String summary) throws IOException, LoginException
{
edit(title, text, summary, markminor, markbot, -2, null);
}
/**
* Edits a page by setting its <var>text</var> to the supplied value. This
* method is {@linkplain #setThrottle(int) throttled}. The edit will be
* marked bot if {@link #isMarkBot()} is {@code true} and minor if {@link
* #isMarkMinor()} is {@code true}.
*
* @param text the text of the page
* @param title the title of the page
* @param summary the edit summary. See [[Help:Edit summary]]. Summaries
* longer than 200 characters are truncated server-side.
* @param basetime the timestamp of the revision on which <var>text</var> is
* based, used to check for edit conflicts. {@code null} disables this.
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialException if page is protected and we can't edit it
* @throws UnsupportedOperationException if you try to edit a Special: or a
* Media: page
* @throws ConcurrentModificationException if an edit conflict occurs
* @see #getPageText
*/
public void edit(String title, String text, String summary, OffsetDateTime basetime) throws IOException, LoginException
{
edit(title, text, summary, markminor, markbot, -2, basetime);
}
/**
* Edits a section by setting its <var>text</var> to the supplied value.
* This method is {@linkplain #setThrottle(int) throttled}. The edit will
* be marked bot if {@link #isMarkBot()} is {@code true} and minor if
* {@link #isMarkMinor()} is {@code true}.
*
* @param text the text of the page
* @param title the title of the page
* @param summary the edit summary. See [[Help:Edit summary]]. Summaries
* longer than 200 characters are truncated server-side.
* @param section the section to edit. Use -1 to specify a new section and
* -2 to disable section editing.
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialException if page is protected and we can't edit it
* @throws UnsupportedOperationException if you try to edit a Special: or a
* Media: page
* @throws ConcurrentModificationException if an edit conflict occurs
* @see #getPageText
* @since 0.25
*/
public void edit(String title, String text, String summary, int section) throws IOException, LoginException
{
edit(title, text, summary, markminor, markbot, section, null);
}
/**
* Edits a page by setting its <var>text</var> to the supplied value. This
* method is {@linkplain #setThrottle(int) throttled}. The edit will be
* marked bot if {@link #isMarkBot()} is {@code true} and minor if {@link
* #isMarkMinor()} is {@code true}.
*
* @param text the text of the page
* @param title the title of the page
* @param summary the edit summary. See [[Help:Edit summary]]. Summaries
* longer than 200 characters are truncated server-side.
* @param section the section to edit. Use -1 to specify a new section and
* -2 to disable section editing.
* @param basetime the timestamp of the revision on which <var>text</var> is
* based, used to check for edit conflicts. {@code null} disables this.
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialException if page is protected and we can't edit it
* @throws UnsupportedOperationException if you try to edit a Special: or a
* Media: page
* @throws ConcurrentModificationException if an edit conflict occurs
* @see #getPageText
* @since 0.25
*/
public void edit(String title, String text, String summary, int section, OffsetDateTime basetime)
throws IOException, LoginException
{
edit(title, text, summary, markminor, markbot, section, basetime);
}
/**
* Edits a page by setting its text to the supplied value. This method is
* {@linkplain #setThrottle(int) throttled}.
*
* @param text the text of the page
* @param title the title of the page
* @param summary the edit summary or the title of the new section. See
* [[Help:Edit summary]]. Summaries longer than 255 characters are
* truncated server-side.
* @param minor whether the edit should be marked as minor. See
* [[Help:Minor edit]]. Overrides {@link #isMarkMinor()}.
* @param bot whether to mark the edit as a bot edit. Ignored if one does
* not have the necessary permissions. Overrides {@link #isMarkBot()}.
* @param section the section to edit. Use -1 to specify a new section and
* -2 to disable section editing.
* @param basetime the timestamp of the revision on which <var>text</var> is
* based, used to check for edit conflicts. {@code null} disables this.
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialExpiredException if cookies have expired
* @throws CredentialException if page is protected and we can't edit it
* @throws UnsupportedOperationException if you try to edit a Special: or
* Media: page
* @throws ConcurrentModificationException if an edit conflict occurs
* @see #getPageText
* @since 0.17
*/
public synchronized void edit(String title, String text, String summary, boolean minor, boolean bot,
int section, OffsetDateTime basetime) throws IOException, LoginException
{
// @revised 0.16 to use API edit. No more screenscraping - yay!
// @revised 0.17 section editing
// @revised 0.25 optional bot flagging
throttle();
// protection
Map<String, Object> info = getPageInfo(title);
if (!checkRights(info, "edit") || (Boolean)info.get("exists") && !checkRights(info, "create"))
{
CredentialException ex = new CredentialException("Permission denied: page is protected.");
log(Level.WARNING, "edit", "Cannot edit - permission denied. " + ex);
throw ex;
}
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "edit");
getparams.put("title", normalize(title));
// post data
Map<String, Object> postparams = new HashMap<>();
postparams.put("text", text);
// edit summary is created automatically if making a new section
if (section != -1)
postparams.put("summary", summary);
postparams.put("token", getToken("csrf"));
if (basetime != null)
{
postparams.put("starttimestamp", info.get("timestamp"));
// I wonder if the time getPageText() was called suffices here
postparams.put("basetimestamp", basetime);
}
if (minor)
postparams.put("minor", "1");
if (bot && user != null && user.isAllowedTo("bot"))
postparams.put("bot", "1");
if (section == -1)
{
postparams.put("section", "new");
postparams.put("sectiontitle", summary);
}
else if (section != -2)
postparams.put("section", section);
String response = makeApiCall(getparams, postparams, "edit");
// done
if (response.contains("error code=\"editconflict\""))
throw new ConcurrentModificationException("Edit conflict on " + title);
checkErrorsAndUpdateStatus(response, "edit");
log(Level.INFO, "edit", "Successfully edited " + title);
}
/**
* Creates a new section on the specified page. Leave <var>subject</var> as
* the empty string if you just want to append. This method is
* {@linkplain #setThrottle(int) throttled}.
*
* @param title the title of the page to edit
* @param subject the subject of the new section
* @param text the text of the new section
* @param minor whether the edit should be marked as minor. See
* [[Help:Minor edit]]. Overrides {@link #isMarkMinor()}.
* @param bot whether to mark the edit as a bot edit. Ignored if one does
* not have the necessary permissions. Overrides {@link #isMarkBot()}.
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialException if page is protected and we can't edit it
* @throws CredentialExpiredException if cookies have expired
* @throws UnsupportedOperationException if you try to edit a Special: or
* Media: page
* @since 0.17
*/
public void newSection(String title, String subject, String text, boolean minor, boolean bot) throws IOException, LoginException
{
edit(title, text, subject, minor, bot, -1, null);
}
/**
* Prepends something to the given page. A convenience method for
* adding maintenance templates, rather than getting and setting the
* page yourself. {@linkplain #setThrottle(int) throttled}.
*
* @param title the title of the page
* @param stuff what to prepend to the page
* @param summary the edit summary. See [[Help:Edit summary]]. Summaries
* longer than 200 characters are truncated server-side.
* @param minor whether the edit is minor. Overrides {@link #isMarkMinor()}.
* @param bot whether to mark the edit as a bot edit. Ignored if one does
* not have the necessary permissions. Overrides {@link #isMarkBot()}.
* @throws AccountLockedException if user is blocked
* @throws CredentialException if page is protected and we can't edit it
* @throws CredentialExpiredException if cookies have expired
* @throws UnsupportedOperationException if you try to retrieve the text
* of a Special: page or a Media: page
* @throws IOException if a network error occurs
*/
public void prepend(String title, String stuff, String summary, boolean minor, boolean bot) throws IOException, LoginException
{
StringBuilder text = new StringBuilder(100000);
text.append(stuff);
// section 0 to save bandwidth
text.append(getSectionText(title, 0));
edit(title, text.toString(), summary, minor, bot, 0, null);
}
/**
* Deletes a page. Does not delete any page with more than 5000 revisions.
* {@linkplain #setThrottle(int) throttled}.
* @param title the page to delete
* @param reason the reason for deletion
* @throws IOException or UncheckedIOException if a network error occurs
* @throws SecurityException if the user lacks the privileges to delete
* @throws CredentialExpiredException if cookies have expired
* @throws AccountLockedException if user is blocked
* @throws UnsupportedOperationException if <var>title</var> is a Special
* or Media page
* @since 0.24
*/
public synchronized void delete(String title, String reason) throws IOException, LoginException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Cannot delete Special and Media pages!");
if (user == null || !user.isAllowedTo("delete"))
throw new SecurityException("Cannot delete: Permission denied");
throttle();
// edit token
Map<String, Object> info = getPageInfo(title);
if (Boolean.FALSE.equals(info.get("exists")))
{
log(Level.INFO, "delete", "Page \"" + title + "\" does not exist.");
return;
}
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "delete");
getparams.put("title", normalize(title));
Map<String, Object> postparams = new HashMap<>();
postparams.put("reason", reason);
postparams.put("token", getToken("csrf"));
String response = makeApiCall(getparams, postparams, "delete");
// done
if (!response.contains("<delete title="))
checkErrorsAndUpdateStatus(response, "delete");
log(Level.INFO, "delete", "Successfully deleted " + title);
}
/**
* Undeletes a page. Equivalent to [[Special:Undelete]]. Restores ALL deleted
* revisions and files by default. This method is {@linkplain
* #setThrottle(int) throttled}.
*
* @param title a page to undelete
* @param reason the reason for undeletion
* @param revisions a list of revisions for selective undeletion
* @throws IOException or UncheckedIOException if a network error occurs
* @throws SecurityException if the user lacks the privileges to undelete
* @throws CredentialExpiredException if cookies have expired
* @throws AccountLockedException if user is blocked
* @throws UnsupportedOperationException if <var>title</var> is a Special
* or Media page
* @since 0.30
*/
public synchronized void undelete(String title, String reason, Revision... revisions) throws IOException, LoginException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Cannot delete Special and Media pages!");
if (user == null || !user.isAllowedTo("undelete"))
throw new SecurityException("Cannot undelete: Permission denied");
throttle();
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "undelete");
getparams.put("title", normalize(title));
Map<String, Object> postparams = new HashMap<>();
postparams.put("reason", reason);
postparams.put("token", getToken("csrf"));
if (revisions.length != 0)
{
StringJoiner sj = new StringJoiner("|");
// https://phabricator.wikimedia.org/T16449
for (Wiki.Revision revision : revisions)
sj.add(revision.getTimestamp().withOffsetSameInstant(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
postparams.put("timestamps", sj.toString());
}
String response = makeApiCall(getparams, postparams, "undelete");
// done
checkErrorsAndUpdateStatus(response, "undelete");
if (response.contains("cantundelete"))
log(Level.WARNING, "undelete", "Can't undelete: " + title + " has no deleted revisions.");
log(Level.INFO, "undelete", "Successfully undeleted " + title);
for (Revision rev : revisions)
rev.pageDeleted = false;
}
/**
* Purges the server-side cache for various pages.
* @param titles the titles of the page to purge
* @param links update the links tables
* @throws IOException if a network error occurs
* @since 0.17
*/
public void purge(boolean links, String... titles) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "purge");
if (links)
getparams.put("forcelinkupdate", "");
Map<String, Object> postparams = new HashMap<>();
for (String x : constructTitleString(titles))
{
postparams.put("title", x);
makeApiCall(getparams, postparams, "purge");
}
log(Level.INFO, "purge", "Successfully purged " + titles.length + " pages.");
}
/**
* Gets the list of images used on a particular page. If there are
* redirected images, both the source and target page are included.
*
* @param title a page
* @return the list of images used in the page. Note that each String in
* the array will begin with the prefix "File:"
* @throws IOException if a network error occurs
* @since 0.16
*/
public String[] getImagesOnPage(String title) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "images");
getparams.put("titles", normalize(title));
List<String> images = makeListQuery("im", getparams, null, "getImagesOnPage", -1, (line, results) ->
{
// xml form: <im ns="6" title="File:Example.jpg" />
for (int a = line.indexOf("<im "); a > 0; a = line.indexOf("<im ", ++a))
results.add(parseAttribute(line, "title", a));
});
int temp = images.size();
log(Level.INFO, "getImagesOnPage", "Successfully retrieved images used on " + title + " (" + temp + " images)");
return images.toArray(new String[temp]);
}
/**
* Gets the list of categories a particular page is in. Includes hidden
* categories.
*
* @param title a page
* @return the list of categories that page is in
* @throws IOException if a network error occurs
* @since 0.16
*/
public String[] getCategories(String title) throws IOException
{
return getCategories(Arrays.asList(title), null, false).get(0).toArray(new String[0]);
}
/**
* Gets the list of categories that the given list of pages belongs to.
* Includes the sortkey of a category if <var>sortkey</var> is true. The
* sortkey would then be appended to the element of the returned strings
* (separated by "|"). Accepted parameters from <var>helper</var> are:
* are:
*
* <ul>
* <li>{@link Wiki.RequestHelper#filterBy(Map) filter by}: "hidden"
* (hidden categories)
* <li>{@link Wiki.RequestHelper#limitedTo(int) local query limit}
* </ul>
*
* @param titles a list of pages
* @param helper a {@link Wiki.RequestHelper} (optional, use null to not
* provide any of the optional parameters noted above)
* @param sortkey return a sortkey as well (default = false)
* @return the list of categories that the page is in
* @throws IOException if a network error occurs
* @since 0.30
*/
public List<List<String>> getCategories(List<String> titles, Wiki.RequestHelper helper, boolean sortkey) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "categories");
int limit = -1;
if (helper != null)
{
helper.setRequestType("cl");
getparams.putAll(helper.addShowParameter());
limit = helper.limit();
}
// copy array so redirect resolver doesn't overwrite
String[] titles2 = new String[titles.size()];
titles.toArray(titles2);
List<Map<String, List<String>>> stuff = new ArrayList<>();
Map<String, Object> postparams = new HashMap<>();
for (String temp : constructTitleString(titles2))
{
postparams.put("titles", temp);
stuff.addAll(makeListQuery("cl", getparams, postparams, "getCategories", limit, (line, results) ->
{
// Split the result into individual listings for each article.
String[] x = line.split("<page ");
if (resolveredirect)
resolveRedirectParser(titles2, x[0]);
// Skip first element to remove front crud.
for (int i = 1; i < x.length; i++)
{
// xml form: <cl ns="14" title="Category:1879 births" sortkey=(long string) sortkeyprefix="" />
// or : <cl ns="14" title="Category:Images for cleanup" sortkey=(long string) sortkeyprefix="Borders" hidden="" />
String parsedtitle = parseAttribute(x[i], "title", 0);
List<String> list = new ArrayList<>();
for (int a = x[i].indexOf("<cl "); a > 0; a = x[i].indexOf("<cl ", ++a))
{
String category = parseAttribute(x[i], "title", a);
if (sortkey)
category += ("|" + parseAttribute(x[i], "sortkeyprefix", a));
list.add(category);
}
Map<String, List<String>> intermediate = new HashMap<>();
intermediate.put(parsedtitle, list);
results.add(intermediate);
}
}));
}
// fill the return list
List<List<String>> ret = new ArrayList<>();
List<String> normtitles = new ArrayList<>();
for (String localtitle : titles2)
{
normtitles.add(normalize(localtitle));
ret.add(new ArrayList<>());
}
// then retrieve the results from the intermediate list of maps,
// ensuring results correspond to inputs
stuff.forEach(map ->
{
String parsedtitle = map.keySet().iterator().next();
List<String> templates = map.get(parsedtitle);
for (int i = 0; i < titles2.length; i++)
if (normtitles.get(i).equals(parsedtitle))
ret.get(i).addAll(templates);
});
log(Level.INFO, "getCategories", "Successfully retrieved categories used on " + titles2.length + " pages.");
return ret;
}
/**
* Gets the list of templates used on a particular page that are in a
* particular namespace(s).
*
* @param title a page
* @param ns a list of namespaces to filter by, empty = all namespaces.
* @return the list of templates used on that page in that namespace
* @throws IOException if a network error occurs
* @since 0.16
*/
public String[] getTemplates(String title, int... ns) throws IOException
{
List<String> temp = getTemplates(new String[] { title }, ns)[0];
return temp.toArray(new String[temp.size()]);
}
/**
* Gets the list of templates used on the given pages that are in a
* particular namespace(s). The order of elements in the return array is
* the same as the order of the list of titles.
*
* @param titles a list of pages
* @param ns a list of namespaces to filter by, empty = all namespaces.
* @return the list of templates used by those pages page in that namespace
* @throws IOException if a network error occurs
* @since 0.32
*/
public List<String>[] getTemplates(String[] titles, int... ns) throws IOException
{
return getTemplates(titles, null, ns);
}
/**
* Determine whether a list of pages contains the given template. The order
* of elements in the return array is the same as the order of the list of
* titles.
*
* @param pages a list of pages
* @param template the template to check for
* @return whether the given pages contain said template
* @throws IOException if a network error occurs
* @since 0.32
*/
public boolean[] pageHasTemplate(String[] pages, String template) throws IOException
{
boolean[] ret = new boolean[pages.length];
List<String>[] result = getTemplates(pages, template);
for (int i = 0; i < result.length; i++)
ret[i] = !(result[i].isEmpty());
return ret;
}
/**
* Gets the list of templates used on the given pages that are in a
* particular namespace(s). The order of elements in the return array is
* the same as the order of the list of titles.
*
* @param titles a list of pages
* @param template restrict results to the supplied page. Useful for checking
* whether a list of pages contains a given template.
* @param ns a list of namespaces to filter by, empty = all namespaces.
* @return the list of templates used by those pages page in that namespace
* @throws IOException if a network error occurs
* @since 0.32
*/
protected List<String>[] getTemplates(String[] titles, String template, int... ns) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "templates");
if (ns.length > 0)
getparams.put("tlnamespace", constructNamespaceString(ns));
if (template != null)
getparams.put("tltemplates", normalize(template));
// copy array so redirect resolver doesn't overwrite
String[] titles2 = Arrays.copyOf(titles, titles.length);
List<Map<String, List<String>>> stuff = new ArrayList<>();
Map<String, Object> postparams = new HashMap<>();
for (String temp : constructTitleString(titles))
{
postparams.put("titles", temp);
stuff.addAll(makeListQuery("tl", getparams, postparams, "getTemplates", -1, (line, results) ->
{
// Split the result into individual listings for each article.
String[] x = line.split("<page ");
if (resolveredirect)
resolveRedirectParser(titles2, x[0]);
// Skip first element to remove front crud.
for (int i = 1; i < x.length; i++)
{
// xml form: <tl ns="10" title="Template:POTD" />
String parsedtitle = parseAttribute(x[i], "title", 0);
List<String> list = new ArrayList<>();
for (int a = x[i].indexOf("<tl "); a > 0; a = x[i].indexOf("<tl ", ++a))
list.add(parseAttribute(x[i], "title", a));
Map<String, List<String>> intermediate = new HashMap<>();
intermediate.put(parsedtitle, list);
results.add(intermediate);
}
}));
}
// merge and reorder
List<String>[] out = new ArrayList[titles.length];
Arrays.setAll(out, ArrayList::new);
stuff.forEach(entry ->
{
String parsedtitle = entry.keySet().iterator().next();
List<String> templates = entry.get(parsedtitle);
for (int i = 0; i < titles2.length; i++)
if (normalize(titles2[i]).equals(parsedtitle))
out[i].addAll(templates);
});
log(Level.INFO, "getTemplates", "Successfully retrieved templates used on " + titles.length + " pages.");
return out;
}
/**
* Gets the list of interwiki links a particular page has. The returned
* map has the format { language code : the page on the external wiki
* linked to }.
*
* @param title a page
* @return a map of interwiki links that page has (empty if there are no
* links)
* @throws IOException if a network error occurs
* @since 0.18
*/
public Map<String, String> getInterWikiLinks(String title) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "langlinks");
getparams.put("titles", normalize(title));
List<String[]> blah = makeListQuery("ll", getparams, null, "getInterWikiLinks", -1, (line, results) ->
{
// xml form: <ll lang="en" />Main Page</ll> or <ll lang="en" /> for [[Main Page]]
for (int a = line.indexOf("<ll "); a > 0; a = line.indexOf("<ll ", ++a))
{
String language = parseAttribute(line, "lang", a);
int b = line.indexOf('>', a) + 1;
int c = line.indexOf('<', b);
String page = decode(line.substring(b, c));
results.add(new String[] { language, page });
}
});
Map<String, String> interwikis = new HashMap<>(750);
blah.forEach(result -> interwikis.put(result[0], result[1]));
log(Level.INFO, "getInterWikiLinks", "Successfully retrieved interwiki links on " + title);
return interwikis;
}
/**
* Gets the list of wikilinks used on a particular page. Patch somewhat by
* wim.jongman
*
* @param title a page
* @return the list of links used in the page
* @throws IOException if a network error occurs
* @since 0.24
*/
public String[] getLinksOnPage(String title) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "links");
getparams.put("titles", normalize(title));
List<String> links = makeListQuery("pl", getparams, null, "getLinksOnPage", -1, (line, results) ->
{
// xml form: <pl ns="6" title="page name" />
for (int a = line.indexOf("<pl "); a > 0; a = line.indexOf("<pl ", ++a))
results.add(parseAttribute(line, "title", a));
});
int size = links.size();
log(Level.INFO, "getLinksOnPage", "Successfully retrieved links used on " + title + " (" + size + " links)");
return links.toArray(new String[size]);
}
/**
* Gets the list of external links used on a particular page.
*
* @param title a page
* @return the list of external links used in the page
* @throws IOException if a network error occurs
* @since 0.29
*/
public String[] getExternalLinksOnPage(String title) throws IOException
{
List<String> temp = getExternalLinksOnPage(Arrays.asList(title)).get(0);
return temp.toArray(new String[temp.size()]);
}
/**
* Gets the list of external links used on a list of pages. The return list
* contains results that correspond to the list of input titles, element wise.
*
* @param titles a list of pages
* @return the lists of external links used on those pages
* @throws IOException if a network error occurs
* @since 0.35
* @see <a href="https://www.mediawiki.org/wiki/API:Extlinks">MediaWiki
* documentation</a>
* @see <a href="https://mediawiki.org/wiki/Manual:Externallinks_table">Externallinks
* table</a>
*/
public List<List<String>> getExternalLinksOnPage(List<String> titles) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "extlinks");
// copy array so redirect resolver doesn't overwrite
String[] titles2 = new String[titles.size()];
titles.toArray(titles2);
List<Map<String, List<String>>> stuff = new ArrayList<>();
Map<String, Object> postparams = new HashMap<>();
for (String temp : constructTitleString(titles2))
{
postparams.put("titles", temp);
stuff.addAll(makeListQuery("el", getparams, postparams, "getExternalLinksOnPage", -1, (line, results) ->
{
// Split the result into individual listings for each article.
String[] x = line.split("<page ");
if (resolveredirect)
resolveRedirectParser(titles2, x[0]);
// Skip first element to remove front crud.
for (int i = 1; i < x.length; i++)
{
// xml form: <el stuff>http://example.com</el>
String parsedtitle = parseAttribute(x[i], "title", 0);
List<String> list = new ArrayList<>();
for (int a = x[i].indexOf("<el "); a > 0; a = x[i].indexOf("<el ", ++a))
{
int start = x[i].indexOf('>', a) + 1;
int end = x[i].indexOf("</el>", start);
list.add(decode(x[i].substring(start, end)));
}
Map<String, List<String>> intermediate = new HashMap<>();
intermediate.put(parsedtitle, list);
results.add(intermediate);
}
}));
}
// fill the return list
List<List<String>> ret = new ArrayList<>();
List<String> normtitles = new ArrayList<>();
for (String localtitle : titles2)
{
normtitles.add(normalize(localtitle));
ret.add(new ArrayList<>());
}
// then retrieve the results from the intermediate list of maps,
// ensuring results correspond to inputs
stuff.forEach(map ->
{
String parsedtitle = map.keySet().iterator().next();
List<String> templates = map.get(parsedtitle);
for (int i = 0; i < titles2.length; i++)
if (normtitles.get(i).equals(parsedtitle))
ret.get(i).addAll(templates);
});
log(Level.INFO, "getExternalLinksOnPage", "Successfully retrieved external links used on " + titles2.length + " pages.");
return ret;
}
/**
* Gets the list of sections on the specified <var>page</var>. The returned
* map pairs the section numbering as in the table of contents with the
* section title, as in the following example:
*
* <pre><samp>
* 1 &#8594; How to nominate
* 1.1 &#8594; Step 1 - Evaluate
* 1.2 &#8594; Step 2 - Create subpage
* 1.2.1 &#8594; Step 2.5 - Transclude and link
* 1.3 &#8594; Step 3 - Update image
* </samp></pre>
*
* @param page the page to get sections for
* @return the section map for that page
* @throws IOException if a network error occurs
* @since 0.18
*/
public LinkedHashMap<String, String> getSectionMap(String page) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "parse");
getparams.put("prop", "sections");
getparams.put("text", "{{:" + normalize(page) + "}}__TOC__");
String line = makeApiCall(getparams, null, "getSectionMap");
// xml form: <s toclevel="1" level="2" line="How to nominate" number="1" />
LinkedHashMap<String, String> map = new LinkedHashMap<>(30);
for (int a = line.indexOf("<s "); a > 0; a = line.indexOf("<s ", ++a))
{
String title = parseAttribute(line, "line", a);
String number = parseAttribute(line, "number", a);
map.put(number, title);
}
log(Level.INFO, "getSectionMap", "Successfully retrieved section map for " + page);
return map;
}
/**
* Gets the most recent revision of a page, or {@code null} if the page
* does not exist.
* @param title a page
* @return the most recent revision of that page
* @throws IOException or UncheckedIOException if a network error occurs
* @throws UnsupportedOperationException if <var>title</var> is a Special
* or Media page
* @since 0.24
*/
public Revision getTopRevision(String title) throws IOException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Special and Media pages do not have histories!");
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("prop", "revisions");
getparams.put("rvlimit", "1");
getparams.put("titles", normalize(title));
getparams.put("rvprop", "timestamp|user|ids|flags|size|comment|parsedcomment|sha1");
String line = makeApiCall(getparams, null, "getTopRevision");
int a = line.indexOf("<rev "); // important space
int b = line.indexOf("/>", a);
if (a < 0) // page does not exist
return null;
return parseRevision(line.substring(a, b), title);
}
/**
* Gets the first revision of a page, or {@code null} if the page does not
* exist.
* @param title a page
* @return the oldest revision of that page
* @throws IOException or UncheckedIOException if a network error occurs
* @throws UnsupportedOperationException if <var>title</var> is a Special
* or Media page
* @since 0.24
*/
public Revision getFirstRevision(String title) throws IOException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Special and Media pages do not have histories!");
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("prop", "revisions");
getparams.put("rvlimit", "1");
getparams.put("rvdir", "newer");
getparams.put("titles", normalize(title));
getparams.put("rvprop", "timestamp|user|ids|flags|size|comment|parsedcomment|sha1");
String line = makeApiCall(getparams, null, "getFirstRevision");
int a = line.indexOf("<rev "); // important space!
int b = line.indexOf("/>", a);
if (a < 0) // page does not exist
return null;
return parseRevision(line.substring(a, b), title);
}
/**
* Gets the newest page name or the name of a page where the asked page
* redirects.
* @param title a title
* @return the page redirected to or {@code null} if not a redirect
* @throws IOException if a network error occurs
* @since 0.29
*/
public String resolveRedirect(String title) throws IOException
{
return resolveRedirects(new String[] { title })[0];
}
/**
* Gets the newest page name or the name of a page where the asked pages
* redirect.
* @param titles a list of titles.
* @return for each title, the page redirected to or the original page
* title if not a redirect
* @throws IOException or UncheckedIOException if a network error occurs
* @since 0.29
* @author Nirvanchik/MER-C
*/
public String[] resolveRedirects(String[] titles) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
if (!resolveredirect)
getparams.put("redirects", "");
Map<String, Object> postparams = new HashMap<>();
String[] ret = Arrays.copyOf(titles, titles.length);
for (String blah : constructTitleString(titles))
{
postparams.put("titles", blah);
String line = makeApiCall(getparams, postparams, "resolveRedirects");
resolveRedirectParser(ret, line);
}
return ret;
}
/**
* Parses the output of queries that resolve redirects (extracted to
* separate method as requirement for all vectorized queries when
* {@link #isResolvingRedirects()} is {@code true}).
*
* @param inputpages the array of pages to resolve redirects for. Entries
* will be overwritten.
* @param xml the xml to parse
* @throws UncheckedIOException if the namespace cache has not been
* populated, and a network error occurs when populating it
* @since 0.34
*/
protected void resolveRedirectParser(String[] inputpages, String xml)
{
// expected form: <redirects><r from="Main page" to="Main Page"/>
// <r from="Home Page" to="Home page"/>...</redirects>
// TODO: look for the <r> tag instead
for (int j = xml.indexOf("<r "); j > 0; j = xml.indexOf("<r ", ++j))
{
String parsedtitle = parseAttribute(xml, "from", j);
for (int i = 0; i < inputpages.length; i++)
if (normalize(inputpages[i]).equals(parsedtitle))
inputpages[i] = parseAttribute(xml, "to", j);
}
}
/**
* Gets the revision history of a page. Accepted parameters from
* <var>helper</var> are:
*
* <ul>
* <li>{@link Wiki.RequestHelper#withinDateRange(OffsetDateTime,
* OffsetDateTime) date range}
* <li>{@link Wiki.RequestHelper#byUser(String) user}
* <li>{@link Wiki.RequestHelper#notByUser(String) not by user}
* <li>{@link Wiki.RequestHelper#reverse(boolean) reverse}
* <li>{@link Wiki.RequestHelper#taggedWith(String) tag}
* <li>{@link Wiki.RequestHelper#limitedTo(int) local query limit}
* </ul>
*
* @param title a page
* @param helper a {@link Wiki.RequestHelper} (optional, use null to not
* provide any of the optional parameters noted above)
* @return the revisions of that page in that time span
* @throws IOException or UncheckedIOException if a network error occurs
* @throws UnsupportedOperationException if <var>title</var> is a Special
* or Media page
* @since 0.19
* @see <a href="https://mediawiki.org/wiki/API:Revisions">MediaWiki
* documentation</a>
*/
public List<Revision> getPageHistory(String title, Wiki.RequestHelper helper) throws IOException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Special and Media pages do not have histories!");
int limit = -1;
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "revisions");
getparams.put("titles", normalize(title));
getparams.put("rvprop", "timestamp|user|ids|flags|size|comment|parsedcomment|sha1");
if (helper != null)
{
helper.setRequestType("rv");
getparams.putAll(helper.addDateRangeParameters());
getparams.putAll(helper.addReverseParameter());
getparams.putAll(helper.addUserParameter());
getparams.putAll(helper.addExcludeUserParameter());
getparams.putAll(helper.addTagParameter());
limit = helper.limit();
}
List<Revision> revisions = makeListQuery("rv", getparams, null, "getPageHistory", limit, (line, results) ->
{
for (int a = line.indexOf("<rev "); a > 0; a = line.indexOf("<rev ", ++a))
{
int b = line.indexOf("/>", a);
results.add(parseRevision(line.substring(a, b), title));
}
});
log(Level.INFO, "getPageHistory", "Successfully retrieved page history of "
+ title + " (" + revisions.size() + " revisions)");
return revisions;
}
/**
* Gets the deleted history of a page. Accepted parameters from
* <var>helper</var> are:
*
* <ul>
* <li>{@link Wiki.RequestHelper#withinDateRange(OffsetDateTime,
* OffsetDateTime) date range}
* <li>{@link Wiki.RequestHelper#byUser(String) user}
* <li>{@link Wiki.RequestHelper#notByUser(String) not by user}
* <li>{@link Wiki.RequestHelper#reverse(boolean) reverse}
* <li>{@link Wiki.RequestHelper#taggedWith(String) tag}
* </ul>
*
* @param title a page (mandatory)
* @param helper a {@link Wiki.RequestHelper} (optional, use null to not
* provide any of the optional parameters noted above)
* @return the deleted revisions of that page subject to the optional
* constraints in helper
* @throws IOException or UncheckedIOException if a network error occurs
* @throws SecurityException if we cannot obtain deleted revisions
* @throws UnsupportedOperationException if <var>title</var> is a Special
* or Media page
* @since 0.30
* @see <a href="https://mediawiki.org/wiki/API:Deletedrevisions">MediaWiki
* documentation</a>
*/
public List<Revision> getDeletedHistory(String title, Wiki.RequestHelper helper) throws IOException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Special and Media pages do not have histories!");
if (user == null || !user.isAllowedTo("deletedhistory"))
throw new SecurityException("Permission denied: not able to view deleted history");
int limit = -1;
Map<String, String> getparams = new HashMap<>();
getparams.put("prop", "deletedrevisions");
getparams.put("drvprop", "ids|user|flags|size|comment|parsedcomment|sha1");
if (helper != null)
{
helper.setRequestType("drv");
getparams.putAll(helper.addDateRangeParameters());
getparams.putAll(helper.addReverseParameter());
getparams.putAll(helper.addUserParameter());
getparams.putAll(helper.addExcludeUserParameter());
getparams.putAll(helper.addTagParameter());
limit = helper.limit();
}
getparams.put("titles", normalize(title));
List<Revision> delrevs = makeListQuery("drv", getparams, null, "getDeletedHistory", limit, (response, results) ->
{
int x = response.indexOf("<deletedrevs>");
if (x < 0) // no deleted history
return;
for (x = response.indexOf("<page ", x); x > 0; x = response.indexOf("<page ", ++x))
{
String deltitle = parseAttribute(response, "title", x);
int y = response.indexOf("</page>", x);
for (int z = response.indexOf("<rev ", x); z < y && z >= 0; z = response.indexOf("<rev ", ++z))
{
int aa = response.indexOf(" />", z);
Revision temp = parseRevision(response.substring(z, aa), deltitle);
temp.pageDeleted = true;
results.add(temp);
}
}
});
log(Level.INFO, "Successfully fetched " + delrevs.size() + " deleted revisions.", "deletedRevs");
return delrevs;
}
/**
* Gets the deleted contributions of a user in the given namespace.
* Equivalent to [[Special:Deletedcontributions]]. Accepted parameters from
* <var>helper</var> are:
*
* <ul>
* <li>{@link Wiki.RequestHelper#withinDateRange(OffsetDateTime, OffsetDateTime) date range}
* <li>{@link Wiki.RequestHelper#reverse(boolean) reverse}
* <li>{@link Wiki.RequestHelper#inNamespaces(int...) namespaces}
* <li>{@link Wiki.RequestHelper#taggedWith(String) tag}
* <li>{@link Wiki.RequestHelper#limitedTo(int) local query limit}
* </ul>
*
* @param username a user (mandatory)
* @param helper a {@link Wiki.RequestHelper} (optional, use null to not
* provide any of the optional parameters noted above)
* @return the deleted contributions of that user
* @throws IOException if a network error occurs
* @throws SecurityException if we cannot obtain deleted revisions
* @since 0.30
* @see <a href="https://mediawiki.org/wiki/API:Alldeletedrevisions">MediaWiki
* documentation</a>
*/
public List<Revision> deletedContribs(String username, Wiki.RequestHelper helper) throws IOException
{
if (user == null || !user.isAllowedTo("deletedhistory"))
throw new SecurityException("Permission denied: not able to view deleted history");
int limit = -1;
Map<String, String> getparams = new HashMap<>();
getparams.put("list", "alldeletedrevisions");
getparams.put("adrprop", "ids|user|flags|size|comment|parsedcomment|timestamp|sha1");
if (helper != null)
{
helper.setRequestType("adr");
getparams.putAll(helper.addDateRangeParameters());
getparams.putAll(helper.addNamespaceParameter());
getparams.putAll(helper.addReverseParameter());
getparams.putAll(helper.addTagParameter());
limit = helper.limit();
}
List<Revision> delrevs = makeListQuery("adr", getparams, null, "deletedContribs", limit, (response, results) ->
{
int x = response.indexOf("<alldeletedrevisions>");
if (x < 0) // no deleted history
return;
for (x = response.indexOf("<page ", x); x > 0; x = response.indexOf("<page ", ++x))
{
String deltitle = parseAttribute(response, "title", x);
int y = response.indexOf("</page>", x);
for (int z = response.indexOf("<rev ", x); z < y && z >= 0; z = response.indexOf("<rev ", ++z))
{
int aa = response.indexOf(" />", z);
Revision temp = parseRevision(response.substring(z, aa), deltitle);
temp.pageDeleted = true;
results.add(temp);
}
}
});
log(Level.INFO, "Successfully fetched " + delrevs.size() + " deleted revisions.", "deletedRevs");
return delrevs;
}
/**
* Returns all deleted pages that begin with the given prefix. WARNING:
* this does not behave like [[Special:Prefixindex]]. See [[Special:Undelete]]
* with no arguments.
*
* @param prefix a prefix without a namespace specifier, empty string
* lists all deleted pages in the namespace.
* @param namespace one (and only one) namespace -- not ALL_NAMESPACES
* @return (see above)
* @throws IOException if a network error occurs
* @throws SecurityException if we cannot view deleted pages
* @throws IllegalArgumentException if namespace == ALL_NAMESPACES
* @since 0.31
*/
public String[] deletedPrefixIndex(String prefix, int namespace) throws IOException
{
if (user == null || !user.isAllowedTo("deletedhistory", "deletedtext"))
throw new SecurityException("Permission denied: not able to view deleted history or text.");
// disallow ALL_NAMESPACES, this query is extremely slow and likely to error out.
if (namespace == ALL_NAMESPACES)
throw new IllegalArgumentException("deletedPrefixIndex: you must choose a namespace.");
// use the generator here to get a list of pages, not revisions
Map<String, String> getparams = new HashMap<>();
getparams.put("generator", "alldeletedrevisions");
getparams.put("gadrdir", "newer");
getparams.put("gadrgeneratetitles", "1");
getparams.put("gadrprefix", prefix);
getparams.put("gadrnamespace", String.valueOf(namespace));
List<String> pages = makeListQuery("gadr", getparams, null, "deletedPrefixIndex", -1, (text, results) ->
{
for (int x = text.indexOf("<page ", 0); x > 0; x = text.indexOf("<page ", ++x))
results.add(parseAttribute(text, "title", x));
});
int size = pages.size();
log(Level.INFO, "deletedPrefixIndex", "Successfully retrieved deleted page list (" + size + " items).");
return pages.toArray(new String[size]);
}
/**
* Gets the text of a deleted page (it's like getPageText, but for deleted
* pages).
* @param page a page
* @return the deleted text, or null if there is no deleted text to retrieve
* @throws IOException if a network error occurs
* @throws SecurityException if we cannot obtain deleted revisions
* @since 0.30
*/
public String getDeletedText(String page) throws IOException
{
if (user == null || !user.isAllowedTo("deletedhistory", "deletedtext"))
throw new SecurityException("Permission denied: not able to view deleted history or text.");
// TODO: this can be multiquery(?)
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("prop", "deletedrevisions");
getparams.put("drvlimit", "1");
getparams.put("drvprop", "content");
getparams.put("titles", normalize(page));
// expected form: <rev timestamp="2009-04-05T22:40:35Z" xml:space="preserve">TEXT OF PAGE</rev>
String line = makeApiCall(getparams, null, "getDeletedText");
int a = line.indexOf("<rev ");
if (a < 0)
return null;
a = line.indexOf('>', a) + 1;
int b = line.indexOf("</rev>", a); // tag not in empty pages
log(Level.INFO, "getDeletedText", "Successfully retrieved deleted text of page " + page);
return (b < 0) ? "" : line.substring(a, b);
}
/**
* Moves a page. Moves the associated talk page and leaves redirects, if
* applicable. Equivalent to [[Special:MovePage]]. This method is
* {@linkplain #setThrottle(int) throttled}. Does not recategorize pages
* in moved categories.
*
* @param title the title of the page to move
* @param newTitle the new title of the page
* @param reason a reason for the move
* @throws UnsupportedOperationException if the original page is in the
* Special or Media namespaces. MediaWiki does not support moving of
* these pages.
* @throws IOException or UncheckedIOException if a network error occurs
* @throws SecurityException if not logged in
* @throws CredentialExpiredException if cookies have expired
* @throws CredentialException if page is protected and we can't move it
* @since 0.16
*/
public void move(String title, String newTitle, String reason) throws IOException, LoginException
{
move(title, newTitle, reason, false, true, false);
}
/**
* Moves a page. Equivalent to [[Special:MovePage]]. This method is
* {@linkplain #setThrottle(int) throttled}. Does not recategorize pages
* in moved categories.
*
* @param title the title of the page to move
* @param newTitle the new title of the page
* @param reason a reason for the move
* @param noredirect don't leave a redirect behind. You need to be a
* admin to do this, otherwise this option is ignored.
* @param movesubpages move the subpages of this page as well. You need to
* be an admin to do this, otherwise this will be ignored.
* @param movetalk move the talk page as well (if applicable)
* @throws UnsupportedOperationException if the original page is in the
* Special or Media namespaces. MediaWiki does not support moving of these
* pages.
* @throws IOException or UncheckedIOException if a network error occurs
* @throws SecurityException if not logged in
* @throws CredentialExpiredException if cookies have expired
* @throws CredentialException if page is protected and we can't move it
* @since 0.16
*/
public synchronized void move(String title, String newTitle, String reason, boolean noredirect, boolean movetalk,
boolean movesubpages) throws IOException, LoginException
{
if (namespace(title) < 0)
throw new UnsupportedOperationException("Tried to move a Special or Media page.");
if (user == null || !user.isAllowedTo("move"))
throw new SecurityException("Permission denied: cannot move pages.");
throttle();
// protection and token
Map<String, Object> info = getPageInfo(title);
// determine whether the page exists
if (Boolean.FALSE.equals(info.get("exists")))
throw new IllegalArgumentException("Tried to move a non-existant page!");
if (!checkRights(info, "move"))
{
CredentialException ex = new CredentialException("Permission denied: page is protected.");
log(Level.WARNING, "move", "Cannot move - permission denied. " + ex);
throw ex;
}
// post data
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "move");
getparams.put("from", normalize(title));
getparams.put("to", normalize(newTitle));
Map<String, Object> postparams = new HashMap<>();
postparams.put("reason", reason);
postparams.put("token", getToken("csrf"));
if (movetalk)
postparams.put("movetalk", "1");
if (noredirect && user.isAllowedTo("suppressredirect"))
postparams.put("noredirect", "1");
if (movesubpages && user.isAllowedTo("move-subpages"))
postparams.put("movesubpages", "1");
String response = makeApiCall(getparams, postparams, "move");
// done
if (!response.contains("move from"))
checkErrorsAndUpdateStatus(response, "move");
log(Level.INFO, "move", "Successfully moved " + title + " to " + newTitle);
}
/**
* Protects a page. This method is {@linkplain #setThrottle(int) throttled}.
* Structure of <var>protectionstate</var> (everything is optional, if a
* value is not present, then the corresponding values will be left
* untouched):
*
* <pre><samp>
* {
* edit: one of { NO_PROTECTION, SEMI_PROTECTION, FULL_PROTECTION }, // restricts editing
* editexpiry: OffsetDateTime, // expiry time for edit protection, null = indefinite
* move, moveexpiry, // as above, prevents page moving
* create, createexpiry, // as above, prevents page creation (no effect on existing pages)
* upload, uploadexpiry, // as above, prevents uploading of files (FILE_NAMESPACE only)
* cascade: Boolean, // Enables cascading protection (requires edit=FULL_PROTECTION). Default: false.
* cascadesource: String // souce of cascading protection (here ignored)
* };
* </samp></pre>
*
* @param page the page
* @param protectionstate (see above)
* @param reason the reason for (un)protection
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialExpiredException if cookies have expired
* @throws SecurityException if we cannot protect
* @since 0.30
*/
public synchronized void protect(String page, Map<String, Object> protectionstate, String reason) throws IOException, LoginException
{
if (user == null || !user.isAllowedTo("protect"))
throw new SecurityException("Cannot protect: permission denied.");
throttle();
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "protect");
getparams.put("title", normalize(page));
Map<String, Object> postparams = new HashMap<>();
postparams.put("reason", reason);
postparams.put("token", getToken("csrf"));
// cascade protection
if (protectionstate.containsKey("cascade"))
postparams.put("cascade", "1");
// protection levels
StringBuilder pro = new StringBuilder();
StringBuilder exp = new StringBuilder();
protectionstate.forEach((key, value) ->
{
if (!key.contains("expiry") && !key.equals("cascade"))
{
pro.append(key);
pro.append('=');
pro.append(value);
pro.append('|');
// https://phabricator.wikimedia.org/T16449
OffsetDateTime expiry = (OffsetDateTime)protectionstate.get(key + "expiry");
exp.append(expiry == null ? "never" : expiry.withOffsetSameInstant(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
exp.append('|');
}
});
pro.delete(pro.length() - 3, pro.length());
exp.delete(exp.length() - 3, exp.length());
postparams.put("protections", pro);
postparams.put("expiry", exp);
String response = makeApiCall(getparams, postparams, "protect");
// done
if (!response.contains("<protect "))
checkErrorsAndUpdateStatus(response, "protect");
log(Level.INFO, "edit", "Successfully protected " + page);
}
/**
* Completely unprotects a page. This method is {@linkplain #setThrottle(int)
* throttled}.
* @param page the page to unprotect
* @param reason the reason for unprotection
* @throws IOException if a network error occurs
* @throws AccountLockedException if user is blocked
* @throws CredentialExpiredException if cookies have expired
* @throws SecurityException if we cannot protect
* @since 0.30
*/
public void unprotect(String page, String reason) throws IOException, LoginException
{
Map<String, Object> state = new HashMap<>();
state.put("edit", NO_PROTECTION);
state.put("move", NO_PROTECTION);
if (namespace(page) == FILE_NAMESPACE)
state.put("upload", NO_PROTECTION);
state.put("create", NO_PROTECTION);
protect(page, state, reason);
}
/**
* Exports the current revision of this page. Equivalent to
* [[Special:Export]].
* @param title the title of the page to export
* @return the exported text
* @throws IOException if a network error occurs
* @since 0.20
*/
public String export(String title) throws IOException
{
Map<String, String> getparams = new HashMap<>();
getparams.put("action", "query");
getparams.put("export", "");
getparams.put("exportnowrap", "");
getparams.put("titles", normalize(title));
return makeApiCall(getparams, null, "export");
}
// REVISION METHODS