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/auth.php b/controllers/auth.php index 3611416..33baa3e 100644 --- a/controllers/auth.php +++ b/controllers/auth.php @@ -64,7 +64,8 @@ $app->get('/', function($format='html') use($app) { ob_start(); render('index', array( 'title' => 'Quill', - 'meta' => '' + 'meta' => '', + 'authorizing' => false )); $html = ob_get_clean(); $res->body($html); diff --git a/controllers/controllers.php b/controllers/controllers.php index b444676..72ac2b8 100644 --- a/controllers/controllers.php +++ b/controllers/controllers.php @@ -72,7 +72,8 @@ $app->get('/new', function() use($app) { 'response_date' => $user->last_micropub_response_date, 'syndication_targets' => json_decode($user->syndication_targets, true), 'test_response' => $test_response, - 'location_enabled' => $user->location_enabled + 'location_enabled' => $user->location_enabled, + 'authorizing' => false )); $app->response()->body($html); } @@ -103,7 +104,8 @@ $app->get('/bookmark', function() use($app) { 'bookmark_content' => $content, 'bookmark_tags' => $tags, 'token' => generate_login_token(), - 'syndication_targets' => json_decode($user->syndication_targets, true) + 'syndication_targets' => json_decode($user->syndication_targets, true), + 'authorizing' => false )); $app->response()->body($html); } @@ -121,7 +123,21 @@ $app->get('/favorite', function() use($app) { $html = render('new-favorite', array( 'title' => 'New Favorite', 'url' => $url, - 'token' => generate_login_token() + 'token' => generate_login_token(), + 'authorizing' => false + )); + $app->response()->body($html); + } +}); + +$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); } @@ -139,7 +155,8 @@ $app->get('/repost', function() use($app) { $html = render('new-repost', array( 'title' => 'New Repost', 'url' => $url, - 'token' => generate_login_token() + 'token' => generate_login_token(), + 'authorizing' => false )); $app->response()->body($html); } @@ -160,17 +177,17 @@ $app->get('/creating-a-token-endpoint', function() use($app) { $app->redirect('http://indiewebcamp.com/token-endpoint', 301); }); $app->get('/creating-a-micropub-endpoint', function() use($app) { - $html = render('creating-a-micropub-endpoint', array('title' => 'Creating a Micropub Endpoint')); + $html = render('creating-a-micropub-endpoint', array('title' => 'Creating a Micropub Endpoint', 'authorizing' => false)); $app->response()->body($html); }); $app->get('/docs', function() use($app) { - $html = render('docs', array('title' => 'Documentation')); + $html = render('docs', array('title' => 'Documentation', 'authorizing' => false)); $app->response()->body($html); }); $app->get('/privacy', function() use($app) { - $html = render('privacy', array('title' => 'Quill Privacy Policy')); + $html = render('privacy', array('title' => 'Quill Privacy Policy', 'authorizing' => false)); $app->response()->body($html); }); @@ -217,7 +234,7 @@ $app->get('/add-to-home', function() use($app) { $app->get('/settings', function() use($app) { if($user=require_login($app)) { - $html = render('settings', array('title' => 'Settings', 'include_facebook' => true)); + $html = render('settings', array('title' => 'Settings', 'include_facebook' => true, 'authorizing' => false)); $app->response()->body($html); } }); @@ -282,6 +299,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 +367,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/config.template.php b/lib/config.template.php index dae8968..ee822bb 100644 --- a/lib/config.template.php +++ b/lib/config.template.php @@ -4,11 +4,16 @@ class Config { public static $base_url = 'http://quill.dev/'; public static $gaid = ''; + // MySQL (default) public static $dbHost = '127.0.0.1'; public static $dbName = 'quill'; public static $dbUsername = 'quill'; public static $dbPassword = ''; + // Sqlite + // public static $dbType = 'sqlite'; + // public static $dbFilePath = './example.db'; + public static $jwtSecret = 'xxx'; public static $fbClientID = ''; diff --git a/lib/helpers.php b/lib/helpers.php index 4f6b4c1..eb8994c 100644 --- a/lib/helpers.php +++ b/lib/helpers.php @@ -1,8 +1,12 @@ 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 +94,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 +231,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/schema/schema.sql b/schema/mysql.sql similarity index 100% rename from schema/schema.sql rename to schema/mysql.sql diff --git a/schema/sqlite.sql b/schema/sqlite.sql new file mode 100644 index 0000000..ac691e3 --- /dev/null +++ b/schema/sqlite.sql @@ -0,0 +1,22 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + url TEXT, + authorization_endpoint TEXT, + token_endpoint TEXT, + micropub_endpoint TEXT, + micropub_access_token TEXT, + micropub_scope TEXT, + micropub_response TEXT, + micropub_success INTEGER default 0, + date_created datetime, + last_login datetime, + last_micropub_response TEXT, + last_micropub_response_date datetime, + location_enabled INTEGER NOT NULL default 0, + syndication_targets TEXT, + facebook_access_token TEXT, + twitter_access_token TEXT, + twitter_token_secret TEXT, + twitter_username TEXT, + instagram_access_token TEXT +); \ 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