[ 'catalog' => 'Catalog', 'admin_studio' => 'Admin Studio', 'nabd_library' => 'Nabd Library', 'tagline' => 'Arabic · English e-library', 'search' => 'Search...', 'switch_lang' => 'العربية', 'switch_lang_code' => 'ar', 'browse_public' => 'Browse public catalog', 'open_admin' => 'Open Admin Studio', 'upload_docs' => 'Upload documents, publish public/private titles, read online, and request AI summaries.', 'mvp_label' => 'Bilingual reader MVP', 'admin_panel' => 'Admin Panel', 'dashboard' => 'Dashboard', 'material_entry' => 'Material Entry', 'categories' => 'Categories', 'subcategories' => 'Subcategories', 'types' => 'Types', 'return_to_site' => 'Return to Site', 'total_documents' => 'Total Documents', 'public_titles' => 'Public Titles', 'private_titles' => 'Private Titles', 'total_downloads' => 'Total Downloads', 'recent_documents' => 'Recent Documents', 'title_author' => 'Title / Author', 'type_category' => 'Type / Category', 'visibility' => 'Visibility', 'actions' => 'Actions', 'view' => 'View', 'showing_last_10' => 'Showing last 10 uploads', 'manage_documents' => 'Manage Documents', 'manage_documents_desc' => 'Manage your library documents, upload new materials, and edit metadata including bilingual titles, summaries, and more.', 'create_new' => 'Create New', 'edit' => 'Edit', 'delete' => 'Delete', 'save' => 'Save', 'cancel' => 'Cancel', 'name_en' => 'English Name', 'name_ar' => 'Arabic Name', 'description_en' => 'English Description', 'description_ar' => 'Arabic Description', 'confirm_delete' => 'Are you sure you want to delete this item?', 'manage_categories_desc' => 'Manage document categories.', 'add_new_category' => 'Add New Category', 'search_placeholder' => 'Search...', 'clear' => 'Clear', 'name' => 'Name', 'no_categories_found' => 'No categories found.', 'add_new_document' => 'Add New Material', 'search_docs_placeholder' => 'Search by title or author...', 'id' => 'ID', 'cover' => 'Cover', 'year' => 'Year', 'publisher' => 'Publisher', 'country' => 'Country', 'page_count' => 'Total Pages', 'summary_en' => 'Summary (English)', 'summary_ar' => 'Summary (Arabic)', 'cover_image' => 'Front Cover Image', 'document_file' => 'Document File', 'visibility_permissions' => 'Visibility & Permissions', 'download' => 'Download', 'print' => 'Print', 'copy' => 'Copy', 'author' => 'Author', 'select_type' => 'Select Type...', 'select_category' => 'Select Category...', 'select_subcategory' => 'Select Subcategory...', 'required_new_docs' => 'Required for new documents.', 'leave_empty_keep' => 'Leave empty to keep existing file.', 'save_changes' => 'Save Changes', 'manage_subcategories_desc' => 'Manage document subcategories.', 'add_new_subcategory' => 'Add New Subcategory', 'no_subcategories_found' => 'No subcategories found.', 'category' => 'Category', 'manage_types_desc' => 'Manage document types.', 'add_new_type' => 'Add New Type', 'no_types_found' => 'No types found.', 'previous' => 'Previous', 'next' => 'Next', 'showing_page' => 'Showing page', 'of' => 'of', 'no_documents_found' => 'No documents found.', 'submission_error' => 'Submission Error', 'current_cover' => 'Current Cover:', 'visibility_public' => 'Public', 'visibility_private' => 'Private', 'unknown' => 'Unknown', 'translation_to_arabic' => 'Translate to Arabic', 'translation_to_english' => 'Translate to English', 'translation_enter_text' => 'Please enter text to translate.', 'translating' => 'Translating...', 'translation_failed' => 'Translation failed. Please try again.', 'translation_error_prefix' => 'Translation error:', 'category_name_required' => 'Both English and Arabic names are required for Category.', 'category_update_required' => 'ID, English name, and Arabic name are required.', 'invalid_category_id' => 'Invalid Category ID.', 'category_created_success' => 'Category created successfully.', 'category_updated_success' => 'Category updated successfully.', 'category_deleted_success' => 'Category deleted successfully.', 'subcategory_name_required' => 'Category, English name, and Arabic name are required.', 'subcategory_update_required' => 'ID, Category, English name, and Arabic name are required.', 'invalid_subcategory_id' => 'Invalid Subcategory ID.', 'subcategory_created_success' => 'Subcategory created successfully.', 'subcategory_updated_success' => 'Subcategory updated successfully.', 'subcategory_deleted_success' => 'Subcategory deleted successfully.', 'type_name_required' => 'Both English and Arabic names are required for Type.', 'type_update_required' => 'ID, English name, and Arabic name are required.', 'invalid_type_id' => 'Invalid Type ID.', 'type_created_success' => 'Type created successfully.', 'type_updated_success' => 'Type updated successfully.', 'type_deleted_success' => 'Type deleted successfully.', 'invalid_document_id' => 'Invalid Document ID.', 'document_created_success' => 'Document created successfully.', 'document_updated_success' => 'Document updated successfully.', 'document_deleted_success' => 'Document deleted successfully.' ], 'ar' => [ 'catalog' => 'الفهرس', 'admin_studio' => 'استوديو الإدارة', 'nabd_library' => 'مكتبة نبض', 'tagline' => 'مكتبة إلكترونية عربية · إنجليزية', 'search' => 'بحث...', 'switch_lang' => 'English', 'switch_lang_code' => 'en', 'browse_public' => 'تصفح الفهرس العام', 'open_admin' => 'فتح استوديو الإدارة', 'upload_docs' => 'ارفع المستندات، انشر العناوين العامة/الخاصة، اقرأ عبر الإنترنت، واطلب ملخصات الذكاء الاصطناعي.', 'mvp_label' => 'نموذج القارئ ثنائي اللغة', 'admin_panel' => 'لوحة التحكم', 'dashboard' => 'لوحة القيادة', 'material_entry' => 'إدخال المواد', 'categories' => 'التصنيف', 'subcategories' => 'التصنيف الفرعي', 'types' => 'الأنواع', 'return_to_site' => 'العودة للموقع', 'total_documents' => 'إجمالي المستندات', 'public_titles' => 'العناوين العامة', 'private_titles' => 'العناوين الخاصة', 'total_downloads' => 'إجمالي التنزيلات', 'recent_documents' => 'أحدث المستندات', 'title_author' => 'العنوان / المؤلف', 'type_category' => 'النوع / الفئة', 'visibility' => 'الظهور', 'actions' => 'الإجراءات', 'view' => 'عرض', 'showing_last_10' => 'عرض آخر 10 ملفات', 'manage_documents' => 'إدارة المستندات', 'manage_documents_desc' => 'إدارة مستندات المكتبة، رفع مواد جديدة، وتعديل البيانات الوصفية بما في ذلك العناوين والملخصات ثنائية اللغة.', 'create_new' => 'إنشاء جديد', 'edit' => 'تعديل', 'delete' => 'حذف', 'save' => 'حفظ', 'cancel' => 'إلغاء', 'name_en' => 'الاسم (إنجليزي)', 'name_ar' => 'الاسم (عربي)', 'description_en' => 'الوصف (إنجليزي)', 'description_ar' => 'الوصف (عربي)', 'confirm_delete' => 'هل أنت متأكد من حذف هذا العنصر؟', 'manage_categories_desc' => 'إدارة التصنيف للمستندات', 'add_new_category' => 'إضافة فئة جديدة', 'search_placeholder' => 'بحث...', 'clear' => 'مسح', 'name' => 'الاسم', 'no_categories_found' => 'لم يتم العثور على فئات.', 'add_new_document' => 'إضافة مادة جديدة', 'search_docs_placeholder' => 'بحث بالعنوان أو المؤلف...', 'id' => 'المعرف', 'cover' => 'الغلاف', 'year' => 'السنة', 'publisher' => 'الناشر', 'country' => 'الدولة', 'page_count' => 'عدد الصفحات', 'summary_en' => 'الملخص (إنجليزي)', 'summary_ar' => 'الملخص (عربي)', 'cover_image' => 'صورة الغلاف الأمامي', 'document_file' => 'ملف المستند', 'visibility_permissions' => 'الظهور والصلاحيات', 'download' => 'تنزيل', 'print' => 'طباعة', 'copy' => 'نسخ', 'author' => 'المؤلف', 'select_type' => 'اختر النوع...', 'select_category' => 'اختر الفئة...', 'select_subcategory' => 'اختر الفئة الفرعية...', 'required_new_docs' => 'مطلوب للمستندات الجديدة.', 'leave_empty_keep' => 'اتركه فارغاً للاحتفاظ بالملف الحالي.', 'save_changes' => 'حفظ التغييرات', 'manage_subcategories_desc' => 'إدارة التصنيف الفرعي للمستندات.', 'add_new_subcategory' => 'إضافة فئة فرعية جديدة', 'no_subcategories_found' => 'لم يتم العثور على فئات فرعية.', 'category' => 'الفئة', 'manage_types_desc' => 'إدارة أنواع المستندات.', 'add_new_type' => 'إضافة نوع جديد', 'no_types_found' => 'لم يتم العثور على أنواع.', 'previous' => 'السابق', 'next' => 'التالي', 'showing_page' => 'عرض صفحة', 'of' => 'من', 'no_documents_found' => 'لم يتم العثور على مستندات.', 'submission_error' => 'خطأ في الإرسال', 'current_cover' => 'الغلاف الحالي:', 'visibility_public' => 'عام', 'visibility_private' => 'خاص', 'unknown' => 'غير معروف', 'translation_to_arabic' => 'ترجمة إلى العربية', 'translation_to_english' => 'ترجمة إلى الإنجليزية', 'translation_enter_text' => 'يرجى إدخال نص للترجمة.', 'translating' => 'جارٍ الترجمة...', 'translation_failed' => 'فشلت الترجمة. يرجى المحاولة مرة أخرى.', 'translation_error_prefix' => 'خطأ في الترجمة:', 'category_name_required' => 'اسما التصنيف بالإنجليزية والعربية مطلوبان.', 'category_update_required' => 'المعرف واسم التصنيف بالإنجليزية والعربية مطلوبة.', 'invalid_category_id' => 'معرف التصنيف غير صالح.', 'category_created_success' => 'تم إنشاء التصنيف بنجاح.', 'category_updated_success' => 'تم تحديث التصنيف بنجاح.', 'category_deleted_success' => 'تم حذف التصنيف بنجاح.', 'subcategory_name_required' => 'التصنيف واسم التصنيف الفرعي بالإنجليزية والعربية مطلوبة.', 'subcategory_update_required' => 'المعرف والتصنيف واسم التصنيف الفرعي بالإنجليزية والعربية مطلوبة.', 'invalid_subcategory_id' => 'معرف التصنيف الفرعي غير صالح.', 'subcategory_created_success' => 'تم إنشاء التصنيف الفرعي بنجاح.', 'subcategory_updated_success' => 'تم تحديث التصنيف الفرعي بنجاح.', 'subcategory_deleted_success' => 'تم حذف التصنيف الفرعي بنجاح.', 'type_name_required' => 'اسما النوع بالإنجليزية والعربية مطلوبان.', 'type_update_required' => 'المعرف واسم النوع بالإنجليزية والعربية مطلوبة.', 'invalid_type_id' => 'معرف النوع غير صالح.', 'type_created_success' => 'تم إنشاء النوع بنجاح.', 'type_updated_success' => 'تم تحديث النوع بنجاح.', 'type_deleted_success' => 'تم حذف النوع بنجاح.', 'invalid_document_id' => 'معرف المستند غير صالح.', 'document_created_success' => 'تم إنشاء المستند بنجاح.', 'document_updated_success' => 'تم تحديث المستند بنجاح.', 'document_deleted_success' => 'تم حذف المستند بنجاح.' ] ]; return $translations[$lang][$key] ?? $key; } 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); } } $migration4Path = __DIR__ . '/../db/migrations/004_create_types_table.sql'; if (is_file($migration4Path)) { // Simple check if table exists $exists = db()->query("SHOW TABLES LIKE 'library_types'")->fetch(); if (!$exists) { $sql = file_get_contents($migration4Path); db()->exec($sql); } } $migration5Path = __DIR__ . '/../db/migrations/005_update_document_fields.sql'; if (is_file($migration5Path)) { // Check if column exists $exists = db()->query("SHOW COLUMNS FROM library_documents LIKE 'cover_image_path'")->fetch(); if (!$exists) { $sql = file_get_contents($migration5Path); db()->exec($sql); } } $migration6Path = __DIR__ . '/../db/migrations/006_add_author_column.sql'; if (is_file($migration6Path)) { // Check if column exists $exists = db()->query("SHOW COLUMNS FROM library_documents LIKE 'author'")->fetch(); if (!$exists) { $sql = file_get_contents($migration6Path); db()->exec($sql); } } $migration7Path = __DIR__ . '/../db/migrations/007_create_reader_activity.sql'; if (is_file($migration7Path)) { $exists = db()->query("SHOW TABLES LIKE 'library_readers'")->fetch(); if (!$exists) { $sql = file_get_contents($migration7Path); if (is_string($sql) && trim($sql) !== '') { db()->exec($sql); } } } $migration8Path = __DIR__ . '/../db/migrations/008_create_library_settings.sql'; if (is_file($migration8Path)) { $exists = db()->query("SHOW TABLES LIKE 'library_settings'")->fetch(); if (!$exists) { $sql = file_get_contents($migration8Path); if (is_string($sql) && trim($sql) !== '') { db()->exec($sql); } } } $uploadDir = __DIR__ . '/../uploads/library'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0775, true); } $coverDir = __DIR__ . '/../uploads/covers'; if (!is_dir($coverDir)) { mkdir($coverDir, 0775, true); } $brandingDir = __DIR__ . '/../uploads/branding'; if (!is_dir($brandingDir)) { mkdir($brandingDir, 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 { $lang = library_get_language(); return [ 'name' => library_profile_name($lang), 'description' => library_profile_description($lang), 'image' => $_SERVER['PROJECT_IMAGE_URL'] ?? '', ]; } function library_profile_defaults(): array { return [ 'library_name_en' => $_SERVER['PROJECT_NAME'] ?? 'Nabd Library', 'library_name_ar' => 'مكتبة نبض', 'short_name' => 'NL', 'tagline_en' => 'Arabic · English e-library', 'tagline_ar' => 'مكتبة رقمية عربية · إنجليزية', 'description_en' => $_SERVER['PROJECT_DESCRIPTION'] ?? 'Bilingual electronic library for Arabic and English documents, online reading, and AI-assisted summaries.', 'description_ar' => 'مكتبة إلكترونية ثنائية اللغة للوثائق العربية والإنجليزية، مع قراءة عبر الإنترنت وملخصات مدعومة بالذكاء الاصطناعي.', 'contact_email' => '', 'contact_phone' => '', 'whatsapp_number' => '', 'website_url' => '', 'address_en' => '', 'address_ar' => '', 'opening_hours_en' => '', 'opening_hours_ar' => '', 'facebook_url' => '', 'instagram_url' => '', 'x_url' => '', 'youtube_url' => '', 'logo_path' => '', 'favicon_path' => '', 'copyright_text_en' => '', 'copyright_text_ar' => '', ]; } function library_get_profile(): array { static $profile = null; if ($profile !== null) { return $profile; } library_bootstrap(); $defaults = library_profile_defaults(); $stmt = db()->query('SELECT * FROM library_settings ORDER BY id ASC LIMIT 1'); $row = $stmt ? ($stmt->fetch() ?: []) : []; $profile = array_merge($defaults, is_array($row) ? $row : []); return $profile; } function library_profile_name(?string $lang = null): string { $lang = $lang ?? library_get_language(); $profile = library_get_profile(); return library_localized_value( $profile['library_name_en'] ?? null, $profile['library_name_ar'] ?? null, $lang, $_SERVER['PROJECT_NAME'] ?? 'Nabd Library' ); } function library_profile_tagline(?string $lang = null): string { $lang = $lang ?? library_get_language(); $profile = library_get_profile(); return library_localized_value( $profile['tagline_en'] ?? null, $profile['tagline_ar'] ?? null, $lang, '' ); } function library_profile_description(?string $lang = null): string { $lang = $lang ?? library_get_language(); $profile = library_get_profile(); return library_localized_value( $profile['description_en'] ?? null, $profile['description_ar'] ?? null, $lang, $_SERVER['PROJECT_DESCRIPTION'] ?? 'Bilingual electronic library for Arabic and English documents, online reading, and AI-assisted summaries.' ); } function library_profile_address(?string $lang = null): string { $lang = $lang ?? library_get_language(); $profile = library_get_profile(); return library_localized_value( $profile['address_en'] ?? null, $profile['address_ar'] ?? null, $lang, '' ); } function library_profile_opening_hours(?string $lang = null): string { $lang = $lang ?? library_get_language(); $profile = library_get_profile(); return library_localized_value( $profile['opening_hours_en'] ?? null, $profile['opening_hours_ar'] ?? null, $lang, '' ); } function library_normalize_profile_value($value, int $maxLength = 0): ?string { $normalized = trim((string) $value); if ($normalized === '') { return null; } if ($maxLength > 0 && function_exists('mb_substr')) { $normalized = mb_substr($normalized, 0, $maxLength); } elseif ($maxLength > 0) { $normalized = substr($normalized, 0, $maxLength); } return $normalized; } function library_normalize_profile_url($value): ?string { $normalized = trim((string) $value); if ($normalized === '') { return null; } if (!preg_match('~^https?://~i', $normalized)) { $normalized = 'https://' . ltrim($normalized, '/'); } if (!filter_var($normalized, FILTER_VALIDATE_URL)) { throw new RuntimeException('Please provide valid URLs for website and social links.'); } return $normalized; } function library_handle_brand_asset(array $file, string $kind): ?string { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { return null; } $originalName = (string) ($file['name'] ?? ''); $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); $rules = [ 'logo' => [ 'allowed' => ['jpg', 'jpeg', 'png', 'webp'], 'max_size' => 4 * 1024 * 1024, 'label' => 'logo', ], 'favicon' => [ 'allowed' => ['ico', 'png', 'webp'], 'max_size' => 1024 * 1024, 'label' => 'favicon', ], ]; if (!isset($rules[$kind])) { throw new RuntimeException('Unsupported upload type.'); } $rule = $rules[$kind]; if (!in_array($extension, $rule['allowed'], true)) { throw new RuntimeException(sprintf('Unsupported %s format. Allowed: %s.', $rule['label'], implode(', ', $rule['allowed']))); } $size = (int) ($file['size'] ?? 0); if ($size <= 0 || $size > $rule['max_size']) { throw new RuntimeException(sprintf('The %s file is too large.', $rule['label'])); } $safeBase = preg_replace('/[^a-zA-Z0-9_-]+/', '-', pathinfo($originalName, PATHINFO_FILENAME)) ?: $kind; $storedName = strtolower($kind . '-' . date('YmdHis') . '-' . $safeBase . '-' . bin2hex(random_bytes(4)) . '.' . $extension); $relativePath = 'uploads/branding/' . $storedName; $absolutePath = __DIR__ . '/../' . $relativePath; if (!move_uploaded_file((string) $file['tmp_name'], $absolutePath)) { throw new RuntimeException(sprintf('Unable to save the %s file.', $rule['label'])); } return $relativePath; } function library_save_profile(array $payload, array $files = []): void { library_bootstrap(); $profile = library_get_profile(); $libraryNameEn = library_normalize_profile_value($payload['library_name_en'] ?? null, 255); $libraryNameAr = library_normalize_profile_value($payload['library_name_ar'] ?? null, 255); if ($libraryNameEn === null && $libraryNameAr === null) { throw new RuntimeException('Please provide at least one library name.'); } $contactEmail = library_normalize_profile_value($payload['contact_email'] ?? null, 255); if ($contactEmail !== null && !filter_var($contactEmail, FILTER_VALIDATE_EMAIL)) { throw new RuntimeException('Please provide a valid contact email address.'); } $logoPath = $profile['logo_path'] ?? null; $faviconPath = $profile['favicon_path'] ?? null; $newLogo = library_handle_brand_asset($files['logo_file'] ?? [], 'logo'); $newFavicon = library_handle_brand_asset($files['favicon_file'] ?? [], 'favicon'); if ($newLogo !== null) { $logoPath = $newLogo; } if ($newFavicon !== null) { $faviconPath = $newFavicon; } $data = [ 'library_name_en' => $libraryNameEn, 'library_name_ar' => library_normalize_profile_value($payload['library_name_ar'] ?? null, 255), 'short_name' => library_normalize_profile_value($payload['short_name'] ?? null, 32), 'tagline_en' => library_normalize_profile_value($payload['tagline_en'] ?? null, 255), 'tagline_ar' => library_normalize_profile_value($payload['tagline_ar'] ?? null, 255), 'description_en' => library_normalize_profile_value($payload['description_en'] ?? null), 'description_ar' => library_normalize_profile_value($payload['description_ar'] ?? null), 'contact_email' => $contactEmail, 'contact_phone' => library_normalize_profile_value($payload['contact_phone'] ?? null, 80), 'whatsapp_number' => library_normalize_profile_value($payload['whatsapp_number'] ?? null, 80), 'website_url' => library_normalize_profile_url($payload['website_url'] ?? null), 'address_en' => library_normalize_profile_value($payload['address_en'] ?? null, 255), 'address_ar' => library_normalize_profile_value($payload['address_ar'] ?? null, 255), 'opening_hours_en' => library_normalize_profile_value($payload['opening_hours_en'] ?? null, 255), 'opening_hours_ar' => library_normalize_profile_value($payload['opening_hours_ar'] ?? null, 255), 'facebook_url' => library_normalize_profile_url($payload['facebook_url'] ?? null), 'instagram_url' => library_normalize_profile_url($payload['instagram_url'] ?? null), 'x_url' => library_normalize_profile_url($payload['x_url'] ?? null), 'youtube_url' => library_normalize_profile_url($payload['youtube_url'] ?? null), 'logo_path' => $logoPath, 'favicon_path' => $faviconPath, 'copyright_text_en' => library_normalize_profile_value($payload['copyright_text_en'] ?? null, 255), 'copyright_text_ar' => library_normalize_profile_value($payload['copyright_text_ar'] ?? null, 255), ]; $existingId = db()->query('SELECT id FROM library_settings ORDER BY id ASC LIMIT 1')->fetchColumn(); if ($existingId) { $sql = 'UPDATE library_settings SET library_name_en = :library_name_en, library_name_ar = :library_name_ar, short_name = :short_name, tagline_en = :tagline_en, tagline_ar = :tagline_ar, description_en = :description_en, description_ar = :description_ar, contact_email = :contact_email, contact_phone = :contact_phone, whatsapp_number = :whatsapp_number, website_url = :website_url, address_en = :address_en, address_ar = :address_ar, opening_hours_en = :opening_hours_en, opening_hours_ar = :opening_hours_ar, facebook_url = :facebook_url, instagram_url = :instagram_url, x_url = :x_url, youtube_url = :youtube_url, logo_path = :logo_path, favicon_path = :favicon_path, copyright_text_en = :copyright_text_en, copyright_text_ar = :copyright_text_ar WHERE id = :id'; $stmt = db()->prepare($sql); $data['id'] = (int) $existingId; $stmt->execute($data); } else { $sql = 'INSERT INTO library_settings ( library_name_en, library_name_ar, short_name, tagline_en, tagline_ar, description_en, description_ar, contact_email, contact_phone, whatsapp_number, website_url, address_en, address_ar, opening_hours_en, opening_hours_ar, facebook_url, instagram_url, x_url, youtube_url, logo_path, favicon_path, copyright_text_en, copyright_text_ar ) VALUES ( :library_name_en, :library_name_ar, :short_name, :tagline_en, :tagline_ar, :description_en, :description_ar, :contact_email, :contact_phone, :whatsapp_number, :website_url, :address_en, :address_ar, :opening_hours_en, :opening_hours_ar, :facebook_url, :instagram_url, :x_url, :youtube_url, :logo_path, :favicon_path, :copyright_text_en, :copyright_text_ar )'; $stmt = db()->prepare($sql); $stmt->execute($data); } unset($GLOBALS['library_profile_cache']); } 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_localized_value(?string $english, ?string $arabic, ?string $lang = null, string $fallback = ''): string { $lang = $lang ?? library_get_language(); $primary = trim((string) ($lang === 'ar' ? $arabic : $english)); $secondary = trim((string) ($lang === 'ar' ? $english : $arabic)); if ($primary !== '') { return $primary; } if ($secondary !== '') { return $secondary; } return $fallback; } function library_localized_document_title(array $document, ?string $lang = null, string $fallback = 'Untitled'): string { return library_localized_value( $document['title_en'] ?? null, $document['title_ar'] ?? null, $lang, $fallback ); } function library_localized_document_description(array $document, ?string $lang = null, string $fallback = ''): string { return library_localized_value( $document['description_en'] ?? null, $document['description_ar'] ?? null, $lang, $fallback ); } function library_localized_document_summary(array $document, ?string $lang = null, string $fallback = ''): string { $summary = library_localized_value( $document['summary_en'] ?? null, $document['summary_ar'] ?? null, $lang, '' ); if ($summary !== '') { return $summary; } $combined = trim((string) ($document['summary_text'] ?? '')); return $combined !== '' ? $combined : $fallback; } function library_document_type_label(string $type): string { $lang = library_get_language(); $map = [ 'en' => [ 'pdf' => 'PDF reader', 'txt' => 'Text note', 'doc' => 'Word document', 'docx' => 'Word document', 'ppt' => 'PowerPoint', 'pptx' => 'PowerPoint', ], 'ar' => [ 'pdf' => 'قارئ PDF', 'txt' => 'ملاحظة نصية', 'doc' => 'مستند Word', 'docx' => 'مستند Word', 'ppt' => 'عرض PowerPoint', 'pptx' => 'عرض PowerPoint', ], ]; return $map[$lang][strtolower($type)] ?? strtoupper($type); } function library_language_label(string $lang): string { $uiLang = library_get_language(); $map = [ 'en' => [ 'en' => 'English', 'ar' => 'Arabic', 'bilingual' => 'Bilingual', ], 'ar' => [ 'en' => 'الإنجليزية', 'ar' => 'العربية', 'bilingual' => 'ثنائي اللغة', ], ]; return $map[$uiLang][$lang] ?? ($uiLang === 'ar' ? 'غير معروف' : 'Unknown'); } function library_visibility_label(string $visibility): string { if (library_get_language() === 'ar') { return $visibility === 'private' ? 'خاص / يتطلب تسجيل الدخول' : 'عام'; } 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', ]; } function library_file_url(string $path): string { if ($path === '') return ''; if ($path[0] !== '/') { return '/' . $path; } return $path; } function library_can_preview(array $doc): bool { // Only PDFs are supported for inline preview in this version return strtolower((string) ($doc['document_type'] ?? '')) === 'pdf'; } function library_increment_views(int $id): void { library_bootstrap(); $stmt = db()->prepare('UPDATE library_documents SET view_count = view_count + 1 WHERE id = ?'); $stmt->execute([$id]); } function library_reader_cookie_name(): string { return 'nabd_reader_token'; } function library_current_request_path(): string { $path = (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH); $path = $path !== '' ? $path : '/'; return substr($path, 0, 255); } function library_current_reader_token(bool $create = true): string { static $token = null; if ($token !== null) { return $token; } $candidate = strtolower(trim((string) ($_SESSION['library_reader_token'] ?? ($_COOKIE[library_reader_cookie_name()] ?? '')))); if (preg_match('/^[a-f0-9]{64}$/', $candidate) === 1) { $_SESSION['library_reader_token'] = $candidate; $token = $candidate; return $candidate; } if (!$create) { $token = ''; return ''; } try { $candidate = bin2hex(random_bytes(32)); } catch (Throwable $e) { $candidate = hash('sha256', uniqid('reader', true) . microtime(true)); } $_SESSION['library_reader_token'] = $candidate; $_COOKIE[library_reader_cookie_name()] = $candidate; if (!headers_sent()) { setcookie(library_reader_cookie_name(), $candidate, [ 'expires' => time() + (60 * 60 * 24 * 365), 'path' => '/', 'httponly' => true, 'samesite' => 'Lax', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', ]); } $token = $candidate; return $candidate; } function library_get_current_reader(bool $create = true): ?array { static $reader = null; if (is_array($reader)) { return $reader; } library_bootstrap(); $token = library_current_reader_token($create); if ($token === '') { return null; } $stmt = db()->prepare('SELECT * FROM library_readers WHERE reader_token = :token LIMIT 1'); $stmt->bindValue(':token', $token, PDO::PARAM_STR); $stmt->execute(); $reader = $stmt->fetch(); if (!$reader && $create) { $insert = db()->prepare('INSERT INTO library_readers (reader_token, preferred_language, last_path, user_agent) VALUES (:token, :language, :path, :user_agent)'); $insert->bindValue(':token', $token, PDO::PARAM_STR); $insert->bindValue(':language', library_get_language(), PDO::PARAM_STR); $insert->bindValue(':path', library_current_request_path(), PDO::PARAM_STR); $insert->bindValue(':user_agent', substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), PDO::PARAM_STR); $insert->execute(); $stmt->execute(); $reader = $stmt->fetch(); } return is_array($reader) ? $reader : null; } function library_touch_current_reader(int $readerId): void { $stmt = db()->prepare('UPDATE library_readers SET preferred_language = :language, last_path = :path, user_agent = :user_agent, last_seen_at = NOW() WHERE id = :id'); $stmt->bindValue(':language', library_get_language(), PDO::PARAM_STR); $stmt->bindValue(':path', library_current_request_path(), PDO::PARAM_STR); $stmt->bindValue(':user_agent', substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), PDO::PARAM_STR); $stmt->bindValue(':id', $readerId, PDO::PARAM_INT); $stmt->execute(); } function library_touch_current_visit(int $readerId): array { static $visit = null; if (is_array($visit) && (int) ($visit['reader_id'] ?? 0) === $readerId) { return $visit; } $sessionKey = (string) ($_SESSION['library_visit_key'] ?? ''); if (preg_match('/^[a-f0-9]{32}$/', $sessionKey) !== 1) { try { $sessionKey = bin2hex(random_bytes(16)); } catch (Throwable $e) { $sessionKey = substr(hash('sha256', session_id() . microtime(true)), 0, 32); } $_SESSION['library_visit_key'] = $sessionKey; } $select = db()->prepare('SELECT * FROM library_reader_visits WHERE reader_id = :reader_id AND session_key = :session_key LIMIT 1'); $select->bindValue(':reader_id', $readerId, PDO::PARAM_INT); $select->bindValue(':session_key', $sessionKey, PDO::PARAM_STR); $select->execute(); $visit = $select->fetch(); if ($visit) { $update = db()->prepare('UPDATE library_reader_visits SET last_activity_at = NOW(), page_views = page_views + 1 WHERE id = :id'); $update->bindValue(':id', (int) $visit['id'], PDO::PARAM_INT); $update->execute(); $select->execute(); $visit = $select->fetch(); return is_array($visit) ? $visit : []; } $insert = db()->prepare('INSERT INTO library_reader_visits (reader_id, session_key, entry_path) VALUES (:reader_id, :session_key, :entry_path)'); $insert->bindValue(':reader_id', $readerId, PDO::PARAM_INT); $insert->bindValue(':session_key', $sessionKey, PDO::PARAM_STR); $insert->bindValue(':entry_path', library_current_request_path(), PDO::PARAM_STR); $insert->execute(); $select->execute(); $visit = $select->fetch(); return is_array($visit) ? $visit : []; } function library_log_reader_activity(int $readerId, ?int $visitId, string $eventType, ?int $documentId = null, array $meta = [], string $context = 'public'): void { $filteredMeta = array_filter($meta, static fn ($value) => $value !== null && $value !== ''); $metaJson = $filteredMeta ? json_encode($filteredMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null; $stmt = db()->prepare('INSERT INTO library_reader_activities (reader_id, visit_id, document_id, event_type, page_path, context, meta_json) VALUES (:reader_id, :visit_id, :document_id, :event_type, :page_path, :context, :meta_json)'); $stmt->bindValue(':reader_id', $readerId, PDO::PARAM_INT); $stmt->bindValue(':visit_id', $visitId, $visitId === null ? PDO::PARAM_NULL : PDO::PARAM_INT); $stmt->bindValue(':document_id', $documentId, $documentId === null ? PDO::PARAM_NULL : PDO::PARAM_INT); $stmt->bindValue(':event_type', substr($eventType, 0, 50), PDO::PARAM_STR); $stmt->bindValue(':page_path', library_current_request_path(), PDO::PARAM_STR); $stmt->bindValue(':context', substr($context, 0, 20), PDO::PARAM_STR); $stmt->bindValue(':meta_json', $metaJson, $metaJson === null ? PDO::PARAM_NULL : PDO::PARAM_STR); $stmt->execute(); } function library_track_request(string $eventType, ?int $documentId = null, array $meta = [], string $context = 'public'): void { static $tracked = false; if ($tracked) { return; } $reader = library_get_current_reader(true); if (!$reader) { return; } $readerId = (int) ($reader['id'] ?? 0); if ($readerId <= 0) { return; } library_touch_current_reader($readerId); $visit = library_touch_current_visit($readerId); $visitId = isset($visit['id']) ? (int) $visit['id'] : null; library_log_reader_activity($readerId, $visitId, $eventType, $documentId, $meta, $context); $tracked = true; } function library_get_current_reader_summary(): ?array { $reader = library_get_current_reader(true); if (!$reader) { return null; } $readerId = (int) ($reader['id'] ?? 0); if ($readerId <= 0) { return null; } $readerStmt = db()->prepare('SELECT * FROM library_readers WHERE id = :id LIMIT 1'); $readerStmt->bindValue(':id', $readerId, PDO::PARAM_INT); $readerStmt->execute(); $readerRow = $readerStmt->fetch(); if (!$readerRow) { return null; } $stmt = db()->prepare( 'SELECT (SELECT COUNT(*) FROM library_reader_visits WHERE reader_id = :reader_id_1) AS total_visits, (SELECT COUNT(*) FROM library_reader_activities WHERE reader_id = :reader_id_2) AS total_activities, (SELECT COUNT(DISTINCT document_id) FROM library_reader_activities WHERE reader_id = :reader_id_3 AND document_id IS NOT NULL) AS documents_opened, (SELECT MAX(created_at) FROM library_reader_activities WHERE reader_id = :reader_id_4 AND document_id IS NOT NULL) AS last_read_at' ); $stmt->bindValue(':reader_id_1', $readerId, PDO::PARAM_INT); $stmt->bindValue(':reader_id_2', $readerId, PDO::PARAM_INT); $stmt->bindValue(':reader_id_3', $readerId, PDO::PARAM_INT); $stmt->bindValue(':reader_id_4', $readerId, PDO::PARAM_INT); $stmt->execute(); $metrics = $stmt->fetch() ?: []; return array_merge($readerRow, $metrics); } function library_get_current_reader_history(int $limit = 8): array { $reader = library_get_current_reader(true); if (!$reader) { return []; } $stmt = db()->prepare( 'SELECT d.id, d.title_en, d.title_ar, d.author, d.document_language, d.cover_image_path, MAX(a.created_at) AS last_opened_at, COUNT(*) AS open_count, MAX(CASE WHEN a.event_type = "reader_opened" THEN a.created_at ELSE NULL END) AS last_reader_opened_at FROM library_reader_activities a INNER JOIN library_documents d ON d.id = a.document_id WHERE a.reader_id = :reader_id AND a.event_type IN ("document_viewed", "reader_opened") GROUP BY d.id, d.title_en, d.title_ar, d.author, d.document_language, d.cover_image_path ORDER BY last_opened_at DESC LIMIT :limit' ); $stmt->bindValue(':reader_id', (int) $reader['id'], PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll() ?: []; } function library_get_current_reader_activities(int $limit = 16): array { $reader = library_get_current_reader(true); if (!$reader) { return []; } $stmt = db()->prepare( 'SELECT a.id, a.visit_id, a.document_id, a.event_type, a.page_path, a.context, a.meta_json, a.created_at, d.title_en, d.title_ar, d.author, d.document_language FROM library_reader_activities a LEFT JOIN library_documents d ON d.id = a.document_id WHERE a.reader_id = :reader_id ORDER BY a.created_at DESC LIMIT :limit' ); $stmt->bindValue(':reader_id', (int) $reader['id'], PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll() ?: []; } function library_get_current_reader_visits(int $limit = 8): array { $reader = library_get_current_reader(true); if (!$reader) { return []; } $stmt = db()->prepare( 'SELECT v.id, v.entry_path, v.started_at, v.last_activity_at, v.page_views, (SELECT COUNT(*) FROM library_reader_activities a WHERE a.visit_id = v.id) AS activity_count, (SELECT COUNT(*) FROM library_reader_activities a WHERE a.visit_id = v.id AND a.document_id IS NOT NULL) AS document_count FROM library_reader_visits v WHERE v.reader_id = :reader_id ORDER BY v.started_at DESC LIMIT :limit' ); $stmt->bindValue(':reader_id', (int) $reader['id'], PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll() ?: []; } function library_generate_summary(int $id): array { library_bootstrap(); $doc = library_fetch_document($id); if (!$doc) { return ['success' => false, 'message' => 'Document not found.']; } // 1. Try to extract text from the file $text = ''; $filePath = __DIR__ . '/../' . ($doc['file_path'] ?? ''); if (file_exists($filePath)) { $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); if ($ext === 'pdf') { // pdftotext is available via poppler-utils // -layout preserves structure // -f 1 -l 20 limits to first 20 pages (usually sufficient for summary) $cmd = sprintf("pdftotext -layout -f 1 -l 20 %s -", escapeshellarg($filePath)); $text = shell_exec($cmd); } elseif (in_array($ext, ['txt', 'md', 'csv', 'json'])) { $text = file_get_contents($filePath); } } $text = trim((string)$text); // 2. Fallback to description/title if extraction failed or empty if ($text === '') { $descEn = trim((string) ($doc['description_en'] ?? '')); $descAr = trim((string) ($doc['description_ar'] ?? '')); $titleEn = trim((string) ($doc['title_en'] ?? '')); $titleAr = trim((string) ($doc['title_ar'] ?? '')); $text = "Title (EN): $titleEn\nTitle (AR): $titleAr\n\nDescription (EN): $descEn\nDescription (AR): $descAr"; } // 3. Truncate text to avoid token limits (approx 20k chars) if (strlen($text) > 20000) { $text = substr($text, 0, 20000) . "\n... [truncated]"; } // Use HEREDOC for cleaner string with quotes $prompt = << 'gpt-5-mini', 'input' => [ ['role' => 'system', 'content' => 'You are a helpful bilingual assistant that outputs JSON.'], ['role' => 'user', 'content' => $prompt], ] ]); if (empty($resp['success'])) { return ['success' => false, 'message' => 'AI request failed: ' . ($resp['error'] ?? 'Unknown error')]; } $outputText = LocalAIApi::extractText($resp); // 5. Parse JSON $sumEn = ''; $sumAr = ''; // Attempt to parse JSON from response $jsonStart = strpos($outputText, '{'); $jsonEnd = strrpos($outputText, '}'); if ($jsonStart !== false && $jsonEnd !== false) { $jsonStr = substr($outputText, $jsonStart, $jsonEnd - $jsonStart + 1); $data = json_decode($jsonStr, true); if (is_array($data)) { $sumEn = $data['summary_en'] ?? ''; $sumAr = $data['summary_ar'] ?? ''; } } // Fallback if JSON parsing failed if (!$sumEn && !$sumAr) { $sumEn = $outputText; // Just dump the text $sumAr = ''; } // 6. Update DB $stmt = db()->prepare('UPDATE library_documents SET summary_text = ?, summary_en = ?, summary_ar = ? WHERE id = ?'); $combined = "English: $sumEn\n\nArabic: $sumAr"; $stmt->execute([$combined, $sumEn, $sumAr, $id]); return ['success' => true, 'message' => 'Summary generated successfully from document content.']; } // --- Category Functions --- function library_get_categories(string $search = ''): array { // Backward compatibility $result = library_get_categories_paginated($search); return $result['data']; } function library_get_categories_paginated(string $search = '', int $limit = 0, int $offset = 0): array { library_bootstrap(); $sql = 'SELECT * FROM library_categories WHERE 1=1'; $countSql = 'SELECT COUNT(*) FROM library_categories WHERE 1=1'; $params = []; if ($search !== '') { $clause = ' AND (name_en LIKE ? OR name_ar LIKE ?)'; $sql .= $clause; $countSql .= $clause; $params[] = "%$search%"; $params[] = "%$search%"; } $sql .= ' ORDER BY name_en ASC'; if ($limit > 0) { $sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = db()->prepare($sql); $stmt->execute($params); $data = $stmt->fetchAll() ?: []; $stmtCount = db()->prepare($countSql); $stmtCount->execute($params); $total = (int)$stmtCount->fetchColumn(); return ['data' => $data, 'total' => $total]; } function library_get_subcategories(?int $categoryId = null, string $search = ''): array { // Backward compatibility $result = library_get_subcategories_paginated($categoryId, $search); return $result['data']; } function library_get_subcategories_paginated(?int $categoryId = null, string $search = '', int $limit = 0, int $offset = 0): array { library_bootstrap(); $sql = 'SELECT * FROM library_subcategories WHERE 1=1'; $countSql = 'SELECT COUNT(*) FROM library_subcategories WHERE 1=1'; $params = []; if ($categoryId !== null) { $clause = ' AND category_id = ?'; $sql .= $clause; $countSql .= $clause; $params[] = $categoryId; } if ($search !== '') { $clause = ' AND (name_en LIKE ? OR name_ar LIKE ?)'; $sql .= $clause; $countSql .= $clause; $params[] = "%$search%"; $params[] = "%$search%"; } $sql .= ' ORDER BY name_en ASC'; if ($limit > 0) { $sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = db()->prepare($sql); $stmt->execute($params); $data = $stmt->fetchAll() ?: []; $stmtCount = db()->prepare($countSql); $stmtCount->execute($params); $total = (int)$stmtCount->fetchColumn(); return ['data' => $data, 'total' => $total]; } 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 --- // --- Type Functions --- function library_get_types(string $search = ''): array { $result = library_get_types_paginated($search); return $result['data']; } function library_get_types_paginated(string $search = '', int $limit = 0, int $offset = 0): array { library_bootstrap(); $sql = 'SELECT * FROM library_types WHERE 1=1'; $countSql = 'SELECT COUNT(*) FROM library_types WHERE 1=1'; $params = []; if ($search !== '') { $clause = ' AND (name_en LIKE ? OR name_ar LIKE ?)'; $sql .= $clause; $countSql .= $clause; $params[] = "%$search%"; $params[] = "%$search%"; } $sql .= ' ORDER BY name_en ASC'; if ($limit > 0) { $sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = db()->prepare($sql); $stmt->execute($params); $data = $stmt->fetchAll() ?: []; $stmtCount = db()->prepare($countSql); $stmtCount->execute($params); $total = (int)$stmtCount->fetchColumn(); return ['data' => $data, 'total' => $total]; } function library_create_type(string $nameEn, string $nameAr): int { library_bootstrap(); $stmt = db()->prepare('INSERT INTO library_types (name_en, name_ar) VALUES (?, ?)'); $stmt->execute([$nameEn, $nameAr]); return (int) db()->lastInsertId(); } function library_get_type_by_id(int $id): ?array { library_bootstrap(); $stmt = db()->prepare('SELECT * FROM library_types WHERE id = ?'); $stmt->execute([$id]); return $stmt->fetch() ?: null; } function library_update_type(int $id, string $nameEn, string $nameAr): void { library_bootstrap(); $stmt = db()->prepare('UPDATE library_types SET name_en = ?, name_ar = ? WHERE id = ?'); $stmt->execute([$nameEn, $nameAr, $id]); } function library_delete_type(int $id): void { library_bootstrap(); $stmt = db()->prepare('DELETE FROM library_types WHERE id = ?'); $stmt->execute([$id]); } // --- End Type Functions --- function library_fetch_documents(bool $publicOnly = false, array $filters = []): array { // Backward compatibility $result = library_fetch_documents_paginated($publicOnly, $filters); return $result['data']; } function library_fetch_documents_paginated(bool $publicOnly = false, array $filters = [], int $limit = 0, int $offset = 0): 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, t.name_en as type_en, t.name_ar as type_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 LEFT JOIN library_types t ON d.type_id = t.id WHERE 1=1'; $countSql = 'SELECT COUNT(*) 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 LEFT JOIN library_types t ON d.type_id = t.id WHERE 1=1'; $params = []; if ($publicOnly) { $clause = ' AND d.visibility = :visibility'; $sql .= $clause; $countSql .= $clause; $params[':visibility'] = 'public'; } // Apply Search Filters if (!empty($filters['q'])) { $q = trim($filters['q']); $clause = ' AND (d.title_en LIKE :q OR d.title_ar LIKE :q OR d.author LIKE :q OR d.description_en LIKE :q OR d.description_ar LIKE :q OR d.summary_en LIKE :q)'; $sql .= $clause; $countSql .= $clause; $params[':q'] = "%$q%"; } $sql .= ' ORDER BY d.is_featured DESC, d.created_at DESC'; if ($limit > 0) { $sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->execute(); $data = $stmt->fetchAll() ?: []; $stmtCount = db()->prepare($countSql); foreach ($params as $key => $value) { $stmtCount->bindValue($key, $value); } $stmtCount->execute(); $total = (int)$stmtCount->fetchColumn(); return ['data' => $data, 'total' => $total]; } function library_fetch_document(int $id, bool $publicOnly = false): ?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, t.name_en as type_en, t.name_ar as type_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 LEFT JOIN library_types t ON d.type_id = t.id WHERE d.id = :id'; $params = [':id' => $id]; if ($publicOnly) { $sql .= ' AND d.visibility = :visibility'; $params[':visibility'] = 'public'; } $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->execute(); return $stmt->fetch() ?: null; } 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_handle_cover_image(array $file): ?string { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { return null; } $originalName = (string) ($file['name'] ?? ''); $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); $allowed = ['jpg', 'jpeg', 'png', 'webp', 'gif']; if (!in_array($extension, $allowed)) { throw new RuntimeException('Unsupported cover image type. Allowed: jpg, png, webp, gif.'); } $size = (int) ($file['size'] ?? 0); if ($size <= 0 || $size > 5 * 1024 * 1024) { throw new RuntimeException('Cover image must be smaller than 5 MB.'); } $safeBase = preg_replace('/[^a-zA-Z0-9_-]+/', '-', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'cover'; $storedName = strtolower(date('YmdHis') . '-' . $safeBase . '-' . bin2hex(random_bytes(4)) . '.' . $extension); $relativePath = 'uploads/covers/' . $storedName; $absolutePath = __DIR__ . '/../' . $relativePath; if (!move_uploaded_file((string) $file['tmp_name'], $absolutePath)) { throw new RuntimeException('Unable to save the cover image.'); } return $relativePath; } function library_create_document(array $payload, array $file, array $coverFile = []): 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; $typeId = !empty($payload['type_id']) ? (int)$payload['type_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; // New Fields $publisher = trim((string) ($payload['publisher'] ?? '')); $publishYear = !empty($payload['publish_year']) ? (int)$payload['publish_year'] : null; $author = trim((string) ($payload['author'] ?? '')); $country = trim((string) ($payload['country'] ?? '')); $pageCount = !empty($payload['page_count']) ? (int)$payload['page_count'] : null; $summaryEn = trim((string) ($payload['summary_en'] ?? '')); $summaryAr = trim((string) ($payload['summary_ar'] ?? '')); $descriptionEn = trim((string) ($payload['description_en'] ?? '')); $descriptionAr = trim((string) ($payload['description_ar'] ?? '')); $fileData = library_handle_uploaded_file(file: $file); $coverPath = library_handle_cover_image($coverFile); $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, cover_image_path, publisher, publish_year, author, country, type_id, page_count, summary_en, summary_ar, description_en, description_ar ) 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, :cover_image_path, :publisher, :publish_year, :author, :country, :type_id, :page_count, :summary_en, :summary_ar, :description_en, :description_ar )'); $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, ':cover_image_path' => $coverPath, ':publisher' => $publisher ?: null, ':publish_year' => $publishYear, ':author' => $author ?: null, ':country' => $country ?: null, ':type_id' => $typeId, ':page_count' => $pageCount, ':summary_en' => $summaryEn ?: null, ':summary_ar' => $summaryAr ?: null, ':description_en' => $descriptionEn ?: null, ':description_ar' => $descriptionAr ?: null, ]); return (int) db()->lastInsertId(); } function library_update_document(int $id, array $payload, array $file = [], array $coverFile = []): void { library_bootstrap(); // Fetch existing document to keep file if not replaced $existing = library_fetch_document($id); if (!$existing) { throw new RuntimeException('Document not found.'); } $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; $typeId = !empty($payload['type_id']) ? (int)$payload['type_id'] : null; $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; $publisher = trim((string) ($payload['publisher'] ?? '')); $publishYear = !empty($payload['publish_year']) ? (int)$payload['publish_year'] : null; $author = trim((string) ($payload['author'] ?? '')); $country = trim((string) ($payload['country'] ?? '')); $pageCount = !empty($payload['page_count']) ? (int)$payload['page_count'] : null; $summaryEn = trim((string) ($payload['summary_en'] ?? '')); $summaryAr = trim((string) ($payload['summary_ar'] ?? '')); $descriptionEn = trim((string) ($payload['description_en'] ?? '')); $descriptionAr = trim((string) ($payload['description_ar'] ?? '')); // Handle File Update $fileData = null; if (!empty($file['name'])) { $fileData = library_handle_uploaded_file($file); } // Handle Cover Update $coverPath = null; if (!empty($coverFile['name'])) { $coverPath = library_handle_cover_image($coverFile); } $sql = 'UPDATE library_documents SET title_en = :title_en, title_ar = :title_ar, category = :category, category_ar = :category_ar, sub_category = :sub_category, sub_category_ar = :sub_category_ar, category_id = :category_id, subcategory_id = :subcategory_id, visibility = :visibility, allow_download = :allow_download, allow_print = :allow_print, allow_copy = :allow_copy, publisher = :publisher, publish_year = :publish_year, author = :author, country = :country, type_id = :type_id, page_count = :page_count, summary_en = :summary_en, summary_ar = :summary_ar, description_en = :description_en, description_ar = :description_ar'; $params = [ ':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, ':allow_download' => $allow_download, ':allow_print' => $allow_print, ':allow_copy' => $allow_copy, ':publisher' => $publisher ?: null, ':publish_year' => $publishYear, ':author' => $author ?: null, ':country' => $country ?: null, ':type_id' => $typeId, ':page_count' => $pageCount, ':summary_en' => $summaryEn ?: null, ':summary_ar' => $summaryAr ?: null, ':description_en' => $descriptionEn ?: null, ':description_ar' => $descriptionAr ?: null, ':id' => $id, ]; if ($fileData) { $sql .= ', document_type = :document_type, file_name = :file_name, file_path = :file_path, file_size_kb = :file_size_kb'; $params[':document_type'] = $fileData['document_type']; $params[':file_name'] = $fileData['file_name']; $params[':file_path'] = $fileData['file_path']; $params[':file_size_kb'] = $fileData['file_size_kb']; } if ($coverPath) { $sql .= ', cover_image_path = :cover_image_path'; $params[':cover_image_path'] = $coverPath; } $sql .= ' WHERE id = :id'; $stmt = db()->prepare($sql); $stmt->execute($params); } function library_delete_document(int $id): void { library_bootstrap(); // Optionally delete files, but for safety we might keep them or delete them. // For now, just delete DB record. $stmt = db()->prepare('DELETE FROM library_documents WHERE id = ?'); $stmt->execute([$id]); } function library_render_pagination(int $currentPage, int $totalPages, string $baseUrl): void { if ($totalPages <= 1) { return; } $queryParts = parse_url($baseUrl, PHP_URL_QUERY); parse_str($queryParts ?? '', $queryParams); $buildUrl = function(int $page) use ($baseUrl, $queryParams) { $queryParams['page'] = $page; $path = parse_url($baseUrl, PHP_URL_PATH); return $path . '?' . http_build_query($queryParams); }; echo ''; }