Browse Source

integrates photo uploading in the main note interface

Quill corrects the photo rotation based on exif data since iOS tends to take landscape photos and set the rotation bit when holding it in portrait mode.
pull/37/head
Aaron Parecki 8 years ago
parent
commit
9e817943ac
8 changed files with 162 additions and 131 deletions
  1. +15
    -17
      controllers/auth.php
  2. +41
    -48
      controllers/controllers.php
  3. +23
    -0
      lib/helpers.php
  4. +5
    -1
      public/js/script.js
  5. +0
    -1
      views/dashboard.php
  6. +0
    -1
      views/layout.php
  7. +78
    -7
      views/new-post.php
  8. +0
    -56
      views/photo.php

+ 15
- 17
controllers/auth.php View File

@ -4,18 +4,18 @@ function buildRedirectURI() {
return Config::$base_url . 'auth/callback'; return Config::$base_url . 'auth/callback';
} }
function build_url($parsed_url) {
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
function build_url($parsed_url) {
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
$app->get('/', function($format='html') use($app) { $app->get('/', function($format='html') use($app) {
$res = $app->response(); $res = $app->response();
@ -38,7 +38,6 @@ $app->get('/auth/start', function() use($app) {
// the "me" parameter is user input, and may be in a couple of different forms: // the "me" parameter is user input, and may be in a couple of different forms:
// aaronparecki.com http://aaronparecki.com http://aaronparecki.com/ // aaronparecki.com http://aaronparecki.com http://aaronparecki.com/
// Normlize the value now (move this into a function in IndieAuth\Client later)
if(!array_key_exists('me', $params) || !($me = IndieAuth\Client::normalizeMeURL($params['me']))) { if(!array_key_exists('me', $params) || !($me = IndieAuth\Client::normalizeMeURL($params['me']))) {
$html = render('auth_error', array( $html = render('auth_error', array(
'title' => 'Sign In', 'title' => 'Sign In',
@ -68,12 +67,12 @@ $app->get('/auth/start', function() use($app) {
$authorizationURL = false; $authorizationURL = false;
} }
// If the user has already signed in before and has a micropub access token,
// and the endpoints are all the same, skip the debugging screens and redirect
// If the user has already signed in before and has a micropub access token,
// and the endpoints are all the same, skip the debugging screens and redirect
// immediately to the auth endpoint. // immediately to the auth endpoint.
// This will still generate a new access token when they finish logging in. // This will still generate a new access token when they finish logging in.
$user = ORM::for_table('users')->where('url', $me)->find_one(); $user = ORM::for_table('users')->where('url', $me)->find_one();
if($user && $user->micropub_access_token
if($user && $user->micropub_access_token
&& $user->micropub_endpoint == $micropubEndpoint && $user->micropub_endpoint == $micropubEndpoint
&& $user->token_endpoint == $tokenEndpoint && $user->token_endpoint == $tokenEndpoint
&& $user->authorization_endpoint == $authorizationEndpoint && $user->authorization_endpoint == $authorizationEndpoint
@ -251,4 +250,3 @@ $app->get('/signout', function() use($app) {
unset($_SESSION['user_id']); unset($_SESSION['user_id']);
$app->redirect('/', 301); $app->redirect('/', 301);
}); });

+ 41
- 48
controllers/controllers.php View File

@ -384,20 +384,6 @@ function create_favorite(&$user, $url) {
return $r; return $r;
} }
function create_photo(&$user, $params, $file) {
$error = validate_photo($file);
if(!$error) {
$file_path = $file['tmp_name'];
$micropub_request = array('content' => $params['note_content']);
$r = micropub_post_for_user($user, $micropub_request, $file_path);
} else {
$r = array('error' => $error);
}
return $r;
}
function create_repost(&$user, $url) { function create_repost(&$user, $url) {
$micropub_request = array( $micropub_request = array(
'repost-of' => $url 'repost-of' => $url
@ -452,40 +438,6 @@ $app->post('/favorite', function() use($app) {
} }
}); });
$app->post('/photo', function() use($app) {
if($user=require_login($app)) {
// var_dump($app->request()->post());
//
// Since $app->request()->post() with multipart is always
// empty (bug in Slim?) We're using the raw $_POST here
// until this gets fixed.
// PHP empties everything in $_POST if the file upload size exceeds
// that is why we have to test if the variables exist first.
$note_content = isset($_POST['note_content']) ? $_POST['note_content'] : null;
$params = array('note_content' => $note_content);
$file = isset($_FILES['note_photo']) ? $_FILES['note_photo'] : null;
$r = create_photo($user, $params, $file);
// Populate the error if there was no location header.
if(empty($r['location']) && empty($r['error'])) {
$r['error'] = "No 'Location' header in response.";
}
$html = render('photo', array(
'title' => 'Photo posted',
'note_content' => $params['note_content'],
'location' => (isset($r['location']) ? $r['location'] : null),
'error' => (isset($r['error']) ? $r['error'] : null),
'response' => (isset($r['response']) ? htmlspecialchars($r['response']) : null),
'authorizing' => false
));
$app->response()->body($html);
}
});
$app->post('/repost', function() use($app) { $app->post('/repost', function() use($app) {
if($user=require_login($app)) { if($user=require_login($app)) {
$params = $app->request()->params(); $params = $app->request()->params();
@ -530,6 +482,47 @@ $app->post('/micropub/post', function() use($app) {
} }
}); });
$app->post('/micropub/multipart', function() use($app) {
if($user=require_login($app)) {
// var_dump($app->request()->post());
//
// Since $app->request()->post() with multipart is always
// empty (bug in Slim?) We're using the raw $_POST here.
// PHP empties everything in $_POST if the file upload size exceeds
// that is why we have to test if the variables exist first.
$file = isset($_FILES['photo']) ? $_FILES['photo'] : null;
if($file) {
$error = validate_photo($file);
unset($_POST['null']);
if(!$error) {
$file_path = $file['tmp_name'];
correct_photo_rotation($file_path);
$r = micropub_post_for_user($user, $_POST, $file_path);
} else {
$r = array('error' => $error);
}
} else {
unset($_POST['null']);
$r = micropub_post_for_user($user, $_POST);
}
// Populate the error if there was no location header.
if(empty($r['location']) && empty($r['error'])) {
$r['error'] = "No 'Location' header in response.";
}
$app->response()->body(json_encode(array(
'response' => (isset($r['response']) ? htmlspecialchars($r['response']) : null),
'location' => (isset($r['location']) ? $r['location'] : null),
'error' => (isset($r['error']) ? $r['error'] : null),
)));
}
});
$app->post('/micropub/postjson', function() use($app) { $app->post('/micropub/postjson', function() use($app) {
if($user=require_login($app)) { if($user=require_login($app)) {
$params = $app->request()->params(); $params = $app->request()->params();

+ 23
- 0
lib/helpers.php View File

@ -323,3 +323,26 @@ function validate_photo(&$file) {
return $e->getMessage(); 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);
}
}

+ 5
- 1
public/js/script.js View File

@ -15,7 +15,11 @@
} }
function csv_to_array(val) { function csv_to_array(val) {
return val.split(/[, ]+/);
if(val.length > 0) {
return val.split(/[, ]+/);
} else {
return [];
}
} }

+ 0
- 1
views/dashboard.php View File

@ -8,7 +8,6 @@
<li><a href="/bookmark"><img src="/images/bookmark.svg" width="60"></a></li> <li><a href="/bookmark"><img src="/images/bookmark.svg" width="60"></a></li>
<li><a href="/favorite"><img src="/images/star.svg" width="60"></a></li> <li><a href="/favorite"><img src="/images/star.svg" width="60"></a></li>
<li><a href="/repost"><img src="/images/repost.svg" width="60"></a></li> <li><a href="/repost"><img src="/images/repost.svg" width="60"></a></li>
<li><a href="/photo"><img src="/images/camera.svg" width="60"></a></li>
<li><a href="/itinerary"><img src="/images/plane.svg" width="60"></a></li> <li><a href="/itinerary"><img src="/images/plane.svg" width="60"></a></li>
<li><a href="/email"><img src="/images/email.svg" width="60"></a></li> <li><a href="/email"><img src="/images/email.svg" width="60"></a></li>
</ul> </ul>

+ 0
- 1
views/layout.php View File

@ -69,7 +69,6 @@ if(property_exists($this, 'include_facebook')) {
<li><a href="/new">Note</a></li> <li><a href="/new">Note</a></li>
<li><a href="/bookmark">Bookmark</a></li> <li><a href="/bookmark">Bookmark</a></li>
<li><a href="/favorite">Favorite</a></li> <li><a href="/favorite">Favorite</a></li>
<li><a href="/photo">Photo</a></li>
<?php } ?> <?php } ?>
<li><a href="/docs">Docs</a></li> <li><a href="/docs">Docs</a></li>

+ 78
- 7
views/new-post.php View File

@ -10,20 +10,27 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="note_in_reply_to"><code>in-reply-to</code> (optional, a URL you are replying to)</label>
<label for="note_in_reply_to"><code>in-reply-to</code> (a URL you are replying to)</label>
<input type="text" id="note_in_reply_to" value="<?= $this->in_reply_to ?>" class="form-control"> <input type="text" id="note_in_reply_to" value="<?= $this->in_reply_to ?>" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="note_category"><code>category</code> (optional, comma-separated list of tags)</label>
<label for="note_category"><code>category</code> (comma-separated list of tags, will be posted as an array)</label>
<input type="text" id="note_category" value="" class="form-control" placeholder="e.g. web, personal"> <input type="text" id="note_category" value="" class="form-control" placeholder="e.g. web, personal">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="note_slug"><code>slug</code> (optional)</label>
<label for="note_slug"><code>slug</code></label>
<input type="text" id="note_slug" value="" class="form-control"> <input type="text" id="note_slug" value="" class="form-control">
</div> </div>
<div class="form-group">
<label for="note_photo"><code>photo</code></label>
<input type="file" name="note_photo" id="note_photo" accept="image/*" onchange="previewPhoto(event)">
<br>
<img src="" id="photo_preview" style="max-width: 300px; max-height: 300px;">
</div>
<div class="form-group"> <div class="form-group">
<label for="note_syndicate-to"><code>syndicate-to</code> <a href="javascript:reload_syndications()">(refresh)</a></label> <label for="note_syndicate-to"><code>syndicate-to</code> <a href="javascript:reload_syndications()">(refresh)</a></label>
<div id="syndication-container"> <div id="syndication-container">
@ -72,7 +79,7 @@
<?php if($this->test_response): ?> <?php if($this->test_response): ?>
<h4>Last response from your Micropub endpoint <span id="last_response_date">(<?= relative_time($this->response_date) ?>)</span></h4> <h4>Last response from your Micropub endpoint <span id="last_response_date">(<?= relative_time($this->response_date) ?>)</span></h4>
<?php endif; ?> <?php endif; ?>
<pre id="test_response" style="width: 100%; min-height: 240px;"><?= htmlspecialchars($this->test_response) ?></pre>
<pre id="test_response" class="<?= $this->test_response ? '' : 'hidden' ?>" style="width: 100%; min-height: 240px;"><?= htmlspecialchars($this->test_response) ?></pre>
<div class="callout"> <div class="callout">
@ -105,13 +112,13 @@
</div> </div>
<style type="text/css"> <style type="text/css">
#note_content_remaining { #note_content_remaining {
float: right; float: right;
font-size: 0.8em; font-size: 0.8em;
font-weight: bold; font-weight: bold;
} }
.pcheck206 { color: #6ba15c; } /* tweet fits within the limit even after adding RT @username */ .pcheck206 { color: #6ba15c; } /* tweet fits within the limit even after adding RT @username */
.pcheck207 { color: #c4b404; } /* danger zone, tweet will overflow when RT @username is added */ .pcheck207 { color: #c4b404; } /* danger zone, tweet will overflow when RT @username is added */
.pcheck200,.pcheck208 { color: #59cb3a; } /* exactly fits 140 chars, both with or without RT */ .pcheck200,.pcheck208 { color: #59cb3a; } /* exactly fits 140 chars, both with or without RT */
@ -120,6 +127,10 @@
</style> </style>
<script> <script>
function previewPhoto(event) {
document.getElementById('photo_preview').src = URL.createObjectURL(event.target.files[0]);
}
$(function(){ $(function(){
$("#note_content").on('change keyup', function(e){ $("#note_content").on('change keyup', function(e){
@ -146,11 +157,69 @@ $(function(){
syndications.push($(btn).data('syndication')); syndications.push($(btn).data('syndication'));
}); });
$.post("/micropub/post", {
var category = csv_to_array($("#note_category").val());
var formData = new FormData();
if(v=$("#note_content").val()) {
formData.append("content", v);
}
if(v=$("#note_in_reply_to").val()) {
formData.append("in-reply-to", v);
}
if(v=$("#note_location").val()) {
formData.append("location", v);
}
if(category.length > 0) {
formData.append("category", category);
}
if(syndications.length > 0) {
formData.append("syndicate-to", syndications);
}
if(v=$("#note_slug").val()) {
formData.append("slug", v);
}
if(document.getElementById("note_photo").files[0]) {
formData.append("photo", document.getElementById("note_photo").files[0]);
}
// Need to append a placeholder field because if the file size max is hit, $_POST will
// be empty, so the server needs to be able to recognize a post with only a file vs a failed one.
// This will be stripped by Quill before it's sent to the Micropub endpoint
formData.append("null","null");
var request = new XMLHttpRequest();
request.open("POST", "/micropub/multipart");
request.onreadystatechange = function() {
if(request.readyState == XMLHttpRequest.DONE) {
console.log(request.responseText);
try {
var response = JSON.parse(request.responseText);
if(response.location) {
window.location = response.location;
// console.log(response.location);
} else {
$("#test_response").html(response.response).removeClass('hidden');
$("#test_success").addClass('hidden');
$("#test_error").removeClass('hidden');
}
} catch(e) {
$("#test_success").addClass('hidden');
$("#test_error").removeClass('hidden');
}
$("#btn_post").removeClass("loading disabled").text("Post");
}
}
$("#btn_post").addClass("loading disabled").text("Working...");
request.send(formData);
/*
$.post("/micropub/multipart", {
content: $("#note_content").val(), content: $("#note_content").val(),
'in-reply-to': $("#note_in_reply_to").val(), 'in-reply-to': $("#note_in_reply_to").val(),
location: $("#note_location").val(), location: $("#note_location").val(),
category: csv_to_array($("#note_category").val()),
category: category,
slug: $("#note_slug").val(), slug: $("#note_slug").val(),
'syndicate-to': syndications 'syndicate-to': syndications
}, function(data){ }, function(data){
@ -180,6 +249,8 @@ $(function(){
$("#last_request_container").show(); $("#last_request_container").show();
$("#test_response").html(response.response); $("#test_response").html(response.response);
}); });
*/
return false; return false;
}); });

+ 0
- 56
views/photo.php View File

@ -1,56 +0,0 @@
<div class="narrow">
<?= partial('partials/header') ?>
<form method="POST" action="/photo" role="form" style="margin-top: 20px;" id="note_form" enctype="multipart/form-data">
<div class="form-group">
<label for="note_photo"><code>photo</code></label>
<div class="uploadBtn btn btn-default">
<span>Choose File</span>
<input type="file" name="note_photo" id="note_photo" accept="image/jpg,image/jpeg,image/gif,image/png">
</div>
<div class="hidden" id="photo_filename_container">
<input type="text" class="form-control" disabled="disabled" id="photo_filename">
</div>
<p class="help-block">Photo JPEG, GIF or PNG.</p>
</div>
<div class="form-group">
<label for="note_content"><code>content</code> (optional)</label>
<textarea name="note_content" id="note_content" value="" class="form-control" style="height: 4em;"><?php if(isset($this->note_content)) echo $this->note_content ?></textarea>
</div>
<button class="btn btn-success" id="btn_post">Post</button>
<div style="clear:both;"></div>
</form>
<?php if(!empty($this->location)): ?>
<div class="alert alert-success">
<strong>Success!</strong> Photo posted to: <em><a href="<?= $this->location ?>"><?= $this->location ?></a></em>
</div>
<?php endif ?>
<?php if(!empty($this->error)): ?>
<div class="alert alert-danger">
<strong>Error:</strong> <em><?= $this->error ?></em>
</div>
<?php endif ?>
<?php if(!empty($this->response)): ?>
<h4>Response:</h4>
<pre><?= $this->response ?></pre>
<?php endif ?>
</div>
<script>
$(function(){
document.getElementById("note_photo").onchange = function () {
var filename = this.value;
if(filename.match(/[^\\]+$/)) {
filename = filename.match(/[^\\]+$/)[0];
}
$("#photo_filename").val(filename);
$("#photo_filename_container").removeClass("hidden");
};
});
</script>

Loading…
Cancel
Save