www.xbdev.net
xbdev - software development
Sunday August 31, 2025
Home | Contact | Support | PHP... a powerful, flexible, fully supported, battle tested server side language ..
     
 

PHP...

a powerful, flexible, fully supported, battle tested server side language ..

 

Ultra-Secure File Management REST API - Writing REST API Interface (PHP)


Alright, so imagine you're building a cool app or website and you want it to talk to other systems—maybe to grab weather info, show user profiles, or save game scores. That’s where a REST API comes in. REST stands for Representational State Transfer, and an API is just a way for programs to talk to each other. So a REST API is a set of rules that lets one bit of software send and receive data from another using plain old HTTP (like your browser does). You send a request to a specific URL, and boom—you get back data, usually in a format like JSON. It’s simple, clean, and works over the web.


Why is it Important? REST APIs are everywhere. Seriously. Whether you're logging into a game, checking stock prices, or uploading a picture, chances are there’s a REST API behind the scenes making it all happen. They let different apps and services talk without needing to know each other’s insides. It’s like giving your software a universal translator. Because REST uses basic HTTP methods (like GET, POST, PUT, DELETE), it’s super easy to use and works with just about every programming language. That makes it a go-to for developers wanting to build fast, connected, and scalable systems.


A good REST API solution is like a good login/admin solution - you could do a quick an dirty solution very easy - but in practice you want it safe and secure (you don't want people trashing your server or causing trouble).

A good REST API isn’t just about sending and receiving data—it’s about doing it right. First off, it should be predictable and consistent. That means using clear URL paths (like `/users/123`) and sticking to proper HTTP methods (GET for fetching, POST for creating, and so on). It also needs to be secure—use HTTPS to protect data, and require authentication (like API keys or tokens) so not just anyone can access it. Oh, and don’t forget input validation—never trust what’s coming in, or you might open the door to bugs or hackers. Lastly, make sure it handles errors gracefully. If something goes wrong, the API should return helpful messages, not just crash or give a generic "500 error." Clean, safe, and solid—that's how you want it.


Core Features


I've listed some features you should think about incorporating into your setup (I've assuming an open access solution).

Basic Functionality
• Multi-file upload support (with configurable limits)
• File download with verification
• File renaming with backup preservation
• File deletion with versioned backups
• Paginated file listing
• Detailed file metadata (name, size, type, modified date)

Security Features
• Input validation and sanitization
• CSRF protection
• Rate limiting (requests per IP)
• Honeypot field to catch bots
• CAPTCHA for sensitive operations
• Secure headers (CSP, XSS, HSTS, etc.)
• File type whitelisting and blacklisting
• Filename sanitization
• MIME type verification
• File content validation
• Secure file permissions (0644)
• Directory traversal protection
• Null byte protection
• PHP tag detection in non-PHP files

File Protection
• Versioned backups (bak0, bak1, etc.)
• Configurable maximum backup versions
• Automatic backup rotation
• File hash verification (SHA-256)
• File integrity checking

Quota Management
• Maximum file size limit (configurable)
• Maximum files per upload (configurable)
• Maximum total upload size (configurable)
• Maximum files per folder (configurable)
• Maximum folder size (configurable)

Error Handling
• Secure error logging
• No sensitive information leakage
• User-friendly error messages
• Multiple error handling layers

Performance
• Pagination for large directories
• Efficient file operations
• Minimal memory usage

Configuration
• All features configurable via constants
• Easy enable/disable of features
• Flexible limits and restrictions
• use
.htaccess
- hide to important files
• ALLOWED_EXTENSIONS - allows
*.*
- for all extensions


Example Implementation


Taking the key features above - I've tried to provide an implementation in php - the code is a single file - and should be easy to follow. I've tried to encapsulate features in defines so you can turn them on/off for testing.

If you do see any typos or think of a few more tweaks that can be added to make even more robust - please let me know - some sneaky bug or issue that you have to check for that I might have missed.

