* Copyright (C) 2004, 2005 $ThePhpWikiProgrammingTeam * * LICENCE * ======= * This file is part of PhpWiki. * * PhpWiki 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 2 of the License, or * (at your option) any later version. * * PhpWiki 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 PhpWiki; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * LIBRARY USED - POSSIBLE PROBLEMS * ================================ * * This file provides an XML-RPC interface for PhpWiki. * It checks for the existence of the xmlrpc-epi c library by Dan Libby * (see http://uk2.php.net/manual/en/ref.xmlrpc.php), and falls back to * the slower PHP counterpart XML-RPC library by Edd Dumbill. * See http://xmlrpc.usefulinc.com/php.html for details. * * INTERFACE SPECIFICTION * ====================== * * The interface specification is that discussed at * http://www.ecyrd.com/JSPWiki/Wiki.jsp?page=WikiRPCInterface * * See also http://www.usemod.com/cgi-bin/mb.pl?XmlRpc * or http://www.devshed.com/c/a/PHP/Using-XMLRPC-with-PHP/ * * Note: All XMLRPC methods are automatically prefixed with "wiki." * eg. "wiki.getAllPages" */ /* ToDo: Remove all warnings from xmlrpc.inc Return list of external links in listLinks Support RSS2 cloud subscription Done: Test hwiki.jar xmlrpc interface (java visualization plugin) Make use of the xmlrpc extension if found. http://xmlrpc-epi.sourceforge.net/ Resolved namespace conflicts */ // Intercept GET requests from confused users. Only POST is allowed here! // There is some indication that $HTTP_SERVER_VARS is deprecated in php > 4.1.0 // in favour of $_Server, but as far as I know, it still works. if ($GLOBALS['HTTP_SERVER_VARS']['REQUEST_METHOD'] != "POST") { die('This is the address of the XML-RPC interface.' . ' You must use XML-RPC calls to access information here'); } // All these global declarations make it so that this file // (XmlRpcServer.php) can be included within a function body // (not in global scope), and things will still work.... global $xmlrpcI4, $xmlrpcInt, $xmlrpcBoolean, $xmlrpcDouble, $xmlrpcString; global $xmlrpcDateTime, $xmlrpcBase64, $xmlrpcArray, $xmlrpcStruct; global $xmlrpcTypes; global $xmlEntities; global $xmlrpcerr, $xmlrpcstr; global $xmlrpc_defencoding; global $xmlrpcName, $xmlrpcVersion; global $xmlrpcerruser, $xmlrpcerrxml; global $xmlrpc_backslash; global $_xh; if (loadPhpExtension('xmlrpc')) { // fast c lib define('XMLRPC_EXT_LOADED', true); global $xmlrpc_util_path; $xmlrpc_util_path = dirname(__FILE__)."/XMLRPC/"; include_once("lib/XMLRPC/xmlrpc_emu.inc"); global $_xmlrpcs_debug; include_once("lib/XMLRPC/xmlrpcs_emu.inc"); } else { // slow php lib define('XMLRPC_EXT_LOADED', true); // Include the php XML-RPC library include_once("lib/XMLRPC/xmlrpc.inc"); global $_xmlrpcs_dmap; global $_xmlrpcs_debug; include_once("lib/XMLRPC/xmlrpcs.inc"); } // API version define ("WIKI_XMLRPC_VERSION", 2); /** * Helper function: Looks up a page revision (most recent by default) in the wiki database * * @param xmlrpcmsg $params : string pagename [int version] * @return WikiDB _PageRevision object, or false if no such page */ function _getPageRevision ($params) { global $request; $ParamPageName = $params->getParam(0); $ParamVersion = $params->getParam(1); $pagename = short_string_decode($ParamPageName->scalarval()); $version = ($ParamVersion) ? ($ParamVersion->scalarval()):(0); // FIXME: test for version <=0 ?? $dbh = $request->getDbh(); if ($dbh->isWikiPage($pagename)) { $page = $dbh->getPage($pagename); if (!$version) { $revision = $page->getCurrentRevision(); } else { $revision = $page->getRevision($version); } return $revision; } return false; } /* * Helper functions for encoding/decoding strings. * * According to WikiRPC spec, all returned strings take one of either * two forms. Short strings (page names, and authors) are converted to * UTF-8, then rawurlencode()d, and returned as XML-RPC strings. * Long strings (page content) are converted to UTF-8 then returned as * XML-RPC base64 binary objects. */ /** * Urlencode ASCII control characters. * * (And control characters...) * * @param string $str * @return string * @see urlencode */ function UrlencodeControlCharacters($str) { return preg_replace('/([\x00-\x1F])/e', "urlencode('\\1')", $str); } /** * Convert a short string (page name, author) to xmlrpcval. */ function short_string ($str) { return new xmlrpcval(UrlencodeControlCharacters(utf8_encode($str)), 'string'); } /** * Convert a large string (page content) to xmlrpcval. */ function long_string ($str) { return new xmlrpcval(utf8_encode($str), 'base64'); } /** * Decode a short string (e.g. page name) */ function short_string_decode ($str) { return utf8_decode(urldecode($str)); } /** * Get an xmlrpc "No such page" error message */ function NoSuchPage ($pagename='') { global $xmlrpcerruser; return new xmlrpcresp(0, $xmlrpcerruser + 1, "No such page ".$pagename); } // **************************************************************************** // Main API functions follow // **************************************************************************** global $wiki_dmap; /** * int getRPCVersionSupported(): Returns 1 for this version of the API */ $wiki_dmap['getRPCVersionSupported'] = array('signature' => array(array($xmlrpcInt)), 'documentation' => 'Get the version of the wiki API', 'function' => 'getRPCVersionSupported'); // The function must be a function in the global scope which services the XML-RPC // method. function getRPCVersionSupported($params) { return new xmlrpcresp(new xmlrpcval((integer)WIKI_XMLRPC_VERSION, "int")); } /** * array getRecentChanges(Date timestamp) : Get list of changed pages since * timestamp, which should be in UTC. The result is an array, where each element * is a struct: * name (string) : Name of the page. The name is UTF-8 with URL encoding to make it ASCII. * lastModified (date) : Date of last modification, in UTC. * author (string) : Name of the author (if available). Again, name is UTF-8 with URL encoding. * version (int) : Current version. * A page MAY be specified multiple times. A page MAY NOT be specified multiple * times with the same modification date. */ $wiki_dmap['getRecentChanges'] = array('signature' => array(array($xmlrpcArray, $xmlrpcDateTime)), 'documentation' => 'Get a list of changed pages since [timestamp]', 'function' => 'getRecentChanges'); function getRecentChanges($params) { global $request; // Get the first parameter as an ISO 8601 date. Assume UTC $encoded_date = $params->getParam(0); $datetime = iso8601_decode($encoded_date->scalarval(), 1); $dbh = $request->getDbh(); $pages = array(); $iterator = $dbh->mostRecent(array('since' => $datetime)); while ($page = $iterator->next()) { // $page contains a WikiDB_PageRevision object // no need to url encode $name, because it is already stored in that format ??? $name = short_string($page->getPageName()); $lastmodified = new xmlrpcval(iso8601_encode($page->get('mtime')), "dateTime.iso8601"); $author = short_string($page->get('author')); $version = new xmlrpcval($page->getVersion(), 'int'); // Build an array of xmlrpc structs $pages[] = new xmlrpcval(array('name' => $name, 'lastModified' => $lastmodified, 'author' => $author, 'version' => $version), 'struct'); } return new xmlrpcresp(new xmlrpcval($pages, "array")); } /** * base64 getPage( String pagename ): Get the raw Wiki text of page, latest version. * Page name must be UTF-8, with URL encoding. Returned value is a binary object, * with UTF-8 encoded page data. */ $wiki_dmap['getPage'] = array('signature' => array(array($xmlrpcBase64, $xmlrpcString)), 'documentation' => 'Get the raw Wiki text of the current version of a page', 'function' => 'getPage'); function getPage($params) { $revision = _getPageRevision($params); if (! $revision ) { $ParamPageName = $params->getParam(0); $pagename = short_string_decode($ParamPageName->scalarval()); return NoSuchPage($pagename); } return new xmlrpcresp(long_string($revision->getPackedContent())); } /** * base64 getPageVersion( String pagename, int version ): Get the raw Wiki text of page. * Returns UTF-8, expects UTF-8 with URL encoding. */ $wiki_dmap['getPageVersion'] = array('signature' => array(array($xmlrpcBase64, $xmlrpcString, $xmlrpcInt)), 'documentation' => 'Get the raw Wiki text of a page version', 'function' => 'getPageVersion'); function getPageVersion($params) { // error checking is done in getPage return getPage($params); } /** * base64 getPageHTML( String pagename ): Return page in rendered HTML. * Returns UTF-8, expects UTF-8 with URL encoding. */ $wiki_dmap['getPageHTML'] = array('signature' => array(array($xmlrpcBase64, $xmlrpcString)), 'documentation' => 'Get the current version of a page rendered in HTML', 'function' => 'getPageHTML'); function getPageHTML($params) { $revision = _getPageRevision($params); if (!$revision) return NoSuchPage(); $content = $revision->getTransformedContent(); $html = $content->asXML(); // HACK: Get rid of outer
if (preg_match('/^\s*
/', $html, $m1) && preg_match('@
\s*$@', $html, $m2)) { $html = substr($html, strlen($m1[0]), -strlen($m2[0])); } return new xmlrpcresp(long_string($html)); } /** * base64 getPageHTMLVersion( String pagename, int version ): Return page in rendered HTML, UTF-8. */ $wiki_dmap['getPageHTMLVersion'] = array('signature' => array(array($xmlrpcBase64, $xmlrpcString, $xmlrpcInt)), 'documentation' => 'Get a version of a page rendered in HTML', 'function' => 'getPageHTMLVersion'); function getPageHTMLVersion($params) { return getPageHTML($params); } /** * getAllPages(): Returns a list of all pages. The result is an array of strings. */ $wiki_dmap['getAllPages'] = array('signature' => array(array($xmlrpcArray)), 'documentation' => 'Returns a list of all pages as an array of strings', 'function' => 'getAllPages'); function getAllPages($params) { global $request; $dbh = $request->getDbh(); $iterator = $dbh->getAllPages(); $pages = array(); while ($page = $iterator->next()) { $pages[] = short_string($page->getName()); } return new xmlrpcresp(new xmlrpcval($pages, "array")); } /** * struct getPageInfo( string pagename ) : returns a struct with elements: * name (string): the canonical page name * lastModified (date): Last modification date * version (int): current version * author (string): author name */ $wiki_dmap['getPageInfo'] = array('signature' => array(array($xmlrpcStruct, $xmlrpcString)), 'documentation' => 'Gets info about the current version of a page', 'function' => 'getPageInfo'); function getPageInfo($params) { $revision = _getPageRevision($params); if (!$revision) return NoSuchPage(); $name = short_string($revision->getPageName()); $version = new xmlrpcval ($revision->getVersion(), "int"); $lastmodified = new xmlrpcval(iso8601_encode($revision->get('mtime'), 0), "dateTime.iso8601"); $author = short_string($revision->get('author')); return new xmlrpcresp(new xmlrpcval(array('name' => $name, 'lastModified' => $lastmodified, 'version' => $version, 'author' => $author), "struct")); } /** * struct getPageInfoVersion( string pagename, int version ) : returns * a struct just like plain getPageInfo(), but this time for a * specific version. */ $wiki_dmap['getPageInfoVersion'] = array('signature' => array(array($xmlrpcStruct, $xmlrpcString, $xmlrpcInt)), 'documentation' => 'Gets info about a page version', 'function' => 'getPageInfoVersion'); function getPageInfoVersion($params) { return getPageInfo($params); } /* array listLinks( string pagename ): Lists all links for a given page. The * returned array contains structs, with the following elements: * name (string) : The page name or URL the link is to. * type (int) : The link type. Zero (0) for internal Wiki link, * one (1) for external link (URL - image link, whatever). */ $wiki_dmap['listLinks'] = array('signature' => array(array($xmlrpcArray, $xmlrpcString)), 'documentation' => 'Lists all links for a given page', 'function' => 'listLinks'); function listLinks($params) { global $request; $ParamPageName = $params->getParam(0); $pagename = short_string_decode($ParamPageName->scalarval()); $dbh = $request->getDbh(); if (! $dbh->isWikiPage($pagename)) return NoSuchPage($pagename); $page = $dbh->getPage($pagename); // The fast WikiDB method. below is the slow method which goes through the formatter // NB no clean way to extract a list of external links yet, so // only internal links returned. i.e. all type 'local'. $linkiterator = $page->getPageLinks(); $linkstruct = array(); while ($currentpage = $linkiterator->next()) { $currentname = $currentpage->getName(); // Compute URL to page $args = array(); // How to check external links? if (!$currentpage->exists()) $args['action'] = 'edit'; // FIXME: Autodetected value of VIRTUAL_PATH wrong, // this make absolute URLs constructed by WikiURL wrong. // Also, if USE_PATH_INFO is false, WikiURL is wrong // due to its use of SCRIPT_NAME. //$use_abspath = USE_PATH_INFO && ! preg_match('/RPC2.php$/', VIRTUAL_PATH); // USE_PATH_INFO must be defined in index.php or config.ini but not before, // otherwise it is ignored and xmlrpc urls are wrong. // SCRIPT_NAME here is always .../RPC2.php if (USE_PATH_INFO and !$args) { $url = preg_replace('/%2f/i', '/', rawurlencode($currentname)); } elseif (!USE_PATH_INFO) { $url = str_replace("/RPC2.php","/index.php", WikiURL($currentname, $args, true)); } else { $url = WikiURL($currentname, $args); } $linkstruct[] = new xmlrpcval(array('page'=> short_string($currentname), 'type'=> new xmlrpcval('local', 'string'), 'href' => short_string($url)), "struct"); } /* $current = $page->getCurrentRevision(); $content = $current->getTransformedContent(); $links = $content->getLinkInfo(); foreach ($links as $link) { // We used to give an href for unknown pages that // included action=edit. I think that's probably the // wrong thing to do. $linkstruct[] = new xmlrpcval(array('page'=> short_string($link->page), 'type'=> new xmlrpcval($link->type, 'string'), 'href' => short_string($link->href), //'pageref' => short_string($link->pageref), ), "struct"); } */ return new xmlrpcresp(new xmlrpcval ($linkstruct, "array")); } /** * struct putPage(String pagename, String content, [String author[, String password]}) * returns a struct with elements: * code (int): 200 on success, 400 or 401 on failure * message (string): success or failure message * version (int): version of new page * * @author: Arnaud Fontaine, Reini Urban */ $wiki_dmap['putPage'] = array('signature' => array(array($xmlrpcStruct, $xmlrpcString, $xmlrpcString, $xmlrpcString, $xmlrpcString)), 'documentation' => 'put the raw Wiki text into a page as new version', 'function' => 'putPage'); function _getUser($userid='') { global $request; if (! $userid ) { if (!isset($_SERVER)) $_SERVER =& $GLOBALS['HTTP_SERVER_VARS']; if (!isset($_ENV)) $_ENV =& $GLOBALS['HTTP_ENV_VARS']; if (isset($_SERVER['REMOTE_USER'])) $userid = $_SERVER['REMOTE_USER']; elseif (isset($_ENV['REMOTE_USER'])) $userid = $_ENV['REMOTE_USER']; elseif (isset($_SERVER['REMOTE_ADDR'])) $userid = $_SERVER['REMOTE_ADDR']; elseif (isset($_ENV['REMOTE_ADDR'])) $userid = $_ENV['REMOTE_ADDR']; elseif (isset($GLOBALS['REMOTE_ADDR'])) $userid = $GLOBALS['REMOTE_ADDR']; } if (ENABLE_USER_NEW) { return WikiUser($userid); } else { return new WikiUser($request, $userid); } } function putPage($params) { global $request; $ParamPageName = $params->getParam(0); $ParamContent = $params->getParam(1); $pagename = short_string_decode($ParamPageName->scalarval()); $content = short_string_decode($ParamContent->scalarval()); $passwd = ''; if (count($params->params) > 2) { $ParamAuthor = $params->getParam(2); $userid = short_string_decode($ParamAuthor->scalarval()); if (count($params->params) > 3) { $ParamPassword = $params->getParam(3); $passwd = short_string_decode($ParamPassword->scalarval()); } } else { $userid = $request->_user->_userid; } $request->_user = _getUser($userid); $request->_user->_group = $request->getGroup(); $request->_user->AuthCheck($userid, $passwd); if (! mayAccessPage ('edit', $pagename)) { return new xmlrpcresp( new xmlrpcval( array('code' => new xmlrpcval(401, "int"), 'version' => new xmlrpcval(0, "int"), 'message' => short_string("no permission for " .$request->_user->UserName())), "struct")); } $now = time(); $dbh = $request->getDbh(); $page = $dbh->getPage($pagename); $current = $page->getCurrentRevision(); $content = trim($content); $version = $current->getVersion(); // $version = -1 will force create a new version if ($current->getPackedContent() != $content) { $init_meta = array('ctime' => $now, 'creator' => $userid, 'creator_id' => $userid, ); $version_meta = array('author' => $userid, 'author_id' => $userid, 'markup' => 2.0, 'summary' => isset($summary) ? $summary : _("xml-rpc change"), 'mtime' => $now, 'pagetype' => 'wikitext', 'wikitext' => $init_meta, ); $version++; $res = $page->save($content, $version, $version_meta); if ($res) $message = "Page $pagename version $version created"; else $message = "Problem creating version $version of page $pagename"; } else { $res = 0; $message = $message = "Page $pagename unchanged"; } return new xmlrpcresp(new xmlrpcval(array('code' => new xmlrpcval($res ? 200 : 400, "int"), 'version' => new xmlrpcval($version, "int"), 'message' => short_string($message)), "struct")); } /** * Publish-Subscribe * Client subscribes to a RecentChanges-like channel, getting a short * callback notification on every change. Like PageChangeNotification, just shorter * and more complicated * RSS2 support (not yet), since radio userland's rss-0.92. now called RSS2. * BTW: Radio Userland deprecated this interface. * * boolean wiki.rssPleaseNotify ( notifyProcedure, port, path, protocol, urlList ) * returns: true or false * * Check of the channel behind the rssurl has a cloud element, * if the client has a direct IP connection (no NAT), * register the client on the WikiDB notification handler * * http://backend.userland.com/publishSubscribeWalkthrough * http://www.soapware.org/xmlStorageSystem#rssPleaseNotify * http://www.thetwowayweb.com/soapmeetsrss#rsscloudInterface */ $wiki_dmap['rssPleaseNotify'] = array('signature' => array(array($xmlrpcBoolean, $xmlrpcStruct)), 'documentation' => 'RSS2 change notification subscriber channel', 'function' => 'rssPleaseNotify'); function rssPleaseNotify($params) { // register the clients IP return new xmlrpcresp(new xmlrpcval (0, "boolean")); } /* * String wiki.rssPleaseNotify ( username ) * returns: true or false */ $wiki_dmap['mailPasswordToUser'] = array('signature' => array(array($xmlrpcBoolean, $xmlrpcString)), 'documentation' => 'RSS2 user management helper', 'function' => 'mailPasswordToUser'); function mailPasswordToUser($params) { global $request; $ParamUserid = $params->getParam(0); $userid = short_string_decode($ParamUserid->scalarval()); $request->_user = _getUser($userid); //$request->_prefs =& $request->_user->_prefs; $email = $request->getPref('email'); $success = 0; if ($email) { $body = WikiURL('') . "\nPassword: " . $request->getPref('passwd'); $success = mail($email, "[".WIKI_NAME."} Password Request", $body); } return new xmlrpcresp(new xmlrpcval ($success, "boolean")); } /** * struct titleSearch(String substring [, Integer option = 0]) * returns an array of matching pagenames. * TODO: standardize options * * @author: Reini Urban */ $wiki_dmap['titleSearch'] = array('signature' => array(array($xmlrpcArray, $xmlrpcString, $xmlrpcInt)), 'documentation' => "Return matching pagenames. Option 1: caseexact, 2: regex, 4: starts_with, 8: exact", 'function' => 'titleSearch'); function titleSearch($params) { global $request; $ParamPageName = $params->getParam(0); $searchstring = short_string_decode($ParamPageName->scalarval()); if (count($params->params) > 1) { $ParamOption = $params->getParam(1); $option = $ParamOption->scalarval(); } else $option = 0; // default option: substring, case-inexact $case_exact = $option & 1; $regex = $option & 2; if (!$regex) { if ($option & 4) { // STARTS_WITH $regex = true; $searchstring = "^".$searchstring; } if ($option & 8) { // EXACT $regex = true; $searchstring = "^".$searchstring."$"; } } else { if ($option & 4 or $option & 8) { global $xmlrpcerruser; return new xmlrpcresp(0, $xmlrpcerruser + 1, "Invalid option"); } } include_once("lib/TextSearchQuery.php"); $query = new TextSearchQuery($searchstring, $case_exact, $regex ? 'auto' : 'none'); $dbh = $request->getDbh(); $iterator = $dbh->titleSearch($query); $pages = array(); while ($page = $iterator->next()) { $pages[] = short_string($page->getName()); } return new xmlrpcresp(new xmlrpcval($pages, "array")); } /** * Construct the server instance, and set up the dispatch map, * which maps the XML-RPC methods on to the wiki functions. * Provide the "wiki." prefix to each function */ class XmlRpcServer extends xmlrpc_server { function XmlRpcServer ($request = false) { global $wiki_dmap; foreach ($wiki_dmap as $name => $val) $dmap['wiki.' . $name] = $val; $this->xmlrpc_server($dmap, 0 /* delay service*/); } function service () { global $ErrorManager; $ErrorManager->pushErrorHandler(new WikiMethodCb($this, '_errorHandler')); xmlrpc_server::service(); $ErrorManager->popErrorHandler(); } function _errorHandler ($e) { $msg = htmlspecialchars($e->asString()); // '--' not allowed within xml comment $msg = str_replace('--', '--', $msg); if (function_exists('xmlrpc_debugmsg')) xmlrpc_debugmsg($msg); return true; } } // (c-file-style: "gnu") // Local Variables: // mode: php // tab-width: 8 // c-basic-offset: 4 // c-hanging-comment-ender-p: nil // indent-tabs-mode: nil // End: ?>