aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArno Richter <oelna@oelna.de>2022-12-11 16:15:21 +0100
committerArno Richter <oelna@oelna.de>2022-12-11 16:16:17 +0100
commit59febc76e35856ae3d7157507e54d2d17fb7a062 (patch)
treeb391ffe06a7c8f73a9d9fefd5dd3798d8ef4671b
parent2207c1f2db82fa52eb5dd8123d82380e790ee74b (diff)
downloadmicroblog-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.php4
-rw-r--r--functions.php11
-rw-r--r--postform.inc.php1
-rw-r--r--rsd.xml.php15
-rw-r--r--single.inc.php1
-rw-r--r--timeline.inc.php2
-rw-r--r--xmlrpc.php312
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);
+}