From 0b075f3ea2616cfde4d976199a69ba631174a336 Mon Sep 17 00:00:00 2001 From: Arno Richter Date: Tue, 13 Dec 2022 22:27:21 +0100 Subject: gave up and sorted files into a directory structure. made snippets for header, nav and footer. made it easier to add additional css files as themes. prepare a little for addition of a real template engine. added a css reset file to share between themes, if warranted. --- .htaccess | 4 +- css/microblog.css | 259 +++++++++++++++++++++++++++ css/reset.css | 23 +++ database.php | 56 ------ functions.php | 258 -------------------------- index.php | 13 +- lib/autolink.php | 335 ++++++++++++++++++++++++++++++++++ lib/database.php | 56 ++++++ lib/functions.php | 258 ++++++++++++++++++++++++++ lib/rsd.xml.php | 15 ++ lib/twitter_api.php | 410 ++++++++++++++++++++++++++++++++++++++++++ lib/xmlrpc.php | 428 ++++++++++++++++++++++++++++++++++++++++++++ lib_autolink.php | 335 ---------------------------------- loginform.inc.php | 64 ------- microblog.css | 256 -------------------------- postform.inc.php | 79 -------- rsd.xml.php | 15 -- single.inc.php | 123 ------------- snippets/footer.snippet.php | 10 ++ snippets/header.snippet.php | 27 +++ snippets/nav.snippet.php | 7 + templates/loginform.inc.php | 39 ++++ templates/postform.inc.php | 54 ++++++ templates/single.inc.php | 100 +++++++++++ templates/timeline.inc.php | 57 ++++++ timeline.inc.php | 83 --------- twitter_api.php | 410 ------------------------------------------ xmlrpc.php | 428 -------------------------------------------- 28 files changed, 2088 insertions(+), 2114 deletions(-) create mode 100644 css/microblog.css create mode 100644 css/reset.css delete mode 100644 database.php delete mode 100644 functions.php create mode 100644 lib/autolink.php create mode 100644 lib/database.php create mode 100644 lib/functions.php create mode 100644 lib/rsd.xml.php create mode 100644 lib/twitter_api.php create mode 100644 lib/xmlrpc.php delete mode 100644 lib_autolink.php delete mode 100644 loginform.inc.php delete mode 100644 microblog.css delete mode 100644 postform.inc.php delete mode 100644 rsd.xml.php delete mode 100644 single.inc.php create mode 100644 snippets/footer.snippet.php create mode 100644 snippets/header.snippet.php create mode 100644 snippets/nav.snippet.php create mode 100644 templates/loginform.inc.php create mode 100644 templates/postform.inc.php create mode 100644 templates/single.inc.php create mode 100644 templates/timeline.inc.php delete mode 100644 timeline.inc.php delete mode 100644 twitter_api.php delete mode 100644 xmlrpc.php diff --git a/.htaccess b/.htaccess index bdabf00..64b0c70 100644 --- a/.htaccess +++ b/.htaccess @@ -19,8 +19,8 @@ RewriteEngine On RewriteBase /microblog # friendly URLs -RewriteRule ^rsd/?$ rsd.xml.php [L] -RewriteRule ^xmlrpc/?(.*)$ xmlrpc.php?/$1 [L] +RewriteRule ^rsd/?$ lib/rsd.xml.php [L] +RewriteRule ^xmlrpc/?(.*)$ lib/xmlrpc.php?/$1 [L] RewriteRule ^feed/json/?$ feed/feed.json [L] RewriteRule ^feed/atom/?$ feed/feed.xml [L] diff --git a/css/microblog.css b/css/microblog.css new file mode 100644 index 0000000..deb68c8 --- /dev/null +++ b/css/microblog.css @@ -0,0 +1,259 @@ +@import './reset.css'; + +:root { + --primary-color: #007aff; + --secondary-color: #fffceb; + + --background-color: #b5b5af; + --text-color: #080f15; +} + +html { + font: 100%/1.4 system-ui, Helvetica, sans-serif; + background-color: var(--background-color); + color: var(--text-color); +} + +.wrap { + width: min(95%, 40rem); + margin: 2rem auto; + padding: 1rem; + background-color: var(--secondary-color); + box-shadow: 0 1.25rem 1rem -1rem rgba(0,0,0,0.25); +} + +.button { + display: block; + background: var(--primary-color); + color: var(--secondary-color); + text-decoration: none; + border-radius: 0.4rem; + padding: 0.2rem 0.5rem; + font-weight: bold; + text-align: center; +} + +.button.alert { + background: coral; +} + +nav.main ul { + display: flex; + margin-bottom: 2rem; +} + +nav.main li { + list-style: none; + flex: 1; +} + +nav.main li a { + /* inherits from .button */ +} + +nav.main li + li a { + margin-left: 0.2rem; +} + +.wrap .posts { + +} + +.wrap .posts > li { + list-style: none; + margin-bottom: 1rem; + border-bottom: 1px solid rgba(0,0,0,0.1); + padding-bottom: 1rem; + + display: grid; + grid-template-columns: repeat(6, 1fr); +} + +.wrap .posts > li > * { + outline: 0px solid #f0f; +} + +.wrap .posts > li:last-child { + margin-bottom: 2rem; + border-bottom: 0; + padding-bottom: 0; +} + +.timeline .pagination { overflow: hidden; } +.timeline .pagination a { + display: block; + background: var(--primary-color); + color: var(--secondary-color); + text-decoration: none; + border-radius: 0.4rem; + padding: 0.2rem 0.6rem; + font-weight: bold; + float: left; +} +.timeline .pagination .next { float: right; } + +.wrap .post-timestamp { + display: block; + color: var(--primary-color); + text-decoration: none; + font-size: 0.8rem; + text-transform: uppercase; + font-weight: bold; + margin-bottom: 0.5rem; + grid-column-start: span 3; +} + +.wrap .post-meta { + grid-column-start: span 3; +} + +.wrap .post-meta ul { + display: flex; + justify-content: flex-end; + gap: 0.75ch; +} + +.wrap .post-meta li { + list-style: none; +} + +.wrap .post-meta li a { + display: block; + color: hsla(0, 0%, 0%, 0.2); + mix-blend-mode: multiply; + text-decoration: none; + font-size: 0.8rem; + text-transform: uppercase; + font-weight: bold; + margin-bottom: 0.5rem; +} + +.wrap .post-meta li a:is(:hover, :focus) { + color: currentColor; +} + +.wrap .post-content { + font-size: 1.25rem; + overflow-wrap: break-word; + grid-column-start: span 6; +} + +.wrap .post-content a { + color: var(--primary-color); + text-decoration: none; +} + +.wrap form.delete { + width: 100%; + grid-column-start: span 6; + display: flex; + margin-block-start: 2rem; +} + +.wrap form.delete input[type="submit"] { + flex: 1; + line-height: 1.4; + cursor: pointer; +} + +.wrap .posts li .message { + width: 100%; + grid-column-start: span 6; + margin-block-start: 2rem; +} + +.postform form, +form.edit, +.login form { + grid-column-start: span 6; + overflow: hidden; +} + +:is(.postform, .edit) textarea { + width: 100%; + border: 2px solid var(--background-color); + background: #fff; + padding: 0.5rem; + font-size: 1.25rem; + resize: vertical; + min-height: 10rem; + margin-bottom: 0.5rem; +} + +:is(.postform, .edit) textarea:focus { + border-color: var(--primary-color); + outline: none; +} + +:is(.postform, .edit) input[type="submit"], +.login input[type="submit"] { + -webkit-appearance: none; + appearance: none; + border: 0; + display: block; + background: var(--primary-color); + color: var(--secondary-color); + text-decoration: none; + border-radius: 0.4rem; + padding: 0.3rem 0.8rem 0.4rem; + font-weight: bold; + text-align: center; + cursor: pointer; + float: right; +} + +:is(.postform, .edit) #count { + float: left; + color: var(--background-color); +} + +:is(.postform, .edit) .message, +.login .message { + background-color: #87b26c; + padding: 0.5rem; + color: var(--secondary-color); + border-radius: 0.4rem; + margin-bottom: 0.5rem; +} + +:is(.postform, .edit) .error, +.login .error { + background-color: #9c2128; +} + +.login input[type="text"], +.login input[type="password"] { + width: 100%; + border: 2px solid var(--background-color); + padding: 0.5rem; + font-size: 1.25rem; + resize: vertical; + margin-bottom: 0.5rem; +} + +.login input[type="text"]:focus, +.login input[type="password"]:focus { + border-color: var(--primary-color); + outline: none; +} + +footer { + width: min(95%, 40rem); + margin: 0.5rem auto 2rem; +} + +footer ul { + list-style: none; + display: flex; + justify-content: center; + gap: 1rem; +} + +footer li a { + color: hsla(0, 0%, 0%, 0.3); + text-decoration: none; + font-size: 0.8rem; + text-transform: uppercase; + font-weight: bold; + margin-bottom: 0.5rem; +} diff --git a/css/reset.css b/css/reset.css new file mode 100644 index 0000000..43415e4 --- /dev/null +++ b/css/reset.css @@ -0,0 +1,23 @@ +/* + The new CSS reset - version 1.7.3 (last updated 7.8.2022) + GitHub page: https://github.com/elad2412/the-new-css-reset +*/ +*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) { + all: unset; + display: revert; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +a, button { cursor: revert; } +ol, ul, menu { list-style: none; } +img { max-width: 100%; } +table { border-collapse: collapse; } +input, textarea { -webkit-user-select: auto; } +textarea { white-space: revert; } +::placeholder { color: unset; } +:where([hidden]) { display: none; } diff --git a/database.php b/database.php deleted file mode 100644 index 0ffdb3a..0000000 --- a/database.php +++ /dev/null @@ -1,56 +0,0 @@ -query("PRAGMA user_version")->fetch(PDO::FETCH_ASSOC)['user_version']; -} catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - die('cannot connect to or open the database'); -} - -// first time setup -if($config['db_version'] == 0) { - try { - $db->exec("CREATE TABLE IF NOT EXISTS `posts` ( - `id` integer PRIMARY KEY NOT NULL, - `post_content` TEXT, - `post_timestamp` INTEGER - ); PRAGMA `user_version` = 1;"); - $config['db_version'] = 1; - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - die('cannot set up initial database table!'); - } -} - -// upgrade database to v2 -if($config['db_version'] == 1) { - try { - $db->exec("PRAGMA user_version = 2; - ALTER TABLE `posts` ADD `post_thread` INTEGER; - ALTER TABLE `posts` ADD `post_edited` INTEGER; - ALTER TABLE `posts` ADD `post_deleted` INTEGER; - "); - $config['db_version'] = 2; - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - die('cannot upgrade database table to v2!'); - } -} - -// upgrade database to v3 -if($config['db_version'] == 2) { - try { - $db->exec("PRAGMA user_version = 3; - ALTER TABLE `posts` ADD `post_guid` TEXT; - "); - $config['db_version'] = 3; - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - die('cannot upgrade database table to v3!'); - } -} - -// debug: get a list of post table columns -// var_dump($db->query("PRAGMA table_info(`posts`)")->fetchAll(PDO::FETCH_COLUMN, 1)); diff --git a/functions.php b/functions.php deleted file mode 100644 index 1586bed..0000000 --- a/functions.php +++ /dev/null @@ -1,258 +0,0 @@ -prepare('INSERT INTO posts (post_content, post_timestamp, post_guid) VALUES (:post_content, :post_timestamp, :post_guid)'); - $statement->bindValue(':post_content', $content, PDO::PARAM_STR); - $statement->bindValue(':post_timestamp', $timestamp, PDO::PARAM_INT); - $statement->bindValue(':post_guid', uuidv4(), PDO::PARAM_STR); - - $statement->execute(); - - return $db->lastInsertId(); -} - -function db_delete($post_id) { - global $db; - if(empty($db)) return false; - if(!is_numeric($post_id) || $post_id <= 0) return false; - - /* - $statement = $db->prepare('DELETE FROM posts WHERE id = :id'); - $statement->bindParam(':id', $post_id, PDO::PARAM_INT); - */ - - // mark as deleted instead (for undo?!) - $statement = $db->prepare('UPDATE posts SET post_deleted = :post_deleted WHERE id = :id'); - $statement->bindValue(':id', $post_id, PDO::PARAM_INT); - $statement->bindValue(':post_deleted', time(), PDO::PARAM_INT); - - $statement->execute(); - - return $statement->rowCount(); -} - -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; - - 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); - - $statement->execute(); - - return $statement->rowCount(); -} - -function db_select_post($id=0) { - global $db; - if(empty($db)) return false; - if($id === 0) return false; - - $statement = $db->prepare('SELECT * FROM posts WHERE id = :id LIMIT 1'); - $statement->bindValue(':id', $id, PDO::PARAM_INT); - $statement->execute(); - $row = $statement->fetch(PDO::FETCH_ASSOC); - - return (!empty($row)) ? $row : false; -} - -function db_select_posts($from, $amount=10, $sort='desc', $offset=0) { - global $db; - if(empty($db)) return false; - if(empty($from)) $from = time(); - if($sort !== 'desc') $sort = 'asc'; - - $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); - $statement->execute(); - $rows = $statement->fetchAll(PDO::FETCH_ASSOC); - - return (!empty($rows)) ? $rows : false; -} - -function db_posts_count() { - global $config; - global $db; - if(empty($db)) return false; - - $statement = $db->prepare('SELECT COUNT(*) AS posts_count FROM posts'); - $statement->execute(); - $row = $statement->fetch(PDO::FETCH_ASSOC); - - return (int) $row['posts_count']; -} - -/* function that pings the official micro.blog endpoint for feed refreshes */ -function ping_microblog() { - global $config; - $ping_url = 'https://micro.blog/ping'; - $feed_url = $config['url'].'/feed/json'; - - $ch = curl_init($ping_url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, 'url='.urlencode($feed_url)); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_NOBODY, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $response = curl_exec($ch); - $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - return ($status == 200) ? true : false; -} - -function rebuild_feeds($amount=10) { - - $posts = db_select_posts(NOW+60, $amount, 'desc'); - - rebuild_json_feed($posts); - rebuild_atom_feed($posts); -} - -function rebuild_json_feed($posts=[]) { - global $config; - - if (!file_exists(ROOT.DS.'feed')) { - mkdir(ROOT.DS.'feed', 0755); - } - - $filename = ROOT.DS.'feed'.DS.'feed.json'; - - $feed = array( - 'version' => 'https://jsonfeed.org/version/1', - 'title' => 'status updates by '.$config['microblog_account'], - 'description' => '', - 'home_page_url' => $config['url'], - 'feed_url' => $config['url'].'/feed/feed.json', - 'user_comment' => '', - 'favicon' => '', - 'author' => array('name' => $config['microblog_account']), - 'items' => array() - ); - - foreach($posts as $post) { - - $feed['items'][] = array( - 'id' => ($post['post_guid'] ? 'urn:uuid:'.$post['post_guid'] : $config['url'].'/'.$post['id']), - 'url' => $config['url'].'/'.$post['id'], - 'title' => '', - 'content_html' => $post['post_content'], - 'date_published' => gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']) - ); - } - - if(file_put_contents($filename, json_encode($feed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))) { - return true; - } else return false; -} - -function rebuild_atom_feed($posts=[]) { - global $config; - - if (!file_exists(ROOT.DS.'feed')) { - mkdir(ROOT.DS.'feed', 0755); - } - - $filename = ROOT.DS.'feed'.DS.'feed.xml'; - - $feed = ''.NL; - $feed .= ''.NL; - $feed .= ''.$config['microblog_account'].''.NL; - $feed .= 'status updates by '.$config['microblog_account'].''.NL; - $feed .= ''.$config['url'].''.NL; - $feed .= ''.gmdate('Y-m-d\TH:i:s\Z').''.NL; - - foreach($posts as $post) { - - $published = gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']); - $updated = ($post['post_edited'] > $post['post_timestamp']) ? gmdate('Y-m-d\TH:i:s\Z', $post['post_edited']) : $published; - - $feed .= ''.NL; - $feed .= ''.date('Y-m-d H:i', $post['post_timestamp']).''.NL; - $feed .= ''.NL; - $feed .= ''.($post['post_guid'] ? 'urn:uuid:'.$post['post_guid'] : $config['url'].'/'.$post['id']).''.NL; - $feed .= ''.$updated.''.NL; - $feed .= ''.$published.''.NL; - $feed .= ''.$post['post_content'].''.NL; - $feed .= ''.NL; - } - - $feed .= ''; - - if(file_put_contents($filename, $feed)) { - return true; - } else return false; -} - -function uuidv4($data = null) { // https://stackoverflow.com/a/15875555/3625228 - - $data = $data ?? openssl_random_pseudo_bytes(16); - assert(strlen($data) == 16); - - $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 - $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 - - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); -} - -function twitter_post_status($status='') { - global $config; - require_once(ROOT.DS.'twitter_api.php'); - - if(empty($status)) return array('errors' => 1); - if(empty($config['twitter']['oauth_access_token']) || - empty($config['twitter']['oauth_access_token_secret']) || - empty($config['twitter']['consumer_key']) || - empty($config['twitter']['consumer_secret'])) return array('errors' => 2); - - $url = 'https://api.twitter.com/1.1/statuses/update.json'; - $postfields = array( - 'status' => $status, - 'trim_user' => 1 - ); - - $twitter = new TwitterAPIExchange($config['twitter']); - return $twitter->buildOauth($url, 'POST')->setPostfields($postfields)->performRequest(); -} diff --git a/index.php b/index.php index c0259c2..f435366 100644 --- a/index.php +++ b/index.php @@ -2,16 +2,18 @@ require_once(__DIR__.DIRECTORY_SEPARATOR.'config.php'); // check user credentials - $config['logged_in'] = false; $config['logged_in'] = check_login(); // subpages + $template = 'timeline'; if(is_numeric(path(0))) { // show a single blog post - require_once(ROOT.DS.'single.inc.php'); + $template = 'single'; + require_once(ROOT.DS.'templates'.DS.'single.inc.php'); } elseif(mb_strtolower(path(0)) === 'login') { - require_once(ROOT.DS.'loginform.inc.php'); + $template = 'login'; + require_once(ROOT.DS.'templates'.DS.'loginform.inc.php'); } elseif(mb_strtolower(path(0)) === 'logout') { $domain = ($_SERVER['HTTP_HOST'] != 'localhost') ? $_SERVER['HTTP_HOST'] : false; @@ -22,7 +24,8 @@ die(); } elseif(mb_strtolower(path(0)) === 'new') { - require_once(ROOT.DS.'postform.inc.php'); + $template = 'postform'; + require_once(ROOT.DS.'templates'.DS.'postform.inc.php'); } else { // redirect everything else to the homepage @@ -32,5 +35,5 @@ } // show the homepage - require_once(ROOT.DS.'timeline.inc.php'); + require_once(ROOT.DS.'templates'.DS.'timeline.inc.php'); } diff --git a/lib/autolink.php b/lib/autolink.php new file mode 100644 index 0000000..da39c03 --- /dev/null +++ b/lib/autolink.php @@ -0,0 +1,335 @@ + + # This code is licensed under the MIT license + # + + #################################################################### + + # + # These are global options. You can set them before calling the autolinking + # functions to change the output. + # + + $GLOBALS['autolink_options'] = array( + + # Should http:// be visibly stripped from the front + # of URLs? + 'strip_protocols' => true, + + ); + + #################################################################### + + function autolink($text, $limit=30, $tagfill='', $auto_title = true){ + + $text = autolink_do($text, '![a-z][a-z-]+://!i', $limit, $tagfill, $auto_title); + $text = autolink_do($text, '!(mailto|skype):!i', $limit, $tagfill, $auto_title); + $text = autolink_do($text, '!www\\.!i', $limit, $tagfill, $auto_title, 'http://'); + return $text; + } + + #################################################################### + + function autolink_do($text, $sub, $limit, $tagfill, $auto_title, $force_prefix=null){ + + $text_l = StrToLower($text); + $cursor = 0; + $loop = 1; + $buffer = ''; + + while (($cursor < strlen($text)) && $loop){ + + $ok = 1; + $matched = preg_match($sub, $text_l, $m, PREG_OFFSET_CAPTURE, $cursor); + + if (!$matched){ + + $loop = 0; + $ok = 0; + + }else{ + + $pos = $m[0][1]; + $sub_len = strlen($m[0][0]); + + $pre_hit = substr($text, $cursor, $pos-$cursor); + $hit = substr($text, $pos, $sub_len); + $pre = substr($text, 0, $pos); + $post = substr($text, $pos + $sub_len); + + $fail_text = $pre_hit.$hit; + $fail_len = strlen($fail_text); + + # + # substring found - first check to see if we're inside a link tag already... + # + + $bits = preg_split("!!i", $pre); + $last_bit = array_pop($bits); + if (preg_match("!\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # looks like a nice spot to autolink from - check the pre + # to see if there was whitespace before this match + # + + if ($ok){ + + if ($pre){ + if (!preg_match('![\s\(\[\{>\pZ\p{Cc}]$!s', $pre)){ + + #echo "fail 2 at $cursor ($pre)
\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + } + + # + # we want to autolink here - find the extent of the url + # + + if ($ok){ + if (preg_match('/^([a-z0-9\-\.\/\-_%~!?=,:;&+*#@\(\)\$]+)/i', $post, $matches)){ + + $url = $hit.$matches[1]; + + $cursor += strlen($url) + strlen($pre_hit); + $buffer .= $pre_hit; + + $url = html_entity_decode($url); + + + # + # remove trailing punctuation from url + # + + while (preg_match('|[.,!;:?]$|', $url)){ + $url = substr($url, 0, strlen($url)-1); + $cursor--; + } + foreach (array('()', '[]', '{}') as $pair){ + $o = substr($pair, 0, 1); + $c = substr($pair, 1, 1); + if (preg_match("!^(\\$c|^)[^\\$o]+\\$c$!", $url)){ + $url = substr($url, 0, strlen($url)-1); + $cursor--; + } + } + + + # + # nice-i-fy url here + # + + $link_url = $url; + $display_url = $url; + + if ($force_prefix) $link_url = $force_prefix.$link_url; + + if ($GLOBALS['autolink_options']['strip_protocols']){ + if (preg_match('!^(http|https)://!i', $display_url, $m)){ + + $display_url = substr($display_url, strlen($m[1])+3); + } + } + + $display_url = autolink_label($display_url, $limit); + + + # + # add the url + # + + $currentTagfill = $tagfill; + if ($display_url != $link_url && !preg_match('@title=@msi',$currentTagfill) && $auto_title) { + + $display_quoted = preg_quote($display_url, '!'); + + if (!preg_match("!^(http|https)://{$display_quoted}$!i", $link_url)){ + + $currentTagfill .= ' title="'.$link_url.'"'; + } + } + + $link_url_enc = HtmlSpecialChars($link_url); + $display_url_enc = HtmlSpecialChars($display_url); + + $buffer .= "{$display_url_enc}"; + + }else{ + #echo "fail 3 at $cursor
\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + } + + # + # add everything from the cursor to the end onto the buffer. + # + + $buffer .= substr($text, $cursor); + + return $buffer; + } + + #################################################################### + + function autolink_label($text, $limit){ + + if (!$limit){ return $text; } + + if (strlen($text) > $limit){ + return substr($text, 0, $limit-3).'...'; + } + + return $text; + } + + #################################################################### + + function autolink_email($text, $tagfill=''){ + + $atom = '[^()<>@,;:\\\\".\\[\\]\\x00-\\x20\\x7f]+'; # from RFC822 + + #die($atom); + + $text_l = StrToLower($text); + $cursor = 0; + $loop = 1; + $buffer = ''; + + while(($cursor < strlen($text)) && $loop){ + + # + # find an '@' symbol + # + + $ok = 1; + $pos = strpos($text_l, '@', $cursor); + + if ($pos === false){ + + $loop = 0; + $ok = 0; + + }else{ + + $pre = substr($text, $cursor, $pos-$cursor); + $hit = substr($text, $pos, 1); + $post = substr($text, $pos + 1); + + $fail_text = $pre.$hit; + $fail_len = strlen($fail_text); + + #die("$pre::$hit::$post::$fail_text"); + + # + # substring found - first check to see if we're inside a link tag already... + # + + $bits = preg_split("!!i", $pre); + $last_bit = array_pop($bits); + if (preg_match("!\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # check backwards + # + + if ($ok){ + if (preg_match("!($atom(\.$atom)*)\$!", $pre, $matches)){ + + # move matched part of address into $hit + + $len = strlen($matches[1]); + $plen = strlen($pre); + + $hit = substr($pre, $plen-$len).$hit; + $pre = substr($pre, 0, $plen-$len); + + }else{ + + #echo "fail 2 at $cursor ($pre)
\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # check forwards + # + + if ($ok){ + if (preg_match("!^($atom(\.$atom)*)!", $post, $matches)){ + + # move matched part of address into $hit + + $len = strlen($matches[1]); + + $hit .= substr($post, 0, $len); + $post = substr($post, $len); + + }else{ + #echo "fail 3 at $cursor ($post)
\n"; + + $ok = 0; + $cursor += $fail_len; + $buffer .= $fail_text; + } + } + + # + # commit + # + + if ($ok) { + + $cursor += strlen($pre) + strlen($hit); + $buffer .= $pre; + $buffer .= "$hit"; + + } + + } + + # + # add everything from the cursor to the end onto the buffer. + # + + $buffer .= substr($text, $cursor); + + return $buffer; + } + + #################################################################### + +?> diff --git a/lib/database.php b/lib/database.php new file mode 100644 index 0000000..0ffdb3a --- /dev/null +++ b/lib/database.php @@ -0,0 +1,56 @@ +query("PRAGMA user_version")->fetch(PDO::FETCH_ASSOC)['user_version']; +} catch(PDOException $e) { + print 'Exception : '.$e->getMessage(); + die('cannot connect to or open the database'); +} + +// first time setup +if($config['db_version'] == 0) { + try { + $db->exec("CREATE TABLE IF NOT EXISTS `posts` ( + `id` integer PRIMARY KEY NOT NULL, + `post_content` TEXT, + `post_timestamp` INTEGER + ); PRAGMA `user_version` = 1;"); + $config['db_version'] = 1; + } catch(PDOException $e) { + print 'Exception : '.$e->getMessage(); + die('cannot set up initial database table!'); + } +} + +// upgrade database to v2 +if($config['db_version'] == 1) { + try { + $db->exec("PRAGMA user_version = 2; + ALTER TABLE `posts` ADD `post_thread` INTEGER; + ALTER TABLE `posts` ADD `post_edited` INTEGER; + ALTER TABLE `posts` ADD `post_deleted` INTEGER; + "); + $config['db_version'] = 2; + } catch(PDOException $e) { + print 'Exception : '.$e->getMessage(); + die('cannot upgrade database table to v2!'); + } +} + +// upgrade database to v3 +if($config['db_version'] == 2) { + try { + $db->exec("PRAGMA user_version = 3; + ALTER TABLE `posts` ADD `post_guid` TEXT; + "); + $config['db_version'] = 3; + } catch(PDOException $e) { + print 'Exception : '.$e->getMessage(); + die('cannot upgrade database table to v3!'); + } +} + +// debug: get a list of post table columns +// var_dump($db->query("PRAGMA table_info(`posts`)")->fetchAll(PDO::FETCH_COLUMN, 1)); diff --git a/lib/functions.php b/lib/functions.php new file mode 100644 index 0000000..268808c --- /dev/null +++ b/lib/functions.php @@ -0,0 +1,258 @@ +prepare('INSERT INTO posts (post_content, post_timestamp, post_guid) VALUES (:post_content, :post_timestamp, :post_guid)'); + $statement->bindValue(':post_content', $content, PDO::PARAM_STR); + $statement->bindValue(':post_timestamp', $timestamp, PDO::PARAM_INT); + $statement->bindValue(':post_guid', uuidv4(), PDO::PARAM_STR); + + $statement->execute(); + + return $db->lastInsertId(); +} + +function db_delete($post_id) { + global $db; + if(empty($db)) return false; + if(!is_numeric($post_id) || $post_id <= 0) return false; + + /* + $statement = $db->prepare('DELETE FROM posts WHERE id = :id'); + $statement->bindParam(':id', $post_id, PDO::PARAM_INT); + */ + + // mark as deleted instead (for undo?!) + $statement = $db->prepare('UPDATE posts SET post_deleted = :post_deleted WHERE id = :id'); + $statement->bindValue(':id', $post_id, PDO::PARAM_INT); + $statement->bindValue(':post_deleted', time(), PDO::PARAM_INT); + + $statement->execute(); + + return $statement->rowCount(); +} + +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; + + 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); + + $statement->execute(); + + return $statement->rowCount(); +} + +function db_select_post($id=0) { + global $db; + if(empty($db)) return false; + if($id === 0) return false; + + $statement = $db->prepare('SELECT * FROM posts WHERE id = :id LIMIT 1'); + $statement->bindValue(':id', $id, PDO::PARAM_INT); + $statement->execute(); + $row = $statement->fetch(PDO::FETCH_ASSOC); + + return (!empty($row)) ? $row : false; +} + +function db_select_posts($from, $amount=10, $sort='desc', $offset=0) { + global $db; + if(empty($db)) return false; + if(empty($from)) $from = time(); + if($sort !== 'desc') $sort = 'asc'; + + $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); + $statement->execute(); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + + return (!empty($rows)) ? $rows : false; +} + +function db_posts_count() { + global $config; + global $db; + if(empty($db)) return false; + + $statement = $db->prepare('SELECT COUNT(*) AS posts_count FROM posts'); + $statement->execute(); + $row = $statement->fetch(PDO::FETCH_ASSOC); + + return (int) $row['posts_count']; +} + +/* function that pings the official micro.blog endpoint for feed refreshes */ +function ping_microblog() { + global $config; + $ping_url = 'https://micro.blog/ping'; + $feed_url = $config['url'].'/feed/json'; + + $ch = curl_init($ping_url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, 'url='.urlencode($feed_url)); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + return ($status == 200) ? true : false; +} + +function rebuild_feeds($amount=10) { + + $posts = db_select_posts(NOW+60, $amount, 'desc'); + + rebuild_json_feed($posts); + rebuild_atom_feed($posts); +} + +function rebuild_json_feed($posts=[]) { + global $config; + + if (!file_exists(ROOT.DS.'feed')) { + mkdir(ROOT.DS.'feed', 0755); + } + + $filename = ROOT.DS.'feed'.DS.'feed.json'; + + $feed = array( + 'version' => 'https://jsonfeed.org/version/1', + 'title' => 'status updates by '.$config['microblog_account'], + 'description' => '', + 'home_page_url' => $config['url'], + 'feed_url' => $config['url'].'/feed/feed.json', + 'user_comment' => '', + 'favicon' => '', + 'author' => array('name' => $config['microblog_account']), + 'items' => array() + ); + + foreach($posts as $post) { + + $feed['items'][] = array( + 'id' => ($post['post_guid'] ? 'urn:uuid:'.$post['post_guid'] : $config['url'].'/'.$post['id']), + 'url' => $config['url'].'/'.$post['id'], + 'title' => '', + 'content_html' => $post['post_content'], + 'date_published' => gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']) + ); + } + + if(file_put_contents($filename, json_encode($feed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))) { + return true; + } else return false; +} + +function rebuild_atom_feed($posts=[]) { + global $config; + + if (!file_exists(ROOT.DS.'feed')) { + mkdir(ROOT.DS.'feed', 0755); + } + + $filename = ROOT.DS.'feed'.DS.'feed.xml'; + + $feed = ''.NL; + $feed .= ''.NL; + $feed .= ''.$config['microblog_account'].''.NL; + $feed .= 'status updates by '.$config['microblog_account'].''.NL; + $feed .= ''.$config['url'].''.NL; + $feed .= ''.gmdate('Y-m-d\TH:i:s\Z').''.NL; + + foreach($posts as $post) { + + $published = gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']); + $updated = ($post['post_edited'] > $post['post_timestamp']) ? gmdate('Y-m-d\TH:i:s\Z', $post['post_edited']) : $published; + + $feed .= ''.NL; + $feed .= ''.date('Y-m-d H:i', $post['post_timestamp']).''.NL; + $feed .= ''.NL; + $feed .= ''.($post['post_guid'] ? 'urn:uuid:'.$post['post_guid'] : $config['url'].'/'.$post['id']).''.NL; + $feed .= ''.$updated.''.NL; + $feed .= ''.$published.''.NL; + $feed .= ''.$post['post_content'].''.NL; + $feed .= ''.NL; + } + + $feed .= ''; + + if(file_put_contents($filename, $feed)) { + return true; + } else return false; +} + +function uuidv4($data = null) { // https://stackoverflow.com/a/15875555/3625228 + + $data = $data ?? openssl_random_pseudo_bytes(16); + assert(strlen($data) == 16); + + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 + + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); +} + +function twitter_post_status($status='') { + global $config; + require_once(ROOT.DS.'lib'.DS.'twitter_api.php'); + + if(empty($status)) return array('errors' => 1); + if(empty($config['twitter']['oauth_access_token']) || + empty($config['twitter']['oauth_access_token_secret']) || + empty($config['twitter']['consumer_key']) || + empty($config['twitter']['consumer_secret'])) return array('errors' => 2); + + $url = 'https://api.twitter.com/1.1/statuses/update.json'; + $postfields = array( + 'status' => $status, + 'trim_user' => 1 + ); + + $twitter = new TwitterAPIExchange($config['twitter']); + return $twitter->buildOauth($url, 'POST')->setPostfields($postfields)->performRequest(); +} diff --git a/lib/rsd.xml.php b/lib/rsd.xml.php new file mode 100644 index 0000000..f0f9c12 --- /dev/null +++ b/lib/rsd.xml.php @@ -0,0 +1,15 @@ + + + oelna/microblog + https://github.com/oelna/microblog + + + + + + + diff --git a/lib/twitter_api.php b/lib/twitter_api.php new file mode 100644 index 0000000..d6e3a58 --- /dev/null +++ b/lib/twitter_api.php @@ -0,0 +1,410 @@ + + * @license MIT License + * @version 1.0.4 + * @link http://github.com/j7mbo/twitter-api-php + */ +class TwitterAPIExchange +{ + /** + * @var string + */ + private $oauth_access_token; + + /** + * @var string + */ + private $oauth_access_token_secret; + + /** + * @var string + */ + private $consumer_key; + + /** + * @var string + */ + private $consumer_secret; + + /** + * @var array + */ + private $postfields; + + /** + * @var string + */ + private $getfield; + + /** + * @var mixed + */ + protected $oauth; + + /** + * @var string + */ + public $url; + + /** + * @var string + */ + public $requestMethod; + + /** + * The HTTP status code from the previous request + * + * @var int + */ + protected $httpStatusCode; + + /** + * Create the API access object. Requires an array of settings:: + * oauth access token, oauth access token secret, consumer key, consumer secret + * These are all available by creating your own application on dev.twitter.com + * Requires the cURL library + * + * @throws \RuntimeException When cURL isn't loaded + * @throws \InvalidArgumentException When incomplete settings parameters are provided + * + * @param array $settings + */ + public function __construct(array $settings) + { + if (!function_exists('curl_init')) + { + throw new RuntimeException('TwitterAPIExchange requires cURL extension to be loaded, see: http://curl.haxx.se/docs/install.html'); + } + + if (!isset($settings['oauth_access_token']) + || !isset($settings['oauth_access_token_secret']) + || !isset($settings['consumer_key']) + || !isset($settings['consumer_secret'])) + { + throw new InvalidArgumentException('Incomplete settings passed to TwitterAPIExchange'); + } + + $this->oauth_access_token = $settings['oauth_access_token']; + $this->oauth_access_token_secret = $settings['oauth_access_token_secret']; + $this->consumer_key = $settings['consumer_key']; + $this->consumer_secret = $settings['consumer_secret']; + } + + /** + * Set postfields array, example: array('screen_name' => 'J7mbo') + * + * @param array $array Array of parameters to send to API + * + * @throws \Exception When you are trying to set both get and post fields + * + * @return TwitterAPIExchange Instance of self for method chaining + */ + public function setPostfields(array $array) + { + if (!is_null($this->getGetfield())) + { + throw new Exception('You can only choose get OR post fields (post fields include put).'); + } + + if (isset($array['status']) && substr($array['status'], 0, 1) === '@') + { + $array['status'] = sprintf("\0%s", $array['status']); + } + + foreach ($array as $key => &$value) + { + if (is_bool($value)) + { + $value = ($value === true) ? 'true' : 'false'; + } + } + + $this->postfields = $array; + + // rebuild oAuth + if (isset($this->oauth['oauth_signature'])) + { + $this->buildOauth($this->url, $this->requestMethod); + } + + return $this; + } + + /** + * Set getfield string, example: '?screen_name=J7mbo' + * + * @param string $string Get key and value pairs as string + * + * @throws \Exception + * + * @return \TwitterAPIExchange Instance of self for method chaining + */ + public function setGetfield($string) + { + if (!is_null($this->getPostfields())) + { + throw new Exception('You can only choose get OR post / post fields.'); + } + + $getfields = preg_replace('/^\?/', '', explode('&', $string)); + $params = array(); + + foreach ($getfields as $field) + { + if ($field !== '') + { + list($key, $value) = explode('=', $field); + $params[$key] = $value; + } + } + + $this->getfield = '?' . http_build_query($params, '', '&'); + + return $this; + } + + /** + * Get getfield string (simple getter) + * + * @return string $this->getfields + */ + public function getGetfield() + { + return $this->getfield; + } + + /** + * Get postfields array (simple getter) + * + * @return array $this->postfields + */ + public function getPostfields() + { + return $this->postfields; + } + + /** + * Build the Oauth object using params set in construct and additionals + * passed to this method. For v1.1, see: https://dev.twitter.com/docs/api/1.1 + * + * @param string $url The API url to use. Example: https://api.twitter.com/1.1/search/tweets.json + * @param string $requestMethod Either POST or GET + * + * @throws \Exception + * + * @return \TwitterAPIExchange Instance of self for method chaining + */ + public function buildOauth($url, $requestMethod) + { + if (!in_array(strtolower($requestMethod), array('post', 'get', 'put', 'delete'))) + { + throw new Exception('Request method must be either POST, GET or PUT or DELETE'); + } + + $consumer_key = $this->consumer_key; + $consumer_secret = $this->consumer_secret; + $oauth_access_token = $this->oauth_access_token; + $oauth_access_token_secret = $this->oauth_access_token_secret; + + $oauth = array( + 'oauth_consumer_key' => $consumer_key, + 'oauth_nonce' => time(), + 'oauth_signature_method' => 'HMAC-SHA1', + 'oauth_token' => $oauth_access_token, + 'oauth_timestamp' => time(), + 'oauth_version' => '1.0' + ); + + $getfield = $this->getGetfield(); + + if (!is_null($getfield)) + { + $getfields = str_replace('?', '', explode('&', $getfield)); + + foreach ($getfields as $g) + { + $split = explode('=', $g); + + /** In case a null is passed through **/ + if (isset($split[1])) + { + $oauth[$split[0]] = urldecode($split[1]); + } + } + } + + $postfields = $this->getPostfields(); + + if (!is_null($postfields)) { + foreach ($postfields as $key => $value) { + $oauth[$key] = $value; + } + } + + $base_info = $this->buildBaseString($url, $requestMethod, $oauth); + $composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret); + $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true)); + $oauth['oauth_signature'] = $oauth_signature; + + $this->url = $url; + $this->requestMethod = $requestMethod; + $this->oauth = $oauth; + + return $this; + } + + /** + * Perform the actual data retrieval from the API + * + * @param boolean $return If true, returns data. This is left in for backward compatibility reasons + * @param array $curlOptions Additional Curl options for this request + * + * @throws \Exception + * + * @return string json If $return param is true, returns json data. + */ + public function performRequest($return = true, $curlOptions = array()) + { + if (!is_bool($return)) + { + throw new Exception('performRequest parameter must be true or false'); + } + + $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); + + $getfield = $this->getGetfield(); + $postfields = $this->getPostfields(); + + if (in_array(strtolower($this->requestMethod), array('put', 'delete'))) + { + $curlOptions[CURLOPT_CUSTOMREQUEST] = $this->requestMethod; + } + + $options = $curlOptions + array( + CURLOPT_HTTPHEADER => $header, + CURLOPT_HEADER => false, + CURLOPT_URL => $this->url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ); + + if (!is_null($postfields)) + { + $options[CURLOPT_POSTFIELDS] = http_build_query($postfields, '', '&'); + } + else + { + if ($getfield !== '') + { + $options[CURLOPT_URL] .= $getfield; + } + } + + $feed = curl_init(); + curl_setopt_array($feed, $options); + $json = curl_exec($feed); + + $this->httpStatusCode = curl_getinfo($feed, CURLINFO_HTTP_CODE); + + if (($error = curl_error($feed)) !== '') + { + curl_close($feed); + + throw new \Exception($error); + } + + curl_close($feed); + + return $json; + } + + /** + * Private method to generate the base string used by cURL + * + * @param string $baseURI + * @param string $method + * @param array $params + * + * @return string Built base string + */ + private function buildBaseString($baseURI, $method, $params) + { + $return = array(); + ksort($params); + + foreach($params as $key => $value) + { + $return[] = rawurlencode($key) . '=' . rawurlencode($value); + } + + return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return)); + } + + /** + * Private method to generate authorization header used by cURL + * + * @param array $oauth Array of oauth data generated by buildOauth() + * + * @return string $return Header used by cURL for request + */ + private function buildAuthorizationHeader(array $oauth) + { + $return = 'Authorization: OAuth '; + $values = array(); + + foreach($oauth as $key => $value) + { + if (in_array($key, array('oauth_consumer_key', 'oauth_nonce', 'oauth_signature', + 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'))) { + $values[] = "$key=\"" . rawurlencode($value) . "\""; + } + } + + $return .= implode(', ', $values); + return $return; + } + + /** + * Helper method to perform our request + * + * @param string $url + * @param string $method + * @param string $data + * @param array $curlOptions + * + * @throws \Exception + * + * @return string The json response from the server + */ + public function request($url, $method = 'get', $data = null, $curlOptions = array()) + { + if (strtolower($method) === 'get') + { + $this->setGetfield($data); + } + else + { + $this->setPostfields($data); + } + + return $this->buildOauth($url, $method)->performRequest(true, $curlOptions); + } + + /** + * Get the HTTP status code for the previous request + * + * @return integer + */ + public function getHttpStatusCode() + { + return $this->httpStatusCode; + } +} diff --git a/lib/xmlrpc.php b/lib/xmlrpc.php new file mode 100644 index 0000000..47948eb --- /dev/null +++ b/lib/xmlrpc.php @@ -0,0 +1,428 @@ + (int) $post['id'], + 'date_created' => $date_created, + 'date_modified' => $date_modified, + 'permalink' => $config['url'].'/'.$post['id'], + 'title' => '', + 'description' => ($post['post_content']), + 'categories' => [], + 'post_status' => 'published', + 'author' => [ + 'name' => $config['microblog_account'], + 'username' => $config['admin_user'] + ] + ]; + + return $mb_post; + } else { + // 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_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_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 + if($method_name == 'microblog.getCategories') { + $categories = [ + /* + [ + 'id' => '1', + 'name' => 'default', + ] + */ + ]; + } else { + $categories = [ + /* + [ + 'description' => 'Default', + 'htmlUrl' => '', + 'rssUrl' => '', + 'title' => 'default', + 'categoryid' => '1', + ] + */ + ]; + } + + return $categories; +} + +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; +} + +function mw_get_recent_posts($method_name, $args) { + + list($_, $username, $password, $amount) = $args; + $offset = 0; + if($method_name == 'microblog.getPosts' && !empty($args[4])) { + $offset = $args[4]; + } + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + if(!$amount) $amount = 25; + $amount = min($amount, 200); // cap the max available posts at 200 (?) + + $posts = db_select_posts(null, $amount, 'asc', $offset); + if(empty($posts)) return []; + + // call make_post() on all items + $mw_posts = array_map('make_post', $posts, array_fill(0, count($posts), $method_name)); + + 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) { + if($method_name == 'microblog.getPost') { + $mw_post = make_post($post, $method_name); + } else { + $mw_post = make_post($post); + } + + return $mw_post; + } else { + return [ + 'faultCode' => 400, + 'faultString' => 'Could not fetch 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.' + ]; + } + + if($method_name == 'microblog.newPost') { + $post = [ + // 'post_title' => $content['title'], + 'post_content' => $content['description'], + 'post_timestamp' => time(), + // 'post_categories' => $content['categories'], + // 'post_status' => $content['post_status'], + ]; + + // use a specific timestamp, if provided + if(isset($content['date_created'])) { + $post['post_timestamp'] = $content['date_created']->timestamp; + } + } else { + $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 + list($post_id, $username, $password, $content, $_) = $args; + + if(!check_credentials($username, $password)) { + return [ + 'faultCode' => 403, + 'faultString' => 'Incorrect username or password.' + ]; + } + + if($method_name == 'microblog.editPost') { + $post = [ + // 'post_title' => $content['title'], + 'post_content' => $content['description'], + 'post_timestamp' => null, + // 'post_categories' => $content['categories'], + // 'post_status' => $content['post_status'], + ]; + + if(!empty($content['date_created'])) { + $post['post_timestamp'] = $content['date_created']->timestamp; + } + } else { + $post = [ + // 'post_hp' => $content['flNotOnHomePage'], + // 'post_title' => $content['title'], + 'post_timestamp' => null, + 'post_content' => $content['description'], + // 'post_url' => $content['link'], + ]; + + if(!empty($content['dateCreated'])) { + $post['post_timestamp'] = $content['dateCreated']->timestamp; + } + } + + $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_delete_post($method_name, $args) { + + if($method_name == 'microblog.deletePost') { + list($post_id, $username, $password) = $args; + } else { + // blogger.deletePost + 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.' + ]; + } +} + +// 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, 'blogger.deletePost', 'mw_delete_post'); + +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, 'metaWeblog.newMediaObject', 'mw_new_mediaobject'); + +// non-standard convenience? +xmlrpc_server_register_method($server, 'metaWeblog.getPosts', 'mw_get_recent_posts'); +xmlrpc_server_register_method($server, 'metaWeblog.deletePost', 'mw_delete_post'); + +// micro.blog API methods (currently just using the metaWeblog functions) +// https://help.micro.blog/t/micro-blog-xml-rpc-api/108 +xmlrpc_server_register_method($server, 'microblog.getCategories', 'mw_get_categories'); +xmlrpc_server_register_method($server, 'microblog.getPosts', 'mw_get_recent_posts'); +xmlrpc_server_register_method($server, 'microblog.getPost', 'mw_get_post'); +xmlrpc_server_register_method($server, 'microblog.newPost', 'mw_new_post'); +xmlrpc_server_register_method($server, 'microblog.editPost', 'mw_edit_post'); +xmlrpc_server_register_method($server, 'microblog.deletePost', 'mw_delete_post'); +// xmlrpc_server_register_method($server, 'microblog.newMediaObject', 'mw_new_mediaobject'); + +// micro.blog pages are not supported +/* +xmlrpc_server_register_method($server, 'microblog.getPages', 'say_hello'); +xmlrpc_server_register_method($server, 'microblog.getPage', 'say_hello'); +xmlrpc_server_register_method($server, 'microblog.newPage', 'say_hello'); +xmlrpc_server_register_method($server, 'microblog.editPage', 'say_hello'); +xmlrpc_server_register_method($server, 'microblog.deletePage', 'say_hello'); +*/ + +// 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'); + // error_log($request_xml."\n\n".$response."\n\n", 3, $logfile); + echo($response); +} diff --git a/lib_autolink.php b/lib_autolink.php deleted file mode 100644 index da39c03..0000000 --- a/lib_autolink.php +++ /dev/null @@ -1,335 +0,0 @@ - - # This code is licensed under the MIT license - # - - #################################################################### - - # - # These are global options. You can set them before calling the autolinking - # functions to change the output. - # - - $GLOBALS['autolink_options'] = array( - - # Should http:// be visibly stripped from the front - # of URLs? - 'strip_protocols' => true, - - ); - - #################################################################### - - function autolink($text, $limit=30, $tagfill='', $auto_title = true){ - - $text = autolink_do($text, '![a-z][a-z-]+://!i', $limit, $tagfill, $auto_title); - $text = autolink_do($text, '!(mailto|skype):!i', $limit, $tagfill, $auto_title); - $text = autolink_do($text, '!www\\.!i', $limit, $tagfill, $auto_title, 'http://'); - return $text; - } - - #################################################################### - - function autolink_do($text, $sub, $limit, $tagfill, $auto_title, $force_prefix=null){ - - $text_l = StrToLower($text); - $cursor = 0; - $loop = 1; - $buffer = ''; - - while (($cursor < strlen($text)) && $loop){ - - $ok = 1; - $matched = preg_match($sub, $text_l, $m, PREG_OFFSET_CAPTURE, $cursor); - - if (!$matched){ - - $loop = 0; - $ok = 0; - - }else{ - - $pos = $m[0][1]; - $sub_len = strlen($m[0][0]); - - $pre_hit = substr($text, $cursor, $pos-$cursor); - $hit = substr($text, $pos, $sub_len); - $pre = substr($text, 0, $pos); - $post = substr($text, $pos + $sub_len); - - $fail_text = $pre_hit.$hit; - $fail_len = strlen($fail_text); - - # - # substring found - first check to see if we're inside a link tag already... - # - - $bits = preg_split("!!i", $pre); - $last_bit = array_pop($bits); - if (preg_match("!\n"; - - $ok = 0; - $cursor += $fail_len; - $buffer .= $fail_text; - } - } - - # - # looks like a nice spot to autolink from - check the pre - # to see if there was whitespace before this match - # - - if ($ok){ - - if ($pre){ - if (!preg_match('![\s\(\[\{>\pZ\p{Cc}]$!s', $pre)){ - - #echo "fail 2 at $cursor ($pre)
\n"; - - $ok = 0; - $cursor += $fail_len; - $buffer .= $fail_text; - } - } - } - - # - # we want to autolink here - find the extent of the url - # - - if ($ok){ - if (preg_match('/^([a-z0-9\-\.\/\-_%~!?=,:;&+*#@\(\)\$]+)/i', $post, $matches)){ - - $url = $hit.$matches[1]; - - $cursor += strlen($url) + strlen($pre_hit); - $buffer .= $pre_hit; - - $url = html_entity_decode($url); - - - # - # remove trailing punctuation from url - # - - while (preg_match('|[.,!;:?]$|', $url)){ - $url = substr($url, 0, strlen($url)-1); - $cursor--; - } - foreach (array('()', '[]', '{}') as $pair){ - $o = substr($pair, 0, 1); - $c = substr($pair, 1, 1); - if (preg_match("!^(\\$c|^)[^\\$o]+\\$c$!", $url)){ - $url = substr($url, 0, strlen($url)-1); - $cursor--; - } - } - - - # - # nice-i-fy url here - # - - $link_url = $url; - $display_url = $url; - - if ($force_prefix) $link_url = $force_prefix.$link_url; - - if ($GLOBALS['autolink_options']['strip_protocols']){ - if (preg_match('!^(http|https)://!i', $display_url, $m)){ - - $display_url = substr($display_url, strlen($m[1])+3); - } - } - - $display_url = autolink_label($display_url, $limit); - - - # - # add the url - # - - $currentTagfill = $tagfill; - if ($display_url != $link_url && !preg_match('@title=@msi',$currentTagfill) && $auto_title) { - - $display_quoted = preg_quote($display_url, '!'); - - if (!preg_match("!^(http|https)://{$display_quoted}$!i", $link_url)){ - - $currentTagfill .= ' title="'.$link_url.'"'; - } - } - - $link_url_enc = HtmlSpecialChars($link_url); - $display_url_enc = HtmlSpecialChars($display_url); - - $buffer .= "{$display_url_enc}"; - - }else{ - #echo "fail 3 at $cursor
\n"; - - $ok = 0; - $cursor += $fail_len; - $buffer .= $fail_text; - } - } - - } - - # - # add everything from the cursor to the end onto the buffer. - # - - $buffer .= substr($text, $cursor); - - return $buffer; - } - - #################################################################### - - function autolink_label($text, $limit){ - - if (!$limit){ return $text; } - - if (strlen($text) > $limit){ - return substr($text, 0, $limit-3).'...'; - } - - return $text; - } - - #################################################################### - - function autolink_email($text, $tagfill=''){ - - $atom = '[^()<>@,;:\\\\".\\[\\]\\x00-\\x20\\x7f]+'; # from RFC822 - - #die($atom); - - $text_l = StrToLower($text); - $cursor = 0; - $loop = 1; - $buffer = ''; - - while(($cursor < strlen($text)) && $loop){ - - # - # find an '@' symbol - # - - $ok = 1; - $pos = strpos($text_l, '@', $cursor); - - if ($pos === false){ - - $loop = 0; - $ok = 0; - - }else{ - - $pre = substr($text, $cursor, $pos-$cursor); - $hit = substr($text, $pos, 1); - $post = substr($text, $pos + 1); - - $fail_text = $pre.$hit; - $fail_len = strlen($fail_text); - - #die("$pre::$hit::$post::$fail_text"); - - # - # substring found - first check to see if we're inside a link tag already... - # - - $bits = preg_split("!!i", $pre); - $last_bit = array_pop($bits); - if (preg_match("!\n"; - - $ok = 0; - $cursor += $fail_len; - $buffer .= $fail_text; - } - } - - # - # check backwards - # - - if ($ok){ - if (preg_match("!($atom(\.$atom)*)\$!", $pre, $matches)){ - - # move matched part of address into $hit - - $len = strlen($matches[1]); - $plen = strlen($pre); - - $hit = substr($pre, $plen-$len).$hit; - $pre = substr($pre, 0, $plen-$len); - - }else{ - - #echo "fail 2 at $cursor ($pre)
\n"; - - $ok = 0; - $cursor += $fail_len; - $buffer .= $fail_text; - } - } - - # - # check forwards - # - - if ($ok){ - if (preg_match("!^($atom(\.$atom)*)!", $post, $matches)){ - - # move matched part of address into $hit - - $len = strlen($matches[1]); - - $hit .= substr($post, 0, $len); - $post = substr($post, $len); - - }else{ - #echo "fail 3 at $cursor ($post)
\n"; - - $ok = 0; - $cursor += $fail_len; - $buffer .= $fail_text; - } - } - - # - # commit - # - - if ($ok) { - - $cursor += strlen($pre) + strlen($hit); - $buffer .= $pre; - $buffer .= "$hit"; - - } - - } - - # - # add everything from the cursor to the end onto the buffer. - # - - $buffer .= substr($text, $cursor); - - return $buffer; - } - - #################################################################### - -?> diff --git a/loginform.inc.php b/loginform.inc.php deleted file mode 100644 index c218baa..0000000 --- a/loginform.inc.php +++ /dev/null @@ -1,64 +0,0 @@ - 'error', - 'message' => 'You entered wrong user credentials. Please try again.' - ); - } - } - - header('Content-Type: text/html; charset=utf-8'); - -?> - - - - <?= empty($config['microblog_account']) ? "" : $config['microblog_account'] . "'s "; ?>micro.blog - - - - - - - - -
- -

