Update codebase
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* tools/env_keys.php — جرد مفاتيح البيئة وزراعة الناقص منها
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* الاستعمال:
|
||||
* php env_keys.php audit # جرد فقط، لا يكتب شيئاً
|
||||
* php env_keys.php seed # يزرع الناقص القابل للتوليد
|
||||
* php env_keys.php seed --dry-run # يُظهر ما سيفعله بلا كتابة
|
||||
*
|
||||
* المفاتيح ليست صنفاً واحداً، والخلط بينها هو ما يحوّل «تدوير مفتاح»
|
||||
* إلى فقدان بيانات لا رجعة فيه. أربعة أصناف:
|
||||
*
|
||||
* • generated — سرّ بيننا وبين أنفسنا. نولّده ونُدوّره متى شئنا.
|
||||
* • session — تدويره يُخرج كل المستخدمين من حساباتهم. مقبول بوعي.
|
||||
* • frozen — **لا يُدوَّر إطلاقاً**. بيانات مخزَّنة مشتقّة منه،
|
||||
* وتغييره يجعلها غير قابلة للقراءة إلى الأبد.
|
||||
* • external — من طرف ثالث. لا يمكن توليده، يُنسخ من لوحته.
|
||||
*/
|
||||
|
||||
$root = dirname(__DIR__, 2);
|
||||
$envFile = getenv('ENV_FILE_PATH') ?: "$root/docker/.env";
|
||||
|
||||
$command = $argv[1] ?? 'audit';
|
||||
$dryRun = in_array('--dry-run', $argv, true);
|
||||
|
||||
// التصنيف كله في env_registry.php — مصدر واحد تشترك فيه هذه الأداة
|
||||
// واللوحة. نسختان منه كانتا ستتباعدان، وأخطر تباعد هو قائمة المجمّدة:
|
||||
// مفتاح يسقط منها في نسخة يعني لوحةً تعرضه قابلاً للتعديل.
|
||||
require_once __DIR__ . '/env_registry.php';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// القراءة
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
if (!is_readable($envFile)) {
|
||||
fwrite(STDERR, "لا يمكن قراءة $envFile\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES);
|
||||
$present = [];
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/', $line, $m)) {
|
||||
$present[$m[1]] = trim($m[2], "\"' ");
|
||||
}
|
||||
}
|
||||
|
||||
/** قيمة نائبة لا سرّاً: موجودة شكلاً وغائبة فعلاً. */
|
||||
$isPlaceholder = static function (string $v): bool {
|
||||
if ($v === '') return true;
|
||||
return (bool) preg_match('/^(<?CHANGE_ME|changeme|your[_-]|xxx+|TODO|REPLACE)/i', $v);
|
||||
};
|
||||
|
||||
echo "ملف البيئة: $envFile\n";
|
||||
echo str_repeat('─', 62) . "\n";
|
||||
|
||||
$toSeed = [];
|
||||
|
||||
$report = static function (string $title, array $keys, array $present,
|
||||
callable $isPlaceholder, ?array &$toSeed = null,
|
||||
bool $seedable = false) {
|
||||
echo "\n▸ $title\n";
|
||||
foreach ($keys as $k => $v) {
|
||||
$name = is_int($k) ? $v : $k;
|
||||
$bytes = is_int($k) ? 0 : $v;
|
||||
|
||||
$exists = array_key_exists($name, $present);
|
||||
$blank = $exists && $isPlaceholder($present[$name]);
|
||||
|
||||
$state = !$exists ? 'مفقود' : ($blank ? 'نائب' : 'مضبوط');
|
||||
printf(" %-30s %s\n", $name, $state);
|
||||
|
||||
if ($seedable && ($state !== 'مضبوط') && $toSeed !== null) {
|
||||
$toSeed[$name] = $bytes ?: 32;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$report('أسرار داخلية (تُولَّد وتُدوَّر بحرية)', ENV_GENERATED, $present, $isPlaceholder, $toSeed, true);
|
||||
$report('مرتبطة بالجلسات (تدويرها يُخرج الجميع)', ENV_SESSION_SCOPED, $present, $isPlaceholder, $toSeed, true);
|
||||
$report('⛔ مجمّدة — لا تُدوَّر (بيانات مخزَّنة مشتقّة منها)', ENV_FROZEN, $present, $isPlaceholder);
|
||||
$report('اعتماد قواعد البيانات (يُغيَّر في MySQL أولاً لا هنا)', ENV_DB_CREDENTIALS, $present, $isPlaceholder);
|
||||
$report('طرف ثالث (تُنسخ من لوحة المزوّد)', ENV_EXTERNAL, $present, $isPlaceholder);
|
||||
|
||||
echo "\n" . str_repeat('─', 62) . "\n";
|
||||
|
||||
if ($command !== 'seed') {
|
||||
echo empty($toSeed)
|
||||
? "لا ينقص شيء قابل للتوليد.\n"
|
||||
: 'قابل للزراعة: ' . count($toSeed) . " مفتاحاً. شغّل: php env_keys.php seed\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// الزراعة
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
if (empty($toSeed)) {
|
||||
echo "لا ينقص شيء قابل للتوليد.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// لا نلمس مفتاحاً مضبوطاً أبداً: هذه الأداة تملأ الفراغ فقط. التدوير
|
||||
// قرار واعٍ له تبعات (جلسات، خوادم أخرى تحمل نفس السرّ)، ولا يجوز أن
|
||||
// يقع كأثر جانبي لتشغيل أداة جرد.
|
||||
$additions = [];
|
||||
foreach ($toSeed as $name => $bytes) {
|
||||
$value = bin2hex(random_bytes($bytes));
|
||||
$additions[$name] = $value;
|
||||
echo "توليد $name (" . ($bytes * 2) . " حرفاً)\n";
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
echo "\n[dry-run] لم يُكتب شيء.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// نسخة احتياطية قبل أي كتابة. ملف .env تالف يعني توقف كل شيء، وهو
|
||||
// غير موجود في git بطبيعته فلا مصدر آخر لاستعادته.
|
||||
$backup = $envFile . '.bak.' . date('Ymd_His');
|
||||
if (!copy($envFile, $backup)) {
|
||||
fwrite(STDERR, "تعذّرت النسخة الاحتياطية — أُلغيت العملية.\n");
|
||||
exit(1);
|
||||
}
|
||||
@chmod($backup, 0600);
|
||||
echo "\nنسخة احتياطية: $backup\n";
|
||||
|
||||
$out = rtrim(file_get_contents($envFile), "\n") . "\n";
|
||||
|
||||
// نستبدل السطر النائب في مكانه، ونُلحق المفقود في النهاية — الاستبدال
|
||||
// في المكان يبقي التعليقات المجاورة مفيدة.
|
||||
foreach ($additions as $name => $value) {
|
||||
if (array_key_exists($name, $present)) {
|
||||
$out = preg_replace(
|
||||
'/^\s*' . preg_quote($name, '/') . '\s*=.*$/m',
|
||||
"$name=$value",
|
||||
$out,
|
||||
1
|
||||
);
|
||||
unset($additions[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($additions)) {
|
||||
$out .= "\n# ── زُرعت تلقائياً بـ tools/env_keys.php في " . date('Y-m-d H:i') . " ──\n";
|
||||
foreach ($additions as $name => $value) {
|
||||
$out .= "$name=$value\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (file_put_contents($envFile, $out) === false) {
|
||||
fwrite(STDERR, "تعذّرت الكتابة. النسخة الاحتياطية سليمة: $backup\n");
|
||||
exit(1);
|
||||
}
|
||||
@chmod($envFile, 0600);
|
||||
|
||||
echo "\nتمّت الزراعة.\n";
|
||||
echo "الخطوة التالية — أعد تشغيل حاويات php لتقرأ الجديد:\n";
|
||||
echo " docker compose restart php php_food php_transit\n\n";
|
||||
echo "تذكير: WEBHOOK_SECRET_KEY يجب أن يطابق RELAY_SECRET في\n";
|
||||
echo "sms_relay/app/build.gradle.kts — وهو يحتاج إعادة بناء للتطبيق.\n";
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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));
|
||||
}
|
||||
Reference in New Issue
Block a user