Skip to content

Instantly share code, notes, and snippets.

@frigiddesert
Forked from fowkswe/upload.php
Last active September 3, 2025 23:53
Show Gist options
  • Select an option

  • Save frigiddesert/f39078f916ba44fc961a06f32f74552c to your computer and use it in GitHub Desktop.

Select an option

Save frigiddesert/f39078f916ba44fc961a06f32f74552c to your computer and use it in GitHub Desktop.
Replacement for sendy's upload.php that sends files to S3 rather than the server. Verified working 2025 with current s3 structure and sendy 6.1.2
# AWS S3 Configuration for Sendy Image Uploads
AWS_ACCESS_KEY_ID=your_aws_access_key_here
AWS_SECRET_ACCESS_KEY=your_aws_secret_key_here
S3_BUCKET=
S3_REGION=

Sendy S3 Upload Integration - Deployment Guide

Secure .env-based approach for AWS S3 image uploads in Sendy

Files to Deploy

1. S3.php Library

https://github.com/tpyo/amazon-s3-php-class

  • Source: dev/includes/helpers/S3.php
  • Deploy to: sendy/includes/helpers/S3.php

2. Updated Upload Script

  • Source: dev/includes/create/upload-final.php
  • Deploy to: sendy/includes/create/upload.php
  • Action: Replace the existing upload.php file (backup original first!)

3. Environment Configuration

  • Source: dev/.env-production
  • Deploy to: sendy/.env
  • Action: Contains AWS credentials (configure before deploying)

Configuration Required

1. AWS S3 Bucket Setup

  • Create an S3 bucket or use existing one
  • Note your bucket name and region
  • Important: Modern S3 buckets have ACLs disabled - this is handled automatically

2. AWS IAM User Setup

Create an IAM user with this policy (replace YOUR_BUCKET_NAME):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}

3. Configure Environment File

Edit dev/.env-production before deployment:

# AWS S3 Configuration for Sendy Image Uploads
AWS_ACCESS_KEY_ID=your_actual_access_key
AWS_SECRET_ACCESS_KEY=your_actual_secret_key
S3_BUCKET=your-bucket-name
S3_REGION=your-region (e.g., us-west-2)

PHP Configuration

Ensure your php.ini has these settings:

upload_tmp_dir = /tmp
upload_max_filesize = 10M
post_max_size = 10M

Deployment Steps

Step 1: Backup Original Files

Docker Installation:

docker cp sendy:/var/www/html/includes/create/upload.php /tmp/upload.php.backup

Direct Installation:

cp /var/www/html/includes/create/upload.php /tmp/upload.php.backup
# Or wherever your Sendy installation is located

Step 2: Configure Environment File

  • Edit dev/.env-production with your actual AWS credentials
  • Update S3_BUCKET and S3_REGION values

Step 3: Deploy Files

Docker Installation:

# Copy files to server temp directory
scp dev/includes/helpers/S3.php user@server:/tmp/
scp dev/includes/create/upload-final.php user@server:/tmp/
scp dev/.env-production user@server:/tmp/.env

# Deploy to Docker container
docker cp /tmp/S3.php sendy:/var/www/html/includes/helpers/
docker cp /tmp/upload-final.php sendy:/var/www/html/includes/create/upload.php  
docker cp /tmp/.env sendy:/var/www/html/

Direct Installation:

# Copy files directly to Sendy installation
scp dev/includes/helpers/S3.php user@server:/var/www/html/includes/helpers/
scp dev/includes/create/upload-final.php user@server:/var/www/html/includes/create/upload.php
scp dev/.env-production user@server:/var/www/html/.env

# Or if copying locally:
cp dev/includes/helpers/S3.php /var/www/html/includes/helpers/
cp dev/includes/create/upload-final.php /var/www/html/includes/create/upload.php  
cp dev/.env-production /var/www/html/.env

Step 4: Set Proper Permissions ⚠️ CRITICAL STEP

Docker Installation:

# Set ownership to web server user
docker exec sendy chown www-data:www-data /var/www/html/includes/helpers/S3.php
docker exec sendy chown www-data:www-data /var/www/html/includes/create/upload.php
docker exec sendy chown www-data:www-data /var/www/html/.env

# Set file permissions
docker exec sendy chmod 644 /var/www/html/.env
docker exec sendy chmod 644 /var/www/html/includes/create/upload.php
docker exec sendy chmod 644 /var/www/html/includes/helpers/S3.php

