verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } // Get the raw invoice data from the form $raw_invoice_data = isset($_POST['invoice_data']) ? $_POST['invoice_data'] : $_POST; // Remove non-invoice fields unset($raw_invoice_data['action']); unset($raw_invoice_data['nonce']); // Process the invoice data $invoice_form_manager = new \EasyInvoice\Forms\Invoice\InvoiceFormManager(); $invoice_data = $invoice_form_manager->processFormData($raw_invoice_data); if (!empty($invoice_data['errors'])) { wp_send_json_error([ 'message' => 'Validation failed', 'errors' => $invoice_data['errors'] ]); } // Handle items separately - process the natural form submission format if (isset($raw_invoice_data['items']) && is_array($raw_invoice_data['items'])) { // Form submits items as items[0][title], items[0][description], etc. // Convert to array of item objects for processing $items_array = []; foreach ($raw_invoice_data['items'] as $index => $item_data) { if (is_array($item_data)) { $items_array[] = $item_data; } } // Process items using the dynamic field system $invoice_data['data']['items'] = $invoice_form_manager->processItemsData($items_array); } // Handle special fields that might not be in the form definition if (isset($raw_invoice_data['invoice_id'])) { $invoice_data['data']['invoice_id'] = intval($raw_invoice_data['invoice_id']); } if (isset($raw_invoice_data['client_id'])) { $invoice_data['data']['client_id'] = intval($raw_invoice_data['client_id']); } $invoice_id = isset($invoice_data['data']['invoice_id']) ? intval($invoice_data['data']['invoice_id']) : 0; $repository = InvoiceServiceProvider::getInvoiceRepository(); if ($invoice_id > 0) { // Update existing invoice - preserve existing invoice number unset($invoice_data['data']['invoice_number']); unset($invoice_data['data']['number']); $invoice = $repository->update($invoice_id, $invoice_data['data']); if (!$invoice) { $this->sendError(__('Failed to update invoice', 'easy-invoice')); } // Use FormProcessor to save form data to database $form_processor = new \EasyInvoice\Forms\FormProcessor(); $all_fields = $invoice_form_manager->getAllFields(); $form_processor->saveFormDataToDatabase($invoice_data['data'], $all_fields, $invoice); $message = __('Invoice updated successfully', 'easy-invoice'); } else { // Create new invoice - allow auto-generated invoice number to be saved // The invoice number will be auto-generated by the form and included in the data $invoice = $repository->create($invoice_data['data']); if (!$invoice) { $this->sendError(__('Failed to create invoice', 'easy-invoice')); } // Use FormProcessor to save form data to database $form_processor = new \EasyInvoice\Forms\FormProcessor(); $all_fields = $invoice_form_manager->getAllFields(); $form_processor->saveFormDataToDatabase($invoice_data['data'], $all_fields, $invoice); $invoice_id = $invoice->getId(); $message = __('Invoice created successfully', 'easy-invoice'); } // Handle items if (isset($invoice_data['data']['items']) && is_array($invoice_data['data']['items'])) { $invoice->setItems($invoice_data['data']['items']); } $invoice_template = get_post_meta($invoice_id, '_easy_invoice_invoice_template', true); $invoice_template = $invoice_template=='' ? 'standard': $invoice_template; update_option('easy_invoice_last_invoice_template',$invoice_template ); // Prepare response data $response_data = array( 'invoice_id' => $invoice_id, 'invoice' => $invoice->toArray(), 'toast' => array( 'type' => 'success', 'message' => $message, 'options' => array('duration' => 4000) ) ); // Include client data if invoice has a client if ($invoice->getClientId()) { $client_repository = ClientServiceProvider::getClientRepository(); $client = $client_repository->find($invoice->getClientId()); if ($client) { $response_data['client'] = array( 'id' => $client->getId(), 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()), 'email' => $client->getEmail() ?: '', 'phone' => $client->getExtraInfo() ?: '', 'company' => $client->getBusinessClientName() ?: '', 'address' => $client->getAddress() ?: '', 'website' => $client->getWebsite() ?: '', ); } } wp_send_json_success($response_data); } /** * Save and send invoice */ public function saveAndSendInvoice() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } // First save the invoice $this->saveInvoice(); // If we get here, the invoice was saved successfully // Now send the invoice via email $invoice_id = isset($_POST['invoice_data']['invoice_id']) ? intval($_POST['invoice_data']['invoice_id']) : 0; if ($invoice_id > 0) { // Send the invoice via email $result = $this->sendInvoiceEmail($invoice_id); if ($result['success']) { $this->sendSuccess(array( 'message' => __('Invoice saved and sent successfully', 'easy-invoice'), 'invoice_id' => $invoice_id )); } else { $this->sendError($result['message']); } } else { $this->sendError(__('Invalid invoice ID for sending', 'easy-invoice')); } } /** * Delete invoice */ public function deleteInvoice() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; if ($invoice_id <= 0) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } $repository = InvoiceServiceProvider::getInvoiceRepository(); $result = $repository->delete($invoice_id); if (!$result) { $this->sendError(__('Failed to delete invoice', 'easy-invoice')); } $this->sendSuccess(array( 'message' => __('Invoice deleted successfully', 'easy-invoice'), 'invoice_id' => $invoice_id, )); } /** * Get invoice */ public function getInvoice() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0; if ($invoice_id <= 0) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } $repository = InvoiceServiceProvider::getInvoiceRepository(); $invoice = $repository->find($invoice_id); if (!$invoice) { $this->sendError(__('Invoice not found', 'easy-invoice')); } $this->sendSuccess(array( 'invoice' => $invoice->toArray(), )); } /** * Save client */ public function saveClient() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0; $client_data = isset($_POST['client_data']) ? $this->sanitizeData($_POST['client_data']) : array(); if (empty($client_data)) { $this->sendError(__('Invalid client data', 'easy-invoice')); } $repository = ClientServiceProvider::getClientRepository(); if ($client_id > 0) { // Update existing client $client = $repository->update($client_id, $client_data); if (!$client) { $this->sendError(__('Failed to update client', 'easy-invoice')); } $message = __('Client updated successfully', 'easy-invoice'); } else { // Create new client $client = $repository->create($client_data); if (!$client) { $this->sendError(__('Failed to create client', 'easy-invoice')); } $client_id = $client->getId(); $message = __('Client created successfully', 'easy-invoice'); } $this->sendSuccess(array( 'message' => $message, 'client_id' => $client_id, 'client' => $client->toArray(), )); } /** * Delete client */ public function deleteClient() { try { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0; $delete_associated_documents = isset($_POST['delete_associated_documents']) ? (bool)$_POST['delete_associated_documents'] : false; if ($client_id <= 0) { $this->sendError(__('Invalid client ID', 'easy-invoice')); } // Check if the user exists and is not an administrator $user = get_user_by('ID', $client_id); if (!$user) { $this->sendError(__('User not found', 'easy-invoice')); } if (in_array('administrator', $user->roles)) { $this->sendError(__('Cannot delete administrator accounts', 'easy-invoice')); } global $wpdb; // Get counts of associated documents $invoice_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_client_id' AND meta_value = %d", $client_id )); $quote_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_quote_client_id' AND meta_value = %d", $client_id )); $payment_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_easy_payment_client_id' AND meta_value = %d", $client_id )); $total_documents = $invoice_count + $quote_count + $payment_count; if ($delete_associated_documents) { // Delete all associated documents $this->log(sprintf('Deleting client %d with all associated documents (%d invoices, %d quotes, %d payments)', $client_id, $invoice_count, $quote_count, $payment_count)); // Delete invoices if ($invoice_count > 0) { $invoices = $wpdb->get_col($wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_client_id' AND meta_value = %d", $client_id )); foreach ($invoices as $invoice_id) { wp_delete_post($invoice_id, true); } } // Delete quotes if ($quote_count > 0) { $quotes = $wpdb->get_col($wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_quote_client_id' AND meta_value = %d", $client_id )); foreach ($quotes as $quote_id) { wp_delete_post($quote_id, true); } } // Delete payments if ($payment_count > 0) { $payments = $wpdb->get_col($wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_payment_client_id' AND meta_value = %d", $client_id )); foreach ($payments as $payment_id) { wp_delete_post($payment_id, true); } } $message = sprintf(__('Client and all associated documents (%d total) deleted successfully', 'easy-invoice'), $total_documents); } else { // Only remove client associations, preserve documents $this->log(sprintf('Removing client associations for client %d (%d invoices, %d quotes, %d payments)', $client_id, $invoice_count, $quote_count, $payment_count)); // Remove client associations from invoices if ($invoice_count > 0) { $wpdb->delete( $wpdb->postmeta, ['meta_key' => '_easy_invoice_client_id', 'meta_value' => $client_id] ); } // Remove client associations from quotes if ($quote_count > 0) { $wpdb->delete( $wpdb->postmeta, ['meta_key' => '_easy_invoice_quote_client_id', 'meta_value' => $client_id] ); } // Remove client associations from payments if ($payment_count > 0) { $wpdb->delete( $wpdb->postmeta, ['meta_key' => '_easy_payment_client_id', 'meta_value' => $client_id] ); } $message = sprintf(__('Client deleted successfully. %d documents preserved but client associations removed.', 'easy-invoice'), $total_documents); } // Delete the WordPress user require_once(ABSPATH . 'wp-admin/includes/user.php'); $result = wp_delete_user($client_id); if (!$result) { $this->sendError(__('Failed to delete client', 'easy-invoice')); } $this->sendSuccess(array( 'message' => $message, 'client_id' => $client_id, 'documents_deleted' => $delete_associated_documents, 'total_documents' => $total_documents )); } catch (\Exception $e) { $this->sendError($e->getMessage()); } } /** * Get client */ public function getClient() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $client_id = isset($_REQUEST['client_id']) ? intval($_REQUEST['client_id']) : 0; if ($client_id <= 0) { $this->sendError(__('Invalid client ID', 'easy-invoice')); } $repository = ClientServiceProvider::getClientRepository(); $client = $repository->find($client_id); if (!$client) { $this->sendError(__('Client not found', 'easy-invoice')); } $client_data = $client->toArray(); // Return comprehensive client data in a unified format that works for both form population and display $this->sendSuccess(array( // Form population fields (for invoice-builder.js and invoice-form.js) 'name' => $client_data['company_name'] ?? $client_data['contact_name'] ?? '', 'email' => $client_data['email'] ?? '', 'phone' => $client_data['phone'] ?? '', 'company' => $client_data['company_name'] ?? '', 'address' => $client_data['billing_address'] ?? '', 'website' => $client_data['website'] ?? '', // Display fields (for client-manager.js) 'business_client_name' => $client->getBusinessClientName(), 'username' => $client->getUsername(), 'extra_info' => $client->getExtraInfo(), 'first_name' => $client->getFirstName(), 'last_name' => $client->getLastName(), // Raw data for backward compatibility 'client' => array( 'name' => $client_data['company_name'] ?? $client_data['contact_name'] ?? '', 'email' => $client_data['email'] ?? '', 'phone' => $client_data['phone'] ?? '', 'company' => $client_data['company_name'] ?? '', 'address' => $client_data['billing_address'] ?? '', 'website' => $client_data['website'] ?? '', ) )); } /** * Verify nonce * * @param string $action The nonce action */ private function verifyNonce($action) { // Check for _nonce (standard format) first if (isset($_REQUEST['_nonce']) && wp_verify_nonce($_REQUEST['_nonce'], $action)) { return; } // Also check for 'nonce' (client form format) if (isset($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], $action)) { return; } // If we get here, neither nonce format was valid $this->sendError(__('Security check failed', 'easy-invoice')); } /** * Sanitize data * * @param array $data The data to sanitize * @return array The sanitized data */ private function sanitizeData($data) { if (!is_array($data)) { return array(); } $sanitized = array(); // Define fields that should allow HTML (like textarea content) $html_fields = [ 'invoice_description', 'description', 'notes', 'terms', 'internal_notes', 'customer_address' ]; // Define numeric fields $numeric_fields = [ 'invoice_id', 'client_id', 'discount_value', 'tax_rate' ]; foreach ($data as $key => $value) { if (is_array($value)) { $sanitized[$key] = $this->sanitizeData($value); } else if (in_array($key, $html_fields)) { // For HTML fields, use wp_kses to allow certain tags but prevent XSS $sanitized[$key] = wp_kses_post($value); } else if (in_array($key, $numeric_fields)) { // For numeric fields, ensure they're valid numbers $sanitized[$key] = is_numeric($value) ? $value : 0; } else { $sanitized[$key] = sanitize_text_field($value); } } return $sanitized; } /** * Send success response */ private function sendSuccess($data = array()) { // Check if we should suppress global toast $suppress_toast = isset($_POST['suppress_global_toast']) && $_POST['suppress_global_toast'] === 'true'; // Add toast notification if not already present and not suppressed if (!isset($data['toast']) && !$suppress_toast) { $message = isset($data['message']) ? $data['message'] : __('Operation completed successfully', 'easy-invoice'); $data['toast'] = array( 'type' => 'success', 'message' => $message, 'options' => array('duration' => 4000) ); } // Remove toast data if suppressed if ($suppress_toast && isset($data['toast'])) { unset($data['toast']); } wp_send_json_success($data); } /** * Send error response */ private function sendError($message, $data = array()) { // Add toast notification $data['toast'] = array( 'type' => 'error', 'message' => $message, 'options' => array('duration' => 6000) ); wp_send_json_error($data); } /** * Download invoice as PDF */ public function downloadPdf() { // Verify nonce $this->verifyNonce('easy_invoice_nonce'); // Check if user has required capability if (!current_user_can('edit_posts')) { $this->sendError(__('You do not have permission to download invoices', 'easy-invoice')); } // Get invoice ID $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0; if (!$invoice_id) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } // Get invoice from repository $repository = InvoiceServiceProvider::getInvoiceRepository(); $invoice = $repository->find($invoice_id); if (!$invoice) { $this->sendError(__('Invoice not found', 'easy-invoice')); } // Get invoice data for PDF generation $invoice_data = \EasyInvoice\Includes\Helpers\PdfHelper::getInvoiceDataForPdf($invoice); // Return success response with invoice data $this->sendSuccess(array( 'message' => __('Invoice data retrieved successfully', 'easy-invoice'), 'invoice_data' => $invoice_data )); } /** * Send invoice via email */ public function sendInvoiceEmail() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } // Get invoice ID from POST data $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; if (!$invoice_id) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } $repository = InvoiceServiceProvider::getInvoiceRepository(); $invoice = $repository->find($invoice_id); if (!$invoice) { $this->sendError(__('Invoice not found', 'easy-invoice')); } // Use EmailManager to send the email $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); $result = $email_manager->sendInvoiceEmail($invoice, 'new'); if ($result['success']) { $this->sendSuccess(array( 'message' => $result['message'] )); } else { $this->sendError($result['message']); } } /** * Download quote as PDF */ public function downloadQuotePdf() { // Verify nonce $this->verifyNonce('easy_invoice_nonce'); // Check if user has required capability if (!current_user_can('edit_posts')) { $this->sendError(__('You do not have permission to download quotes', 'easy-invoice')); } // Get quote ID $quote_id = isset($_POST['quote_id']) ? intval($_POST['quote_id']) : 0; if (!$quote_id) { $this->sendError(__('Invalid quote ID', 'easy-invoice')); } // Get quote from repository $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository(); $quote = $repository->find($quote_id); if (!$quote) { $this->sendError(__('Quote not found', 'easy-invoice')); } // For now, return success response with quote data // PDF generation can be implemented later with actual PDF creation $this->sendSuccess(array( 'message' => __('Quote data retrieved successfully', 'easy-invoice'), 'quote_data' => $quote->toArray(), 'download_url' => add_query_arg(array( 'action' => 'easy_invoice_generate_quote_pdf', 'quote_id' => $quote_id, 'nonce' => wp_create_nonce('generate_quote_pdf') ), admin_url('admin-ajax.php')) )); } /** * Save quote */ public function saveQuote() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } // Get the raw quote data from the form $raw_quote_data = isset($_POST['quote_data']) ? $_POST['quote_data'] : $_POST; // Remove non-quote fields unset($raw_quote_data['action']); unset($raw_quote_data['nonce']); // Process the quote data $quote_form_manager = new \EasyInvoice\Forms\Quote\QuoteFormManager(); $quote_data = $quote_form_manager->processFormData($raw_quote_data); if (!empty($quote_data['errors'])) { wp_send_json_error([ 'message' => 'Validation failed', 'errors' => $quote_data['errors'] ]); } // Handle items separately - process the natural form submission format if (isset($raw_quote_data['items']) && is_array($raw_quote_data['items'])) { // Form submits items as items[0][title], items[0][description], etc. // Convert to array of item objects for processing $items_array = []; foreach ($raw_quote_data['items'] as $index => $item_data) { if (is_array($item_data)) { $items_array[] = $item_data; } } // Process items using the dynamic field system $quote_data['data']['items'] = $quote_form_manager->processItemsData($items_array); } // Handle special fields that might not be in the form definition if (isset($raw_quote_data['quote_id'])) { $quote_data['data']['quote_id'] = intval($raw_quote_data['quote_id']); } if (isset($raw_quote_data['client_id'])) { $quote_data['data']['client_id'] = intval($raw_quote_data['client_id']); } $quote_id = isset($quote_data['data']['quote_id']) ? intval($quote_data['data']['quote_id']) : 0; $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository(); if ($quote_id > 0) { // Update existing quote - preserve existing quote number unset($quote_data['data']['quote_number']); unset($quote_data['data']['number']); // Get the existing quote first $quote = $repository->find($quote_id); if (!$quote) { $this->sendError(__('Failed to find quote for update', 'easy-invoice')); } // Use FormProcessor to save form data to database BEFORE repository update $form_processor = new \EasyInvoice\Forms\FormProcessor(); $all_fields = $quote_form_manager->getAllFields(); $form_processor->saveFormDataToDatabase($quote_data['data'], $all_fields, $quote); // Now update the quote with the processed data, passing the existing quote object $quote = $repository->update($quote_id, $quote_data['data'], $quote); if (!$quote) { $this->sendError(__('Failed to update quote', 'easy-invoice')); } $message = __('Quote updated successfully', 'easy-invoice'); } else { // Create new quote - allow auto-generated quote number to be saved // The quote number will be auto-generated by the form and included in the data $quote = $repository->create($quote_data['data']); if (!$quote) { $this->sendError(__('Failed to create quote', 'easy-invoice')); } // Use FormProcessor to save form data to database $form_processor = new \EasyInvoice\Forms\FormProcessor(); $all_fields = $quote_form_manager->getAllFields(); $form_processor->saveFormDataToDatabase($quote_data['data'], $all_fields, $quote); $quote_id = $quote->getId(); $message = __('Quote created successfully', 'easy-invoice'); } // Handle items if (isset($quote_data['data']['items']) && is_array($quote_data['data']['items'])) { $quote->setItems($quote_data['data']['items']); // Save the quote to persist the items to database $quote->save(); } $quote_template = get_post_meta($quote_id, '_easy_invoice_quote_quote_template', true); $quote_template = $quote_template=='' ? 'standard': $quote_template; update_option('easy_invoice_last_quote_template',$quote_template ); // Prepare response data $response_data = array( 'quote_id' => $quote_id, 'quote' => $quote->toArray(), 'toast' => array( 'type' => 'success', 'message' => $message, 'options' => array('duration' => 4000) ) ); // Include client data if quote has a client if ($quote->getClientId()) { $client_repository = ClientServiceProvider::getClientRepository(); $client = $client_repository->find($quote->getClientId()); if ($client) { $response_data['client'] = array( 'id' => $client->getId(), 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()), 'email' => $client->getEmail() ?: '', 'phone' => $client->getExtraInfo() ?: '', 'company' => $client->getBusinessClientName() ?: '', 'address' => $client->getAddress() ?: '', 'website' => $client->getWebsite() ?: '', ); } } $this->sendSuccess($response_data); } /** * Toggle a template as favorite */ /** * Check if an email already exists for any client */ public function checkEmailExists() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : ''; if (empty($email)) { $this->sendSuccess(array('exists' => false)); } $repository = ClientServiceProvider::getClientRepository(); $existing_clients = $repository->findByEmail($email); $this->sendSuccess(array( 'exists' => !empty($existing_clients), 'count' => count($existing_clients) )); } /** * Generate a secure password. */ public function generatePassword() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $password = wp_generate_password(16, true, true); // Check if we should suppress global toast $suppress_toast = isset($_POST['suppress_global_toast']) && $_POST['suppress_global_toast'] === 'true'; $this->sendSuccess(array( 'password' => $password, 'suppress_toast' => $suppress_toast )); } /** * Sanitize invoice items * * @param array $items Raw items data * @return array Sanitized items data */ private function sanitizeItems(array $items): array { $sanitized_items = []; // Get field configuration for dynamic processing $form_manager = new \EasyInvoice\Forms\Invoice\InvoiceFormManager(); $field_config = $form_manager->getItemFields(); foreach ($items as $item) { if (!is_array($item)) { continue; } $sanitized_item = []; // Process each field dynamically based on configuration foreach ($field_config as $field) { $field_name = $field['name'] ?? ''; $field_type = $field['type'] ?? 'text'; $raw_value = $item[$field_name] ?? ''; // Apply field-specific sanitization switch ($field_type) { case 'text': $sanitized_item[$field_name] = sanitize_text_field($raw_value); break; case 'textarea': $sanitized_item[$field_name] = wp_kses_post($raw_value); break; case 'number': $sanitized_item[$field_name] = is_numeric($raw_value) ? floatval($raw_value) : 0; break; case 'checkbox': $sanitized_item[$field_name] = !empty($raw_value) ? true : false; break; default: $sanitized_item[$field_name] = sanitize_text_field($raw_value); break; } } // Handle legacy field names for backward compatibility if (isset($item['name']) && !isset($sanitized_item['title'])) { $sanitized_item['title'] = sanitize_text_field($item['name']); } if (isset($item['title']) && !isset($sanitized_item['title'])) { $sanitized_item['title'] = sanitize_text_field($item['title']); } // Only add items that have at least a title/name if (!empty($sanitized_item['title'])) { $sanitized_items[] = $sanitized_item; } } return $sanitized_items; } /** * Add a new client (specifically for the client form in templates/clients-page.php) */ public function addClient() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } // Check if required fields are present $required_fields = ['business_client_name', 'email', 'username']; foreach ($required_fields as $field) { if (!isset($_POST[$field]) || empty($_POST[$field])) { $this->sendError(__('Missing required field: ' . $field, 'easy-invoice')); } } // Prepare client data $client_data = [ ClientFields::BUSINESS_CLIENT_NAME => sanitize_text_field($_POST['business_client_name']), ClientFields::EMAIL => sanitize_email($_POST['email']), ClientFields::USERNAME => sanitize_user($_POST['username']), ClientFields::PASSWORD => $_POST['password'], ClientFields::ADDRESS => sanitize_textarea_field($_POST['address']), ClientFields::EXTRA_INFO => sanitize_textarea_field($_POST['extra_info']), ClientFields::FIRST_NAME => sanitize_text_field($_POST['first_name']), ClientFields::LAST_NAME => sanitize_text_field($_POST['last_name']), ClientFields::WEBSITE => esc_url_raw($_POST['website']), ClientFields::PHONE => isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '', ]; // Basic validation if (empty($client_data[ClientFields::BUSINESS_CLIENT_NAME]) && (empty($client_data[ClientFields::FIRST_NAME]) || empty($client_data[ClientFields::LAST_NAME]))) { $this->sendError(__('Please provide a client name or first/last name.', 'easy-invoice')); } if (empty($client_data[ClientFields::EMAIL])) { $this->sendError(__('Email address is required', 'easy-invoice')); } $repository = ClientServiceProvider::getClientRepository(); // Create new client $client = $repository->create($client_data); if (!$client) { $this->sendError(__('Failed to create client', 'easy-invoice')); } $client_id = $client->getId(); $response_data = array( 'message' => __('Client added successfully', 'easy-invoice'), 'client_id' => $client_id, 'client' => $client->toArray(), ); $this->sendSuccess($response_data); } /** * Update client from the client edit form */ public function updateClient() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0; if ($client_id <= 0) { $this->sendError(__('Invalid client ID', 'easy-invoice')); } // Prepare client data $client_data = [ ClientFields::BUSINESS_CLIENT_NAME => sanitize_text_field($_POST['business_client_name']), ClientFields::EMAIL => sanitize_email($_POST['email']), ClientFields::USERNAME => sanitize_user($_POST['username']), ClientFields::PASSWORD => $_POST['password'], // Keep password as is, don't sanitize ClientFields::ADDRESS => sanitize_textarea_field($_POST['address']), ClientFields::PHONE => isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '', ClientFields::EXTRA_INFO => sanitize_textarea_field($_POST['extra_info']), ClientFields::FIRST_NAME => sanitize_text_field($_POST['first_name']), ClientFields::LAST_NAME => sanitize_text_field($_POST['last_name']), ClientFields::WEBSITE => esc_url_raw($_POST['website']) ]; // Remove empty values except password (password can be empty for updates) $client_data = array_filter($client_data, function($value, $key) { if ($key === ClientFields::PASSWORD) { return true; // Always include password field } return $value !== ''; }, ARRAY_FILTER_USE_BOTH); if (empty($client_data)) { $this->sendError(__('No data provided to update.', 'easy-invoice')); } $repository = ClientServiceProvider::getClientRepository(); // Update existing client $client = $repository->update($client_id, $client_data); if (!$client) { $this->sendError(__('Failed to update client', 'easy-invoice')); } $this->sendSuccess(array( 'message' => __('Client updated successfully', 'easy-invoice'), 'client_id' => $client_id, 'client' => $client->toArray(), )); } /** * Update invoices data with missing client information and totals */ public function updateInvoicesData() { $this->verifyNonce('easy_invoice_admin_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $repository = InvoiceServiceProvider::getInvoiceRepository(); $client_repository = ClientServiceProvider::getClientRepository(); // Get all invoices $invoices = $repository->all(); $updated_count = 0; foreach ($invoices as $invoice) { $updated = false; // Check if client data is missing $client_id = $invoice->getClientId(); if ($client_id > 0) { $client = $client_repository->find($client_id); if ($client) { // Update customer name if missing $customer_name = $invoice->getCustomerName(); if (empty($customer_name)) { $customer_name = $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()); $invoice->setCustomerName($customer_name); $updated = true; } // Update customer email if missing $customer_email = $invoice->getCustomerEmail(); if (empty($customer_email)) { $customer_email = $client->getEmail(); $invoice->setCustomerEmail($customer_email); $updated = true; } // Update customer address if missing $customer_address = $invoice->getCustomerAddress(); if (empty($customer_address)) { $customer_address = $client->getAddress(); $invoice->setCustomerAddress($customer_address); $updated = true; } } } // Check if total is missing or zero $total = $invoice->getTotal(); if (empty($total) || $total == 0) { // Recalculate total from items $items = $invoice->getItems(); if (!empty($items)) { $subtotal = 0; foreach ($items as $item) { if (method_exists($item, 'getAmount')) { $subtotal += $item->getAmount(); } elseif (isset($item['amount'])) { $subtotal += $item['amount']; } } // Calculate discount and tax $discount = $invoice->getDiscountAmount(); $tax = $invoice->getTaxAmount(); $total = $subtotal - $discount + $tax; // Save the calculated total $invoice->setMeta('_easy_invoice_total', $total); $updated = true; } } if ($updated) { $updated_count++; } } $this->sendSuccess(array( 'message' => sprintf(__('Updated %d invoices with missing data', 'easy-invoice'), $updated_count), 'updated_count' => $updated_count )); } /** * Download invoice as PDF (public access) */ public function downloadInvoicePdf() { // Verify nonce $this->verifyNonce('easy_invoice_nonce'); // Get invoice ID $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; if (!$invoice_id) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } // Get invoice from repository (only published invoices for public access) $repository = InvoiceServiceProvider::getInvoiceRepository(); // For admins, allow access to any invoice status if (current_user_can('manage_options')) { $invoice = $repository->find($invoice_id); } else { // For non-admins, only allow access to published invoices $invoice = $repository->findPublished($invoice_id); } if (!$invoice) { $this->sendError(__('Invoice not found', 'easy-invoice')); } // Get invoice data for PDF generation $invoice_data = \EasyInvoice\Includes\Helpers\PdfHelper::getInvoiceDataForPdf($invoice); // For now, return success response with invoice data // PDF generation can be implemented later with actual PDF creation $this->sendSuccess(array( 'message' => __('Invoice data retrieved successfully', 'easy-invoice'), 'invoice_data' => $invoice_data, 'download_url' => add_query_arg(array( 'action' => 'easy_invoice_generate_pdf', 'invoice_id' => $invoice_id, 'nonce' => wp_create_nonce('generate_pdf') ), admin_url('admin-ajax.php')) )); } /** * Send invoice via email (public access) */ public function sendInvoiceEmailPublic() { $this->verifyNonce('easy_invoice_send_invoice_email'); // Get invoice ID $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0; if (!$invoice_id) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } // Get invoice from repository (only published invoices for public access) $repository = InvoiceServiceProvider::getInvoiceRepository(); // For admins, allow access to any invoice status if (current_user_can('manage_options')) { $invoice = $repository->find($invoice_id); } else { // For non-admins, only allow access to published invoices $invoice = $repository->findPublished($invoice_id); } if (!$invoice) { $this->sendError(__('Invoice not found', 'easy-invoice')); } // Use EmailManager to send the email $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); $result = $email_manager->sendInvoiceEmail($invoice, 'new'); if ($result['success']) { $this->sendSuccess(array( 'message' => $result['message'] )); } else { $this->sendError($result['message']); } } /** * Send quote via email (admin + public; guests only for published quotes). */ public function sendQuoteEmailPublic() { $this->verifyNonce('easy_invoice_send_quote_email'); $quote_id = isset($_POST['quote_id']) ? intval($_POST['quote_id']) : 0; if (!$quote_id) { $this->sendError(__('Invalid quote ID', 'easy-invoice')); } $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository(); if (current_user_can('manage_options')) { $quote = $repository->find($quote_id); } else { $quote = $repository->findPublished($quote_id); } if (!$quote) { $this->sendError(__('Quote not found', 'easy-invoice')); } $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); $result = $email_manager->sendQuoteEmail($quote, 'new'); if ($result['success']) { $quote_log_service = new \EasyInvoice\Services\QuoteLogService(); $quote_log_service->logSent($quote_id, $quote->getCustomerEmail()); $this->sendSuccess(array( 'message' => $result['message'], )); } else { $this->sendError($result['message']); } } /** * Generate invoice PDF */ public function generateInvoicePdf() { // Verify nonce $this->verifyNonce('generate_pdf'); // Get invoice ID $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0; if (!$invoice_id) { $this->sendError(__('Invalid invoice ID', 'easy-invoice')); } // Get invoice from repository $repository = InvoiceServiceProvider::getInvoiceRepository(); // For admins, allow access to any invoice status if (current_user_can('manage_options')) { $invoice = $repository->find($invoice_id); } else { // For non-admins, only allow access to published invoices $invoice = $repository->findPublished($invoice_id); } if (!$invoice) { $this->sendError(__('Invoice not found', 'easy-invoice')); } // Redirect to the invoice single page with PDF generation $invoice_url = get_permalink($invoice_id); if ($invoice_url) { wp_redirect(add_query_arg('auto_download_pdf', '1', $invoice_url)); exit; } else { $this->sendError(__('Could not generate invoice URL', 'easy-invoice')); } } /** * Generate quote PDF */ public function generateQuotePdf() { // Verify nonce $this->verifyNonce('generate_quote_pdf'); // Get quote ID $quote_id = isset($_REQUEST['quote_id']) ? intval($_REQUEST['quote_id']) : 0; if (!$quote_id) { $this->sendError(__('Invalid quote ID', 'easy-invoice')); } // Get quote from repository — mirror invoice PDF: only published quotes for non-admins (incl. nopriv). $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository(); if (current_user_can('manage_options')) { $quote = $repository->find($quote_id); } else { $quote = $repository->findPublished($quote_id); } if (!$quote) { $this->sendError(__('Quote not found', 'easy-invoice')); } // Redirect to the quote single page with PDF generation $quote_url = get_permalink($quote_id); if ($quote_url) { wp_redirect(add_query_arg('auto_download_pdf', '1', $quote_url)); exit; } else { $this->sendError(__('Could not generate quote URL', 'easy-invoice')); } } /** * Search clients for the dropdown */ public function searchClients() { $this->verifyNonce('easy_invoice_nonce'); if (!current_user_can('manage_options')) { $this->sendError(__('You do not have permission to perform this action', 'easy-invoice')); } $query = isset($_POST['query']) ? sanitize_text_field($_POST['query']) : ''; // Get client repository $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); // Search clients $clients = $client_repository->search($query); if (empty($clients)) { $this->sendSuccess(array()); } // Format clients for dropdown $formatted_clients = array(); foreach ($clients as $client) { // Get the WordPress user data directly $user = get_user_by('id', $client->getId()); if (!$user) { continue; } // Use Client model properties first, fallback to WordPress user fields $business_name = $client->business_client_name ?: ''; $first_name = $client->first_name ?: $user->first_name ?: ''; $last_name = $client->last_name ?: $user->last_name ?: ''; $email = $client->email ?: $user->user_email ?: ''; // Create display name $client_name = $business_name ?: ($first_name . ' ' . $last_name); if (empty(trim($client_name))) { $client_name = $user->display_name ?: 'User ' . $client->getId(); } // Include all clients, even those with empty emails $formatted_clients[] = array( 'id' => $client->getId(), 'name' => $client_name, 'email' => $email, 'company' => $business_name, 'phone' => $client->phone ?: '', 'address' => $client->address ?: '', 'website' => $client->website ?: '', 'display_name' => $client_name . ' (' . $email . ')' ); } $this->sendSuccess($formatted_clients); } /** * Save additional CSS for invoice/quote */ public function saveAdditionalCSS() { // Verify nonce if (!wp_verify_nonce($_POST['nonce'], 'save_additional_css_nonce')) { $this->sendError('Security check failed'); return; } // Check user capabilities - require administrator if (!current_user_can('manage_options')) { $this->sendError('You do not have permission to perform this action'); return; } // Validate and sanitize post ID $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; if ($post_id <= 0) { $this->sendError('Invalid post ID'); return; } // Verify post exists and user can edit it $post = get_post($post_id); if (!$post || !current_user_can('edit_post', $post_id)) { $this->sendError('You cannot edit this post'); return; } // Verify post type is invoice or quote $valid_post_types = [ \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE ]; if (!in_array($post->post_type, $valid_post_types)) { $this->sendError('Invalid post type'); return; } // Get and sanitize CSS content $css = isset($_POST['css']) ? $_POST['css'] : ''; // Enhanced CSS sanitization $css = $this->sanitizeCSS($css); // Limit CSS length to prevent abuse if (strlen($css) > 50000) { // 50KB limit $this->sendError('CSS content too long'); return; } // Save CSS to post meta $result = update_post_meta($post_id, '_easy_invoice_additional_css', $css); if ($result !== false) { $this->sendSuccess(array( 'message' => 'CSS saved successfully', 'css' => $css, 'post_id' => $post_id )); } else { $this->sendError('Failed to save CSS'); } } /** * Enhanced CSS sanitization - preserves valid CSS while removing threats */ private function sanitizeCSS($css) { // Remove PHP tags first $css = preg_replace('/<\?php.*?\?>/is', '', $css); // Remove HTML tags (script, iframe, object, embed) $css = preg_replace('/]*>.*?<\/script>/is', '', $css); $css = preg_replace('/]*>.*?<\/iframe>/is', '', $css); $css = preg_replace('/]*>.*?<\/object>/is', '', $css); $css = preg_replace('/]*>/is', '', $css); // Remove dangerous CSS constructs $css = preg_replace('/expression\s*\(/i', '', $css); // CSS expressions $css = preg_replace('/javascript\s*:/i', '', $css); // JavaScript protocol $css = preg_replace('/@import\s+url\s*\(/i', '', $css); // @import url() $css = preg_replace('/@import\s+["\'][^"\']+["\']/', '', $css); // @import with quotes $css = preg_replace('/behavior\s*:\s*url\s*\(/i', '', $css); // IE behavior $css = preg_replace('/binding\s*:/i', '', $css); // XBL binding // Remove dangerous CSS functions (but keep safe ones) $dangerous_functions = ['eval', 'exec', 'system', 'passthru', 'shell_exec', 'phpinfo', 'file_get_contents', 'file_put_contents', 'fopen', 'fwrite', 'curl_exec']; foreach ($dangerous_functions as $func) { $css = preg_replace('/\b' . preg_quote($func, '/') . '\s*\(/i', '', $css); } // Remove data URLs that could contain malicious content $css = preg_replace('/data\s*:\s*["\'][^"\']*["\']/i', '', $css); // Remove vbscript: protocol $css = preg_replace('/vbscript\s*:/i', '', $css); // Remove any remaining HTML-like constructs $css = htmlspecialchars_decode($css, ENT_QUOTES); // Basic cleanup - remove excessive whitespace but preserve CSS structure $css = preg_replace('/\s+/', ' ', $css); $css = preg_replace('/;\s*}/', '}', $css); $css = preg_replace('/\s*{\s*/', ' {', $css); $css = preg_replace('/;\s*;/', ';', $css); return trim($css); } }