Browse Source

support media endpoint, autosave notes in local storage

* looks for a media endpoint in the micropub config
* if media endpoint is available, both the note interface and the editor will upload files to it instead of posting the photo directly
* the note interface autosaves in-progress notes in localstorage
pull/52/head
Aaron Parecki 9 years ago
parent
commit
542aa812f8
  1. 4
      controllers/auth.php
  2. 28
      controllers/controllers.php
  3. 33
      controllers/editor.php
  4. 42
      lib/helpers.php
  5. 0
      public/libs/localforage.js
  6. 2
      schema/migrations/0001.sql
  7. 1
      schema/mysql.sql
  8. 4
      views/auth_start.php
  9. 2
      views/editor.php
  10. 1
      views/layout.php
  11. 99
      views/new-post.php
  12. 1
      views/partials/syndication-js.php

4
controllers/auth.php

@ -212,9 +212,9 @@ $app->get('/auth/callback', function() use($app) {
$user->save(); $user->save();
$_SESSION['user_id'] = $user->id(); $_SESSION['user_id'] = $user->id();
// Make a request to the micropub endpoint to discover the syndication targets if any.
// Make a request to the micropub endpoint to discover the syndication targets and media endpoint if any.
// Errors are silently ignored here. The user will be able to retry from the new post interface and get feedback. // Errors are silently ignored here. The user will be able to retry from the new post interface and get feedback.
get_syndication_targets($user);
get_micropub_config($user);
} }
unset($_SESSION['auth_state']); unset($_SESSION['auth_state']);

28
controllers/controllers.php

