PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.14
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.14
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.1.14, at includes/Admin/EasyInvoiceAjax.php

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