Please enter your login information.

- -

- -
-
-
- -
-
- - - diff --git a/microblog.css b/microblog.css deleted file mode 100644 index 29ceae6..0000000 --- a/microblog.css +++ /dev/null @@ -1,256 +0,0 @@ -* { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - box-sizing: border-box; -} - -html { - font: 100%/1.4 system-ui, Helvetica, sans-serif; - background-color: #b5b5af; - color: #080f15; -} - -.wrap { - width: min(95%, 40rem); - margin: 2rem auto; - padding: 1rem; - background-color: #fffceb; - box-shadow: 0 1.25rem 1rem -1rem rgba(0,0,0,0.25); -} - -.button { - display: block; - background: #007aff; - color: #fffceb; - text-decoration: none; - border-radius: 0.4rem; - padding: 0.2rem 0.5rem; - font-weight: bold; - text-align: center; -} - -.button.alert { - background: coral; -} - -nav.main ul { - display: flex; - margin-bottom: 2rem; -} - -nav.main li { - list-style: none; - flex: 1; -} - -nav.main li a { - /* inherits from .button */ -} - -nav.main li + li a { - margin-left: 0.2rem; -} - -.wrap .posts { - -} - -.wrap .posts > li { - list-style: none; - margin-bottom: 1rem; - border-bottom: 1px solid rgba(0,0,0,0.1); - padding-bottom: 1rem; - - display: grid; - grid-template-columns: repeat(6, 1fr); -} - -.wrap .posts > li > * { - outline: 0px solid #f0f; -} - -.wrap .posts > li:last-child { - margin-bottom: 2rem; - border-bottom: 0; - padding-bottom: 0; -} - -.timeline .pagination { overflow: hidden; } -.timeline .pagination a { - display: block; - background: #007aff; - color: #fffceb; - text-decoration: none; - border-radius: 0.4rem; - padding: 0.2rem 0.6rem; - font-weight: bold; - float: left; -} -.timeline .pagination .next { float: right; } - -.wrap .post-timestamp { - display: block; - color: #007aff; - text-decoration: none; - font-size: 0.8rem; - text-transform: uppercase; - font-weight: bold; - margin-bottom: 0.5rem; - grid-column-start: span 3; -} - -.wrap .post-meta { - grid-column-start: span 3; -} - -.wrap .post-meta ul { - display: flex; - justify-content: flex-end; - gap: 0.75ch; -} - -.wrap .post-meta li { - list-style: none; -} - -.wrap .post-meta li a { - display: block; - color: hsla(0, 0%, 0%, 0.2); - mix-blend-mode: multiply; - text-decoration: none; - font-size: 0.8rem; - text-transform: uppercase; - font-weight: bold; - margin-bottom: 0.5rem; -} - -.wrap .post-meta li a:is(:hover, :focus) { - color: currentColor; -} - -.wrap .post-content { - font-size: 1.25rem; - overflow-wrap: break-word; - grid-column-start: span 6; -} - -.wrap .post-content a { - color: #007aff; - text-decoration: none; -} - -.wrap form.delete { - width: 100%; - grid-column-start: span 6; - display: flex; - margin-block-start: 2rem; -} - -.wrap form.delete input[type="submit"] { - flex: 1; - line-height: 1.4; - cursor: pointer; -} - -.wrap .posts li .message { - width: 100%; - grid-column-start: span 6; - margin-block-start: 2rem; -} - -.postform form, -form.edit, -.login form { - grid-column-start: span 6; - overflow: hidden; -} - -:is(.postform, .edit) textarea { - width: 100%; - border: 2px solid #b5b5af; - padding: 0.5rem; - font-size: 1.25rem; - resize: vertical; - min-height: 10rem; - margin-bottom: 0.5rem; -} - -:is(.postform, .edit) textarea:focus { - border-color: #007aff; - outline: none; -} - -:is(.postform, .edit) input[type="submit"], -.login input[type="submit"] { - -webkit-appearance: none; - appearance: none; - border: 0; - display: block; - background: #007aff; - color: #fffceb; - text-decoration: none; - border-radius: 0.4rem; - padding: 0.3rem 0.8rem 0.4rem; - font-weight: bold; - text-align: center; - cursor: pointer; - float: right; -} - -:is(.postform, .edit) #count { - float: left; - color: #b5b5af; -} - -:is(.postform, .edit) .message, -.login .message { - background-color: #87b26c; - padding: 0.5rem; - color: #fffceb; - border-radius: 0.4rem; - margin-bottom: 0.5rem; -} - -:is(.postform, .edit) .error, -.login .error { - background-color: #9c2128; -} - -.login input[type="text"], -.login input[type="password"] { - width: 100%; - border: 2px solid #b5b5af; - padding: 0.5rem; - font-size: 1.25rem; - resize: vertical; - margin-bottom: 0.5rem; -} - -.login input[type="text"]:focus, -.login input[type="password"]:focus { - border-color: #007aff; - outline: none; -} - -footer { - width: min(95%, 40rem); - margin: 0.5rem auto 2rem; -} - -footer ul { - list-style: none; - display: flex; - justify-content: center; - gap: 1rem; -} - -footer li a { - color: hsla(0, 0%, 0%, 0.3); - text-decoration: none; - font-size: 0.8rem; - text-transform: uppercase; - font-weight: bold; - margin-bottom: 0.5rem; -} diff --git a/postform.inc.php b/postform.inc.php deleted file mode 100644 index 8ac41e4..0000000 --- a/postform.inc.php +++ /dev/null @@ -1,79 +0,0 @@ - 0) { - $message = array( - 'status' => 'success', - 'message' => 'Successfully posted status #'.$id - ); - - rebuild_feeds(); - if($config['ping'] == true) ping_microblog(); - if($config['crosspost_to_twitter'] == true) { - $twitter_response = json_decode(twitter_post_status($_POST['content']), true); - - if(!empty($twitter_response['errors'])) { - $message['message'] .= ' (But crossposting to twitter failed!)'; - } - } - - header('Location: '.$config['url']); - die(); - } - } - - header('Content-Type: text/html; charset=utf-8'); - -?> - - - - micro.blog - - - - - - - - -
- - -

- -
- -

- -
-
- - - diff --git a/rsd.xml.php b/rsd.xml.php deleted file mode 100644 index 910e6b2..0000000 --- a/rsd.xml.php +++ /dev/null @@ -1,15 +0,0 @@ - - - oelna/microblog - https://github.com/oelna/microblog - - - - - - - diff --git a/single.inc.php b/single.inc.php deleted file mode 100644 index f10db2f..0000000 --- a/single.inc.php +++ /dev/null @@ -1,123 +0,0 @@ - - - - - <?= empty($config['microblog_account']) ? "" : $config['microblog_account'] . "'s " ?>micro.blog - entry #<?= $id ?> - - - - - - - - -
- -
    - -
  • - -
    - -

    - - - - -
    - - - - -

    - -
    - - - -
    - -

    - - - -
  • - -

    No post with this ID.

    - -
-
- - - diff --git a/snippets/footer.snippet.php b/snippets/footer.snippet.php new file mode 100644 index 0000000..d2238f0 --- /dev/null +++ b/snippets/footer.snippet.php @@ -0,0 +1,10 @@ + diff --git a/snippets/header.snippet.php b/snippets/header.snippet.php new file mode 100644 index 0000000..d3270d8 --- /dev/null +++ b/snippets/header.snippet.php @@ -0,0 +1,27 @@ + + + + + + <?= empty($config['microblog_account']) ? "" : $config['microblog_account'] . "'s "; ?>micro.blog<?= $title_suffix ?> + + + + + + + + + + + diff --git a/snippets/nav.snippet.php b/snippets/nav.snippet.php new file mode 100644 index 0000000..3628451 --- /dev/null +++ b/snippets/nav.snippet.php @@ -0,0 +1,7 @@ + diff --git a/templates/loginform.inc.php b/templates/loginform.inc.php new file mode 100644 index 0000000..b852de7 --- /dev/null +++ b/templates/loginform.inc.php @@ -0,0 +1,39 @@ + 'error', + 'message' => 'You entered wrong user credentials. Please try again.' + ); + } + } + + $title_suffix = 'login'; + require(ROOT.DS.'snippets'.DS.'header.snippet.php'); + +?> +
+ +

