PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.18
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.18
2.4.3 2.4.4 2.4.2 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 All 60 releases
easy-invoice / includes / Admin / EasyInvoiceAjax.php

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

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