39301-vm/includes/library.php
2026-03-25 08:05:46 +00:00

392 lines
12 KiB
PHP

<?php
declare(strict_types=1);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
require_once __DIR__ . '/../db/config.php';
function library_bootstrap(): void
{
static $booted = false;
if ($booted) {
return;
}
$migrationPath = __DIR__ . '/../db/migrations/001_library_documents.sql';
if (is_file($migrationPath)) {
$sql = file_get_contents($migrationPath);
if (is_string($sql) && trim($sql) !== '') {
db()->exec($sql);
}
}
// Run new migrations if needed
$migration2Path = __DIR__ . '/../db/migrations/002_add_library_metadata.sql';
if (is_file($migration2Path)) {
// Simple check if columns exist
$exists = db()->query("SHOW COLUMNS FROM library_documents LIKE 'category_ar'")->fetch();
if (!$exists) {
$sql = file_get_contents($migration2Path);
db()->exec($sql);
}
}
$migration3Path = __DIR__ . '/../db/migrations/003_normalize_categories.sql';
if (is_file($migration3Path)) {
// Simple check if table exists
$exists = db()->query("SHOW TABLES LIKE 'library_categories'")->fetch();
if (!$exists) {
$sql = file_get_contents($migration3Path);
db()->exec($sql);
}
}
$uploadDir = __DIR__ . '/../uploads/library';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
library_seed_demo_documents();
$booted = true;
}
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function library_project_meta(): array
{
return [
'name' => $_SERVER['PROJECT_NAME'] ?? 'Nabd Library',
'description' => $_SERVER['PROJECT_DESCRIPTION'] ?? 'Bilingual electronic library for Arabic and English documents, online reading, and AI-assisted summaries.',
'image' => $_SERVER['PROJECT_IMAGE_URL'] ?? '',
];
}
function library_set_flash(string $type, string $message): void
{
$_SESSION['library_flash'][] = ['type' => $type, 'message' => $message];
}
function library_get_flashes(): array
{
$flashes = $_SESSION['library_flash'] ?? [];
unset($_SESSION['library_flash']);
return is_array($flashes) ? $flashes : [];
}
function library_seed_demo_documents(): void
{
$count = (int) (db()->query('SELECT COUNT(*) FROM library_documents')->fetchColumn() ?: 0);
if ($count > 0) {
return;
}
$pdfRelative = 'uploads/library/demo-library-guide.pdf';
$txtRelative = 'uploads/library/demo-bilingual-notes.txt';
$pdfAbsolute = __DIR__ . '/../' . $pdfRelative;
$txtAbsolute = __DIR__ . '/../' . $txtRelative;
}
function library_old(string $key, string $default = ''): string
{
return isset($_POST[$key]) ? trim((string) $_POST[$key]) : $default;
}
function library_document_type_label(string $type): string
{
$map = [
'pdf' => 'PDF reader',
'txt' => 'Text note',
'doc' => 'Word document',
'docx' => 'Word document',
'ppt' => 'PowerPoint',
'pptx' => 'PowerPoint',
];
return $map[strtolower($type)] ?? strtoupper($type);
}
function library_language_label(string $lang): string
{
$map = [
'en' => 'English',
'ar' => 'Arabic',
'bilingual' => 'Bilingual',
];
return $map[$lang] ?? 'Unknown';
}
function library_visibility_label(string $visibility): string
{
return $visibility === 'private' ? 'Private / login' : 'Public';
}
function library_allowed_extensions(): array
{
return [
'pdf' => 'PDF reader',
'txt' => 'Text note',
'doc' => 'Word document',
'docx' => 'Word document',
'ppt' => 'PowerPoint',
'pptx' => 'PowerPoint',
];
}
// --- Category Functions ---
function library_get_categories(): array
{
library_bootstrap();
$stmt = db()->query('SELECT * FROM library_categories ORDER BY name_en ASC');
return $stmt ? $stmt->fetchAll() : [];
}
function library_get_subcategories(?int $categoryId = null): array
{
library_bootstrap();
if ($categoryId !== null) {
$stmt = db()->prepare('SELECT * FROM library_subcategories WHERE category_id = ? ORDER BY name_en ASC');
$stmt->execute([$categoryId]);
return $stmt->fetchAll() ?: [];
}
$stmt = db()->query('SELECT * FROM library_subcategories ORDER BY name_en ASC');
return $stmt ? $stmt->fetchAll() : [];
}
function library_create_category(string $nameEn, string $nameAr): int
{
library_bootstrap();
$stmt = db()->prepare('INSERT INTO library_categories (name_en, name_ar) VALUES (?, ?)');
$stmt->execute([$nameEn, $nameAr]);
return (int) db()->lastInsertId();
}
function library_create_subcategory(int $categoryId, string $nameEn, string $nameAr): int
{
library_bootstrap();
$stmt = db()->prepare('INSERT INTO library_subcategories (category_id, name_en, name_ar) VALUES (?, ?, ?)');
$stmt->execute([$categoryId, $nameEn, $nameAr]);
return (int) db()->lastInsertId();
}
function library_get_category_by_id(int $id): ?array
{
library_bootstrap();
$stmt = db()->prepare('SELECT * FROM library_categories WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch() ?: null;
}
function library_get_subcategory_by_id(int $id): ?array
{
library_bootstrap();
$stmt = db()->prepare('SELECT * FROM library_subcategories WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch() ?: null;
}
function library_update_category(int $id, string $nameEn, string $nameAr): void
{
library_bootstrap();
$stmt = db()->prepare('UPDATE library_categories SET name_en = ?, name_ar = ? WHERE id = ?');
$stmt->execute([$nameEn, $nameAr, $id]);
}
function library_delete_category(int $id): void
{
library_bootstrap();
$stmt = db()->prepare('DELETE FROM library_categories WHERE id = ?');
$stmt->execute([$id]);
}
function library_update_subcategory(int $id, int $categoryId, string $nameEn, string $nameAr): void
{
library_bootstrap();
$stmt = db()->prepare('UPDATE library_subcategories SET category_id = ?, name_en = ?, name_ar = ? WHERE id = ?');
$stmt->execute([$categoryId, $nameEn, $nameAr, $id]);
}
function library_delete_subcategory(int $id): void
{
library_bootstrap();
$stmt = db()->prepare('DELETE FROM library_subcategories WHERE id = ?');
$stmt->execute([$id]);
}
// --- End Category Functions ---
function library_fetch_documents(bool $publicOnly = false, array $filters = []): array
{
library_bootstrap();
$sql = 'SELECT d.*, c.name_en as cat_en, c.name_ar as cat_ar, sc.name_en as sub_en, sc.name_ar as sub_ar
FROM library_documents d
LEFT JOIN library_categories c ON d.category_id = c.id
LEFT JOIN library_subcategories sc ON d.subcategory_id = sc.id
WHERE 1=1';
$params = [];
if ($publicOnly) {
$sql .= ' AND d.visibility = :visibility';
$params[':visibility'] = 'public';
}
$sql .= ' ORDER BY d.is_featured DESC, d.created_at DESC';
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
return $stmt->fetchAll() ?: [];
}
function library_recent_documents(int $limit = 3, bool $publicOnly = false): array
{
library_bootstrap();
$sql = 'SELECT * FROM library_documents WHERE 1=1';
if ($publicOnly) {
$sql .= ' AND visibility = "public"';
}
$sql .= ' ORDER BY created_at DESC LIMIT ' . (int)$limit;
$stmt = db()->query($sql);
return $stmt ? $stmt->fetchAll() : [];
}
function library_catalog_metrics(): array
{
library_bootstrap();
$sql = 'SELECT
COUNT(*) AS total_count,
SUM(CASE WHEN visibility = "public" THEN 1 ELSE 0 END) AS public_count,
SUM(CASE WHEN visibility = "private" THEN 1 ELSE 0 END) AS private_count,
SUM(CASE WHEN summary_text IS NOT NULL THEN 1 ELSE 0 END) AS summarized_count
FROM library_documents';
$row = db()->query($sql)->fetch() ?: [];
return [
'total_count' => (int) ($row['total_count'] ?? 0),
'public_count' => (int) ($row['public_count'] ?? 0),
'private_count' => (int) ($row['private_count'] ?? 0),
'summarized_count' => (int) ($row['summarized_count'] ?? 0),
];
}
function library_handle_uploaded_file(array $file): array
{
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
throw new RuntimeException('Please upload a document file.');
}
$originalName = (string) ($file['name'] ?? '');
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$allowed = library_allowed_extensions();
if (!isset($allowed[$extension])) {
throw new RuntimeException('Unsupported file type.');
}
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > 12 * 1024 * 1024) {
throw new RuntimeException('File must be smaller than 12 MB.');
}
$safeBase = preg_replace('/[^a-zA-Z0-9_-]+/', '-', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'document';
$storedName = strtolower(date('YmdHis') . '-' . $safeBase . '-' . bin2hex(random_bytes(4)) . '.' . $extension);
$relativePath = 'uploads/library/' . $storedName;
$absolutePath = __DIR__ . '/../' . $relativePath;
if (!move_uploaded_file((string) $file['tmp_name'], $absolutePath)) {
throw new RuntimeException('Unable to save the uploaded file.');
}
return [
'file_name' => $originalName,
'file_path' => $relativePath,
'document_type' => $extension,
'file_size_kb' => (int) ceil($size / 1024),
];
}
function library_create_document(array $payload, array $file): int
{
library_bootstrap();
$titleEn = trim((string) ($payload['title_en'] ?? ''));
$titleAr = trim((string) ($payload['title_ar'] ?? ''));
// Process IDs
$categoryId = !empty($payload['category_id']) ? (int)$payload['category_id'] : null;
$subcategoryId = !empty($payload['subcategory_id']) ? (int)$payload['subcategory_id'] : null;
// Fetch names for backward compatibility if needed, or just store IDs
$categoryName = '';
$categoryNameAr = '';
$subName = '';
$subNameAr = '';
if ($categoryId) {
$cat = library_get_category_by_id($categoryId);
if ($cat) {
$categoryName = $cat['name_en'];
$categoryNameAr = $cat['name_ar'];
}
}
if ($subcategoryId) {
$sub = library_get_subcategory_by_id($subcategoryId);
if ($sub) {
$subName = $sub['name_en'];
$subNameAr = $sub['name_ar'];
}
}
$visibility = (string) ($payload['visibility'] ?? 'public');
$allow_download = !empty($payload['allow_download']) ? 1 : 0;
$allow_print = !empty($payload['allow_print']) ? 1 : 0;
$allow_copy = !empty($payload['allow_copy']) ? 1 : 0;
$fileData = library_handle_uploaded_file($file);
$stmt = db()->prepare('INSERT INTO library_documents (
title_en, title_ar,
category, category_ar, sub_category, sub_category_ar,
category_id, subcategory_id,
visibility, document_type,
file_name, file_path, file_size_kb, allow_download, allow_print, allow_copy
) VALUES (
:title_en, :title_ar,
:category, :category_ar, :sub_category, :sub_category_ar,
:category_id, :subcategory_id,
:visibility, :document_type,
:file_name, :file_path, :file_size_kb, :allow_download, :allow_print, :allow_copy
)');
$stmt->execute([
':title_en' => $titleEn ?: null,
':title_ar' => $titleAr ?: null,
':category' => $categoryName ?: null,
':category_ar' => $categoryNameAr ?: null,
':sub_category' => $subName ?: null,
':sub_category_ar' => $subNameAr ?: null,
':category_id' => $categoryId,
':subcategory_id' => $subcategoryId,
':visibility' => $visibility,
':document_type' => $fileData['document_type'],
':file_name' => $fileData['file_name'],
':file_path' => $fileData['file_path'],
':file_size_kb' => $fileData['file_size_kb'],
':allow_download' => $allow_download,
':allow_print' => $allow_print,
':allow_copy' => $allow_copy,
]);
return (int) db()->lastInsertId();
}