172 lines
7.0 KiB
PHP
172 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Registers supplied Grade 10 textbook PDFs as review-only source assets.
|
|
*
|
|
* Default mode is read-only. --apply copies PDFs into curriculum storage and
|
|
* creates draft, rights-review-required content_assets records. It never
|
|
* approves rights, publishes a bundle, or invents curriculum lessons.
|
|
*
|
|
* Usage:
|
|
* php backend/scripts/import_grade10_book_sources.php
|
|
* php backend/scripts/import_grade10_book_sources.php --apply
|
|
*/
|
|
|
|
use App\Core\Database;
|
|
|
|
$apply = in_array('--apply', $argv, true);
|
|
$projectRoot = dirname(__DIR__, 2);
|
|
$sourceRoot = $projectRoot . '/books';
|
|
$curriculumRoot = $projectRoot . '/backend/storage/curriculum';
|
|
$manifestPath = $curriculumRoot . '/manifest.json';
|
|
|
|
if (!is_dir($sourceRoot) || !is_file($manifestPath)) {
|
|
fwrite(STDERR, "Books directory or curriculum manifest is unavailable.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$manifest = json_decode((string) file_get_contents($manifestPath), true);
|
|
if (!is_array($manifest)) {
|
|
fwrite(STDERR, "Curriculum manifest is not valid JSON.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$knownSemesters = [];
|
|
foreach (($manifest['grade_10']['subjects'] ?? []) as $subjectKey => $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));
|
|
}
|