From 85e80df0ba127936f9ba646ba00e25b37ddc2ec4 Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 23 Jul 2015 00:32:01 +0200 Subject: [PATCH] Adds possibility to post photos. With this it is possible to post a photo note with a description, nothing more. It doesn't move the file in the file system, just posts it from the temp location to the users server. It also does validate for file size, content type and max upload size and shows the errors to the user. If everything goes according to plan the response from the users server is shown, together with a link with the posted photos URL. --- composer.json | 3 +- composer.lock | 75 +++++++++++++++++++++++--------- controllers/controllers.php | 61 ++++++++++++++++++++++++++ lib/helpers.php | 85 ++++++++++++++++++++++++++++++++----- views/layout.php | 1 + views/photo.php | 38 +++++++++++++++++ 6 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 views/photo.php diff --git a/composer.json b/composer.json index cf2f57e..1156a2b 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "mpratt/relativetime": ">=1.0", "firebase/php-jwt": "dev-master", "ruudk/twitter-oauth": "dev-master", - "andreyco/instagram": "3.*" + "andreyco/instagram": "3.*", + "p3k/multipart": "*" }, "autoload": { "files": [ diff --git a/composer.lock b/composer.lock index eb3dfc1..819c8ea 100644 --- a/composer.lock +++ b/composer.lock @@ -1,9 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" ], - "hash": "f2f8fdb671b52ce22dc0a5e133f9a13d", + "hash": "561c25a6b782004d9b05656de5d67971", "packages": [ { "name": "andreyco/instagram", @@ -51,27 +52,25 @@ { "name": "firebase/php-jwt", "version": "dev-master", - "target-dir": "Firebase/PHP-JWT", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "83b8899cb73d85d648af93f37ec0ac89f4a5bbae" + "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b8899cb73d85d648af93f37ec0ac89f4a5bbae", - "reference": "83b8899cb73d85d648af93f37ec0ac89f4a5bbae", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", + "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": ">=5.3.0" }, "type": "library", "autoload": { - "classmap": [ - "Authentication/", - "Exceptions/" - ] + "psr-4": { + "Firebase\\JWT\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -91,7 +90,7 @@ ], "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", - "time": "2014-11-18 17:58:25" + "time": "2015-07-22 18:31:08" }, { "name": "indieauth/client", @@ -365,16 +364,16 @@ }, { "name": "mpratt/relativetime", - "version": "1.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/mpratt/RelativeTime.git", - "reference": "5dd7078d2bc830227c1f5a0081c68c323fb18555" + "reference": "219e6568fa3e7b181244f93be493fbab4c89c056" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpratt/RelativeTime/zipball/5dd7078d2bc830227c1f5a0081c68c323fb18555", - "reference": "5dd7078d2bc830227c1f5a0081c68c323fb18555", + "url": "https://api.github.com/repos/mpratt/RelativeTime/zipball/219e6568fa3e7b181244f93be493fbab4c89c056", + "reference": "219e6568fa3e7b181244f93be493fbab4c89c056", "shasum": "" }, "require": { @@ -409,7 +408,43 @@ "time", "time-ago" ], - "time": "2013-09-23 22:51:48" + "time": "2015-05-28 14:13:23" + }, + { + "name": "p3k/multipart", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/aaronpk/php-multipart-encoder.git", + "reference": "f5400011b20046cebbdfed686d051fb2aa600a14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aaronpk/php-multipart-encoder/zipball/f5400011b20046cebbdfed686d051fb2aa600a14", + "reference": "f5400011b20046cebbdfed686d051fb2aa600a14", + "shasum": "" + }, + "require": { + "php": ">5.4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/p3k/Multipart.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache 2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + } + ], + "description": "Multipart Encoding Library", + "time": "2015-07-16 19:28:02" }, { "name": "ruudk/twitter-oauth", @@ -486,12 +521,12 @@ "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/codeguy/Slim.git", + "url": "https://github.com/slimphp/Slim.git", "reference": "b8181de1112a1e2f565b40158b621c34ded38053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeguy/Slim/zipball/b8181de1112a1e2f565b40158b621c34ded38053", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/b8181de1112a1e2f565b40158b621c34ded38053", "reference": "b8181de1112a1e2f565b40158b621c34ded38053", "shasum": "" }, @@ -533,6 +568,8 @@ "firebase/php-jwt": 20, "ruudk/twitter-oauth": 20 }, + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/controllers/controllers.php b/controllers/controllers.php index b444676..337f780 100644 --- a/controllers/controllers.php +++ b/controllers/controllers.php @@ -127,6 +127,19 @@ $app->get('/favorite', function() use($app) { } }); +$app->get('/photo', function() use($app) { + if($user=require_login($app)) { + $params = $app->request()->params(); + + $html = render('photo', array( + 'title' => 'New Photo', + 'note_content' => '', + 'authorizing' => false + )); + $app->response()->body($html); + } +}); + $app->get('/repost', function() use($app) { if($user=require_login($app)) { $params = $app->request()->params(); @@ -282,6 +295,20 @@ function create_favorite(&$user, $url) { 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) { $micropub_request = array( 'repost-of' => $url @@ -336,6 +363,40 @@ $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) { if($user=require_login($app)) { $params = $app->request()->params(); diff --git a/lib/helpers.php b/lib/helpers.php index 4f6b4c1..ad16e1b 100644 --- a/lib/helpers.php +++ b/lib/helpers.php @@ -70,9 +70,9 @@ function get_timezone($lat, $lng) { return null; } -function micropub_post_for_user(&$user, $params) { +function micropub_post_for_user(&$user, $params, $file_path = NULL) { // Now send to the micropub endpoint - $r = micropub_post($user->micropub_endpoint, $params, $user->micropub_access_token); + $r = micropub_post($user->micropub_endpoint, $params, $user->micropub_access_token, $file_path); $user->last_micropub_response = substr(json_encode($r), 0, 1024); $user->last_micropub_response_date = date('Y-m-d H:i:s'); @@ -90,21 +90,33 @@ function micropub_post_for_user(&$user, $params) { return $r; } -function micropub_post($endpoint, $params, $access_token) { +function micropub_post($endpoint, $params, $access_token, $file_path = NULL) { $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 = http_build_query(array_merge(array( - 'h' => 'entry' - ), $params)); - $post = preg_replace('/%5B[0-9]+%5D/', '%5B%5D', $post); // change [0] to [] + + $httpheaders = array('Authorization: Bearer ' . $access_token); + $params = array_merge(array('h' => 'entry'), $params); + + if(!$file_path) { + $post = http_build_query($params); + $post = preg_replace('/%5B[0-9]+%5D/', '%5B%5D', $post); // change [0] to [] + } else { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimetype = finfo_file($finfo, $file_path); + $multipart = new p3k\Multipart(); + $multipart->addArray($params); + $multipart->addFile('photo', $file_path, $mimetype); + $post = $multipart->data(); + array_push($httpheaders, 'Content-Type: ' . $multipart->contentType()); + } + + curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheaders); 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); @@ -215,4 +227,57 @@ function instagram_client() { )); } +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'] > 1000000) { + 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(); + } +} \ No newline at end of file diff --git a/views/layout.php b/views/layout.php index 072dea6..7c2dab9 100644 --- a/views/layout.php +++ b/views/layout.php @@ -66,6 +66,7 @@ if(property_exists($this, 'include_facebook')) {
  • New Note
  • Bookmark
  • Favorite
  • +
  • Photo
  • Docs
  • diff --git a/views/photo.php b/views/photo.php new file mode 100644 index 0000000..364a55d --- /dev/null +++ b/views/photo.php @@ -0,0 +1,38 @@ +
    + + +
    + +
    + + +

    Photo JPEG, GIF or PNG.

    +
    + +
    + + +
    + + +
    + + location)): ?> +
    + Success! Photo posted to: location ?> +
    + + + error)): ?> +
    + Error: error ?> +
    + + + response)): ?> +

    Response:

    +
    response ?>
    + + + +
    \ No newline at end of file