package dw.xmlrpc; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import dw.xmlrpc.exception.DokuException; import dw.xmlrpc.exception.DokuIncompatibleVersionException; import dw.xmlrpc.exception.DokuMethodDoesNotExistsException; import dw.xmlrpc.exception.DokuMisConfiguredWikiException; import dw.xmlrpc.exception.DokuNoChangesException; import dw.xmlrpc.exception.DokuPageDoesNotExistException; /** * Main public class to actually make an xmlrpc query * * Instantiate one such client for a given wiki and a given user, then make * xmlrpc query using its methods. * * Most methods may throw DokuException because many things can go wrong * (bad url, wrong credential, no network, unreachable server, ...), so you may * want to make sure you handle them correcty */ public class DokuJClient { private final CoreClient _client; private final Locker _locker; private final Attacher _attacher; private Logger _logger; private final String COOKIE_PREFIX = "DW"; /** * Let override the default Logger */ public void setLogger(Logger logger){ _logger = logger; _client.setLogger(logger); } /** * Instantiate a client for the given user on the given wiki * * The wiki should be configured in a way to let this user access the * xmlrpc interface * * @param url Should looks like http[s]://server/mywiki/lib/exe/xmlrpc.php * @param user Login of the user * @param password Password of the user * @throws MalformedURLException * @throws DokuException */ public DokuJClient(String url, String user, String password) throws MalformedURLException, DokuException{ this(url); loginWithRetry(user, password, 2); } /** * Instantiate a client for an anonymous user on the given wiki * * Likely to be unsuitable for most wiki since anonymous user are often * not authorized to use the xmlrpc interface * * @param url Should looks like http[s]://server/mywiki/lib/exe/xmlrpc.php * @throws MalformedURLException */ public DokuJClient(String url) throws MalformedURLException{ this(CoreClientFactory.build(url)); } public DokuJClient(DokuJClientConfig dokuConfig) throws DokuException{ this(CoreClientFactory.build(dokuConfig)); if ( dokuConfig.user() != null){ loginWithRetry(dokuConfig.user(), dokuConfig.password(), 2); } } private DokuJClient(CoreClient client){ _client = client; _locker = new Locker(_client); _attacher = new Attacher(_client); Logger logger = Logger.getLogger(DokuJClient.class.toString()); setLogger(logger); } public boolean hasDokuwikiCookies(){ for(String cookieKey : cookies().keySet()){ if ( cookieKey.startsWith(COOKIE_PREFIX) ){ return true; } } return false; } public Map<String, String> cookies(){ return _client.cookies(); } //Because it's been observed that some hosting services sometime mess up a bit with cookies... private void loginWithRetry(String user, String password, int nbMaxRetry) throws DokuException { boolean success = false; for(int retry=0 ; retry < nbMaxRetry && !success ; retry++ ){ success = login(user, password); } } public Boolean login(String user, String password) throws DokuException{ Object[] params = new Object[]{user, password}; return (Boolean) genericQuery("dokuwiki.login", params) && hasDokuwikiCookies(); } /** * Uploads a file to the wiki * * @param attachmentId Id the file should have once uploaded (eg: ns1:ns2:myfile.gif) * @param localPath The path to the file to upload * @param overwrite TRUE to overwrite if a file with this id already exist on the wiki * @throws IOException * @throws DokuException */ public void putAttachment(String attachmentId, String localPath, boolean overwrite) throws IOException, DokuException{ putAttachment(attachmentId, new File(localPath), overwrite); } /** * Uploads a file to the wiki * * @param attachmentId Id the file should have once uploaded (eg: ns1:ns2:myfile.gif) * @param localFile The file to upload * @param overwrite TRUE to overwrite if a file with this id already exist on the wiki * @throws IOException * @throws DokuException */ public void putAttachment(String attachmentId, File localFile, boolean overwrite) throws IOException, DokuException{ putAttachment(attachmentId, _attacher.serializeFile(localFile), overwrite); } /** * Uploads a file to the wiki * * @param attachmentId Id the file should have once uploaded (eg: ns1:ns2:myfile.gif) * @param localFile base64 encoded file * @param overwrite TRUE to overwrite if a file with this id already exist on the wiki * @throws IOException * @throws DokuException */ public void putAttachment(String attachmentId, byte[] localFile, boolean overwrite) throws DokuException{ _attacher.putAttachment(attachmentId, localFile, overwrite); } /** * Returns information about a media file * * @param fileId Id of the file on the wiki (eg: ns1:ns2:myfile.gif) * @throws DokuException */ public AttachmentInfo getAttachmentInfo(String fileId) throws DokuException{ return _attacher.getAttachmentInfo(fileId); } /** * Deletes a file. Fails if the file is still referenced from any page in the wiki. * * @param fileId Id of the file on the wiki (eg: ns1:ns2:myfile.gif) * @throws DokuException */ public void deleteAttachment(String fileId) throws DokuException{ _attacher.deleteAttachment(fileId); } /** * Let download a file from the wiki * * @param fileId Id of the file on the wiki (eg: ns1:ns2:myfile.gif) * @param localPath Where to put the file * @throws DokuException * @throws IOException */ public File getAttachment(String fileId, String localPath) throws DokuException, IOException{ byte[] b = getAttachment(fileId); File f = new File(localPath); _attacher.deserializeFile(b, f); return f; } /** * Let download a file from the wiki * * @param fileId Id of the file on the wiki (eg: ns1:ns2:myfile.gif) * @throws DokuException * @return the data of the file, encoded in base64 */ public byte[] getAttachment(String fileId) throws DokuException { return _attacher.getAttachment(fileId); } /** * Returns information about a list of media files in a given namespace * * @param namespace Where to look for files * @throws DokuException */ public List<AttachmentDetails> getAttachments(String namespace) throws DokuException{ return getAttachments(namespace, null); } /** * Returns information about a list of media files in a given namespace * * @param namespace Where to look for files * @param additionalParams Potential additional parameters directly sent to Dokuwiki. * Available parameters are: * * recursive: TRUE if also files in subnamespaces are to be included, defaults to FALSE * * pattern: an optional PREG compatible regex which has to match the file id * @throws DokuException */ public List<AttachmentDetails> getAttachments(String namespace, Map<String, Object> additionalParams) throws DokuException{ return _attacher.getAttachments(namespace, additionalParams); } /** * Returns a list of recent changed media since given timestamp * @param timestamp * @throws DokuException */ public List<MediaChange> getRecentMediaChanges(Integer timestamp) throws DokuException{ return _attacher.getRecentMediaChanges(timestamp); } /** * Wrapper around {@link #getRecentMediaChanges(Integer)} * @param date Do not return changes older than this date */ public List<MediaChange> getRecentMediaChanges(Date date) throws DokuException { return getRecentMediaChanges((int)(date.getTime() / 1000)); } /** * Returns the current time at the remote wiki server as Unix timestamp * @throws DokuException */ public Integer getTime() throws DokuException{ return (Integer) genericQuery("dokuwiki.getTime"); } /** * Returns the XML RPC interface version of the remote Wiki. * This is DokuWiki implementation specific and independent of the supported * standard API version returned by wiki.getRPCVersionSupported * @throws DokuException */ public Integer getXMLRPCAPIVersion() throws DokuException{ return (Integer) genericQuery("dokuwiki.getXMLRPCAPIVersion"); } /** * Returns the DokuWiki version of the remote Wiki * @throws DokuException */ public String getVersion() throws DokuException{ return (String) genericQuery("dokuwiki.getVersion"); } /** * Returns the available versions of a Wiki page. * * The number of pages in the result is controlled via the "recent" configuration setting of the wiki. * * @param pageId Id of the page (eg: ns1:ns2:mypage) * @throws DokuException */ public List<PageVersion> getPageVersions(String pageId) throws DokuException { return getPageVersions(pageId, 0); } /** * Returns the available versions of a Wiki page. * * The number of pages in the result is controlled via the recent configuration setting of the wiki. * * @param pageId Id of the page (eg: ns1:ns2:mypage) * @param offset Can be used to list earlier versions in the history. * @throws DokuException */ public List<PageVersion> getPageVersions(String pageId, Integer offset) throws DokuException { boolean useFixForLegacyWiki = false; if ( offset > 0 ){ if ( getXMLRPCAPIVersion() < 10 ){ useFixForLegacyWiki = true; offset--; } } Object[] params = new Object[]{pageId, offset}; Object[] result = (Object[]) genericQuery("wiki.getPageVersions", params); if ( useFixForLegacyWiki && result.length > 1 ){ result = Arrays.copyOfRange(result, 1, result.length); } return ObjectConverter.toPageVersion(result, pageId); } /** * Returns the raw Wiki text for a specific revision of a Wiki page. * @param pageId Id of the page (eg: ns1:ns2:mypage) * @param timestamp Version of the page * @throws DokuException */ public String getPageVersion(String pageId, Integer timestamp) throws DokuException{ Object[]params = new Object[]{pageId, timestamp}; return (String) genericQuery("wiki.getPageVersion", params); } /** * Lists all pages within a given namespace * @param namespace Namespace to look for (eg: ns1:ns2) * @throws DokuException */ public List<PageDW> getPagelist(String namespace) throws DokuException { return getPagelist(namespace, null); } /** * Lists all pages within a given namespace * @param namespace Namespace to look for (eg: ns1:ns2) * @param options Options passed directly to dokuwiki's search_all_pages() * @throws DokuException */ public List<PageDW> getPagelist(String namespace, Map<String, Object> options) throws DokuException { List<Object> params = new ArrayList<Object>(); params.add(namespace); params.add(ensureWeComputeThePageHash(options)); Object result = genericQuery("dokuwiki.getPagelist", params.toArray()); return ObjectConverter.toPageDW((Object[]) result); } private Map<String, Object> ensureWeComputeThePageHash(Map<String, Object> initialOptions){ Map<String, Object> result; if ( initialOptions == null ){ result = new HashMap<String, Object>(); } else { result = new HashMap<String, Object>(initialOptions); } if ( !result.containsKey("hash") ){ result.put("hash", true); } return result; } /** * Returns the permission of the given wikipage. * @param pageId Id of the page (eg: ns1:ns2:mypage) * @throws DokuException */ public Integer aclCheck(String pageId) throws DokuException{ Object res = _client.genericQuery("wiki.aclCheck", pageId); return ObjectConverter.toPerms(res); } /** * Returns the supported RPC API version * * cf http://www.jspwiki.org/wiki/WikiRPCInterface2 for more info * @throws DokuException */ public Integer getRPCVersionSupported() throws DokuException{ return (Integer) genericQuery("wiki.getRPCVersionSupported"); } /** * Allows to lock or unlock a whole bunch of pages at once. * Useful when you are about to do a operation over multiple pages * @param pagesToLock Ids of pages to lock * @param pagesToUnlock Ids of pages to unlock * @throws DokuException */ public LockResult setLocks(List<String> pagesToLock, List<String> pagesToUnlock) throws DokuException{ return _locker.setLocks(pagesToLock, pagesToUnlock); } /** * Lock a page * @param pageId Id of the page to lock (eg: ns1:ns2:mypage) * @return TRUE the page has been successfully locked, FALSE otherwise * @throws DokuException */ public boolean lock(String pageId) throws DokuException{ return _locker.lock(pageId).locked().contains(pageId); } /** * Unlock a page * @param pageId Id of the page to unlock (eg: ns1:ns2:mypage) * @return TRUE the page has been successfully unlocked, FALSE otherwise * @throws DokuException */ public boolean unlock(String pageId) throws DokuException{ return _locker.unlock(pageId).unlocked().contains(pageId); } /** * Returns the title of the wiki * @throws DokuException */ public String getTitle() throws DokuException{ return (String) genericQuery("dokuwiki.getTitle"); } /** * Appends text to a Wiki Page. * @param pageId Id of the page to edit (eg: ns1:ns2:mypage) * @param rawWikiText Text to add to the current page content * @throws DokuException */ public void appendPage(String pageId, String rawWikiText) throws DokuException { appendPage(pageId, rawWikiText, null); } /** * Appends text to a Wiki Page. * @param pageId Id of the page to edit (eg: ns1:ns2:mypage) * @param rawWikiText Text to add to the current page content * @param summary A summary of the modification * @param minor Whether it's a minor modification * @throws DokuException */ public void appendPage(String pageId, String rawWikiText, String summary, boolean minor) throws DokuException { Map<String, Object> options = new HashMap<String, Object>(); options.put("sum", summary); options.put("minor", minor); appendPage(pageId, rawWikiText, options); } /** * Appends text to a Wiki Page. * @param pageId Id of the page to edit (eg: ns1:ns2:mypage) * @param rawWikiText Text to add to the current page content * @param options Options passed to Dokuwiki. ie: 'sum' and/or 'minor' * @throws DokuException */ public void appendPage(String pageId, String rawWikiText, Map<String, Object> options) throws DokuException { if ( options == null ){ options = new HashMap<String, Object>(); } genericQuery("dokuwiki.appendPage", new Object[]{pageId, rawWikiText, options}); } /** * Returns the raw Wiki text for a page * @param pageId Id of the page to fetch (eg: ns1:ns2:mypage) * @throws DokuException */ public String getPage(String pageId) throws DokuException { return (String) genericQuery("wiki.getPage", pageId); } /** * Saves a Wiki Page * @param pageId Id of the page to save * @param rawWikiText Text to put * @throws DokuException */ public void putPage(String pageId, String rawWikiText)throws DokuException { putPage(pageId, rawWikiText, null); } /** * Saves a Wiki Page * @param pageId Id of the page to save * @param rawWikiText Text to put * @param summary Summary of the edition * @param minor Whether it's a minor edition * @throws DokuException */ public void putPage(String pageId, String rawWikiText, String summary, boolean minor) throws DokuException{ Map<String, Object> options = new HashMap<String, Object>(); options.put("sum", summary); options.put("minor", minor); putPage(pageId, rawWikiText, options); } /** * Saves a Wiki Page * @param pageId Id of the page to save * @param rawWikiText Text to put * @param options Options passed to Dokuwiki. ie: 'sum' and/or 'minor' * @throws DokuException */ public void putPage(String pageId, String rawWikiText, Map<String, Object> options)throws DokuException { if (options == null){ options = new HashMap<String, Object>(); } genericQuery("wiki.putPage", new Object[]{pageId, rawWikiText, options}); } /** * Performs a fulltext search * @param pattern A query string as described on https://www.dokuwiki.org/search * @return Matching pages. Snippets are provided for the first 15 results. * @throws DokuException */ public List<SearchResult> search(String pattern) throws DokuException{ Object[] results = (Object[]) genericQuery("dokuwiki.search", pattern); return ObjectConverter.toSearchResult(results); } /** * Returns information about a Wiki page * @param pageId Id of the page wanted (eg: ns1:ns2:mypage) * @throws DokuException */ public PageInfo getPageInfo(String pageId) throws DokuException{ try { Object result = genericQuery("wiki.getPageInfo",pageId); return ObjectConverter.toPageInfo(result); } catch(DokuMisConfiguredWikiException e){ //Because "Adora Belle" (DW-2013-05-10) seems to have a bug with this command when the page doesn't exist if ( ! isConfiguredToAcceptXmlRpcQueries() ){ throw e; } throw new DokuPageDoesNotExistException(null); } } /** * Returns information about a specific version of a Wiki page * @param pageId Id of the page wanted(eg: ns1:ns2:mypage) * @param timestamp version wanted * @throws DokuException */ public PageInfo getPageInfoVersion(String pageId, Integer timestamp) throws DokuException { Object[] params = new Object[]{pageId, timestamp}; Object result = genericQuery("wiki.getPageInfoVersion", params); return ObjectConverter.toPageInfo(result); } /** * Returns a list of all Wiki pages in the remote Wiki * @throws DokuException */ public List<Page> getAllPages() throws DokuException { Object result = genericQuery("wiki.getAllPages"); return ObjectConverter.toPage((Object[]) result); } /** * Returns a list of backlinks of a Wiki page * @param pageId Id of the page wanted (eg: ns1:ns2:mypage) * @throws DokuException */ public List<String> getBackLinks(String pageId) throws DokuException{ Object result = genericQuery("wiki.getBackLinks", pageId); return ObjectConverter.toString((Object[]) result); } /** * Returns the rendered XHTML body of a Wiki page * @param pageId Id of the wanted page (eg: ns1:ns2:mypage) * @throws DokuException */ public String getPageHTML(String pageId) throws DokuException { return (String) genericQuery("wiki.getPageHTML", pageId); } /** * Returns the rendered HTML of a specific version of a Wiki page * @param pageId Id of the wanted page (eg: ns1:ns2:mypage) * @param timestamp Version wanted * @throws DokuException */ public String getPageHTMLVersion(String pageId, Integer timestamp) throws DokuException{ Object[] params = new Object[]{pageId, timestamp}; return (String) genericQuery("wiki.getPageHTMLVersion", params); } /** * Returns a list of all links contained in a Wiki page * @param pageId Id of the wanted page (eg: ns1:ns2:mypage) * @throws DokuException */ public List<LinkInfo> listLinks(String pageId) throws DokuException { Object result = genericQuery("wiki.listLinks", pageId); return ObjectConverter.toLinkInfo((Object[]) result); } /** * Returns a list of recent changes since a given timestamp * * According to Dokuwiki documentation (https://www.dokuwiki.org/recent_changes): * * * Only the most recent change for each page is listed, regardless of how many times that page was changed. * * The number of changes shown per page is controlled by the "recent" setting. * * Users are only shown pages to which they have read access * * @param timestamp Do not return changes older than this timestamp * @throws DokuException */ public List<PageChange> getRecentChanges(Integer timestamp) throws DokuException{ Object result; try { result = genericQuery("wiki.getRecentChanges", timestamp); } catch (DokuNoChangesException e){ return new ArrayList<PageChange>(); } Object[] pageChanges; try { pageChanges = (Object[]) result; } catch (ClassCastException e){ //It likely happens when there are no changes, with only a few versions of DW //(newer versions yield a DokuNoChangesException instead) //Hence it might be enough to just return an empty list... but in doubt I'd rather cast @SuppressWarnings("unchecked") Map<String, Object> pageChangesMap = (Map<String, Object>) result; pageChanges = pageChangesMap.values().toArray(); } return ObjectConverter.toPageChange(pageChanges); } /** * Wrapper around {@link #getRecentChanges(Integer)} * @param date Do not return changes older than this date */ public List<PageChange> getRecentChanges(Date date) throws DokuException { return getRecentChanges((int)(date.getTime() / 1000)); } /** * Tries to logoff by expiring auth cookies and the associated PHP session */ public void logoff() throws DokuException { try { _client.genericQuery("dokuwiki.logoff"); } catch(DokuMethodDoesNotExistsException e){ if (_logger != null){ _logger.log(Level.WARNING, "This Dokuwiki instance doesn't support 'logoff' (api version < 9). " + "We're clearing the cookies of this client, but we can't destroy the server side php session"); } } _client.clearCookies(); } /** * Only available for dokuwiki-2013-12-08 (Binky) or newer */ public boolean addAcl(String scope, String username, int permission) throws DokuException{ try { return (Boolean) genericQuery("plugin.acl.addAcl", new Object[]{scope, username, permission}); } catch (DokuMethodDoesNotExistsException e){ throw new DokuIncompatibleVersionException("dokuwiki-2013-12-08 (Binky)"); } } /** * Only available for dokuwiki-2013-12-08 (Binky) or newer */ public boolean delAcl(String scope, String username) throws DokuException{ try { return (Boolean) genericQuery("plugin.acl.delAcl", new Object[]{scope, username}); } catch (DokuMethodDoesNotExistsException e){ throw new DokuIncompatibleVersionException("dokuwiki-2013-12-08 (Binky)"); } } /** * Let execute any xmlrpc query without argument * @param action The name of the xmlrpc method to invoke * @return Whatever the xmlrpc should return, as an Object * @throws DokuException */ public Object genericQuery(String action) throws DokuException { return _client.genericQuery(action); } /** * Let execute any xmlrpc query with one argument * @param action The name of the xmlrpc method to invoke * @param param The unique parameter, as an Object * @return Whatever the xmlrpc should return, as an Object * @throws DokuException */ public Object genericQuery(String action, Object param) throws DokuException{ return _client.genericQuery(action, param); } /** * Let execute any xmlrpc query with an arbitrary number of arguments * @param action The name of the xmlrpc method to invoke * @param params The parameters, as an array of Objects * @return Whatever the xmlrpc should return, as an Object * @throws DokuException */ public Object genericQuery(String action, Object[] params) throws DokuException{ return _client.genericQuery(action, params); } private boolean isConfiguredToAcceptXmlRpcQueries() throws DokuException{ try { getTitle(); } catch(DokuMisConfiguredWikiException e){ return false; } return true; } }