summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAaron Parecki <aaron@parecki.com>2016-05-11 17:47:17 +0200
committerAaron Parecki <aaron@parecki.com>2016-05-11 17:47:17 +0200
commit542aa812f8606dad16ab456c3e5da438cc501644 (patch)
tree1a5b071eda488c11d6958a8c6cde83a5e09c8d4c
parent29f0c9b0543cdbf7780ce6e45204bd62a4ba4f52 (diff)
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
-rw-r--r--controllers/auth.php4
-rw-r--r--controllers/controllers.php28
-rw-r--r--controllers/editor.php33
-rw-r--r--lib/helpers.php42
-rw-r--r--public/libs/localforage.js (renamed from public/editor-files/localforage/localforage.js)0
-rw-r--r--schema/migrations/0001.sql2
-rw-r--r--schema/mysql.sql1
-rw-r--r--views/auth_start.php4
-rw-r--r--views/editor.php2
-rw-r--r--views/layout.php1
-rw-r--r--views/new-post.php99
-rw-r--r--views/partials/syndication-js.php1
12 files changed, 187 insertions, 30 deletions
diff --git a/controllers/auth.php b/controllers/auth.php
index 748f7ad..c6c4ad8 100644
--- a/controllers/auth.php
+++ b/controllers/auth.php
@@ -212,9 +212,9 @@ $app->get('/auth/callback', function() use($app) {
$user->save();
$_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.
- get_syndication_targets($user);
+ get_micropub_config($user);
}
unset($_SESSION['auth_state']);
diff --git a/controllers/controllers.php b/controllers/controllers.php
index 4373fcf..2a90892 100644
--- a/controllers/controllers.php
+++ b/controllers/controllers.php
@@ -76,6 +76,7 @@ $app->get('/new', function() use($app) {
'title' => 'New Post',
'in_reply_to' => $in_reply_to,
'micropub_endpoint' => $user->micropub_endpoint,
+ 'media_endpoint' => $user->micropub_media_endpoint,
'micropub_scope' => $user->micropub_scope,
'micropub_access_token' => $user->micropub_access_token,
'response_date' => $user->last_micropub_response_date,
@@ -452,7 +453,7 @@ $app->post('/repost', function() use($app) {
$app->get('/micropub/syndications', function() use($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(
'targets' => $data['targets'],
'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) {
if($user=require_login($app)) {
$params = $app->request()->params();
diff --git a/controllers/editor.php b/controllers/editor.php
index a3c0496..fbb72cf 100644
--- a/controllers/editor.php
+++ b/controllers/editor.php
@@ -35,19 +35,30 @@ $app->post('/editor/publish', 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) {
diff --git a/lib/helpers.php b/lib/helpers.php
index f0c226b..76f209e 100644
--- a/lib/helpers.php
+++ b/lib/helpers.php
@@ -109,6 +109,20 @@ function micropub_post_for_user(&$user, $params, $file_path = NULL, $json = fals
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) {
$ch = curl_init();
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);
$error = curl_error($ch);
$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(
'request' => $request,
'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(is_array($r['data']['syndicate-to'])) {
$data = $r['data']['syndicate-to'];
} else {
- $data = array();
+ $data = [];
}
foreach($data as $t) {
@@ -212,23 +226,31 @@ function get_syndication_targets(&$user) {
}
if(array_key_exists('uid', $t) && array_key_exists('name', $t)) {
- $targets[] = array(
+ $targets[] = [
'target' => $t['name'],
'uid' => $t['uid'],
'favicon' => $icon
- );
+ ];
}
}
}
- if(count($targets)) {
+
+ if(count($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();
}
- return array(
+ return [
'targets' => $targets,
'response' => $r
- );
+ ];
}
function static_map($latitude, $longitude, $height=180, $width=700, $zoom=14) {
diff --git a/public/editor-files/localforage/localforage.js b/public/libs/localforage.js
index 42e5391..42e5391 100644
--- a/public/editor-files/localforage/localforage.js
+++ b/public/libs/localforage.js
diff --git a/schema/migrations/0001.sql b/schema/migrations/0001.sql
new file mode 100644
index 0000000..87d67b6
--- /dev/null
+++ b/schema/migrations/0001.sql
@@ -0,0 +1,2 @@
+ALTER TABLE users
+ADD COLUMN `micropub_media_endpoint` VARCHAR(255) NOT NULL DEFAULT '' AFTER `micropub_endpoint`;
diff --git a/schema/mysql.sql b/schema/mysql.sql
index e9f6d1d..8eccb3a 100644
--- a/schema/mysql.sql
+++ b/schema/mysql.sql
@@ -4,6 +4,7 @@ CREATE TABLE `users` (
`authorization_endpoint` varchar(255) DEFAULT NULL,
`token_endpoint` varchar(255) DEFAULT NULL,
`micropub_endpoint` varchar(255) DEFAULT NULL,
+ `micropub_media_endpoint` varchar(255) DEFAULT NULL,
`micropub_access_token` text,
`micropub_scope` varchar(255) DEFAULT NULL,
`micropub_response` text,
diff --git a/views/auth_start.php b/views/auth_start.php
index 8635f07..93f45e3 100644
--- a/views/auth_start.php
+++ b/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>
<?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: ?>
<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>
diff --git a/views/editor.php b/views/editor.php
index 6d235c4..44b2987 100644
--- a/views/editor.php
+++ b/views/editor.php
@@ -30,7 +30,7 @@
<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-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="72x72" href="/images/quill-icon-72.png">
diff --git a/views/layout.php b/views/layout.php
index 03929b1..59025b1 100644
--- a/views/layout.php
+++ b/views/layout.php
@@ -33,6 +33,7 @@
<meta name="theme-color" content="#428bca">
<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/date.js"></script>
<script src="/js/cassis.js"></script>
diff --git a/views/new-post.php b/views/new-post.php
index da3927d..18e81d1 100644
--- a/views/new-post.php
+++ b/views/new-post.php
@@ -108,6 +108,12 @@
<td>micropub endpoint</td>
<td><code><?= $this->micropub_endpoint ?></code> (should be a URL)</td>
</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>
<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>
@@ -137,14 +143,93 @@
</style>
<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(){
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");
$("#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(){
$("#note_photo").val("");
@@ -163,7 +248,7 @@ $(function(){
// If the user didn't enter any categories, add them from the post
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) {
$("#note_category").val(tags.map(function(tag){ return tag.replace('#',''); }).join(", "));
}
@@ -223,8 +308,11 @@ $(function(){
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]);
+ } 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
@@ -240,6 +328,7 @@ $(function(){
console.log(request.responseText);
try {
var response = JSON.parse(request.responseText);
+ localforage.removeItem('current-note');
if(response.location) {
window.location = response.location;
// console.log(response.location);
@@ -362,6 +451,8 @@ $(function(){
}
bind_syndication_buttons();
+
+ restoreNoteState();
});
<?= partial('partials/syndication-js') ?>
diff --git a/views/partials/syndication-js.php b/views/partials/syndication-js.php
index 6267327..cd50c3b 100644
--- a/views/partials/syndication-js.php
+++ b/views/partials/syndication-js.php
@@ -20,6 +20,7 @@ function reload_syndications() {
function bind_syndication_buttons() {
$("#syndication-container button").unbind("click").click(function(){
$(this).toggleClass('btn-info');
+ saveNoteState();
return false;
});
}