9. Files
It is possible to import or call code from another file with PHP. This way we can split our dynamic pages into smaller, easier to manage fragments, avoiding the famous Spaghetti code (writing all our code in a single file with an endless number of lines). For this good practice we have up to 4 different tools on our utility belt.
| Function | Description | Error case | Example |
|---|---|---|---|
include '' |
Includes the file every time. | Gives a warning, but continues execution. | include 'your_file.php' |
include_once '' |
Includes the file only once. | Gives a warning, but continues execution. | include_once 'your_file.php' |
require '' |
Includes the file every time. | Stops execution (Fatal error). | require 'your_file.php' |
require_once '' |
Includes the file only once. | Stops execution (Fatal error). | require_once 'your_file.php' |
Let's see how it works with an example. I'm going to have a file called header.php with the following content.
<html>
<head>
</head>
<body>
Another one named footer.php.
<footer>It's me</footer>
</body>
</html>
Now I create a new file.
<?php include 'header.php'; ?>
<h1>Inicio</h1>
<?php include 'footer.php'; ?>
This would give me the following HTML.
<html>
<head>
</head>
<body>
<h1>Inicio</h1>
<footer>It's me</footer>
</body>
</html>
Powerful, right? We avoid repeating parts of our code that are very repetitive.
This time I'm going to repeat the footer include.
Now I create a new file.
<?php include 'header.php'; ?>
<h1>Inicio</h1>
<?php include 'footer.php'; ?>
<?php include 'footer.php'; ?>
Which would generate a duplicated footer.
<html>
<head>
</head>
<body>
<h1>Inicio</h1>
<footer>It's me</footer>
</body>
</html>
<footer>It's me</footer>
</body>
</html>
We can prevent it with include_once.
<?php include 'header.php'; ?>
<h1>Inicio</h1>
<?php include_once 'footer.php'; ?>
<?php include_once 'footer.php'; ?>
If it has already been called, it ignores it.
<html>
<head>
</head>
<body>
<h1>Inicio</h1>
<footer>It's me</footer>
</body>
</html>
Uploading a file
A file is a binary element that is neither a number nor a text: image, video, music, doc, iso... We are unable to read it unless we have a bionic eye and a chip in our brain. If you lack these two requirements, you can only upload it, through a form, and store it in a folder.
It must be handled in a special way. We will need to always use the POST method and add enctype="multipart/form-data". Finally, use the file type input (file).
<!-- Form -->
<form method="post" enctype="multipart/form-data">
<p>
<!-- Image field -->
<input type="file" name="fichero_usuario">
</p>
<p>
<!-- Submit button -->
<input type="submit" value="Enviar">
</p>
</form>
When our form is submitted, the file will be stored in a variable called $_FILES. It is an array with all the information you are going to need.
| Name | Example content | Description |
|---|---|---|
| $_FILES['fichero_usuario']['name'] | 'photo_at_the_beach.jpg' | File name |
| $_FILES['fichero_usuario']['type'] | 'image/png' | MIME (file format) |
| $_FILES['fichero_usuario']['size'] | 3232424 | Size in bytes (5MB -> 5 x 1024 x 1024 bytes) |
| $_FILES['fichero_usuario']['error'] | 0 | Error code. 0 means everything went well, you can find the others here |
| $_FILES['fichero_usuario']['tmp_name'] | 213 | Temporary name |
Now we only have to move it from the temporary folder to the final one, using the move_uploaded_file() method.
move_uploaded_file($_FILES['fichero_usuario']['tmp_name'], $fichero_subido);
Here you can see a complete example.
<html>
<body>
<?php
//======================================================================
// PROCESS IMAGE
//======================================================================
// We check if the data arrives via POST
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Define the directory where it will be saved
$dir_subida = './subidos/';
// Define the final path of the file
$fichero_subido = $dir_subida . basename($_FILES['fichero_usuario']['name']);
// Move the file from the temporary folder to the defined path
if (move_uploaded_file($_FILES['fichero_usuario']['tmp_name'], $fichero_subido)) {
// Confirmation message when everything went well
echo '<p>Uploaded perfectly.</p>';
// Display the image that was just uploaded
echo '<p><img width="500" src="' . $fichero_subido . '"></p>';
} else {
// Error message: Size limit? Attack?
echo '<p>Oops! Something happened.</p>';
}
}
?>
<!-- Form -->
<form method="post" enctype="multipart/form-data">
<p>
<!-- Image field -->
<input type="file" name="fichero_usuario">
</p>
<p>
<!-- Submit button -->
<input type="submit" value="Enviar">
</p>
</form>
</body>
</html>
Multi-file (uploading several files)
It is possible to upload several files under the same name. We just have to add some brackets ([]) after the name as if it were an array.
<!-- Form -->
<form method="post" enctype="multipart/form-data">
<p>
<!-- Image fields -->
<input type="file" name="imagen[]">
<input type="file" name="imagen[]">
<input type="file" name="imagen[]">
</p>
<p>
<!-- Submit button -->
<input type="submit" value="Enviar">
</p>
</form>
When receiving the data, we will have to iterate over the variable, just like an array. It is recommended to check in each case that the file has been uploaded correctly.
// We check if the data arrives via POST
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// We iterate over all the files
foreach ($_FILES["imagen"]["error"] as $posicion => $error) {
// We check if it has been uploaded correctly
if ($error == UPLOAD_ERR_OK) {
// Define the directory where it will be saved
$dir_subida = './subidos/';
// Define the final path of the file
$fichero_subido = $dir_subida . basename($_FILES['imagen']['name'][$posicion]);
// Move the file from the temporary folder to the defined path
if (move_uploaded_file($_FILES['imagen']['tmp_name'][$posicion], $fichero_subido)) {
// Confirmation message when everything went well
echo '<p>Uploaded perfectly' . $_FILES['imagen']['name'][$posicion] . '.</p>';
// Display the image that was just uploaded
echo '<p><img width="500" src="' . $fichero_subido . '"></p>';
} else {
// Error message: Size limit? Attack?
echo '<p>Oops! Something happened.</p>';
}
}
}
}
Together it would look like this.
<html>
<body>
<?php
//======================================================================
// PROCESS IMAGES
//======================================================================
// We check if the data arrives via POST
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// We iterate over all the files
foreach ($_FILES["imagen"]["error"] as $posicion => $error) {
// We check if it has been uploaded correctly
if ($error == UPLOAD_ERR_OK) {
// Define the directory where it will be saved
$dir_subida = './subidos/';
// Define the final path of the file
$fichero_subido = $dir_subida . basename($_FILES['imagen']['name'][$posicion]);
// Move the file from the temporary folder to the defined path
if (move_uploaded_file($_FILES['imagen']['tmp_name'][$posicion], $fichero_subido)) {
// Confirmation message when everything went well
echo '<p>Uploaded perfectly' . $_FILES['imagen']['name'][$posicion] . '.</p>';
// Display the image that was just uploaded
echo '<p><img width="500" src="' . $fichero_subido . '"></p>';
} else {
// Error message: Size limit? Attack?
echo '<p>Oops! Something happened.</p>';
}
}
}
}
?>
<!-- Form -->
<form method="post" enctype="multipart/form-data">
<p>
<!-- Image fields -->
<input type="file" name="imagen[]">
<input type="file" name="imagen[]">
<input type="file" name="imagen[]">
</p>
<p>
<!-- Submit button -->
<input type="submit" value="Enviar">
</p>
</form>
</body>
</html>
Deleting files
To delete a file we must use the unlink method.
unlink('archivo');
We just have to provide the path we want.
unlink('subidos/coche_rojo.jpg');
Preventing overwriting
What happens if we upload two files with the same name? The previous one would disappear, it would be overwritten because it has the same name and is saved in the same place. We must ensure that the file has a unique name.
A trick to solve it is by generating a hash, or a unique alphanumeric sequence for each file that replaces the name. A very popular algorithm is SHA-1.
In cryptography, SHA-1 (Secure Hash Algorithm 1) is a cryptographic hash function that has been broken, but which is still widely used to generate a 40-digit hexadecimal number. Its author was the United States National Security Agency, and it is used for processing United States information.
If we wanted to get a hash from a text, we should use sha1().
echo sha1('texto');
// ea631551f5569f612dd702b900c596c2a99c0dfd
For files we have a specific function called hash_file(). In this example we use the SHA-256 algorithm, safer than SHA-1.
echo hash_file('sha256', $_FILES['fichero_usuario']['tmp_name']);
// f8c3bf62a9aa3e6fc1619c250e48abe7519373d3edf41be62eb5dc45199af2ef
If we had a file called reloj.jpg.
$fichero_subido = $dir_subida . hash_file('sha256', $_FILES['fichero_usuario']['tmp_name']) . basename($_FILES['fichero_usuario']['name']);
echo $fichero_subido;
// subidos/f8c3bf62a9aa3e6fc1619c250e48abe7519373d3edf41be62eb5dc45199af2efreloj.jpg
No matter how many reloj.jpg files are uploaded, they will never be overwritten, unless at the binary level they are exactly the same (which would not be a problem either, because it would be the same file).
Maximum size
If you want to limit the size of all files you can do it by adding a special input.
<input type="hidden" name="MAX_FILE_SIZE" value="20000" />
The value is measured in bytes.
If a file exceeds our boundary it will give an error, but it will never actually be uploaded.
Another way to change it, in this case permanently, is by modifying some PHP variables in its configuration file.
sudo nano /etc/php/{versión}/cli/php.ini
Edit the following variables if you want to limit it to 100Mb.
upload_max_filesize=100Mb
post_max_size=100Mb
Still, validate the size with PHP, it's easy to manipulate the limit within the browser. Remember: never trust the user.
Image processing
PHP is not only limited to generating HTML and moving files, it can also process images. There is a multitude of possibilities.
- Resize (used to create thumbnails).
- Crop.
- Apply color filters.
- Create images (as it sounds).
- Add watermarks.
- Change format.
To create a thumbnail you could do it using the native Imagick library.
First we will have to install it on the system. With Ubuntu or Debian it is very simple.
sudo apt install php-imagick
Now you can work with it. In the following example we capture imagen.jpg and resize it to 100px wide. Finally it is saved with the name miniatura.jpg.
$imagen = new Imagick('imagen.jpg');
// If 0 is provided as the width or height parameter,
// the aspect ratio is kept
$imagen->thumbnailImage(100, 0);
// Save it
file_put_contents('miniatura.jpg', $imagen);
It has various tools to manipulate well-known formats such as: JPEG, GIF, PNG and WebP (among others). You can see more in the documentation.
Complete example
You can see an example that covers all the previous cases.
- Validates that an image has been attached in the form.
- Validates that it is a jpg or png image.
- Validates that it does not exceed a certain size. In this case 2Mb.
- Creates a thumbnail. In this case 100px wide.
- Changes the name to a random one to avoid possible conflicts with other files.
- Displays the thumbnail in the HTML.
<?php
//======================================================================
// VARIABLES
//======================================================================
$errorAvatar = 0;
$avatar = null;
$rutaThumbnail = null;
// Define the directory where it will be saved
define('PATH_AVATAR', './subidos/');
define('PATH_AVATAR_THUMBNAIL', './subidos/thumbnails/');
// Max avatar size: 2 Mb
define('MAX_SIZE_AVATAR_MB', 2);
define('MAX_SIZE_AVATAR', MAX_SIZE_AVATAR_MB * 1024 * 1024);
// In /etc/php/7.4/cli/php.ini edit the following variables
// upload_max_filesize=100Mb
// post_max_size=100Mb
// Thumbnail width
define('WIDTH_THUMBNAIL', 100);
//======================================================================
// PROCESS FORM
//======================================================================
// We check if the data arrives via POST
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES)) {
//-----------------------------------------------------
// Collect avatar
//-----------------------------------------------------
// Check if the directory exists, and if not, create it
if (!is_dir(PATH_AVATAR_THUMBNAIL)) {
mkdir(PATH_AVATAR_THUMBNAIL, 0775, true);
}
// Define the final path of the file
$nombreFoto = hash_file('sha256', $_FILES['avatar']['tmp_name']) . basename($_FILES['avatar']['name']);
$ficheroSubido = PATH_AVATAR . $nombreFoto;
//-----------------------------------------------------
// Error handling
//-----------------------------------------------------
// Maximum size
if ($_FILES['avatar']['size'] > MAX_SIZE_AVATAR) {
$errorAvatar = 1;
}
// Only JPG and PNG
if ($_FILES['avatar']['type'] !== 'image/png' && $_FILES['avatar']['type'] !== 'image/jpeg') {
$errorAvatar = 2;
}
// Required
if ($_FILES['avatar']['size'] === 0) {
$errorAvatar = 3;
}
//-----------------------------------------------------
// Process image
//-----------------------------------------------------
if ($errorAvatar === 0) {
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $ficheroSubido)) {
// Move the file from the temporary folder to the defined path
$avatar = $ficheroSubido;
// We create a thumbnail
// Don't forget to install it with: sudo apt install php-imagick
$imagen = new Imagick($avatar);
// If 0 is provided as the width or height parameter,
// the aspect ratio is kept
$imagen->thumbnailImage(WIDTH_THUMBNAIL, 0);
$rutaThumbnail = PATH_AVATAR_THUMBNAIL . $nombreFoto;
file_put_contents($rutaThumbnail, $imagen);
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Profile</title>
</head>
<body>
<?php if (isset($rutaThumbnail)): ?>
<img src="<?= $rutaThumbnail; ?>" alt="my avatar" width="<?= WIDTH_THUMBNAIL ?>">
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<p>
<label>
Photo:
<input type="file" name="avatar">
</label>
</p>
<?php if ($errorAvatar === 1): ?>
<p style="color: red">
Size too large, please make sure it is less than <?= MAX_SIZE_AVATAR_MB ?>Mb
</p>
<?php elseif ($errorAvatar === 2): ?>
<p style="color: red">
Only JPG or PNG images are allowed.
</p>
<?php elseif ($errorAvatar === 3): ?>
<p style="color: red">
You must include an image
</p>
<?php endif; ?>
<p>
<input type="submit" value="Guardar">
</p>
</form>
</body>
</html>
Validation and Security
Uploading files without validating them correctly is one of the most dangerous vulnerabilities in a web application. An attacker could upload malicious code (for example a PHP file disguised as an image) and execute it on your server. That is why it is essential to always validate the files you upload.
Golden rule: Never trust the data that comes from the client. A user can easily modify the MIME type from the browser.
Validate the real MIME type
The MIME type that comes in $_FILES['archivo']['type'] can be spoofed by the client. To validate correctly we must use PHP functions that analyze the actual content of the file.
<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$archivoTemporal = $_FILES['imagen']['tmp_name'];
// Get the real MIME type of the file
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeReal = finfo_file($finfo, $archivoTemporal);
finfo_close($finfo);
// Whitelist of allowed types
$tiposPermitidos = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeReal, $tiposPermitidos)) {
die('Error: Only images are allowed (JPG, PNG, GIF, WEBP)');
}
echo "Valid MIME type: $mimeReal";
}
?>
<form method="post" enctype="multipart/form-data">
<input type="file" name="imagen">
<input type="submit" value="Subir">
</form>
Validate the file size
Limit the maximum size to prevent someone from saturating your server with huge files.
<?php
// Maximum size: 5MB
$tamañoMaximo = 5 * 1024 * 1024; // 5MB in bytes
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($_FILES['imagen']['size'] > $tamañoMaximo) {
die('Error: The file exceeds the maximum size of 5MB');
}
if ($_FILES['imagen']['size'] === 0) {
die('Error: The file is empty');
}
echo 'Valid size: ' . round($_FILES['imagen']['size'] / 1024, 2) . ' KB';
}
?>
Sanitize the file name
File names can contain dangerous characters or relative paths (../../../etc/passwd). You must always generate a safe name.
<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Get the real extension
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeReal = finfo_file($finfo, $_FILES['imagen']['tmp_name']);
finfo_close($finfo);
// Map MIME to extension
$extensiones = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp'
];
$extension = $extensiones[$mimeReal] ?? 'bin';
// Generate a unique and safe name
$nombreSeguro = uniqid('img_', true) . '.' . $extension;
$rutaDestino = './subidos/' . $nombreSeguro;
if (move_uploaded_file($_FILES['imagen']['tmp_name'], $rutaDestino)) {
echo "File uploaded correctly: $nombreSeguro";
}
}
?>
Why not use the original name? Because it could contain special characters, spaces, relative paths or dangerous extensions. An attacker could upload
shell.php.jpghoping your server executes it as PHP.
Store outside the webroot
The safest way to store uploaded files is to save them outside the public folder of your web server. This way, even if someone uploads a malicious PHP file, the web server won't be able to execute it.
<?php
// Directory outside the webroot (not directly accessible from the browser)
$directorioSeguro = '../uploads_privados/';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$nombreSeguro = uniqid('doc_', true) . '.pdf';
$rutaCompleta = $directorioSeguro . $nombreSeguro;
if (move_uploaded_file($_FILES['documento']['tmp_name'], $rutaCompleta)) {
// Save the name in the database to retrieve it later
echo "Document stored securely";
}
}
?>
To serve the file afterwards, create a script that reads it and sends it with the correct headers:
<?php
// download.php
$archivoId = $_GET['id'] ?? '';
// Here you would validate that the user has permission to download this file
// and get the real name from the database
$rutaArchivo = '../uploads_privados/' . $archivoId;
if (file_exists($rutaArchivo)) {
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="documento.pdf"');
readfile($rutaArchivo);
exit;
}
?>
Complete and secure example
Here is an example that combines all the best practices:
<?php
// Configuration
$directorioSubidas = './subidos/';
$tamañoMaximo = 5 * 1024 * 1024; // 5MB
$tiposPermitidos = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$extensiones = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp'
];
$error = '';
$exito = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES['imagen'])) {
// 1. Verify that there were no errors in the upload
if ($_FILES['imagen']['error'] !== UPLOAD_ERR_OK) {
$error = 'Error uploading the file';
}
// 2. Validate size
elseif ($_FILES['imagen']['size'] === 0) {
$error = 'The file is empty';
}
elseif ($_FILES['imagen']['size'] > $tamañoMaximo) {
$error = 'The file exceeds the maximum size of 5MB';
}
else {
// 3. Validate the real MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeReal = finfo_file($finfo, $_FILES['imagen']['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeReal, $tiposPermitidos)) {
$error = 'Only images are allowed (JPG, PNG, GIF, WEBP)';
}
else {
// 4. Generate a safe name
$extension = $extensiones[$mimeReal];
$nombreSeguro = uniqid('img_', true) . '.' . $extension;
$rutaDestino = $directorioSubidas . $nombreSeguro;
// 5. Move the file
if (move_uploaded_file($_FILES['imagen']['tmp_name'], $rutaDestino)) {
$exito = "Image uploaded correctly: $nombreSeguro";
} else {
$error = 'Error moving the file';
}
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Secure image upload</title>
</head>
<body>
<h1>Upload image (with secure validation)</h1>
<?php if ($error): ?>
<p style="color: red; font-weight: bold;"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<?php if ($exito): ?>
<p style="color: green; font-weight: bold;"><?= htmlspecialchars($exito) ?></p>
<img src="<?= htmlspecialchars($directorioSubidas . basename($nombreSeguro)) ?>" width="400" alt="Uploaded image">
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<label>
Select an image (JPG, PNG, GIF, WEBP, max 5MB):
<input type="file" name="imagen" accept="image/*" required>
</label>
<button type="submit">Upload image</button>
</form>
</body>
</html>
Remember: Security is like the layers of an onion. Each layer of validation adds up. Never trust only client-side validations (HTML5
accept, JavaScript), always validate on the server.
Activity 1
- Create a website where a username and password are requested.
- If it is correct, it must reach a page protected by a session.
- Add a button to close the session.
This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.
Desafíos de programación atemporales y multiparadigmáticos
Te encuentras ante un librillo de actividades, divididas en 2 niveles de dificultad. Te enfrentarás a los casos más comunes que te puedes encontrar en pruebas técnicas o aprender conceptos elementales de programación.
Buy the bookWill you buy me a coffee?
This is how I keep writing without ads or paywalls.
Sure, it's on me!
Comments
There are no comments yet.