@ -76,6 +76,7 @@ $app->get('/new', function() use($app) {
'title' => 'New Post', 'title' => 'New Post',
'in_reply_to' => $in_reply_to, 'in_reply_to' => $in_reply_to,
'micropub_endpoint' => $user->micropub_endpoint, 'micropub_endpoint' => $user->micropub_endpoint,
'media_endpoint' => $user->micropub_media_endpoint,
'micropub_scope' => $user->micropub_scope, 'micropub_scope' => $user->micropub_scope,
'micropub_access_token' => $user->micropub_access_token, 'micropub_access_token' => $user->micropub_access_token,
'response_date' => $user->last_micropub_response_date, 'response_date' => $user->last_micropub_response_date,
@ -452,7 +453,7 @@ $app->post('/repost', function() use($app) {
$app->get('/micropub/syndications', function() use($app) { $app->get('/micropub/syndications', function() use($app) {
if($user=require_login($app)) { if($user=require_login($app)) {
$data = get_syndication_targets($user);
$data = get_micropub_config($user, ['q'=>'syndicate-to']);
$app->response()->body(json_encode(array( $app->response()->body(json_encode(array(
'targets' => $data['targets'], 'targets' => $data['targets'],
'response' => $data['response'] 'response' => $data['response']
@ -522,6 +523,31 @@ $app->post('/micropub/multipart', 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_for_user($user, $file_path);
} else {
$r = array('error' => $error);
}
if(empty($r['location']) && empty($r['error'])) {
$r['error'] = "No 'Location' header in response.";
}
$app->response()->body(json_encode(array(
'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();

33
controllers/editor.php

@ -35,19 +35,30 @@ $app->post('/editor/publish', function() use($app) {
}); });
$app->post('/editor/upload', function() use($app) { $app->post('/editor/upload', function() use($app) {
// Fake a file uploader by echo'ing back the data URI
$fn = $_FILES['files']['tmp_name'][0];
$imageData = base64_encode(file_get_contents($fn));
$src = 'data:'.mime_content_type($fn).';base64,'.$imageData;
if($user=require_login($app)) {
$fn = $_FILES['files']['tmp_name'][0];
$imageURL = false;
$app->response()['Content-type'] = 'application/json';
$app->response()->body(json_encode([
'files' => [
[
'url'=>$src
if($user->micropub_media_endpoint) {
// If the user has a media endpoint, upload to that and return that URL
correct_photo_rotation($fn);
$r = micropub_media_post_for_user($user, $fn);
if(!empty($r['location'])) {
$imageURL = $r['location'];
}
}
if(!$imageURL) {
// Otherwise, fake a file uploader by echo'ing back the data URI
$imageData = base64_encode(file_get_contents($fn));
$imageURL = 'data:'.mime_content_type($fn).';base64,'.$imageData;
}
$app->response()['Content-type'] = 'application/json';
$app->response()->body(json_encode([
'files' => [
['url'=>$imageURL]
] ]
]
]));
]));
}
}); });
$app->post('/editor/delete-file', function() use($app) { $app->post('/editor/delete-file', function() use($app) {

42
lib/helpers.php

@ -109,6 +109,20 @@ function micropub_post_for_user(&$user, $params, $file_path = NULL, $json = fals
return $r; return $r;
} }
function micropub_media_post_for_user(&$user, $file_path) {
// Send to the media endpoint
$r = micropub_post($user->micropub_media_endpoint, [], $user->micropub_access_token, $file_path, true);
// Check the response and look for a "Location" header containing the URL
if($r['response'] && preg_match('/Location: (.+)/', $r['response'], $match)) {
$r['location'] = trim($match[1]);
} else {
$r['location'] = false;
}
return $r;
}
function micropub_post($endpoint, $params, $access_token, $file_path = NULL, $json = false) { function micropub_post($endpoint, $params, $access_token, $file_path = NULL, $json = false) {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
@ -154,7 +168,7 @@ function micropub_post($endpoint, $params, $access_token, $file_path = NULL, $js
$response = curl_exec($ch); $response = curl_exec($ch);
$error = curl_error($ch); $error = curl_error($ch);
$sent_headers = curl_getinfo($ch, CURLINFO_HEADER_OUT); $sent_headers = curl_getinfo($ch, CURLINFO_HEADER_OUT);
$request = $sent_headers . $post;
$request = $sent_headers . (is_string($post) ? $post : http_build_query($post));
return array( return array(
'request' => $request, 'request' => $request,
'response' => $response, 'response' => $response,
@ -193,15 +207,15 @@ function micropub_get($endpoint, $params, $access_token) {
); );
} }
function get_syndication_targets(&$user) {
$targets = array();
function get_micropub_config(&$user, $query=[]) {
$targets = [];
$r = micropub_get($user->micropub_endpoint, array('q'=>'syndicate-to'), $user->micropub_access_token);
$r = micropub_get($user->micropub_endpoint, $query, $user->micropub_access_token);
if($r['data'] && array_key_exists('syndicate-to', $r['data'])) { if($r['data'] && array_key_exists('syndicate-to', $r['data'])) {
if(is_array($r['data']['syndicate-to'])) { if(is_array($r['data']['syndicate-to'])) {
$data = $r['data']['syndicate-to']; $data = $r['data']['syndicate-to'];
} else { } else {
$data = array();
$data = [];
} }
foreach($data as $t) { foreach($data as $t) {
@ -212,23 +226,31 @@ function get_syndication_targets(&$user) {
} }
if(array_key_exists('uid', $t) && array_key_exists('name', $t)) { if(array_key_exists('uid', $t) && array_key_exists('name', $t)) {
$targets[] = array(
$targets[] = [
'target' => $t['name'], 'target' => $t['name'],
'uid' => $t['uid'], 'uid' => $t['uid'],
'favicon' => $icon 'favicon' => $icon
);
];
} }
} }
} }
if(count($targets)) {
if(count($targets))
$user->syndication_targets = json_encode($targets); $user->syndication_targets = json_encode($targets);
$media_endpoint = false;
if(array_key_exists('media_endpoint', $r['data'])) {
$user->micropub_media_endpoint = $r['data']['media_endpoint'];
}
if(count($targets) || $media_endpoint) {
$user->save(); $user->save();
} }
return array(
return [
'targets' => $targets, 'targets' => $targets,
'response' => $r 'response' => $r
);
];
} }
function static_map($latitude, $longitude, $height=180, $width=700, $zoom=14) { function static_map($latitude, $longitude, $height=180, $width=700, $zoom=14) {

0
public/editor-files/localforage/localforage.js → public/libs/localforage.js

2
schema/migrations/0001.sql

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

1
schema/mysql.sql

@ -4,6 +4,7 @@ CREATE TABLE `users` (
`authorization_endpoint` varchar(255) DEFAULT NULL, `authorization_endpoint` varchar(255) DEFAULT NULL,
`token_endpoint` varchar(255) DEFAULT NULL, `token_endpoint` varchar(255) DEFAULT NULL,
`micropub_endpoint` varchar(255) DEFAULT NULL, `micropub_endpoint` varchar(255) DEFAULT NULL,
`micropub_media_endpoint` varchar(255) DEFAULT NULL,
`micropub_access_token` text, `micropub_access_token` text,
`micropub_scope` varchar(255) DEFAULT NULL, `micropub_scope` varchar(255) DEFAULT NULL,
`micropub_response` text, `micropub_response` text,

4
views/auth_start.php

@ -36,7 +36,9 @@
<p><i>The Micropub endpoint is the URL this app will use to post new photos.</i></p> <p><i>The Micropub endpoint is the URL this app will use to post new photos.</i></p>
<?php if($this->micropubEndpoint): ?> <?php if($this->micropubEndpoint): ?>
<div class="bs-callout bs-callout-success">Found your Micropub endpoint: <code><?= $this->micropubEndpoint ?></code></div>
<div class="bs-callout bs-callout-success">
Found your Micropub endpoint: <code><?= $this->micropubEndpoint ?></code>
</div>
<?php else: ?> <?php else: ?>
<div class="bs-callout bs-callout-danger">Could not find your Micropub endpoint!</div> <div class="bs-callout bs-callout-danger">Could not find your Micropub endpoint!</div>
<p>You need to set your Micropub endpoint in a <code>&lt;link&gt;</code> tag on your home page.</p> <p>You need to set your Micropub endpoint in a <code>&lt;link&gt;</code> tag on your home page.</p>

2
views/editor.php

@ -30,7 +30,7 @@
<script src="/editor-files/handlebars.min.js"></script> <script src="/editor-files/handlebars.min.js"></script>
<script src="/editor-files/medium-editor/js/medium-editor.min.js"></script> <script src="/editor-files/medium-editor/js/medium-editor.min.js"></script>
<script src="/editor-files/medium-editor/js/medium-editor-insert-plugin.min.js"></script> <script src="/editor-files/medium-editor/js/medium-editor-insert-plugin.min.js"></script>
<script src="/editor-files/localforage/localforage.js"></script>
<script src="/libs/localforage.js"></script>
<link rel="apple-touch-icon" sizes="57x57" href="/images/quill-icon-57.png"> <link rel="apple-touch-icon" sizes="57x57" href="/images/quill-icon-57.png">
<link rel="apple-touch-icon" sizes="72x72" href="/images/quill-icon-72.png"> <link rel="apple-touch-icon" sizes="72x72" href="/images/quill-icon-72.png">

1
views/layout.php

@ -33,6 +33,7 @@
<meta name="theme-color" content="#428bca"> <meta name="theme-color" content="#428bca">
<script src="/js/jquery-1.7.1.min.js"></script> <script src="/js/jquery-1.7.1.min.js"></script>
<script src="/libs/localforage.js"></script>
<script src="/js/script.js"></script> <script src="/js/script.js"></script>
<script src="/js/date.js"></script> <script src="/js/date.js"></script>
<script src="/js/cassis.js"></script> <script src="/js/cassis.js"></script>

99
views/new-post.php

@ -108,6 +108,12 @@
<td>micropub endpoint</td> <td>micropub endpoint</td>
<td><code><?= $this->micropub_endpoint ?></code> (should be a URL)</td> <td><code><?= $this->micropub_endpoint ?></code> (should be a URL)</td>
</tr> </tr>
<?php if($this->media_endpoint): ?>
<tr>
<td>media endpoint</td>
<td><code><?= $this->media_endpoint ?></code> (should be a URL)</td>
</tr>
<?php endif; ?>
<tr> <tr>
<td>access token</td> <td>access token</td>
<td>String of length <b><?= strlen($this->micropub_access_token) ?></b><?= (strlen($this->micropub_access_token) > 0) ? (', ending in <code>' . substr($this->micropub_access_token, -7) . '</code>') : '' ?> (should be greater than length 0)</td> <td>String of length <b><?= strlen($this->micropub_access_token) ?></b><?= (strlen($this->micropub_access_token) > 0) ? (', ending in <code>' . substr($this->micropub_access_token, -7) . '</code>') : '' ?> (should be greater than length 0)</td>
@ -137,14 +143,93 @@
</style> </style>
<script> <script>
function saveNoteState() {
var state = {
content: $("#note_content").val(),
inReplyTo: $("#note_in_reply_to").val(),
category: $("#note_category").val(),
slug: $("#note_slug").val(),
photo: $("#note_photo_url").val()
};
state.syndications = [];
$("#syndication-container button.btn-info").each(function(i,btn){
state.syndications[$(btn).data('syndicate-to')] = 'selected';
});
localforage.setItem('current-note', state);
}
function restoreNoteState() {
localforage.getItem('current-note', function(err,note){
if(note) {
$("#note_content").val(note.content);
$("#note_in_reply_to").val(note.inReplyTo);
$("#note_category").val(note.category);
$("#note_slug").val(note.slug);
if(note.photo) {
replacePhotoWithPhotoURL(note.photo);
}
console.log(note.syndications)
$("#syndication-container button").each(function(i,btn){
if($(btn).data('syndicate-to') in note.syndications) {
$(btn).addClass('btn-info');
}
});
$("#note_content").change();
}
});
}
function replacePhotoWithPhotoURL(url) {
$("#note_photo").after('<input type="url" name="note_photo_url" id="note_photo_url" value="" class="form-control">');
$("#note_photo_url").val(url);
$("#note_photo").remove();
$("#photo_preview").attr("src", url);
$("#photo_preview_container").removeClass("hidden");
}
$(function(){ $(function(){
var userHasSetCategory = false; var userHasSetCategory = false;
var hasMediaEndpoint = <?= $this->media_endpoint ? 'true' : 'false' ?>;
$("#note_content, #note_category, #note_in_reply_to, #note_slug").on('keyup change', function(e){
saveNoteState();
})
// Preview the photo when one is chosen
$("#photo_preview_container").addClass("hidden"); $("#photo_preview_container").addClass("hidden");
$("#note_photo").on("change", function(e){ $("#note_photo").on("change", function(e){
$("#photo_preview_container").removeClass("hidden");
$("#photo_preview").attr("src", URL.createObjectURL(e.target.files[0]) );
// If the user has a media endpoint, upload the photo to it right now
if(hasMediaEndpoint) {
// TODO: add loading state indicator here
console.log("Uploading file to media endpoint...");
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);
} else {
$("#photo_preview").attr("src", URL.createObjectURL(e.target.files[0]) );
$("#photo_preview_container").removeClass("hidden");
}
}); });
$("#remove_photo").on("click", function(){ $("#remove_photo").on("click", function(){
$("#note_photo").val(""); $("#note_photo").val("");
@ -163,7 +248,7 @@ $(function(){
// If the user didn't enter any categories, add them from the post // If the user didn't enter any categories, add them from the post
if(!userHasSetCategory) { if(!userHasSetCategory) {
var tags = $("#note_content").val().match(/#[a-z0-9]+/g);
var tags = $("#note_content").val().match(/#[a-z][a-z0-9]+/ig);
if(tags) { if(tags) {
$("#note_category").val(tags.map(function(tag){ return tag.replace('#',''); }).join(", ")); $("#note_category").val(tags.map(function(tag){ return tag.replace('#',''); }).join(", "));
} }
@ -223,8 +308,11 @@ $(function(){
formData.append("slug", v); formData.append("slug", v);
} }
if(document.getElementById("note_photo").files[0]) {
// Add either the photo as a file, or the photo URL depending on whether the user has a media endpoint
if(document.getElementById("note_photo") && document.getElementById("note_photo").files[0]) {
formData.append("photo", document.getElementById("note_photo").files[0]); formData.append("photo", document.getElementById("note_photo").files[0]);
} else if($("#note_photo_url").val()) {
formData.append("photo", $("#note_photo_url").val());
} }
// Need to append a placeholder field because if the file size max is hit, $_POST will // Need to append a placeholder field because if the file size max is hit, $_POST will
@ -240,6 +328,7 @@ $(function(){
console.log(request.responseText); console.log(request.responseText);
try { try {
var response = JSON.parse(request.responseText); var response = JSON.parse(request.responseText);
localforage.removeItem('current-note');
if(response.location) { if(response.location) {
window.location = response.location; window.location = response.location;
// console.log(response.location); // console.log(response.location);
@ -362,6 +451,8 @@ $(function(){
} }
bind_syndication_buttons(); bind_syndication_buttons();
restoreNoteState();
}); });
<?= partial('partials/syndication-js') ?> <?= partial('partials/syndication-js') ?>

1
views/partials/syndication-js.php

@ -20,6 +20,7 @@ function reload_syndications() {
function bind_syndication_buttons() { function bind_syndication_buttons() {
$("#syndication-container button").unbind("click").click(function(){ $("#syndication-container button").unbind("click").click(function(){
$(this).toggleClass('btn-info'); $(this).toggleClass('btn-info');
saveNoteState();
return false; return false;
}); });
} }
Loading…
Cancel
Save