Direct Installation:

# Set ownership to web server user (adjust user as needed: www-data, apache, nginx)
chown www-data:www-data /var/www/html/includes/helpers/S3.php
chown www-data:www-data /var/www/html/includes/create/upload.php
chown www-data:www-data /var/www/html/.env

# Set file permissions (important for .env security)
chmod 644 /var/www/html/.env
chmod 644 /var/www/html/includes/create/upload.php
chmod 644 /var/www/html/includes/helpers/S3.php

Note: Replace www-data with your web server user:

  • Ubuntu/Debian: www-data
  • CentOS/RHEL: apache
  • Some configurations: nginx

Step 5: Verify Permissions

Docker Installation:

docker exec sendy ls -la /var/www/html/.env

Direct Installation:

ls -la /var/www/html/.env

Should show: -rw-r--r-- 1 www-data www-data (or your web server user)

Step 6: Test Upload

  • Try uploading an image through Sendy's editor
  • Check browser console for any error messages
  • Verify image appears in your S3 bucket at s3://your-bucket/sendy/images/

Troubleshooting

Common Issues

  1. "AWS credentials not configured"

    • Check .env file exists: docker exec sendy cat /var/www/html/.env
    • Verify .env file permissions: docker exec sendy ls -la /var/www/html/.env
    • Ensure .env file owned by www-data: chown www-data:www-data /var/www/html/.env
  2. "The bucket does not allow ACLs"

    • Modern S3 buckets disable ACLs by default - this is handled in upload-final.php
    • Ensure you're using the updated upload script without ACL parameters
  3. "S3 upload failed"

    • Check bucket name and region are correct in .env file
    • Verify IAM user has proper permissions
    • Test credentials with: aws s3 ls s3://your-bucket-name --region your-region
  4. Permission denied errors

    • All files must be owned by www-data:www-data
    • .env file must be readable by web server (644 permissions)
    • Upload script must be executable by web server

Debugging

The upload script includes console logging. Check browser developer tools console for detailed error messages.

File Structure After Deployment

sendy/
├── includes/
│   ├── create/
│   │   └── upload.php (updated with S3 functionality)
│   └── helpers/
│       └── S3.php (new file)

