diff options
author | Arno Richter <oelna@oelna.de> | 2022-12-11 16:15:21 +0100 |
---|---|---|
committer | Arno Richter <oelna@oelna.de> | 2022-12-11 16:16:17 +0100 |
commit | 59febc76e35856ae3d7157507e54d2d17fb7a062 (patch) | |
tree | b391ffe06a7c8f73a9d9fefd5dd3798d8ef4671b | |
parent | 2207c1f2db82fa52eb5dd8123d82380e790ee74b (diff) | |
download | microblog-59febc76e35856ae3d7157507e54d2d17fb7a062.tar.gz microblog-59febc76e35856ae3d7157507e54d2d17fb7a062.tar.bz2 microblog-59febc76e35856ae3d7157507e54d2d17fb7a062.zip |
add first edition of XMLRPC support via the metaWeblog API. Closes #18
-rw-r--r-- | config-dist.php | 4 | ||||
-rw-r--r-- | functions.php | 11 | ||||
-rw-r--r-- | postform.inc.php | 1 | ||||
-rw-r--r-- | rsd.xml.php | 15 | ||||
-rw-r--r-- | single.inc.php | 1 | ||||
-rw-r--r-- | timeline.inc.php | 2 | ||||
-rw-r--r-- | xmlrpc.php | 312 |
7 files changed, 343 insertions, 3 deletions
diff --git a/config-dist.php b/config-dist.php index 8f33ac7..96b0423 100644 --- a/config-dist.php +++ b/config-dist.php @@ -25,6 +25,7 @@ $config = array( 'microblog_account' => '', // fill in a @username if you like 'admin_user' => 'admin', 'admin_pass' => 'dove-life-bird-lust', + 'app_token' => '3e83b13d99bf0de6c6bde5ac5ca4ae687a3d46db', // random secret used as auth with XMLRPC 'cookie_life' => 60*60*24*7*4, // cookie life in seconds 'ping' => true, // enable automatic pinging of the micro.blog service 'crosspost_to_twitter' => false, // set this to true to automatically crosspost to a twitter account (requires app credentials, see below) @@ -36,6 +37,9 @@ $config = array( ) ); +$config['xmlrpc'] = function_exists('xmlrpc_server_create'); +$config['local_time_offset'] = date('P'); + // load functions require_once(ROOT.DS.'database.php'); require_once(ROOT.DS.'functions.php'); diff --git a/functions.php b/functions.php index 38354bb..8464267 100644 --- a/functions.php +++ b/functions.php @@ -40,13 +40,18 @@ function db_delete($post_id) { return $statement->rowCount(); } -function db_update($post_id, $content) { +function db_update($post_id, $content, $timestamp=null) { global $db; if(empty($db)) return false; if(empty($content)) return false; if(!is_numeric($post_id) || $post_id <= 0) return false; - $statement = $db->prepare('UPDATE posts SET post_content = :post_content, post_edited = :post_edited WHERE id = :id'); + if($timestamp !== null) { + $statement = $db->prepare('UPDATE posts SET post_content = :post_content, post_edited = :post_edited, post_timestamp = :post_timestamp WHERE id = :id'); + $statement->bindValue(':post_timestamp', $timestamp, PDO::PARAM_INT); + } else { + $statement = $db->prepare('UPDATE posts SET post_content = :post_content, post_edited = :post_edited WHERE id = :id'); + } $statement->bindValue(':id', $post_id, PDO::PARAM_INT); $statement->bindValue(':post_content', $content, PDO::PARAM_STR); $statement->bindValue(':post_edited', time(), PDO::PARAM_INT); @@ -77,7 +82,7 @@ function db_select_posts($from=NOW, $amount=10, $sort='desc', $page=1) { $offset = ($page-1)*$config['posts_per_page']; - $statement = $db->prepare('SELECT * FROM posts WHERE post_timestamp < :post_timestamp AND post_deleted IS NULL ORDER BY id '.$sort.' LIMIT :limit OFFSET :page'); + $statement = $db->prepare('SELECT * FROM posts WHERE post_timestamp < :post_timestamp AND post_deleted IS NULL ORDER BY post_timestamp '.$sort.' LIMIT :limit OFFSET :page'); $statement->bindValue(':post_timestamp', $from, PDO::PARAM_INT); $statement->bindValue(':limit', $amount, PDO::PARAM_INT); $statement->bindValue(':page', $offset, PDO::PARAM_INT); diff --git a/postform.inc.php b/postform.inc.php index f1707f5..70fdc7b 100644 --- a/postform.inc.php +++ b/postform.inc.php @@ -71,6 +71,7 @@ <ul> <li><a href="<?= $config['url'] ?>/feed/feed.xml">ATOM Feed</a></li> <li><a href="<?= $config['url'] ?>/feed/feed.json">JSON Feed</a></li> + <?php if($config['xmlrpc']): ?><li><a href="<?= $config['url'] ?>/xmlrpc.php">XML-RPC</a></li><?php endif; ?> </ul> </nav> </footer> diff --git a/rsd.xml.php b/rsd.xml.php new file mode 100644 index 0000000..17b8370 --- /dev/null +++ b/rsd.xml.php @@ -0,0 +1,15 @@ +<?php + require_once(__DIR__.DIRECTORY_SEPARATOR.'config.php'); + + header('Content-Type: text/xml; charset=utf-8'); +?><rsd xmlns="http://archipelago.phrasewise.com/rsd" version="1.0"> + <service> + <engineName>oelna/microblog</engineName> + <engineLink>https://github.com/oelna/microblog</engineLink> + <homePageLink><?= $config['url'] ?></homePageLink> + <apis> + <api name="MetaWeblog" blogID="1" preferred="true" apiLink="<?= $config['url'] ?>/xmlrpc.php"/> + <!--<api name="Micro.blog" blogID="1" preferred="false" apiLink="<?= $config['url'] ?>/xmlrpc.php"/>--> + </apis> + </service> +</rsd> diff --git a/single.inc.php b/single.inc.php index a2b202f..ce41e1e 100644 --- a/single.inc.php +++ b/single.inc.php @@ -108,6 +108,7 @@ <ul> <li><a href="<?= $config['url'] ?>/feed/feed.xml">ATOM Feed</a></li> <li><a href="<?= $config['url'] ?>/feed/feed.json">JSON Feed</a></li> + <?php if($config['xmlrpc']): ?><li><a href="<?= $config['url'] ?>/xmlrpc.php">XML-RPC</a></li><?php endif; ?> </ul> </nav> </footer> diff --git a/timeline.inc.php b/timeline.inc.php index 9609f03..ef58293 100644 --- a/timeline.inc.php +++ b/timeline.inc.php @@ -24,6 +24,7 @@ <meta name="viewport" content="width=device-width" /> <link rel="alternate" type="application/json" title="JSON Feed" href="<?= $config['url'] ?>/feed/feed.json" /> <link rel="alternate" type="application/atom+xml" title="Atom Feed" href="<?= $config['url'] ?>/feed/feed.xml" /> + <?php if($config['xmlrpc']): ?><link rel="EditURI" type="application/rsd+xml" title="RSD" href="<?= $config['url'] ?>/rsd.xml.php" /><?php endif; ?> <link rel="stylesheet" href="<?= $config['url'] ?>/microblog.css" /> <script src="<?= $config['url'] ?>/microblog.js" type="module" defer></script> </head> @@ -70,6 +71,7 @@ <ul> <li><a href="<?= $config['url'] ?>/feed/feed.xml">ATOM Feed</a></li> <li><a href="<?= $config['url'] ?>/feed/feed.json">JSON Feed</a></li> + <?php if($config['xmlrpc']): ?><li><a href="<?= $config['url'] ?>/xmlrpc.php">XML-RPC</a></li><?php endif; ?> </ul> </nav> </footer> diff --git a/xmlrpc.php b/xmlrpc.php new file mode 100644 index 0000000..9438592 --- /dev/null +++ b/xmlrpc.php @@ -0,0 +1,312 @@ +<?php + +$request_xml = file_get_contents("php://input"); + +// check prerequisites +if(!function_exists('xmlrpc_server_create')) { exit('No XML-RPC support detected!'); } +if(empty($request_xml)) { exit('XML-RPC server accepts POST requests only.'); } + +// load config +require_once(__DIR__.DIRECTORY_SEPARATOR.'config.php'); + +function check_credentials($username, $password) { + global $config; + + $xmlrpc_auth = $config['admin_pass']; + if(!empty($config['app_token'])) { + $xmlrpc_auth = $config['app_token']; + } + + if($username == $config['admin_user'] && $password == $xmlrpc_auth) { + return true; + } else { + return false; + } +} + +function say_hello($method_name, $args) { + return 'Hello'; +} + +function mw_make_post($post) { + global $config; + + $date_created = date('Y-m-d\TH:i:s', $post['post_timestamp']).$config['local_time_offset']; + $date_created_gmt = gmdate('Y-m-d\TH:i:s', $post['post_timestamp']).'Z'; + if(!empty($post['post_edited']) && $post['post_edited'] > 0) { + $date_modified = date('Y-m-d\TH:i:s', $post['post_edited']).$config['local_time_offset']; + $date_modified_gmt = gmdate('Y-m-d\TH:i:s', $post['post_edited']).'Z'; + } else { + $date_modified = date('Y-m-d\TH:i:s', 0).$config['local_time_offset']; + $date_modified_gmt = gmdate('Y-m-d\TH:i:s', 0).'Z'; + } + + @xmlrpc_set_type($date_created, 'datetime'); + @xmlrpc_set_type($date_created_gmt, 'datetime'); + @xmlrpc_set_type($date_modified, 'datetime'); + @xmlrpc_set_type($date_modified_gmt, 'datetime'); + + // convert the post format to a standard metaWeblog post + $mw_post = [ + 'postid' => (int) $post['id'], + 'title' => '', + 'description' => ($post['post_content']), // Post content + 'link' => $config['url'].'/'.$post['id'], // Post URL + // string userid†: ID of post author. + 'dateCreated' => $date_created, + 'date_created_gmt' => $date_created_gmt, + 'date_modified' => $date_modified, + 'date_modified_gmt' => $date_modified_gmt, + // string wp_post_thumbnail†+ 'permalink' => $config['url'].'/'.$post['id'], // Post URL, equivalent to link. + 'categories' => [], // Names of categories assigned to the post. + 'mt_keywords' => '', // Names of tags assigned to the post. + 'mt_excerpt' => '', + 'mt_text_more' => '', // Post "Read more" text. + ]; + + return $mw_post; +} + +function mw_get_recent_posts($method_name, $args) { + + list($_, $username, $password, $amount) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + if(!$amount) $amount = 25; + + $posts = db_select_posts(time()+10, $amount); + $mw_posts = array_map('mw_make_post', $posts); + + return $mw_posts; +} + +function mw_get_post($method_name, $args) { + + list($post_id, $username, $password) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + $post = db_select_post($post_id); + if($post) { + $mw_post = mw_make_post($post); + return $mw_post; + } else { + return [ + 'faultCode' => 400, + 'faultString' => 'Could not fetch post.' + ]; + } +} + +function mw_delete_post($method_name, $args) { + + if($method_name == 'blogger.deletePost') { + list($_, $post_id, $username, $password, $_) = $args; + } else { + list($post_id, $username, $password) = $args; + } + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + $success = db_delete($post_id); + if($success > 0) { + rebuild_feeds(); + + return true; + } else { + return [ + 'faultCode' => 400, + 'faultString' => 'Could not delete post.' + ]; + } +} + +function mw_new_post($method_name, $args) { + + // blog_id, unknown, unknown, array of post content, unknown + list($blog_id, $username, $password, $content, $_) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + $post = [ + // 'post_hp' => $content['flNotOnHomePage'], + 'post_timestamp' => time(), + // 'post_title' => $content['title'], + 'post_content' => $content['description'], + // 'post_url' => $content['link'], + ]; + + // use a specific timestamp, if provided + if(isset($content['dateCreated'])) { + $post['post_timestamp'] = $content['dateCreated']->timestamp; + } + + $insert_id = db_insert($post['post_content'], $post['post_timestamp']); + if($insert_id && $insert_id > 0) { + // success + rebuild_feeds(); + + return (int) $insert_id; + } else { + // insert failed + // error codes: https://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + // more error codes? https://github.com/zendframework/zend-xmlrpc/blob/master/src/Fault.php + return [ + 'faultCode' => 400, + 'faultString' => 'Could not create post.' + ]; + } +} + +function mw_edit_post($method_name, $args) { + + // post_id, unknown, unknown, array of post content, unknown + list($post_id, $username, $password, $content, $_) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + $post = [ + // 'post_hp' => $content['flNotOnHomePage'], + 'post_timestamp' => $content['dateCreated']->timestamp, + // 'post_title' => $content['title'], + 'post_content' => $content['description'], + // 'post_url' => $content['link'], + ]; + + $update = db_update($post_id, $post['post_content'], $post['post_timestamp']); + if($update && $update > 0) { + // success + rebuild_feeds(); + + return true; + } else { + return [ + 'faultCode' => 400, + 'faultString' => 'Could not write post update.' + ]; + } +} + +function mw_get_categories($method_name, $args) { + + list($_, $username, $password) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + // we don't support categories, so only return a fake one + $categories = [ + [ + 'description' => 'Default', + 'htmlUrl' => '', + 'rssUrl' => '', + 'title' => 'default', + 'categoryid' => '1', + ] + ]; + + return $categories; +} + +function mw_get_users_blogs($method_name, $args) { + global $config; + + list($_, $username, $password) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + $bloginfo = [ + 'blogid' => '1', + 'url' => $config['url'], + 'blogName' => (empty($config['microblog_account']) ? "" : $config['microblog_account'] . "'s ").' microblog', + ]; + + return $bloginfo; +} + +function mw_get_user_info($method_name, $args) { + global $config; + + list($_, $username, $password) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + $userinfo = [ + 'userid' => '1', + 'firstname' => '', + 'lastname' => '', + 'nickname' => $config['microblog_account'], + 'email' => '', + 'url' => $config['url'], + ]; + + return $userinfo; +} + +// https://codex.wordpress.org/XML-RPC_MetaWeblog_API +// https://community.devexpress.com/blogs/theprogressbar/metablog.ashx +// idea: http://www.hixie.ch/specs/pingback/pingback#TOC3 +$server = xmlrpc_server_create(); +xmlrpc_server_register_method($server, 'demo.sayHello', 'say_hello'); +xmlrpc_server_register_method($server, 'blogger.getUsersBlogs', 'mw_get_users_blogs'); +xmlrpc_server_register_method($server, 'blogger.getUserInfo', 'mw_get_user_info'); +xmlrpc_server_register_method($server, 'metaWeblog.getCategories', 'mw_get_categories'); +xmlrpc_server_register_method($server, 'metaWeblog.getRecentPosts', 'mw_get_recent_posts'); +xmlrpc_server_register_method($server, 'metaWeblog.newPost', 'mw_new_post'); +xmlrpc_server_register_method($server, 'metaWeblog.editPost', 'mw_edit_post'); +xmlrpc_server_register_method($server, 'metaWeblog.getPost', 'mw_get_post'); +xmlrpc_server_register_method($server, 'blogger.deletePost', 'mw_delete_post'); +xmlrpc_server_register_method($server, 'metaWeblog.deletePost', 'mw_delete_post'); // does this exist? +// xmlrpc_server_register_method($server, 'metaWeblog.newMediaObject', 'say_hello'); // todo + +// https://docstore.mik.ua/orelly/webprog/pcook/ch12_08.htm +$response = xmlrpc_server_call_method($server, $request_xml, null, [ + 'escaping' => 'markup', + 'encoding' => 'UTF-8' +]); + +if($response) { + header('Content-Type: text/xml; charset=utf-8'); + echo($response); +} |