Files
tripz-llc/backend/scripts/backfill_blind_index.php
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00

240 lines
8.9 KiB
PHP

<?php
/**
* scripts/backfill_blind_index.php
*
* يملأ أعمدة الفهرس الأعمى للسجلات الموجودة.
*
* الاستخدام:
* php backfill_blind_index.php # تشغيل فعلي
* php backfill_blind_index.php --dry-run # عرض ما سيحدث دون كتابة
* php backfill_blind_index.php --table=driver --batch=200
*
* آمن للتشغيل والخدمة تعمل:
* - لا يقرأ ولا يكتب أي عمود مشفّر، يكتب أعمدة الفهرس فقط.
* - يعمل على دفعات مع فاصل قصير حتى لا يضغط القاعدة.
* - قابل لإعادة التشغيل: يتخطى الصفوف المملوءة ما لم يُمرَّر --force.
*/
declare(strict_types=1);
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("This script runs from the command line only.\n");
}
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../core/Security/BlindIndex.php';
$options = getopt('', ['dry-run', 'force', 'audit', 'verify::', 'table::', 'batch::']);
$dryRun = isset($options['dry-run']);
$force = isset($options['force']);
$only = $options['table'] ?? null;
$batch = max(50, (int) ($options['batch'] ?? 500));
/** @var EncryptionHelper $encryptionHelper */
global $encryptionHelper;
try {
$blind = new BlindIndex();
} catch (RuntimeException $e) {
exit("✘ " . $e->getMessage() . "\n");
}
$con = Database::get('main');
/**
* --verify=<قيمة>: يبحث عن رقم/بريد عبر الفهرس ويطبع النتيجة، للتأكد من أن
* البحث يعمل فعلاً بعد التعبئة دون فتح واجهة أو قاعدة بيانات.
* لا يطبع أي بيانات حساسة كاملة.
*/
if (isset($options['verify'])) {
$needle = (string) $options['verify'];
if ($needle === '') exit("Usage: --verify=0791234567\n");
$con = Database::get('main');
$found = false;
foreach ([['driver', 'driver.phone', 'phone_bidx'],
['driver', 'driver.email', 'email_bidx'],
['passengers', 'passengers.phone', 'phone_bidx'],
['passengers', 'passengers.email', 'email_bidx']] as [$table, $scope, $column]) {
$idx = $blind->index($scope, $needle);
if (!$idx) continue;
$st = $con->prepare("SELECT id FROM `$table` WHERE `$column` = ? LIMIT 5");
$st->execute([$idx]);
$ids = $st->fetchAll(PDO::FETCH_COLUMN);
if ($ids) {
$found = true;
echo "✔ $table via $column → " . count($ids) . " match(es): " . implode(', ', $ids) . "\n";
}
}
if (!$found) {
echo "✘ no match for that value in any indexed column.\n";
echo " Check that the value is correct and that the backfill has run.\n";
}
exit;
}
/**
* --audit: يقارن كل فهرس مخزَّن بالفهرس المحسوب من القيمة المشفّرة.
*
* الهدف كشف "الانحراف": صف عُدِّل هاتفه أو بريده عبر مسار كتابة لا يُحدّث
* الفهرس، فيصبح البحث يجد السجل بقيمته القديمة أو لا يجده إطلاقاً. الانحراف
* صامت بطبيعته، ولن يظهر إلا حين يفشل بحث حقيقي.
*/
if (isset($options['audit'])) {
$con = Database::get('main');
$checks = [
['driver', 'driver.phone', 'phone', 'phone_bidx'],
['driver', 'driver.email', 'email', 'email_bidx'],
['passengers', 'passengers.phone', 'phone', 'phone_bidx'],
['passengers', 'passengers.email', 'email', 'email_bidx'],
['adminUser', 'adminUser.phone', 'phone', 'phone_bidx'],
];
$problems = 0;
foreach ($checks as [$table, $scope, $source, $column]) {
try {
$rows = $con->query("SELECT id, `$source`, `$column` FROM `$table`")->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo " ! $table skipped: " . $e->getMessage() . "\n";
continue;
}
$stale = $missing = 0;
foreach ($rows as $row) {
$plain = $encryptionHelper->decryptData($row[$source] ?? null);
if ($plain === false || $plain === '') continue;
$expected = $blind->index($scope, $plain);
if ($expected === null) continue;
if (empty($row[$column])) $missing++;
elseif ($row[$column] !== $expected) $stale++;
}
$problems += $stale + $missing;
$verdict = ($stale + $missing) === 0 ? 'ok' : "MISSING=$missing STALE=$stale";
echo sprintf(" %-24s %s\n", "$table.$column", $verdict);
}
echo $problems === 0
? "\n✔ every index matches its encrypted value.\n"
: "\n✘ $problems row(s) out of sync — re-run with --force to rebuild.\n";
exit($problems === 0 ? 0 : 1);
}
$targets = [
'driver' => [
'phone_bidx' => ['scope' => 'driver.phone', 'columns' => ['phone']],
'email_bidx' => ['scope' => 'driver.email', 'columns' => ['email']],
'name_bidx' => ['scope' => 'driver.name', 'columns' => ['first_name', 'last_name']],
'national_bidx' => ['scope' => 'driver.national', 'columns' => ['national_number']],
// نفس نطاق otpPhoneKey: يسمح بربط جداول التحقق بصاحب الحساب
'phone_key' => ['scope' => 'otp.phone', 'columns' => ['phone'], 'prefix' => 'K:'],
],
'passengers' => [
'phone_bidx' => ['scope' => 'passengers.phone', 'columns' => ['phone']],
'email_bidx' => ['scope' => 'passengers.email', 'columns' => ['email']],
'name_bidx' => ['scope' => 'passengers.name', 'columns' => ['first_name', 'last_name']],
'phone_key' => ['scope' => 'otp.phone', 'columns' => ['phone'], 'prefix' => 'K:'],
],
'adminUser' => [
'phone_bidx' => ['scope' => 'adminUser.phone', 'columns' => ['phone']],
'email_bidx' => ['scope' => 'adminUser.email', 'columns' => ['email']],
],
'users' => [
'phone_bidx' => ['scope' => 'users.phone', 'columns' => ['phone']],
'email_bidx' => ['scope' => 'users.email', 'columns' => ['email']],
],
];
echo $dryRun ? "── DRY RUN — nothing will be written ──\n" : "── Backfilling blind indexes ──\n";
foreach ($targets as $table => $fields) {
if ($only && $only !== $table) continue;
echo "\n[$table]\n";
$sourceColumns = [];
foreach ($fields as $spec) {
foreach ($spec['columns'] as $c) $sourceColumns[$c] = true;
}
$select = 'id, ' . implode(', ', array_keys($sourceColumns));
$where = $force ? '' : ' WHERE ' . implode(' OR ', array_map(
fn($f) => "`$f` IS NULL",
array_keys($fields)
));
try {
$rows = $con->query("SELECT $select FROM `$table`$where")->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo " ✘ skipped: " . $e->getMessage() . "\n";
continue;
}
$total = count($rows);
echo " rows to process: $total\n";
if ($total === 0) continue;
$updated = 0;
$failed = 0;
foreach (array_chunk($rows, $batch) as $chunk) {
if (!$dryRun) $con->beginTransaction();
foreach ($chunk as $row) {
$set = [];
$params = [':id' => $row['id']];
foreach ($fields as $column => $spec) {
// فك التشفير لقراءة القيمة الأصلية — القيمة المخزَّنة لا تتغير.
$parts = [];
foreach ($spec['columns'] as $src) {
$plain = $encryptionHelper->decryptData($row[$src] ?? null);
if ($plain === false) {
$failed++;
$parts = [];
break;
}
if ($plain !== '') $parts[] = $plain;
}
if (!$parts) continue;
$value = implode(' ', $parts);
$index = $blind->index($spec['scope'], $value);
if ($index === null) continue;
if (!empty($spec['prefix'])) $index = $spec['prefix'] . $index;
$set[] = "`$column` = :$column";
$params[":$column"] = $index;
}
if (!$set) continue;
if ($dryRun) {
$updated++;
continue;
}
$stmt = $con->prepare("UPDATE `$table` SET " . implode(', ', $set) . " WHERE id = :id");
$stmt->execute($params);
$updated++;
}
if (!$dryRun) $con->commit();
usleep(50_000); // نفس متعمّد حتى لا تُحتكر القاعدة أثناء الخدمة
echo " … $updated/$total\n";
}
echo " ✔ indexed: $updated" . ($failed ? " (undecryptable values skipped: $failed)" : '') . "\n";
}
echo "\nDone." . ($dryRun ? " (dry run — re-run without --dry-run to apply)" : '') . "\n";