I have been kicking around trying to implement the S3 uploader into my application and getting closer but no cigar. Here is my setup in a nutshell:
- Running: 5.2.0 S3 with jQuery
- Server: PHP 5.6.6 on ArchLinux
- Max file size 25MB (defined in both s3.php and s3demo-cors.php)
- Debug enabled: which just pops the Javascript alert with the XHR error code 0 message (can't seem to gain visibility into whats going on behind the scenes with debug or in the apache logs)
- I specifically want to only use HTTPS for my bucket endpoint for security sake.
- Testing with Google Chrome Version 42.0.2311.90 m and IE 11.0.9600
- With Chrome it just fails, with IE it fails BUT it at least shows the progress meter for uploading the file, it hits 65% then fails.
I have followed the blog post here: http://blog.fineuploader.com/2013/08/16/fine-uploader-s3-upload-directly-to-amazon-s3-from-your-browser/ multiple times (hoping I am not missing something).
- QUESTION: One thing I am curious about is the JSON policy and signing. I am not really seeing the code in the s3demo-cors.php example that creates the json formatted details perhaps I am missing that element?
I know my IAM permissions are valid because other PHP tests allow various PutObject and list commands successfully.
I have verified my CORS config is setup as follows for testing:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>DELETE</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<ExposeHeader>ETag</ExposeHeader>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
I have a few files I am using for this:
s3.php = my test page with the fineuploader instance
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- jQuery
====================================================================== -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<!-- Fine Uploader Gallery CSS file
====================================================================== -->
<link href="fine-uploader-gallery.css" rel="stylesheet">
<!-- Fine Uploader S3 jQuery JS file
====================================================================== -->
<script src="s3.jquery.fine-uploader.js"></script>
<!-- Fine Uploader Customized Gallery template
====================================================================== -->
<script type="text/template" id="qq-template-s3">
<div class="qq-uploader-selector qq-uploader qq-gallery" qq-drop-area-text="Drop files here">
<div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div>
</div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="qq-upload-button-selector qq-upload-button">
<div>Upload a file</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" role="region" aria-live="polite" aria-relevant="additions removals">
<li>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
<div class="qq-progress-bar-container-selector qq-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<div class="qq-thumbnail-wrapper">
<a class="preview-link" target="_blank">
<img class="qq-thumbnail-selector" qq-max-size="120" qq-server-scale>
</a>
</div>
<button class="qq-upload-cancel-selector qq-upload-cancel">X</button>
<button class="qq-upload-retry-selector qq-upload-retry">
<span class="qq-btn qq-retry-icon" aria-label="Retry"></span>
Retry
</button>
<div class="qq-file-info">
<div class="qq-file-name">
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"></span>
</div>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button class="qq-btn qq-upload-delete-selector qq-upload-delete">
<span class="qq-btn qq-delete-icon" aria-label="Delete"></span>
</button>
<button class="qq-btn qq-upload-pause-selector qq-upload-pause">
<span class="qq-btn qq-pause-icon" aria-label="Pause"></span>
</button>
<button class="qq-btn qq-upload-continue-selector qq-upload-continue">
<span class="qq-btn qq-continue-icon" aria-label="Continue"></span>
</button>
</div>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button class="qq-cancel-button-selector">No</button>
<button class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button class="qq-cancel-button-selector">Cancel</button>
<button class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div>
</script>
<style>
#fine-uploader-s3 .preview-link {
display: block;
height: 100%;
width: 100%;
}
</style>
<title>Fine Uploader S3 Demo</title>
</head>
<body>
<!-- Fine Uploader DOM Element
====================================================================== -->
<div id="fine-uploader-s3"></div>
<!-- Your code to create an instance of Fine Uploader and bind to the DOM/template
====================================================================== -->
<script>
$('#fine-uploader-s3').fineUploaderS3({
debug: true,
template: 'qq-template-s3',
request: {
endpoint: "https://s3.amazonaws.com/<HIDDEN>",
accessKey: "<HIDDEN>"
},
signature: {
endpoint: "s3demo-cors.php"
},
uploadSuccess: {
endpoint: "s3demo-cors.php?success",
params: {
isBrowserPreviewCapable: qq.supportedFeatures.imagePreviews
}
},
iframeSupport: {
localBlankPagePath: "success.php"
},
cors: {
expected: true
},
chunking: {
enabled: true
},
resume: {
enabled: true
},
retry: {
enableAuto: true // defaults to false
},
deleteFile: {
enabled: true,
method: "POST",
endpoint: "s3demo-cors.php"
},
validation: {
itemLimit: 100,
sizeLimit: 25000000
},
thumbnails: {
placeholders: {
notAvailablePath: "not_available-generic.png",
waitingPath: "waiting-generic.png"
}
},
callbacks: {
onComplete: function(id, name, response) {
var previewLink = qq(this.getItemByFileId(id)).getByClass('preview-link')[0];
if (response.success) {
previewLink.setAttribute("href", response.tempLink)
}
},
onError: function(id, name, errorReason, xhrOrXdr) {
alert(qq.format("Error on file number {} - {}. Reason: {}", id, name, errorReason));
}
}
});
</script>
</body>
</html>
My PHP server side code is from your examples s3demo-cors.php (sorry code formatting got a little garbled when pasting in here)
<?php
/**
* PHP Server-Side Example for Fine Uploader S3.
* Maintained by Widen Enterprises.
*
* Note: This is the exact server-side code used by the S3 example
* on fineuploader.com.
*
* This example:
* - handles both CORS and non-CORS environments
* - handles delete file requests for both DELETE and POST methods
* - Performs basic inspections on the policy documents and REST headers before signing them
* - Ensures again the file size does not exceed the max (after file is in S3)
* - signs policy documents (simple uploads) and REST requests
* (chunked/multipart uploads)
* - returns a thumbnailUrl in the response for older browsers so thumbnails can be displayed next to the file
*
* Requirements:
* - PHP 5.3 or newer
* - Amazon PHP SDK (only if utilizing the AWS SDK for deleting files or otherwise examining them)
*
* If you need to install the AWS SDK, see http://docs.aws.amazon.com/aws-sdk-php-2/guide/latest/installation.html.
*/
// You can remove these two lines if you are not using Fine Uploader's
// delete file feature
//require 'aws-autoloader.php';
require 'vendor/autoload.php';
use Aws\S3\S3Client;
// These assume you have the associated AWS keys stored in
// the associated system environment variables
$clientPrivateKey = '<HIDDEN>';
// These two keys are only needed if the delete file feature is enabled
// or if you are, for example, confirming the file size in a successEndpoint
// handler via S3's SDK, as we are doing in this example.
$serverPublicKey = $_SERVER['PARAM1'];
$serverPrivateKey = $_SERVER['PARAM2'];
// The following variables are used when validating the policy document
// sent by the uploader:
$expectedBucketName = "<HIDDEN>";
// $expectedMaxSize is the value you set the sizeLimit property of the
// validation option. We assume it is `null` here. If you are performing
// validation, then change this to match the integer value you specified
// otherwise your policy document will be invalid.
// http://docs.fineuploader.com/branch/develop/api/options.html#validation- option
$expectedMaxSize = 25000000;
$method = getRequestMethod();
// This first conditional will only ever evaluate to true in a
// CORS environment
if ($method == 'OPTIONS') {
handlePreflight();
}
// This second conditional will only ever evaluate to true if
// the delete file feature is enabled
else if ($method == "DELETE") {
handleCorsRequest(); // only needed in a CORS environment
deleteObject();
}
// This is all you really need if not using the delete file feature
// and not working in a CORS environment
else if ($method == 'POST') {
handleCorsRequest();
// Assumes the successEndpoint has a parameter of "success" associated with it,
// to allow the server to differentiate between a successEndpoint request
// and other POST requests (all requests are sent to the same endpoint in this example).
// This condition is not needed if you don't require a callback on upload success.
if (isset($_REQUEST["success"])) {
verifyFileInS3(shouldIncludeThumbnail());
}
else {
signRequest();
}
}
// This will retrieve the "intended" request method. Normally, this is the
// actual method of the request. Sometimes, though, the intended request method
// must be hidden in the parameters of the request. For example, when attempting to
// send a DELETE request in a cross-origin environment in IE9 or older, it is not
// possible to send a DELETE request. So, we send a POST with the intended method,
// DELETE, in a "_method" parameter.
function getRequestMethod() {
global $HTTP_RAW_POST_DATA;
// This should only evaluate to true if the Content-Type is undefined
// or unrecognized, such as when XDomainRequest has been used to
// send the request.
if(isset($HTTP_RAW_POST_DATA)) {
parse_str($HTTP_RAW_POST_DATA, $_POST);
}
if (isset($_POST['_method'])) {
return $_POST['_method'];
}
return $_SERVER['REQUEST_METHOD'];
}
// Only needed in cross-origin setups
function handleCorsRequest() {
// If you are relying on CORS, you will need to adjust the allowed domain here.
header('Access-Control-Allow-Origin: *');
}
// Only needed in cross-origin setups
function handlePreflight() {
handleCorsRequest();
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
}
function getS3Client() {
global $serverPublicKey, $serverPrivateKey;
return S3Client::factory(array(
'key' => $serverPublicKey,
'secret' => $serverPrivateKey
));
}
// Only needed if the delete file feature is enabled
function deleteObject() {
getS3Client()->deleteObject(array(
'Bucket' => $_POST['bucket'],
'Key' => $_POST['key']
));
}
function signRequest() {
header('Content-Type: application/json');
$responseBody = file_get_contents('php://input');
$contentAsObject = json_decode($responseBody, true);
$jsonContent = json_encode($contentAsObject);
if (!empty($contentAsObject["headers"])) {
signRestRequest($contentAsObject["headers"]);
}
else {
signPolicy($jsonContent);
}
}
function signRestRequest($headersStr) {
if (isValidRestRequest($headersStr)) {
$response = array('signature' => sign($headersStr));
echo json_encode($response);
}
else {
echo json_encode(array("invalid" => true));
}
}
function isValidRestRequest($headersStr) {
global $expectedBucketName;
$pattern = "/\/$expectedBucketName\/.+$/";
preg_match($pattern, $headersStr, $matches);
return count($matches) > 0;
}
function signPolicy($policyStr) {
$policyObj = json_decode($policyStr, true);
if (isPolicyValid($policyObj)) {
$encodedPolicy = base64_encode($policyStr);
$response = array('policy' => $encodedPolicy, 'signature' => sign($encodedPolicy));
echo json_encode($response);
}
else {
echo json_encode(array("invalid" => true));
}
}
function isPolicyValid($policy) {
global $expectedMaxSize, $expectedBucketName;
$conditions = $policy["conditions"];
$bucket = null;
$parsedMaxSize = null;
for ($i = 0; $i < count($conditions); ++$i) {
$condition = $conditions[$i];
if (isset($condition["bucket"])) {
$bucket = $condition["bucket"];
}
else if (isset($condition[0]) && $condition[0] == "content-length-range") {
$parsedMaxSize = $condition[2];
}
}
return $bucket == $expectedBucketName && $parsedMaxSize == (string)$expectedMaxSize;
}
function sign($stringToSign) {
global $clientPrivateKey;
return base64_encode(hash_hmac(
'sha1',
$stringToSign,
$clientPrivateKey,
true
));
}
// This is not needed if you don't require a callback on upload success.
function verifyFileInS3($includeThumbnail) {
global $expectedMaxSize;
$bucket = $_POST["bucket"];
$key = $_POST["key"];
// If utilizing CORS, we return a 200 response with the error message in the body
// to ensure Fine Uploader can parse the error message in IE9 and IE8,
// since XDomainRequest is used on those browsers for CORS requests. XDomainRequest
// does not allow access to the response body for non-success responses.
if (isset($expectedMaxSize) && getObjectSize($bucket, $key) > $expectedMaxSize) {
// You can safely uncomment this next line if you are not depending on CORS
header("HTTP/1.0 500 Internal Server Error");
deleteObject();
echo json_encode(array("error" => "File is too big!", "preventRetry" => true));
}
else {
$link = getTempLink($bucket, $key);
$response = array("tempLink" => $link);
if ($includeThumbnail) {
$response["thumbnailUrl"] = $link;
}
echo json_encode($response);
}
}
// Provide a time-bombed public link to the file.
function getTempLink($bucket, $key) {
$client = getS3Client();
$url = "{$bucket}/{$key}";
$request = $client->get($url);
return $client->createPresignedUrl($request, '+15 minutes');
}
function getObjectSize($bucket, $key) {
$objInfo = getS3Client()->headObject(array(
'Bucket' => $bucket,
'Key' => $key
));
return $objInfo['ContentLength'];
}
// Return true if it's likely that the associate file is natively
// viewable in a browser. For simplicity, just uses the file extension
// to make this determination, along with an array of extensions that one
// would expect all supported browsers are able to render natively.
function isFileViewableImage($filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$viewableExtensions = array("jpeg", "jpg", "gif", "png");
return in_array($ext, $viewableExtensions);
}
// Returns true if we should attempt to include a link
// to a thumbnail in the uploadSuccess response. In it's simplest form
// (which is our goal here - keep it simple) we only include a link to
// a viewable image and only if the browser is not capable of generating a client-side preview.
function shouldIncludeThumbnail() {
$filename = $_POST["name"];
$isPreviewCapable = $_POST["isBrowserPreviewCapable"] == "true";
$isFileViewableImage = isFileViewableImage($filename);
return !$isPreviewCapable && $isFileViewableImage;
}
?>