Please enter your login information.

+ +

+ +
+
+
+ +
+
+ + + diff --git a/templates/postform.inc.php b/templates/postform.inc.php new file mode 100644 index 0000000..149028b --- /dev/null +++ b/templates/postform.inc.php @@ -0,0 +1,54 @@ + 0) { + $message = array( + 'status' => 'success', + 'message' => 'Successfully posted status #'.$id + ); + + rebuild_feeds(); + if($config['ping'] == true) ping_microblog(); + if($config['crosspost_to_twitter'] == true) { + $twitter_response = json_decode(twitter_post_status($_POST['content']), true); + + if(!empty($twitter_response['errors'])) { + $message['message'] .= ' (But crossposting to twitter failed!)'; + } + } + + header('Location: '.$config['url']); + die(); + } + } + + $title_suffix = 'new post'; + require(ROOT.DS.'snippets'.DS.'header.snippet.php'); + +?> +
+ + +

+ +
+ +

+ +
+
+ + + diff --git a/templates/single.inc.php b/templates/single.inc.php new file mode 100644 index 0000000..af486d5 --- /dev/null +++ b/templates/single.inc.php @@ -0,0 +1,100 @@ + +
+ +
    + +
  • + +
    + +

    + + + + +
    + + + + +

    + +
    + + + +
    + +

    + + + +
  • + +

    No post with this ID.

    + +
