Skip to content

Commit

Permalink
initial code commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott Merrill committed Mar 27, 2018
1 parent 4df701a commit c329f01
Show file tree
Hide file tree
Showing 6 changed files with 520 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
secret.txt
17 changes: 17 additions & 0 deletions README.md
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.
157 changes: 157 additions & 0 deletions common.php
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');
}
}
}
?>
93 changes: 93 additions & 0 deletions index.php
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 . '/');
?>
Loading

0 comments on commit c329f01

Please sign in to comment.