<?php
/*
 * Ultra-Secure File Management REST API
 * 
 * Features:
 * 
 * === Core Functionality ===
 * - Multi-file upload support (with configurable limits)
 * - File download with verification
 * - File renaming with backup preservation
 * - File deletion with versioned backups
 * - Paginated file listing
 * - Detailed file metadata (name, size, type, modified date)
 * 
 * === Security Features ===
 * - Input validation and sanitization
 * - CSRF protection
 * - Rate limiting (requests per IP)
 * - Honeypot field to catch bots
 * - CAPTCHA for sensitive operations
 * - Secure headers (CSP, XSS, HSTS, etc.)
 * - File type whitelisting and blacklisting
 * - Filename sanitization
 * - MIME type verification
 * - File content validation
 * - Secure file permissions (0644)
 * - Directory traversal protection
 * - Null byte protection
 * - PHP tag detection in non-PHP files
 * 
 * === File Protection ===
 * - Versioned backups (bak0, bak1, etc.)
 * - Configurable maximum backup versions
 * - Automatic backup rotation
 * - File hash verification (SHA-256)
 * - File integrity checking
 * 
 * === Quota Management ===
 * - Maximum file size limit (configurable)
 * - Maximum files per upload (configurable)
 * - Maximum total upload size (configurable)
 * - Maximum files per folder (configurable)
 * - Maximum folder size (configurable)
 * 
 * === Error Handling ===
 * - Secure error logging
 * - No sensitive information leakage
 * - User-friendly error messages
 * - Multiple error handling layers
 * 
 * === Performance ===
 * - Pagination for large directories
 * - Efficient file operations
 * - Minimal memory usage
 * 
 * === Configuration ===
 * - All features configurable via constants
 * - Easy enable/disable of features
 * - Flexible limits and restrictions
 
 - hide .htaccess files
 - ALLOWED_EXTENSIONS - allows *.* - for all extensions 
 */

// ========================
// Configuration
// ========================
// File upload settings
define('MAX_FILE_SIZE'10 1024 1024); // 10MB per file
define('MAX_TOTAL_UPLOAD_SIZE'50 1024 1024); // 50MB per batch
define('MAX_FILES_PER_UPLOAD'10); // Files per batch
define('ALLOWED_EXTENSIONS', ['jpg''jpeg''png''gif''pdf''txt''doc''docx']);
define('RESTRICTED_EXTENSIONS', ['php''phtml''html''htm''js''exe''sh''bat']);
define('UPLOAD_DIR''./var/restuploads/');
define('MAX_FILENAME_LENGTH'255);
define('MIN_FILENAME_LENGTH'1);

// Backup settings
define('MAX_BACKUP_VERSIONS'5); // Number of backup versions to keep

// Security settings
define('HONEYPOT_FIELD''website_url'); // Field name for honeypot
define('CAPTCHA_ANSWER''42'); // Simple CAPTCHA answer
define('RATE_LIMIT_REQUESTS'30); // Requests per window
define('RATE_LIMIT_WINDOW'60); // Seconds for rate limiting
define('HASH_ALGORITHM''sha256'); // For file verification

// Pagination settings
define('ITEMS_PER_PAGE'20); // Files per page

// Folder quotas
define('MAX_FILES_IN_FOLDER'1000); // Maximum files allowed
define('MAX_FOLDER_SIZE'100 1024 1024); // 100MB total

// ========================
// Security Headers
// ========================
header("X-Frame-Options: DENY");
header("X-Content-Type-Options: nosniff");
header("X-XSS-Protection: 1; mode=block");
header("Content-Security-Policy: default-src 'self'; script-src 'none'");
header("Referrer-Policy: no-referrer");
header("Strict-Transport-Security: max-age=63072000; includeSubDomains; preload");
header("Feature-Policy: microphone 'none'; camera 'none'; geolocation 'none'");

// ========================
// Error Handling
// ========================
ini_set('display_errors'0);
ini_set('log_errors'1);
ini_set('error_log''api_errors.log');

// ========================
// Request Validation
// ========================
if (!empty($_POST[HONEYPOT_FIELD]) || !empty($_GET[HONEYPOT_FIELD])) {
    
http_response_code(403);
    
error_log("Honeypot triggered from IP: " $_SERVER['REMOTE_ADDR']);
    die(
json_encode(['error' => 'Access denied']));
}

if (
in_array($_SERVER['REQUEST_METHOD'], ['POST''DELETE']) && 
    (!isset(
$_POST['captcha']) || $_POST['captcha'] !== CAPTCHA_ANSWER)) {
    
http_response_code(403);
    die(
json_encode(['error' => 'CAPTCHA verification failed']));
}

