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

1,603 lines 59.0 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_easy_invoice_send_quote_email', array($this, 'sendQuoteEmailPublic'));
46 add_action('wp_ajax_nopriv_easy_invoice_download_invoice_pdf', array($this, 'downloadInvoicePdf'));
47 add_action('wp_ajax_nopriv_easy_invoice_send_invoice_email', array($this, 'sendInvoiceEmailPublic'));
48 add_action('wp_ajax_nopriv_easy_invoice_send_quote_email', array($this, 'sendQuoteEmailPublic'));
49
50 // PDF generation actions
51 add_action('wp_ajax_easy_invoice_generate_pdf', array($this, 'generateInvoicePdf'));
52 add_action('wp_ajax_easy_invoice_generate_quote_pdf', array($this, 'generateQuotePdf'));
53
54 // Additional CSS actions
55 add_action('wp_ajax_save_additional_css', array($this, 'saveAdditionalCSS'));
56 add_action('wp_ajax_nopriv_easy_invoice_generate_pdf', array($this, 'generateInvoicePdf'));
57 add_action('wp_ajax_nopriv_easy_invoice_generate_quote_pdf', array($this, 'generateQuotePdf'));
58
59 // Quote document actions
60 add_action('wp_ajax_easy_invoice_download_quote_pdf', array($this, 'downloadQuotePdf'));
61 add_action('wp_ajax_easy_invoice_save_quote', array($this, 'saveQuote'));
62
63 // Client actions
64 add_action('wp_ajax_easy_invoice_save_client', array($this, 'saveClient'));
65 add_action('wp_ajax_easy_invoice_delete_client', array($this, 'deleteClient'));
66 add_action('wp_ajax_easy_invoice_get_client', array($this, 'getClient'));
67 add_action('wp_ajax_easy_invoice_add_client', array($this, 'addClient'));
68 add_action('wp_ajax_easy_invoice_update_client', array($this, 'updateClient'));
69 add_action('wp_ajax_easy_invoice_check_email_exists', array($this, 'checkEmailExists'));
70 add_action('wp_ajax_easy_invoice_generate_password', array($this, 'generatePassword'));
71 add_action('wp_ajax_easy_invoice_search_clients', array($this, 'searchClients'));
72 add_action('wp_ajax_easy_invoice_update_invoices_data', array($this, 'updateInvoicesData'));
73 }
74
75 /**
76 * Save invoice
77 */
78 public function saveInvoice() {
79 $this->verifyNonce('easy_invoice_nonce');
80
81 if (!current_user_can('manage_options')) {
82 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
83 }
84
85 // Get the raw invoice data from the form
86 $raw_invoice_data = isset($_POST['invoice_data']) ? $_POST['invoice_data'] : $_POST;
87
88 // Remove non-invoice fields
89 unset($raw_invoice_data['action']);
90 unset($raw_invoice_data['nonce']);
91
92 // Process the invoice data
93 $invoice_form_manager = new \EasyInvoice\Forms\Invoice\InvoiceFormManager();
94 $invoice_data = $invoice_form_manager->processFormData($raw_invoice_data);
95
96 if (!empty($invoice_data['errors'])) {
97 wp_send_json_error([
98 'message' => 'Validation failed',
99 'errors' => $invoice_data['errors']
100 ]);
101 }
102
103 // Handle items separately - process the natural form submission format
104 if (isset($raw_invoice_data['items']) && is_array($raw_invoice_data['items'])) {
105 // Form submits items as items[0][title], items[0][description], etc.
106 // Convert to array of item objects for processing
107 $items_array = [];
108 foreach ($raw_invoice_data['items'] as $index => $item_data) {
109 if (is_array($item_data)) {
110 $items_array[] = $item_data;
111 }
112 }
113
114 // Process items using the dynamic field system
115 $invoice_data['data']['items'] = $invoice_form_manager->processItemsData($items_array);
116 }
117
118 // Handle special fields that might not be in the form definition
119 if (isset($raw_invoice_data['invoice_id'])) {
120 $invoice_data['data']['invoice_id'] = intval($raw_invoice_data['invoice_id']);
121 }
122
123 if (isset($raw_invoice_data['client_id'])) {
124 $invoice_data['data']['client_id'] = intval($raw_invoice_data['client_id']);
125 }
126
127
128
129 $invoice_id = isset($invoice_data['data']['invoice_id']) ? intval($invoice_data['data']['invoice_id']) : 0;
130
131 $repository = InvoiceServiceProvider::getInvoiceRepository();
132
133 if ($invoice_id > 0) {
134 // Update existing invoice - preserve existing invoice number
135 unset($invoice_data['data']['invoice_number']);
136 unset($invoice_data['data']['number']);
137
138 $invoice = $repository->update($invoice_id, $invoice_data['data']);
139
140 if (!$invoice) {
141
142 $this->sendError(__('Failed to update invoice', 'easy-invoice'));
143 }
144
145 // Use FormProcessor to save form data to database
146 $form_processor = new \EasyInvoice\Forms\FormProcessor();
147 $all_fields = $invoice_form_manager->getAllFields();
148 $form_processor->saveFormDataToDatabase($invoice_data['data'], $all_fields, $invoice);
149
150 $message = __('Invoice updated successfully', 'easy-invoice');
151 } else {
152 // Create new invoice - allow auto-generated invoice number to be saved
153 // The invoice number will be auto-generated by the form and included in the data
154
155 $invoice = $repository->create($invoice_data['data']);
156
157
158 if (!$invoice) {
159 $this->sendError(__('Failed to create invoice', 'easy-invoice'));
160 }
161
162 // Use FormProcessor to save form data to database
163 $form_processor = new \EasyInvoice\Forms\FormProcessor();
164 $all_fields = $invoice_form_manager->getAllFields();
165 $form_processor->saveFormDataToDatabase($invoice_data['data'], $all_fields, $invoice);
166
167 $invoice_id = $invoice->getId();
168 $message = __('Invoice created successfully', 'easy-invoice');
169 }
170
171 // Handle items
172 if (isset($invoice_data['data']['items']) && is_array($invoice_data['data']['items'])) {
173 $invoice->setItems($invoice_data['data']['items']);
174 }
175
176 $invoice_template = get_post_meta($invoice_id, '_easy_invoice_invoice_template', true);
177
178 $invoice_template = $invoice_template=='' ? 'standard': $invoice_template;
179
180 update_option('easy_invoice_last_invoice_template',$invoice_template );
181
182 // Prepare response data
183 $response_data = array(
184 'invoice_id' => $invoice_id,
185 'invoice' => $invoice->toArray(),
186 'toast' => array(
187 'type' => 'success',
188 'message' => $message,
189 'options' => array('duration' => 4000)
190 )
191 );
192
193 // Include client data if invoice has a client
194 if ($invoice->getClientId()) {
195 $client_repository = ClientServiceProvider::getClientRepository();
196 $client = $client_repository->find($invoice->getClientId());
197 if ($client) {
198 $response_data['client'] = array(
199 'id' => $client->getId(),
200 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
201 'email' => $client->getEmail() ?: '',
202 'phone' => $client->getExtraInfo() ?: '',
203 'company' => $client->getBusinessClientName() ?: '',
204 'address' => $client->getAddress() ?: '',
205 'website' => $client->getWebsite() ?: '',
206 );
207 }
208 }
209
210 wp_send_json_success($response_data);
211 }
212
213 /**
214 * Save and send invoice
215 */
216 public function saveAndSendInvoice() {
217 $this->verifyNonce('easy_invoice_nonce');
218
219 if (!current_user_can('manage_options')) {
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 (!current_user_can('manage_options')) {
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 (!current_user_can('manage_options')) {
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 (!current_user_can('manage_options')) {
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 (!current_user_can('manage_options')) {
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 // Delete the WordPress user
471 require_once(ABSPATH . 'wp-admin/includes/user.php');
472 $result = wp_delete_user($client_id);
473
474 if (!$result) {
475 $this->sendError(__('Failed to delete client', 'easy-invoice'));
476 }
477
478 $this->sendSuccess(array(
479 'message' => $message,
480 'client_id' => $client_id,
481 'documents_deleted' => $delete_associated_documents,
482 'total_documents' => $total_documents
483 ));
484
485 } catch (\Exception $e) {
486 $this->sendError($e->getMessage());
487 }
488 }
489
490 /**
491 * Get client
492 */
493 public function getClient() {
494 $this->verifyNonce('easy_invoice_nonce');
495
496 if (!current_user_can('manage_options')) {
497 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
498 }
499
500 $client_id = isset($_REQUEST['client_id']) ? intval($_REQUEST['client_id']) : 0;
501
502 if ($client_id <= 0) {
503 $this->sendError(__('Invalid client ID', 'easy-invoice'));
504 }
505
506 $repository = ClientServiceProvider::getClientRepository();
507 $client = $repository->find($client_id);
508
509 if (!$client) {
510 $this->sendError(__('Client not found', 'easy-invoice'));
511 }
512
513 $client_data = $client->toArray();
514
515 // Return comprehensive client data in a unified format that works for both form population and display
516 $this->sendSuccess(array(
517 // Form population fields (for invoice-builder.js and invoice-form.js)
518 'name' => $client_data['company_name'] ?? $client_data['contact_name'] ?? '',
519 'email' => $client_data['email'] ?? '',
520 'phone' => $client_data['phone'] ?? '',
521 'company' => $client_data['company_name'] ?? '',
522 'address' => $client_data['billing_address'] ?? '',
523 'website' => $client_data['website'] ?? '',
524
525 // Display fields (for client-manager.js)
526 'business_client_name' => $client->getBusinessClientName(),
527 'username' => $client->getUsername(),
528 'extra_info' => $client->getExtraInfo(),
529 'first_name' => $client->getFirstName(),
530 'last_name' => $client->getLastName(),
531
532 // Raw data for backward compatibility
533 'client' => array(
534 'name' => $client_data['company_name'] ?? $client_data['contact_name'] ?? '',
535 'email' => $client_data['email'] ?? '',
536 'phone' => $client_data['phone'] ?? '',
537 'company' => $client_data['company_name'] ?? '',
538 'address' => $client_data['billing_address'] ?? '',
539 'website' => $client_data['website'] ?? '',
540 )
541 ));
542 }
543
544 /**
545 * Verify nonce
546 *
547 * @param string $action The nonce action
548 */
549 private function verifyNonce($action) {
550 // Check for _nonce (standard format) first
551 if (isset($_REQUEST['_nonce']) && wp_verify_nonce($_REQUEST['_nonce'], $action)) {
552 return;
553 }
554
555 // Also check for 'nonce' (client form format)
556 if (isset($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], $action)) {
557 return;
558 }
559
560 // If we get here, neither nonce format was valid
561 $this->sendError(__('Security check failed', 'easy-invoice'));
562 }
563
564 /**
565 * Sanitize data
566 *
567 * @param array $data The data to sanitize
568 * @return array The sanitized data
569 */
570 private function sanitizeData($data) {
571 if (!is_array($data)) {
572 return array();
573 }
574
575 $sanitized = array();
576
577 // Define fields that should allow HTML (like textarea content)
578 $html_fields = [
579 'invoice_description', 'description', 'notes', 'terms',
580 'internal_notes', 'customer_address'
581 ];
582
583 // Define numeric fields
584 $numeric_fields = [
585 'invoice_id', 'client_id', 'discount_value', 'tax_rate'
586 ];
587
588 foreach ($data as $key => $value) {
589 if (is_array($value)) {
590 $sanitized[$key] = $this->sanitizeData($value);
591 } else if (in_array($key, $html_fields)) {
592 // For HTML fields, use wp_kses to allow certain tags but prevent XSS
593 $sanitized[$key] = wp_kses_post($value);
594 } else if (in_array($key, $numeric_fields)) {
595 // For numeric fields, ensure they're valid numbers
596 $sanitized[$key] = is_numeric($value) ? $value : 0;
597 } else {
598 $sanitized[$key] = sanitize_text_field($value);
599 }
600 }
601
602 return $sanitized;
603 }
604
605 /**
606 * Send success response
607 */
608 private function sendSuccess($data = array()) {
609 // Check if we should suppress global toast
610 $suppress_toast = isset($_POST['suppress_global_toast']) && $_POST['suppress_global_toast'] === 'true';
611
612 // Add toast notification if not already present and not suppressed
613 if (!isset($data['toast']) && !$suppress_toast) {
614 $message = isset($data['message']) ? $data['message'] : __('Operation completed successfully', 'easy-invoice');
615 $data['toast'] = array(
616 'type' => 'success',
617 'message' => $message,
618 'options' => array('duration' => 4000)
619 );
620 }
621
622 // Remove toast data if suppressed
623 if ($suppress_toast && isset($data['toast'])) {
624 unset($data['toast']);
625 }
626
627 wp_send_json_success($data);
628 }
629
630 /**
631 * Send error response
632 */
633 private function sendError($message, $data = array()) {
634 // Add toast notification
635 $data['toast'] = array(
636 'type' => 'error',
637 'message' => $message,
638 'options' => array('duration' => 6000)
639 );
640
641 wp_send_json_error($data);
642 }
643
644 /**
645 * Download invoice as PDF
646 */
647 public function downloadPdf() {
648 // Verify nonce
649 $this->verifyNonce('easy_invoice_nonce');
650
651 // Check if user has required capability
652 if (!current_user_can('edit_posts')) {
653 $this->sendError(__('You do not have permission to download invoices', 'easy-invoice'));
654 }
655
656 // Get invoice ID
657 $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0;
658
659 if (!$invoice_id) {
660 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
661 }
662
663 // Get invoice from repository
664 $repository = InvoiceServiceProvider::getInvoiceRepository();
665 $invoice = $repository->find($invoice_id);
666
667 if (!$invoice) {
668 $this->sendError(__('Invoice not found', 'easy-invoice'));
669 }
670
671 // Get invoice data for PDF generation
672 $invoice_data = \EasyInvoice\Includes\Helpers\PdfHelper::getInvoiceDataForPdf($invoice);
673
674 // Return success response with invoice data
675 $this->sendSuccess(array(
676 'message' => __('Invoice data retrieved successfully', 'easy-invoice'),
677 'invoice_data' => $invoice_data
678 ));
679 }
680
681 /**
682 * Send invoice via email
683 */
684 public function sendInvoiceEmail() {
685 $this->verifyNonce('easy_invoice_nonce');
686
687 if (!current_user_can('manage_options')) {
688 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
689 }
690
691 // Get invoice ID from POST data
692 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
693
694 if (!$invoice_id) {
695 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
696 }
697
698 $repository = InvoiceServiceProvider::getInvoiceRepository();
699 $invoice = $repository->find($invoice_id);
700
701 if (!$invoice) {
702 $this->sendError(__('Invoice not found', 'easy-invoice'));
703 }
704
705 // Use EmailManager to send the email
706 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
707 $result = $email_manager->sendInvoiceEmail($invoice, 'new');
708
709 if ($result['success']) {
710 $this->sendSuccess(array(
711 'message' => $result['message']
712 ));
713 } else {
714 $this->sendError($result['message']);
715 }
716 }
717
718 /**
719 * Download quote as PDF
720 */
721 public function downloadQuotePdf() {
722 // Verify nonce
723 $this->verifyNonce('easy_invoice_nonce');
724
725 // Check if user has required capability
726 if (!current_user_can('edit_posts')) {
727 $this->sendError(__('You do not have permission to download quotes', 'easy-invoice'));
728 }
729
730 // Get quote ID
731 $quote_id = isset($_POST['quote_id']) ? intval($_POST['quote_id']) : 0;
732
733 if (!$quote_id) {
734 $this->sendError(__('Invalid quote ID', 'easy-invoice'));
735 }
736
737 // Get quote from repository
738 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
739 $quote = $repository->find($quote_id);
740
741 if (!$quote) {
742 $this->sendError(__('Quote not found', 'easy-invoice'));
743 }
744
745 // For now, return success response with quote data
746 // PDF generation can be implemented later with actual PDF creation
747 $this->sendSuccess(array(
748 'message' => __('Quote data retrieved successfully', 'easy-invoice'),
749 'quote_data' => $quote->toArray(),
750 'download_url' => add_query_arg(array(
751 'action' => 'easy_invoice_generate_quote_pdf',
752 'quote_id' => $quote_id,
753 'nonce' => wp_create_nonce('generate_quote_pdf')
754 ), admin_url('admin-ajax.php'))
755 ));
756 }
757
758 /**
759 * Save quote
760 */
761 public function saveQuote() {
762 $this->verifyNonce('easy_invoice_nonce');
763
764 if (!current_user_can('manage_options')) {
765 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
766 }
767
768 // Get the raw quote data from the form
769 $raw_quote_data = isset($_POST['quote_data']) ? $_POST['quote_data'] : $_POST;
770
771 // Remove non-quote fields
772 unset($raw_quote_data['action']);
773 unset($raw_quote_data['nonce']);
774
775 // Process the quote data
776 $quote_form_manager = new \EasyInvoice\Forms\Quote\QuoteFormManager();
777 $quote_data = $quote_form_manager->processFormData($raw_quote_data);
778
779 if (!empty($quote_data['errors'])) {
780 wp_send_json_error([
781 'message' => 'Validation failed',
782 'errors' => $quote_data['errors']
783 ]);
784 }
785
786 // Handle items separately - process the natural form submission format
787 if (isset($raw_quote_data['items']) && is_array($raw_quote_data['items'])) {
788 // Form submits items as items[0][title], items[0][description], etc.
789 // Convert to array of item objects for processing
790 $items_array = [];
791 foreach ($raw_quote_data['items'] as $index => $item_data) {
792 if (is_array($item_data)) {
793 $items_array[] = $item_data;
794 }
795 }
796
797 // Process items using the dynamic field system
798 $quote_data['data']['items'] = $quote_form_manager->processItemsData($items_array);
799 }
800
801 // Handle special fields that might not be in the form definition
802 if (isset($raw_quote_data['quote_id'])) {
803 $quote_data['data']['quote_id'] = intval($raw_quote_data['quote_id']);
804 }
805
806 if (isset($raw_quote_data['client_id'])) {
807 $quote_data['data']['client_id'] = intval($raw_quote_data['client_id']);
808 }
809
810
811
812 $quote_id = isset($quote_data['data']['quote_id']) ? intval($quote_data['data']['quote_id']) : 0;
813
814 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
815
816 if ($quote_id > 0) {
817 // Update existing quote - preserve existing quote number
818 unset($quote_data['data']['quote_number']);
819 unset($quote_data['data']['number']);
820
821 // Get the existing quote first
822 $quote = $repository->find($quote_id);
823
824 if (!$quote) {
825 $this->sendError(__('Failed to find quote for update', 'easy-invoice'));
826 }
827
828 // Use FormProcessor to save form data to database BEFORE repository update
829 $form_processor = new \EasyInvoice\Forms\FormProcessor();
830 $all_fields = $quote_form_manager->getAllFields();
831 $form_processor->saveFormDataToDatabase($quote_data['data'], $all_fields, $quote);
832
833 // Now update the quote with the processed data, passing the existing quote object
834 $quote = $repository->update($quote_id, $quote_data['data'], $quote);
835
836 if (!$quote) {
837 $this->sendError(__('Failed to update quote', 'easy-invoice'));
838 }
839
840 $message = __('Quote updated successfully', 'easy-invoice');
841 } else {
842 // Create new quote - allow auto-generated quote number to be saved
843 // The quote number will be auto-generated by the form and included in the data
844
845 $quote = $repository->create($quote_data['data']);
846
847 if (!$quote) {
848 $this->sendError(__('Failed to create quote', 'easy-invoice'));
849 }
850
851 // Use FormProcessor to save form data to database
852 $form_processor = new \EasyInvoice\Forms\FormProcessor();
853 $all_fields = $quote_form_manager->getAllFields();
854 $form_processor->saveFormDataToDatabase($quote_data['data'], $all_fields, $quote);
855
856 $quote_id = $quote->getId();
857 $message = __('Quote created successfully', 'easy-invoice');
858 }
859
860 // Handle items
861 if (isset($quote_data['data']['items']) && is_array($quote_data['data']['items'])) {
862 $quote->setItems($quote_data['data']['items']);
863 // Save the quote to persist the items to database
864 $quote->save();
865 }
866
867 $quote_template = get_post_meta($quote_id, '_easy_invoice_quote_quote_template', true);
868
869 $quote_template = $quote_template=='' ? 'standard': $quote_template;
870
871 update_option('easy_invoice_last_quote_template',$quote_template );
872 // Prepare response data
873 $response_data = array(
874 'quote_id' => $quote_id,
875 'quote' => $quote->toArray(),
876 'toast' => array(
877 'type' => 'success',
878 'message' => $message,
879 'options' => array('duration' => 4000)
880 )
881 );
882
883 // Include client data if quote has a client
884 if ($quote->getClientId()) {
885 $client_repository = ClientServiceProvider::getClientRepository();
886 $client = $client_repository->find($quote->getClientId());
887 if ($client) {
888 $response_data['client'] = array(
889 'id' => $client->getId(),
890 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
891 'email' => $client->getEmail() ?: '',
892 'phone' => $client->getExtraInfo() ?: '',
893 'company' => $client->getBusinessClientName() ?: '',
894 'address' => $client->getAddress() ?: '',
895 'website' => $client->getWebsite() ?: '',
896 );
897 }
898 }
899
900
901 $this->sendSuccess($response_data);
902 }
903
904 /**
905 * Toggle a template as favorite
906 */
907
908
909
910
911 /**
912 * Check if an email already exists for any client
913 */
914 public function checkEmailExists() {
915 $this->verifyNonce('easy_invoice_nonce');
916
917 if (!current_user_can('manage_options')) {
918 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
919 }
920
921 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
922
923 if (empty($email)) {
924 $this->sendSuccess(array('exists' => false));
925 }
926
927 $repository = ClientServiceProvider::getClientRepository();
928 $existing_clients = $repository->findByEmail($email);
929
930 $this->sendSuccess(array(
931 'exists' => !empty($existing_clients),
932 'count' => count($existing_clients)
933 ));
934 }
935
936 /**
937 * Generate a secure password.
938 */
939 public function generatePassword() {
940 $this->verifyNonce('easy_invoice_nonce');
941
942 if (!current_user_can('manage_options')) {
943 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
944 }
945
946 $password = wp_generate_password(16, true, true);
947
948 // Check if we should suppress global toast
949 $suppress_toast = isset($_POST['suppress_global_toast']) && $_POST['suppress_global_toast'] === 'true';
950
951 $this->sendSuccess(array(
952 'password' => $password,
953 'suppress_toast' => $suppress_toast
954 ));
955 }
956
957 /**
958 * Sanitize invoice items
959 *
960 * @param array $items Raw items data
961 * @return array Sanitized items data
962 */
963 private function sanitizeItems(array $items): array {
964 $sanitized_items = [];
965
966 // Get field configuration for dynamic processing
967 $form_manager = new \EasyInvoice\Forms\Invoice\InvoiceFormManager();
968 $field_config = $form_manager->getItemFields();
969
970 foreach ($items as $item) {
971 if (!is_array($item)) {
972 continue;
973 }
974
975 $sanitized_item = [];
976
977 // Process each field dynamically based on configuration
978 foreach ($field_config as $field) {
979 $field_name = $field['name'] ?? '';
980 $field_type = $field['type'] ?? 'text';
981 $raw_value = $item[$field_name] ?? '';
982
983 // Apply field-specific sanitization
984 switch ($field_type) {
985 case 'text':
986 $sanitized_item[$field_name] = sanitize_text_field($raw_value);
987 break;
988 case 'textarea':
989 $sanitized_item[$field_name] = wp_kses_post($raw_value);
990 break;
991 case 'number':
992 $sanitized_item[$field_name] = is_numeric($raw_value) ? floatval($raw_value) : 0;
993 break;
994 case 'checkbox':
995 $sanitized_item[$field_name] = !empty($raw_value) ? true : false;
996 break;
997 default:
998 $sanitized_item[$field_name] = sanitize_text_field($raw_value);
999 break;
1000 }
1001 }
1002
1003 // Handle legacy field names for backward compatibility
1004 if (isset($item['name']) && !isset($sanitized_item['title'])) {
1005 $sanitized_item['title'] = sanitize_text_field($item['name']);
1006 }
1007 if (isset($item['title']) && !isset($sanitized_item['title'])) {
1008 $sanitized_item['title'] = sanitize_text_field($item['title']);
1009 }
1010
1011 // Only add items that have at least a title/name
1012 if (!empty($sanitized_item['title'])) {
1013 $sanitized_items[] = $sanitized_item;
1014 }
1015 }
1016
1017 return $sanitized_items;
1018 }
1019
1020 /**
1021 * Add a new client (specifically for the client form in templates/clients-page.php)
1022 */
1023 public function addClient() {
1024
1025 $this->verifyNonce('easy_invoice_nonce');
1026
1027 if (!current_user_can('manage_options')) {
1028 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1029 }
1030
1031 // Check if required fields are present
1032 $required_fields = ['business_client_name', 'email', 'username'];
1033 foreach ($required_fields as $field) {
1034 if (!isset($_POST[$field]) || empty($_POST[$field])) {
1035 $this->sendError(__('Missing required field: ' . $field, 'easy-invoice'));
1036 }
1037 }
1038
1039 // Prepare client data
1040 $client_data = [
1041 ClientFields::BUSINESS_CLIENT_NAME => sanitize_text_field($_POST['business_client_name']),
1042 ClientFields::EMAIL => sanitize_email($_POST['email']),
1043 ClientFields::USERNAME => sanitize_user($_POST['username']),
1044 ClientFields::PASSWORD => $_POST['password'],
1045 ClientFields::ADDRESS => sanitize_textarea_field($_POST['address']),
1046 ClientFields::EXTRA_INFO => sanitize_textarea_field($_POST['extra_info']),
1047 ClientFields::FIRST_NAME => sanitize_text_field($_POST['first_name']),
1048 ClientFields::LAST_NAME => sanitize_text_field($_POST['last_name']),
1049 ClientFields::WEBSITE => esc_url_raw($_POST['website']),
1050 ClientFields::PHONE => isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '',
1051 ];
1052
1053
1054
1055 // Basic validation
1056 if (empty($client_data[ClientFields::BUSINESS_CLIENT_NAME]) && (empty($client_data[ClientFields::FIRST_NAME]) || empty($client_data[ClientFields::LAST_NAME]))) {
1057 $this->sendError(__('Please provide a client name or first/last name.', 'easy-invoice'));
1058 }
1059
1060 if (empty($client_data[ClientFields::EMAIL])) {
1061 $this->sendError(__('Email address is required', 'easy-invoice'));
1062 }
1063
1064 $repository = ClientServiceProvider::getClientRepository();
1065
1066 // Create new client
1067 $client = $repository->create($client_data);
1068
1069 if (!$client) {
1070 $this->sendError(__('Failed to create client', 'easy-invoice'));
1071 }
1072
1073 $client_id = $client->getId();
1074
1075 $response_data = array(
1076 'message' => __('Client added successfully', 'easy-invoice'),
1077 'client_id' => $client_id,
1078 'client' => $client->toArray(),
1079 );
1080
1081 $this->sendSuccess($response_data);
1082 }
1083
1084 /**
1085 * Update client from the client edit form
1086 */
1087 public function updateClient() {
1088 $this->verifyNonce('easy_invoice_nonce');
1089
1090 if (!current_user_can('manage_options')) {
1091 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1092 }
1093
1094 $client_id = isset($_POST['client_id']) ? intval($_POST['client_id']) : 0;
1095
1096 if ($client_id <= 0) {
1097 $this->sendError(__('Invalid client ID', 'easy-invoice'));
1098 }
1099
1100 // Prepare client data
1101 $client_data = [
1102 ClientFields::BUSINESS_CLIENT_NAME => sanitize_text_field($_POST['business_client_name']),
1103 ClientFields::EMAIL => sanitize_email($_POST['email']),
1104 ClientFields::USERNAME => sanitize_user($_POST['username']),
1105 ClientFields::PASSWORD => $_POST['password'], // Keep password as is, don't sanitize
1106 ClientFields::ADDRESS => sanitize_textarea_field($_POST['address']),
1107 ClientFields::PHONE => isset($_POST['phone']) ? sanitize_text_field($_POST['phone']) : '',
1108 ClientFields::EXTRA_INFO => sanitize_textarea_field($_POST['extra_info']),
1109 ClientFields::FIRST_NAME => sanitize_text_field($_POST['first_name']),
1110 ClientFields::LAST_NAME => sanitize_text_field($_POST['last_name']),
1111 ClientFields::WEBSITE => esc_url_raw($_POST['website'])
1112 ];
1113
1114 // Remove empty values except password (password can be empty for updates)
1115 $client_data = array_filter($client_data, function($value, $key) {
1116 if ($key === ClientFields::PASSWORD) {
1117 return true; // Always include password field
1118 }
1119 return $value !== '';
1120 }, ARRAY_FILTER_USE_BOTH);
1121
1122 if (empty($client_data)) {
1123 $this->sendError(__('No data provided to update.', 'easy-invoice'));
1124 }
1125
1126 $repository = ClientServiceProvider::getClientRepository();
1127
1128 // Update existing client
1129 $client = $repository->update($client_id, $client_data);
1130
1131 if (!$client) {
1132 $this->sendError(__('Failed to update client', 'easy-invoice'));
1133 }
1134
1135 $this->sendSuccess(array(
1136 'message' => __('Client updated successfully', 'easy-invoice'),
1137 'client_id' => $client_id,
1138 'client' => $client->toArray(),
1139 ));
1140 }
1141
1142 /**
1143 * Update invoices data with missing client information and totals
1144 */
1145 public function updateInvoicesData() {
1146 $this->verifyNonce('easy_invoice_admin_nonce');
1147
1148 if (!current_user_can('manage_options')) {
1149 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1150 }
1151
1152 $repository = InvoiceServiceProvider::getInvoiceRepository();
1153 $client_repository = ClientServiceProvider::getClientRepository();
1154
1155 // Get all invoices
1156 $invoices = $repository->all();
1157 $updated_count = 0;
1158
1159 foreach ($invoices as $invoice) {
1160 $updated = false;
1161
1162 // Check if client data is missing
1163 $client_id = $invoice->getClientId();
1164 if ($client_id > 0) {
1165 $client = $client_repository->find($client_id);
1166 if ($client) {
1167 // Update customer name if missing
1168 $customer_name = $invoice->getCustomerName();
1169 if (empty($customer_name)) {
1170 $customer_name = $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName());
1171 $invoice->setCustomerName($customer_name);
1172 $updated = true;
1173 }
1174
1175 // Update customer email if missing
1176 $customer_email = $invoice->getCustomerEmail();
1177 if (empty($customer_email)) {
1178 $customer_email = $client->getEmail();
1179 $invoice->setCustomerEmail($customer_email);
1180 $updated = true;
1181 }
1182
1183 // Update customer address if missing
1184 $customer_address = $invoice->getCustomerAddress();
1185 if (empty($customer_address)) {
1186 $customer_address = $client->getAddress();
1187 $invoice->setCustomerAddress($customer_address);
1188 $updated = true;
1189 }
1190 }
1191 }
1192
1193 // Check if total is missing or zero
1194 $total = $invoice->getTotal();
1195 if (empty($total) || $total == 0) {
1196 // Recalculate total from items
1197 $items = $invoice->getItems();
1198 if (!empty($items)) {
1199 $subtotal = 0;
1200 foreach ($items as $item) {
1201 if (method_exists($item, 'getAmount')) {
1202 $subtotal += $item->getAmount();
1203 } elseif (isset($item['amount'])) {
1204 $subtotal += $item['amount'];
1205 }
1206 }
1207
1208 // Calculate discount and tax
1209 $discount = $invoice->getDiscountAmount();
1210 $tax = $invoice->getTaxAmount();
1211
1212 $total = $subtotal - $discount + $tax;
1213
1214 // Save the calculated total
1215 $invoice->setMeta('_easy_invoice_total', $total);
1216 $updated = true;
1217 }
1218 }
1219
1220 if ($updated) {
1221 $updated_count++;
1222 }
1223 }
1224
1225 $this->sendSuccess(array(
1226 'message' => sprintf(__('Updated %d invoices with missing data', 'easy-invoice'), $updated_count),
1227 'updated_count' => $updated_count
1228 ));
1229 }
1230
1231 /**
1232 * Download invoice as PDF (public access)
1233 */
1234 public function downloadInvoicePdf() {
1235 // Verify nonce
1236 $this->verifyNonce('easy_invoice_nonce');
1237
1238 // Get invoice ID
1239 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1240
1241 if (!$invoice_id) {
1242 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
1243 }
1244
1245 // Get invoice from repository (only published invoices for public access)
1246 $repository = InvoiceServiceProvider::getInvoiceRepository();
1247
1248 // For admins, allow access to any invoice status
1249 if (current_user_can('manage_options')) {
1250 $invoice = $repository->find($invoice_id);
1251 } else {
1252 // For non-admins, only allow access to published invoices
1253 $invoice = $repository->findPublished($invoice_id);
1254 }
1255
1256 if (!$invoice) {
1257 $this->sendError(__('Invoice not found', 'easy-invoice'));
1258 }
1259
1260 // Get invoice data for PDF generation
1261 $invoice_data = \EasyInvoice\Includes\Helpers\PdfHelper::getInvoiceDataForPdf($invoice);
1262
1263 // For now, return success response with invoice data
1264 // PDF generation can be implemented later with actual PDF creation
1265 $this->sendSuccess(array(
1266 'message' => __('Invoice data retrieved successfully', 'easy-invoice'),
1267 'invoice_data' => $invoice_data,
1268 'download_url' => add_query_arg(array(
1269 'action' => 'easy_invoice_generate_pdf',
1270 'invoice_id' => $invoice_id,
1271 'nonce' => wp_create_nonce('generate_pdf')
1272 ), admin_url('admin-ajax.php'))
1273 ));
1274 }
1275
1276 /**
1277 * Send invoice via email (public access)
1278 */
1279 public function sendInvoiceEmailPublic() {
1280 $this->verifyNonce('easy_invoice_send_invoice_email');
1281
1282 // Get invoice ID
1283 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
1284
1285 if (!$invoice_id) {
1286 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
1287 }
1288
1289 // Get invoice from repository (only published invoices for public access)
1290 $repository = InvoiceServiceProvider::getInvoiceRepository();
1291
1292 // For admins, allow access to any invoice status
1293 if (current_user_can('manage_options')) {
1294 $invoice = $repository->find($invoice_id);
1295 } else {
1296 // For non-admins, only allow access to published invoices
1297 $invoice = $repository->findPublished($invoice_id);
1298 }
1299
1300 if (!$invoice) {
1301 $this->sendError(__('Invoice not found', 'easy-invoice'));
1302 }
1303
1304 // Use EmailManager to send the email
1305 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1306 $result = $email_manager->sendInvoiceEmail($invoice, 'new');
1307
1308 if ($result['success']) {
1309 $this->sendSuccess(array(
1310 'message' => $result['message']
1311 ));
1312 } else {
1313 $this->sendError($result['message']);
1314 }
1315 }
1316
1317 /**
1318 * Send quote via email (admin + public; guests only for published quotes).
1319 */
1320 public function sendQuoteEmailPublic() {
1321 $this->verifyNonce('easy_invoice_send_quote_email');
1322
1323 $quote_id = isset($_POST['quote_id']) ? intval($_POST['quote_id']) : 0;
1324
1325 if (!$quote_id) {
1326 $this->sendError(__('Invalid quote ID', 'easy-invoice'));
1327 }
1328
1329 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
1330
1331 if (current_user_can('manage_options')) {
1332 $quote = $repository->find($quote_id);
1333 } else {
1334 $quote = $repository->findPublished($quote_id);
1335 }
1336
1337 if (!$quote) {
1338 $this->sendError(__('Quote not found', 'easy-invoice'));
1339 }
1340
1341 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1342 $result = $email_manager->sendQuoteEmail($quote, 'new');
1343
1344 if ($result['success']) {
1345 $quote_log_service = new \EasyInvoice\Services\QuoteLogService();
1346 $quote_log_service->logSent($quote_id, $quote->getCustomerEmail());
1347
1348 $this->sendSuccess(array(
1349 'message' => $result['message'],
1350 ));
1351 } else {
1352 $this->sendError($result['message']);
1353 }
1354 }
1355
1356 /**
1357 * Generate invoice PDF
1358 */
1359 public function generateInvoicePdf() {
1360 // Verify nonce
1361 $this->verifyNonce('generate_pdf');
1362
1363 // Get invoice ID
1364 $invoice_id = isset($_REQUEST['invoice_id']) ? intval($_REQUEST['invoice_id']) : 0;
1365
1366 if (!$invoice_id) {
1367 $this->sendError(__('Invalid invoice ID', 'easy-invoice'));
1368 }
1369
1370 // Get invoice from repository
1371 $repository = InvoiceServiceProvider::getInvoiceRepository();
1372
1373 // For admins, allow access to any invoice status
1374 if (current_user_can('manage_options')) {
1375 $invoice = $repository->find($invoice_id);
1376 } else {
1377 // For non-admins, only allow access to published invoices
1378 $invoice = $repository->findPublished($invoice_id);
1379 }
1380
1381 if (!$invoice) {
1382 $this->sendError(__('Invoice not found', 'easy-invoice'));
1383 }
1384
1385 // Redirect to the invoice single page with PDF generation
1386 $invoice_url = get_permalink($invoice_id);
1387 if ($invoice_url) {
1388 wp_redirect(add_query_arg('auto_download_pdf', '1', $invoice_url));
1389 exit;
1390 } else {
1391 $this->sendError(__('Could not generate invoice URL', 'easy-invoice'));
1392 }
1393 }
1394
1395 /**
1396 * Generate quote PDF
1397 */
1398 public function generateQuotePdf() {
1399 // Verify nonce
1400 $this->verifyNonce('generate_quote_pdf');
1401
1402 // Get quote ID
1403 $quote_id = isset($_REQUEST['quote_id']) ? intval($_REQUEST['quote_id']) : 0;
1404
1405 if (!$quote_id) {
1406 $this->sendError(__('Invalid quote ID', 'easy-invoice'));
1407 }
1408
1409 // Get quote from repository — mirror invoice PDF: only published quotes for non-admins (incl. nopriv).
1410 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
1411 if (current_user_can('manage_options')) {
1412 $quote = $repository->find($quote_id);
1413 } else {
1414 $quote = $repository->findPublished($quote_id);
1415 }
1416
1417 if (!$quote) {
1418 $this->sendError(__('Quote not found', 'easy-invoice'));
1419 }
1420
1421 // Redirect to the quote single page with PDF generation
1422 $quote_url = get_permalink($quote_id);
1423 if ($quote_url) {
1424 wp_redirect(add_query_arg('auto_download_pdf', '1', $quote_url));
1425 exit;
1426 } else {
1427 $this->sendError(__('Could not generate quote URL', 'easy-invoice'));
1428 }
1429 }
1430
1431 /**
1432 * Search clients for the dropdown
1433 */
1434 public function searchClients() {
1435 $this->verifyNonce('easy_invoice_nonce');
1436
1437 if (!current_user_can('manage_options')) {
1438 $this->sendError(__('You do not have permission to perform this action', 'easy-invoice'));
1439 }
1440
1441 $query = isset($_POST['query']) ? sanitize_text_field($_POST['query']) : '';
1442
1443 // Get client repository
1444 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1445
1446 // Search clients
1447 $clients = $client_repository->search($query);
1448
1449 if (empty($clients)) {
1450 $this->sendSuccess(array());
1451 }
1452
1453 // Format clients for dropdown
1454 $formatted_clients = array();
1455 foreach ($clients as $client) {
1456 // Get the WordPress user data directly
1457 $user = get_user_by('id', $client->getId());
1458 if (!$user) {
1459 continue;
1460 }
1461
1462 // Use Client model properties first, fallback to WordPress user fields
1463 $business_name = $client->business_client_name ?: '';
1464 $first_name = $client->first_name ?: $user->first_name ?: '';
1465 $last_name = $client->last_name ?: $user->last_name ?: '';
1466 $email = $client->email ?: $user->user_email ?: '';
1467
1468
1469
1470 // Create display name
1471 $client_name = $business_name ?: ($first_name . ' ' . $last_name);
1472 if (empty(trim($client_name))) {
1473 $client_name = $user->display_name ?: 'User ' . $client->getId();
1474 }
1475
1476 // Include all clients, even those with empty emails
1477 $formatted_clients[] = array(
1478 'id' => $client->getId(),
1479 'name' => $client_name,
1480 'email' => $email,
1481 'company' => $business_name,
1482 'phone' => $client->phone ?: '',
1483 'address' => $client->address ?: '',
1484 'website' => $client->website ?: '',
1485 'display_name' => $client_name . ' (' . $email . ')'
1486 );
1487 }
1488
1489 $this->sendSuccess($formatted_clients);
1490 }
1491
1492 /**
1493 * Save additional CSS for invoice/quote
1494 */
1495 public function saveAdditionalCSS() {
1496 // Verify nonce
1497 if (!wp_verify_nonce($_POST['nonce'], 'save_additional_css_nonce')) {
1498 $this->sendError('Security check failed');
1499 return;
1500 }
1501
1502 // Check user capabilities - require administrator
1503 if (!current_user_can('manage_options')) {
1504 $this->sendError('You do not have permission to perform this action');
1505 return;
1506 }
1507
1508 // Validate and sanitize post ID
1509 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1510 if ($post_id <= 0) {
1511 $this->sendError('Invalid post ID');
1512 return;
1513 }
1514
1515 // Verify post exists and user can edit it
1516 $post = get_post($post_id);
1517 if (!$post || !current_user_can('edit_post', $post_id)) {
1518 $this->sendError('You cannot edit this post');
1519 return;
1520 }
1521
1522 // Verify post type is invoice or quote
1523 $valid_post_types = [
1524 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
1525 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
1526 ];
1527 if (!in_array($post->post_type, $valid_post_types)) {
1528 $this->sendError('Invalid post type');
1529 return;
1530 }
1531
1532 // Get and sanitize CSS content
1533 $css = isset($_POST['css']) ? $_POST['css'] : '';
1534
1535 // Enhanced CSS sanitization
1536 $css = $this->sanitizeCSS($css);
1537
1538 // Limit CSS length to prevent abuse
1539 if (strlen($css) > 50000) { // 50KB limit
1540 $this->sendError('CSS content too long');
1541 return;
1542 }
1543
1544 // Save CSS to post meta
1545 $result = update_post_meta($post_id, '_easy_invoice_additional_css', $css);
1546
1547 if ($result !== false) {
1548 $this->sendSuccess(array(
1549 'message' => 'CSS saved successfully',
1550 'css' => $css,
1551 'post_id' => $post_id
1552 ));
1553 } else {
1554 $this->sendError('Failed to save CSS');
1555 }
1556 }
1557
1558 /**
1559 * Enhanced CSS sanitization - preserves valid CSS while removing threats
1560 */
1561 private function sanitizeCSS($css) {
1562 // Remove PHP tags first
1563 $css = preg_replace('/<\?php.*?\?>/is', '', $css);
1564
1565 // Remove HTML tags (script, iframe, object, embed)
1566 $css = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $css);
1567 $css = preg_replace('/<iframe[^>]*>.*?<\/iframe>/is', '', $css);
1568 $css = preg_replace('/<object[^>]*>.*?<\/object>/is', '', $css);
1569 $css = preg_replace('/<embed[^>]*>/is', '', $css);
1570
1571 // Remove dangerous CSS constructs
1572 $css = preg_replace('/expression\s*\(/i', '', $css); // CSS expressions
1573 $css = preg_replace('/javascript\s*:/i', '', $css); // JavaScript protocol
1574 $css = preg_replace('/@import\s+url\s*\(/i', '', $css); // @import url()
1575 $css = preg_replace('/@import\s+["\'][^"\']+["\']/', '', $css); // @import with quotes
1576 $css = preg_replace('/behavior\s*:\s*url\s*\(/i', '', $css); // IE behavior
1577 $css = preg_replace('/binding\s*:/i', '', $css); // XBL binding
1578
1579 // Remove dangerous CSS functions (but keep safe ones)
1580 $dangerous_functions = ['eval', 'exec', 'system', 'passthru', 'shell_exec', 'phpinfo', 'file_get_contents', 'file_put_contents', 'fopen', 'fwrite', 'curl_exec'];
1581 foreach ($dangerous_functions as $func) {
1582 $css = preg_replace('/\b' . preg_quote($func, '/') . '\s*\(/i', '', $css);
1583 }
1584
1585 // Remove data URLs that could contain malicious content
1586 $css = preg_replace('/data\s*:\s*["\'][^"\']*["\']/i', '', $css);
1587
1588 // Remove vbscript: protocol
1589 $css = preg_replace('/vbscript\s*:/i', '', $css);
1590
1591 // Remove any remaining HTML-like constructs
1592 $css = htmlspecialchars_decode($css, ENT_QUOTES);
1593
1594 // Basic cleanup - remove excessive whitespace but preserve CSS structure
1595 $css = preg_replace('/\s+/', ' ', $css);
1596 $css = preg_replace('/;\s*}/', '}', $css);
1597 $css = preg_replace('/\s*{\s*/', ' {', $css);
1598 $css = preg_replace('/;\s*;/', ';', $css);
1599
1600 return trim($css);
1601 }
1602 }
1603