814 lines
36 KiB
PHP
Executable File
814 lines
36 KiB
PHP
Executable File
<?php
|
|
|
|
// declare(strict_types=1);
|
|
|
|
namespace App\Controllers;
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
use CodeIgniter\API\ResponseTrait;
|
|
|
|
use App\Models\AddImgModel;
|
|
use App\Models\FEContentModel;
|
|
use App\Models\ClientModel;
|
|
use App\Models\FAQModel;
|
|
|
|
class AppContentManagementController extends AdminController
|
|
{
|
|
use ResponseTrait;
|
|
protected $myLogger;
|
|
protected $addImgModel;
|
|
protected $feContentModel;
|
|
protected $clientModel;
|
|
protected $faqModel;
|
|
|
|
|
|
public function __construct()
|
|
{
|
|
$this->myLogger = \Config\Services::mylogger();
|
|
$this->addImgModel = new AddImgModel();
|
|
$this->feContentModel = new FEContentModel();
|
|
$this->clientModel = new ClientModel();
|
|
$this->faqModel = new FAQModel();
|
|
}
|
|
//listing
|
|
public function add_image_index()
|
|
{
|
|
$headerData['tab_name'] = 'Advertisement Images';
|
|
$headerData['page_name'] = 'Advertisement Images';
|
|
// $data['addImageList'] = $this->addImgModel->findAll();
|
|
$data['addImageList'] = $this->addImgModel->select('advertisement_images.*,
|
|
clients.client_name,
|
|
clients.short_name,
|
|
CASE WHEN advertisement_images.is_active = 1 THEN "Active" ELSE "Inactive" END AS status', false)
|
|
->join('clients', 'advertisement_images.client_id = clients.id', 'left')
|
|
->where('advertisement_images.is_active', 1)
|
|
->findAll();
|
|
|
|
$data['client'] = $this->clientModel->where('is_active', 1)->where('client_type', 1)->findAll();
|
|
|
|
// dd($data);
|
|
|
|
echo view('layout/header', $headerData);
|
|
echo view('add_image_list', $data);
|
|
echo view('layout/footer');
|
|
|
|
// $this->loadLayout('client_onboarding', $data);
|
|
}
|
|
|
|
// add and edit
|
|
public function add_advertise_image() {
|
|
try {
|
|
|
|
$data = $this->request->getPost();
|
|
$sanitized_post_data = sanitizeInputArrayAdvanced($data);
|
|
$id = (int)($sanitized_post_data['add_image_id'] ?? 0);
|
|
|
|
$rules = [
|
|
'add_image_id' => [
|
|
'rules' => 'permit_empty|integer|is_natural',
|
|
'errors' => [
|
|
'integer' => 'Image ID must be a valid number',
|
|
'is_natural' => 'Image ID must be a non-negative number'
|
|
]
|
|
],
|
|
'client_id' => [
|
|
'rules' => 'required|integer|is_natural',
|
|
'errors' => [
|
|
'required' => 'Client is required',
|
|
'integer' => 'Invalid client selected',
|
|
'is_natural' => 'Invalid client selected',
|
|
]
|
|
],
|
|
'advertise_image' => [
|
|
'rules' => ($id === 0 ? 'uploaded[advertise_image]|' : '')
|
|
. 'is_image[advertise_image]'
|
|
. '|mime_in[advertise_image,image/jpg,image/jpeg,image/png]'
|
|
. '|max_size[advertise_image,200]'
|
|
. '|max_dims[advertise_image,1640,664]',
|
|
'errors' => [
|
|
'uploaded' => 'Image is required',
|
|
'is_image' => 'File must be an image',
|
|
'mime_in' => 'Only JPG, JPEG, PNG allowed',
|
|
'max_size' => 'Image size must not exceed 200 KB',
|
|
'max_dims' => 'Image dimensions must not exceed 1640x664 pixels',
|
|
]
|
|
],
|
|
];
|
|
if (!$this->validate($rules)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Input validation failed',
|
|
'code' => 400,
|
|
'errors' => $this->validator->getErrors()
|
|
]);
|
|
}
|
|
|
|
|
|
$file = $this->request->getFile('advertise_image');
|
|
$client_id = (int)($sanitized_post_data['client_id'] ?? 0);
|
|
|
|
if ($client_id !== 0) {
|
|
$clientExists = $this->clientModel->where('id', $client_id)->where('is_active', 1)->first();
|
|
if (!$clientExists) {
|
|
return $this->respond(['status' => false, 'message' => 'Invalid client selected.'], 400);
|
|
}
|
|
}
|
|
|
|
if (!$file || !$file->isValid()) {
|
|
return $this->respond(['status' => false, 'message' => 'No file uploaded or invalid file.'], 400);
|
|
}
|
|
|
|
if (!validate_upload_extension($file, UPLOAD_EXT_IMAGES)) {
|
|
return $this->respond(['status' => false, 'message' => 'Only JPG, JPEG, PNG files are allowed.'], 400);
|
|
}
|
|
|
|
$dimInfo = @getimagesize($file->getTempName());
|
|
if ($dimInfo === false || (int) $dimInfo[0] !== 1640 || (int) $dimInfo[1] !== 664) {
|
|
return $this->respond(['status' => false, 'message' => 'Image dimensions must be exactly 1640x664 pixels.'], 400);
|
|
}
|
|
|
|
$fileName = sanitize_upload_filename($file->getClientName());
|
|
$existing = $this->addImgModel->where('name', $fileName)->where('client_id', $client_id)->where('is_active', 1)->first();
|
|
if($existing){ return $this->respond(['status' => false, 'message' => 'This file has already been uploaded in active state.'], 400); }
|
|
|
|
$uploadPath = ROOTPATH . 'public/uploads/add_image_upload/';
|
|
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
|
|
|
|
$file->move($uploadPath, $fileName);
|
|
|
|
$details = ['name' => $fileName,'client_id'=>$client_id];
|
|
|
|
if ($id === 0) {
|
|
$this->addImgModel->insert($details);
|
|
} else {
|
|
$existingRecord = $this->addImgModel->find($id);
|
|
if (!$existingRecord) {
|
|
return $this->respond(['status' => false, 'message' => 'Record not found for update.'], 404);
|
|
}
|
|
$this->addImgModel->update($id, $details);
|
|
}
|
|
|
|
return $this->respond(['status' => true, 'message' => 'Image saved successfully.']);
|
|
} catch (\Exception $e) {
|
|
return $this->respond(['status' => false, 'message' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
// soft delete
|
|
public function remove_advertise_image()
|
|
{
|
|
try {
|
|
$rules = [
|
|
'add_image_id' => [
|
|
'rules' => 'required|integer|is_natural_no_zero',
|
|
'errors' => [
|
|
'required' => 'Image ID is required',
|
|
'integer' => 'Image ID must be a valid number',
|
|
'is_natural_no_zero' => 'Image ID must be a positive number'
|
|
]
|
|
]
|
|
];
|
|
|
|
if (!$this->validate($rules)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Input validation failed',
|
|
'code' => 400,
|
|
'errors' => $this->validator->getErrors()
|
|
]);
|
|
}
|
|
|
|
$id = (int)$this->request->getPost('add_image_id');
|
|
|
|
$existing = $this->addImgModel->find($id);
|
|
if (!$existing) {
|
|
return $this->respond(['status' => false, 'message' => 'Record not found'], 404);
|
|
}
|
|
|
|
$this->addImgModel->update($id, ['is_active' => 0]);
|
|
|
|
return $this->respond(['status' => true, 'message' => 'Deleted successfully']);
|
|
|
|
} catch (\Exception $e) {
|
|
return $this->respond(['status' => false, 'message' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
|
|
// Preview image Went Edit.
|
|
public function showAdvertiseImage($filename)
|
|
{
|
|
$filename = basename($filename);
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9_\-]+\.(jpg|jpeg|png|gif|webp)$/i', $filename)) {
|
|
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
|
}
|
|
|
|
$path = ROOTPATH . 'public/uploads/add_image_upload/' . $filename;
|
|
$realPath = realpath($path);
|
|
$allowedDir = realpath(ROOTPATH . 'public/uploads/add_image_upload');
|
|
|
|
if (!$realPath || strpos($realPath, $allowedDir) !== 0 || !file_exists($realPath)) {
|
|
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
|
}
|
|
|
|
$mime = mime_content_type($realPath);
|
|
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
if (!in_array($mime, $allowedMimes, true)) {
|
|
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
|
}
|
|
|
|
return $this->response->setHeader('Content-Type', $mime)->setBody(file_get_contents($realPath));
|
|
}
|
|
|
|
|
|
|
|
public function frontend_content()
|
|
{
|
|
|
|
$method = $this->request->getMethod();
|
|
|
|
if ($this->request->getMethod() === 'post') {
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* STEP 1: INITIAL VALIDATION
|
|
* --------------------------------------------------------------------------
|
|
* These are the basic validation rules. For 'content' and 'notes', we only
|
|
* check if they are provided and within the allowed length.
|
|
* The more advanced security check for script tags happens next.
|
|
*/
|
|
$rules = [
|
|
'fe_id' => [
|
|
'rules' => 'permit_empty|integer|is_natural',
|
|
'errors' => [
|
|
'integer' => 'ID must be a valid number',
|
|
'is_natural' => 'ID must be a non-negative number'
|
|
]
|
|
],
|
|
'type' => [
|
|
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9_ \-]+$/]',
|
|
'errors' => [
|
|
'required' => 'Type is required',
|
|
'max_length' => 'Type cannot exceed 255 characters',
|
|
'regex_match' => 'Type can contain only letters, numbers, spaces, _ and -'
|
|
]
|
|
],
|
|
'content_section' => [
|
|
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9_ \-]+$/]',
|
|
'errors' => [
|
|
'required' => 'Content Section is required',
|
|
'max_length' => 'Content Section cannot exceed 255 characters',
|
|
'regex_match' => 'Content Section can contain only letters, numbers, spaces, _ and -'
|
|
]
|
|
],
|
|
'heading' => [
|
|
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/]+$/]',
|
|
'errors' => [
|
|
'required' => 'Heading is required',
|
|
'max_length' => 'Heading cannot exceed 255 characters',
|
|
'regex_match' => 'Heading can contain only letters, numbers, spaces, and these characters: . , ; : ! ? ( ) & / -'
|
|
]
|
|
],
|
|
'content' => [
|
|
'rules' => 'required|max_length[5000]',
|
|
'errors' => [
|
|
'required' => 'Content is required',
|
|
'max_length' => 'Content cannot exceed 5000 characters',
|
|
]
|
|
],
|
|
'notes' => [
|
|
'rules' => 'required|max_length[1500]',
|
|
'errors' => [
|
|
'required' => 'Notes are required',
|
|
'max_length' => 'Notes cannot exceed 1500 characters',
|
|
]
|
|
],
|
|
];
|
|
|
|
if (!$this->validate($rules)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Input validation failed',
|
|
'code' => 400,
|
|
'errors' => $this->validator->getErrors()
|
|
]);
|
|
}
|
|
|
|
/**************************************************************************
|
|
* REFACTORED SANITIZATION LOGIC (XSS Protection)
|
|
**************************************************************************
|
|
*
|
|
* Per the user's request, we are avoiding the generic `sanitizeInputArrayAdvanced`
|
|
* on the `content` and `notes` fields, as they require special HTML
|
|
* handling.
|
|
*
|
|
* The new process is:
|
|
* 1. Get the raw `content` and `notes` directly from the POST request.
|
|
* 2. Perform the critical XSS validation on this raw content using `hasXssTags()`.
|
|
* If it fails, the request is rejected immediately. This satisfies all
|
|
* the failure test cases (Tests 4-9).
|
|
* 3. Take all *other* POST data and sanitize it using the generic
|
|
* `sanitizeInputArrayAdvanced` function.
|
|
* 4. Sanitize the now-validated `content` and `notes` using our specific
|
|
* `sanitizeHtml()` function, which allows safe HTML.
|
|
* 5. Combine the sanitized data into a final array for database insertion.
|
|
*
|
|
*************************************************************************/
|
|
|
|
// Step 1: Get raw `content` and `notes`.
|
|
$rawContent = $this->request->getPost('content');
|
|
$rawNotes = $this->request->getPost('notes');
|
|
|
|
// Step 2: Perform critical XSS validation on raw input.
|
|
$xssErrors = [];
|
|
if ($this->hasXssTags($rawContent)) {
|
|
$xssErrors['content'] = 'Content contains restricted tags. Script, iframe and event handlers are not allowed';
|
|
}
|
|
if ($this->hasXssTags($rawNotes)) {
|
|
$xssErrors['notes'] = 'Notes contains restricted tags. Script, iframe and event handlers are not allowed';
|
|
}
|
|
|
|
if (!empty($xssErrors)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Input validation failed',
|
|
'code' => 400,
|
|
'errors' => $xssErrors
|
|
]);
|
|
}
|
|
|
|
// Step 3: Sanitize all *other* POST data.
|
|
$otherPostData = $this->request->getPost();
|
|
unset($otherPostData['content'], $otherPostData['notes']);
|
|
$data = sanitizeInputArrayAdvanced($otherPostData);
|
|
|
|
// Step 4 & 5: Sanitize and re-combine `content` and `notes`.
|
|
$data['content'] = $this->sanitizeHtml($rawContent);
|
|
$data['notes'] = $this->sanitizeHtml($rawNotes);
|
|
|
|
$id = $data['fe_id'] ?? null;
|
|
|
|
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false, 'message' => 'Invalid ID format', 'code' => 400
|
|
]);
|
|
}
|
|
$id = !empty($id) ? (int)$id : null;
|
|
|
|
unset($data['fe_id']);
|
|
|
|
foreach ($data as $k => $v) {
|
|
if ($v === '' || $v === null) {unset($data[$k]);}
|
|
}
|
|
|
|
// INSERT / UPDATE
|
|
if (empty($id)) {
|
|
$status = $this->feContentModel->insert($data);
|
|
$text = "Created";
|
|
} else {
|
|
$existing = $this->feContentModel->find($id);
|
|
if (!$existing) {
|
|
return $this->response->setStatusCode(404)->setJSON([
|
|
'status' => false, 'message' => 'Record not found', 'code' => 404
|
|
]);
|
|
}
|
|
$status = $this->feContentModel->update($id, $data);
|
|
$text = "Updated";
|
|
}
|
|
|
|
return $this->respond([
|
|
'status' => $status ? true : false,
|
|
'message' => "Frontend Content $text " . ($status ? 'successfully' : 'failed'),
|
|
'code' => $status ? 200 : 400,
|
|
'data' => $data,
|
|
] , $status ? 200 : 400 );
|
|
}
|
|
elseif ($method === 'get') {
|
|
|
|
$id = $this->request->getGet('fe_id') ?? null;
|
|
|
|
if (!empty($id)) {
|
|
|
|
if (!ctype_digit((string)$id) || (int)$id <= 0) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false, 'code' => 400, 'message' => 'Invalid ID format'
|
|
]);
|
|
}
|
|
$id = (int)$id;
|
|
|
|
$data['fe_list'] = $this->feContentModel->where('id', $id)->orderBy('id', 'DESC')->findAll();
|
|
|
|
if (!empty($data)) {
|
|
return $this->respond(['status' => true, 'code' => 200, 'data' => $data], 200);
|
|
} else {
|
|
return $this->respond(['status' => false, 'code' => 400, 'message' => 'No data found'], 200);
|
|
}
|
|
}
|
|
|
|
$data['fe_list'] = $this->feContentModel->orderBy('id', 'DESC')->findAll();
|
|
|
|
return $this->loadLayout('frontend_content_list', ['data' => $data,'tab_name' => 'Front-End Content','page_name' => 'Front-End Content']);
|
|
|
|
} elseif ($method === 'delete') {
|
|
|
|
$id = $this->request->getGet('fe_id') ?? null;
|
|
|
|
if (empty($id) || !ctype_digit((string)$id) || (int)$id <= 0) {
|
|
return $this->respond([
|
|
'status' => false,
|
|
'code' => 400,
|
|
'message' => 'Valid numeric ID is required for deletion'
|
|
], 400);
|
|
}
|
|
$id = (int)$id;
|
|
|
|
$existing = $this->feContentModel->find($id);
|
|
if (!$existing) {
|
|
return $this->respond([
|
|
'status' => false,
|
|
'code' => 404,
|
|
'message' => 'Record not found'
|
|
], 404);
|
|
}
|
|
|
|
$update_status = $this->feContentModel->where('id', $id)->set(['is_active' => 0])->update();
|
|
|
|
if ($update_status) {
|
|
return $this->respond([
|
|
'status' => true,
|
|
'code' => 200,
|
|
'message' => 'Data removed successfully',
|
|
'fe_id' => $id
|
|
], 200);
|
|
} else {
|
|
return $this->respond([
|
|
'status' => false,
|
|
'code' => 400,
|
|
'message' => 'Failed to remove data',
|
|
'fe_id' => $id
|
|
], 200);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private const ALLOWED_RETURN_TYPES = ['api', 'web'];
|
|
|
|
public function FAQ()
|
|
{
|
|
$method = strtolower($this->request->getMethod());
|
|
$returnType = strtolower($this->request->getGet('return_type') ?? 'api');
|
|
if (!in_array($returnType, self::ALLOWED_RETURN_TYPES, true)) {
|
|
$returnType = 'api';
|
|
}
|
|
$ref = ['timestamp' => date('Y-m-d H:i:s')];
|
|
|
|
try {
|
|
// --- 1. POST: CREATE OR UPDATE ---
|
|
if ($method === 'post') {
|
|
$rules = [
|
|
'category' => [
|
|
// Allow letters, numbers, spaces and / - . , " '
|
|
'rules' => 'required|max_length[100]|regex_match[/^[a-zA-Z0-9 \\/\\-\\.\,\"\\\']+$/]',
|
|
'errors' => [
|
|
'required' => 'Category is required',
|
|
'max_length' => 'Category cannot exceed 100 characters',
|
|
'regex_match' => 'Category can contain only letters, numbers, spaces, and these characters: / - . , " \'',
|
|
|
|
]
|
|
],
|
|
'question' => [
|
|
// Allow only letters, numbers, spaces and basic punctuation . , ; : ! ? ( ) & / -
|
|
'rules' => 'required|max_length[1000]|regex_match[/^[a-zA-Z0-9 _\\-.,;:!?()&\\/]+$/]',
|
|
'errors' => [
|
|
'required' => 'Question is required',
|
|
'max_length' => 'Question cannot exceed 1000 characters',
|
|
'regex_match' => 'Question can contain only letters, numbers, spaces, and these characters: . , ; : ! ? ( ) & / -',
|
|
]
|
|
],
|
|
'answer' => [
|
|
'rules' => 'required|max_length[5000]',
|
|
'errors' => [
|
|
'required' => 'Answer is required',
|
|
'max_length' => 'Answer cannot exceed 5000 characters',
|
|
]
|
|
]
|
|
];
|
|
|
|
if (!$this->validate($rules)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => 'error',
|
|
'message' => 'Input validation failed',
|
|
'code' => 400,
|
|
'errors' => $this->validator->getErrors(),
|
|
'ref' => $ref
|
|
]);
|
|
}
|
|
|
|
/**************************************************************************
|
|
* XSS PROTECTION FOR 'question' and 'answer'
|
|
**************************************************************************
|
|
*
|
|
* Applying the same security model as `frontend_content`.
|
|
*
|
|
* 1. Validate raw `question` and `answer` for malicious tags using `hasXssTags()`.
|
|
* If found, reject the request immediately.
|
|
* 2. Sanitize all *other* fields using the generic `sanitizeInputArrayAdvanced`.
|
|
* 3. Sanitize the `question` and `answer` using the HTML-aware `sanitizeHtml()`
|
|
* function to allow safe tags before saving.
|
|
*
|
|
*************************************************************************/
|
|
|
|
// Step 1: Validate raw input for XSS threats.
|
|
$rawQuestion = $this->request->getPost('question');
|
|
$rawAnswer = $this->request->getPost('answer');
|
|
$xssErrors = [];
|
|
|
|
if ($this->hasXssTags($rawQuestion)) {
|
|
$xssErrors['question'] = 'Question contains restricted tags. Script, iframe and event handlers are not allowed';
|
|
}
|
|
if ($this->hasXssTags($rawAnswer)) {
|
|
$xssErrors['answer'] = 'Answer contains restricted tags. Script, iframe and event handlers are not allowed';
|
|
}
|
|
|
|
if (!empty($xssErrors)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => 'error',
|
|
'message' => 'Input validation failed',
|
|
'code' => 400,
|
|
'errors' => $xssErrors,
|
|
'ref' => $ref
|
|
]);
|
|
}
|
|
|
|
// Step 2 & 3: Sanitize and combine data.
|
|
$otherPostData = $this->request->getPost();
|
|
unset($otherPostData['question'], $otherPostData['answer']);
|
|
$data = sanitizeInputArrayAdvanced($otherPostData);
|
|
|
|
$data['question'] = $this->sanitizeHtml($rawQuestion);
|
|
$data['answer'] = $this->sanitizeHtml($rawAnswer);
|
|
|
|
$id = $data['faq_id'] ?? null;
|
|
|
|
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => 'error', 'message' => 'Invalid FAQ ID format', 'code' => 400, 'ref' => $ref
|
|
]);
|
|
}
|
|
$id = !empty($id) ? (int)$id : null;
|
|
unset($data['faq_id']);
|
|
|
|
if (empty($id)) {
|
|
$status = $this->faqModel->insert($data);
|
|
$msg = "Created";
|
|
} else {
|
|
$existing = $this->faqModel->find($id);
|
|
if (!$existing) {
|
|
return $this->response->setStatusCode(404)->setJSON([
|
|
'status' => 'error', 'message' => 'FAQ not found', 'code' => 404, 'ref' => $ref
|
|
]);
|
|
}
|
|
$status = $this->faqModel->update($id, $data);
|
|
$msg = "Updated";
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'status' => $status ? 'success' : 'error',
|
|
'message' => "FAQ $msg " . ($status ? 'successfully' : 'failed'),
|
|
'code' => $status ? 200 : 400,
|
|
'data' => $data,
|
|
'ref' => $ref
|
|
])->setStatusCode($status ? 200 : 400);
|
|
}
|
|
|
|
// --- 2. GET: FETCH LIST OR SINGLE ---
|
|
elseif ($method === 'get') {
|
|
|
|
$id = $this->request->getGet('faq_id');
|
|
|
|
if (!empty($id)) {
|
|
if (!ctype_digit((string)$id) || (int)$id <= 0) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => 'error', 'message' => 'Invalid FAQ ID format', 'code' => 400, 'ref' => $ref
|
|
]);
|
|
}
|
|
$id = (int)$id;
|
|
|
|
if($returnType === 'web'){
|
|
$row = $this->faqModel->find($id);
|
|
}else{
|
|
$row = $this->faqModel->where('is_active', 1)->find($id);
|
|
}
|
|
|
|
$data['faq_list'] = $row ? [$row] : [];
|
|
} else {
|
|
if($returnType === 'web'){
|
|
$data['faq_list'] = $this->faqModel->orderBy('id', 'desc')->findAll();
|
|
}else{
|
|
$data['faq_list'] = $this->faqModel->where('is_active', 1)->orderBy('id', 'desc')->findAll();
|
|
}
|
|
}
|
|
|
|
if ($returnType === 'web') {
|
|
$data['tab_name'] = "FAQ";
|
|
$data['page_name'] = "FAQ";
|
|
// print_r($data);die;
|
|
return $this->loadLayout('faq_list', $data);
|
|
}
|
|
|
|
// API Response Logic
|
|
if (empty($data['faq_list'])) {
|
|
return $this->response->setJSON([
|
|
'status' => $returnType === 'web' ? false : 'error',
|
|
'message' => 'No data found',
|
|
'code' => 404,
|
|
'data' => [],
|
|
'ref' => $ref
|
|
])->setStatusCode(200); // Using 200 with error status is common for mobile apps to prevent crashes
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'status' => $returnType === 'web' ? true : 'success',
|
|
'message' => 'Data retrieved',
|
|
'code' => 200,
|
|
'data' => $data,
|
|
'ref' => $ref
|
|
])->setStatusCode(200);
|
|
}
|
|
|
|
// --- 3. DELETE: SOFT DELETE ---
|
|
elseif ($method === 'delete') {
|
|
$id = $this->request->getGet('faq_id');
|
|
|
|
if (empty($id) || !ctype_digit((string)$id) || (int)$id <= 0) {
|
|
return $this->response->setJSON([
|
|
'status' => 'error', 'message' => 'Valid numeric FAQ ID is required', 'code' => 400, 'ref' => $ref
|
|
])->setStatusCode(400);
|
|
}
|
|
$id = (int)$id;
|
|
|
|
$status = $this->faqModel->find($id) ? $this->faqModel->update($id, ['is_active' => 0]) : false;
|
|
|
|
return $this->response->setJSON([
|
|
'status' => $status ? ($returnType === 'web' ? true : 'success') : ($returnType === 'web' ? false : 'error'),
|
|
'message' => $status ? 'Data removed successfully' : 'Failed to remove or ID missing',
|
|
'code' => $status ? 200 : 400,
|
|
'data' => [],
|
|
'ref' => $ref
|
|
])->setStatusCode( 200 );
|
|
}
|
|
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'FAQ error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
|
return $this->response->setJSON([
|
|
'status' => $returnType === 'web' ? false : 'error',
|
|
'code' => 500,
|
|
'message' => 'An internal error occurred. Please try again later.',
|
|
'ref' => $ref
|
|
])->setStatusCode(500);
|
|
}
|
|
}
|
|
|
|
// public function frontend_content_files()
|
|
// {
|
|
|
|
// // 1. Define your physical system path
|
|
// define('UPLOAD_PATH', ROOTPATH . 'public/uploads/add_image_upload/');
|
|
|
|
// // 2. Define the public URL so the editor can display the image later
|
|
// // This is what the browser uses to see the image (e.g., https://site.com/uploads/...)
|
|
// // $publicUrl = "https://yourdomain.com/public/uploads/add_image_upload/";
|
|
// $publicUrl = ROOTPATH . 'public/uploads/add_image_upload/';
|
|
// // if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
|
|
|
|
// header('Content-Type: application/json');
|
|
|
|
// if ($_FILES['files']) {
|
|
// // Ensure directory exists
|
|
// if (!is_dir(UPLOAD_PATH)) {
|
|
// mkdir(UPLOAD_PATH, 0775, true);
|
|
// }
|
|
|
|
// $fileName = time() . '_' . basename($_FILES['files']['name'][0]);
|
|
// $fullPath = UPLOAD_PATH . $fileName;
|
|
|
|
// if (move_uploaded_file($_FILES['files']['tmp_name'][0], $fullPath)) {
|
|
// echo json_encode([
|
|
// "success" => true,
|
|
// "data" => [
|
|
// "baseurl" => $publicUrl,
|
|
// "files" => [$fileName]
|
|
// ]
|
|
// ]);
|
|
// } else {
|
|
// echo json_encode(["success" => false, "error" => "Failed to move file to " . UPLOAD_PATH]);
|
|
// }
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
// Add these two private methods inside AppContentManagementController
|
|
|
|
|
|
/**
|
|
* =================================================================================
|
|
* HTML SANITIZATION & VALIDATION HELPER METHODS
|
|
* =================================================================================
|
|
* The following two methods are the core of the XSS protection logic.
|
|
*/
|
|
|
|
|
|
/**
|
|
* sanitizeHtml()
|
|
*
|
|
* This function cleans a string of HTML, ensuring it is safe to display in a browser.
|
|
* It allows a specific set of safe HTML tags and removes any dangerous attributes
|
|
* from those tags.
|
|
*
|
|
* @param string $input The raw HTML string from user input.
|
|
* @return string The cleaned, safe HTML string.
|
|
*/
|
|
private function sanitizeHtml(string $input): string
|
|
{
|
|
/**
|
|
* Define a whitelist of allowed HTML tags. Any tag not in this list will be
|
|
* completely removed. We are allowing basic formatting, lists, tables, etc.
|
|
*/
|
|
// ✅ Added <s>, <u>, <h1>-<h6>, <blockquote>, <pre>, <code>, <hr> for Jodit support
|
|
$allowed_tags = '<p><b><i><s><u><strong><em><ul><ol><li><br><a><img><table><thead><tbody><tr><th><td><span><div><h1><h2><h3><h4><h5><h6><blockquote><pre><code><hr><sub><sup>';
|
|
|
|
// Use strip_tags() to remove all tags that are not in our whitelist.
|
|
$clean = strip_tags($input, $allowed_tags);
|
|
|
|
/**
|
|
* Define a blacklist of dangerous attributes. These are often used for XSS
|
|
* attacks (e.g., `onclick`, `onmouseover`). We search for and remove these
|
|
* attributes from any remaining tags.
|
|
*/
|
|
$dangerous_attrs = [
|
|
'/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', // e.g., onclick="..."
|
|
'/\s*on\w+\s*=\s*[^\s>]*/i', // e.g., onclick=...
|
|
'/\s*javascript\s*:[^"\'"]*/i', // e.g., href="javascript:..."
|
|
'/\s*vbscript\s*:[^"\'"]*/i', // e.g., href="vbscript:..."
|
|
];
|
|
|
|
// Use preg_replace to find and remove the dangerous attributes.
|
|
foreach ($dangerous_attrs as $pattern) {
|
|
$clean = preg_replace($pattern, '', $clean);
|
|
}
|
|
|
|
return $clean;
|
|
}
|
|
|
|
|
|
/**
|
|
* hasXssTags()
|
|
*
|
|
* This function scans a string for common XSS-related tags, protocols, and event
|
|
* handlers. It is used as a primary check to quickly reject any input that is
|
|
* clearly malicious.
|
|
*
|
|
* @param string $str The raw string from user input.
|
|
* @return bool Returns `true` if a dangerous pattern is found, `false` otherwise.
|
|
*/
|
|
private function hasXssTags(string $str): bool
|
|
{
|
|
// Decode the string to handle entities (e.g., `%3Cscript%3E`) and prevent evasion.
|
|
$decoded = html_entity_decode($str, ENT_QUOTES, 'UTF-8');
|
|
$decoded = urldecode($decoded);
|
|
$decoded = str_replace(["\0", "\x00"], '', $decoded); // Remove null bytes
|
|
|
|
/**
|
|
* Define a blacklist of dangerous patterns. This includes tags like `<script>`
|
|
* and `<iframe>`, as well as patterns like `javascript:` and `onclick=`.
|
|
* The `/i` flag makes the search case-insensitive.
|
|
*/
|
|
$dangerous_patterns = [
|
|
'/<\s*script/i', // <script
|
|
'/<\s*\/\s*script/i', // </script
|
|
'/javascript\s*:/i', // javascript:
|
|
'/vbscript\s*:/i', // vbscript:
|
|
'/<\s*iframe/i', // <iframe>
|
|
'/<\s*object/i', // <object>
|
|
'/<\s*embed/i', // <embed>
|
|
'/<\s*applet/i', // <applet>
|
|
'/on\w+\s*=/i', // on...= (e.g., onclick=, onmouseover=, onerror=)
|
|
'/data\s*:\s*text\/html/i', // data:text/html
|
|
'/expression\s*\(/i', // CSS expression()
|
|
'/\balert\s*\(/i', // alert(...)
|
|
];
|
|
|
|
// Loop through the patterns and check if any of them exist in the decoded string.
|
|
foreach ($dangerous_patterns as $pattern) {
|
|
if (preg_match($pattern, $decoded)) return true; // Found a threat
|
|
}
|
|
|
|
// If we get here, no threats were found.
|
|
return false;
|
|
}
|
|
|
|
} |