Browse Source

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.
pull/22/head
Jeena 9 years ago
parent
commit
85e80df0ba
6 changed files with 233 additions and 30 deletions
  1. +2
    -1
      composer.json
  2. +56
    -19
      composer.lock
  3. +61
    -0
      controllers/controllers.php
  4. +75
    -10
      lib/helpers.php
  5. +1
    -0
      views/layout.php
  6. +38
    -0
      views/photo.php

+ 2
- 1
composer.json View File

@ -10,7 +10,8 @@
"mpratt/relativetime": ">=1.0", "mpratt/relativetime": ">=1.0",
"firebase/php-jwt": "dev-master", "firebase/php-jwt": "dev-master",
"ruudk/twitter-oauth": "dev-master", "ruudk/twitter-oauth": "dev-master",
"andreyco/instagram": "3.*"
"andreyco/instagram": "3.*",
"p3k/multipart": "*"
}, },
"autoload": { "autoload": {
"files": [ "files": [

+ 56
- 19
composer.lock View File

@ -1,9 +1,10 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "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": [ "packages": [
{ {
"name": "andreyco/instagram", "name": "andreyco/instagram",
@ -51,27 +52,25 @@
{ {
"name": "firebase/php-jwt", "name": "firebase/php-jwt",
"version": "dev-master", "version": "dev-master",
"target-dir": "Firebase/PHP-JWT",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/firebase/php-jwt.git", "url": "https://github.com/firebase/php-jwt.git",
"reference": "83b8899cb73d85d648af93f37ec0ac89f4a5bbae"
"reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1"
}, },
"dist": { "dist": {
"type": "zip", "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": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.2.0"
"php": ">=5.3.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"classmap": [
"Authentication/",
"Exceptions/"
]
"psr-4": {
"Firebase\\JWT\\": "src"
}
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "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.", "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", "homepage": "https://github.com/firebase/php-jwt",
"time": "2014-11-18 17:58:25"
"time": "2015-07-22 18:31:08"
}, },
{ {
"name": "indieauth/client", "name": "indieauth/client",
@ -365,16 +364,16 @@
}, },
{ {
"name": "mpratt/relativetime", "name": "mpratt/relativetime",
"version": "1.0",
"version": "1.5.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/mpratt/RelativeTime.git", "url": "https://github.com/mpratt/RelativeTime.git",
"reference": "5dd7078d2bc830227c1f5a0081c68c323fb18555"
"reference": "219e6568fa3e7b181244f93be493fbab4c89c056"
}, },
"dist": { "dist": {
"type": "zip", "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": "" "shasum": ""
}, },
"require": { "require": {
@ -409,7 +408,43 @@
"time", "time",
"time-ago" "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", "name": "ruudk/twitter-oauth",
@ -486,12 +521,12 @@
"version": "2.2.0", "version": "2.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/codeguy/Slim.git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "b8181de1112a1e2f565b40158b621c34ded38053" "reference": "b8181de1112a1e2f565b40158b621c34ded38053"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/codeguy/Slim/zipball/b8181de1112a1e2f565b40158b621c34ded38053",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/b8181de1112a1e2f565b40158b621c34ded38053",
"reference": "b8181de1112a1e2f565b40158b621c34ded38053", "reference": "b8181de1112a1e2f565b40158b621c34ded38053",
"shasum": "" "shasum": ""
}, },
@ -533,6 +568,8 @@
"firebase/php-jwt": 20, "firebase/php-jwt": 20,
"ruudk/twitter-oauth": 20 "ruudk/twitter-oauth": 20
}, },
"prefer-stable": false,
"prefer-lowest": false,
"platform": [], "platform": [],
"platform-dev": [] "platform-dev": []
} }

+ 61
- 0
controllers/controllers.php View File

@ -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) { $app->get('/repost', function() use($app) {
if($user=require_login($app)) { if($user=require_login($app)) {
$params = $app->request()->params(); $params = $app->request()->params();
@ -282,6 +295,20 @@ function create_favorite(&$user, $url) {
return $r; 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) { function create_repost(&$user, $url) {
$micropub_request = array( $micropub_request = array(
'repost-of' => $url '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) { $app->post('/repost', function() use($app) {
if($user=require_login($app)) { if($user=require_login($app)) {
$params = $app->request()->params(); $params = $app->request()->params();

+ 75
- 10
lib/helpers.php View File

@ -70,9 +70,9 @@ function get_timezone($lat, $lng) {
return null; 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 // 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 = substr(json_encode($r), 0, 1024);
$user->last_micropub_response_date = date('Y-m-d H:i:s'); $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; return $r;
} }
function micropub_post($endpoint, $params, $access_token) {
function micropub_post($endpoint, $params, $access_token, $file_path = NULL) {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer ' . $access_token
));
curl_setopt($ch, CURLOPT_POST, true); 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_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true); curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$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);
@ -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();
}
}

+ 1
- 0
views/layout.php View File

@ -66,6 +66,7 @@ if(property_exists($this, 'include_facebook')) {
<li><a href="/new">New Note</a></li> <li><a href="/new">New Note</a></li>
<li><a href="/bookmark">Bookmark</a></li> <li><a href="/bookmark">Bookmark</a></li>
<li><a href="/favorite">Favorite</a></li> <li><a href="/favorite">Favorite</a></li>
<li><a href="/photo">Photo</a></li>
<? } ?> <? } ?>
<li><a href="/docs">Docs</a></li> <li><a href="/docs">Docs</a></li>

+ 38
- 0
views/photo.php View File

@ -0,0 +1,38 @@
<div class="narrow">
<?= partial('partials/header') ?>
<form method="POST" role="form" style="margin-top: 20px;" id="note_form" enctype="multipart/form-data">
<div class="form-group">
<label for="note_photo"><code>photo</code></label>
<input type="file" name="note_photo" id="note_photo" accept="image/jpg,image/jpeg,image/gif,image/png">
<p class="help-block">Photo JPEG, GIF or PNG.</p>
</div>
<div class="form-group">
<label for="note_content"><code>content</code> (optional)</label>
<textarea name="note_content" id="note_content" value="" class="form-control" style="height: 4em;"><? if(isset($this->note_content)) echo $this->note_content ?></textarea>
</div>
<button class="btn btn-success" id="btn_post">Post</button>
</form>
<? if(!empty($this->location)): ?>
<div class="alert alert-success">
<strong>Success!</strong> Photo posted to: <em><a href="<?= $this->location ?>"><?= $this->location ?></a></em>
</div>
<? endif ?>
<? if(!empty($this->error)): ?>
<div class="alert alert-danger">
<strong>Error:</strong> <em><?= $this->error ?></em>
</div>
<? endif ?>
<? if(!empty($this->response)): ?>
<h4>Response:</h4>
<pre><?= $this->response ?></pre>
<? endif ?>
</div>

Loading…
Cancel
Save