PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Admin / EasyInvoiceAjax.php

EasyInvoiceAjax.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.1, at includes/Admin/EasyInvoiceAjax.php

1,644 lines 61.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * EasyInvoice AJAX Class
4 *
5 * @package Easy_Invoice
6 * @subpackage Admin
7 */
8
9 namespace EasyInvoice\Admin;
10
11 use EasyInvoice\Models\Invoice;
12 use EasyInvoice\Models\InvoiceItem;
13 use EasyInvoice\Providers\InvoiceServiceProvider;
14 use EasyInvoice\Providers\ClientServiceProvider;
15 use EasyInvoice\Constants\ClientFields;
16
17 /**
18 * EasyInvoiceAjax Class
19 *
20 * Handles all AJAX functionality for the plugin.
21 */
22 class EasyInvoiceAjax {
23 /**
24 * Initialize AJAX handlers
25 */
26 public function init() {
27 // Invoice actions
28 add_action('wp_ajax_easy_invoice_delete', array($this, 'deleteInvoice'));
29 add_action('wp_ajax_easy_invoice_get', array($this, 'getInvoice'));
30 add_action('wp_ajax_easy_invoice_save_invoice', array($this, 'saveInvoice'));
31 add_action('wp_ajax_easy_invoice_save_and_send_invoice', array($this, 'saveAndSendInvoice'));
32
33 // Template actions
34
35
36
37 // Document and email actions
38 add_action('wp_ajax_easy_invoice_download_pdf', array($this, 'downloadPdf'));
39 add_action('wp_ajax_easy_invoice_send_email', array($this, 'sendInvoiceEmail'));
40
41 // Single page actions (for public access)
42 add_action('wp_ajax_easy_invoice_download_invoice_pdf', array($this, 'downloadInvoicePdf'));
43 add_action('wp_ajax_easy_invoice_send_invoice_email', array($this, 'sendInvoiceEmailPublic'));
44 add_action('wp_ajax_easy_invoice_send_quote_email', array($this, 'sendQuoteEmailPublic'));
45 add_action('wp_ajax_nopriv_easy_invoice_download_invoice_pdf', array($this, 'downloadInvoicePdf'));
46 add_action('wp_ajax_nopriv_easy_invoice_send_invoice_email', array($this, 'sendInvoiceEmailPublic'));
47 add_action('wp_ajax_nopriv_easy_invoice_send_quote_email', array($this, 'sendQuoteEmailPublic'));
48
49 // PDF generation actions
50 add_action('wp_ajax_easy_invoice_generate_pdf', array($this, 'generateInvoicePdf'));
51 add_action('wp_ajax_easy_invoice_generate_quote_pdf', array($this, 'generateQuotePdf'));
52
53 // Additional CSS actions
54 add_action('wp_ajax_save_additional_css', array($this, 'saveAdditionalCSS'));
55 add_action('wp_ajax_nopriv_easy_invoice_generate_pdf', array($this, 'generateInvoicePdf'));
56 add_action('wp_ajax_nopriv_easy_invoice_generate_quote_pdf', array($this, 'generateQuotePdf'));
57
58 // Quote document actions
59 add_action('wp_ajax_easy_invoice_download_quote_pdf', array($this, 'downloadQuotePdf'));
60 add_action('wp_ajax_easy_invoice_save_quote', array($this, 'saveQuote'));
61
62 // Client actions
63 add_action('wp_ajax_easy_invoice_save_client', array($this, 'saveClient'));
64 add_action('wp_ajax_easy_invoice_delete_client', array($this, 'deleteClient'));
65 add_action('wp_ajax_easy_invoice_get_client', array($this, 'getClient'));
66 add_action('wp_ajax_easy_invoice_add_client', array($this, 'addClient'));
67 add_action('wp_ajax_easy_invoice_update_client', array($this, 'updateClient'));
68 add_action('wp_ajax_easy_invoice_check_email_exists', array($this, 'checkEmailExists'));
69 add_action('wp_ajax_easy_invoice_generate_password', array($this, 'generatePassword'));
70 add_action('wp_ajax_easy_invoice_search_clients', array($this, 'searchClients'));
71 add_action('wp_ajax_easy_invoice_update_invoices_data', array($this, 'updateInvoicesData'));
72 }
73
74 /**
75 * Save invoice
76 */
77 public function saveInvoice() {
78 $this->verifyNonce('easy_invoice_nonce');
79
80 if (!easy_invoice_user_can('ei_create_invoice')) {
81 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
82 }
83
84 // Get the raw invoice data from the form
85 $raw_invoice_data = isset($_POST['invoice_data']) ? $_POST['invoice_data'] : $_POST;
86
87 // Remove non-invoice fields
88 unset($raw_invoice_data['action']);
89 unset($raw_invoice_data['nonce']);
90
91 // Process the invoice data
92 $invoice_form_manager = new \EasyInvoice\Forms\Invoice\InvoiceFormManager();
93 $invoice_data = $invoice_form_manager->processFormData($raw_invoice_data);
94
95 if (!empty($invoice_data['errors'])) {
96 wp_send_json_error([
97 'message' => 'Validation failed',
98 'errors' => $invoice_data['errors']
99 ]);
100 }
101
102 // Handle items separately - process the natural form submission format
103 if (isset($raw_invoice_data['items']) && is_array($raw_invoice_data['items'])) {
104 // Form submits items as items[0][title], items[0][description], etc.
105 // Convert to array of item objects for processing
106 $items_array = [];
107 foreach ($raw_invoice_data['items'] as $index => $item_data) {
108 if (is_array($item_data)) {
109 $items_array[] = $item_data;
110 }
111 }
112
113 // Process items using the dynamic field system
114 $invoice_data['data']['items'] = $invoice_form_manager->processItemsData($items_array);
115 }
116
117 // Handle special fields that might not be in the form definition
118 if (isset($raw_invoice_data['invoice_id'])) {
119 $invoice_data['data']['invoice_id'] = intval($raw_invoice_data['invoice_id']);
120 }
121
122 if (isset($raw_invoice_data['client_id'])) {
123 $invoice_data['data']['client_id'] = intval($raw_invoice_data['client_id']);
124 }
125
126
127
128 $invoice_id = isset($invoice_data['data']['invoice_id']) ? intval($invoice_data['data']['invoice_id']) : 0;
129
130 $repository = InvoiceServiceProvider::getInvoiceRepository();
131
132 if ($invoice_id > 0) {
133 // Update existing invoice - preserve existing invoice number
134 unset($invoice_data['data']['invoice_number']);
135 unset($invoice_data['data']['number']);
136
137 $invoice = $repository->update($invoice_id, $invoice_data['data']);
138
139 if (!$invoice) {
140
141 $this->sendError(__('Failed to update invoice', 'easy-invoice'));
142 }
143
144 // Use FormProcessor to save form data to database
145 $form_processor = new \EasyInvoice\Forms\FormProcessor();
146 $all_fields = $invoice_form_manager->getAllFields();
147 $form_processor->saveFormDataToDatabase($invoice_data['data'], $all_fields, $invoice);
148
149 $message = __('Invoice updated successfully', 'easy-invoice');
150 } else {
151 // Create new invoice - allow auto-generated invoice number to be saved
152 // The invoice number will be auto-generated by the form and included in the data
153
154 $invoice = $repository->create($invoice_data['data']);
155
156
157 if (!$invoice) {
158 $this->sendError(__('Failed to create invoice', 'easy-invoice'));
159 }
160
161 // Use FormProcessor to save form data to database
162 $form_processor = new \EasyInvoice\Forms\FormProcessor();
163 $all_fields = $invoice_form_manager->getAllFields();
164 $form_processor->saveFormDataToDatabase($invoice_data['data'], $all_fields, $invoice);
165
166 $invoice_id = $invoice->getId();
167 $message = __('Invoice created successfully', 'easy-invoice');
168 }
169
170 // Handle items
171 if (isset($invoice_data['data']['items']) && is_array($invoice_data['data']['items'])) {
172 $invoice->setItems($invoice_data['data']['items']);
173 }
174
175 $invoice_template = get_post_meta($invoice_id, '_easy_invoice_invoice_template', true);
176
177 $invoice_template = $invoice_template=='' ? 'standard': $invoice_template;
178
179 update_option('easy_invoice_last_invoice_template',$invoice_template );
180
181 // Prepare response data
182 $response_data = array(
183 'invoice_id' => $invoice_id,
184 'invoice' => $invoice->toArray(),
185 'toast' => array(
186 'type' => 'success',
187 'message' => $message,
188 'options' => array('duration' => 4000)
189 )
190 );
191
192 // Include client data if invoice has a client
193 if ($invoice->getClientId()) {
194 $client_repository = ClientServiceProvider::getClientRepository();
195 $client = $client_repository->find($invoice->getClientId());
196 if ($client) {
197 $response_data['client'] = array(
198 'id' => $client->getId(),
199 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
200 'email' => $client->getEmail() ?: '',
201 'phone' => $client->getExtraInfo() ?: '',
202 'company' => $client->getBusinessClientName() ?: '',
203 'address' => $client->getAddress() ?: '',
204 'website' => $client->getWebsite() ?: '',
205 );
206 }
207 }
208
209 wp_send_json_success($response_data);
210 }
211
212 /**
213 * Save and send invoice
214 */
215 public function saveAndSendInvoice() {
216 $this->verifyNonce('easy_invoice_nonce');
217
218 // Compound action: needs both create-edit and send rights.
219 if (!easy_invoice_user_can('ei_create_invoice') || !easy_invoice_user_can('ei_send_invoice')) {
220 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
221 }
222
223 // First save the invoice
224 $this->saveInvoice();
225
226 // If we get here, the invoice was saved successfully
227 // Now send the invoice via email
228 $invoice_id = isset($_POST['invoice_data']['invoice_id']) ? intval($_POST['invoice_data']['invoice_id']) : 0;
229
230 if ($invoice_id > 0) {
231 // Send the invoice via email
232 $result = $this->sendInvoiceEmail($invoice_id);
233
234 if ($result['success']) {
235 $this->sendSuccess(array(
236 'message' => __('Invoice saved and sent successfully', 'easy-invoice'),
237 'invoice_id' => $invoice_id
238 ));
239 } else {
240 $this->sendError($result['message']);
241 }
242 } else {
243 $this->sendError(__('Invalid invoice ID for sending', 'easy-invoice'));
244 }
245 }
246
247 /**
248 * Delete invoice
249 */
250 public function deleteInvoice() {
251 $this->verifyNonce('easy_invoice_nonce');
252
253 if (!easy_invoice_user_can('ei_delete_invoice')) {
254 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
255 }
256
257 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
258
259 if ($invoice_id <= 0) {
260 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
261 }
262
263 $repository = InvoiceServiceProvider::getInvoiceRepository();
264 $result = $repository->delete($invoice_id);
265
266 if (!$result) {
267 $this->sendError(__('Failed to delete invoice', 'easy-invoice'));
268 }
269
270 $this->sendSuccess(array(
271 'message' => __('Invoice deleted successfully', 'easy-invoice'),
272 'invoice_id' => $invoice_id,
273 ));
274 }
275
276 /**
277 * Get invoice
278 */
279 public function getInvoice() {
280 $this->verifyNonce('easy_invoice_nonce');
281
282 if (!easy_invoice_user_can('ei_view_invoices')) {
283 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
284 }
285
286 $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0;
287
288 if ($invoice_id <= 0) {
289 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
290 }
291
292 $repository = InvoiceServiceProvider::getInvoiceRepository();
293 $invoice = $repository->find($invoice_id);
294
295 if (!$invoice) {
296 $this->sendError(__('Invoice not found', 'easy-invoice'));
297 }
298
299 $this->sendSuccess(array(
300 'invoice' => $invoice->toArray(),
301 ));
302 }
303
304 /**
305 * Save client
306 */
307 public function saveClient() {
308 $this->verifyNonce('easy_invoice_nonce');
309
310 if (!easy_invoice_user_can('ei_manage_clients')) {
311 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
312 }
313
314 $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0;
315 $client_data = isset($_POST['client_data']) ? $this->sanitizeData($_POST['client_data']) : array();
316
317 if (empty($client_data)) {
318 $this->sendError(__('Invalid client data', 'easy-invoice'));
319 }
320
321 $repository = ClientServiceProvider::getClientRepository();
322
323 if ($client_id > 0) {
324 // Update existing client
325 $client = $repository->update($client_id, $client_data);
326
327 if (!$client) {
328 $this->sendError(__('Failed to update client', 'easy-invoice'));
329 }
330
331 $message = __('Client updated successfully', 'easy-invoice');
332 } else {
333 // Create new client
334 $client = $repository->create($client_data);
335
336 if (!$client) {
337 $this->sendError(__('Failed to create client', 'easy-invoice'));
338 }
339
340 $client_id = $client->getId();
341 $message = __('Client created successfully', 'easy-invoice');
342 }
343
344 $this->sendSuccess(array(
345 'message' => $message,
346 'client_id' => $client_id,
347 'client' => $client->toArray(),
348 ));
349 }
350
351 /**
352 * Delete client
353 */
354 public function deleteClient() {
355 try {
356 $this->verifyNonce('easy_invoice_nonce');
357
358 if (!easy_invoice_user_can('ei_manage_clients')) {
359 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
360 }
361
362 $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0;
363 $delete_associated_documents = isset($_POST['delete_associated_documents']) ? (bool)$_POST['delete_associated_documents'] : false;
364
365 if ($client_id <= 0) {
366 $this->sendError(__('Invalid client ID', 'easy-invoice'));
367 }
368
369 // Check if the user exists and is not an administrator
370 $user = get_user_by('ID', $client_id);
371 if (!$user) {
372 $this->sendError(__('User not found', 'easy-invoice'));
373 }
374
375 if (in_array('administrator', $user->roles)) {
376 $this->sendError(__('Cannot delete administrator accounts', 'easy-invoice'));
377 }
378
379 global $wpdb;
380
381 // Get counts of associated documents
382 $invoice_count = $wpdb->get_var($wpdb->prepare(
383 "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_client_id' AND meta_value = %d",
384 $client_id
385 ));
386
387 $quote_count = $wpdb->get_var($wpdb->prepare(
388 "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_quote_client_id' AND meta_value = %d",
389 $client_id
390 ));
391
392 $payment_count = $wpdb->get_var($wpdb->prepare(
393 "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_easy_payment_client_id' AND meta_value = %d",
394 $client_id
395 ));
396
397 $total_documents = $invoice_count + $quote_count + $payment_count;
398
399 if ($delete_associated_documents) {
400 // Delete all associated documents
401 $this->log(sprintf('Deleting client %d with all associated documents (%d invoices, %d quotes, %d payments)',
402 $client_id, $invoice_count, $quote_count, $payment_count));
403
404 // Delete invoices
405 if ($invoice_count > 0) {
406 $invoices = $wpdb->get_col($wpdb->prepare(
407 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_client_id' AND meta_value = %d",
408 $client_id
409 ));
410 foreach ($invoices as $invoice_id) {
411 wp_delete_post($invoice_id, true);
412 }
413 }
414
415 // Delete quotes
416 if ($quote_count > 0) {
417 $quotes = $wpdb->get_col($wpdb->prepare(
418 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_quote_client_id' AND meta_value = %d",
419 $client_id
420 ));
421 foreach ($quotes as $quote_id) {
422 wp_delete_post($quote_id, true);
423 }
424 }
425
426 // Delete payments
427 if ($payment_count > 0) {
428 $payments = $wpdb->get_col($wpdb->prepare(
429 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_invoice_payment_client_id' AND meta_value = %d",
430 $client_id
431 ));
432 foreach ($payments as $payment_id) {
433 wp_delete_post($payment_id, true);
434 }
435 }
436
437 $message = sprintf(__('Client and all associated documents (%d total) deleted successfully', 'easy-invoice'), $total_documents);
438 } else {
439 // Only remove client associations, preserve documents
440 $this->log(sprintf('Removing client associations for client %d (%d invoices, %d quotes, %d payments)',
441 $client_id, $invoice_count, $quote_count, $payment_count));
442
443 // Remove client associations from invoices
444 if ($invoice_count > 0) {
445 $wpdb->delete(
446 $wpdb->postmeta,
447 ['meta_key' => '_easy_invoice_client_id', 'meta_value' => $client_id]
448 );
449 }
450
451 // Remove client associations from quotes
452 if ($quote_count > 0) {
453 $wpdb->delete(
454 $wpdb->postmeta,
455 ['meta_key' => '_easy_invoice_quote_client_id', 'meta_value' => $client_id]
456 );
457 }
458
459 // Remove client associations from payments
460 if ($payment_count > 0) {
461 $wpdb->delete(
462 $wpdb->postmeta,
463 ['meta_key' => '_easy_payment_client_id', 'meta_value' => $client_id]
464 );
465 }
466
467 $message = sprintf(__('Client deleted successfully. %d documents preserved but client associations removed.', 'easy-invoice'), $total_documents);
468 }
469
470 // Snapshot identity BEFORE delete — once wp_delete_user runs the
471 // user record is gone and we can't backfill the audit context.
472 $deleted_login = $user && $user->user_login ? $user->user_login : '';
473 $deleted_email = $user && $user->user_email ? $user->user_email : '';
474
475 // Delete the WordPress user
476 require_once(ABSPATH . 'wp-admin/includes/user.php');
477 $result = wp_delete_user($client_id);
478
479 if (!$result) {
480 $this->sendError(__('Failed to delete client', 'easy-invoice'));
481 }
482
483 // Audit: record the delete with enough context to investigate later.
484 if (function_exists('easy_invoice_audit_log')) {
485 easy_invoice_audit_log('client_deleted', 'client', $client_id, [
486 'login' => $deleted_login,
487 'email' => $deleted_email,
488 'invoices_affected' => (int) $invoice_count,
489 'quotes_affected' => (int) $quote_count,
490 'payments_affected' => (int) $payment_count,
491 'cascade_delete' => $delete_associated_documents,
492 ]);
493 }
494
495 $this->sendSuccess(array(
496 'message' => $message,
497 'client_id' => $client_id,
498 'documents_deleted' => $delete_associated_documents,
499 'total_documents' => $total_documents
500 ));
501
502 } catch (\Exception $e) {
503 $this->sendError($e->getMessage());
504 }
505 }
506
507 /**
508 * Get client
509 */
510 public function getClient() {
511 $this->verifyNonce('easy_invoice_nonce');
512
513 if (!easy_invoice_user_can('ei_view_clients')) {
514 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
515 }
516
517 $client_id = isset($_REQUEST['client_id']) ? intval($_REQUEST['client_id']) : 0;
518
519 if ($client_id <= 0) {
520 $this->sendError(__('Invalid client ID', 'easy-invoice'));
521 }
522
523 $repository = ClientServiceProvider::getClientRepository();
524 $client = $repository->find($client_id);
525
526 if (!$client) {
527 $this->sendError(__('Client not found', 'easy-invoice'));
528 }
529
530 $client_data = $client->toArray();
531
532 // Return comprehensive client data in a unified format that works for both form population and display
533 $this->sendSuccess(array(
534 // Form population fields (for invoice-builder.js and invoice-form.js)
535 'name' => $client_data['company_name'] ?? $client_data['contact_name'] ?? '',
536 'email' => $client_data['email'] ?? '',
537 'phone' => $client_data['phone'] ?? '',
538 'company' => $client_data['company_name'] ?? '',
539 'address' => $client_data['billing_address'] ?? '',
540 'website' => $client_data['website'] ?? '',
541
542 // Display fields (for client-manager.js)
543 'business_client_name' => $client->getBusinessClientName(),
544 'username' => $client->getUsername(),
545 'extra_info' => $client->getExtraInfo(),
546 'first_name' => $client->getFirstName(),
547 'last_name' => $client->getLastName(),
548
549 // Raw data for backward compatibility
550 'client' => array(
551 'name' => $client_data['company_name'] ?? $client_data['contact_name'] ?? '',
552 'email' => $client_data['email'] ?? '',
553 'phone' => $client_data['phone'] ?? '',
554 'company' => $client_data['company_name'] ?? '',
555 'address' => $client_data['billing_address'] ?? '',
556 'website' => $client_data['website'] ?? '',
557 )
558 ));
559 }
560
561 /**
562 * Verify nonce
563 *
564 * @param string $action The nonce action
565 */
566 private function verifyNonce($action) {
567 // Check for _nonce (standard format) first
568 if (isset($_REQUEST['_nonce']) && wp_verify_nonce($_REQUEST['_nonce'], $action)) {
569 return;
570 }
571
572 // Also check for 'nonce' (client form format)
573 if (isset($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], $action)) {
574 return;
575 }
576
577 // If we get here, neither nonce format was valid
578 $this->sendError(__('Security check failed', 'easy-invoice'));
579 }
580
581 /**
582 * Sanitize data
583 *
584 * @param array $data The data to sanitize
585 * @return array The sanitized data
586 */
587 private function sanitizeData($data) {
588 if (!is_array($data)) {
589 return array();
590 }
591
592 $sanitized = array();
593
594 // Define fields that should allow HTML (like textarea content)
595 $html_fields = [
596 'invoice_description', 'description', 'notes', 'terms',
597 'internal_notes', 'customer_address'
598 ];
599
600 // Define numeric fields
601 $numeric_fields = [
602 'invoice_id', 'client_id', 'discount_value', 'tax_rate'
603 ];
604
605 foreach ($data as $key => $value) {
606 if (is_array($value)) {
607 $sanitized[$key] = $this->sanitizeData($value);
608 } else if (in_array($key, $html_fields)) {
609 // For HTML fields, use wp_kses to allow certain tags but prevent XSS
610 $sanitized[$key] = wp_kses_post($value);
611 } else if (in_array($key, $numeric_fields)) {
612 // For numeric fields, ensure they're valid numbers
613 $sanitized[$key] = is_numeric($value) ? $value : 0;
614 } else {
615 $sanitized[$key] = sanitize_text_field($value);
616 }
617 }
618
619 return $sanitized;
620 }
621
622 /**
623 * Send success response
624 */
625 private function sendSuccess($data = array()) {
626 // Check if we should suppress global toast
627 $suppress_toast = isset($_POST['suppress_global_toast']) && $_POST['suppress_global_toast'] === 'true';
628
629 // Add toast notification if not already present and not suppressed
630 if (!isset($data['toast']) && !$suppress_toast) {
631 $message = isset($data['message']) ? $data['message'] : __('Operation completed successfully', 'easy-invoice');
632 $data['toast'] = array(
633 'type' => 'success',
634 'message' => $message,
635 'options' => array('duration' => 4000)
636 );
637 }
638
639 // Remove toast data if suppressed
640 if ($suppress_toast && isset($data['toast'])) {
641 unset($data['toast']);
642 }
643
644 wp_send_json_success($data);
645 }
646
647 /**
648 * Send error response
649 */
650 private function sendError($message, $data = array()) {
651 // Add toast notification
652 $data['toast'] = array(
653 'type' => 'error',
654 'message' => $message,
655 'options' => array('duration' => 6000)
656 );
657
658 wp_send_json_error($data);
659 }
660
661 /**
662 * Download invoice as PDF
663 */
664 public function downloadPdf() {
665 // Verify nonce
666 $this->verifyNonce('easy_invoice_nonce');
667
668 // Check if user has required capability
669 if (!easy_invoice_user_can('ei_view_invoices')) {
670 $this->sendError(__('You do not have permission to download invoices', 'easy-invoice'));
671 }
672
673 // Get invoice ID
674 $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0;
675
676 if (!$invoice_id) {
677 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
678 }
679
680 // Get invoice from repository
681 $repository = InvoiceServiceProvider::getInvoiceRepository();
682 $invoice = $repository->find($invoice_id);
683
684 if (!$invoice) {
685 $this->sendError(__('Invoice not found', 'easy-invoice'));
686 }
687
688 // Get invoice data for PDF generation
689 $invoice_data = \EasyInvoice\Includes\Helpers\PdfHelper::getInvoiceDataForPdf($invoice);
690
691 // Return success response with invoice data
692 $this->sendSuccess(array(
693 'message' => __('Invoice data retrieved successfully', 'easy-invoice'),
694 'invoice_data' => $invoice_data
695 ));
696 }
697
698 /**
699 * Send invoice via email
700 */
701 public function sendInvoiceEmail() {
702 $this->verifyNonce('easy_invoice_nonce');
703
704 if (!easy_invoice_user_can('ei_send_invoice')) {
705 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
706 }
707
708 // Get invoice ID from POST data
709 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
710
711 if (!$invoice_id) {
712 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
713 }
714
715 $repository = InvoiceServiceProvider::getInvoiceRepository();
716 $invoice = $repository->find($invoice_id);
717
718 if (!$invoice) {
719 $this->sendError(__('Invoice not found', 'easy-invoice'));
720 }
721
722 // Use EmailManager to send the email
723 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
724 $result = $email_manager->sendInvoiceEmail($invoice, 'new');
725
726 if ($result['success']) {
727 // Audit: who sent which invoice to which client, at what time.
728 if (function_exists('easy_invoice_audit_log')) {
729 easy_invoice_audit_log('invoice_sent', 'invoice', $invoice_id, [
730 'recipient' => method_exists($invoice, 'getCustomerEmail') ? $invoice->getCustomerEmail() : '',
731 'context' => 'new',
732 ]);
733 }
734 $this->sendSuccess(array(
735 'message' => $result['message']
736 ));
737 } else {
738 $this->sendError($result['message']);
739 }
740 }
741
742 /**
743 * Download quote as PDF
744 */
745 public function downloadQuotePdf() {
746 // Verify nonce
747 $this->verifyNonce('easy_invoice_nonce');
748
749 // Check if user has required capability
750 if (!easy_invoice_user_can('ei_view_quotes')) {
751 $this->sendError(__('You do not have permission to download quotes', 'easy-invoice'));
752 }
753
754 // Get quote ID
755 $quote_id = isset($_POST['quote_id']) ? intval($_POST['quote_id']) : 0;
756
757 if (!$quote_id) {
758 $this->sendError(__('Invalid quote ID', 'easy-invoice'));
759 }
760
761 // Get quote from repository
762 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
763 $quote = $repository->find($quote_id);
764
765 if (!$quote) {
766 $this->sendError(__('Quote not found', 'easy-invoice'));
767 }
768
769 // For now, return success response with quote data
770 // PDF generation can be implemented later with actual PDF creation
771 $this->sendSuccess(array(
772 'message' => __('Quote data retrieved successfully', 'easy-invoice'),
773 'quote_data' => $quote->toArray(),
774 'download_url' => add_query_arg(array(
775 'action' => 'easy_invoice_generate_quote_pdf',
776 'quote_id' => $quote_id,
777 'nonce' => wp_create_nonce('generate_quote_pdf')
778 ), admin_url('admin-ajax.php'))
779 ));
780 }
781
782 /**
783 * Save quote
784 */
785 public function saveQuote() {
786 $this->verifyNonce('easy_invoice_nonce');
787
788 if (!easy_invoice_user_can('ei_create_quote')) {
789 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
790 }
791
792 // Get the raw quote data from the form
793 $raw_quote_data = isset($_POST['quote_data']) ? $_POST['quote_data'] : $_POST;
794
795 // Remove non-quote fields
796 unset($raw_quote_data['action']);
797 unset($raw_quote_data['nonce']);
798
799 // Process the quote data
800 $quote_form_manager = new \EasyInvoice\Forms\Quote\QuoteFormManager();
801 $quote_data = $quote_form_manager->processFormData($raw_quote_data);
802
803 if (!empty($quote_data['errors'])) {
804 wp_send_json_error([
805 'message' => 'Validation failed',
806 'errors' => $quote_data['errors']
807 ]);
808 }
809
810 // Handle items separately - process the natural form submission format
811 if (isset($raw_quote_data['items']) && is_array($raw_quote_data['items'])) {
812 // Form submits items as items[0][title], items[0][description], etc.
813 // Convert to array of item objects for processing
814 $items_array = [];
815 foreach ($raw_quote_data['items'] as $index => $item_data) {
816 if (is_array($item_data)) {
817 $items_array[] = $item_data;
818 }
819 }
820
821 // Process items using the dynamic field system
822 $quote_data['data']['items'] = $quote_form_manager->processItemsData($items_array);
823 }
824
825 // Handle special fields that might not be in the form definition
826 if (isset($raw_quote_data['quote_id'])) {
827 $quote_data['data']['quote_id'] = intval($raw_quote_data['quote_id']);
828 }
829
830 if (isset($raw_quote_data['client_id'])) {
831 $quote_data['data']['client_id'] = intval($raw_quote_data['client_id']);
832 }
833
834
835
836 $quote_id = isset($quote_data['data']['quote_id']) ? intval($quote_data['data']['quote_id']) : 0;
837
838 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
839
840 if ($quote_id > 0) {
841 // Update existing quote - preserve existing quote number
842 unset($quote_data['data']['quote_number']);
843 unset($quote_data['data']['number']);
844
845 // Get the existing quote first
846 $quote = $repository->find($quote_id);
847
848 if (!$quote) {
849 $this->sendError(__('Failed to find quote for update', 'easy-invoice'));
850 }
851
852 // Use FormProcessor to save form data to database BEFORE repository update
853 $form_processor = new \EasyInvoice\Forms\FormProcessor();
854 $all_fields = $quote_form_manager->getAllFields();
855 $form_processor->saveFormDataToDatabase($quote_data['data'], $all_fields, $quote);
856
857 // Now update the quote with the processed data, passing the existing quote object
858 $quote = $repository->update($quote_id, $quote_data['data'], $quote);
859
860 if (!$quote) {
861 $this->sendError(__('Failed to update quote', 'easy-invoice'));
862 }
863
864 $message = __('Quote updated successfully', 'easy-invoice');
865 } else {
866 // Create new quote - allow auto-generated quote number to be saved
867 // The quote number will be auto-generated by the form and included in the data
868
869 $quote = $repository->create($quote_data['data']);
870
871 if (!$quote) {
872 $this->sendError(__('Failed to create quote', 'easy-invoice'));
873 }
874
875 // Use FormProcessor to save form data to database
876 $form_processor = new \EasyInvoice\Forms\FormProcessor();
877 $all_fields = $quote_form_manager->getAllFields();
878 $form_processor->saveFormDataToDatabase($quote_data['data'], $all_fields, $quote);
879
880 $quote_id = $quote->getId();
881 $message = __('Quote created successfully', 'easy-invoice');
882 }
883
884 // Handle items
885 if (isset($quote_data['data']['items']) && is_array($quote_data['data']['items'])) {
886 $quote->setItems($quote_data['data']['items']);
887 // Save the quote to persist the items to database
888 $quote->save();
889 }
890
891 $quote_template = get_post_meta($quote_id, '_easy_invoice_quote_quote_template', true);
892
893 $quote_template = $quote_template=='' ? 'standard': $quote_template;
894
895 update_option('easy_invoice_last_quote_template',$quote_template );
896 // Prepare response data
897 $response_data = array(
898 'quote_id' => $quote_id,
899 'quote' => $quote->toArray(),
900 'toast' => array(
901 'type' => 'success',
902 'message' => $message,
903 'options' => array('duration' => 4000)
904 )
905 );
906
907 // Include client data if quote has a client
908 if ($quote->getClientId()) {
909 $client_repository = ClientServiceProvider::getClientRepository();
910 $client = $client_repository->find($quote->getClientId());
911 if ($client) {
912 $response_data['client'] = array(
913 'id' => $client->getId(),
914 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
915 'email' => $client->getEmail() ?: '',
916 'phone' => $client->getExtraInfo() ?: '',
917 'company' => $client->getBusinessClientName() ?: '',
918 'address' => $client->getAddress() ?: '',
919 'website' => $client->getWebsite() ?: '',
920 );
921 }
922 }
923
924
925 $this->sendSuccess($response_data);
926 }
927
928 /**
929 * Toggle a template as favorite
930 */
931
932
933
934
935 /**
936 * Check if an email already exists for any client
937 */
938 public function checkEmailExists() {
939 $this->verifyNonce('easy_invoice_nonce');
940
941 // Email-lookup is used during client creation; anyone who can manage
942 // clients can check duplicates.
943 if (!easy_invoice_user_can('ei_manage_clients')) {
944 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
945 }
946
947 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
948
949 if (empty($email)) {
950 $this->sendSuccess(array('exists' => false));
951 }
952
953 $repository = ClientServiceProvider::getClientRepository();
954 $existing_clients = $repository->findByEmail($email);
955
956 $this->sendSuccess(array(
957 'exists' => !empty($existing_clients),
958 'count' => count($existing_clients)
959 ));
960 }
961
962 /**
963 * Generate a secure password.
964 */
965 public function generatePassword() {
966 $this->verifyNonce('easy_invoice_nonce');
967
968 // Used when creating a client (WP user); same gate as client management.
969 if (!easy_invoice_user_can('ei_manage_clients')) {
970 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
971 }
972
973 $password = wp_generate_password(16, true, true);
974
975 // Check if we should suppress global toast
976 $suppress_toast = isset($_POST['suppress_global_toast']) && $_POST['suppress_global_toast'] === 'true';
977
978 $this->sendSuccess(array(
979 'password' => $password,
980 'suppress_toast' => $suppress_toast
981 ));
982 }
983
984 /**
985 * Sanitize invoice items
986 *
987 * @param array $items Raw items data
988 * @return array Sanitized items data
989 */
990 private function sanitizeItems(array $items): array {
991 $sanitized_items = [];
992
993 // Get field configuration for dynamic processing
994 $form_manager = new \EasyInvoice\Forms\Invoice\InvoiceFormManager();
995 $field_config = $form_manager->getItemFields();
996
997 foreach ($items as $item) {
998 if (!is_array($item)) {
999 continue;
1000 }
1001
1002 $sanitized_item = [];
1003
1004 // Process each field dynamically based on configuration
1005 foreach ($field_config as $field) {
1006 $field_name = $field['name'] ?? '';
1007 $field_type = $field['type'] ?? 'text';
1008 $raw_value = $item[$field_name] ?? '';
1009
1010 // Apply field-specific sanitization
1011 switch ($field_type) {
1012 case 'text':
1013 $sanitized_item[$field_name] = sanitize_text_field($raw_value);
1014 break;
1015 case 'textarea':
1016 $sanitized_item[$field_name] = wp_kses_post($raw_value);
1017 break;
1018 case 'number':
1019 $sanitized_item[$field_name] = is_numeric($raw_value) ? floatval($raw_value) : 0;
1020 break;
1021 case 'checkbox':
1022 $sanitized_item[$field_name] = !empty($raw_value) ? true : false;
1023 break;
1024 default:
1025 $sanitized_item[$field_name] = sanitize_text_field($raw_value);
1026 break;
1027 }
1028 }
1029
1030 // Handle legacy field names for backward compatibility
1031 if (isset($item['name']) && !isset($sanitized_item['title'])) {
1032 $sanitized_item['title'] = sanitize_text_field($item['name']);
1033 }
1034 if (isset($item['title']) && !isset($sanitized_item['title'])) {
1035 $sanitized_item['title'] = sanitize_text_field($item['title']);
1036 }
1037
1038 // Only add items that have at least a title/name
1039 if (!empty($sanitized_item['title'])) {
1040 $sanitized_items[] = $sanitized_item;
1041 }
1042 }
1043
1044 return $sanitized_items;
1045 }
1046
1047 /**
1048 * Add a new client (specifically for the client form in templates/clients-page.php)
1049 */
1050 public function addClient() {
1051
1052 $this->verifyNonce('easy_invoice_nonce');
1053
1054 if (!easy_invoice_user_can('ei_manage_clients')) {
1055 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1056 }
1057
1058 // Check if required fields are present
1059 $required_fields = ['business_client_name', 'email', 'username'];
1060 foreach ($required_fields as $field) {
1061 if (!isset($_POST[$field]) || empty($_POST[$field])) {
1062 $this->sendError(__('Missing required field: ' . $field, 'easy-invoice'));
1063 }
1064 }
1065
1066 // Prepare client data
1067 $client_data = [
1068 ClientFields::BUSINESS_CLIENT_NAME => sanitize_text_field($_POST['business_client_name']),
1069 ClientFields::EMAIL => sanitize_email($_POST['email']),
1070 ClientFields::USERNAME => sanitize_user($_POST['username']),
1071 ClientFields::PASSWORD => $_POST['password'],
1072 ClientFields::ADDRESS => sanitize_textarea_field($_POST['address']),
1073 ClientFields::EXTRA_INFO => sanitize_textarea_field($_POST['extra_info']),
1074 ClientFields::FIRST_NAME => sanitize_text_field($_POST['first_name']),
1075 ClientFields::LAST_NAME => sanitize_text_field($_POST['last_name']),
1076 ClientFields::WEBSITE => esc_url_raw($_POST['website']),
1077 ClientFields::PHONE => isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '',
1078 ];
1079
1080
1081
1082 // Basic validation
1083 if (empty($client_data[ClientFields::BUSINESS_CLIENT_NAME]) && (empty($client_data[ClientFields::FIRST_NAME]) || empty($client_data[ClientFields::LAST_NAME]))) {
1084 $this->sendError(__('Please provide a client name or first/last name.', 'easy-invoice'));
1085 }
1086
1087 if (empty($client_data[ClientFields::EMAIL])) {
1088 $this->sendError(__('Email address is required', 'easy-invoice'));
1089 }
1090
1091 $repository = ClientServiceProvider::getClientRepository();
1092
1093 // Create new client
1094 $client = $repository->create($client_data);
1095
1096 if (!$client) {
1097 $this->sendError(__('Failed to create client', 'easy-invoice'));
1098 }
1099
1100 $client_id = $client->getId();
1101
1102 $response_data = array(
1103 'message' => __('Client added successfully', 'easy-invoice'),
1104 'client_id' => $client_id,
1105 'client' => $client->toArray(),
1106 );
1107
1108 $this->sendSuccess($response_data);
1109 }
1110
1111 /**
1112 * Update client from the client edit form
1113 */
1114 public function updateClient() {
1115 $this->verifyNonce('easy_invoice_nonce');
1116
1117 if (!easy_invoice_user_can('ei_manage_clients')) {
1118 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1119 }
1120
1121 $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0;
1122
1123 if ($client_id <= 0) {
1124 $this->sendError(__('Invalid client ID', 'easy-invoice'));
1125 }
1126
1127 // Prepare client data
1128 $client_data = [
1129 ClientFields::BUSINESS_CLIENT_NAME => sanitize_text_field($_POST['business_client_name']),
1130 ClientFields::EMAIL => sanitize_email($_POST['email']),
1131 ClientFields::USERNAME => sanitize_user($_POST['username']),
1132 ClientFields::PASSWORD => $_POST['password'], // Keep password as is, don't sanitize
1133 ClientFields::ADDRESS => sanitize_textarea_field($_POST['address']),
1134 ClientFields::PHONE => isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '',
1135 ClientFields::EXTRA_INFO => sanitize_textarea_field($_POST['extra_info']),
1136 ClientFields::FIRST_NAME => sanitize_text_field($_POST['first_name']),
1137 ClientFields::LAST_NAME => sanitize_text_field($_POST['last_name']),
1138 ClientFields::WEBSITE => esc_url_raw($_POST['website'])
1139 ];
1140
1141 // Remove empty values except password (password can be empty for updates)
1142 $client_data = array_filter($client_data, function($value, $key) {
1143 if ($key === ClientFields::PASSWORD) {
1144 return true; // Always include password field
1145 }
1146 return $value !== '';
1147 }, ARRAY_FILTER_USE_BOTH);
1148
1149 if (empty($client_data)) {
1150 $this->sendError(__('No data provided to update.', 'easy-invoice'));
1151 }
1152
1153 $repository = ClientServiceProvider::getClientRepository();
1154
1155 // Update existing client
1156 $client = $repository->update($client_id, $client_data);
1157
1158 if (!$client) {
1159 $this->sendError(__('Failed to update client', 'easy-invoice'));
1160 }
1161
1162 $this->sendSuccess(array(
1163 'message' => __('Client updated successfully', 'easy-invoice'),
1164 'client_id' => $client_id,
1165 'client' => $client->toArray(),
1166 ));
1167 }
1168
1169 /**
1170 * Update invoices data with missing client information and totals
1171 */
1172 public function updateInvoicesData() {
1173 $this->verifyNonce('easy_invoice_admin_nonce');
1174
1175 // Bulk migration / repair of invoice records — admin-only.
1176 if (!current_user_can('manage_options')) {
1177 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1178 }
1179
1180 $repository = InvoiceServiceProvider::getInvoiceRepository();
1181 $client_repository = ClientServiceProvider::getClientRepository();
1182
1183 // Get all invoices
1184 $invoices = $repository->all();
1185 $updated_count = 0;
1186
1187 foreach ($invoices as $invoice) {
1188 $updated = false;
1189
1190 // Check if client data is missing
1191 $client_id = $invoice->getClientId();
1192 if ($client_id > 0) {
1193 $client = $client_repository->find($client_id);
1194 if ($client) {
1195 // Update customer name if missing
1196 $customer_name = $invoice->getCustomerName();
1197 if (empty($customer_name)) {
1198 $customer_name = $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName());
1199 $invoice->setCustomerName($customer_name);
1200 $updated = true;
1201 }
1202
1203 // Update customer email if missing
1204 $customer_email = $invoice->getCustomerEmail();
1205 if (empty($customer_email)) {
1206 $customer_email = $client->getEmail();
1207 $invoice->setCustomerEmail($customer_email);
1208 $updated = true;
1209 }
1210
1211 // Update customer address if missing
1212 $customer_address = $invoice->getCustomerAddress();
1213 if (empty($customer_address)) {
1214 $customer_address = $client->getAddress();
1215 $invoice->setCustomerAddress($customer_address);
1216 $updated = true;
1217 }
1218 }
1219 }
1220
1221 // Check if total is missing or zero
1222 $total = $invoice->getTotal();
1223 if (empty($total) || $total == 0) {
1224 // Recalculate total from items
1225 $items = $invoice->getItems();
1226 if (!empty($items)) {
1227 $subtotal = 0;
1228 foreach ($items as $item) {
1229 if (method_exists($item, 'getAmount')) {
1230 $subtotal += $item->getAmount();
1231 } elseif (isset($item['amount'])) {
1232 $subtotal += $item['amount'];
1233 }
1234 }
1235
1236 // Calculate discount and tax
1237 $discount = $invoice->getDiscountAmount();
1238 $tax = $invoice->getTaxAmount();
1239
1240 $total = $subtotal - $discount + $tax;
1241
1242 // Save the calculated total
1243 $invoice->setMeta('_easy_invoice_total', $total);
1244 $updated = true;
1245 }
1246 }
1247
1248 if ($updated) {
1249 $updated_count++;
1250 }
1251 }
1252
1253 $this->sendSuccess(array(
1254 'message' => sprintf(__('Updated %d invoices with missing data', 'easy-invoice'), $updated_count),
1255 'updated_count' => $updated_count
1256 ));
1257 }
1258
1259 /**
1260 * Download invoice as PDF (public access)
1261 */
1262 public function downloadInvoicePdf() {
1263 // Verify nonce
1264 $this->verifyNonce('easy_invoice_nonce');
1265
1266 // Get invoice ID
1267 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1268
1269 if (!$invoice_id) {
1270 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
1271 }
1272
1273 // Get invoice from repository (only published invoices for public access)
1274 $repository = InvoiceServiceProvider::getInvoiceRepository();
1275
1276 // For admins, allow access to any invoice status
1277 if (current_user_can('manage_options')) {
1278 $invoice = $repository->find($invoice_id);
1279 } else {
1280 // For non-admins, only allow access to published invoices
1281 $invoice = $repository->findPublished($invoice_id);
1282 }
1283
1284 if (!$invoice) {
1285 $this->sendError(__('Invoice not found', 'easy-invoice'));
1286 }
1287
1288 // Get invoice data for PDF generation
1289 $invoice_data = \EasyInvoice\Includes\Helpers\PdfHelper::getInvoiceDataForPdf($invoice);
1290
1291 // For now, return success response with invoice data
1292 // PDF generation can be implemented later with actual PDF creation
1293 $this->sendSuccess(array(
1294 'message' => __('Invoice data retrieved successfully', 'easy-invoice'),
1295 'invoice_data' => $invoice_data,
1296 'download_url' => add_query_arg(array(
1297 'action' => 'easy_invoice_generate_pdf',
1298 'invoice_id' => $invoice_id,
1299 'nonce' => wp_create_nonce('generate_pdf')
1300 ), admin_url('admin-ajax.php'))
1301 ));
1302 }
1303
1304 /**
1305 * Send invoice via email (public access)
1306 */
1307 public function sendInvoiceEmailPublic() {
1308 $this->verifyNonce('easy_invoice_send_invoice_email');
1309
1310 // Get invoice ID
1311 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1312
1313 if (!$invoice_id) {
1314 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
1315 }
1316
1317 // Get invoice from repository (only published invoices for public access)
1318 $repository = InvoiceServiceProvider::getInvoiceRepository();
1319
1320 // For admins, allow access to any invoice status
1321 if (current_user_can('manage_options')) {
1322 $invoice = $repository->find($invoice_id);
1323 } else {
1324 // For non-admins, only allow access to published invoices
1325 $invoice = $repository->findPublished($invoice_id);
1326 }
1327
1328 if (!$invoice) {
1329 $this->sendError(__('Invoice not found', 'easy-invoice'));
1330 }
1331
1332 // Use EmailManager to send the email
1333 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1334 $result = $email_manager->sendInvoiceEmail($invoice, 'new');
1335
1336 if ($result['success']) {
1337 $this->sendSuccess(array(
1338 'message' => $result['message']
1339 ));
1340 } else {
1341 $this->sendError($result['message']);
1342 }
1343 }
1344
1345 /**
1346 * Send quote via email (admin + public; guests only for published quotes).
1347 */
1348 public function sendQuoteEmailPublic() {
1349 $this->verifyNonce('easy_invoice_send_quote_email');
1350
1351 $quote_id = isset($_POST['quote_id']) ? intval($_POST['quote_id']) : 0;
1352
1353 if (!$quote_id) {
1354 $this->sendError(__('Invalid quote ID', 'easy-invoice'));
1355 }
1356
1357 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
1358
1359 if (current_user_can('manage_options')) {
1360 $quote = $repository->find($quote_id);
1361 } else {
1362 $quote = $repository->findPublished($quote_id);
1363 }
1364
1365 if (!$quote) {
1366 $this->sendError(__('Quote not found', 'easy-invoice'));
1367 }
1368
1369 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1370 $result = $email_manager->sendQuoteEmail($quote, 'new');
1371
1372 if ($result['success']) {
1373 $quote_log_service = new \EasyInvoice\Services\QuoteLogService();
1374 $quote_log_service->logSent($quote_id, $quote->getCustomerEmail());
1375
1376 $this->sendSuccess(array(
1377 'message' => $result['message'],
1378 ));
1379 } else {
1380 $this->sendError($result['message']);
1381 }
1382 }
1383
1384 /**
1385 * Generate invoice PDF
1386 */
1387 public function generateInvoicePdf() {
1388 // Verify nonce
1389 $this->verifyNonce('generate_pdf');
1390
1391 // Get invoice ID
1392 $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0;
1393
1394 if (!$invoice_id) {
1395 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
1396 }
1397
1398 // Get invoice from repository
1399 $repository = InvoiceServiceProvider::getInvoiceRepository();
1400
1401 // For admins, allow access to any invoice status
1402 if (current_user_can('manage_options')) {
1403 $invoice = $repository->find($invoice_id);
1404 } else {
1405 // For non-admins, only allow access to published invoices
1406 $invoice = $repository->findPublished($invoice_id);
1407 }
1408
1409 if (!$invoice) {
1410 $this->sendError(__('Invoice not found', 'easy-invoice'));
1411 }
1412
1413 // Redirect to the invoice single page with PDF generation
1414 $invoice_url = get_permalink($invoice_id);
1415 if ($invoice_url) {
1416 wp_redirect(add_query_arg('auto_download_pdf', '1', $invoice_url));
1417 exit;
1418 } else {
1419 $this->sendError(__('Could not generate invoice URL', 'easy-invoice'));
1420 }
1421 }
1422
1423 /**
1424 * Generate quote PDF
1425 */
1426 public function generateQuotePdf() {
1427 // Verify nonce
1428 $this->verifyNonce('generate_quote_pdf');
1429
1430 // Get quote ID
1431 $quote_id = isset($_REQUEST['quote_id']) ? intval($_REQUEST['quote_id']) : 0;
1432
1433 if (!$quote_id) {
1434 $this->sendError(__('Invalid quote ID', 'easy-invoice'));
1435 }
1436
1437 // Get quote from repository — mirror invoice PDF: only published quotes for non-admins (incl. nopriv).
1438 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
1439 if (current_user_can('manage_options')) {
1440 $quote = $repository->find($quote_id);
1441 } else {
1442 $quote = $repository->findPublished($quote_id);
1443 }
1444
1445 if (!$quote) {
1446 $this->sendError(__('Quote not found', 'easy-invoice'));
1447 }
1448
1449 // Redirect to the quote single page with PDF generation
1450 $quote_url = get_permalink($quote_id);
1451 if ($quote_url) {
1452 wp_redirect(add_query_arg('auto_download_pdf', '1', $quote_url));
1453 exit;
1454 } else {
1455 $this->sendError(__('Could not generate quote URL', 'easy-invoice'));
1456 }
1457 }
1458
1459 /**
1460 * Search clients for the dropdown
1461 */
1462 public function searchClients() {
1463 $this->verifyNonce('easy_invoice_nonce');
1464
1465 if (!easy_invoice_user_can('ei_view_clients')) {
1466 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1467 }
1468
1469 $query = isset($_POST['query']) ? sanitize_text_field($_POST['query']) : '';
1470
1471 // Get client repository
1472 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1473
1474 // Search clients
1475 $clients = $client_repository->search($query);
1476
1477 // Row-level security: restrict to assigned clients for Sales reps
1478 // (users with ei_view_clients but no ei_view_all_clients). Null
1479 // return = unrestricted, no-op.
1480 if (function_exists('easy_invoice_visible_client_ids')) {
1481 $visible = easy_invoice_visible_client_ids();
1482 if (is_array($visible)) {
1483 $allowed = array_flip(array_map('intval', $visible));
1484 $clients = array_values(array_filter($clients, static function ($c) use ($allowed) {
1485 return isset($allowed[(int) $c->getId()]);
1486 }));
1487 }
1488 }
1489
1490 if (empty($clients)) {
1491 $this->sendSuccess(array());
1492 }
1493
1494 // Format clients for dropdown
1495 $formatted_clients = array();
1496 foreach ($clients as $client) {
1497 // Get the WordPress user data directly
1498 $user = get_user_by('id', $client->getId());
1499 if (!$user) {
1500 continue;
1501 }
1502
1503 // Use Client model properties first, fallback to WordPress user fields
1504 $business_name = $client->business_client_name ?: '';
1505 $first_name = $client->first_name ?: $user->first_name ?: '';
1506 $last_name = $client->last_name ?: $user->last_name ?: '';
1507 $email = $client->email ?: $user->user_email ?: '';
1508
1509
1510
1511 // Create display name
1512 $client_name = $business_name ?: ($first_name . ' ' . $last_name);
1513 if (empty(trim($client_name))) {
1514 $client_name = $user->display_name ?: 'User ' . $client->getId();
1515 }
1516
1517 // Include all clients, even those with empty emails
1518 $formatted_clients[] = array(
1519 'id' => $client->getId(),
1520 'name' => $client_name,
1521 'email' => $email,
1522 'company' => $business_name,
1523 'phone' => $client->phone ?: '',
1524 'address' => $client->address ?: '',
1525 'website' => $client->website ?: '',
1526 'display_name' => $client_name . ' (' . $email . ')'
1527 );
1528 }
1529
1530 $this->sendSuccess($formatted_clients);
1531 }
1532
1533 /**
1534 * Save additional CSS for invoice/quote
1535 */
1536 public function saveAdditionalCSS() {
1537 // Verify nonce
1538 if (!wp_verify_nonce($_POST['nonce'], 'save_additional_css_nonce')) {
1539 $this->sendError('Security check failed');
1540 return;
1541 }
1542
1543 // Check user capabilities - require administrator
1544 if (!current_user_can('manage_options')) {
1545 $this->sendError('You do not have permission to perform this action');
1546 return;
1547 }
1548
1549 // Validate and sanitize post ID
1550 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1551 if ($post_id <= 0) {
1552 $this->sendError('Invalid post ID');
1553 return;
1554 }
1555
1556 // Verify post exists and user can edit it
1557 $post = get_post($post_id);
1558 if (!$post || !current_user_can('edit_post', $post_id)) {
1559 $this->sendError('You cannot edit this post');
1560 return;
1561 }
1562
1563 // Verify post type is invoice or quote
1564 $valid_post_types = [
1565 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
1566 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
1567 ];
1568 if (!in_array($post->post_type, $valid_post_types)) {
1569 $this->sendError('Invalid post type');
1570 return;
1571 }
1572
1573 // Get and sanitize CSS content
1574 $css = isset($_POST['css']) ? $_POST['css'] : '';
1575
1576 // Enhanced CSS sanitization
1577 $css = $this->sanitizeCSS($css);
1578
1579 // Limit CSS length to prevent abuse
1580 if (strlen($css) > 50000) { // 50KB limit
1581 $this->sendError('CSS content too long');
1582 return;
1583 }
1584
1585 // Save CSS to post meta
1586 $result = update_post_meta($post_id, '_easy_invoice_additional_css', $css);
1587
1588 if ($result !== false) {
1589 $this->sendSuccess(array(
1590 'message' => 'CSS saved successfully',
1591 'css' => $css,
1592 'post_id' => $post_id
1593 ));
1594 } else {
1595 $this->sendError('Failed to save CSS');
1596 }
1597 }
1598
1599 /**
1600 * Enhanced CSS sanitization - preserves valid CSS while removing threats
1601 */
1602 private function sanitizeCSS($css) {
1603 // Remove PHP tags first
1604 $css = preg_replace('/<\?php.*?\?>/is', '', $css);
1605
1606 // Remove HTML tags (script, iframe, object, embed)
1607 $css = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $css);
1608 $css = preg_replace('/<iframe[^>]*>.*?<\/iframe>/is', '', $css);
1609 $css = preg_replace('/<object[^>]*>.*?<\/object>/is', '', $css);
1610 $css = preg_replace('/<embed[^>]*>/is', '', $css);
1611
1612 // Remove dangerous CSS constructs
1613 $css = preg_replace('/expression\s*\(/i', '', $css); // CSS expressions
1614 $css = preg_replace('/javascript\s*:/i', '', $css); // JavaScript protocol
1615 $css = preg_replace('/@import\s+url\s*\(/i', '', $css); // @import url()
1616 $css = preg_replace('/@import\s+["\'][^"\']+["\']/', '', $css); // @import with quotes
1617 $css = preg_replace('/behavior\s*:\s*url\s*\(/i', '', $css); // IE behavior
1618 $css = preg_replace('/binding\s*:/i', '', $css); // XBL binding
1619
1620 // Remove dangerous CSS functions (but keep safe ones)
1621 $dangerous_functions = ['eval', 'exec', 'system', 'passthru', 'shell_exec', 'phpinfo', 'file_get_contents', 'file_put_contents', 'fopen', 'fwrite', 'curl_exec'];
1622 foreach ($dangerous_functions as $func) {
1623 $css = preg_replace('/\b' . preg_quote($func, '/') . '\s*\(/i', '', $css);
1624 }
1625
1626 // Remove data URLs that could contain malicious content
1627 $css = preg_replace('/data\s*:\s*["\'][^"\']*["\']/i', '', $css);
1628
1629 // Remove vbscript: protocol
1630 $css = preg_replace('/vbscript\s*:/i', '', $css);
1631
1632 // Remove any remaining HTML-like constructs
1633 $css = htmlspecialchars_decode($css, ENT_QUOTES);
1634
1635 // Basic cleanup - remove excessive whitespace but preserve CSS structure
1636 $css = preg_replace('/\s+/', ' ', $css);
1637 $css = preg_replace('/;\s*}/', '}', $css);
1638 $css = preg_replace('/\s*{\s*/', ' {', $css);
1639 $css = preg_replace('/;\s*;/', ';', $css);
1640
1641 return trim($css);
1642 }
1643 }
1644