File Uploads with PHP
Learning Objectives
- Master PHP programming concepts
- Write clean, maintainable code
- Apply best practices
- Build dynamic applications
Understanding File Uploads
Handling file uploads is an essential skill for web developers. From user profile pictures to document repositories, many web applications require the ability to accept and process files from users. However, file uploads also present unique security challenges that require careful handling.
The Package Delivery Analogy
Think of file uploads like a package delivery service:
- The form is like an order form where the user specifies what they want to send.
- The multipart encoding is like the special packaging required for different types of items.
- File size limits are like weight restrictions on packages.
- File type validation is like checking if the contents match what was declared on the customs form.
- Storage directories are like sorting facilities where packages get routed to their final destinations.
- Security measures are like inspections to ensure nothing dangerous is being sent.
Just as a delivery service has procedures to safely handle packages of different types and sizes, your application needs proper protocols to securely manage uploaded files.
Creating File Upload Forms
The first step in handling file uploads is creating HTML forms properly configured to accept files.
Form Requirements for File Uploads
<!-- Basic File Upload Form -->
<form action="upload.php" method="post" enctype="multipart/form-data">
<!-- The enctype attribute is essential for file uploads -->
<label for="file">Select file:</label>
<input type="file" name="file" id="file">
<input type="submit" name="submit" value="Upload File">
</form>
There are three crucial requirements for file upload forms:
- method="post": File uploads must use the POST method (GET cannot handle file data)
- enctype="multipart/form-data": This special encoding type is required to handle file data
- <input type="file">: The file input field allows users to select files from their device
File Input Attributes
<!-- Advanced File Input Options -->
<input
type="file"
name="documents[]"
multiple
accept=".pdf,.doc,.docx"
required
>
The file input element supports several useful attributes:
| Attribute | Description | Example |
|---|---|---|
multiple |
Allows users to select multiple files | <input type="file" multiple> |
accept |
Specifies file types allowed | <input type="file" accept="image/*"> |
required |
Makes the file selection mandatory | <input type="file" required> |
name="files[]" |
Array notation for multiple files | <input type="file" name="images[]" multiple> |
The accept Attribute:
The accept attribute can specify:
- File extensions:
.jpg, .png, .pdf - MIME types:
image/jpeg, image/png, application/pdf - MIME type wildcards:
image/*, audio/*, video/*
Important: Client-side restrictions like the accept attribute provide a better user experience but are not security measures. Users can bypass these limitations, so always implement server-side validation.
Complete Form Examples
Single File Upload:
<!-- Single File Upload Form -->
<form action="upload.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="profile_picture">Profile Picture:</label>
<input type="file" name="profile_picture" id="profile_picture" accept="image/*" required>
<p class="help-text">Maximum file size: 2MB. Supported formats: JPG, PNG, GIF.</p>
</div>
<div class="form-group">
<label for="display_name">Display Name:</label>
<input type="text" name="display_name" id="display_name" required>
</div>
<button type="submit" name="submit">Upload Profile</button>
</form>
Multiple File Upload:
<!-- Multiple File Upload Form -->
<form action="gallery_upload.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="gallery_images">Gallery Images:</label>
<input type="file" name="gallery_images[]" id="gallery_images" multiple accept="image/*" required>
<p class="help-text">Select multiple images (hold Ctrl/Cmd while selecting).</p>
<p class="help-text">Maximum 5 files, 2MB each. Supported formats: JPG, PNG, GIF.</p>
</div>
<div class="form-group">
<label for="gallery_title">Gallery Title:</label>
<input type="text" name="gallery_title" id="gallery_title" required>
</div>
<div class="form-group">
<label for="gallery_description">Description:</label>
<textarea name="gallery_description" id="gallery_description" rows="4"></textarea>
</div>
<button type="submit" name="submit">Create Gallery</button>
</form>
Different File Types Upload:
<!-- Different File Types Upload Form -->
<form action="document_upload.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="document">Document:</label>
<input type="file" name="document" id="document" accept=".pdf,.doc,.docx,.txt" required>
<p class="help-text">Maximum file size: 5MB. Supported formats: PDF, DOC, DOCX, TXT.</p>
</div>
<div class="form-group">
<label for="supporting_image">Supporting Image (optional):</label>
<input type="file" name="supporting_image" id="supporting_image" accept="image/*">
<p class="help-text">Maximum file size: 2MB. Supported formats: JPG, PNG.</p>
</div>
<div class="form-group">
<label for="document_title">Document Title:</label>
<input type="text" name="document_title" id="document_title" required>
</div>
<button type="submit" name="submit">Upload Document</button>
</form>
Processing File Uploads in PHP
Once a file is submitted through a form, PHP makes the file data available through the $_FILES superglobal array. Understanding this array is crucial for file upload handling.
The $_FILES Superglobal
When a file is uploaded, PHP creates a temporary file and populates the $_FILES array with information about the upload.
<?php
// Structure of $_FILES for a single file upload with name="file"
print_r($_FILES);
/*
$_FILES['file'] = array(
'name' => 'example.jpg', // Original filename
'type' => 'image/jpeg', // MIME type (as reported by browser)
'tmp_name' => '/tmp/php7FE.tmp', // Temporary file location on server
'error' => 0, // Error code (0 means no error)
'size' => 123456 // File size in bytes
);
*/
For multiple file uploads (using name="files[]"):
<?php
// Structure of $_FILES for multiple file uploads with name="files[]"
print_r($_FILES);
/*
$_FILES['files'] = array(
'name' => array(0 => 'file1.jpg', 1 => 'file2.png', 2 => 'file3.pdf'),
'type' => array(0 => 'image/jpeg', 1 => 'image/png', 2 => 'application/pdf'),
'tmp_name' => array(0 => '/tmp/phpA1B.tmp', 1 => '/tmp/phpC3D.tmp', 2 => '/tmp/phpE5F.tmp'),
'error' => array(0 => 0, 1 => 0, 2 => 0),
'size' => array(0 => 123456, 1 => 234567, 2 => 345678)
);
*/
Error codes in $_FILES['file']['error']:
| Value | Constant | Description |
|---|---|---|
| 0 | UPLOAD_ERR_OK | No error, file uploaded successfully |
| 1 | UPLOAD_ERR_INI_SIZE | File exceeds upload_max_filesize in php.ini |
| 2 | UPLOAD_ERR_FORM_SIZE | File exceeds MAX_FILE_SIZE specified in HTML form |
| 3 | UPLOAD_ERR_PARTIAL | File was only partially uploaded |
| 4 | UPLOAD_ERR_NO_FILE | No file was uploaded |
| 6 | UPLOAD_ERR_NO_TMP_DIR | Missing temporary folder |
| 7 | UPLOAD_ERR_CANT_WRITE | Failed to write file to disk |
| 8 | UPLOAD_ERR_EXTENSION | A PHP extension stopped the upload |
Basic File Upload Processing
<?php
// Basic file upload processing
// Check if form was submitted
if (isset($_POST['submit'])) {
// Check if file was uploaded without errors
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
// Get file details
$tmp_name = $_FILES['file']['tmp_name'];
$name = $_FILES['file']['name'];
$size = $_FILES['file']['size'];
$type = $_FILES['file']['type'];
// Define upload directory (make sure it exists and is writable)
$upload_dir = 'uploads/';
// Move the temporary file to the uploads directory
if (move_uploaded_file($tmp_name, $upload_dir . $name)) {
echo "File uploaded successfully!";
// You can now process the file further, insert records into a database, etc.
} else {
echo "Error moving uploaded file.";
}
} else {
// Handle different error codes
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_INI_SIZE:
echo "The file exceeds the upload_max_filesize directive in php.ini.";
break;
case UPLOAD_ERR_FORM_SIZE:
echo "The file exceeds the MAX_FILE_SIZE directive specified in the HTML form.";
break;
case UPLOAD_ERR_PARTIAL:
echo "The file was only partially uploaded.";
break;
case UPLOAD_ERR_NO_FILE:
echo "No file was uploaded.";
break;
case UPLOAD_ERR_NO_TMP_DIR:
echo "Missing a temporary folder.";
break;
case UPLOAD_ERR_CANT_WRITE:
echo "Failed to write file to disk.";
break;
case UPLOAD_ERR_EXTENSION:
echo "A PHP extension stopped the file upload.";
break;
default:
echo "Unknown upload error.";
}
}
}
?>
File Validation and Security
Proper validation is crucial for file uploads. Without it, your server could be vulnerable to various attacks including malicious file execution, directory traversal, or denial of service.
Essential Validation Checks
- File existence and upload errors: Check if the file was uploaded without errors
- File size: Ensure the file isn't too large for your application
- File type: Verify the file is of an allowed type (using multiple methods)
- File name: Sanitize filenames to prevent directory traversal and conflicts
- File content: For extra security, validate that the content matches the expected type
File Size Validation
<?php
// File size validation
// Define maximum file size (in bytes)
// 1 MB = 1,048,576 bytes
$max_file_size = 1048576; // 1 MB
// Check file size
if ($_FILES['file']['size'] > $max_file_size) {
die("Error: File size exceeds the maximum limit of " . ($max_file_size / 1048576) . " MB.");
}
// You can also set a maximum file size in the HTML form
// This is a client-side check that can be bypassed, so always validate on the server too
?>
<!-- Setting MAX_FILE_SIZE in a form (optional, client-side only) -->
<form action="upload.php" method="post" enctype="multipart/form-data">
<!-- This must be BEFORE the file input field -->
<input type="hidden" name="MAX_FILE_SIZE" value="1048576" /> <!-- 1 MB -->
<input type="file" name="file" id="file">
<input type="submit" name="submit" value="Upload">
</form>
Modifying PHP Configuration for Larger Files
To allow larger file uploads, you may need to modify PHP configuration in php.ini:
; PHP configuration settings for file uploads
upload_max_filesize = 10M
post_max_size = 10M ; Should be larger than upload_max_filesize
memory_limit = 128M ; Should be larger than post_max_size
max_execution_time = 300 ; Allow more time for larger uploads
max_input_time = 300
Alternatively, you can set these values in your .htaccess file (if using Apache):
# .htaccess settings for file uploads
php_value upload_max_filesize 10M
php_value post_max_size 10M
php_value memory_limit 128M
php_value max_execution_time 300
php_value max_input_time 300
File Type Validation
Never trust the MIME type reported by the browser. Use multiple methods to validate file types:
<?php
// File type validation (multiple methods)
// 1. Check file extension
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
$file_name = $_FILES['file']['name'];
$file_extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_extension, $allowed_extensions)) {
die("Error: File type not allowed. Allowed types: " . implode(', ', $allowed_extensions));
}
// 2. Check MIME type using finfo (more reliable than $_FILES['file']['type'])
$finfo = new finfo(FILEINFO_MIME_TYPE);
$file_mime = $finfo->file($_FILES['file']['tmp_name']);
$allowed_mimes = [
'image/jpeg',
'image/png',
'image/gif'
];
if (!in_array($file_mime, $allowed_mimes)) {
die("Error: Invalid file format. Detected file type: " . $file_mime);
}
// 3. For images, you can also use getimagesize() for extra validation
if (strpos($file_mime, 'image/') === 0) {
$image_info = @getimagesize($_FILES['file']['tmp_name']);
if ($image_info === false) {
die("Error: Invalid image file.");
}
// Additional checks for image dimensions if needed
list($width, $height) = $image_info;
if ($width > 2000 || $height > 2000) {
die("Error: Image dimensions exceed the maximum allowed (2000x2000 pixels).");
}
}
?>
Filename Sanitization
Always sanitize filenames to prevent security issues and ensure compatibility:
<?php
// Filename sanitization
/**
* Sanitize and generate a safe filename
*
* @param string $filename Original filename
* @return string Sanitized filename
*/
function sanitize_filename($filename) {
// Get file extension
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// Get filename without extension
$name = pathinfo($filename, PATHINFO_FILENAME);
// Remove special characters
$name = preg_replace('/[^a-zA-Z0-9_-]/', '', $name);
// Ensure the name isn't empty after sanitization
if (empty($name)) {
$name = 'file_' . time();
}
// Add a unique identifier to prevent overwriting existing files
$name = $name . '_' . time() . '_' . mt_rand(1000, 9999);
// Reassemble the filename
return $name . '.' . $extension;
}
// Usage example
$original_filename = $_FILES['file']['name'];
$safe_filename = sanitize_filename($original_filename);
// Now use the safe filename when moving the file
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $safe_filename);
// Store the original filename and the new filename in your database for reference
?>
Comprehensive File Upload Validation
<?php
/**
* Validate and process file upload with comprehensive security measures
*
* @param array $file The $_FILES array element for the upload
* @param array $allowed_types Allowed MIME types
* @param int $max_size Maximum file size in bytes
* @param string $upload_dir Directory to store uploaded files
* @return array Result array with status, message, and file path if successful
*/
function validate_and_process_upload($file, $allowed_types, $max_size, $upload_dir) {
// Initialize result array
$result = [
'success' => false,
'message' => '',
'file_path' => ''
];
// Check for upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
$result['message'] = get_upload_error_message($file['error']);
return $result;
}
// Check file size
if ($file['size'] > $max_size) {
$result['message'] = "Error: File size exceeds the limit of " . round($max_size / 1048576, 2) . " MB.";
return $result;
}
// Validate file type using finfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$file_mime = $finfo->file($file['tmp_name']);
if (!in_array($file_mime, $allowed_types)) {
$result['message'] = "Error: Invalid file type. Only the following types are allowed: " . implode(', ', $allowed_types);
return $result;
}
// Get file extension from MIME type (more reliable than from filename)
$mime_extensions = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'application/pdf' => 'pdf',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'text/plain' => 'txt'
];
if (isset($mime_extensions[$file_mime])) {
$extension = $mime_extensions[$file_mime];
} else {
// Fallback to original extension if MIME type not in our list
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
}
// Generate a safe, unique filename
$safe_filename = generate_safe_filename($extension);
// Ensure upload directory exists and is writable
if (!is_dir($upload_dir)) {
if (!mkdir($upload_dir, 0755, true)) {
$result['message'] = "Error: Failed to create upload directory.";
return $result;
}
}
if (!is_writable($upload_dir)) {
$result['message'] = "Error: Upload directory is not writable.";
return $result;
}
// Create complete path for the file
$upload_path = rtrim($upload_dir, '/') . '/' . $safe_filename;
// Move the uploaded file
if (move_uploaded_file($file['tmp_name'], $upload_path)) {
// Set appropriate file permissions
chmod($upload_path, 0644);
$result['success'] = true;
$result['message'] = "File uploaded successfully.";
$result['file_path'] = $upload_path;
} else {
$result['message'] = "Error: Failed to move uploaded file.";
}
return $result;
}
/**
* Get readable error message for upload error code
*
* @param int $error_code Error code from $_FILES['file']['error']
* @return string Human-readable error message
*/
function get_upload_error_message($error_code) {
switch ($error_code) {
case UPLOAD_ERR_INI_SIZE:
return "The file exceeds the upload_max_filesize directive in php.ini.";
case UPLOAD_ERR_FORM_SIZE:
return "The file exceeds the MAX_FILE_SIZE directive specified in the HTML form.";
case UPLOAD_ERR_PARTIAL:
return "The file was only partially uploaded.";
case UPLOAD_ERR_NO_FILE:
return "No file was uploaded.";
case UPLOAD_ERR_NO_TMP_DIR:
return "Missing a temporary folder.";
case UPLOAD_ERR_CANT_WRITE:
return "Failed to write file to disk.";
case UPLOAD_ERR_EXTENSION:
return "A PHP extension stopped the file upload.";
default:
return "Unknown upload error.";
}
}
/**
* Generate a safe and unique filename
*
* @param string $extension File extension
* @return string Safe filename with extension
*/
function generate_safe_filename($extension) {
// Using UUID for uniqueness
$uuid = sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
return $uuid . '.' . $extension;
}
// Usage example
if (isset($_POST['submit']) && isset($_FILES['document'])) {
$allowed_types = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain'
];
$max_size = 5 * 1048576; // 5 MB
$upload_dir = 'uploads/documents/';
$result = validate_and_process_upload($_FILES['document'], $allowed_types, $max_size, $upload_dir);
if ($result['success']) {
echo $result['message'];
// Database recording example
$document_data = [
'title' => $_POST['document_title'] ?? 'Untitled Document',
'file_path' => $result['file_path'],
'original_name' => $_FILES['document']['name'],
'file_size' => $_FILES['document']['size'],
'file_type' => $_FILES['document']['type'],
'uploaded_at' => date('Y-m-d H:i:s')
];
// Example: $db->insert('documents', $document_data);
} else {
echo "Error: " . $result['message'];
}
}
?>
Handling Multiple File Uploads
Processing multiple file uploads requires looping through the files and validating each one separately.
Processing Multiple File Uploads
<?php
// Processing multiple file uploads
/**
* Process multiple file uploads
*
* @param array $files The $_FILES array for multiple uploads
* @param array $allowed_types Allowed MIME types
* @param int $max_file_size Maximum file size in bytes
* @param string $upload_dir Directory to store uploaded files
* @param int $max_files Maximum number of files allowed
* @return array Results array with status and messages
*/
function process_multiple_uploads($files, $allowed_types, $max_file_size, $upload_dir, $max_files = 10) {
$results = [
'success' => true,
'messages' => [],
'file_paths' => []
];
// Check if files were uploaded
if (!isset($files['name'][0]) || empty($files['name'][0])) {
$results['success'] = false;
$results['messages'][] = "No files were uploaded.";
return $results;
}
// Count files
$file_count = count($files['name']);
// Check number of files
if ($file_count > $max_files) {
$results['success'] = false;
$results['messages'][] = "Too many files. Maximum allowed: $max_files files.";
return $results;
}
// Process each file
for ($i = 0; $i < $file_count; $i++) {
// Only process if no upload error
if ($files['error'][$i] === UPLOAD_ERR_OK) {
// Create a temporary file array similar to single file upload
$file = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
// Use our previously defined function to validate and process the file
$upload_result = validate_and_process_upload($file, $allowed_types, $max_file_size, $upload_dir);
if ($upload_result['success']) {
$results['messages'][] = "File '{$files['name'][$i]}' uploaded successfully.";
$results['file_paths'][] = $upload_result['file_path'];
} else {
$results['success'] = false;
$results['messages'][] = "Error with file '{$files['name'][$i]}': {$upload_result['message']}";
}
} else {
$results['success'] = false;
$results['messages'][] = "Error with file '{$files['name'][$i]}': " . get_upload_error_message($files['error'][$i]);
}
}
return $results;
}
// Usage example
if (isset($_POST['submit']) && isset($_FILES['gallery_images'])) {
$allowed_types = [
'image/jpeg',
'image/png',
'image/gif'
];
$max_file_size = 2 * 1048576; // 2 MB
$upload_dir = 'uploads/gallery/';
$max_files = 5;
$results = process_multiple_uploads($_FILES['gallery_images'], $allowed_types, $max_file_size, $upload_dir, $max_files);
if ($results['success']) {
echo "All files uploaded successfully!";
// Database recording example for gallery
$gallery_data = [
'title' => $_POST['gallery_title'] ?? 'New Gallery',
'description' => $_POST['gallery_description'] ?? '',
'created_at' => date('Y-m-d H:i:s'),
'image_count' => count($results['file_paths'])
];
// Example: $db->insert('galleries', $gallery_data);
// $gallery_id = $db->lastInsertId();
// Now insert each image
foreach ($results['file_paths'] as $index => $file_path) {
$image_data = [
'gallery_id' => $gallery_id,
'file_path' => $file_path,
'position' => $index + 1
];
// Example: $db->insert('gallery_images', $image_data);
}
} else {
echo "Errors occurred during upload:
";
echo implode("
", $results['messages']);
}
}
?>
Image Upload and Processing
Images are the most commonly uploaded file type. PHP provides several functions for advanced image processing.
PHP GD Library for Image Processing
The GD library allows you to create, manipulate, and save images in various formats.
Common Image Operations:
- Creating thumbnails
- Resizing images
- Cropping images
- Adding watermarks
- Converting between formats
- Applying filters
<?php
// Image processing with GD Library
/**
* Create a thumbnail from an uploaded image
*
* @param string $source_path Path to the source image
* @param string $thumb_path Path for the thumbnail
* @param int $width Thumbnail width
* @param int $height Thumbnail height
* @param bool $crop Whether to crop the image to exact dimensions
* @return bool True on success, false on failure
*/
function create_thumbnail($source_path, $thumb_path, $width = 200, $height = 200, $crop = true) {
// Check if source file exists
if (!file_exists($source_path)) {
return false;
}
// Get image information
$image_info = getimagesize($source_path);
if ($image_info === false) {
return false;
}
// Create image resource based on type
switch ($image_info[2]) {
case IMAGETYPE_JPEG:
$source_image = imagecreatefromjpeg($source_path);
break;
case IMAGETYPE_PNG:
$source_image = imagecreatefrompng($source_path);
break;
case IMAGETYPE_GIF:
$source_image = imagecreatefromgif($source_path);
break;
default:
return false;
}
// Check if image creation was successful
if (!$source_image) {
return false;
}
// Get original dimensions
$original_width = imagesx($source_image);
$original_height = imagesy($source_image);
// Calculate thumbnail dimensions while maintaining aspect ratio
if ($crop) {
// When cropping, determine the portion of the image to use
$source_ratio = $original_width / $original_height;
$thumb_ratio = $width / $height;
if ($source_ratio > $thumb_ratio) {
// Image is wider than thumbnail, crop sides
$new_width = $original_height * $thumb_ratio;
$new_height = $original_height;
$src_x = ($original_width - $new_width) / 2;
$src_y = 0;
} else {
// Image is taller than thumbnail, crop top/bottom
$new_width = $original_width;
$new_height = $original_width / $thumb_ratio;
$src_x = 0;
$src_y = ($original_height - $new_height) / 2;
}
// Create the thumbnail canvas
$thumb_image = imagecreatetruecolor($width, $height);
// For PNGs and GIFs, preserve transparency
if ($image_info[2] == IMAGETYPE_PNG || $image_info[2] == IMAGETYPE_GIF) {
imagecolortransparent($thumb_image, imagecolorallocate($thumb_image, 0, 0, 0));
imagealphablending($thumb_image, false);
imagesavealpha($thumb_image, true);
}
// Copy and resize part of the source image with resampling
if (!imagecopyresampled(
$thumb_image, $source_image,
0, 0, (int)$src_x, (int)$src_y,
$width, $height, (int)$new_width, (int)$new_height
)) {
return false;
}
} else {
// Without cropping, resize while maintaining aspect ratio
$ratio = min($width / $original_width, $height / $original_height);
$new_width = (int)($original_width * $ratio);
$new_height = (int)($original_height * $ratio);
// Create the thumbnail canvas
$thumb_image = imagecreatetruecolor($new_width, $new_height);
// For PNGs and GIFs, preserve transparency
if ($image_info[2] == IMAGETYPE_PNG || $image_info[2] == IMAGETYPE_GIF) {
imagecolortransparent($thumb_image, imagecolorallocate($thumb_image, 0, 0, 0));
imagealphablending($thumb_image, false);
imagesavealpha($thumb_image, true);
}
// Copy and resize the entire source image with resampling
if (!imagecopyresampled(
$thumb_image, $source_image,
0, 0, 0, 0,
$new_width, $new_height, $original_width, $original_height
)) {
return false;
}
}
// Save the thumbnail based on original format
$success = false;
switch ($image_info[2]) {
case IMAGETYPE_JPEG:
$success = imagejpeg($thumb_image, $thumb_path, 90); // 90 is quality (0-100)
break;
case IMAGETYPE_PNG:
$success = imagepng($thumb_image, $thumb_path, 9); // 9 is compression level (0-9)
break;
case IMAGETYPE_GIF:
$success = imagegif($thumb_image, $thumb_path);
break;
}
// Free up memory
imagedestroy($source_image);
imagedestroy($thumb_image);
return $success;
}
/**
* Add a watermark to an image
*
* @param string $image_path Path to the source image
* @param string $watermark_path Path to the watermark image
* @param string $output_path Path to save the watermarked image
* @param string $position Position of watermark: 'center', 'topleft', 'topright', 'bottomleft', 'bottomright'
* @param int $opacity Watermark opacity (0-100)
* @return bool True on success, false on failure
*/
function add_image_watermark($image_path, $watermark_path, $output_path, $position = 'bottomright', $opacity = 50) {
// Check if files exist
if (!file_exists($image_path) || !file_exists($watermark_path)) {
return false;
}
// Get main image info
$image_info = getimagesize($image_path);
if ($image_info === false) {
return false;
}
// Create image resource based on type
switch ($image_info[2]) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($image_path);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($image_path);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($image_path);
break;
default:
return false;
}
// Get watermark image info
$watermark_info = getimagesize($watermark_path);
if ($watermark_info === false) {
imagedestroy($image);
return false;
}
// Create watermark resource
switch ($watermark_info[2]) {
case IMAGETYPE_PNG: // PNG is recommended for watermarks due to transparency support
$watermark = imagecreatefrompng($watermark_path);
break;
case IMAGETYPE_GIF:
$watermark = imagecreatefromgif($watermark_path);
break;
case IMAGETYPE_JPEG:
$watermark = imagecreatefromjpeg($watermark_path);
break;
default:
imagedestroy($image);
return false;
}
// Get dimensions
$image_width = imagesx($image);
$image_height = imagesy($image);
$watermark_width = imagesx($watermark);
$watermark_height = imagesy($watermark);
// Calculate watermark position
switch ($position) {
case 'topleft':
$dest_x = 10;
$dest_y = 10;
break;
case 'topright':
$dest_x = $image_width - $watermark_width - 10;
$dest_y = 10;
break;
case 'bottomleft':
$dest_x = 10;
$dest_y = $image_height - $watermark_height - 10;
break;
case 'center':
$dest_x = ($image_width - $watermark_width) / 2;
$dest_y = ($image_height - $watermark_height) / 2;
break;
case 'bottomright':
default:
$dest_x = $image_width - $watermark_width - 10;
$dest_y = $image_height - $watermark_height - 10;
break;
}
// Set watermark opacity
if ($opacity < 100) {
// Create a new image with alpha channel
$alpha_watermark = imagecreatetruecolor($watermark_width, $watermark_height);
// Fill with transparent color
$transparent = imagecolorallocatealpha($alpha_watermark, 255, 255, 255, 127);
imagefilledrectangle($alpha_watermark, 0, 0, $watermark_width, $watermark_height, $transparent);
// Copy original watermark
imagecopy($alpha_watermark, $watermark, 0, 0, 0, 0, $watermark_width, $watermark_height);
// Calculate opacity (127 is fully transparent, 0 is fully opaque)
$opacity_level = 127 - (int)(($opacity / 100) * 127);
// Apply opacity to each pixel
for ($x = 0; $x < $watermark_width; $x++) {
for ($y = 0; $y < $watermark_height; $y++) {
$color_index = imagecolorat($alpha_watermark, $x, $y);
$colors = imagecolorsforindex($alpha_watermark, $color_index);
// Only modify alpha channel if pixel isn't already fully transparent
if ($colors['alpha'] < 127) {
$color = imagecolorallocatealpha(
$alpha_watermark,
$colors['red'],
$colors['green'],
$colors['blue'],
$opacity_level
);
imagesetpixel($alpha_watermark, $x, $y, $color);
}
}
}
// Free the original watermark
imagedestroy($watermark);
$watermark = $alpha_watermark;
}
// Enable alpha blending
imagealphablending($image, true);
// Copy watermark onto image
imagecopy($image, $watermark, $dest_x, $dest_y, 0, 0, $watermark_width, $watermark_height);
// Save the watermarked image
$success = false;
switch ($image_info[2]) {
case IMAGETYPE_JPEG:
$success = imagejpeg($image, $output_path, 90);
break;
case IMAGETYPE_PNG:
$success = imagepng($image, $output_path, 9);
break;
case IMAGETYPE_GIF:
$success = imagegif($image, $output_path);
break;
}
// Free up memory
imagedestroy($image);
imagedestroy($watermark);
return $success;
}
// Example usage for thumbnail creation
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
// Validate file type, size, etc. (as shown in previous examples)
// Define paths
$upload_dir = 'uploads/images/';
$thumbs_dir = 'uploads/thumbs/';
// Generate filename
$filename = time() . '_' . mt_rand(1000, 9999) . '.jpg';
$upload_path = $upload_dir . $filename;
$thumb_path = $thumbs_dir . $filename;
// Move the uploaded file
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_path)) {
// Create thumbnail
if (create_thumbnail($upload_path, $thumb_path, 200, 200, true)) {
echo "Image uploaded and thumbnail created successfully!";
} else {
echo "Image uploaded but failed to create thumbnail.";
}
// Add watermark (if needed)
$watermark_path = 'assets/watermark.png';
$watermarked_path = $upload_dir . 'watermarked_' . $filename;
if (add_image_watermark($upload_path, $watermark_path, $watermarked_path)) {
echo "Watermark added successfully!";
}
} else {
echo "Failed to move uploaded file.";
}
}
?>
Progress Indicators for Large File Uploads
For large file uploads, it's important to provide users with progress indicators to improve the user experience.
Implementing Upload Progress with JavaScript
<!-- HTML Form with Progress Bar -->
<form id="upload-form" action="process_upload.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="file">Select File:</label>
<input type="file" name="file" id="file" required>
</div>
<div class="progress" style="display: none;">
<div class="progress-bar" style="width: 0%;">0%</div>
</div>
<div class="form-group">
<button type="submit" name="submit" id="submit-btn">Upload</button>
</div>
<div id="status"></div>
</form>
<!-- JavaScript for Upload Progress -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('upload-form');
const fileInput = document.getElementById('file');
const submitBtn = document.getElementById('submit-btn');
const progressContainer = document.querySelector('.progress');
const progressBar = document.querySelector('.progress-bar');
const statusDiv = document.getElementById('status');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Check if a file was selected
if (!fileInput.files[0]) {
statusDiv.innerHTML = 'Please select a file to upload.';
return;
}
// Create FormData object
const formData = new FormData(form);
// Create and configure XMLHttpRequest
const xhr = new XMLHttpRequest();
// Configure for progress tracking
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
// Show progress container
progressContainer.style.display = 'block';
// Update progress bar
progressBar.style.width = percentComplete + '%';
progressBar.textContent = percentComplete + '%';
// Update status
statusDiv.innerHTML = 'Uploading... ' + percentComplete + '%';
}
});
// Handle completion
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
statusDiv.innerHTML = 'Upload complete! ' + response.message;
// Show preview if it's an image
if (response.file_url && response.file_type && response.file_type.startsWith('image/')) {
const previewImg = document.createElement('img');
previewImg.src = response.file_url;
previewImg.alt = 'Uploaded Image';
previewImg.style.maxWidth = '100%';
previewImg.style.maxHeight = '300px';
statusDiv.appendChild(document.createElement('br'));
statusDiv.appendChild(previewImg);
}
} else {
statusDiv.innerHTML = 'Error: ' + response.message;
}
} catch (e) {
statusDiv.innerHTML = 'Error parsing server response.';
}
} else {
statusDiv.innerHTML = 'Error: Server returned status ' + xhr.status;
}
});
// Handle errors
xhr.addEventListener('error', function() {
statusDiv.innerHTML = 'Upload failed. Please try again.';
});
// Handle abortion
xhr.addEventListener('abort', function() {
statusDiv.innerHTML = 'Upload aborted.';
});
// Open and send the request
xhr.open('POST', form.action);
xhr.send(formData);
// Disable submit button during upload
submitBtn.disabled = true;
});
});
</script>
PHP Server-Side for Progress Tracking
<?php
// process_upload.php - Server-side script to handle AJAX upload
// Set content type to JSON
header('Content-Type: application/json');
// Handle file upload
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
// Validate file (size, type, etc.) as shown in previous examples
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$max_size = 10 * 1048576; // 10 MB
// Check file size
if ($_FILES['file']['size'] > $max_size) {
echo json_encode([
'success' => false,
'message' => 'File exceeds the maximum size limit of 10 MB.'
]);
exit;
}
// Check file type
$finfo = new finfo(FILEINFO_MIME_TYPE);
$file_type = $finfo->file($_FILES['file']['tmp_name']);
if (!in_array($file_type, $allowed_types)) {
echo json_encode([
'success' => false,
'message' => 'Invalid file type. Allowed types: JPG, PNG, GIF, PDF.'
]);
exit;
}
// Generate a safe filename
$original_name = $_FILES['file']['name'];
$extension = pathinfo($original_name, PATHINFO_EXTENSION);
$new_filename = uniqid() . '_' . time() . '.' . $extension;
// Define upload directory
$upload_dir = 'uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$upload_path = $upload_dir . $new_filename;
// Move the uploaded file
if (move_uploaded_file($_FILES['file']['tmp_name'], $upload_path)) {
// Success response
echo json_encode([
'success' => true,
'message' => 'File uploaded successfully.',
'file_url' => $upload_path,
'file_name' => $original_name,
'file_type' => $file_type,
'file_size' => $_FILES['file']['size']
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to save the uploaded file.'
]);
}
} else {
// Handle file upload errors
$error_message = 'No file uploaded or an error occurred.';
if (isset($_FILES['file']['error']) && $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
// Get specific error message
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_INI_SIZE:
$error_message = 'The file exceeds the upload_max_filesize directive in php.ini.';
break;
case UPLOAD_ERR_FORM_SIZE:
$error_message = 'The file exceeds the MAX_FILE_SIZE directive in the HTML form.';
break;
case UPLOAD_ERR_PARTIAL:
$error_message = 'The file was only partially uploaded.';
break;
case UPLOAD_ERR_NO_FILE:
$error_message = 'No file was uploaded.';
break;
case UPLOAD_ERR_NO_TMP_DIR:
$error_message = 'Missing a temporary folder.';
break;
case UPLOAD_ERR_CANT_WRITE:
$error_message = 'Failed to write file to disk.';
break;
case UPLOAD_ERR_EXTENSION:
$error_message = 'A PHP extension stopped the file upload.';
break;
}
}
echo json_encode([
'success' => false,
'message' => $error_message
]);
}
?>
Handling File Downloads
Once files are uploaded, you often need to provide a way for users to download them. Proper file download handling is important for security and user experience.
<?php
// download.php - Secure file download script
/**
* Handles secure file downloads
*
* This script prevents direct access to files, enforces authentication if needed,
* and provides proper headers for file downloads.
*
* Usage: download.php?file=filename.pdf
*
* Security features:
* - Validates file existence
* - Restricts access to specified file types
* - Prevents directory traversal
* - Can check user permissions
* - Uses appropriate Content-Type headers
* - Supports download resuming (Range requests)
*/
// Start session for user authentication (if needed)
session_start();
// Check if user is authenticated (example)
// if (!isset($_SESSION['user_id'])) {
// header('HTTP/1.0 403 Forbidden');
// echo 'Access denied. Please login.';
// exit;
// }
// Check if file parameter exists
if (!isset($_GET['file']) || empty($_GET['file'])) {
header('HTTP/1.0 400 Bad Request');
echo 'Error: No file specified.';
exit;
}
// Base directory for downloads (outside web root for better security)
$download_dir = realpath(__DIR__ . '/../downloads/');
// Sanitize the requested filename
$requested_file = basename($_GET['file']);
// File mapping (for extra security - map request parameters to actual files)
// This helps hide real file paths and names from users
$file_map = [
'sample.pdf' => 'documents/sample_document.pdf',
'report.pdf' => 'reports/annual_report_2023.pdf',
'image1.jpg' => 'images/landscape_photo.jpg',
// Add more mappings as needed
];
// If using file mapping
if (isset($file_map[$requested_file])) {
$file_path = $download_dir . '/' . $file_map[$requested_file];
} else {
// Direct file access (less secure but more flexible)
// Ensure the file is within the downloads directory to prevent directory traversal
$file_path = realpath($download_dir . '/' . $requested_file);
// Check if the file path is within the allowed download directory
if ($file_path === false || strpos($file_path, $download_dir) !== 0) {
header('HTTP/1.0 404 Not Found');
echo 'Error: File not found.';
exit;
}
}
// Check if file exists
if (!file_exists($file_path) || !is_file($file_path)) {
header('HTTP/1.0 404 Not Found');
echo 'Error: File not found.';
exit;
}
// Check if file is readable
if (!is_readable($file_path)) {
header('HTTP/1.0 403 Forbidden');
echo 'Error: File is not readable.';
exit;
}
// Optional: Check file extension/type for extra security
$allowed_extensions = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'txt'];
$file_extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
if (!in_array($file_extension, $allowed_extensions)) {
header('HTTP/1.0 403 Forbidden');
echo 'Error: File type not allowed.';
exit;
}
// Get file size
$file_size = filesize($file_path);
// Extract file name for the download
$file_name = pathinfo($file_path, PATHINFO_BASENAME);
// Map file extensions to MIME types
$mime_types = [
'pdf' => 'application/pdf',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'txt' => 'text/plain',
// Add more MIME types as needed
];
// Set the content type header
$content_type = $mime_types[$file_extension] ?? 'application/octet-stream';
header('Content-Type: ' . $content_type);
// Set headers for download
header('Content-Disposition: attachment; filename="' . $file_name . '"');
header('Content-Length: ' . $file_size);
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: public');
header('Expires: 0');
// For large files, enable output buffering control
if ($file_size > 1048576) { // 1MB
// Disable output buffering
if (ob_get_level()) {
ob_end_clean();
}
// Enable output compression
ini_set('zlib.output_compression', 'Off');
}
// Support for range requests (resumable downloads)
if (isset($_SERVER['HTTP_RANGE'])) {
// Parse range header
list($range_unit, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if ($range_unit === 'bytes') {
// Multiple ranges could be specified, but we'll only handle the first range
list($range) = explode(',', $range, 2);
// Range format: bytes=X-Y where X is start offset and Y is end offset
list($start, $end) = explode('-', $range, 2);
// Set start and end bytes
$start = empty($start) ? 0 : intval($start);
$end = empty($end) ? ($file_size - 1) : intval($end);
// Validate range
if ($start > $end || $start >= $file_size || $end >= $file_size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header('Content-Range: bytes */' . $file_size);
exit;
}
// Set partial content headers
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $file_size);
header('Content-Length: ' . ($end - $start + 1));
// Seek to start position
$fp = fopen($file_path, 'rb');
fseek($fp, $start);
// Output the data
$buffer_size = 8192; // 8KB chunks
$bytes_to_read = $end - $start + 1;
while ($bytes_to_read > 0 && !feof($fp)) {
$bytes_to_output = min($buffer_size, $bytes_to_read);
echo fread($fp, $bytes_to_output);
flush();
$bytes_to_read -= $bytes_to_output;
}
fclose($fp);
} else {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header('Content-Range: bytes */' . $file_size);
exit;
}
} else {
// Output the full file
readfile($file_path);
}
exit;
?>
Essential Security Measures
- Store uploaded files outside the web root when possible, or in a directory that blocks script execution
- Use random, unique filenames instead of the original names to prevent overwriting and predictable access
- Validate file types using multiple methods (extension, MIME type, file content)
- Limit file sizes to prevent denial of service attacks
- Implement proper permissions for upload directories (e.g., 755 for directories, 644 for files)
- Use a whitelist approach for allowed file types rather than a blacklist
- Scan uploaded files for malware when possible
- Implement user authentication and authorization for file uploads and downloads
- Use HTTPS to protect files during transfer
- Consider using content delivery networks (CDNs) for serving static files
Protecting Upload Directories with .htaccess
If using Apache, you can secure your upload directory with a .htaccess file:
# Disable script execution in upload directory
<FilesMatch "\.(?i:php|phtml|php3|php4|php5|php7|phps|phar|pht|phtm|htaccess|pl|py|jsp|asp|htm|html|shtml|sh|cgi|exe)$">
Order Deny,Allow
Deny from all
</FilesMatch>
# Only allow specific file types to be accessed
<FilesMatch "\.(?i:jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|txt)$">
Order Allow,Deny
Allow from all
</FilesMatch>
# Protect against directory listing
Options -Indexes
# Protect hidden files from being viewed
<Files .*>
Order Deny,Allow
Deny from all
</Files>
# Disable PHP script execution
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
# Alternate method for disabling PHP
RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .phps
RemoveType .php .phtml .php3 .php4 .php5 .php7 .phps
Protecting Upload Directories with web.config (IIS)
If using IIS, you can secure your upload directory with a web.config file:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<fileExtensions>
<!-- Block script file types -->
<add fileExtension=".php" allowed="false" />
<add fileExtension=".phtml" allowed="false" />
<add fileExtension=".php3" allowed="false" />
<add fileExtension=".php4" allowed="false" />
<add fileExtension=".php5" allowed="false" />
<add fileExtension=".php7" allowed="false" />
<add fileExtension=".phps" allowed="false" />
<add fileExtension=".cgi" allowed="false" />
<add fileExtension=".exe" allowed="false" />
<add fileExtension=".pl" allowed="false" />
<add fileExtension=".asp" allowed="false" />
<add fileExtension=".aspx" allowed="false" />
<add fileExtension=".jsp" allowed="false" />
<add fileExtension=".htaccess" allowed="false" />
</fileExtensions>
</requestFiltering>
</security>
<directoryBrowse enabled="false" />
</system.webServer>
</configuration>
Homework: File Upload Practice
Complete the following exercises to practice file upload techniques in PHP.
Task 1: Basic File Upload System
Create a simple file upload system with the following features:
- Allow users to upload images (JPG, PNG, GIF)
- Validate file types, sizes, and dimensions
- Generate unique filenames to prevent overwriting
- Create thumbnails for uploaded images
- Display a gallery of uploaded images
Task 2: Secure Document Management System
Build a document management system with these requirements:
- Support for multiple file types (PDF, DOC, DOCX, TXT)
- User authentication (simple login system)
- File categorization and metadata (title, description, tags)
- Secure download mechanism with permissions
- Search functionality for document titles and descriptions
Task 3: AJAX Multi-File Upload
Create an AJAX-based multiple file upload system:
- Drag-and-drop file upload interface
- Progress bar for each file upload
- File type and size validation before upload
- Ability to cancel uploads in progress
- Display upload results with thumbnails for images
Bonus Challenge: WordPress File Upload Plugin
Create a simple WordPress plugin that:
- Adds a custom post type for document management
- Creates a file upload meta box for the post type
- Implements file type validation and security measures
- Provides a shortcode to display file download links on the front-end
- Includes user permission management for file access
Additional Resources
PHP Documentation
WordPress Documentation
Security Resources
- OWASP: Unrestricted File Upload
- PHP Security: Filesystem Security
- Paragon Initiative: How to Securely Allow Users to Upload Files