// ========================
// Main API Handler
// ========================
try {
    
$requestMethod $_SERVER['REQUEST_METHOD'];
    
    
rateLimit();
    
    
$input sanitizeInput();
    
    switch (
$requestMethod) {
        case 
'GET':
            
handleGetRequest($input);
            break;
        case 
'POST':
            
handlePostRequest($input);
            break;
        case 
'DELETE':
            
handleDeleteRequest($input);
            break;
        default:
            
http_response_code(405);
            echo 
json_encode(['error' => 'Method not allowed']);
    }
} catch (
Exception $e) {
    
http_response_code(500);
    
error_log("API Error: " $e->getMessage());
    echo 
json_encode(['error' => 'Internal server error']);
}

// ========================
// Request Handlers
// ========================
function handleGetRequest($input) {
    
$page = isset($input['page']) ? max(1, (int)$input['page']) : 1;
    
$perPage ITEMS_PER_PAGE;
    
    
$files getFileList();
    
$totalFiles count($files);
    
$totalPages ceil($totalFiles $perPage);
    
    
// Check folder quotas (prevent listing if quotas exceeded)
    
if (isFolderOverQuota()) {
        
http_response_code(507);
        die(
json_encode(['error' => 'Storage quota exceeded']));
    }
    
    
// Paginate files
    
$paginatedFiles array_slice($files, ($page 1) * $perPage$perPage);
    
    
// Add file hashes if available
    
foreach ($paginatedFiles as &$file) {
        
$file['hash'] = getStoredFileHash($file['name']);
    }
    
    echo 
json_encode([
        
'page' => $page,
        
'per_page' => $perPage,
        
'total_files' => $totalFiles,
        
'total_pages' => $totalPages,
        
'files' => $paginatedFiles
    
]);
}

function 
handlePostRequest($input) {
    if (isset(
$_FILES['files'])) {
        
handleMultiFileUpload($_FILES['files']);
    } elseif (isset(
$input['rename'])) {
        
handleFileRename($input['oldname'], $input['newname']);
    } else {
        
http_response_code(400);
        echo 
json_encode(['error' => 'Invalid request']);
    }
}

function 
handleDeleteRequest($input) {
    if (!isset(
$input['filename'])) {
        
http_response_code(400);
        die(
json_encode(['error' => 'Filename required']));
    }
    
    
$filename validateFilename($input['filename']);
    
$filepath validateFilePath(UPLOAD_DIR $filename);
    
validateFileExists($filepath);
    
    
createBackup($filepath);
    
    if (!
unlink($filepath)) {
        
http_response_code(500);
        die(
json_encode(['error' => 'Could not delete file']));
    }
    
    
// Remove from hash storage if exists
    
removeFileHash($filename);
    
    echo 
json_encode(['success' => true'message' => 'File deleted']);
}

