443 lines
14 KiB
PHP
443 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Admin;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\PageModel;
|
|
use App\Models\AuditLogModel;
|
|
use App\Services\ContentRenderer;
|
|
|
|
class Pages extends BaseController
|
|
{
|
|
protected $pageModel;
|
|
protected $auditLogModel;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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 = '<p><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><cite><pre><code><table><tbody><tr><td><th><hr><figure><img><figcaption><a><div><strong><em><u><s><br>';
|
|
|
|
// Strip all tags except allowed
|
|
$html = strip_tags($html, $allowedTags);
|
|
|
|
// Remove dangerous attributes
|
|
$html = preg_replace('/on\w+="[^"]*"/i', '', $html);
|
|
$html = preg_replace('/javascript:/i', '', $html);
|
|
|
|
return $html;
|
|
}
|
|
}
|