package com.laytonsmith.tools.docgen; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.Web.CookieJar; import com.laytonsmith.PureUtilities.Web.HTTPMethod; import com.laytonsmith.PureUtilities.Web.HTTPResponse; import com.laytonsmith.PureUtilities.Web.RequestSettings; import com.laytonsmith.PureUtilities.Web.WebUtility; import com.laytonsmith.PureUtilities.XMLDocument; import com.laytonsmith.PureUtilities.ZipReader; import com.laytonsmith.annotations.api; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.functions.Function; import com.laytonsmith.core.functions.FunctionBase; import com.laytonsmith.core.functions.FunctionList; import com.laytonsmith.persistence.DataSourceException; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.xpath.XPathExpressionException; import org.xml.sax.SAXException; /** * * */ public class DocGenUIHandler { private final String pagePrefix = "<!-- This page is maintained automatically. If you would like to" + " make changes to it, the proper way to do this is to make a pull request for the plugin itself. -->\n\n"; public static class QuickStop extends RuntimeException{ // } public static interface ProgressManager{ void setProgress(Integer i); void setStatus(String status); } public static void main(String [] args){ DocGenUI.main(args); } private static final Map<String, List<String>> baseHeaders = new HashMap<String, List<String>>(); static{ baseHeaders.put("User-Agent", Arrays.asList(new String[]{"CommandHelper-DocUploader"})); } URL url; String username; String password; String prefix; String rootPath; boolean isStaged; boolean doFunctions; boolean doExamples; boolean doEvents; boolean doTemplates; int totalPages = 0; int atPage = 0; private ProgressManager progress; boolean stop = false; URL endpoint; public DocGenUIHandler(ProgressManager progress, URL url, String username, String password, String prefix, String rootPath, boolean isStaged, boolean doFunctions, boolean doExamples, boolean doEvents, boolean doTemplates) { this.progress = progress; this.url = url; this.username = username; this.password = password; this.prefix = prefix; this.rootPath = rootPath; this.isStaged = isStaged; this.doFunctions = doFunctions; this.doExamples = doExamples; this.doEvents = doEvents; this.doTemplates = doTemplates; if(!this.prefix.endsWith("/")){ this.prefix += "/"; } if(this.prefix.startsWith("/")){ this.prefix = this.prefix.substring(1); } } public void stop(){ stop = true; } public void checkStop(){ if(stop){ throw new QuickStop(); } } public void go() throws Exception{ try{ endpoint = new URL(url.toString() + "/w/api.php"); if(getPage(endpoint).getResponseCode() != 200){ throw new Exception("Unable to reach wiki API."); } if(doExamples){ testCompileExamples(); } doLogin(); //Now, gather up the page count, so we can set our progress bar correctly if(doFunctions){ totalPages += getFunctionCount(); } if(doExamples){ totalPages += getExampleCount(); } if(doEvents){ totalPages += getEventCount(); } if(doTemplates){ totalPages += getTemplateCount(); } totalPages += getMiscCount(); progress.setProgress(0); if(doFunctions){ doFunctions(); } if(doExamples){ doExamples(); } if(doEvents){ doEvents(); } if(doTemplates){ doTemplates(); } } finally{ progress.setProgress(0); } } private void doFunctions() throws XPathExpressionException, ConfigCompileException{ doUpload(DocGen.functions(DocGen.MarkupType.WIKI, api.Platforms.INTERPRETER_JAVA, isStaged), "/API", true); } private void doExamples() throws ConfigCompileException, XPathExpressionException, IOException, DataSourceException, Exception{ //So they are alphabetical, so we always have a consistent upload order, to //facilitate tracing problems. SortedSet<String> names = new TreeSet<String>(); for(FunctionBase base : FunctionList.getFunctionList(api.Platforms.INTERPRETER_JAVA)){ String name = base.getName(); if(base.appearInDocumentation()){ names.add(name); } } for(String name : names){ String docs = DocGen.examples(name, isStaged); doUpload(docs, "/API/" + name, true); } } private void doEvents() throws XPathExpressionException{ doUpload(DocGen.events(DocGen.MarkupType.WIKI), "/Event_API", true); } private void doTemplates() throws IOException, XPathExpressionException{ try { File root = new File(DocGenUIHandler.class.getResource("/docs").toURI()); ZipReader reader = new ZipReader(root); Queue<File> q = new LinkedList<File>(); q.addAll(Arrays.asList(reader.listFiles())); while(q.peek() != null){ ZipReader r = new ZipReader(q.poll()); if(r.isDirectory()){ q.addAll(Arrays.asList(r.listFiles())); } else { String articleName = "/" + r.getFile().getName(); doUpload(DocGen.Template(r.getFile().getName(), isStaged), articleName, true); } } } catch (URISyntaxException ex) { Logger.getLogger(DocGenUIHandler.class.getName()).log(Level.SEVERE, null, ex); } } /** * Uploads a file to a page. If protect is null, the protections are left as is. If protect is false, * protections are removed if they are present. If protect is true, protections are added if they are * not present. * @param wikiMarkup * @param page * @param protect * @throws XPathExpressionException */ void doUpload(String wikiMarkup, String page, Boolean protect) throws XPathExpressionException{ checkStop(); if(page.startsWith("/")){ //The prefix already has this page = page.substring(1); } wikiMarkup = pagePrefix + wikiMarkup; //The full path String fullPath = prefix + page; progress.setStatus("Uploading " + fullPath); //First we need to get the edit token XMLDocument content = getXML(endpoint, mapCreator( "action", "query", "titles", fullPath, "prop", "revisions", "rvprop", "sha1", "format", "xml" )); checkStop(); String sha1 = content.getNode("/api/query/pages/page/revisions/rev/@sha1"); String sha1local = getSha1(wikiMarkup); if(!sha1.equals(sha1local)){ XMLDocument query = getXML(endpoint, mapCreator( "action", "query", "titles", fullPath, "prop", "info", "intoken", "edit", "format", "xml" )); checkStop(); String edittoken = query.getNode("/api/query/pages/page/@edittoken"); XMLDocument edit = getXML(endpoint, mapCreator( "action", "edit", "title", fullPath, "text", wikiMarkup, "summary", "Automatic documentation update. (This is a bot edit)", "bot", "true", "format", "xml", //This must always come last "token", edittoken ), false); } if(protect != null){ XMLDocument query = getXML(endpoint, mapCreator( "action", "query", "titles", fullPath, "prop", "info", "inprop", "protection", "intoken", "protect", "format", "xml" )); checkStop(); String protectToken = query.getNode("/api/query/pages/page/@protecttoken"); boolean isProtectedEdit = false; boolean isProtectedMove = false; if(query.nodeExists("/api/query/pages/page/protection/pr")){ for(int i = 1; i <= query.countChildren("/api/query/pages/page/protection"); i++){ //If only sysops can edit and move if(query.getNode("/api/query/pages/page/protection/pr[" + i + "]/@level").equals("sysop") && query.getNode("/api/query/pages/page/protection/pr[" + i + "]/@type").equals("edit")){ isProtectedEdit = true; } if(query.getNode("/api/query/pages/page/protection/pr[" + i + "]/@level").equals("sysop") && query.getNode("/api/query/pages/page/protection/pr[" + i + "]/@type").equals("move")){ isProtectedMove = true; } } } boolean isProtected = false; if(isProtectedEdit && isProtectedMove){ isProtected = true; } if(protect && !isProtected){ //Protect it getXML(endpoint, mapCreator( "action", "protect", "title", fullPath, "token", protectToken, "protections", "edit=sysop|move=sysop", "expiry", "infinite", "reason", "Autoprotecting page (This is a bot edit)" )); } else if(!protect && isProtected){ //Unprotect it getXML(endpoint, mapCreator( "action", "protect", "title", fullPath, "token", protectToken, "protections", "edit=autoconfirmed|move=autoconfirmed", "expiry", "infinite", "reason", "Autoprotecting page (This is a bot edit)" )); } } incProgress(); } private void incProgress(){ atPage++; if(totalPages == 0){ progress.setProgress(null); } else { progress.setProgress((int)((float)atPage / (float)totalPages * 100.0)); } } private int getFunctionCount(){ return 1; } private int getExampleCount(){ return FunctionList.getFunctionList(api.Platforms.INTERPRETER_JAVA).size(); } private int getEventCount(){ return 1; } private int getTemplateCount() throws IOException{ try { int count = 0; ZipReader reader = new ZipReader(new File(DocGenUIHandler.class.getResource("/docs").toURI())); Queue<File> q = new LinkedList<File>(); q.addAll(Arrays.asList(reader.listFiles())); while(q.peek() != null){ ZipReader r = new ZipReader(q.poll()); if(r.isDirectory()){ q.addAll(Arrays.asList(r.listFiles())); } else { count++; } } return count; } catch (URISyntaxException ex) { throw new APIException(ex); } } private int getMiscCount(){ int total = 0; return total; } private void doLogin() throws MalformedURLException, XPathExpressionException{ checkStop(); XMLDocument login = getXML(endpoint, mapCreator( "format", "xml", "action", "login", "lgname", username, "lgpassword", password )); if("NeedToken".equals(login.getNode("/api/login/@result"))){ XMLDocument login2 = getXML(endpoint, mapCreator( "format", "xml", "action", "login", "lgname", username, "lgpassword", password, "lgtoken", login.getNode("/api/login/@token") )); if(!"Success".equals(login2.getNode("/api/login/@result"))){ if("WrongPass".equals(login2.getNode("/api/login/@result"))){ throw new APIException("Wrong password."); } throw new APIException("Could not log in successfully."); } } progress.setStatus("Logged in"); password = null; } public static class APIException extends RuntimeException{ public APIException(String message){ super(message); } public APIException(Throwable cause) { super("API responded incorrectly.", cause); } } private static String getSha1(String content){ try { MessageDigest digest = java.security.MessageDigest.getInstance("SHA1"); digest.update(content.getBytes()); String hash = StringUtils.toHex(digest.digest()).toLowerCase(); return hash; } catch (NoSuchAlgorithmException ex) { throw new RuntimeException("An error occured while trying to hash your data", ex); } } private static Map<String, String> mapCreator(String ... strings){ if(strings.length % 2 != 0){ throw new Error("Only an even number of parameters may be passed to mapCreator"); } Map<String, String> map = new HashMap<String, String>(); for(int i = 0; i < strings.length; i+=2){ map.put(strings[i], strings[i + 1]); } return map; } private static CookieJar cookieStash = new CookieJar(); private static XMLDocument getXML(URL url, Map<String, String> params) throws APIException{ return getXML(url, params, true); } private static XMLDocument getXML(URL url, Map<String, String> params, boolean useURL) throws APIException{ try{ XMLDocument doc = new XMLDocument(getPage(url, params, useURL).getContent()); if(doc.nodeExists("/api/error")){ //Doh. throw new APIException(doc.getNode("/api/error/@info")); } return doc; } catch(XPathExpressionException e){ throw new APIException(e); } catch(SAXException e){ throw new APIException(e); } catch(IOException e){ throw new APIException(e); } } private static HTTPResponse getPage(URL url) throws IOException{ return getPage(url, null); } private static HTTPResponse getPage(URL url, Map<String, String> params) throws IOException{ return getPage(url, params, false); } /** * The wiki apparently wants the parameters in the URL, but the method set to post. If useURL is * true, it will merge the params into the url. * @param url * @param params * @param useURL * @return * @throws IOException */ private static HTTPResponse getPage(URL url, Map<String, String> params, boolean useURL) throws IOException{ Map<String, List<String>> headers = new HashMap<String, List<String>>(baseHeaders); if (params != null && !params.isEmpty() && useURL) { StringBuilder b = new StringBuilder(url.getQuery() == null ? "" : url.getQuery()); if (b.length() != 0) { b.append("&"); } RequestSettings temp = new RequestSettings().setParameters(params); b.append(WebUtility.encodeParameters(temp.getParameters())); String query = b.toString(); url = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath() + "?" + query); } headers.put("Host", Arrays.asList(new String[]{url.getHost()})); RequestSettings settings = new RequestSettings().setMethod(HTTPMethod.POST).setHeaders(headers) .setParameters(params).setCookieJar(cookieStash).setFollowRedirects(true); return WebUtility.GetPage(url, settings); } public static void testCompileExamples(){ for(FunctionBase fb : FunctionList.getFunctionList(api.Platforms.INTERPRETER_JAVA)){ if(fb instanceof Function){ Function f = (Function)fb; try{ f.examples(); } catch(ConfigCompileException e){ throw new RuntimeException("Compilation error while compiling examples for " + f.getName(), e); } } } } }