'chemistry_10', 'الرياضيات' => 'math_10', 'الفيزياء' => 'physics_10', 'الأحياء' => 'biology_10', 'حياتية' => 'biology_10', 'علوم الأرض' => 'earth_sciences_10', 'اللغة العربية' => 'arabic_10', 'العربية' => 'arabic_10', 'الإنجليزية' => 'english_10', 'الانجليزية' => 'english_10', 'التربية الإسلامية' => 'islamic_10', 'الإسلامية' => 'islamic_10', 'تاريخ' => 'history_10', 'جغرافيا' => 'geography_10', 'الوطنية' => 'civics_10', 'المالية' => 'financial_10', 'الرقمية' => 'digital_skills_10', 'حاسوب' => 'digital_skills_10', ]; if (!is_dir($booksRoot)) { fwrite(STDERR, "Books directory is missing.\n"); exit(1); } $ready = []; $blocked = []; foreach (glob($booksRoot . '/*.pdf') ?: [] as $filePath) { $filename = basename($filePath); if (!str_contains($filename, 'كتاب الطالب')) { echo "SKIP {$filename}: not a student textbook\n"; continue; } $subject = null; foreach ($subjectMap as $keyword => $key) { if (str_contains($filename, $keyword)) { $subject = $key; break; } } if ($subject === null) { $blocked[] = "{$filename}: unknown subject"; continue; } if (str_contains($filename, 'الفصل الثاني')) { $semester = 'semester_2'; } elseif (str_contains($filename, 'الفصل الأول')) { $semester = 'semester_1'; } else { $blocked[] = "{$filename}: explicit semester is missing"; continue; } $sha = hash_file('sha256', $filePath); if ($sha === false) { $blocked[] = "{$filename}: unreadable source"; continue; } $asset = Database::selectOne( "SELECT id, storage_driver, storage_key, sha256, review_status, rights_status, source_reference FROM content_assets WHERE sha256 = ? AND asset_type = 'textbook_pdf' LIMIT 1", [$sha] ); if (!$asset || $asset['review_status'] !== 'approved' || $asset['rights_status'] !== 'cleared' || trim((string)$asset['source_reference']) === '') { $blocked[] = "{$filename}: reviewed textbook asset and rights evidence are missing"; continue; } if ($asset['storage_driver'] !== 'local') { $blocked[] = "{$filename}: verify the remote asset with its storage provider before linking"; continue; } $storagePath = realpath($curriculumRoot . '/' . $asset['storage_key']); $rootPath = realpath($curriculumRoot); if ($storagePath === false || $rootPath === false || !str_starts_with($storagePath, $rootPath . DIRECTORY_SEPARATOR) || !hash_equals($sha, (string)hash_file('sha256', $storagePath))) { $blocked[] = "{$filename}: approved stored file is missing or its checksum differs"; continue; } $bundle = Database::selectOne( "SELECT pb.id FROM publication_bundles pb JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id WHERE cl.grade_key = 'grade_10' AND cl.subject_key = ? AND cl.semester_key = ? AND cl.source_status = 'approved' AND pb.status = 'published' ORDER BY cl.unit_key, cl.lesson_key, pb.id LIMIT 1", [$subject, $semester] ); if (!$bundle) { $blocked[] = "{$filename}: no published bundle for {$subject}/{$semester}"; continue; } $ready[] = ['name' => $filename, 'bundle_id' => (int)$bundle['id'], 'asset_id' => (int)$asset['id']]; } foreach ($blocked as $reason) echo "BLOCKED {$reason}\n"; foreach ($ready as $row) echo "READY {$row['name']} => bundle {$row['bundle_id']}\n"; if (!$apply) { echo "DRY RUN: " . count($ready) . " ready, " . count($blocked) . " blocked.\n"; exit($blocked === [] ? 0 : 2); } if ($blocked !== []) { fwrite(STDERR, "REFUSE: resolve all textbook review and matching blockers before --apply.\n"); exit(2); } if ($ready === []) { fwrite(STDERR, "REFUSE: no reviewed textbook to link.\n"); exit(2); } $pdo = Database::getConnection(); $pdo->beginTransaction(); try { foreach ($ready as $row) { $current = Database::selectOne( "SELECT pb.status, cl.source_status, a.review_status, a.rights_status FROM publication_bundles pb JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id JOIN content_assets a ON a.id = ? WHERE pb.id = ? FOR UPDATE", [$row['asset_id'], $row['bundle_id']] ); if (!$current || $current['status'] !== 'published' || $current['source_status'] !== 'approved' || $current['review_status'] !== 'approved' || $current['rights_status'] !== 'cleared') { throw new RuntimeException('Approval changed while linking textbooks.'); } Database::query( "INSERT INTO publication_bundle_assets (publication_bundle_id, content_asset_id, role, sort_order) VALUES (?, ?, 'textbook', 1) ON DUPLICATE KEY UPDATE sort_order = VALUES(sort_order)", [$row['bundle_id'], $row['asset_id']] ); } $pdo->commit(); echo "Linked " . count($ready) . " reviewed textbooks.\n"; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); fwrite(STDERR, "FAILED: {$e->getMessage()}\n"); exit(1); }