pageModel = new PageModel(); $this->auditLogModel = new AuditLogModel(); } /** * Display list of pages */ public function index() { $perPage = 10; $page = $this->request->getGet('page') ?? 1; $status = $this->request->getGet('status'); $search = $this->request->getGet('search'); // Build query with filters $this->pageModel->select('pages.*'); // Filter by status if ($status && in_array($status, ['draft', 'published'])) { $this->pageModel->where('pages.status', $status); } // Search if ($search) { $this->pageModel->groupStart() ->like('pages.title', $search) ->orLike('pages.content_html', $search) ->orLike('pages.excerpt', $search) ->groupEnd(); } // Get paginated results $pages = $this->pageModel->orderBy('pages.created_at', 'DESC') ->paginate($perPage, 'default', $page); $pager = $this->pageModel->pager; $data = [ 'title' => 'Halaman', 'pages' => $pages, 'pager' => $pager, 'currentStatus' => $status, 'currentSearch' => $search, 'stats' => [ 'total' => $this->pageModel->countByStatus(), 'published' => $this->pageModel->countByStatus('published'), 'draft' => $this->pageModel->countByStatus('draft'), ], ]; return view('admin/pages/index', $data); } /** * Show form to create new page */ public function create() { $data = [ 'title' => 'Tambah Halaman', 'page' => null, ]; return view('admin/pages/form', $data); } /** * Store new page */ public function store() { $validation = \Config\Services::validation(); $rules = [ 'title' => 'required|min_length[3]|max_length[255]', 'content_json' => 'permit_empty', 'status' => 'required|in_list[draft,published]', ]; if (!$this->validate($rules)) { return redirect()->back() ->withInput() ->with('validation', $validation); } $title = $this->request->getPost('title'); $slug = $this->pageModel->generateSlug($title); $contentJson = $this->request->getPost('content_json') ?? '{}'; $contentHtml = $this->request->getPost('content_html') ?? ''; $excerpt = $this->request->getPost('excerpt') ?? ''; $featuredImage = $this->request->getPost('featured_image') ?? null; $status = $this->request->getPost('status'); $userId = session()->get('user_id'); // Validate and parse JSON $blocks = []; if (!empty($contentJson)) { $parsed = json_decode($contentJson, true); if (json_last_error() === JSON_ERROR_NONE && isset($parsed['blocks'])) { $blocks = $parsed['blocks']; } } // Render HTML from JSON if not provided if (empty($contentHtml) && !empty($blocks)) { $contentHtml = ContentRenderer::renderEditorJsToHtml($blocks); } // Sanitize HTML $contentHtml = $this->sanitizeHtml($contentHtml); // Extract excerpt if empty if (empty($excerpt) && !empty($blocks)) { $excerpt = ContentRenderer::extractExcerpt($blocks); } $data = [ 'title' => $title, 'slug' => $slug, 'content' => $contentHtml, // Keep for backward compatibility 'content_json' => $contentJson, 'content_html' => $contentHtml, 'excerpt' => $excerpt, 'featured_image' => $featuredImage, 'status' => $status, ]; if ($this->pageModel->insert($data)) { // Log action $this->auditLogModel->logAction('page_created', $userId); return redirect()->to('/admin/pages') ->with('success', 'Halaman berhasil ditambahkan.'); } return redirect()->back() ->withInput() ->with('error', 'Gagal menambahkan halaman.'); } /** * Show form to edit page */ public function edit($id) { $page = $this->pageModel->find($id); if (!$page) { return redirect()->to('/admin/pages') ->with('error', 'Halaman tidak ditemukan.'); } $data = [ 'title' => 'Edit Halaman', 'page' => $page, ]; return view('admin/pages/form', $data); } /** * Update page */ public function update($id) { $page = $this->pageModel->find($id); if (!$page) { return redirect()->to('/admin/pages') ->with('error', 'Halaman tidak ditemukan.'); } $validation = \Config\Services::validation(); $rules = [ 'title' => 'required|min_length[3]|max_length[255]', 'content_json' => 'permit_empty', 'status' => 'required|in_list[draft,published]', ]; if (!$this->validate($rules)) { return redirect()->back() ->withInput() ->with('validation', $validation); } $title = $this->request->getPost('title'); $oldTitle = $page['title']; $contentJson = $this->request->getPost('content_json') ?? '{}'; $contentHtml = $this->request->getPost('content_html') ?? ''; $excerpt = $this->request->getPost('excerpt') ?? ''; $featuredImage = $this->request->getPost('featured_image') ?? null; $status = $this->request->getPost('status'); $userId = session()->get('user_id'); // Validate and parse JSON $blocks = []; if (!empty($contentJson)) { $parsed = json_decode($contentJson, true); if (json_last_error() === JSON_ERROR_NONE && isset($parsed['blocks'])) { $blocks = $parsed['blocks']; } } // Render HTML from JSON if not provided if (empty($contentHtml) && !empty($blocks)) { $contentHtml = ContentRenderer::renderEditorJsToHtml($blocks); } // Sanitize HTML $contentHtml = $this->sanitizeHtml($contentHtml); // Extract excerpt if empty if (empty($excerpt) && !empty($blocks)) { $excerpt = ContentRenderer::extractExcerpt($blocks); } // Generate new slug if title changed $slug = ($title !== $oldTitle) ? $this->pageModel->generateSlug($title, $id) : $page['slug']; $data = [ 'title' => $title, 'slug' => $slug, 'content' => $contentHtml, // Keep for backward compatibility 'content_json' => $contentJson, 'content_html' => $contentHtml, 'excerpt' => $excerpt, 'featured_image' => $featuredImage, 'status' => $status, ]; try { $this->pageModel->skipValidation(true); $result = $this->pageModel->update($id, $data); if ($result === false) { $errors = $this->pageModel->errors(); $errorMessage = !empty($errors) ? implode(', ', $errors) : 'Gagal memperbarui halaman.'; log_message('error', 'Page update failed - ID: ' . $id . ', Errors: ' . json_encode($errors)); return redirect()->back() ->withInput() ->with('error', $errorMessage); } // Log action $this->auditLogModel->logAction('page_updated', $userId); return redirect()->to('/admin/pages') ->with('success', 'Halaman berhasil diperbarui.'); } catch (\Exception $e) { log_message('error', 'Page update exception - ID: ' . $id . ', Error: ' . $e->getMessage()); return redirect()->back() ->withInput() ->with('error', 'Terjadi kesalahan saat memperbarui halaman: ' . $e->getMessage()); } } /** * Autosave page (AJAX) */ public function autosave($id) { if (!$this->request->isAJAX()) { return $this->response->setJSON(['success' => false, 'message' => 'Invalid request']); } $page = $this->pageModel->find($id); if (!$page) { return $this->response->setJSON(['success' => false, 'message' => 'Page not found']); } $contentJson = $this->request->getPost('content_json') ?? '{}'; $contentHtml = $this->request->getPost('content_html') ?? ''; // Validate JSON $blocks = []; if (!empty($contentJson)) { $parsed = json_decode($contentJson, true); if (json_last_error() === JSON_ERROR_NONE && isset($parsed['blocks'])) { $blocks = $parsed['blocks']; } } // Render HTML if not provided if (empty($contentHtml) && !empty($blocks)) { $contentHtml = ContentRenderer::renderEditorJsToHtml($blocks); } // Sanitize HTML $contentHtml = $this->sanitizeHtml($contentHtml); // Extract excerpt $excerpt = ''; if (!empty($blocks)) { $excerpt = ContentRenderer::extractExcerpt($blocks); } $data = [ 'content_json' => $contentJson, 'content_html' => $contentHtml, 'excerpt' => $excerpt, ]; try { $this->pageModel->skipValidation(true); $this->pageModel->update($id, $data); return $this->response->setJSON([ 'success' => true, 'message' => 'Autosaved', 'timestamp' => date('Y-m-d H:i:s'), ]); } catch (\Exception $e) { log_message('error', 'Autosave failed - ID: ' . $id . ', Error: ' . $e->getMessage()); return $this->response->setJSON(['success' => false, 'message' => 'Autosave failed']); } } /** * Upload image (AJAX) */ public function upload() { if (!$this->request->isAJAX()) { return $this->response->setJSON(['success' => 0, 'message' => 'Invalid request']); } $file = $this->request->getFile('image'); if (!$file || !$file->isValid()) { return $this->response->setJSON(['success' => 0, 'message' => 'No file uploaded']); } // Validate file type $allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!in_array($file->getMimeType(), $allowedTypes)) { return $this->response->setJSON(['success' => 0, 'message' => 'Invalid file type. Only JPG, PNG, and WebP are allowed.']); } // Validate file size (2MB max) if ($file->getSize() > 2 * 1024 * 1024) { return $this->response->setJSON(['success' => 0, 'message' => 'File size exceeds 2MB limit.']); } // Generate random filename $extension = $file->getExtension(); $newName = uniqid('page_', true) . '.' . $extension; $uploadPath = WRITEPATH . 'uploads/pages/'; // Create directory if not exists if (!is_dir($uploadPath)) { mkdir($uploadPath, 0755, true); } // Move file if ($file->move($uploadPath, $newName)) { $url = base_url('writable/uploads/pages/' . $newName); return $this->response->setJSON([ 'success' => 1, 'file' => [ 'url' => $url, ], ]); } return $this->response->setJSON(['success' => 0, 'message' => 'Upload failed']); } /** * Delete page */ public function delete($id) { $page = $this->pageModel->find($id); if (!$page) { return redirect()->to('/admin/pages') ->with('error', 'Halaman tidak ditemukan.'); } $userId = session()->get('user_id'); if ($this->pageModel->delete($id)) { // Log action $this->auditLogModel->logAction('page_deleted', $userId); return redirect()->to('/admin/pages') ->with('success', 'Halaman berhasil dihapus.'); } return redirect()->to('/admin/pages') ->with('error', 'Gagal menghapus halaman.'); } /** * Sanitize HTML using basic PHP functions * For production, consider using HTMLPurifier library * * @param string $html * @return string */ protected function sanitizeHtml(string $html): string { // Basic sanitization - allow common HTML tags $allowedTags = '