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 , ,

-

,
,
, , 
for Jodit support $allowed_tags = '