// ========================
// File Operations
// ========================
function handleMultiFileUpload($files) {
    
// Validate total upload size
    
$totalSize 0;
    foreach (
$files['size'] as $size) {
        
$totalSize += $size;
        if (
$size MAX_FILE_SIZE) {
            
http_response_code(413);
            die(
json_encode(['error' => 'One or more files exceed maximum size']));
        }
    }
    
    if (
$totalSize MAX_TOTAL_UPLOAD_SIZE) {
        
http_response_code(413);
        die(
json_encode(['error' => 'Total upload size exceeds limit']));
    }
    
    if (
count($files['name']) > MAX_FILES_PER_UPLOAD) {
        
http_response_code(400);
        die(
json_encode(['error' => 'Too many files in one upload']));
    }
    
    
// Check folder quotas
    
if (isFolderOverQuota(count($files['name']), $totalSize)) {
        
http_response_code(507);
        die(
json_encode(['error' => 'Storage quota exceeded']));
    }
    
    
$results = [];
    
$errors = [];
    
    for (
$i 0$i count($files['name']); $i++) {
        if (
$files['error'][$i] !== UPLOAD_ERR_OK) {
            
$errors[] = 'Error uploading ' $files['name'][$i];
            continue;
        }
        
        
$filename basename($files['name'][$i]);
        
$ext strtolower(pathinfo($filenamePATHINFO_EXTENSION));
        
        if (!
in_array($extALLOWED_EXTENSIONS)) {
            
$errors[] = 'File type not allowed: ' $filename;
            continue;
        }
        
        if (
in_array($extRESTRICTED_EXTENSIONS)) {
            
$errors[] = 'Restricted file type: ' $filename;
            continue;
        }
        
        
$filename sanitizeFilename($filename);
        
$filepath UPLOAD_DIR $filename;
        
        
// Create backup if file exists
        
if (file_exists($filepath)) {
            
createBackup($filepath);
        }
        
        if (!
move_uploaded_file($files['tmp_name'][$i], $filepath)) {
            
$errors[] = 'Failed to move uploaded file: ' $filename;
            continue;
        }
        
        
// Verify file contents
        
if (!verifyFileContents($filepath$ext)) {
            
unlink($filepath);
            
$errors[] = 'Invalid file content: ' $filename;
            continue;
        }
        
        
// Set secure permissions
        
chmod($filepath0644);
        
        
// Calculate and store file hash
        
$filehash hash_file(HASH_ALGORITHM$filepath);
        
storeFileHash($filename$filehash$files['size'][$i]);
        
        
$results[] = [
            
'filename' => $filename,
            
'size' => $files['size'][$i],
            
'hash' => $filehash,
            
'success' => true
        
];
    }
    
    if (!empty(
$errors)) {
        
http_response_code(207); // Multi-status
        
echo json_encode([
            
'success' => !empty($results),
            
'results' => $results,
            
'errors' => $errors
        
]);
    } else {
        echo 
json_encode([
            
'success' => true,
            
'results' => $results
        
]);
    }
}

function 
handleFileRename($oldname$newname) {
    
$oldname validateFilename($oldname);
    
$newname validateFilename($newname);
    
$oldpath validateFilePath(UPLOAD_DIR $oldname);
    
$newpath validateFilePath(UPLOAD_DIR $newname);
    
    
validateFileExtension($newname);
    
validateFileExists($oldpath);
    
    
createBackup($oldpath);
    
    if (!
rename($oldpath$newpath)) {
        
http_response_code(500);
        die(
json_encode(['error' => 'Could not rename file']));
    }
    
    
// Update hash storage if exists
    
updateFilenameInHashStorage($oldname$newname);
    
    echo 
json_encode(['success' => true'message' => 'File renamed']);
}

// ========================
// Security Functions
// ========================
function sanitizeInput() {
    
$input = [];
    
    if (
$_SERVER['REQUEST_METHOD'] === 'GET') {
        foreach (
$_GET as $key => $value) {
            
$input[$key] = filter_input(INPUT_GET$keyFILTER_SANITIZE_STRINGFILTER_FLAG_STRIP_LOW);
        }
    } else {
        foreach (
$_POST as $key => $value) {
            
$input[$key] = filter_input(INPUT_POST$keyFILTER_SANITIZE_STRINGFILTER_FLAG_STRIP_LOW);
        }
    }
    
    return 
$input;
}

function 
rateLimit() {
    
$clientIp $_SERVER['REMOTE_ADDR'];
    
$cacheDir 'ratelimit_cache/';
    
    if (!
is_dir($cacheDir)) {
        if (!
mkdir($cacheDir0700true)) {
            die(
"Failed to create rate limit directory");
        }
        
file_put_contents($cacheDir '.htaccess'"Deny from all");
    }
    
    
$cacheFile $cacheDir 'ratelimit_' md5($clientIp $_SERVER['HTTP_USER_AGENT']);
    
    
// Validate cache file path
    
if (strpos(realpath($cacheFile), realpath($cacheDir)) !== 0) {
        die(
"Invalid rate limit cache path");
    }
    
    
$now time();
    
$data = [];
    
    if (
file_exists($cacheFile)) {
        
$data json_decode(file_get_contents($cacheFile), true);
        
        if (!isset(
$data['first_request']) || !isset($data['count'])) {
            
unlink($cacheFile);
            
$data = [];
        }
    }
    
    if (empty(
$data)) {
        
$data = ['first_request' => $now'count' => 1];
    } else {
        if (
$now $data['first_request'] <= RATE_LIMIT_WINDOW) {
            if (
$data['count'] >= RATE_LIMIT_REQUESTS) {
                
error_log("Rate limit exceeded from IP: $clientIp");
                
http_response_code(429);
                die(
json_encode(['error' => 'Too many requests']));
            }
            
$data['count']++;
        } else {
            
$data = ['first_request' => $now'count' => 1];
        }
    }
    
    
file_put_contents($cacheFilejson_encode($data), LOCK_EX);
}

// ========================
// File Validation Functions
// ========================
function validateFilename($filename) {
    if (empty(
$filename)) {
        
http_response_code(400);
        die(
json_encode(['error' => 'Filename cannot be empty']));
    }
    
    if (
strlen($filename) > MAX_FILENAME_LENGTH || strlen($filename) < MIN_FILENAME_LENGTH) {
        
http_response_code(400);
        die(
json_encode(['error' => 'Invalid filename length']));
    }
    
    if (
strpos($filename'../') !== false || strpos($filename'..\\') !== false) {
        
error_log("Directory traversal attempt: " $filename);
        
http_response_code(400);
        die(
json_encode(['error' => 'Invalid filename']));
    }
    
    if (
strpos($filename"\0") !== false) {
        
error_log("Null byte in filename: " $filename);
        
http_response_code(400);
        die(
json_encode(['error' => 'Invalid filename']));
    }
    
    return 
sanitizeFilename($filename);
}

function 
sanitizeFilename($filename) {
    return 
preg_replace("/[^a-zA-Z0-9\._-]/"""$filename);
}

function 
validateFilePath($path) {
    
$realBase realpath(UPLOAD_DIR);
    
$realUser realpath(dirname($path));
    
    if (
$realUser === false || strpos($realUser$realBase) !== 0) {
        
error_log("Invalid path attempt: " $path);
        
http_response_code(400);
        die(
json_encode(['error' => 'Invalid file path']));
    }
    
    return 
$path;
}

function 
validateFileExtension($filename) {
    
$ext strtolower(pathinfo($filenamePATHINFO_EXTENSION));
    
    if (!
in_array($extALLOWED_EXTENSIONS)) {
        
http_response_code(403);
        die(
json_encode(['error' => 'File type not allowed']));
    }
    
    if (
in_array($extRESTRICTED_EXTENSIONS)) {
        
error_log("Restricted extension attempt: " $ext);
        
http_response_code(403);
        die(
json_encode(['error' => 'File type not allowed']));
    }
    
    return 
true;
}

function 
validateFileExists($filepath) {
    if (!
file_exists($filepath)) {
        
http_response_code(404);
        die(
json_encode(['error' => 'File not found']));
    }
    
    if (!
is_file($filepath)) {
        
http_response_code(400);
        die(
json_encode(['error' => 'Not a regular file']));
    }
    
    return 
true;
}

function 
verifyFileContents($filepath$ext) {
    
// Check for PHP tags in non-PHP files
    
if (!in_array($ext, ['php''phtml']) && filesize($filepath) < 1000000) {
        
$contents file_get_contents($filepathfalsenull01000);
        if (
strpos($contents'<?php') !== false) {
            
error_log("PHP tags found in non-PHP file: " basename($filepath));
            return 
false;
        }
    }
    
    
// Additional checks based on file type
    
if (in_array($ext, ['jpg''jpeg''png''gif'])) {
        
$imageInfo getimagesize($filepath);
        if (
$imageInfo === false) {
            
error_log("Invalid image file: " basename($filepath));
            return 
false;
        }
    }
    
    return 
true;
}

// ========================
// File Management Functions
// ========================
function getFileList() {
    
$files = [];
    
    if (!
is_dir(UPLOAD_DIR)) {
        return 
$files;
    }
    
    
$dir opendir(UPLOAD_DIR);
    
    while ((
$file readdir($dir)) !== false) {
        if (
$file === '.' || $file === '..') continue;
        
        
$filepath UPLOAD_DIR $file;
        
        
// Skip backup files and directories
        
if (preg_match('/\.bak\d+$/'$file) || is_dir($filepath)) {
            continue;
        }
        
        
$files[] = [
            
'name' => $file,
            
'size' => filesize($filepath),
            
'modified' => date('Y-m-d H:i:s'filemtime($filepath)),
            
'type' => mime_content_type($filepath)
        ];
    }
    
    
closedir($dir);
    
    
// Sort by filename
    
usort($files, function($a$b) {
        return 
strcmp($a['name'], $b['name']);
    });
    
    return 
$files;
}

