User:HersfoldCiteBot/Source

This page contains a copy of the code used to run User:HersfoldCiteBot. This will be updated to reflect changes made to the code, which will be summarized at ../Version.

Permalink to this revision

HersfoldCiteBot.java
specified when using {{ correctedTemplates = new ArrayList;

if(!fatalErrorExists){

int startIndex = findCiteWebStart(pagecontent, 0);

while(startIndex != -1 && !fatalErrorExists){ int endIndex = pagecontent.indexOf("}}", startIndex) + 2; //include }} String citeTemplate = pagecontent.substring(startIndex, endIndex); final String citeTemplateOriginal = citeTemplate;

if(!correctedTemplates.contains(citeTemplate)){

// Correct missing titles // Looks for a lack of |title= OR for ...|title= (blank space) |... // Both are empty parameters if(indexOfRegex(citeTemplate, "\\|\\s*title\\s*=") == -1 || indexOfRegex(citeTemplate, "\\|\\s*title\\s*=\\s*\\|") != -1){ addToLog("Trying to add a title= parameter to " + citeTemplate);

boolean existingArg = false; int titleArgEnd = 0; if(indexOfRegex(citeTemplate, "\\|\\s*title\\s*=\\s*\\|") != -1){ titleArgEnd = citeTemplate.indexOf("=", indexOfRegex(citeTemplate, "title\\s*=")) + 1; existingArg = true; }

int urlStart = indexOfRegex(citeTemplate, "url\\s*="); boolean urlNotFound = false; if(urlStart == -1){ urlStart = citeTemplate.indexOf("http://"); if(urlStart == -1){ urlStart = citeTemplate.indexOf("https://"); if(urlStart == -1){ addToLog("I can't find the |url= parameter in this citation."); ArrayList problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList; problems.add("I can't find the |url= parameter in this citation: " + citeTemplate + " "); toBeReviewed.put(pagetitle, problems); urlNotFound = true; }							}							// Add |url= parameter to fix template // If statement is present to avoid {{cite web||url=http://... 							if(citeTemplate.substring(0, urlStart).endsWith("|")){ citeTemplate = citeTemplate.substring(0, urlStart - 1) + "|url=" + citeTemplate.substring(urlStart); }							else{ citeTemplate = citeTemplate.substring(0, urlStart) + "|url=" + citeTemplate.substring(urlStart); }							urlStart = citeTemplate.indexOf("=", indexOfRegex(citeTemplate, "url\\s*=")) + 1; // Correct index }						else{ urlStart = citeTemplate.indexOf("=", urlStart) + 1; // index of the = in the parameter plus 1 }						if(!urlNotFound){ int pipe = citeTemplate.indexOf("|", urlStart); int space = citeTemplate.indexOf(" ", urlStart); int bracket = citeTemplate.indexOf("}}", urlStart); int urlEnd; if(((pipe < space || space == -1) && pipe < bracket) && pipe != -1) urlEnd = pipe; else if(space < bracket && space != -1) urlEnd = space; else urlEnd = bracket;

boolean titlefound = false;

// Check to see if title is included with url ([url title] error) if(urlEnd == space){ int argEnd; if(pipe != -1 && pipe < bracket){ argEnd = pipe; }								else{ argEnd = bracket; }

String possibleTitle = citeTemplate.substring(urlEnd + 1, argEnd).trim;

// If there are alphanumeric characters after the end of the URL, we'll assume that is the title if(indexOfRegex(possibleTitle, "[A-Za-z0-9]") != -1){ titlefound = true; if(existingArg){ //This is more complex than I'd like, but oh well. citeTemplate = citeTemplate.replaceFirst("\\|\\s*title\\s*=", ""); // remove existing empty param, assuming there is only one citeTemplate = citeTemplate.replace(possibleTitle, ""); // remove identified title bracket = citeTemplate.indexOf("}}"); // figure out where that ended up										citeTemplate = citeTemplate.substring(0, bracket) + "|title=" + ASSUMED_TITLE_DISCLAIMER + possibleTitle + "}}"; }									else{ citeTemplate = citeTemplate.substring(0, urlEnd + 1) + "|title=" + ASSUMED_TITLE_DISCLAIMER + citeTemplate.substring(urlEnd + 1); }								}							}

if(!titlefound){

String url = citeTemplate.substring(urlStart, urlEnd).trim; boolean logged = false; if(url.matches("\\s*")){ // If is empty or completely whitespace addToLog("There does not appear to be a URL for this template."); ArrayList problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList; problems.add("There does not seem to be a URL in this template: " + citeTemplate); toBeReviewed.put(pagetitle, problems); logged = true; }								else{

if(!url.startsWith("http://") && !url.startsWith("https://")) url = "http://" + url;

// Don't try to find titles for PDFs, not going to work if(!url.contains(".pdf") && !url.contains(".PDF")){ String linktitle = null; titlefound = false;

try{ URLConnection connection = new URL(url).openConnection; connection.connect; BufferedReader urlcontent = new BufferedReader(new InputStreamReader(connection.getInputStream, "UTF-8"));

while(urlcontent.ready && !titlefound){ String nextline = urlcontent.readLine; if(nextline != null && (nextline.contains(" ") || nextline.contains(""))){ int titleStart = nextline.indexOf(" ") + 7; if(titleStart == -1){ titleStart = nextline.indexOf("") + 7; }													int titleEnd = -1; titleEnd = nextline.indexOf(" ", titleStart); if(titleEnd == -1){ titleEnd = nextline.indexOf("", titleStart); }													if(titleEnd != -1){ linktitle = nextline.substring(titleStart, titleEnd); }													else{ linktitle = ""; while(titleEnd == -1){ nextline = urlcontent.readLine; titleEnd = nextline.indexOf(" "); if(titleEnd == -1){ titleEnd = nextline.indexOf(""); }															if(titleEnd == -1){ linktitle += nextline; }															else{ linktitle += nextline.substring(0, titleEnd); }														}													}													titlefound = true; }												else if(nextline == null || (nextline.contains(" ") || nextline.contains(""))){ addToLog("Unable to find title for page at " + url); ArrayList problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList; problems.add("Unable to find a title for the page at " + url); toBeReviewed.put(pagetitle, problems); logged = true; }											}										}										catch(UnsupportedEncodingException e){}//Won't happen catch(IOException e){ addToLog("IOException recieved when trying to access " + url + " ."); ArrayList problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList; problems.add("IOException received when trying to access " + url + " . Depending on the reason for the " +													"error (provided at the end of this entry), this may be a temporary problem that the bot can " +													"resolve itself on a later run. IOException message: " + e.getMessage); toBeReviewed.put(pagetitle, problems); logged = true; }

if(linktitle != null){ String newtitle = (existingArg ? "" : "|title=") + linktitle + BOT_TITLE_DISCLAIMER;

if(existingArg){ citeTemplate = citeTemplate.substring(0, titleArgEnd) + newtitle + citeTemplate.substring(titleArgEnd); }											else{ int bracketIndex = citeTemplate.indexOf("}}"); citeTemplate = citeTemplate.substring(0, bracketIndex) + newtitle + "}}"; }										}										else if(!logged){ addToLog("Unable to find title for page at " + url); ArrayList problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList<String>; problems.add("Unable to find a title for the page at " + url); toBeReviewed.put(pagetitle, problems); }									}									else{ addToLog("Referenced website at " + url + " appears to be a PDF, requires manual attention to add title."); ArrayList<String> problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList<String>; problems.add("Referenced website at " + url + " appears to be a PDF, requires manual attention to add title."); toBeReviewed.put(pagetitle, problems); }								}							}						}					}

if(!fatalErrorExists){

// Add missing archivedate= args for web.archive.org sites // If archiveurl= is neither missing nor empty, AND archivedate= is either missing or empty if((indexOfRegex(citeTemplate, "\\|\\s*archiveurl\\s*=") != -1 && indexOfRegex(citeTemplate, "\\|\\s*archiveurl\\s*=\\s*\\|") == -1) &&								(indexOfRegex(citeTemplate, "\\|\\s*archivedate\\s*=") == -1 || indexOfRegex(citeTemplate, "\\|\\s*archivedate\\s*=\\s*\\|") != -1)){ addToLog("Trying to add an archivedate= parameter to " + citeTemplate);

boolean existingArg = false; int dateArgEnd = 0; if(indexOfRegex(citeTemplate, "\\|\\s*archivedate\\s*=\\s*\\|") != -1){ dateArgEnd = citeTemplate.indexOf("=", indexOfRegex(citeTemplate, "archivedate\\s*=")) + 1; existingArg = true; }

int urlStart = citeTemplate.indexOf("=", indexOfRegex(citeTemplate, "archiveurl\\s*=")) + 1; int pipe = citeTemplate.indexOf("|", urlStart); int space = citeTemplate.indexOf(" ", urlStart); int bracket = citeTemplate.indexOf("}}", urlStart); int urlEnd; if(((pipe < space || space == -1) && pipe < bracket) && pipe != -1) urlEnd = pipe; else if(space < bracket && space != -1) urlEnd = space; else urlEnd = bracket;

String archiveurl = citeTemplate.substring(urlStart, urlEnd).trim;

if(archiveurl.contains("web.archive.org")){ int yearStart = archiveurl.indexOf("/web/") + 5; int monthStart = yearStart + 4; int dayStart = monthStart + 2;

String year = archiveurl.substring(yearStart, monthStart); String month = archiveurl.substring(monthStart, dayStart); String day = archiveurl.substring(dayStart, dayStart + 2);

// Verify if(year.matches("\\d{4}") && month.matches("\\d{2}") && day.matches("\\d{2}")){ switch(Integer.parseInt(month)){ case 1: month = "January"; break; case 2: month = "February"; break; case 3: month = "March"; break; case 4: month = "April"; break; case 5: month = "May"; break; case 6: month = "June"; break; case 7: month = "July"; break; case 8: month = "August"; break; case 9: month = "September"; break; case 10: month = "October"; break; case 11: month = "November"; break; case 12: month = "December"; break; default: // we'll just leave it as is									}

if(existingArg) citeTemplate = citeTemplate.substring(0, dateArgEnd) + month + " " + day + " " + year + citeTemplate.substring(dateArgEnd); else citeTemplate = citeTemplate.substring(0, bracket) + "|archivedate=" + month + " " + day + " " + year + "}}"; }								else{ addToLog("This web.archive.org link appears to be botched. Marking for review..."); ArrayList<String> problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList<String>; problems.add("web.archive.org link " + archiveurl + " appears to be botched. Please check and add the archival date."); toBeReviewed.put(pagetitle, problems); }							}							else{ addToLog("The archive link isn't from web.archive.org. Marking for review..."); ArrayList<String> problems = null; if(toBeReviewed.containsKey(pagetitle)) problems = toBeReviewed.get(pagetitle); else problems = new ArrayList<String>; problems.add(archiveurl + " isn't a web.archive.org address, so I wasn't able to pull a date from it."); toBeReviewed.put(pagetitle, problems); }						}

/* DISABLED per recommendations at BRFA // While we're at it, add missing dates too if(indexOfRegex(citeTemplate, "\\|\\s*accessdate\\s*=") == -1 || indexOfRegex(citeTemplate, "\\|\\s*accessdate\\s*=\\s*\\|") != -1){ addToLog("Trying to add an accessdate= parameter to " + citeTemplate);

boolean existingArg = false; int dateArgEnd = 0; if(indexOfRegex(citeTemplate, "\\|\\s*accessdate\\s*=\\s*\\|") != -1){ dateArgEnd = citeTemplate.indexOf("=", indexOfRegex(citeTemplate, "accessdate\\s*=")) + 1; existingArg = true; }

currentTime.add(Calendar.MILLISECOND, (int)(System.currentTimeMillis - lastTime));

String timestamp = "" + currentTime.get(Calendar.DAY_OF_MONTH);

switch(currentTime.get(Calendar.MONTH)){ case Calendar.JANUARY: timestamp += " January "; break; case Calendar.FEBRUARY: timestamp += " February "; break; case Calendar.MARCH: timestamp += " March "; break; case Calendar.APRIL: timestamp += " April "; break; case Calendar.MAY: timestamp += " May "; break; case Calendar.JUNE: timestamp += " June "; break; case Calendar.JULY: timestamp += " July "; break; case Calendar.AUGUST: timestamp += " August "; break; case Calendar.SEPTEMBER: timestamp += " September "; break; case Calendar.OCTOBER: timestamp += " October "; break; case Calendar.NOVEMBER: timestamp += " November "; break; case Calendar.DECEMBER: timestamp += " December "; break; }

timestamp += "" + currentTime.get(Calendar.YEAR);

if(existingArg) citeTemplate = citeTemplate.substring(0, dateArgEnd) + timestamp + citeTemplate.substring(dateArgEnd); else{ int bracketIndex = citeTemplate.indexOf("}}"); citeTemplate = citeTemplate.substring(0, bracketIndex) + "|accessdate=" + timestamp + "}}"; }						}						DISABLED per recommendations at BRFA */

// Now that everything is done, replace the template correctedTemplates.add(citeTemplate); if(!citeTemplate.equals(citeTemplateOriginal)){ pagecontent = pagecontent.replace(citeTemplateOriginal, citeTemplate); }					}				}				startIndex = findCiteWebStart(pagecontent, startIndex + citeTemplate.length); }		}

return pagecontent; }

private int indexOfRegex(String source, String regex){ int index = 0;

while(source.length > 0){ if(source.matches(regex + "(.|\\n)*")) // (.|\\n)* needed to ensure we're not matching the whole string return index; else{ source = source.substring(1); index++; }		}

return -1; }	/*	public static void main(String args[]){ HersfoldCiteBot bot = new HersfoldCiteBot; bot.login("this is not the bot's password"); String content = bot.correctCiteWebErrors("User:Hersfold/Hersfold's Sandbox"); bot.logout; System.out.println("\n\n\n" + content); }	*/

}

Wiki.java
This file is originally from User:MER-C/Wiki.java. This version may have some slight differences specifically for this bot or to fix minor errors.

*        *  Note that the duration of a block may be given as a period of time * (e.g. "31 hours") or a timestamp (e.g. 20071216160302). To tell * these apart, feed it into <tt>Long.parseLong</tt> and catch any * resulting exceptions. *        *  @return the details of the log entry * @since 0.08 */       public Object getDetails {           return details; }

/**        *  Returns a string representation of this log entry. * @return a string representation of this object * @since 0.08 */       public String toString {           // @revised 0.17 to a more traditional Java approach StringBuilder s = new StringBuilder("LogEntry[type="); s.append(type); s.append(",action="); s.append(action == null ? "[hidden]" : action); s.append(",user="); s.append(user == null ? "[hidden]" : user.getUsername); s.append(",timestamp="); s.append(calendarToTimestamp(timestamp)); s.append(",target="); s.append(target == null ? "[hidden]" : target); s.append(",reason="); s.append(reason == null ? "[hidden]" : reason); s.append(",details="); if (details instanceof Object[]) s.append(Arrays.asList((Object[])details)); // crude formatting hack else s.append(details); s.append("]"); return s.toString; }

/**        *  Compares this log entry to another one based on the recentness * of their timestamps. * @param other the log entry to compare * @return whether this object is equal to         *  @since 0.18 */       public int compareTo(Wiki.LogEntry other) {           if (timestamp.equals(other.timestamp)) return 0; // might not happen, but return timestamp.after(other.timestamp) ? 1 : -1;       }    }

/**    *  Represents a contribution and/or a revision to a page. * @since 0.17 */   public class Revision implements Comparable<Revision> {       private boolean minor; private String summary; private long revid, rcid = -1; private Calendar timestamp; private String user; private String title;

/**        *  Constructs a new Revision object. * @param revid the id of the revision (this is a long since         *   on en.wikipedia.org is now (November 2007) ~10%         *  of <tt>Integer.MAX_VALUE</tt>         *  @param timestamp when this revision was made         *  @param article the concerned article         *  @param summary the edit summary         *  @param user the user making this revision (may be anonymous, if not * use <tt>User.getUsername</tt>)         *  @param minor whether this was a minor edit         *  @since 0.17         */        protected Revision(long revid, Calendar timestamp, String title, String summary, String user, boolean minor)        {            this.revid = revid;            this.timestamp = timestamp;            this.summary = summary;            this.minor = minor;            this.user = user;			this.title = title;        }

/**        *  Fetches the contents of this revision. WARNING: fails if the * revision is deleted. * @return the contents of the appropriate article at <tt>timestamp</tt> * @throws IOException if a network error occurs * @throws IllegalArgumentException if page == Special:Log/xxx. * @since 0.17 */       public String getText throws IOException {           // logs have no content if (revid == -1L) throw new IllegalArgumentException("Log entries have no valid content!");

// go for it           String url = base + URLEncoder.encode(title, "UTF-8") + "&oldid=" + revid + "&action=raw"; String temp = fetch(url, "Revision.getText", false); log(Level.INFO, "Successfully retrieved text of revision " + revid, "Revision.getText"); return decode(temp); }

/**        *  Gets the rendered text of this revision. WARNING: fails if the * revision is deleted. * @return the rendered contents of the appropriate article at         *  <tt>timestamp</tt> * @throws IOException if a network error occurs * @throws IllegalArgumentException if page == Special:Log/xxx. * @since 0.17 */       public String getRenderedText throws IOException {           // logs have no content if (revid == -1L) throw new IllegalArgumentException("Log entries have no valid content!");

// go for it           String url = base + URLEncoder.encode(title, "UTF-8") + "&oldid=" + revid + "&action=render"; String temp = fetch(url, "Revision.getRenderedText", false); log(Level.INFO, "Successfully retrieved rendered text of revision " + revid, "Revision.getRenderedText"); return decode(temp); }

/**        *  Determines whether this Revision is the most recent revision of         *  the relevant page. *        *  @return see above * @throws IOException if a network error occurs * @since 0.17 */       public boolean isTop throws IOException {           String url = query + "action=query&prop=revisions&titles=" + URLEncoder.encode(title, "UTF-8") + "&rvlimit=1&rvprop=timestamp|ids"; String line = fetch(url, "Revision.isTop", false); // fetch the oldid int a = line.indexOf("revid=\"") + 7;           int b = line.indexOf("\"", a); long oldid2 = Long.parseLong(line.substring(a, b)); return revid == oldid2; }

/**        *  Returns a HTML rendered diff between this and the specified * revision. Such revisions should be on the same page. * @param other another revision on the same page. NEXT_REVISION, * PREVIOUS_REVISION and CURRENT_REVISION can be used here for obvious * effect. * @return the difference between this and the other revision. See * http://en.wikipedia.org/w/index.php?diff=343490272 for an example. * @throws IOException if a network error occurs * @since 0.21 */       public String diff(Revision other) throws IOException {           return diff(other.revid, ""); }

/**        *  Returns a HTML rendered diff between this revision and the given * text. Useful for emulating the "show changes" functionality. * @param text some wikitext * @return the difference between this and the the text provided * @throws IOException if a network error occurs * @since 0.21 */       public String diff(String text) throws IOException {           return diff(0L, text); }

/**        *  Fetches a HTML rendered diff. * @param oldid the id of another revision; (exclusive) or         *  @param text some wikitext to compare agains * @return a difference between oldid or text * @throws IOException if a network error occurs * @since 0.21 */       protected String diff(long oldid, String text) throws IOException {           // send via POST URLConnection connection = new URL(query).openConnection; logurl(query, "Revision.diff"); setCookies(connection, cookies); connection.setDoOutput(true); connection.connect; PrintWriter out = new PrintWriter(connection.getOutputStream); out.write("action=query&prop=revisions&revids=" + revid);

// no switch for longs? WTF? if (oldid == NEXT_REVISION) out.write("&rvdiffto=next"); else if (oldid == CURRENT_REVISION) out.write("&rvdiffto=cur"); else if (oldid == PREVIOUS_REVISION) out.write("&rvdiffto=previous"); else if (oldid == 0L) {               out.write("&rvdifftotext="); out.write(text); }           else {               out.write("&rvdiffto="); out.write("" + oldid); }           out.close;

// parse BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream), "UTF-8")); String line; StringBuilder diff = new StringBuilder(100000); while ((line = in.readLine) != null) {               int y = line.indexOf(">", line.indexOf("<diff")) + 1; int z = line.indexOf(" "); if (y != -1) {                   diff.append(line.substring(y + 6)); diff.append("\n"); }               else if (z != -1) {                   diff.append(line.substring(0, z)); diff.append("\n"); break; // done }               else {                   diff.append(line); diff.append("\n"); }           }            return decode(diff.toString); }

/**        *  Determines whether this Revision is equal to another object. * @param o an object * @return whether o is equal to this object * @since 0.17 */       public boolean equals(Object o)        { if (!(o instanceof Revision)) return false; return toString.equals(o.toString); }

/**        *  Returns a hash code of this revision. * @return a hash code * @since 0.17 */       public int hashCode {           return (int)revid * 2 - Wiki.this.hashCode; }

/**        *  Checks whether this edit was marked as minor. See * Help:Minor edit for details. *        *  @return whether this revision was marked as minor * @since 0.17 */       public boolean isMinor {           return minor; }

/**        *  Returns the edit summary for this revision. WARNING: returns null * if the summary was RevisionDeleted. * @return the edit summary * @since 0.17 */       public String getSummary {           return summary; }

/**        *  Returns the user or anon who created this revision. You should * pass this (if not an IP) to <tt>getUser(String)</tt> to obtain a         *  User object. WARNING: returns null if the user was RevisionDeleted. * @return the user or anon * @since 0.17 */       public String getUser {           return user; }

/**        *  Returns the page to which this revision was made. * @return the page * @since 0.17 */       public String getPage {           return title; }

/**        *  Returns the oldid of this revision. Don't confuse this with * <tt>rcid</tt> * @return the oldid (long) * @since 0.17 */       public long getRevid {           return revid; }

/**        *  Gets the time that this revision was made. * @return the timestamp * @since 0.17 */       public Calendar getTimestamp {           return timestamp; }

/**        *  Returns a string representation of this revision. * @return see above * @since 0.17 */       public String toString {           StringBuilder sb = new StringBuilder("Revision[oldid="); sb.append(revid); sb.append(",page=\"");           sb.append(title);            sb.append("\",user="); sb.append(user == null ? "[hidden]" : user); sb.append(",timestamp="); sb.append(calendarToTimestamp(timestamp)); sb.append(",summary=\"");           sb.append(summary == null ? "[hidden]" : summary);            sb.append("\",minor="); sb.append(minor); sb.append(",rcid="); sb.append(rcid == -1 ? "unset" : rcid); sb.append("]"); return sb.toString; }

/**        *  Compares this revision to another revision based on the recentness * of their timestamps. * @param other the revision to compare * @return whether this object is equal to         *  @since 0.18 */       public int compareTo(Wiki.Revision other) {           if (timestamp.equals(other.timestamp)) return 0; // might not happen, but return timestamp.after(other.timestamp) ? 1 : -1;       }

/**        *  Sets the <tt>rcid</tt> of this revision, used for patrolling. * This parameter is optional. I can't think of a good reason why * this should be publicly editable. * @param rcid the rcid of this revision (long) * @since 0.17 */       protected void setRcid(long rcid) {           this.rcid = rcid; }

/**        *  Gets the <tt>rcid</tt> of this revision for patrolling purposes. * @return the rcid of this revision (long) * @since 0.17 */       public long getRcid {           return rcid; }

/**        *  Reverts this revision using the rollback method. See * <tt>Wiki.rollback</tt>. * @throws IOException if a network error occurs * @throws CredentialNotFoundException if not logged in or user is not * an admin * @throws AccountLockedException if the user is blocked * @since 0.19 */       public void rollback throws IOException, LoginException {           Wiki.this.rollback(this, false, ""); }

/**        *  Reverts this revision using the rollback method. See * <tt>Wiki.rollback</tt>. * @param bot mark this and the reverted revision(s) as bot edits * @param reason (optional) a custom reason * @throws IOException if a network error occurs * @throws CredentialNotFoundException if not logged in or user is not * an admin * @throws AccountLockedException if the user is blocked * @since 0.19 */       public void rollback(boolean bot, String reason) throws IOException, LoginException {           Wiki.this.rollback(this, bot, reason); }   }

// INTERNALS

// miscellany

/**    *  A generic URL content fetcher. This is only useful for GET requests, * which is almost everything that doesn't modify the wiki. Might be    *  useful for subclasses. *    *  Here we also check the database lag and wait 30s if it exceeds * <tt>maxlag</tt>. See mw:Manual:Maxlag parameter for the server-side * analog (which isn't implemented here, because I'm too lazy to retry     *  the request). *    *  @param url the url to fetch * @param caller the caller of this method * @param write whether we need to fetch the cookies from this connection * in a token-fetching exercise (edit and friends) * @throws IOException if a network error occurs * @since 0.18 */   protected String fetch(String url, String caller, boolean write) throws IOException {       // check the database lag logurl(url, caller); do // this is just a dummy loop {           if (maxlag < 1) // disabled break; // only bother to check every 30 seconds if ((System.currentTimeMillis - lastlagcheck) < 30000) // TODO: this really should be a preference break;

try {               // if we use this, this can block unrelated read requests while we edit a page synchronized(domain) {                   // update counter. We do this before the actual check, so that only one thread does the check. lastlagcheck = System.currentTimeMillis; int lag = getCurrentDatabaseLag; while (lag > maxlag) {                       log(Level.WARNING, "Sleeping for 30s as current database lag exceeds the maximum allowed value of " + maxlag + " s", caller); Thread.sleep(30000); lag = getCurrentDatabaseLag; }               }            }            catch (InterruptedException ex) {               // nobody cares }       }        while (false);

// connect URLConnection connection = new URL(url).openConnection; setCookies(connection, cookies); connection.connect; BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream), "UTF-8"));

// get the cookies if (write) {           grabCookies(connection, cookies2); cookies2.putAll(cookies); }

// get the text String line; StringBuilder text = new StringBuilder(100000); while ((line = in.readLine) != null) {           text.append(line); text.append("\n"); }       in.close; return text.toString; }

/**    *  Checks for errors from standard read/write requests. * @param line the response from the server to analyze * @param caller what we tried to do     *  @throws AccountLockedException if the user is blocked * @throws HttpRetryException if the database is locked or action was * throttled and a retry failed * @throws UnknownError in the case of a MediaWiki bug * @since 0.18 */   protected void checkErrors(String line, String caller) throws IOException, LoginException {       // System.out.println(line); // empty response from server if (line.equals("")) throw new UnknownError("Received empty response from server!"); // successful if (line.contains("result=\"Success\"")) return; // rate limit (automatic retry), though might be a long one (e.g. email) if (line.contains("error code=\"ratelimited\"")) {           log(Level.WARNING, "Server-side throttle hit.", caller); throw new HttpRetryException("Action throttled.", 503); }       // blocked! if (line.contains("error code=\"blocked") || line.contains("error code=\"autoblocked\""))       {            log(Level.SEVERE, "Cannot " + caller + " - user is blocked!.", caller);            throw new AccountLockedException("Current user is blocked!");        }        // cascade protected        if (line.contains("error code=\"cascadeprotected\""))        {            log(Level.WARNING, "Cannot " + caller + " - page is subject to cascading protection.", caller);            throw new CredentialException("Page is cascade protected");        }        // database lock (automatic retry)        if (line.contains("error code=\"readonly\""))        {            log(Level.WARNING, "Database locked!", caller);            throw new HttpRetryException("Database locked!", 503);        }        // unknown error        if (line.contains("error code=\"unknownerror\""))            throw new UnknownError("Unknown MediaWiki API error, response was " + line); // generic (automatic retry) throw new IOException("MediaWiki error, response was " + line); }

/**    *  Strips entity references like &quot; from the supplied string. This * might be useful for subclasses. * @param in the string to remove URL encoding from * @return that string without URL encoding * @since 0.11 */   protected String decode(String in) {       // Remove entity references. Oddly enough, URLDecoder doesn't nuke these. in = in.replace("&lt;", "<").replace("&gt;", ">"); // html tags in = in.replace("&amp;", "&"); in = in.replace("&quot;", "\"");       in = in.replace("&#039;", "'");        return in;    }

/**    *  Finalizes the object on garbage collection. * @since 0.14 */   protected void finalize {       // I have no idea why this is called when we are still using // this Wiki object. Silly Java. //       Thread.dumpStack; //       logout; //       namespaces = null; }

// user rights methods

/**    *  Checks whether the currently logged on user has sufficient rights to     *  edit/move a protected page. *    *  @param level a protection level * @param move whether the action is a move * @return whether the user can perform the specified action * @throws IOException if we can't get the user rights * @throws AccountLockedException if user is blocked * @throws AssertionError if any defined assertions are false * @since 0.10 */   private boolean checkRights(int level, boolean move) throws IOException, AccountLockedException {       // admins can do anything, this also covers FULL_PROTECTION if ((user.userRights & ADMIN) == ADMIN) return true; switch (level) {           case NO_PROTECTION: return true; case SEMI_PROTECTION: return user != null; // not logged in => can't edit case MOVE_PROTECTION: case SEMI_AND_MOVE_PROTECTION: return !move; // fall through is OK: the user cannot move a protected page // cases PROTECTED_DELETED_PAGE and FULL_PROTECTION are unnecessary default: return false; }   }

/**    *  Performs a status check, including assertions. * @throws AssertionError if any assertions are false * @throws AccountLockedException if the user is blocked * @throws IOException if a network error occurs * @see #setAssertionMode * @since 0.11 */   protected void statusCheck throws IOException, AccountLockedException {       // @revised 0.18 was assertions, put some more stuff in here

// check if MediaWiki hasn't logged us out if (cookies != null && !cookies.containsValue(user.getUsername)) {           log(Level.WARNING, "Cookies expired", "statusCheck"); logout; }

// perform various status checks every 100 or so edits if (statuscounter > statusinterval) {           // purge user rights in case of desysop or loss of other priviliges if (user != null) user.userRights(false); // check for new messages if ((assertion & ASSERT_NO_MESSAGES) == ASSERT_NO_MESSAGES) assert !(hasNewMessages) : "User has new messages";

statuscounter = 0; }       else statuscounter++;

// do some more assertions if ((assertion & ASSERT_LOGGED_IN) == ASSERT_LOGGED_IN) assert (user != null) : "Not logged in"; if ((assertion & ASSERT_BOT) == ASSERT_BOT) assert (user.userRights & BOT) == BOT : "Not a bot"; }

// cookie methods

/**    *  Sets cookies to an unconnected URLConnection and enables gzip * compression of returned text. * @param u an unconnected URLConnection * @param map the cookie store */   private void setCookies(URLConnection u, Map<String, String> map) {       StringBuilder cookie = new StringBuilder(100); for (Map.Entry<String, String> entry : map.entrySet) {           cookie.append(entry.getKey); cookie.append("="); cookie.append(entry.getValue); cookie.append("; "); }       u.setRequestProperty("Cookie", cookie.toString);

// enable gzip compression u.setRequestProperty("Accept-encoding", "gzip"); u.setRequestProperty("User-Agent", useragent); }

/**    *  Grabs cookies from the URL connection provided. * @param u an unconnected URLConnection * @param map the cookie store */   private void grabCookies(URLConnection u, Map<String, String> map) {       // reset the cookie store map.clear; String headerName = null; for (int i = 1; (headerName = u.getHeaderFieldKey(i)) != null; i++) if (headerName.equals("Set-Cookie")) {               String cookie = u.getHeaderField(i);

// _session cookies are for cookies2, otherwise this causes problems if (cookie.contains("_session") && map == cookies) continue;

cookie = cookie.substring(0, cookie.indexOf(";")); String name = cookie.substring(0, cookie.indexOf("=")); String value = cookie.substring(cookie.indexOf("=") + 1, cookie.length); map.put(name, value); }   }

// logging methods

/**    *  Logs a successful result. * @param text string the string to log * @param method what we are currently doing * @param level the level to log at     *  @since 0.06 */   private void log(Level level, String text, String method) {       StringBuilder sb = new StringBuilder(100); sb.append('['); sb.append(domain); sb.append("] "); sb.append(text); sb.append('.'); logger.logp(level, "Wiki", method + "", sb.toString); }

/**    *  Logs a url fetch. * @param url the url we are fetching * @param method what we are currently doing * @since 0.08 */   private void logurl(String url, String method) {       logger.logp(Level.FINE, "Wiki", method + "", "Fetching URL " + url); }

// calendar/timestamp methods

/**    *  Turns a calendar into a timestamp of the format yyyymmddhhmmss. Might * be useful for subclasses. * @param c the calendar to convert * @return the converted calendar * @see #timestampToCalendar * @since 0.08 */   protected final String calendarToTimestamp(Calendar c)    { StringBuilder x = new StringBuilder; x.append(c.get(Calendar.YEAR)); int i = c.get(Calendar.MONTH) + 1; // January == 0! if (i < 10) x.append("0"); // add a zero if required x.append(i); i = c.get(Calendar.DATE); if (i < 10) x.append("0"); x.append(i); i = c.get(Calendar.HOUR_OF_DAY); if (i < 10) x.append("0"); x.append(i); i = c.get(Calendar.MINUTE); if (i < 10) x.append("0"); x.append(i); i = c.get(Calendar.SECOND); if (i < 10) x.append("0"); x.append(i); return x.toString; }

/**    *  Turns a timestamp of the format yyyymmddhhmmss into a Calendar object. * Might be useful for subclasses. *    *  @param timestamp the timestamp to convert * @return the converted Calendar * @see #calendarToTimestamp * @since 0.08 */   protected final Calendar timestampToCalendar(String timestamp) {       GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); int year = Integer.parseInt(timestamp.substring(0, 4)); int month = Integer.parseInt(timestamp.substring(4, 6)) - 1; // January == 0! int day = Integer.parseInt(timestamp.substring(6, 8)); int hour = Integer.parseInt(timestamp.substring(8, 10)); int minute = Integer.parseInt(timestamp.substring(10, 12)); int second = Integer.parseInt(timestamp.substring(12, 14)); calendar.set(year, month, day, hour, minute, second); return calendar; }

/**    *  Converts a timestamp of the form used by the API * (yyyy-mm-ddThh:mm:ssZ) to the form * yyyymmddhhmmss, which can be fed into <tt>timestampToCalendar</tt>. *    *  @param timestamp the timestamp to convert * @return the converted timestamp * @see #timestampToCalendar * @since 0.12 */   private String convertTimestamp(String timestamp) {       StringBuilder ts = new StringBuilder(timestamp.substring(0, 4)); ts.append(timestamp.substring(5, 7)); ts.append(timestamp.substring(8, 10)); ts.append(timestamp.substring(11, 13)); ts.append(timestamp.substring(14, 16)); ts.append(timestamp.substring(17, 19)); return ts.toString; }

// serialization

/**    *  Writes this wiki to a file. * @param out an ObjectOutputStream to write to     *  @throws IOException if there are local IO problems * @since 0.10 */   private void writeObject(ObjectOutputStream out) throws IOException {       out.writeObject(user.getUsername); out.writeObject(cookies); out.writeInt(throttle); out.writeInt(maxlag); out.writeInt(assertion); out.writeObject(scriptPath); out.writeObject(domain); out.writeObject(namespaces); out.write(statusinterval); out.writeObject(useragent); }

/**    *  Reads a copy of a wiki from a file. * @param in an ObjectInputStream to read from * @throws IOException if there are local IO problems * @throws ClassNotFoundException if we can't recognize the input * @since 0.10 */   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {       String z = (String)in.readObject; user = new User(z); cookies = (HashMap<String, String>)in.readObject; throttle = in.readInt; maxlag = in.readInt; assertion = in.readInt; scriptPath = (String)in.readObject; domain = (String)in.readObject; namespaces = (HashMap<String, Integer>)in.readObject; statusinterval = in.readInt; useragent = (String)in.readObject;

// various other intializations cookies2 = new HashMap<String, String>(10); base = "http://" + domain + scriptPath + "/index.php?title="; query = "http://" + domain + scriptPath + "/api.php?format=xml&";

// force a status check on next edit statuscounter = statusinterval; } }