Files
2026-08-09 16:56:13 +03:00

162 lines
7.5 KiB
PHP

<?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";