143 lines
6.2 KiB
PHP
143 lines
6.2 KiB
PHP
<?php
|
||
/**
|
||
* tools/env_registry.php — تصنيف مفاتيح البيئة وقراءة/كتابة .env
|
||
* ─────────────────────────────────────────────────────────────
|
||
* مصدر واحد للحقيقة تستعمله الأداة السطرية (env_keys.php) واللوحة
|
||
* (panel/index.php) معاً. نسختان من التصنيف كانتا ستتباعدان، وأخطر ما
|
||
* فيهما التباعد هو قائمة «المجمّدة» — مفتاح يسقط منها في نسخة يعني
|
||
* لوحةً تعرضه قابلاً للتعديل.
|
||
*/
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// الأصناف
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/** أسرار داخلية: طرفاها كلاهما لنا. التوليد والتدوير آمنان. */
|
||
const ENV_GENERATED = [
|
||
'S2S_SHARED_KEY' => 32,
|
||
'WEBHOOK_SECRET_KEY' => 32,
|
||
'INTERNAL_SOCKET_KEY' => 32,
|
||
'BOT_SECRET_KEY' => 24,
|
||
'FCM_INTERNAL_API_KEY' => 32,
|
||
'MIGRATION_ADMIN_KEY' => 24,
|
||
'PAYMENT_KEY' => 32,
|
||
];
|
||
|
||
/** تدويرها يبطل كل الجلسات القائمة — حدث تشغيلي واعٍ لا روتين. */
|
||
const ENV_SESSION_SCOPED = [
|
||
'JWT_SECRET_KEY' => 64,
|
||
'SECRET_KEY_HMAC' => 32,
|
||
];
|
||
|
||
/**
|
||
* ⛔ مجمّدة. بيانات مخزَّنة في القاعدة مشتقّة منها:
|
||
* ENC_KEY — أسماء الركّاب وهواتفهم ولوحات السيارات مشفّرة به
|
||
* BLIND_INDEX_PEPPER — أعمدة *_bidx محسوبة به ومخزَّنة
|
||
* TRACKING_SECRET_SALT / FP_PEPPER — قيم مشتقّة مخزَّنة
|
||
*
|
||
* تغييرها لا يعيد تشفير شيء: يجعل الصفوف القديمة غير قابلة للقراءة
|
||
* إلى الأبد، ولا نسخة احتياطية تنفع لأنها مشفّرة بالمفتاح القديم.
|
||
* تدويرها ترحيلُ بيانات لا تغييرُ إعداد.
|
||
*
|
||
* اللوحة تعرض حالتها فقط — لا قيمتها ولا حقل تعديل.
|
||
*/
|
||
const ENV_FROZEN = [
|
||
'ENC_KEY', 'ENCRYPTION_KEY_PATH', 'ENCRYPTION_MODE',
|
||
'BLIND_INDEX_PEPPER', 'TRACKING_SECRET_SALT', 'FP_PEPPER',
|
||
];
|
||
|
||
/** لا تُولَّد — تُنسخ من لوحة المزوّد. قابلة للتحرير يدوياً. */
|
||
const ENV_EXTERNAL = [
|
||
'GEMINI_API_KEY', 'NABEH_API_KEY', 'NABEH_PASSWORD', 'NABEH_OTP_APP_KEY',
|
||
'ELEVENLABS_API_KEY', 'CREATOMATE_API_KEY', 'SOCIAL_BOT_TOKEN',
|
||
'MAIL_PASSWORD', 'SIRO_SMTP_PASSWORD', 'SMS_PASSWORD_EGYPT',
|
||
'VOICE_CALL_API_KEY', 'RASEEL_DRIVER_ACCESS_TOKEN',
|
||
];
|
||
|
||
/**
|
||
* كلمات مرور القواعد ← اسم المستخدم ومتغيّر اسم القاعدة.
|
||
*
|
||
* تغييرها في .env وحده لا يفعل شيئاً: متغيّرات صورة mysql تُقرأ مرة
|
||
* واحدة عند تهيئة مجلّد بيانات فارغ، وبعدها كلمة المرور تعيش في
|
||
* mysql.user. الترتيب الصحيح ALTER USER ثم .env — واللوحة تفعله
|
||
* بهذا الترتيب وتتحقّق قبل أن تكتب.
|
||
*/
|
||
const ENV_DB_CREDENTIALS = [
|
||
'DB_PRIMARY_PASS_V2' => ['user' => 'DB_PRIMARY_USER_V2', 'db' => 'DB_PRIMARY_NAME_V2'],
|
||
'DB_PRIMARY_PASS' => ['user' => 'DB_PRIMARY_USER', 'db' => 'DB_PRIMARY_NAME'],
|
||
'DB_TRACKING_PASS' => ['user' => 'DB_TRACKING_USER', 'db' => 'DB_TRACKING_NAME'],
|
||
'DB_RIDE_PASS' => ['user' => 'DB_RIDE_USER', 'db' => 'DB_RIDE_NAME'],
|
||
'DB_TRANSIT_PASS' => ['user' => 'DB_TRANSIT_USER', 'db' => 'DB_TRANSIT_NAME'],
|
||
'DB_FOOD_PASS' => ['user' => 'DB_FOOD_USER', 'db' => 'DB_FOOD_NAME'],
|
||
];
|
||
|
||
/** يقرأ .env إلى مصفوفة. */
|
||
function envRead(string $file): array
|
||
{
|
||
if (!is_readable($file)) return [];
|
||
$out = [];
|
||
foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) {
|
||
if (preg_match('/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/', $line, $m)) {
|
||
$out[$m[1]] = trim($m[2], "\"' ");
|
||
}
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/** قيمة نائبة: موجودة شكلاً وغائبة فعلاً. */
|
||
function envIsPlaceholder(?string $v): bool
|
||
{
|
||
if ($v === null || $v === '') return true;
|
||
return (bool) preg_match('/^(<?CHANGE_ME|changeme|your[_-]|xxx+|TODO|REPLACE)/i', $v);
|
||
}
|
||
|
||
/**
|
||
* يكتب مفاتيح في .env مع نسخة احتياطية.
|
||
*
|
||
* الاستبدال في المكان يبقي التعليقات المجاورة مفيدة، والمفقود يُلحق
|
||
* في النهاية.
|
||
*
|
||
* @return string|null مسار النسخة الاحتياطية، أو null عند الفشل.
|
||
*/
|
||
function envWrite(string $file, array $changes): ?string
|
||
{
|
||
if (empty($changes)) return null;
|
||
|
||
// .env التالف يوقف كل شيء، وهو خارج git بطبيعته فلا مصدر لاستعادته.
|
||
$backup = $file . '.bak.' . date('Ymd_His');
|
||
if (!copy($file, $backup)) return null;
|
||
@chmod($backup, 0600);
|
||
|
||
$out = rtrim((string) file_get_contents($file), "\n") . "\n";
|
||
$append = [];
|
||
|
||
foreach ($changes as $name => $value) {
|
||
$line = "$name=$value";
|
||
if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=/m', $out)) {
|
||
$out = preg_replace(
|
||
'/^\s*' . preg_quote($name, '/') . '\s*=.*$/m',
|
||
str_replace('$', '\\$', $line),
|
||
$out,
|
||
1
|
||
);
|
||
} else {
|
||
$append[] = $line;
|
||
}
|
||
}
|
||
|
||
if ($append) {
|
||
$out .= "\n# ── " . date('Y-m-d H:i') . " ──\n" . implode("\n", $append) . "\n";
|
||
}
|
||
|
||
if (file_put_contents($file, $out) === false) return null;
|
||
@chmod($file, 0600);
|
||
|
||
return $backup;
|
||
}
|
||
|
||
/** سرّ عشوائي بصيغة hex. */
|
||
function envGenerate(int $bytes = 32): string
|
||
{
|
||
return bin2hex(random_bytes($bytes));
|
||
}
|