$subject) { foreach (array_keys($subject['semesters'] ?? []) as $semesterKey) { $knownSemesters[$subjectKey][$semesterKey] = true; } } $subjectPatterns = [ 'arabic_10' => ['اللغة العربية', 'العربية لغتي'], 'english_10' => ['اللغة الإنجليزية'], 'math_10' => ['الرياضيات'], 'physics_10' => ['الفيزياء'], 'chemistry_10' => ['الكيمياء'], 'biology_10' => ['العلوم الحياتية'], 'earth_science_10' => ['علوم الأرض والبيئة'], 'digital_skills_10' => ['المهارات الرقمية'], 'islamic_10' => ['التربية الإسلامية'], 'history_10' => ['التاريخ'], 'geography_10' => ['الجغرافيا'], 'civic_10' => ['التربية الوطنية والمدنية'], 'financial_literacy_10' => ['الثقافة المالية'], ]; $rows = []; foreach (glob($sourceRoot . '/*.pdf') ?: [] as $sourcePath) { $filename = basename($sourcePath); $subjectKey = null; foreach ($subjectPatterns as $candidate => $patterns) { foreach ($patterns as $pattern) { if (str_contains($filename, $pattern)) { $subjectKey = $candidate; break 2; } } } $semesterKey = str_contains($filename, 'الفصل الأول') ? 'semester_1' : (str_contains($filename, 'الفصل الثاني') ? 'semester_2' : null); $isInstitutionalBook = str_starts_with($filename, 'كتاب الطالب') || str_starts_with($filename, 'كتاب التمارين'); $sha256 = hash_file('sha256', $sourcePath); $safeName = preg_replace('/[^A-Za-z0-9._-]+/', '-', pathinfo($filename, PATHINFO_FILENAME)) ?: 'book'; // Arabic filenames normalize to "book" with the ASCII-only fallback; // append integrity bytes so no two supplied books can overwrite each other. $storageKey = sprintf('sources/grade_10/%s/%s/%s-%s.pdf', $subjectKey ?: 'unclassified', $semesterKey ?: 'unclassified', trim($safeName, '-'), substr($sha256, 0, 12)); $rows[] = [ 'filename' => $filename, 'source_path' => $sourcePath, 'subject_key' => $subjectKey, 'semester_key' => $semesterKey, 'asset_type' => $isInstitutionalBook ? 'textbook_pdf' : 'other', 'pages' => pdfPages($sourcePath), 'byte_size' => filesize($sourcePath), 'sha256' => $sha256, 'storage_key' => $storageKey, 'manifest_scope_exists' => $subjectKey !== null && $semesterKey !== null && isset($knownSemesters[$subjectKey][$semesterKey]), 'intake_status' => !$isInstitutionalBook ? 'manual_rights_and_academic_review_required' : ($subjectKey === null || $semesterKey === null ? 'unclassified_metadata' : 'ready_for_source_review'), ]; } usort($rows, static fn(array $a, array $b): int => strcmp($a['filename'], $b['filename'])); $report = [ 'mode' => $apply ? 'apply' : 'dry_run', 'policy' => 'Draft source registration only; no publication or rights clearance.', 'books_found' => count($rows), 'manifest_supported_sources' => count(array_filter($rows, static fn(array $row): bool => $row['manifest_scope_exists'])), 'sources_outside_current_manifest' => count(array_filter($rows, static fn(array $row): bool => !$row['manifest_scope_exists'])), 'rows' => $rows, ]; if (!$apply) { echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; exit(0); } require_once dirname(__DIR__) . '/app/bootstrap.php'; $pdo = Database::getConnection(); $pdo->beginTransaction(); try { foreach ($rows as $row) { if ($row['asset_type'] !== 'textbook_pdf' || $row['subject_key'] === null || $row['semester_key'] === null) { continue; } $destination = $curriculumRoot . '/' . $row['storage_key']; if (!is_dir(dirname($destination)) && !mkdir(dirname($destination), 0750, true) && !is_dir(dirname($destination))) { throw new RuntimeException('Unable to create textbook storage directory.'); } if (!is_file($destination)) { if (!copy($row['source_path'], $destination)) { throw new RuntimeException('Unable to copy textbook source: ' . $row['filename']); } } if (!hash_equals($row['sha256'], hash_file('sha256', $destination))) { throw new RuntimeException('Copied textbook checksum mismatch: ' . $row['filename']); } Database::query( "INSERT INTO content_assets (uuid, asset_type, storage_driver, storage_key, mime_type, byte_size, sha256, source_reference, rights_status, review_status) VALUES (?, 'textbook_pdf', 'local', ?, 'application/pdf', ?, ?, ?, 'review_required', 'draft') ON DUPLICATE KEY UPDATE byte_size=VALUES(byte_size), source_reference=VALUES(source_reference)", [uuid(), $row['storage_key'], $row['byte_size'], $row['sha256'], 'books/' . $row['filename']] ); } $pdo->commit(); $report['registered_draft_sources'] = true; echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } fwrite(STDERR, "Source import rolled back: {$e->getMessage()}\n"); exit(1); } function pdfPages(string $path): ?int { $output = []; $status = 0; exec('pdfinfo ' . escapeshellarg($path) . ' 2>/dev/null', $output, $status); if ($status !== 0) { return null; } foreach ($output as $line) { if (preg_match('/^Pages:\s+(\d+)$/', $line, $match)) { return (int) $match[1]; } } return null; } function uuid(): string { $bytes = random_bytes(16); $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4)); }