-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Scott Merrill
committed
Mar 27, 2018
1 parent
4df701a
commit c329f01
Showing
6 changed files
with
520 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
secret.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,19 @@ | ||
# micropub | ||
a minimal PHP micropub endpoint, with media support | ||
|
||
This is based heavily off of the following projects: | ||
* [rhiaro's MVP micropub](https://rhiaro.co.uk/2015/04/minimum-viable-micropub) | ||
* [dgold's Nanopub](https://github.com/dg01d/nanopub/) | ||
* [aaronpk's MVP Media Endpoint](https://gist.github.com/aaronpk/4bee1753688ca9f3036d6f31377edf14) | ||
|
||
My personal setup is a little convoluted. I run a variety of sites on my server, all with different document roots. I run PHP in a container, which mounts my host's `/var/www/html` into the container. On my host, `/var/www/html` holds a WordPress multi-site setup. My other sites are all static sites generated with [Hugo](https://gohugo.io/). | ||
|
||
I use [Caddy](https://caddyserver.com/) as my web server. In my static sites, I have the following directive to make all PHP requests work with the PHP container: | ||
``` | ||
fastcgi / 127.0.0.1:9000 php { root /var/www/html } | ||
``` | ||
But because PHP is running in a container, it does not have access to anything outside of `/var/www/html`. In order to get my micropub-published files into my static sites, I use [incron](http://inotify.aiken.cz/?section=incron&page=about&lang=en). I create a new `incrontab` entry for each static site that should be micropub-enabled, to watch a directory that corresponds with the site's domain name. When a new file is written, the `micropub.sh` script in this repository will execute, copying and moving the files as necessary. If it's a Markdown file, `hugo` is invoked to rebuild the site. | ||
|
||
The `micropub.sh` script copies AND moves images. This is so that micropub endpoints can see and access uploaded images without requiring a full site rebuild. The image is copied into the `/images/` directory of the site's docroot, and moved to the `/static/images` directory of the source of my Hugo site. | ||
|
||
The `is.php` script in this repo is an example of how to use most of this functionality **without** a full micropub setup. I use it to power [https://skippy.is/](https://skippy.is/) for easy uploading from my phone. It builds the Markdown file in exactly the way I want with minimal input from me. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
<?php | ||
function http_status ($code) { | ||
$http_codes = array( | ||
'200' => '200 OK', | ||
'400' => '400 Bad Request', | ||
'401' => '401 Unauthorized', | ||
'403' => '403 Forbidden', | ||
'409' => '409 Conflict', | ||
'413' => '413 Payload Too Large', | ||
'415' => '415 Unsupported Media Type', | ||
'502' => '502 Bad Gateway', | ||
); | ||
return $http_codes[$code]; | ||
} | ||
|
||
function quit ($error, $description = 'An error occurred.', $code = '400') { | ||
header("HTTP/1.1 " . http_status($code)); | ||
echo json_encode(['error' => $error, 'error_description' => $description]); | ||
die(); | ||
} | ||
|
||
/** | ||
* API call function. This could easily be used for any modern writable API | ||
* | ||
* @param $url adressable url of the external API | ||
* @param $auth authorisation header for the API | ||
* @param $adata php array of the data to be sent | ||
* | ||
* @return HTTP response from API | ||
*/ | ||
function post_to_api($url, $auth, $adata) | ||
{ | ||
$fields = ''; | ||
foreach ($adata as $key => $value) { | ||
$fields .= $key . '=' . $value . '&'; | ||
} | ||
rtrim($fields, '&'); | ||
$post = curl_init(); | ||
curl_setopt($post, CURLOPT_URL, $url); | ||
curl_setopt($post, CURLOPT_POST, count($adata)); | ||
curl_setopt($post, CURLOPT_POSTFIELDS, $fields); | ||
curl_setopt($post, CURLOPT_RETURNTRANSFER, 1); | ||
curl_setopt( | ||
$post, CURLOPT_HTTPHEADER, array( | ||
'Content-Type: application/x-www-form-urlencoded', | ||
'Authorization: '.$auth | ||
) | ||
); | ||
$result = curl_exec($post); | ||
curl_close($post); | ||
return $result; | ||
} | ||
|
||
/** | ||
* getallheaders() replacement for nginx | ||
* | ||
* Replaces the getallheaders function which relies on Apache | ||
* | ||
* @return array incoming headers from _POST | ||
*/ | ||
if (!function_exists('getallheaders')) { | ||
function getallheaders() { | ||
$headers = []; | ||
foreach ($_SERVER as $name => $value) { | ||
if (substr($name, 0, 5) == 'HTTP_') { | ||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; | ||
} | ||
} | ||
return $headers; | ||
} | ||
} | ||
|
||
/** | ||
* Validate incoming requests, using IndieAuth | ||
* | ||
* This section largely adopted from rhiaro | ||
* | ||
* @param array $token the authorization token to check | ||
* | ||
* @return boolean true if authorised | ||
*/ | ||
function indieAuth($token, $me = '') { | ||
/** | ||
* Check token is valid | ||
*/ | ||
if ( $me == '' ) { $me = $_SERVER['HTTP_HOST']; } | ||
$ch = curl_init("https://tokens.indieauth.com/token"); | ||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); | ||
curl_setopt($ch, CURLOPT_HTTPHEADER, Array("Authorization: $token")); | ||
$response = Array(); | ||
$curl_response = curl_exec($ch); | ||
if (false === $curl_response) { | ||
quit('noauth', 'Unable to connect to indiauth service', '502'); | ||
} | ||
parse_str($curl_response, $response); | ||
curl_close($ch); | ||
if (! isset($response['me']) && ! isset($response['scope']) ) { | ||
} | ||
//$me = $response['me']; | ||
//$scopes = explode(' ', $response['scope']); | ||
if (empty($response) || ! isset($response['me']) || ! isset($response['scope']) ) { | ||
quit('unauthorized', 'The request lacks authentication credentials', '401'); | ||
} elseif ($response['me'] != $me) { | ||
quit('unauthorized', 'The request lacks valid authentication credentials', '401'); | ||
} elseif (!in_array('create', $scopes) && !in_array('post', $scopes)) { | ||
quit('denied', 'Client does not have access to this resource', '403'); | ||
} | ||
// we got here, so all checks passed. return true. | ||
return true; | ||
} | ||
|
||
# respond to queries about config and syndication | ||
function what_can_i_do() { | ||
if ($_GET['q'] == "syndicate-to") { | ||
$array = array( | ||
"syndicate-to" => array( | ||
0 => array( | ||
"uid" => "https://twitter.com", | ||
"name" => "Twitter" | ||
), | ||
) | ||
); | ||
header('Content-Type: application/json'); | ||
echo json_encode($array, 32 | 64 | 128 | 256); | ||
exit; | ||
} | ||
|
||
if ($_GET['q'] == "config") { | ||
$array = array( | ||
"media-endpoint" => 'https://' . $_SERVER['HTTP_HOST'] . '/micropub/media.php', | ||
"syndicate-to" => array( | ||
0 => array( | ||
"uid" => "https://twitter.com", | ||
"name" => "Twitter" | ||
), | ||
) | ||
); | ||
header('Content-Type: application/json'); | ||
echo json_encode($array, 32 | 64 | 128 | 256); | ||
exit; | ||
} | ||
} | ||
|
||
# ensure that this battle station is fully operational. | ||
function check_target_dir($target_dir = '') { | ||
if ( empty($target_dir)) { | ||
quit('unknown_dir', 'Unspecified directory', 400); | ||
} | ||
# make sure our upload directory exists | ||
if ( ! file_exists($target_dir) ) { | ||
# fail if we can't create the directory | ||
if ( FALSE === mkdir($target_dir, 0777, true) ) { | ||
quit('cannot_mkdir', 'The upload directory could not be created.', '400'); | ||
} | ||
} | ||
} | ||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
<?php | ||
require 'common.php'; | ||
|
||
$target_dir = getcwd() . '/' . $_SERVER['HTTP_HOST']; | ||
check_target_dir($target_dir); | ||
|
||
// GET Requests:- config, syndicate to & source | ||
if (isset($_GET['q'])) { | ||
what_can_i_do(); | ||
} | ||
|
||
// Take headers and other incoming data | ||
$headers = getallheaders(); | ||
if ($headers === false ) { | ||
quit('invalid_headers', 'The request lacks valid headers', '400'); | ||
} | ||
$headers = array_change_key_case($headers, CASE_LOWER); | ||
$data = array(); | ||
if (!empty($_POST['access_token'])) { | ||
$token = "Bearer ".$_POST['access_token']; | ||
$headers["authorization"] = $token; | ||
} | ||
|
||
if (! isset($headers['authorization']) ) { | ||
quit('no_auth', 'No authorization token supplied.', 400); | ||
} | ||
// check the token for this connection. | ||
indieAuth($headers['authorization'], $_SERVER['HTTP_HOST']); | ||
|
||
// Are we getting a form-encoded submission? | ||
if (isset($_POST['h'])) { | ||
$h = $_POST['h']; | ||
unset($_POST['h']); | ||
// create an object containing all the POST fields | ||
$data = [ | ||
'type' => ['h-'.$h], | ||
'properties' => array_map( | ||
function ($a) { | ||
return is_array($a) ? $a : [$a]; | ||
}, $_POST | ||
) | ||
]; | ||
} else { | ||
// nope, we're getting JSON, so decode it. | ||
$data = json_decode(file_get_contents('php://input'), true); | ||
} | ||
|
||
if (empty($data)) { | ||
quit('no_content', 'No content', '400'); | ||
} | ||
|
||
if (empty($data['properties']['content']['0']) | ||
&& empty($data['properties']['photo']['0']) ) { | ||
// If this is a POST and there's no content or photo, exit | ||
if (empty($data['action'])) { | ||
quit('missing_content', 'Missing content.', '400'); | ||
} | ||
} | ||
|
||
if ( empty($data['properties']['mp-slug'][0]) ) { | ||
$title = date('YmdHi'); | ||
} else { | ||
$title = trim( $data['properties']['mp-slug'][0] ); | ||
} | ||
$slug = strtolower( preg_replace("/[^-\w+]/", "", str_replace(' ', '-', $title) ) ); | ||
$image_link = $data['properties']['photo']['0']; | ||
|
||
// Build up the post file | ||
$post = "---\n"; | ||
$post .= "title: $title \n"; | ||
$post .= 'date: ' . date('Y-m-d H:i:s') . "\n"; | ||
$post .= "permalink: $slug\n"; | ||
$post .= "twitterimage: $image_link\n"; | ||
$post .= "---\n"; | ||
$post .= $data['properties']['content']['0'] . "\n"; | ||
|
||
$markdown = './' . $_SERVER['HTTP_HOST'] . '/' . $slug . '.md'; | ||
$fh = fopen( $markdown, 'w' ); | ||
if ( ! $fh = fopen( $markdown, 'w' ) ) { | ||
quit('file_error', 'Unable to open Markdown file'. 400); | ||
} | ||
if ( fwrite($fh, $post ) === FALSE ) { | ||
quit('write_error', 'Unable to write Markdown file', 400); | ||
} | ||
fwrite($fh, $post); | ||
fclose($fh); | ||
chmod($file, 0777); | ||
# sleep for 2 seconds, to allow incron to copy/move the file, and invoke Hugo | ||
sleep(2); | ||
|
||
header('HTTP/1.1 201 Created'); | ||
header('Location: https://' . $_SERVER['HTTP_HOST'] . '/' . $slug . '/'); | ||
?> |
Oops, something went wrong.