Browse Source

add photo support to Teacup

posts photos to the media endpoint if set
pull/10/head
Aaron Parecki 7 years ago
parent
commit
29459f22dc
7 changed files with 227 additions and 44 deletions
  1. +2
    -1
      composer.json
  2. +5
    -10
      controllers/auth.php
  3. +38
    -0
      controllers/controllers.php
  4. +113
    -33
      lib/helpers.php
  5. +5
    -0
      schema/migrations/0001.sql
  6. +0
    -0
      schema/schema.sql
  7. +64
    -0
      views/new-post.php

+ 2
- 1
composer.json View File

@ -7,7 +7,8 @@
"indieweb/date-formatter": "0.1.*", "indieweb/date-formatter": "0.1.*",
"indieauth/client": "0.1.*", "indieauth/client": "0.1.*",
"mpratt/relativetime": ">=1.0", "mpratt/relativetime": ">=1.0",
"firebase/php-jwt": "dev-master"
"firebase/php-jwt": "dev-master",
"p3k/multipart": "*"
}, },
"autoload": { "autoload": {
"files": [ "files": [

+ 5
- 10
controllers/auth.php View File

@ -250,14 +250,6 @@ $app->get('/auth/callback', function() use($app) {
if(k($token['auth'], array('me','access_token','scope'))) { if(k($token['auth'], array('me','access_token','scope'))) {
$_SESSION['auth'] = $token['auth']; $_SESSION['auth'] = $token['auth'];
$_SESSION['me'] = $params['me']; $_SESSION['me'] = $params['me'];
// TODO?
// This client requires the "post" scope.
// Make a request to the micropub endpoint to discover the syndication targets if any.
// Errors are silently ignored here. The user will be able to retry from the new post interface and get feedback.
// get_syndication_targets($user);
} }
} else { } else {
@ -281,9 +273,8 @@ $app->get('/auth/callback', function() use($app) {
} }
} }
// Verify the login actually succeeded // Verify the login actually succeeded
if(!array_key_exists('me', $_SESSION)) {
if(!k($token['auth'], 'me')) {
$html = render('auth_error', array( $html = render('auth_error', array(
'title' => 'Sign-In Failed', 'title' => 'Sign-In Failed',
'error' => 'Unable to verify the sign-in attempt', 'error' => 'Unable to verify the sign-in attempt',
@ -316,6 +307,10 @@ $app->get('/auth/callback', function() use($app) {
$_SESSION['user_id'] = $user->id(); $_SESSION['user_id'] = $user->id();
if($tokenEndpoint) {
// Make a request to the micropub endpoint to discover the media endpoint if set.
get_micropub_config($user);
}
unset($_SESSION['auth_state']); unset($_SESSION['auth_state']);

+ 38
- 0
controllers/controllers.php View File

@ -72,6 +72,7 @@ $app->get('/new', function() use($app) {
$html = render('new-post', array( $html = render('new-post', array(
'title' => 'New Post', 'title' => 'New Post',
'micropub_endpoint' => $user->micropub_endpoint, 'micropub_endpoint' => $user->micropub_endpoint,
'micropub_media_endpoint' => $user->micropub_media_endpoint,
'token_scope' => $user->token_scope, 'token_scope' => $user->token_scope,
'access_token' => $user->access_token, 'access_token' => $user->access_token,
'response_date' => $user->last_micropub_response_date, 'response_date' => $user->last_micropub_response_date,
@ -223,6 +224,10 @@ $app->post('/post', function() use($app) {
$verb = 'ate'; $verb = 'ate';
} }
if($user->micropub_media_endpoint && k($params, 'note_photo')) {
$entry->photo_url = $params['note_photo'];
}
$entry->type = $type; $entry->type = $type;
$entry->save(); $entry->save();
@ -240,6 +245,9 @@ $app->post('/post', function() use($app) {
'location' => k($params, 'location'), 'location' => k($params, 'location'),
'summary' => $text_content 'summary' => $text_content
); );
if($entry->photo_url) {
$mp_request['photo'] = $entry->photo_url;
}
if($user->enable_array_micropub) { if($user->enable_array_micropub) {
$mp_request[$verb] = [ $mp_request[$verb] = [
'type' => 'h-food', 'type' => 'h-food',
@ -281,6 +289,36 @@ $app->post('/post', function() use($app) {
} }
}); });
$app->post('/micropub/media', function() use($app) {
if($user=require_login($app)) {
$file = isset($_FILES['photo']) ? $_FILES['photo'] : null;
$error = validate_photo($file);
unset($_POST['null']);
if(!$error) {
$file_path = $file['tmp_name'];
correct_photo_rotation($file_path);
$r = micropub_media_post($user->micropub_media_endpoint, $user->access_token, $file_path);
} else {
$r = array('error' => $error);
}
$response = $r['response'];
$url = null;
if($response && preg_match('/Location: (.+)/', $response, $match)) {
$url = trim($match[1]);
} else {
$r['error'] = "No 'Location' header in response.";
}
$app->response()['Content-type'] = 'application/json';
$app->response()->body(json_encode(array(
'location' => $url,
'error' => (isset($r['error']) ? $r['error'] : null),
)));
}
});
$app->get('/options.json', function() use($app) { $app->get('/options.json', function() use($app) {
if($user=require_login($app)) { if($user=require_login($app)) {
$params = $app->request()->params(); $params = $app->request()->params();

+ 113
- 33
lib/helpers.php View File

@ -95,6 +95,7 @@ if(!function_exists('http_build_url')) {
return "$scheme$user$pass$host$port$path$query$fragment"; return "$scheme$user$pass$host$port$path$query$fragment";
} }
} }
function micropub_post($endpoint, $params, $access_token) { function micropub_post($endpoint, $params, $access_token) {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
@ -122,6 +123,33 @@ function micropub_post($endpoint, $params, $access_token) {
); );
} }
function micropub_media_post($endpoint, $access_token, $file) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer ' . $access_token
));
curl_setopt($ch, CURLOPT_POST, true);
$post = [
'photo' => new CURLFile($file)
];
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$response = curl_exec($ch);
$error = curl_error($ch);
$sent_headers = curl_getinfo($ch, CURLINFO_HEADER_OUT);
return array(
'response' => $response,
'error' => $error,
'curlinfo' => curl_getinfo($ch)
);
}
function micropub_get($endpoint, $params, $access_token) { function micropub_get($endpoint, $params, $access_token) {
$url = parse_url($endpoint); $url = parse_url($endpoint);
if(!k($url, 'query')) { if(!k($url, 'query')) {
@ -134,13 +162,14 @@ function micropub_get($endpoint, $params, $access_token) {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_HTTPHEADER, array( curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer ' . $access_token
'Authorization: Bearer ' . $access_token,
'Accept: application/json',
)); ));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch); $response = curl_exec($ch);
$data = array(); $data = array();
if($response) { if($response) {
parse_str($response, $data);
$data = @json_decode($response, true);
} }
$error = curl_error($ch); $error = curl_error($ch);
return array( return array(
@ -151,43 +180,17 @@ function micropub_get($endpoint, $params, $access_token) {
); );
} }
function get_syndication_targets(&$user) {
function get_micropub_config(&$user) {
$targets = array(); $targets = array();
$r = micropub_get($user->micropub_endpoint, array('q'=>'syndicate-to'), $user->micropub_access_token);
if($r['data'] && array_key_exists('syndicate-to', $r['data'])) {
$targetURLs = preg_split('/, ?/', $r['data']['syndicate-to']);
foreach($targetURLs as $t) {
// If the syndication target doesn't have a scheme, add http
if(!preg_match('/^http/', $t))
$tmp = 'http://' . $t;
// Parse the target expecting it to be a URL
$url = parse_url($tmp);
// If there's a host, and the host contains a . then we can assume there's a favicon
// parse_url will parse strings like http://twitter into an array with a host of twitter, which is not resolvable
if(array_key_exists('host', $url) && strpos($url['host'], '.') !== false) {
$targets[] = array(
'target' => $t,
'favicon' => 'http://' . $url['host'] . '/favicon.ico'
);
} else {
$targets[] = array(
'target' => $t,
'favicon' => false
);
}
}
}
if(count($targets)) {
$user->syndication_targets = json_encode($targets);
$r = micropub_get($user->micropub_endpoint, [], $user->access_token);
if(array_key_exists('media_endpoint', $r['data'])) {
$user->micropub_media_endpoint = $r['data']['media_endpoint'];
$user->save(); $user->save();
} }
return array( return array(
'targets' => $targets,
'response' => $r 'response' => $r
); );
} }
@ -255,6 +258,83 @@ function entry_date($entry, $user) {
// return $date; // return $date;
} }
function validate_photo(&$file) {
try {
if ($_SERVER['REQUEST_METHOD'] == 'POST' && count($_POST) < 1 ) {
throw new RuntimeException('File upload size exceeded.');
}
// Undefined | Multiple Files | $_FILES Corruption Attack
// If this request falls under any of them, treat it invalid.
if (
!isset($file['error']) ||
is_array($file['error'])
) {
throw new RuntimeException('Invalid parameters.');
}
// Check $file['error'] value.
switch ($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
throw new RuntimeException('No file sent.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new RuntimeException('Exceeded filesize limit.');
default:
throw new RuntimeException('Unknown errors.');
}
// You should also check filesize here.
if ($file['size'] > 4000000) {
throw new RuntimeException('Exceeded filesize limit.');
}
// DO NOT TRUST $file['mime'] VALUE !!
// Check MIME Type by yourself.
$finfo = new finfo(FILEINFO_MIME_TYPE);
if (false === $ext = array_search(
$finfo->file($file['tmp_name']),
array(
'jpg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
),
true
)) {
throw new RuntimeException('Invalid file format.');
}
} catch (RuntimeException $e) {
return $e->getMessage();
}
}
// Reads the exif rotation data and actually rotates the photo.
// Only does anything if the exif library is loaded, otherwise is a noop.
function correct_photo_rotation($filename) {
if(class_exists('IMagick')) {
$image = new IMagick($filename);
$orientation = $image->getImageOrientation();
switch($orientation) {
case IMagick::ORIENTATION_BOTTOMRIGHT:
$image->rotateImage(new ImagickPixel('#00000000'), 180);
break;
case IMagick::ORIENTATION_RIGHTTOP:
$image->rotateImage(new ImagickPixel('#00000000'), 90);
break;
case IMagick::ORIENTATION_LEFTBOTTOM:
$image->rotateImage(new ImagickPixel('#00000000'), -90);
break;
}
$image->setImageOrientation(IMagick::ORIENTATION_TOPLEFT);
$image->writeImage($filename);
}
}
function default_drink_options() { function default_drink_options() {
return [ return [
['title'=>'Coffee','subtitle'=>'','type'=>'drink'], ['title'=>'Coffee','subtitle'=>'','type'=>'drink'],

+ 5
- 0
schema/migrations/0001.sql View File

@ -0,0 +1,5 @@
ALTER TABLE users
ADD COLUMN `micropub_media_endpoint` VARCHAR(255) NOT NULL DEFAULT '' AFTER `micropub_endpoint`;
ALTER TABLE entries
ADD COLUMN `photo_url` VARCHAR(255) NOT NULL DEFAULT '' AFTER `canonical_url`;

+ 0
- 0
schema/schema.sql View File


+ 64
- 0
views/new-post.php View File

@ -11,6 +11,26 @@
<input type="text" class="form-control" style="max-width:75px;" id="note_tzoffset" name="note_tzoffset" value=""> <input type="text" class="form-control" style="max-width:75px;" id="note_tzoffset" name="note_tzoffset" value="">
</div> </div>
<?php if($this->micropub_media_endpoint): ?>
<div class="form-group">
<h3>Photo</h3>
<input type="file" name="note_photo_upload" id="note_photo_upload" accept="image/*">
<div id="note_photo_loading" class="hidden">
<img src="/images/spinner.gif"> Uploading...
</div>
<div id="photo_preview_container" class="hidden">
<input type="text" name="note_photo" id="note_photo" class="form-control" readonly="readonly"><br>
<img src="" id="photo_preview" style="max-width: 300px; max-height: 300px;">
<div>
<button type="button" class="btn btn-danger btn-sm" id="remove_photo"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Remove image</button>
</div>
</div>
</div>
<?php endif; ?>
<div id="entry-buttons"> <div id="entry-buttons">
</div> </div>
@ -97,6 +117,14 @@ $(function(){
return num; return num;
} }
function replacePhotoWithPhotoURL(url) {
$("#note_photo").val(url);
$("#note_photo_upload").addClass("hidden");
$("#note_photo_loading").addClass("hidden");
$("#photo_preview").attr("src", url);
$("#photo_preview_container").removeClass("hidden");
}
function bind_keyboard_shortcuts() { function bind_keyboard_shortcuts() {
$(".text-custom-eat").keydown(function(e){ $(".text-custom-eat").keydown(function(e){
if(e.keyCode == 13) { if(e.keyCode == 13) {
@ -204,6 +232,42 @@ $(function(){
}); });
} }
$("#photo_preview_container").addClass("hidden");
$("#note_photo_upload").on("change", function(e){
$("#note_photo_upload").addClass("hidden");
$("#note_photo_loading").removeClass("hidden");
var formData = new FormData();
formData.append("null","null");
formData.append("photo", e.target.files[0]);
var request = new XMLHttpRequest();
request.open("POST", "/micropub/media");
request.onreadystatechange = function() {
if(request.readyState == XMLHttpRequest.DONE) {
try {
var response = JSON.parse(request.responseText);
if(response.location) {
// Replace the file upload form with the URL
replacePhotoWithPhotoURL(response.location);
saveNoteState();
} else {
console.log("Endpoint did not return a location header", response);
}
} catch(e) {
console.log(e);
}
}
}
request.send(formData);
});
$("#remove_photo").on("click", function(){
$("#note_photo").val("");
$("#note_photo_upload").removeClass("hidden").val("");
$("#photo_preview").attr("src", "" );
$("#photo_preview_container").addClass("hidden");
});
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
// App Start // App Start

Loading…
Cancel
Save