Security Notes

  • Never commit AWS credentials to version control
  • Use IAM users with minimal required permissions
  • Regularly rotate AWS access keys
  • Monitor S3 bucket access logs
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
include('../functions.php');
include('../login/auth.php');
require_once('../helpers/S3.php');
// Validate required parameters
$app = isset($_GET['app']) && is_numeric($_GET['app']) ? mysqli_real_escape_string($mysqli, (int)$_GET['app']) : exit;
// Check if file was uploaded
if (!isset($_FILES['upload']) || $_FILES['upload']['error'] !== UPLOAD_ERR_OK) {
$uploadError = $_FILES['upload']['error'] ?? 'No file uploaded';
switch ($uploadError) {
case UPLOAD_ERR_INI_SIZE:
die('The uploaded file exceeds the upload_max_filesize directive in php.ini');
case UPLOAD_ERR_FORM_SIZE:
die('The uploaded file exceeds the MAX_FILE_SIZE directive');
case UPLOAD_ERR_PARTIAL:
die('The uploaded file was only partially uploaded');
case UPLOAD_ERR_NO_FILE:
die('No file was uploaded');
case UPLOAD_ERR_NO_TMP_DIR:
die('Missing a temporary folder');
case UPLOAD_ERR_CANT_WRITE:
die('Failed to write file to disk');
case UPLOAD_ERR_EXTENSION:
die('File upload stopped by extension');
default:
die('Upload error: ' . $uploadError);
}
}
// Init file variables
$file = $_FILES['upload']['tmp_name'];
$file_name = $_FILES['upload']['name'];
$extension_explode = explode('.', $file_name);
$extension = strtolower(end($extension_explode));
$extension2 = isset($extension_explode[count($extension_explode)-2]) ? $extension_explode[count($extension_explode)-2] : '';
// Security checks
if($extension2 == 'php' || $file_name == '.htaccess') {
die('File type not allowed for security reasons');
}
// Check if file is an allowed image type
$allowed = array("jpeg", "jpg", "gif", "png");
if(!in_array($extension, $allowed)) {
die('Only JPEG, JPG, GIF, and PNG files are allowed');
}
// Validate file exists and has content
if (!file_exists($file)) {
die('Temporary file does not exist');
}
if (filesize($file) === 0) {
die('Uploaded file is empty');
}
$time = time();
// Load environment variables from .env file with verbose logging
$logFile = '/tmp/sendy_s3_debug.log';
$currentDir = __DIR__;
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Upload script started\n", FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Current directory: $currentDir\n", FILE_APPEND);
$envFile = '../../.env';
$fullEnvPath = realpath($envFile);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Looking for .env at: $envFile\n", FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Full .env path: $fullEnvPath\n", FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - .env file exists: " . (file_exists($envFile) ? 'YES' : 'NO') . "\n", FILE_APPEND);
if (file_exists($envFile)) {
$envContent = file_get_contents($envFile);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - .env file content:\n$envContent\n", FILE_APPEND);
$env = parse_ini_file($envFile);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Parsed .env: " . print_r($env, true) . "\n", FILE_APPEND);
$awsAccessKey = $env['AWS_ACCESS_KEY_ID'] ?? '';
$awsSecretKey = $env['AWS_SECRET_ACCESS_KEY'] ?? '';
$bucketName = $env['S3_BUCKET'] ?? 'rimtours-sendyuploads';
$region = $env['S3_REGION'] ?? 'us-west-2';
file_put_contents($logFile, date('Y-m-d H:i:s') . " - AWS Access Key: " . (strlen($awsAccessKey) > 0 ? substr($awsAccessKey, 0, 8) . '...' : 'EMPTY') . "\n", FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - AWS Secret Key: " . (strlen($awsSecretKey) > 0 ? 'FOUND (' . strlen($awsSecretKey) . ' chars)' : 'EMPTY') . "\n", FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Bucket: $bucketName\n", FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Region: $region\n", FILE_APPEND);
} else {
file_put_contents($logFile, date('Y-m-d H:i:s') . " - ERROR: .env file not found at $envFile\n", FILE_APPEND);
die('.env file not found. Please create /var/www/html/.env with AWS credentials.');
}
if (!$awsAccessKey || !$awsSecretKey) {
file_put_contents($logFile, date('Y-m-d H:i:s') . " - ERROR: AWS credentials empty or missing\n", FILE_APPEND);
die('AWS credentials not configured in .env file.');
}
// S3 Configuration
$endpoint = 's3.' . $region . '.amazonaws.com';
try {
// Initialize S3 connection
$s3 = new S3($awsAccessKey, $awsSecretKey, false, $endpoint);
// Create S3 filename with folder structure
$s3Filename = 'sendy/images/' . $time . '_' . basename($file_name);
// Upload to S3
$inputFile = $s3->inputFile($file);
if (!$inputFile) {
throw new Exception('Unable to create input file for S3 upload');
}
$uploadResult = $s3->putObject($inputFile, $bucketName, $s3Filename, S3::ACL_PUBLIC_READ);
if ($uploadResult) {
// Get app domain settings for proper URL construction
$q = 'SELECT custom_domain, custom_domain_protocol, custom_domain_enabled FROM apps WHERE id = '.$app;
$r = mysqli_query($mysqli, $q);
$custom_domain = '';
$custom_domain_protocol = 'https';
$custom_domain_enabled = false;
if ($r && mysqli_num_rows($r) > 0) {
while($row = mysqli_fetch_array($r)) {
$custom_domain = $row['custom_domain'];
$custom_domain_protocol = $row['custom_domain_protocol'];
$custom_domain_enabled = $row['custom_domain_enabled'];
}
}
// Construct the S3 URL
$fileUrl = 'https://' . $bucketName . '.' . $endpoint . '/' . $s3Filename;
// CKEditor callback parameters
$funcNum = (int)$_GET['CKEditorFuncNum'];
$CKEditor = $_GET['CKEditor'] ?? '';
$langCode = $_GET['langCode'] ?? '';
$message = '';
// Return success to CKEditor
echo "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction($funcNum, '$fileUrl', '$message');</script>";
} else {
throw new Exception('S3 upload failed - putObject returned false');
}
} catch (Exception $e) {
$errorMessage = htmlspecialchars($e->getMessage());
error_log('S3 Upload Error: ' . $errorMessage);
// Return error to CKEditor
$funcNum = (int)$_GET['CKEditorFuncNum'];
$message = 'Upload failed: ' . $errorMessage;
echo "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction($funcNum, '', '$message');</script>";
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment