237 lines
8.9 KiB
PHP
237 lines
8.9 KiB
PHP
<?php
|
|
// ============================================================
|
|
// core/helpers.php — دوال مساعدة موحدة
|
|
// ============================================================
|
|
|
|
// ── فلترة المدخلات (محسّنة) ─────────────────────────────────
|
|
function filterRequest(string $name, string $type = 'string'): mixed
|
|
{
|
|
// قراءة من POST أو JSON body
|
|
$value = null;
|
|
|
|
if (isset($_POST[$name]) && $_POST[$name] !== '') {
|
|
$value = $_POST[$name];
|
|
} else {
|
|
// محاولة قراءة من JSON body
|
|
static $jsonBody = null;
|
|
if ($jsonBody === null) {
|
|
$raw = file_get_contents('php://input');
|
|
$jsonBody = json_decode($raw, true) ?? [];
|
|
}
|
|
$value = $jsonBody[$name] ?? null;
|
|
}
|
|
|
|
if ($value === null || $value === '') return null;
|
|
|
|
$value = trim((string)$value);
|
|
|
|
// إزالة control characters
|
|
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
|
|
|
|
return match ($type) {
|
|
'int' => filter_var($value, FILTER_VALIDATE_INT) !== false ? (int)$value : null,
|
|
'float' => filter_var($value, FILTER_VALIDATE_FLOAT) !== false ? (float)$value : null,
|
|
'email' => filter_var($value, FILTER_VALIDATE_EMAIL) ?: null,
|
|
'url' => filter_var($value, FILTER_VALIDATE_URL) ?: null,
|
|
'bool' => filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
|
|
default => $value, // string — بدون htmlspecialchars (نتركه لـ PDO)
|
|
};
|
|
}
|
|
|
|
// ── ردود JSON موحدة ─────────────────────────────────────────
|
|
function jsonSuccess(mixed $data = null, string $message = 'success', int $code = 200): never
|
|
{
|
|
http_response_code($code);
|
|
// توحيد الأسلوب ليكون متوافقاً مع الكود القديم (وضع البيانات في message)
|
|
$payload = ($data !== null && (!empty($data) || is_array($data))) ? $data : $message;
|
|
echo json_encode(['status' => 'success', 'message' => $payload], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function jsonError(string $message, int $code = 400, mixed $extra = null): never
|
|
{
|
|
http_response_code($code);
|
|
$response = ['status' => 'failure', 'message' => $message];
|
|
if ($extra !== null) $response['details'] = $extra;
|
|
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
// (للتوافق مع الكود القديم)
|
|
function printSuccess(mixed $message = 'success'): void
|
|
{
|
|
echo json_encode(['status' => 'success', 'message' => $message], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
function printFailure(mixed $message = 'failure'): void
|
|
{
|
|
echo json_encode(['status' => 'failure', 'message' => $message], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
function result(int $count): void
|
|
{
|
|
if ($count > 0) {
|
|
printSuccess();
|
|
} else {
|
|
printFailure();
|
|
}
|
|
}
|
|
function sendEmail(string $from, string $to, string $title, string $body): void
|
|
{
|
|
$from = str_replace(["\r", "\n", "\r\n"], '', $from);
|
|
$to = str_replace(["\r", "\n", "\r\n"], '', $to);
|
|
$title = str_replace(["\r", "\n", "\r\n"], '', $title);
|
|
|
|
$header = "From: $from\r\n";
|
|
$header .= "Reply-To: $from\r\n";
|
|
$header .= "MIME-Version: 1.0\r\n";
|
|
$header .= "Content-Type: text/html; charset=UTF-8\r\n";
|
|
|
|
mail($to, $title, $body, $header);
|
|
}
|
|
|
|
// ── رفع صورة آمن ──────────────────────────────────────────────
|
|
function uploadImageSecure(
|
|
string $fileKey,
|
|
string $targetDir,
|
|
string $prefix = '',
|
|
array $allowedMimes = ['image/jpeg', 'image/png', 'image/webp']
|
|
): array {
|
|
if (!isset($_FILES[$fileKey]) || $_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK) {
|
|
return ['success' => false, 'error' => 'File upload error'];
|
|
}
|
|
|
|
$file = $_FILES[$fileKey];
|
|
$maxSize = 5 * 1024 * 1024; // 5MB
|
|
|
|
// حجم الملف
|
|
if ($file['size'] > $maxSize) {
|
|
return ['success' => false, 'error' => 'File too large (max 5MB)'];
|
|
}
|
|
|
|
// MIME validation حقيقي (ليس extension فقط)
|
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
|
$mimeType = $finfo->file($file['tmp_name']);
|
|
|
|
if (!in_array($mimeType, $allowedMimes, true)) {
|
|
return ['success' => false, 'error' => "Invalid file type: $mimeType"];
|
|
}
|
|
|
|
// اسم ملف آمن وعشوائي
|
|
$ext = match ($mimeType) {
|
|
'image/jpeg' => 'jpg',
|
|
'image/png' => 'png',
|
|
'image/webp' => 'webp',
|
|
default => 'bin',
|
|
};
|
|
$filename = ($prefix ? "{$prefix}_" : '') . bin2hex(random_bytes(8)) . ".$ext";
|
|
|
|
if (!is_dir($targetDir)) {
|
|
mkdir($targetDir, 0750, true);
|
|
}
|
|
|
|
$targetPath = rtrim($targetDir, '/') . '/' . $filename;
|
|
|
|
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
|
return ['success' => false, 'error' => 'Failed to move uploaded file'];
|
|
}
|
|
|
|
return ['success' => true, 'filename' => $filename, 'path' => $targetPath];
|
|
}
|
|
|
|
// ── تحميل ملف .env ───────────────────────────────────────────
|
|
function loadEnvironment(string $path): void
|
|
{
|
|
if (!file_exists($path)) {
|
|
error_log("[ENV] File not found: $path");
|
|
return;
|
|
}
|
|
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
if (str_starts_with(trim($line), '#')) continue;
|
|
if (!str_contains($line, '=')) continue;
|
|
[$key, $value] = explode('=', $line, 2);
|
|
$key = trim($key);
|
|
$value = trim($value, " \t\n\r\0\x0B\"'");
|
|
if ($key && !getenv($key)) {
|
|
putenv("$key=$value");
|
|
$_ENV[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Logging منظم ──────────────────────────────────────────────
|
|
function securityLog(string $message, array $context = []): void
|
|
{
|
|
$logDir = __DIR__ . '/../logs';
|
|
if (!is_dir($logDir)) {
|
|
@mkdir($logDir, 0750, true);
|
|
}
|
|
$entry = date('Y-m-d H:i:s') . ' [SECURITY] ' . $message;
|
|
if ($context) $entry .= ' | ' . json_encode($context, JSON_UNESCAPED_UNICODE);
|
|
@error_log($entry . PHP_EOL, 3, $logDir . '/security.log');
|
|
}
|
|
|
|
function appLog(string $message, string $level = 'INFO'): void
|
|
{
|
|
$logDir = __DIR__ . '/../logs';
|
|
if (!is_dir($logDir)) {
|
|
@mkdir($logDir, 0750, true);
|
|
}
|
|
$entry = date('Y-m-d H:i:s') . " [$level] " . $message;
|
|
@error_log($entry . PHP_EOL, 3, $logDir . '/app.log');
|
|
}
|
|
|
|
function uploadLog(string $message, string $level = 'INFO', array $context = []): void
|
|
{
|
|
$logDir = __DIR__ . '/../logs';
|
|
if (!is_dir($logDir)) {
|
|
@mkdir($logDir, 0750, true);
|
|
}
|
|
|
|
if (!isset($context['ip'])) {
|
|
$context['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
}
|
|
if (!isset($context['user_agent'])) {
|
|
$context['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
|
}
|
|
|
|
if (isset($context['upload_error_code'])) {
|
|
$errCode = $context['upload_error_code'];
|
|
$context['upload_error_desc'] = match ($errCode) {
|
|
UPLOAD_ERR_OK => 'UPLOAD_ERR_OK (0): No error, file uploaded successfully.',
|
|
UPLOAD_ERR_INI_SIZE => 'UPLOAD_ERR_INI_SIZE (1): The uploaded file exceeds the upload_max_filesize directive in php.ini.',
|
|
UPLOAD_ERR_FORM_SIZE => 'UPLOAD_ERR_FORM_SIZE (2): The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
|
|
UPLOAD_ERR_PARTIAL => 'UPLOAD_ERR_PARTIAL (3): The uploaded file was only partially uploaded (common on weak/3G networks).',
|
|
UPLOAD_ERR_NO_FILE => 'UPLOAD_ERR_NO_FILE (4): No file was uploaded.',
|
|
UPLOAD_ERR_NO_TMP_DIR => 'UPLOAD_ERR_NO_TMP_DIR (6): Missing a temporary folder.',
|
|
UPLOAD_ERR_CANT_WRITE => 'UPLOAD_ERR_CANT_WRITE (7): Failed to write file to disk.',
|
|
UPLOAD_ERR_EXTENSION => 'UPLOAD_ERR_EXTENSION (8): A PHP extension stopped the file upload.',
|
|
default => "Unknown upload error code: $errCode",
|
|
};
|
|
}
|
|
|
|
$entry = date('Y-m-d H:i:s') . " [$level] " . $message;
|
|
if ($context) {
|
|
$entry .= ' | ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
@error_log($entry . PHP_EOL, 3, $logDir . '/upload.log');
|
|
}
|
|
|
|
function debugLog(string $message): void
|
|
{
|
|
appLog($message, 'DEBUG');
|
|
}
|
|
|
|
function getInternalSocketKey(): string
|
|
{
|
|
$key = getenv('INTERNAL_SOCKET_KEY');
|
|
if ($key) {
|
|
return trim($key);
|
|
}
|
|
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/siro-api/.internal_socket_key';
|
|
if (file_exists($path)) {
|
|
return trim((string)@file_get_contents($path));
|
|
}
|
|
return '';
|
|
}
|
|
|