Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor file-download code to allow AWS etc #327

Merged
merged 10 commits into from
Mar 5, 2015
9 changes: 3 additions & 6 deletions admin/attributes_controller.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php
/**
* @package admin
* @copyright Copyright 2003-2014 Zen Cart Development Team
* @copyright Copyright 2003-2015 Zen Cart Development Team
* @copyright Portions Copyright 2003 osCommerce
* @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0
* @version GIT: $Id: Author: DrByte Sat Jul 21 17:10:54 2012 -0400 Modified in v1.6.0 $
* @version $Id: Modified in v1.6.0 $
*/
require('includes/application_top.php');

Expand Down Expand Up @@ -1535,11 +1535,8 @@ function popupWindow(url) {
$download_display = $db->Execute($download_display_query_raw);
if ($download_display->RecordCount() > 0) {

// Moved to /admin/includes/configure.php
if (!defined('DIR_FS_DOWNLOAD')) define('DIR_FS_DOWNLOAD', DIR_FS_CATALOG . 'download/');

$filename_is_missing='';
if ( !file_exists(DIR_FS_DOWNLOAD . $download_display->fields['products_attributes_filename']) ) {
if ( !zen_orders_products_downloads($download_display->fields['products_attributes_filename']) ) {
$filename_is_missing = zen_image(DIR_WS_IMAGES . 'icon_status_red.gif');
} else {
$filename_is_missing = zen_image(DIR_WS_IMAGES . 'icon_status_green.gif');
Expand Down
19 changes: 8 additions & 11 deletions admin/downloads_manager.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php
/**
* @package admin
* @copyright Copyright 2003-2014 Zen Cart Development Team
* @copyright Copyright 2003-2015 Zen Cart Development Team
* @copyright Portions Copyright 2003 osCommerce
* @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0
* @version GIT: $Id: Author: DrByte Jun 30 2014 Modified in v1.5.4 $
* @version $Id: Modified in v1.6.0 $
*/

require('includes/application_top.php');
Expand Down Expand Up @@ -124,15 +124,12 @@ function go_option() {
$padInfo = new objectInfo($padInfo_array);
}

// Moved to /admin/includes/configure.php
if (!defined('DIR_FS_DOWNLOAD')) define('DIR_FS_DOWNLOAD', DIR_FS_CATALOG . 'download/');

$filename_is_missing='';
if ( !file_exists(DIR_FS_DOWNLOAD . $products_downloads_query->fields['products_attributes_filename']) ) {
$filename_is_missing = zen_image(DIR_WS_IMAGES . 'icon_status_red.gif');
} else {
$filename_is_missing = zen_image(DIR_WS_IMAGES . 'icon_status_green.gif');
}
$filename_is_missing='';
if ( !zen_orders_products_downloads($products_downloads_query->fields['products_attributes_filename']) ) {
$filename_is_missing = zen_image(DIR_WS_IMAGES . 'icon_status_red.gif');
} else {
$filename_is_missing = zen_image(DIR_WS_IMAGES . 'icon_status_green.gif');
}
?>
<?php
if (isset($padInfo) && is_object($padInfo) && ($products_downloads_query->fields['products_attributes_id'] == $padInfo->products_attributes_id)) {
Expand Down
44 changes: 33 additions & 11 deletions admin/includes/functions/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @copyright Copyright 2003-2015 Zen Cart Development Team
* @copyright Portions Copyright 2003 osCommerce
* @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0
* @version GIT: $Id: Modified in v1.6.0 $
* @version $Id: Modified in v1.6.0 $
*/

////
Expand Down Expand Up @@ -2516,7 +2516,7 @@ function zen_has_product_attributes_downloads($products_id, $check_valid=false)
if ($check_valid == true) {
$valid_downloads = '';
while (!$download_display->EOF) {
if (!file_exists(DIR_FS_DOWNLOAD . $download_display->fields['products_attributes_filename'])) {
if (!file_exists(zen_get_download_handler($download_display->fields['products_attributes_filename']))) {
$valid_downloads .= '<br />&nbsp;&nbsp;' . zen_image(DIR_WS_IMAGES . 'icon_status_red.gif') . ' Invalid: ' . $download_display->fields['products_attributes_filename'];
// break;
} else {
Expand Down Expand Up @@ -3264,19 +3264,41 @@ function zen_date_diff($date1, $date2) {
* check that the specified download filename exists on the filesystem
*/
function zen_orders_products_downloads($check_filename) {
global $db;
global $zco_notifier;

$valid_downloads = true;
if (!defined('DIR_FS_DOWNLOAD')) define('DIR_FS_DOWNLOAD', DIR_FS_CATALOG . 'download/');
$handler = zen_get_download_handler($check_filename);

if (!file_exists(DIR_FS_DOWNLOAD . $check_filename)) {
$valid_downloads = false;
// break;
} else {
$valid_downloads = true;
if ($handler == 'local') {
return file_exists(DIR_FS_DOWNLOAD . $check_filename);
}

return $valid_downloads;
/**
* An observer hooking this notifier should set $handler to blank if it tries a validation and fails.
* Or, if validation passes, simply set $handler to the service name (first chars before first colon in filename)
* Or, or there is no way to verify, do nothing to $handler.
*/
$zco_notifier->notify('NOTIFY_TEST_DOWNLOADABLE_FILE_EXISTS', $check_filename, $handler);

// if handler is set but isn't local (internal) then we simply return true since there's no way to "test"
if ($handler != '') return true;

// else if the notifier caused $handler to be empty then that means it failed verification, so we return false
return false;
}

/**
* check if the specified download filename matches a handler for an external download service
* If yes, it will be because the filename contains colons as delimiters ... service:filename:filesize
*/
function zen_get_download_handler($filename) {
$file_parts = explode(':', $filename);

// if the filename doesn't contain any colons, then there's no delimiter to return, so must be using built-in file handling
if (sizeof($file_parts) < 2) {
return 'local';
}

return $file_parts[0];
}

/**
Expand Down
230 changes: 230 additions & 0 deletions includes/classes/observers/auto.downloads_via_aws.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php
/**
* @package plugins
* @copyright Copyright 2003-2015 Zen Cart Development Team
* @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0
* @version $Id: Designed for v1.6.0 $
*/

/**
* This observer class is intended to allow downloadable files to be served
* from Amazon AWS S3 buckets, and also automatically expire the links
* so that customers can't share them or otherwise steal the files
*
*/
class zcObserverDownloadsViaAws extends base {

// this is where you can configure your AWS settings:
// --------------------------------------------------
// Alternatively, you can define them as constants in
// the extra_configures folder: AMAZON_S3_ACCESS_KEY
// and AMAZON_S3_ACCESS_SECRET
// --------------------------------------------------
/**
* You may set your Amazon AWS S3 Access Key and Secret Key here, as long as you're not committing this file to a version-control system like git.
* @var string
*/
private $aws_key = "MY_AMAZON_S3_ACCESS_KEY";
/**
* @var string
*/
private $aws_secret = "MY_AMAZON_S3_SECRET_XXXXXXXXX";

/**
* This is used to calculate a link that's good for 30 seconds,
* which is plenty of time for it to get started, & prevents
* unauthorized sharing and theft. Default is 30 seconds.
* @var integer
*/
protected $link_expiry_time = 30;

/**
* URL to Amazon S3 server
* @var string URL
*/
protected $aws_server = "https://s3.amazonaws.com";

/**
* Class constructor
*/
public function __construct() {
// read config from constants if available
if ($this->aws_key == 'MY_AMAZON_S3_ACCESS_KEY' && defined('AMAZON_S3_ACCESS_KEY')) $this->aws_key = AMAZON_S3_ACCESS_KEY;
if ($this->aws_secret == 'MY_AMAZON_S3_SECRET_XXXXXXXXX' && defined('AMAZON_S3_ACCESS_SECRET')) $this->aws_secret = AMAZON_S3_ACCESS_SECRET;

// if not configured, then don't activate
if ($this->aws_key == 'MY_AMAZON_S3_ACCESS_KEY' || $this->aws_key == '' || $this->aws_secret == '' || $this->aws_secret == 'MY_AMAZON_S3_SECRET_XXXXXXXXX') return false;

// attach listener
$this->attach($this, array('NOTIFY_CHECK_DOWNLOAD_HANDLER', 'NOTIFY_DOWNLOAD_READY_TO_START', 'NOTIFY_MODULE_DOWNLOAD_TEMPLATE_DETAILS', 'NOTIFY_TEST_DOWNLOADABLE_FILE_EXISTS'));
}


/**
* Parse the file details for display on template page
*
* @param string $eventID name of the observer event fired
* @param array $array $download->fields data
* @param array $data array passed by reference
*/
protected function updateNotifyModuleDownloadTemplateDetails(&$class, $eventID, $array, &$data)
{
// available fields:
// $data['service'] = 'local'
// $data['filename'] = db query result from orders_products_filename
// $data['expiry_timestamp']
// $data['expiry']
// $data['downloads_remaining']
// $data['unlimited_downloads']
// $data['file_exists'] = file_exists(DIR_FS_DOWNLOAD . $data['filename']);
// $data['is_downloadable'] = $data['file_exists'] && ($data['downloads_remaining'] > 0 && $data['expiry_timestamp'] > time()) || $data['unlimited_downloads'];
// $data['filesize'] = ($data['file_exists']) ? filesize(DIR_FS_DOWNLOAD . $file['orders_products_filename']) : 'Unknown';
// $data['date_purchased_day']
// $data['download_maxdays']
// $data['products_name']
// $data['orders_products_download_id'] = id for URL link
// $data['download_count']

$file_parts = $this->parseFileParts($data['filename']);

if ($file_parts === false) return;
if ($file_parts[0] != 'aws') return;

$data['service'] = $file_parts[0];

// use just the filename portion, skipping the bucket name for customer-facing display purposes
$data['filename'] = substr($file_parts[1], strrpos($file_parts[1], '/') + 1);

$data['filesize'] = isset($file_parts[2]) ? number_format($file_parts[2], 0) : '';
$data['filesize_units'] = '';

$data['is_downloadable'] = $data['file_exists'] = $this->testFileExists($data['filename']);
}

/**
* This observer should set $handler to blank if it fails to validate whether $filename exists on the external service.
* If validation passes, simply set $handler to the service name (first chars before first colon in filename) (or do nothing since it's probably already correct).
* If there is no way to verify, do nothing to $handler.
*
* @param string $eventID name of the observer event fired
* @param string $filename filename to verify exists
* @param string $handler name of external service handler
*/
protected function updateNotifyTestDownloadableFileExists(&$class, $eventID, $filename, &$handler)
{
$result = $this->testFileExists($filename);

if ($result === false) {
$handler = '';
}
}

/**
*
* @param string $eventID name of the observer event fired
* @param array $var deprecated array, used only for backward compatibility
* @param array $fields data feeding all download activities
* @param string $origin_filename (mutable)
* @param string $browser_filename (mutable)
* @param string $source_directory (mutable)
* @param boolean $file_exists (mutable)
*/
protected function updateNotifyCheckDownloadHandler(&$class, $eventID, $var, &$fields, &$origin_filename, &$browser_filename, &$source_directory, &$file_exists, &$service, &$isExpired, &$download_timestamp)
{
$file_parts = $this->parseFileParts($origin_filename);
if ($file_parts[0] == 'aws') {
$origin_filename = $file_parts[1];
$browser_filename = substr($origin_filename, strrpos($origin_filename, '/') + 1);
$source_directory = $file_parts[0];
$file_exists = true;
$service = $file_parts[0];
}
}

/**
* This fires when the download module wants to redirect to the external download service
* So, this method parses the passed file, obtains the URL, and does the redirect
*
* @param string $eventID name of the observer event fired
* @param string $ipaddress customer IP
* @param string $service (mutable)
* @param string $origin_filename (mutable)
* @param string $browser_filename (mutable)
* @param string $source_directory (mutable)
* @param integer $downloadFilesize (mutable)
* @param string $mime_type (mutable)
* @param array $fields array of data from db query feeding the download page
* @param string $browser_extra_headers (mutable)
*/
protected function updateNotifyDownloadReadyToStart(&$class, $eventID, $ipaddress, &$service, &$origin_filename, &$browser_filename, &$source_directory, &$downloadFilesize, $mime_type, $fields, $browser_extra_headers)
{
// verify that the passed file is indeed intended for aws
if ($source_directory != 'aws') {
$file_parts = $this->parseFileParts($origin_filename);
if ($file_parts[0] != 'aws') return false;
$origin_filename = $file_parts[1];
$browser_filename = substr($origin_filename, strrpos($origin_filename, '/') + 1);
$source_directory = $file_parts[0];
$downloadFilesize = $file_parts[2];
}

// prepare AWS URL
$url = $this->buildRedirectUrl($origin_filename);

// redirect to external download script
header("HTTP/1.1 303 See Other");
zen_redirect($url);

zen_exit();
}

/**
* parse file details to determine if its download should be handled by AWS
* If AWS, the filename will use colons as delimiters ... aws:bucket/filename:filesize
*
* @param string $filename
* @return boolean|array
*/
private function parseFileParts($filename) {

$file_parts = explode(':', $filename);

if (sizeof($file_parts) == 1) return false;

return $file_parts;
}

/**
* Prepare signed expiring URL for AWS redirect
*
* @param string $bucketAndFilename
* @return string $url
*/
private function buildRedirectUrl($bucketAndFilename) {

// this calculates a link that's good for 30 seconds, which is plenty of time for it to get started, and prevents theft
$expires = time() + $this->link_expiry_time;

$raw_request = "GET\n\n\n" . $expires . "\n/" . $bucketAndFilename;
$sig = urlencode(base64_encode((hash_hmac("sha1", utf8_encode($raw_request), $this->aws_secret, true))));

$params = 'AWSAccessKeyId=' . $this->aws_key . '&Expires=' . $expires . '&Signature=' . $sig;

return $this->aws_server . '/' . $bucketAndFilename . '?' . $params;
}

/**
* Use AWS SDK to test whether the bucket+file (designated by $filename) exists
* If it does not exist, return false
*
* @param string $filename
* @return boolean Result of SDK test
*/
private function testFileExists($filename)
{
// @TODO: could optionally add an AWS SDK call to actually check that the object (bucket+file) exists
// but for now we're simply assuming that it does
return true;
}

}