+
+ + + diff --git a/templates/timeline.inc.php b/templates/timeline.inc.php new file mode 100644 index 0000000..234ea83 --- /dev/null +++ b/templates/timeline.inc.php @@ -0,0 +1,57 @@ + +
+ + + +

No posts found.

+ + +
+ + + diff --git a/timeline.inc.php b/timeline.inc.php deleted file mode 100644 index 35f7b9d..0000000 --- a/timeline.inc.php +++ /dev/null @@ -1,83 +0,0 @@ - - - - - <?= empty($config['microblog_account']) ? "" : $config['microblog_account'] . "'s "; ?>micro.blog - - - - - - - - -
- - - -

No posts found.

- - -
- - - diff --git a/twitter_api.php b/twitter_api.php deleted file mode 100644 index d6e3a58..0000000 --- a/twitter_api.php +++ /dev/null @@ -1,410 +0,0 @@ - - * @license MIT License - * @version 1.0.4 - * @link http://github.com/j7mbo/twitter-api-php - */ -class TwitterAPIExchange -{ - /** - * @var string - */ - private $oauth_access_token; - - /** - * @var string - */ - private $oauth_access_token_secret; - - /** - * @var string - */ - private $consumer_key; - - /** - * @var string - */ - private $consumer_secret; - - /** - * @var array - */ - private $postfields; - - /** - * @var string - */ - private $getfield; - - /** - * @var mixed - */ - protected $oauth; - - /** - * @var string - */ - public $url; - - /** - * @var string - */ - public $requestMethod; - - /** - * The HTTP status code from the previous request - * - * @var int - */ - protected $httpStatusCode; - - /** - * Create the API access object. Requires an array of settings:: - * oauth access token, oauth access token secret, consumer key, consumer secret - * These are all available by creating your own application on dev.twitter.com - * Requires the cURL library - * - * @throws \RuntimeException When cURL isn't loaded - * @throws \InvalidArgumentException When incomplete settings parameters are provided - * - * @param array $settings - */ - public function __construct(array $settings) - { - if (!function_exists('curl_init')) - { - throw new RuntimeException('TwitterAPIExchange requires cURL extension to be loaded, see: http://curl.haxx.se/docs/install.html'); - } - - if (!isset($settings['oauth_access_token']) - || !isset($settings['oauth_access_token_secret']) - || !isset($settings['consumer_key']) - || !isset($settings['consumer_secret'])) - { - throw new InvalidArgumentException('Incomplete settings passed to TwitterAPIExchange'); - } - - $this->oauth_access_token = $settings['oauth_access_token']; - $this->oauth_access_token_secret = $settings['oauth_access_token_secret']; - $this->consumer_key = $settings['consumer_key']; - $this->consumer_secret = $settings['consumer_secret']; - } - - /** - * Set postfields array, example: array('screen_name' => 'J7mbo') - * - * @param array $array Array of parameters to send to API - * - * @throws \Exception When you are trying to set both get and post fields - * - * @return TwitterAPIExchange Instance of self for method chaining - */ - public function setPostfields(array $array) - { - if (!is_null($this->getGetfield())) - { - throw new Exception('You can only choose get OR post fields (post fields include put).'); - } - - if (isset($array['status']) && substr($array['status'], 0, 1) === '@') - { - $array['status'] = sprintf("\0%s", $array['status']); - } - - foreach ($array as $key => &$value) - { - if (is_bool($value)) - { - $value = ($value === true) ? 'true' : 'false'; - } - } - - $this->postfields = $array; - - // rebuild oAuth - if (isset($this->oauth['oauth_signature'])) - { - $this->buildOauth($this->url, $this->requestMethod); - } - - return $this; - } - - /** - * Set getfield string, example: '?screen_name=J7mbo' - * - * @param string $string Get key and value pairs as string - * - * @throws \Exception - * - * @return \TwitterAPIExchange Instance of self for method chaining - */ - public function setGetfield($string) - { - if (!is_null($this->getPostfields())) - { - throw new Exception('You can only choose get OR post / post fields.'); - } - - $getfields = preg_replace('/^\?/', '', explode('&', $string)); - $params = array(); - - foreach ($getfields as $field) - { - if ($field !== '') - { - list($key, $value) = explode('=', $field); - $params[$key] = $value; - } - } - - $this->getfield = '?' . http_build_query($params, '', '&'); - - return $this; - } - - /** - * Get getfield string (simple getter) - * - * @return string $this->getfields - */ - public function getGetfield() - { - return $this->getfield; - } - - /** - * Get postfields array (simple getter) - * - * @return array $this->postfields - */ - public function getPostfields() - { - return $this->postfields; - } - - /** - * Build the Oauth object using params set in construct and additionals - * passed to this method. For v1.1, see: https://dev.twitter.com/docs/api/1.1 - * - * @param string $url The API url to use. Example: https://api.twitter.com/1.1/search/tweets.json - * @param string $requestMethod Either POST or GET - * - * @throws \Exception - * - * @return \TwitterAPIExchange Instance of self for method chaining - */ - public function buildOauth($url, $requestMethod) - { - if (!in_array(strtolower($requestMethod), array('post', 'get', 'put', 'delete'))) - { - throw new Exception('Request method must be either POST, GET or PUT or DELETE'); - } - - $consumer_key = $this->consumer_key; - $consumer_secret = $this->consumer_secret; - $oauth_access_token = $this->oauth_access_token; - $oauth_access_token_secret = $this->oauth_access_token_secret; - - $oauth = array( - 'oauth_consumer_key' => $consumer_key, - 'oauth_nonce' => time(), - 'oauth_signature_method' => 'HMAC-SHA1', - 'oauth_token' => $oauth_access_token, - 'oauth_timestamp' => time(), - 'oauth_version' => '1.0' - ); - - $getfield = $this->getGetfield(); - - if (!is_null($getfield)) - { - $getfields = str_replace('?', '', explode('&', $getfield)); - - foreach ($getfields as $g) - { - $split = explode('=', $g); - - /** In case a null is passed through **/ - if (isset($split[1])) - { - $oauth[$split[0]] = urldecode($split[1]); - } - } - } - - $postfields = $this->getPostfields(); - - if (!is_null($postfields)) { - foreach ($postfields as $key => $value) { - $oauth[$key] = $value; - } - } - - $base_info = $this->buildBaseString($url, $requestMethod, $oauth); - $composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret); - $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true)); - $oauth['oauth_signature'] = $oauth_signature; - - $this->url = $url; - $this->requestMethod = $requestMethod; - $this->oauth = $oauth; - - return $this; - } - - /** - * Perform the actual data retrieval from the API - * - * @param boolean $return If true, returns data. This is left in for backward compatibility reasons - * @param array $curlOptions Additional Curl options for this request - * - * @throws \Exception - * - * @return string json If $return param is true, returns json data. - */ - public function performRequest($return = true, $curlOptions = array()) - { - if (!is_bool($return)) - { - throw new Exception('performRequest parameter must be true or false'); - } - - $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); - - $getfield = $this->getGetfield(); - $postfields = $this->getPostfields(); - - if (in_array(strtolower($this->requestMethod), array('put', 'delete'))) - { - $curlOptions[CURLOPT_CUSTOMREQUEST] = $this->requestMethod; - } - - $options = $curlOptions + array( - CURLOPT_HTTPHEADER => $header, - CURLOPT_HEADER => false, - CURLOPT_URL => $this->url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 10, - ); - - if (!is_null($postfields)) - { - $options[CURLOPT_POSTFIELDS] = http_build_query($postfields, '', '&'); - } - else - { - if ($getfield !== '') - { - $options[CURLOPT_URL] .= $getfield; - } - } - - $feed = curl_init(); - curl_setopt_array($feed, $options); - $json = curl_exec($feed); - - $this->httpStatusCode = curl_getinfo($feed, CURLINFO_HTTP_CODE); - - if (($error = curl_error($feed)) !== '') - { - curl_close($feed); - - throw new \Exception($error); - } - - curl_close($feed); - - return $json; - } - - /** - * Private method to generate the base string used by cURL - * - * @param string $baseURI - * @param string $method - * @param array $params - * - * @return string Built base string - */ - private function buildBaseString($baseURI, $method, $params) - { - $return = array(); - ksort($params); - - foreach($params as $key => $value) - { - $return[] = rawurlencode($key) . '=' . rawurlencode($value); - } - - return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return)); - } - - /** - * Private method to generate authorization header used by cURL - * - * @param array $oauth Array of oauth data generated by buildOauth() - * - * @return string $return Header used by cURL for request - */ - private function buildAuthorizationHeader(array $oauth) - { - $return = 'Authorization: OAuth '; - $values = array(); - - foreach($oauth as $key => $value) - { - if (in_array($key, array('oauth_consumer_key', 'oauth_nonce', 'oauth_signature', - 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'))) { - $values[] = "$key=\"" . rawurlencode($value) . "\""; - } - } - - $return .= implode(', ', $values); - return $return; - } - - /** - * Helper method to perform our request - * - * @param string $url - * @param string $method - * @param string $data - * @param array $curlOptions - * - * @throws \Exception - * - * @return string The json response from the server - */ - public function request($url, $method = 'get', $data = null, $curlOptions = array()) - { - if (strtolower($method) === 'get') - { - $this->setGetfield($data); - } - else - { - $this->setPostfields($data); - } - - return $this->buildOauth($url, $method)->performRequest(true, $curlOptions); - } - - /** - * Get the HTTP status code for the previous request - * - * @return integer - */ - public function getHttpStatusCode() - { - return $this->httpStatusCode; - } -} diff --git a/xmlrpc.php b/xmlrpc.php deleted file mode 100644 index 47948eb..0000000 --- a/xmlrpc.php +++ /dev/null @@ -1,428 +0,0 @@ - (int) $post['id'], - 'date_created' => $date_created, - 'date_modified' => $date_modified, - 'permalink' => $config['url'].'/'.$post['id'], - 'title' => '', - 'description' => ($post['post_content']), - 'categories' => [], - 'post_status' => 'published', - 'author' => [ - 'name' => $config['microblog_account'], - 'username' => $config['admin_user'] - ] - ]; - - return $mb_post; - } else { - // 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_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_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 - if($method_name == 'microblog.getCategories') { - $categories = [ - /* - [ - 'id' => '1', - 'name' => 'default', - ] - */ - ]; - } else { - $categories = [ - /* - [ - 'description' => 'Default', - 'htmlUrl' => '', - 'rssUrl' => '', - 'title' => 'default', - 'categoryid' => '1', - ] - */ - ]; - } - - return $categories; -} - -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; -} - -function mw_get_recent_posts($method_name, $args) { - - list($_, $username, $password, $amount) = $args; - $offset = 0; - if($method_name == 'microblog.getPosts' && !empty($args[4])) { - $offset = $args[4]; - } - - if(!check_credentials($username, $password)) { - return [ - 'faultCode' => 403, - 'faultString' => 'Incorrect username or password.' - ]; - } - - if(!$amount) $amount = 25; - $amount = min($amount, 200); // cap the max available posts at 200 (?) - - $posts = db_select_posts(null, $amount, 'asc', $offset); - if(empty($posts)) return []; - - // call make_post() on all items - $mw_posts = array_map('make_post', $posts, array_fill(0, count($posts), $method_name)); - - 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) { - if($method_name == 'microblog.getPost') { - $mw_post = make_post($post, $method_name); - } else { - $mw_post = make_post($post); - } - - return $mw_post; - } else { - return [ - 'faultCode' => 400, - 'faultString' => 'Could not fetch 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.' - ]; - } - - if($method_name == 'microblog.newPost') { - $post = [ - // 'post_title' => $content['title'], - 'post_content' => $content['description'], - 'post_timestamp' => time(), - // 'post_categories' => $content['categories'], - // 'post_status' => $content['post_status'], - ]; - - // use a specific timestamp, if provided - if(isset($content['date_created'])) { - $post['post_timestamp'] = $content['date_created']->timestamp; - } - } else { - $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 - list($post_id, $username, $password, $content, $_) = $args; - - if(!check_credentials($username, $password)) { - return [ - 'faultCode' => 403, - 'faultString' => 'Incorrect username or password.' - ]; - } - - if($method_name == 'microblog.editPost') { - $post = [ - // 'post_title' => $content['title'], - 'post_content' => $content['description'], - 'post_timestamp' => null, - // 'post_categories' => $content['categories'], - // 'post_status' => $content['post_status'], - ]; - - if(!empty($content['date_created'])) { - $post['post_timestamp'] = $content['date_created']->timestamp; - } - } else { - $post = [ - // 'post_hp' => $content['flNotOnHomePage'], - // 'post_title' => $content['title'], - 'post_timestamp' => null, - 'post_content' => $content['description'], - // 'post_url' => $content['link'], - ]; - - if(!empty($content['dateCreated'])) { - $post['post_timestamp'] = $content['dateCreated']->timestamp; - } - } - - $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_delete_post($method_name, $args) { - - if($method_name == 'microblog.deletePost') { - list($post_id, $username, $password) = $args; - } else { - // blogger.deletePost - 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.' - ]; - } -} - -// 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, 'blogger.deletePost', 'mw_delete_post'); - -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, 'metaWeblog.newMediaObject', 'mw_new_mediaobject'); - -// non-standard convenience? -xmlrpc_server_register_method($server, 'metaWeblog.getPosts', 'mw_get_recent_posts'); -xmlrpc_server_register_method($server, 'metaWeblog.deletePost', 'mw_delete_post'); - -// micro.blog API methods (currently just using the metaWeblog functions) -// https://help.micro.blog/t/micro-blog-xml-rpc-api/108 -xmlrpc_server_register_method($server, 'microblog.getCategories', 'mw_get_categories'); -xmlrpc_server_register_method($server, 'microblog.getPosts', 'mw_get_recent_posts'); -xmlrpc_server_register_method($server, 'microblog.getPost', 'mw_get_post'); -xmlrpc_server_register_method($server, 'microblog.newPost', 'mw_new_post'); -xmlrpc_server_register_method($server, 'microblog.editPost', 'mw_edit_post'); -xmlrpc_server_register_method($server, 'microblog.deletePost', 'mw_delete_post'); -// xmlrpc_server_register_method($server, 'microblog.newMediaObject', 'mw_new_mediaobject'); - -// micro.blog pages are not supported -/* -xmlrpc_server_register_method($server, 'microblog.getPages', 'say_hello'); -xmlrpc_server_register_method($server, 'microblog.getPage', 'say_hello'); -xmlrpc_server_register_method($server, 'microblog.newPage', 'say_hello'); -xmlrpc_server_register_method($server, 'microblog.editPage', 'say_hello'); -xmlrpc_server_register_method($server, 'microblog.deletePage', 'say_hello'); -*/ - -// 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'); - // error_log($request_xml."\n\n".$response."\n\n", 3, $logfile); - echo($response); -} -- cgit v1.2.3