diff options
author | Aaron Parecki <aaron@parecki.com> | 2016-05-11 17:47:17 +0200 |
---|---|---|
committer | Aaron Parecki <aaron@parecki.com> | 2016-05-11 17:47:17 +0200 |
commit | 542aa812f8606dad16ab456c3e5da438cc501644 (patch) | |
tree | 1a5b071eda488c11d6958a8c6cde83a5e09c8d4c | |
parent | 29f0c9b0543cdbf7780ce6e45204bd62a4ba4f52 (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.php | 4 | ||||
-rw-r--r-- | controllers/controllers.php | 28 | ||||
-rw-r--r-- | controllers/editor.php | 33 | ||||
-rw-r--r-- | lib/helpers.php | 42 | ||||
-rw-r--r-- | public/libs/localforage.js (renamed from public/editor-files/localforage/localforage.js) | 0 | ||||
-rw-r--r-- | schema/migrations/0001.sql | 2 | ||||
-rw-r--r-- | schema/mysql.sql | 1 | ||||
-rw-r--r-- | views/auth_start.php | 4 | ||||
-rw-r--r-- | views/editor.php | 2 | ||||
-rw-r--r-- | views/layout.php | 1 | ||||
-rw-r--r-- | views/new-post.php | 99 | ||||
-rw-r--r-- | views/partials/syndication-js.php | 1 |
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><link></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; }); } |