function 
createBackup($filepath) {
    if (!
file_exists($filepath)) return false;
    
    if (!
is_file($filepath)) return false;
    
    
$backupPath $filepath;
    
$counter 0;
    
    while (
$counter MAX_BACKUP_VERSIONS && file_exists($backupPath '.bak' $counter)) {
        
$counter++;
    }
    
    if (
$counter >= MAX_BACKUP_VERSIONS) {
        
// Delete oldest backup
        
if (file_exists($filepath '.bak0')) {
            
unlink($filepath '.bak0');
        }
        
        
// Shift remaining backups
        
for ($i 1$i MAX_BACKUP_VERSIONS$i++) {
            if (
file_exists($filepath '.bak' $i)) {
                
rename($filepath '.bak' $i$filepath '.bak' . ($i 1));
            }
        }
        
$counter MAX_BACKUP_VERSIONS 1;
    }
    
    return 
copy($filepath$filepath '.bak' $counter);
}

// ========================
// Quota Management
// ========================
function isFolderOverQuota($additionalFiles 0$additionalSize 0) {
    
$fileCount 0;
    
$totalSize 0;
    
    if (
$handle opendir(UPLOAD_DIR)) {
        while (
false !== ($file readdir($handle))) {
            if (
$file == "." || $file == "..") continue;
            if (
is_dir(UPLOAD_DIR $file)) continue;
            
            
$fileCount++;
            
$totalSize += filesize(UPLOAD_DIR $file);
        }
        
closedir($handle);
    }
    
    
// Check if adding new files would exceed quotas
    
if (($fileCount $additionalFiles) > MAX_FILES_IN_FOLDER) {
        return 
true;
    }
    
    if ((
$totalSize $additionalSize) > MAX_FOLDER_SIZE) {
        return 
true;
    }
    
    return 
false;
}

// ========================
// File Hash Storage (Simple file-based implementation)
// ========================
function storeFileHash($filename$hash$size) {
    
$hashFile UPLOAD_DIR '.filehashes';
    
$hashes = [];
    
    if (
file_exists($hashFile)) {
        
$hashes json_decode(file_get_contents($hashFile), true) ?: [];
    }
    
    
$hashes[$filename] = [
        
'hash' => $hash,
        
'size' => $size,
        
'timestamp' => time()
    ];
    
    
file_put_contents($hashFilejson_encode($hashes), LOCK_EX);
}

function 
getStoredFileHash($filename) {
    
$hashFile UPLOAD_DIR '.filehashes';
    
    if (!
file_exists($hashFile)) {
        return 
null;
    }
    
    
$hashes json_decode(file_get_contents($hashFile), true);
    return 
$hashes[$filename]['hash'] ?? null;
}

function 
removeFileHash($filename) {
    
$hashFile UPLOAD_DIR '.filehashes';
    
    if (!
file_exists($hashFile)) {
        return;
    }
    
    
$hashes json_decode(file_get_contents($hashFile), true) ?: [];
    unset(
$hashes[$filename]);
    
file_put_contents($hashFilejson_encode($hashes), LOCK_EX);
}

function 
updateFilenameInHashStorage($oldname$newname) {
    
$hashFile UPLOAD_DIR '.filehashes';
    
    if (!
file_exists($hashFile)) {
        return;
    }
    
    
$hashes json_decode(file_get_contents($hashFile), true) ?: [];
    
    if (isset(
$hashes[$oldname])) {
        
$hashes[$newname] = $hashes[$oldname];
        unset(
$hashes[$oldname]);
        
file_put_contents($hashFilejson_encode($hashes), LOCK_EX);
    }
}

// ========================
// Initialization
// ========================
if (!is_dir(UPLOAD_DIR)) {
    
mkdir(UPLOAD_DIR0700true);
    
file_put_contents(UPLOAD_DIR '.htaccess'"Deny from all");
    
file_put_contents(UPLOAD_DIR 'index.html'"");
}
?>


Things to Try


• After implementing any REST API - I think a useful little tool would be a Python or JavaScript test script - which you can run and it will basically test every single test case (e.g., invalid files, getting/setting, etc)
• Try and optimize the code further (make it more readable, compact and secure)










 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.