Changeset 3330741
- Timestamp:
- 07/19/2025 10:20:01 PM (7 months ago)
- Location:
- nexlifydesk/trunk
- Files:
-
- 15 edited
-
assets/css/nexlifydesk-admin.css (modified) (7 diffs)
-
assets/js/nexlifydesk.js (modified) (9 diffs)
-
includes/class-nexlifydesk-admin.php (modified) (10 diffs)
-
includes/class-nexlifydesk-ajax.php (modified) (8 diffs)
-
includes/class-nexlifydesk-database.php (modified) (8 diffs)
-
includes/class-nexlifydesk-tickets.php (modified) (35 diffs)
-
includes/nexlifydesk-functions.php (modified) (2 diffs)
-
nexlifydesk.php (modified) (6 diffs)
-
readme.txt (modified) (6 diffs)
-
templates/admin/settings.php (modified) (3 diffs)
-
templates/admin/ticket-single.php (modified) (9 diffs)
-
templates/admin/tickets-list.php (modified) (3 diffs)
-
templates/frontend/partials/single-reply.php (modified) (3 diffs)
-
templates/frontend/ticket-form.php (modified) (1 diff)
-
templates/frontend/ticket-single.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
nexlifydesk/trunk/assets/css/nexlifydesk-admin.css
r3326104 r3330741 210 210 .nexlifydesk-admin-ticket-list-ui { 211 211 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 212 max-width: 1200px; 212 max-width: 1440px; 213 border-radius: 10px; 214 background-color: white; 215 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 213 216 margin: 0 auto; 214 217 padding: 20px; … … 334 337 } 335 338 336 .nexlifydesk-admin-ticket-list-ui .ticket-grid { 339 /* Gmail-style ticket list */ 340 .nexlifydesk-admin-ticket-list-ui .bulk-actions { 341 display: flex; 342 align-items: center; 343 gap: 10px; 344 margin-right: auto; 345 } 346 347 .nexlifydesk-admin-ticket-list-ui .bulk-action-dropdown { 348 padding: 8px 12px; 349 border: 1px solid #e2e8f0; 350 border-radius: 6px; 351 background: white; 352 font-size: 14px; 353 } 354 355 .nexlifydesk-admin-ticket-list-ui .ticket-list { 356 background: white; 357 border-radius: 8px; 358 border: 1px solid #e2e8f0; 359 overflow: hidden; 360 } 361 362 .nexlifydesk-admin-ticket-list-ui .ticket-list-header { 337 363 display: grid; 338 gap: 1.5rem; 339 grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); 340 } 341 342 .nexlifydesk-admin-ticket-list-ui .loading-tickets { 343 grid-column: 1 / -1; 364 grid-template-columns: 40px 3fr 2fr 80px 80px 120px 100px; 365 gap: 12px; 366 padding: 12px 16px; 367 background: #f8fafc; 368 border-bottom: 1px solid #e2e8f0; 369 font-size: 12px; 370 font-weight: 600; 371 color: #64748b; 372 text-transform: uppercase; 373 letter-spacing: 0.05em; 374 } 375 376 .nexlifydesk-admin-ticket-list-ui .ticket-row { 377 display: grid; 378 grid-template-columns: 40px 3fr 2fr 80px 80px 120px 100px; 379 gap: 12px; 380 padding: 16px; 381 border-bottom: 1px solid #f1f5f9; 382 transition: background-color 0.2s ease; 383 cursor: pointer; 384 } 385 386 .nexlifydesk-admin-ticket-list-ui .ticket-row.unread { 387 background-color: #f8fafc; 388 border-left: 4px solid #3b82f6; 389 } 390 391 @media (max-width: 1200px) { 392 .nexlifydesk-admin-ticket-list-ui .ticket-list-header, 393 .nexlifydesk-admin-ticket-list-ui .ticket-row { 394 grid-template-columns: 40px 4fr 2fr 70px 70px 100px 90px; 395 gap: 8px; 396 } 397 } 398 399 @media (max-width: 992px) { 400 .nexlifydesk-admin-ticket-list-ui .ticket-list-header, 401 .nexlifydesk-admin-ticket-list-ui .ticket-row { 402 grid-template-columns: 40px 5fr 2fr 60px 60px 80px 80px; 403 gap: 6px; 404 } 405 } 406 407 .nexlifydesk-admin-ticket-list-ui .ticket-row:hover { 408 background-color: #f8fafc; 409 } 410 411 .nexlifydesk-admin-ticket-list-ui .ticket-row:last-child { 412 border-bottom: none; 413 } 414 415 .nexlifydesk-admin-ticket-list-ui .row-checkbox, 416 .nexlifydesk-admin-ticket-list-ui .header-checkbox { 417 display: flex; 418 align-items: center; 419 justify-content: center; 420 } 421 422 .nexlifydesk-admin-ticket-list-ui .row-subject { 423 display: flex; 424 flex-direction: column; 425 gap: 4px; 426 min-width: 0; 427 } 428 429 .nexlifydesk-admin-ticket-list-ui .ticket-link { 430 text-decoration: none; 431 color: inherit; 432 display: flex; 433 align-items: center; 434 gap: 8px; 435 min-width: 0; 436 } 437 438 .nexlifydesk-admin-ticket-list-ui .ticket-id { 439 font-size: 12px; 440 color: #667eea; 441 font-weight: 600; 442 flex-shrink: 0; 443 } 444 445 .nexlifydesk-admin-ticket-list-ui .ticket-title { 446 font-size: 14px; 447 font-weight: 600; 448 color: #1e293b; 449 overflow: hidden; 450 text-overflow: ellipsis; 451 white-space: nowrap; 452 min-width: 0; 453 } 454 455 .nexlifydesk-admin-ticket-list-ui .ticket-title.unread-title { 456 font-weight: 700; 457 color: #1e40af; 458 } 459 460 .nexlifydesk-admin-ticket-list-ui .unread-dot { 461 display: inline-block; 462 width: 8px; 463 height: 8px; 464 background-color: #3b82f6; 465 border-radius: 50%; 466 margin-right: 8px; 467 flex-shrink: 0; 468 animation: pulse 2s infinite; 469 } 470 471 @keyframes pulse { 472 0% { 473 opacity: 1; 474 } 475 50% { 476 opacity: 0.5; 477 } 478 100% { 479 opacity: 1; 480 } 481 } 482 483 .nexlifydesk-admin-ticket-list-ui .ticket-preview { 484 font-size: 12px; 485 color: #64748b; 486 line-height: 1.4; 487 overflow: hidden; 488 text-overflow: ellipsis; 489 white-space: nowrap; 490 } 491 492 .nexlifydesk-admin-ticket-list-ui .row-customer { 493 display: flex; 494 flex-direction: column; 495 gap: 2px; 496 min-width: 0; 497 } 498 499 .nexlifydesk-admin-ticket-list-ui .customer-name { 500 font-size: 14px; 501 font-weight: 500; 502 color: #1e293b; 503 overflow: hidden; 504 text-overflow: ellipsis; 505 white-space: nowrap; 506 } 507 508 .nexlifydesk-admin-ticket-list-ui .customer-email { 509 font-size: 12px; 510 color: #64748b; 511 overflow: hidden; 512 text-overflow: ellipsis; 513 white-space: nowrap; 514 } 515 516 .nexlifydesk-admin-ticket-list-ui .status-badge { 517 padding: 4px 8px; 518 border-radius: 12px; 519 font-size: 11px; 520 font-weight: 600; 521 text-transform: uppercase; 522 letter-spacing: 0.05em; 523 display: inline-block; 524 } 525 526 .nexlifydesk-admin-ticket-list-ui .status-open { 527 background: #dbeafe; 528 color: #1e40af; 529 } 530 531 .nexlifydesk-admin-ticket-list-ui .status-in_progress, 532 .nexlifydesk-admin-ticket-list-ui .status-in-progress { 533 background: #fef3c7; 534 color: #92400e; 535 } 536 537 .nexlifydesk-admin-ticket-list-ui .status-resolved { 538 background: #d1fae5; 539 color: #065f46; 540 } 541 542 .nexlifydesk-admin-ticket-list-ui .status-closed { 543 background: #f3f4f6; 544 color: #374151; 545 } 546 547 .nexlifydesk-admin-ticket-list-ui .priority-badge { 548 padding: 4px 8px; 549 border-radius: 12px; 550 font-size: 11px; 551 font-weight: 600; 552 text-transform: uppercase; 553 letter-spacing: 0.05em; 554 display: inline-block; 555 } 556 557 .nexlifydesk-admin-ticket-list-ui .priority-high { 558 background: #fee2e2; 559 color: #dc2626; 560 } 561 562 .nexlifydesk-admin-ticket-list-ui .priority-medium { 563 background: #fef3c7; 564 color: #d97706; 565 } 566 567 .nexlifydesk-admin-ticket-list-ui .priority-low { 568 background: #f0f9ff; 569 color: #0369a1; 570 } 571 572 .nexlifydesk-admin-ticket-list-ui .assignee-info { 573 display: flex; 574 align-items: center; 575 gap: 8px; 576 } 577 578 .nexlifydesk-admin-ticket-list-ui .assignee-info .avatar { 579 width: 24px; 580 height: 24px; 581 border-radius: 50%; 582 background: linear-gradient(135deg, #0f2027, #203a43, #2c5364); 583 display: flex; 584 align-items: center; 585 justify-content: center; 586 color: white; 587 font-size: 10px; 588 font-weight: 600; 589 } 590 591 .nexlifydesk-admin-ticket-list-ui .assignee-info span { 592 font-size: 12px; 593 color: #1e293b; 594 } 595 596 .nexlifydesk-admin-ticket-list-ui .unassigned { 597 font-size: 12px; 598 color: #94a3b8; 599 font-style: italic; 600 } 601 602 .nexlifydesk-admin-ticket-list-ui .row-date { 603 display: flex; 604 flex-direction: column; 605 gap: 2px; 606 } 607 608 .nexlifydesk-admin-ticket-list-ui .date-time { 609 font-size: 12px; 610 color: #1e293b; 611 font-weight: 500; 612 } 613 614 .nexlifydesk-admin-ticket-list-ui .time-ago { 615 font-size: 11px; 616 color: #64748b; 617 } 618 619 .nexlifydesk-admin-ticket-list-ui .no-tickets { 344 620 text-align: center; 345 621 padding: 2rem; … … 348 624 } 349 625 626 .nexlifydesk-admin-ticket-list-ui .loading-tickets { 627 text-align: center; 628 padding: 2rem; 629 font-size: 16px; 630 color: #64748b; 631 } 632 350 633 .nexlifydesk-admin-ticket-list-ui .loading-tickets .spinner { 351 634 float: none; … … 353 636 } 354 637 355 .nexlifydesk-admin-ticket-list-ui .ticket-card { 638 /* Real-time updates - smooth transitions */ 639 .nexlifydesk-admin-ticket-list-ui .ticket-row { 640 transition: all 0.3s ease; 641 } 642 643 .nexlifydesk-admin-ticket-list-ui .ticket-row.updating { 644 opacity: 0.7; 645 } 646 647 .nexlifydesk-admin-ticket-list-ui .ticket-row.new-reply { 648 animation: highlight 3s ease-out; 649 } 650 651 @keyframes highlight { 652 0% { 653 background-color: #dbeafe; 654 border-left-color: #3b82f6; 655 } 656 100% { 657 background-color: inherit; 658 border-left-color: inherit; 659 } 660 } 661 662 /* Bulk action modals */ 663 .bulk-modal { 664 position: fixed; 665 top: 0; 666 left: 0; 667 width: 100%; 668 height: 100%; 669 background: rgba(0, 0, 0, 0.5); 670 display: flex; 671 align-items: center; 672 justify-content: center; 673 z-index: 10000; 674 } 675 676 .bulk-modal .modal-content { 356 677 background: white; 357 border-radius: 12px; 358 padding: 1.5rem; 359 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 678 padding: 24px; 679 border-radius: 8px; 680 min-width: 400px; 681 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 682 } 683 684 .bulk-modal h3 { 685 margin: 0 0 16px 0; 686 font-size: 18px; 687 color: #1e293b; 688 } 689 690 .bulk-modal select { 691 width: 100%; 692 padding: 8px 12px; 360 693 border: 1px solid #e2e8f0; 361 transition: all 0.3s ease; 362 cursor: pointer; 363 } 364 365 .nexlifydesk-admin-ticket-list-ui .ticket-card:hover { 366 transform: translateY(-2px); 367 box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); 368 border-color: #667eea; 369 } 370 371 .nexlifydesk-admin-ticket-list-ui .ticket-header { 372 display: flex; 373 justify-content: space-between; 374 align-items: flex-start; 375 margin-bottom: 1rem; 376 } 377 378 .nexlifydesk-admin-ticket-list-ui .ticket-id { 379 font-size: 0.875rem; 380 color: #667eea; 381 font-weight: 600; 382 } 383 384 .nexlifydesk-admin-ticket-list-ui .ticket-title { 385 font-size: 1.125rem; 386 font-weight: 600; 387 color: #1e293b; 388 margin: 0.5rem 0; 389 line-height: 1.4; 390 } 391 392 .nexlifydesk-admin-ticket-list-ui .ticket-description { 393 color: #64748b; 394 margin-bottom: 1rem; 395 line-height: 1.5; 396 } 397 398 .nexlifydesk-admin-ticket-list-ui .ticket-meta { 399 display: flex; 400 flex-wrap: wrap; 401 gap: 0.75rem; 402 align-items: center; 403 margin-bottom: 1rem; 404 } 405 406 .nexlifydesk-admin-ticket-list-ui .status-badge { 407 padding: 0.25rem 0.75rem; 408 border-radius: 20px; 409 font-size: 0.75rem; 410 font-weight: 600; 411 text-transform: uppercase; 412 letter-spacing: 0.05em; 413 } 414 415 .nexlifydesk-admin-ticket-list-ui .status-open { 416 background: #dbeafe; 417 color: #1e40af; 418 } 419 420 .nexlifydesk-admin-ticket-list-ui .status-in_progress, 421 .nexlifydesk-admin-ticket-list-ui .status-in-progress { 422 background: #fef3c7; 423 color: #92400e; 424 } 425 426 .nexlifydesk-admin-ticket-list-ui .status-resolved { 427 background: #d1fae5; 428 color: #065f46; 429 } 430 431 .nexlifydesk-admin-ticket-list-ui .status-closed { 432 background: #f3f4f6; 433 color: #374151; 434 } 435 436 .nexlifydesk-admin-ticket-list-ui .priority-badge { 437 padding: 0.25rem 0.75rem; 438 border-radius: 20px; 439 font-size: 0.75rem; 440 font-weight: 600; 441 text-transform: uppercase; 442 letter-spacing: 0.05em; 443 } 444 445 .nexlifydesk-admin-ticket-list-ui .priority-high { 446 background: #fee2e2; 447 color: #dc2626; 448 } 449 450 .nexlifydesk-admin-ticket-list-ui .priority-medium { 451 background: #fef3c7; 452 color: #d97706; 453 } 454 455 .nexlifydesk-admin-ticket-list-ui .priority-low { 456 background: #f0f9ff; 457 color: #0369a1; 458 } 459 460 .nexlifydesk-admin-ticket-list-ui .ticket-footer { 461 display: flex; 462 justify-content: space-between; 463 align-items: center; 464 padding-top: 1rem; 465 border-top: 1px solid #f1f5f9; 466 font-size: 0.875rem; 467 color: #64748b; 468 } 469 470 .nexlifydesk-admin-ticket-list-ui .assignee { 471 display: flex; 472 align-items: center; 473 gap: 0.5rem; 474 } 475 476 .nexlifydesk-admin-ticket-list-ui .avatar { 477 width: 24px; 478 height: 24px; 479 border-radius: 50%; 480 background: linear-gradient(135deg, #0f2027, #203a43, #2c5364); 481 display: flex; 482 align-items: center; 483 justify-content: center; 484 color: white; 485 font-size: 0.75rem; 486 font-weight: 600; 694 border-radius: 6px; 695 margin-bottom: 16px; 696 } 697 698 .bulk-modal .modal-actions { 699 display: flex; 700 gap: 8px; 701 justify-content: flex-end; 702 } 703 704 /* Delete modal specific styles */ 705 .bulk-modal .selected-tickets-info { 706 background-color: #f8f9fa; 707 padding: 8px 12px; 708 border-radius: 4px; 709 font-size: 14px; 710 color: #555; 711 margin: 12px 0; 712 } 713 714 .bulk-modal .delete-option { 715 color: #d63638; 487 716 } 488 717 … … 499 728 flex-direction: column; 500 729 align-items: stretch; 730 gap: 12px; 731 } 732 733 .nexlifydesk-admin-ticket-list-ui .bulk-actions { 734 order: 2; 501 735 } 502 736 503 737 .nexlifydesk-admin-ticket-list-ui .search-bar { 504 738 min-width: unset; 505 } 506 507 .nexlifydesk-admin-ticket-list-ui .ticket-grid { 739 order: 1; 740 } 741 742 .nexlifydesk-admin-ticket-list-ui .ticket-list-header { 743 display: none; 744 } 745 746 .nexlifydesk-admin-ticket-list-ui .ticket-row { 508 747 grid-template-columns: 1fr; 748 gap: 8px; 749 padding: 12px; 750 } 751 752 .nexlifydesk-admin-ticket-list-ui .row-checkbox { 753 position: absolute; 754 top: 12px; 755 right: 12px; 509 756 } 510 757 511 758 .nexlifydesk-admin-ticket-list-ui .stats { 512 759 grid-template-columns: repeat(2, 1fr); 760 } 761 762 .bulk-modal .modal-content { 763 min-width: 90vw; 764 margin: 20px; 513 765 } 514 766 } … … 976 1228 } 977 1229 1230 /* Encryption Key Instructions Styling */ 1231 .nexlifydesk-encryption-key-instructions { 1232 background: #f8f9fa; 1233 border: 1px solid #e1e5e9; 1234 border-radius: 5px; 1235 padding: 20px; 1236 margin: 20px 0; 1237 } 1238 1239 .nexlifydesk-encryption-key-instructions h3 { 1240 margin-top: 0; 1241 color: #2c3e50; 1242 } 1243 1244 .nexlifydesk-encryption-key-instructions h4 { 1245 color: #34495e; 1246 margin-top: 20px; 1247 margin-bottom: 10px; 1248 } 1249 1250 .nexlifydesk-key-generation { 1251 background: #fff; 1252 border: 1px solid #ddd; 1253 border-radius: 3px; 1254 padding: 15px; 1255 margin: 15px 0; 1256 } 1257 1258 .nexlifydesk-key-installation { 1259 background: #fff; 1260 border: 1px solid #ddd; 1261 border-radius: 3px; 1262 padding: 15px; 1263 margin: 15px 0; 1264 } 1265 1266 .nexlifydesk-key-status { 1267 background: #fff; 1268 border: 1px solid #ddd; 1269 border-radius: 3px; 1270 padding: 15px; 1271 margin: 15px 0; 1272 } 1273 1274 .nexlifydesk-key-status p { 1275 margin: 5px 0; 1276 } 1277 1278 .nexlifydesk-key-status .dashicons { 1279 font-size: 16px; 1280 width: 16px; 1281 height: 16px; 1282 margin-right: 5px; 1283 } 1284 1285 #encryption-key-output { 1286 background: #f9f9f9; 1287 border: 1px solid #ccc; 1288 border-radius: 3px; 1289 padding: 10px; 1290 font-family: Consolas, Monaco, monospace; 1291 font-size: 12px; 1292 resize: vertical; 1293 } 1294 978 1295 /* Internal Notes */ 979 1296 .nexlifydesk-admin-single-ticket-ui .message.internal-note { … … 1018 1335 margin-bottom: 16px; 1019 1336 } 1337 1338 /* IMAP Authentication Spam Protection Styles */ 1339 .nexlifydesk-spam-protection { 1340 border-top: 1px solid #ccd0d4; 1341 padding-top: 20px; 1342 margin-top: 20px; 1343 } 1344 1345 .nexlifydesk-spam-protection h3 { 1346 color: #23282d; 1347 margin-bottom: 15px; 1348 } 1349 1350 .nexlifydesk-spam-protection .form-table th { 1351 width: 200px; 1352 } 1353 1354 .nexlifydesk-spam-protection textarea { 1355 font-family: 'Courier New', monospace; 1356 font-size: 12px; 1357 } 1358 1359 .nexlifydesk-spam-protection .description { 1360 color: #666; 1361 font-style: italic; 1362 } -
nexlifydesk/trunk/assets/js/nexlifydesk.js
r3326104 r3330741 72 72 } 73 73 $error.text(''); 74 75 var totalSize = 0; 74 76 for (var i = 0; i < files.length; i++) { 75 77 var file = files[i]; 76 78 var fileExt = file.name.split('.').pop().toLowerCase(); 79 totalSize += file.size; 80 77 81 if (file.size > maxSize) { 78 82 $error.text('File "' + file.name + '" is too large. Maximum size is ' + (maxSize / 1024 / 1024) + 'MB.'); … … 84 88 $fileInput.val(''); 85 89 return; 90 } 91 } 92 93 var maxTotalSize = maxSize * 10; 94 if (totalSize > maxTotalSize * 0.8) { 95 var totalSizeMB = (totalSize / 1024 / 1024).toFixed(1); 96 var maxTotalSizeMB = (maxTotalSize / 1024 / 1024).toFixed(1); 97 if (totalSize > maxTotalSize) { 98 $error.text('Total file size (' + totalSizeMB + 'MB) exceeds limit of ' + maxTotalSizeMB + 'MB. Please reduce file sizes.'); 99 $fileInput.val(''); 100 return; 101 } else { 102 $error.css('color', 'orange').text('Warning: Large file size (' + totalSizeMB + 'MB). Upload may take longer.'); 86 103 } 87 104 } … … 142 159 }, 143 160 error: function(xhr, status, error) { 161 var errorMsg = nexlifydesk_vars.error_message || 'An error occurred. Please try again.'; 162 163 if (xhr.status === 413) { 164 errorMsg = 'Request too large. Please reduce file sizes or number of files and try again.'; 165 } else if (xhr.status === 504 || xhr.status === 502) { 166 errorMsg = 'Server timeout. Please try again or contact support if the issue persists.'; 167 } else if (xhr.status === 500) { 168 errorMsg = 'Server error. Please try again or contact support if the issue persists.'; 169 } 170 144 171 $('#nexlifydesk-message') 145 172 .removeClass('success') 146 173 .addClass('error') 147 .text( nexlifydesk_vars.error_message || 'An error occurred. Please try again.')174 .text(errorMsg) 148 175 .show(); 149 176 }, … … 569 596 } else { 570 597 $tableContainer.html('<tr><td colspan="5">' + nexlifydesk_admin_vars.error_loading_tickets_text + ' ' + response.data + '</td></tr>'); 571 console.error('Error fetching admin tickets:', response.data);572 598 } 573 599 }, 574 600 error: function(xhr, status, error) { 575 601 $tableContainer.html('<tr><td colspan="5">' + nexlifydesk_admin_vars.ajax_error_loading_tickets_text + '</td></tr>'); 576 console.error('AJAX Error fetching admin tickets:', status, error, xhr.responseText);577 602 } 578 603 }); … … 618 643 619 644 var formHtml = '<tr class="edit-position-row"><td colspan="4">'; 620 formHtml += '<form class="nexlifydesk-edit-position-form" method="post" action="' + nexlifydesk_ admin_vars.admin_post_url + '">';645 formHtml += '<form class="nexlifydesk-edit-position-form" method="post" action="' + nexlifydesk_vars.admin_post_url + '">'; 621 646 formHtml += '<input type="hidden" name="action" value="nexlifydesk_save_agent_position">'; 622 647 formHtml += '<input type="hidden" name="edit_position" value="1">'; … … 687 712 } 688 713 689 function loadTickets(status, search) {690 if (!isPluginValid) return;691 692 $('#nexlifydesk-tickets-list').html('<p>' + nexlifydesk_admin_vars.loading_tickets_text + '</p>');693 694 $.ajax({695 url: nexlifydesk_admin_vars.ajaxurl,696 type: 'POST',697 data: {698 action: 'nexlifydesk_admin_get_tickets',699 status: status || 'all',700 search: search || '',701 nonce: nexlifydesk_admin_vars.nonce702 },703 success: function(response) {704 if (response.success && response.data) {705 displayTickets(response.data);706 } else {707 var errorMsg = response.data || nexlifydesk_vars.no_tickets_found_text;708 $('#nexlifydesk-tickets-list').html('<p>' + errorMsg + '</p>');709 }710 },711 error: function(xhr, status, error) {712 713 var errorMsg = nexlifydesk_admin_vars.ajax_error_loading_tickets_text;714 if (xhr.responseText) {715 try {716 var errorResponse = JSON.parse(xhr.responseText);717 if (errorResponse.data) {718 errorMsg += ' ' + errorResponse.data;719 }720 } catch (e) {721 errorMsg += ' ' + xhr.responseText.substring(0, 100);722 }723 }724 725 $('#nexlifydesk-tickets-list').html('<p style="color: red;">' + errorMsg + '</p>');726 }727 });728 }729 730 function displayTickets(tickets) {731 var texts = window.nexlifydesk_tickets_list_texts || {};732 733 if (!tickets || tickets.length === 0) {734 $('#nexlifydesk-tickets-list').html('<p>' + (texts.no_tickets_found || 'No tickets found.') + '</p>');735 return;736 }737 738 var html = '<table class="wp-list-table widefat fixed striped">';739 html += '<thead><tr>';740 html += '<th>' + (texts.ticket_id || 'Ticket ID') + '</th>';741 html += '<th>' + (texts.subject || 'Subject') + '</th>';742 html += '<th>' + (texts.status || 'Status') + '</th>';743 html += '<th>' + (texts.assigned_to || 'Assigned To') + '</th>';744 html += '<th>' + (texts.created || 'Created') + '</th>';745 html += '<th>' + (texts.actions || 'Actions') + '</th>';746 html += '</tr></thead><tbody>';747 748 $.each(tickets, function(index, ticket) {749 html += '<tr>';750 html += '<td>' + escapeHtml(ticket.ticket_id) + '</td>';751 html += '<td>' + nl2br_js(escapeHtml(ticket.subject)) + '</td>';752 html += '<td><span class="status-' + escapeHtml(ticket.status) + '">' + escapeHtml(ticket.status.charAt(0).toUpperCase() + ticket.status.slice(1)) + '</span></td>';753 html += '<td>' + escapeHtml(ticket.assigned_to_display_name) + '</td>';754 html += '<td>' + escapeHtml(ticket.created_at) + '</td>';755 html += '<td><a href="?page=nexlifydesk_tickets&ticket_id=' + ticket.id + '" class="button button-small">' + (texts.view || 'View') + '</a></td>';756 html += '</tr>';757 });758 759 html += '</tbody></table>';760 $('#nexlifydesk-tickets-list').html(html);761 }762 763 function nl2br_js(str) {764 if (!str) return '';765 return str.replace(/\n/g, '<br>');766 }767 768 function escapeHtml(text) {769 if (!text) return '';770 var map = {771 '&': '&',772 '<': '<',773 '>': '>',774 '"': '"',775 "'": '''776 };777 return text.toString().replace(/[&<>"']/g, function(m) { return map[m]; });778 }779 780 $(document).ready(function() {781 if ($('#nexlifydesk-tickets-list').length) {782 if (typeof loadTickets === 'function') {783 loadTickets();784 }785 786 $('#ticket-filter-btn').on('click', function() {787 var status = $('#ticket-status-filter').val();788 var search = $('#ticket-search').val();789 if (typeof loadTickets === 'function') {790 loadTickets(status, search);791 }792 });793 794 $('#ticket-search').on('keypress', function(e) {795 if (e.which == 13) {796 $('#ticket-filter-btn').click();797 }798 });799 }800 });801 714 })(jQuery); 802 715 … … 1111 1024 $(function() { 1112 1025 if ($('.nexlifydesk-admin-ticket-list-ui').length) { 1113 loadTickets();1114 1026 1115 1027 let searchTimeout; … … 1265 1177 button.prop('disabled', false).text('Purge Data'); 1266 1178 resultDiv.html('<span style="color: red;">Error purging data. Please try again.<br>JS: ' + error + '</span>'); 1267 console.error('[NexlifyDesk] AJAX error:', error, xhr);1268 1179 }); 1269 1180 } 1270 1181 }); 1182 } 1183 }); 1184 1185 jQuery(document).ready(function($) { 1186 var $clearRateLimitBtn = $('#nexlifydesk_clear_rate_limit'); 1187 var $checkRateLimitBtn = $('#nexlifydesk_check_rate_limit'); 1188 1189 if ($clearRateLimitBtn.length) { 1190 $clearRateLimitBtn.off('click').on('click', function() { 1191 var button = $(this); 1192 var resultDiv = $('#nexlifydesk_clear_rate_limit_result'); 1193 var emailInput = $('#nexlifydesk_clear_rate_limit_email'); 1194 var email = emailInput.val().trim(); 1195 1196 resultDiv.html(''); 1197 1198 if (!email || !isValidEmail(email)) { 1199 resultDiv.html('<span style="color: red;">Please enter a valid email address.</span>'); 1200 return; 1201 } 1202 1203 var ajaxurl = (typeof nexlifydesk_vars !== 'undefined' && nexlifydesk_vars.ajaxurl) ? nexlifydesk_vars.ajaxurl : (typeof ajaxurl !== 'undefined' ? ajaxurl : window.ajaxurl); 1204 var nonce = (typeof nexlifydesk_admin_vars !== 'undefined' && nexlifydesk_admin_vars.nonce) ? nexlifydesk_admin_vars.nonce : ''; 1205 1206 button.prop('disabled', true).text('Clearing...'); 1207 $.post(ajaxurl, { 1208 action: 'nexlifydesk_clear_rate_limit', 1209 email: email, 1210 nonce: nonce 1211 }, function(response) { 1212 button.prop('disabled', false).text('Clear Rate Limit'); 1213 if (response && response.success) { 1214 resultDiv.html('<span style="color: green;">' + response.data + '</span>'); 1215 emailInput.val(''); 1216 } else { 1217 resultDiv.html('<span style="color: red;">' + (response && response.data ? response.data : 'Error clearing rate limit. Please try again.') + '</span>'); 1218 } 1219 }).fail(function(xhr, status, error) { 1220 button.prop('disabled', false).text('Clear Rate Limit'); 1221 resultDiv.html('<span style="color: red;">Error clearing rate limit. Please try again.</span>'); 1222 }); 1223 }); 1224 } 1225 1226 if ($checkRateLimitBtn.length) { 1227 $checkRateLimitBtn.off('click').on('click', function() { 1228 var button = $(this); 1229 var resultDiv = $('#nexlifydesk_check_rate_limit_result'); 1230 var emailInput = $('#nexlifydesk_check_rate_limit_email'); 1231 var email = emailInput.val().trim(); 1232 1233 resultDiv.html(''); 1234 1235 if (!email || !isValidEmail(email)) { 1236 resultDiv.html('<span style="color: red;">Please enter a valid email address.</span>'); 1237 return; 1238 } 1239 1240 var ajaxurl = (typeof nexlifydesk_vars !== 'undefined' && nexlifydesk_vars.ajaxurl) ? nexlifydesk_vars.ajaxurl : (typeof ajaxurl !== 'undefined' ? ajaxurl : window.ajaxurl); 1241 var nonce = (typeof nexlifydesk_admin_vars !== 'undefined' && nexlifydesk_admin_vars.nonce) ? nexlifydesk_admin_vars.nonce : ''; 1242 1243 button.prop('disabled', true).text('Checking...'); 1244 $.post(ajaxurl, { 1245 action: 'nexlifydesk_check_rate_limit', 1246 email: email, 1247 nonce: nonce 1248 }, function(response) { 1249 button.prop('disabled', false).text('Check Status'); 1250 if (response && response.success) { 1251 resultDiv.html('<span style="color: blue;">' + response.data + '</span>'); 1252 } else { 1253 resultDiv.html('<span style="color: red;">' + (response && response.data ? response.data : 'Error checking rate limit. Please try again.') + '</span>'); 1254 } 1255 }).fail(function(xhr, status, error) { 1256 button.prop('disabled', false).text('Check Status'); 1257 resultDiv.html('<span style="color: red;">Error checking rate limit. Please try again.</span>'); 1258 }); 1259 }); 1260 } 1261 1262 function isValidEmail(email) { 1263 var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 1264 return emailRegex.test(email); 1271 1265 } 1272 1266 }); … … 1291 1285 }); 1292 1286 }); 1287 1288 jQuery(document).ready(function($){ 1289 if (typeof ajaxurl === 'undefined') { 1290 var ajaxurl = window.nexlifydesk_admin_vars ? window.nexlifydesk_admin_vars.ajaxurl : ''; 1291 } 1292 1293 function toggleProviderSettings() { 1294 var provider = $('#provider-select').val(); 1295 $('.provider-settings').hide(); 1296 $('#' + provider + '-settings').show(); 1297 1298 if (provider === 'aws') { 1299 var awsIntegrationType = $('#aws_integration_type').val() || 'imap'; 1300 if (awsIntegrationType === 'imap') { 1301 $('#aws-imap-settings').show(); 1302 $('#aws-lambda-settings').hide(); 1303 } else { 1304 $('#aws-imap-settings').hide(); 1305 $('#aws-lambda-settings').show(); 1306 } 1307 } else { 1308 $('#aws-imap-settings').hide(); 1309 $('#aws-lambda-settings').hide(); 1310 } 1311 1312 var sslEnabled = window.nexlifydesk_ssl_enabled || false; 1313 1314 if (!sslEnabled && provider === 'aws') { 1315 // alert('Note: SSL is recommended for AWS WorkMail for security purposes.'); 1316 } 1317 } 1318 1319 if ($('#provider-select').length) { 1320 toggleProviderSettings(); 1321 $('#provider-select').on('change', toggleProviderSettings); 1322 } 1323 1324 $('#aws_integration_type').on('change', function() { 1325 const integrationType = $(this).val(); 1326 1327 if (integrationType === 'imap') { 1328 $('#aws-imap-settings').show(); 1329 $('#aws-lambda-settings').hide(); 1330 } else if (integrationType === 'lambda') { 1331 $('#aws-imap-settings').hide(); 1332 $('#aws-lambda-settings').show(); 1333 } 1334 }); 1335 1336 $('#nexlifydesk-fetch-emails-now').on('click', function(){ 1337 var $btn = $(this); 1338 var $status = $('#nexlifydesk-fetch-emails-status'); 1339 $btn.prop('disabled', true); 1340 $status.text('Fetching...'); 1341 1342 $.post(ajaxurl, { 1343 action: 'nexlifydesk_fetch_emails_now', 1344 _ajax_nonce: window.nexlifydesk_fetch_emails_nonce || '' 1345 }, function(response){ 1346 $btn.prop('disabled', false); 1347 if(response.success){ 1348 $status.text('Fetch complete!'); 1349 }else{ 1350 $status.text(response.data || 'Fetch failed.'); 1351 } 1352 }).fail(function(){ 1353 $btn.prop('disabled', false); 1354 $status.text('Fetch failed.'); 1355 }); 1356 }); 1357 1358 $('#test-aws-connection').on('click', function(){ 1359 var $btn = $(this); 1360 var $result = $('#aws-connection-result'); 1361 1362 $btn.prop('disabled', true).text('Testing...'); 1363 $result.html('<p style="color: #666;">Connecting to AWS WorkMail...</p>'); 1364 1365 $.post(ajaxurl, { 1366 action: 'nexlifydesk_test_aws_connection', 1367 nonce: window.nexlifydesk_aws_test_nonce || '', 1368 region: $('#aws_region').val(), 1369 organization_id: $('#aws_organization_id').val(), 1370 email: $('#aws_email').val(), 1371 password: $('#aws_password').val(), 1372 access_key_id: $('#aws_access_key_id').val(), 1373 secret_access_key: $('#aws_secret_access_key').val() 1374 }).done(function(response) { 1375 $btn.prop('disabled', false).text('Test AWS Connection'); 1376 if (response.success) { 1377 $result.html('<p style="color: green;">✅ ' + response.data.message + '</p>'); 1378 } else { 1379 $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>'); 1380 } 1381 }).fail(function() { 1382 $btn.prop('disabled', false).text('Test AWS Connection'); 1383 $result.html('<p style="color: red;">❌ Connection test failed.</p>'); 1384 }); 1385 }); 1386 1387 $('#test-aws-fetch-emails').on('click', function(){ 1388 var $btn = $(this); 1389 var $result = $('#aws-fetch-result'); 1390 1391 $btn.prop('disabled', true).text('Fetching Emails...'); 1392 $result.html('<p style="color: #666;">Attempting to fetch emails from AWS WorkMail...</p>'); 1393 1394 $.post(ajaxurl, { 1395 action: 'nexlifydesk_manual_fetch_emails', 1396 nonce: window.nexlifydesk_aws_test_nonce || '', 1397 region: $('#aws_region').val(), 1398 organization_id: $('#aws_organization_id').val(), 1399 email: $('#aws_email').val(), 1400 password: $('#aws_password').val(), 1401 access_key_id: $('#aws_access_key_id').val(), 1402 secret_access_key: $('#aws_secret_access_key').val() 1403 }).done(function(response) { 1404 $btn.prop('disabled', false).text('Test Email Fetch'); 1405 if (response.success) { 1406 var message = response.data.message.replace(/\n/g, '<br>'); 1407 $result.html('<div style="color: green; font-family: monospace; white-space: pre-wrap; background: #f0f9ff; padding: 10px; border-left: 4px solid #0087be;">📧 ' + message + '</div>'); 1408 } else { 1409 $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>'); 1410 } 1411 }).fail(function() { 1412 $btn.prop('disabled', false).text('Test Email Fetch'); 1413 $result.html('<p style="color: red;">❌ Email fetch test failed.</p>'); 1414 }); 1415 }); 1416 1417 $('#test-google-connection').on('click', function(){ 1418 var $btn = $(this); 1419 var $result = $('#google-connection-result'); 1420 1421 $btn.prop('disabled', true).text('Testing...'); 1422 $result.html('<p style="color: #666;">Testing Google connection...</p>'); 1423 1424 $.post(ajaxurl, { 1425 action: 'nexlifydesk_test_google_connection', 1426 nonce: window.nexlifydesk_google_test_nonce || '', 1427 client_id: $('#google_client_id').val(), 1428 client_secret: $('#google_client_secret').val() 1429 }).done(function(response) { 1430 $btn.prop('disabled', false).text('Test Google Connection'); 1431 if (response.success) { 1432 $result.html('<p style="color: green;">✅ ' + response.data.message + '</p>'); 1433 } else { 1434 $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>'); 1435 } 1436 }).fail(function() { 1437 $btn.prop('disabled', false).text('Test Google Connection'); 1438 $result.html('<p style="color: red;">❌ Google connection test failed.</p>'); 1439 }); 1440 }); 1441 1442 $('#test-google-fetch-emails').on('click', function(){ 1443 var $btn = $(this); 1444 var $result = $('#google-fetch-result'); 1445 1446 $btn.prop('disabled', true).text('Fetching Emails...'); 1447 $result.html('<p style="color: #666;">Attempting to fetch emails from Google...</p>'); 1448 1449 $.post(ajaxurl, { 1450 action: 'nexlifydesk_manual_fetch_google_emails', 1451 nonce: window.nexlifydesk_google_test_nonce || '' 1452 }).done(function(response) { 1453 $btn.prop('disabled', false).text('Test Email Fetch'); 1454 if (response.success) { 1455 var message = response.data.message.replace(/\n/g, '<br>'); 1456 $result.html('<div style="color: green; font-family: monospace; white-space: pre-wrap; background: #f0f9ff; padding: 10px; border-left: 4px solid #0087be;">📧 ' + message + '</div>'); 1457 } else { 1458 $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>'); 1459 } 1460 }).fail(function() { 1461 $btn.prop('disabled', false).text('Test Email Fetch'); 1462 $result.html('<p style="color: red;">❌ Google email fetch test failed.</p>'); 1463 }); 1464 }); 1465 1466 $('#test-custom-connection').on('click', function(){ 1467 var $btn = $(this); 1468 var $result = $('#custom-connection-result'); 1469 1470 $btn.prop('disabled', true).text('Testing...'); 1471 $result.html('<p style="color: #666;">Testing custom IMAP/POP3 connection...</p>'); 1472 1473 $.post(ajaxurl, { 1474 action: 'nexlifydesk_test_custom_connection', 1475 nonce: window.nexlifydesk_custom_test_nonce || '', 1476 host: $('#host').val(), 1477 port: $('#port').val(), 1478 username: $('#username').val(), 1479 password: $('#password').val(), 1480 encryption: $('#encryption').val(), 1481 protocol: $('#protocol').val() 1482 }).done(function(response) { 1483 $btn.prop('disabled', false).text('Test Custom Connection'); 1484 if (response.success) { 1485 $result.html('<p style="color: green;">✅ ' + response.data.message + '</p>'); 1486 } else { 1487 $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>'); 1488 } 1489 }).fail(function() { 1490 $btn.prop('disabled', false).text('Test Custom Connection'); 1491 $result.html('<p style="color: red;">❌ Custom connection test failed.</p>'); 1492 }); 1493 }); 1494 1495 $('#test-custom-fetch-emails').on('click', function(){ 1496 var $btn = $(this); 1497 var $result = $('#custom-fetch-result'); 1498 1499 $btn.prop('disabled', true).text('Fetching Emails...'); 1500 $result.html('<p style="color: #666;">Attempting to fetch emails from custom server...</p>'); 1501 1502 $.post(ajaxurl, { 1503 action: 'nexlifydesk_manual_fetch_emails', 1504 nonce: window.nexlifydesk_custom_test_nonce || '', 1505 provider: 'custom' 1506 }).done(function(response) { 1507 $btn.prop('disabled', false).text('Test Email Fetch'); 1508 if (response.success) { 1509 var message = response.data.message.replace(/\n/g, '<br>'); 1510 $result.html('<div style="color: green; font-family: monospace; white-space: pre-wrap; background: #f0f9ff; padding: 10px; border-left: 4px solid #0087be;">📧 ' + message + '</div>'); 1511 } else { 1512 $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>'); 1513 } 1514 }).fail(function() { 1515 $btn.prop('disabled', false).text('Test Email Fetch'); 1516 $result.html('<p style="color: red;">❌ Custom email fetch test failed.</p>'); 1517 }); 1518 }); 1519 }); 1520 1521 jQuery(document).ready(function($){ 1522 $('#edit-custom-password').on('click', function() { 1523 const passwordField = $('#password'); 1524 const button = $(this); 1525 const hiddenField = $('#password-preserved-flag'); 1526 1527 passwordField.prop('disabled', false) 1528 .prop('readonly', false) 1529 .css('background-color', '#fff') 1530 .css('border', '1px solid #8c8f94') 1531 .css('cursor', 'text') 1532 .attr('placeholder', 'Enter new password') 1533 .val('') 1534 .focus(); 1535 1536 hiddenField.remove(); 1537 1538 button.text('Cancel').off('click').on('click', function() { 1539 passwordField.prop('disabled', true) 1540 .prop('readonly', true) 1541 .css('background-color', '#f7f7f7') 1542 .css('border', '1px solid #ddd') 1543 .css('cursor', 'not-allowed') 1544 .attr('placeholder', '') 1545 .val('••••••••••••••••'); 1546 1547 passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[password_preserved]" value="1" id="password-preserved-flag">'); 1548 1549 button.text('Edit Password').off('click').on('click', arguments.callee); 1550 }); 1551 }); 1552 1553 $('#edit-aws-password').on('click', function() { 1554 const passwordField = $('#aws_password'); 1555 const button = $(this); 1556 const hiddenField = $('#aws-password-preserved-flag'); 1557 1558 passwordField.prop('disabled', false) 1559 .prop('readonly', false) 1560 .css('background-color', '#fff') 1561 .css('border', '1px solid #8c8f94') 1562 .css('cursor', 'text') 1563 .attr('placeholder', 'Enter new password') 1564 .val('') 1565 .focus(); 1566 1567 hiddenField.remove(); 1568 1569 button.text('Cancel').off('click').on('click', function() { 1570 passwordField.prop('disabled', true) 1571 .prop('readonly', true) 1572 .css('background-color', '#f7f7f7') 1573 .css('border', '1px solid #ddd') 1574 .css('cursor', 'not-allowed') 1575 .attr('placeholder', '') 1576 .val('••••••••••••••••'); 1577 1578 passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[aws_password_preserved]" value="1" id="aws-password-preserved-flag">'); 1579 1580 button.text('Edit Password').off('click').on('click', arguments.callee); 1581 }); 1582 }); 1583 1584 $('#edit-aws-secret-key').on('click', function() { 1585 const passwordField = $('#aws_secret_access_key'); 1586 const button = $(this); 1587 const hiddenField = $('#aws-secret-key-preserved-flag'); 1588 1589 passwordField.prop('disabled', false) 1590 .prop('readonly', false) 1591 .css('background-color', '#fff') 1592 .css('border', '1px solid #8c8f94') 1593 .css('cursor', 'text') 1594 .attr('placeholder', 'Enter new secret key') 1595 .val('') 1596 .focus(); 1597 1598 hiddenField.remove(); 1599 1600 button.text('Cancel').off('click').on('click', function() { 1601 passwordField.prop('disabled', true) 1602 .prop('readonly', true) 1603 .css('background-color', '#f7f7f7') 1604 .css('border', '1px solid #ddd') 1605 .css('cursor', 'not-allowed') 1606 .attr('placeholder', '') 1607 .val('••••••••••••••••'); 1608 1609 passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[aws_secret_access_key_preserved]" value="1" id="aws-secret-key-preserved-flag">'); 1610 1611 button.text('Edit Secret Key').off('click').on('click', arguments.callee); 1612 }); 1613 }); 1614 1615 $('#edit-google-secret').on('click', function() { 1616 const passwordField = $('#google_client_secret'); 1617 const button = $(this); 1618 const hiddenField = $('#google-secret-preserved-flag'); 1619 1620 passwordField.prop('disabled', false) 1621 .prop('readonly', false) 1622 .css('background-color', '#fff') 1623 .css('border', '1px solid #8c8f94') 1624 .css('cursor', 'text') 1625 .attr('placeholder', 'Enter new client secret') 1626 .val('') 1627 .focus(); 1628 1629 hiddenField.remove(); 1630 1631 button.text('Cancel').off('click').on('click', function() { 1632 passwordField.prop('disabled', true) 1633 .prop('readonly', true) 1634 .css('background-color', '#f7f7f7') 1635 .css('border', '1px solid #ddd') 1636 .css('cursor', 'not-allowed') 1637 .attr('placeholder', '') 1638 .val('••••••••••••••••'); 1639 1640 passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[google_client_secret_preserved]" value="1" id="google-secret-preserved-flag">'); 1641 1642 button.text('Edit Secret').off('click').on('click', arguments.callee); 1643 }); 1644 }); 1645 1646 $('#aws_integration_type').on('change', function() { 1647 const integrationType = $(this).val(); 1648 1649 if (integrationType === 'lambda') { 1650 alert('Lambda integration is coming soon! Please use IMAP integration for now.'); 1651 $(this).val('imap'); 1652 $('#aws-imap-settings').show(); 1653 $('#aws-lambda-settings').hide(); 1654 } else { 1655 $('#aws-imap-settings').show(); 1656 $('#aws-lambda-settings').hide(); 1657 } 1658 }); 1659 1660 $(document).ready(function() { 1661 $('#aws_integration_type option[value="lambda"]').prop('disabled', true); 1662 }); 1663 1664 $('#regenerate-webhook-secret').on('click', function() { 1665 const newSecret = generateRandomString(32); 1666 $('#lambda_webhook_secret').val(newSecret); 1667 }); 1668 1669 $('#copy-webhook-url').on('click', function() { 1670 const webhookUrl = $(this).prev('input').val(); 1671 navigator.clipboard.writeText(webhookUrl).then(function() { 1672 alert('Webhook URL copied to clipboard!'); 1673 }); 1674 }); 1675 1676 $('#test-lambda-webhook').on('click', function() { 1677 const button = $(this); 1678 const resultDiv = $('#lambda-webhook-result'); 1679 1680 button.prop('disabled', true).text('Testing...'); 1681 resultDiv.html('<p>Testing webhook connection...</p>'); 1682 1683 const testData = { 1684 from: '[email protected]', 1685 to: '[email protected]', 1686 subject: 'Test Email from Lambda', 1687 body: 'This is a test email to verify the Lambda webhook integration.', 1688 message_id: 'test-' + Date.now(), 1689 timestamp: Math.floor(Date.now() / 1000) 1690 }; 1691 1692 $.ajax({ 1693 url: $('#copy-webhook-url').prev('input').val(), 1694 method: 'POST', 1695 contentType: 'application/json', 1696 headers: { 1697 'X-NexlifyDesk-Secret': $('#lambda_webhook_secret').val() 1698 }, 1699 data: JSON.stringify(testData), 1700 success: function(response) { 1701 resultDiv.html('<div style="color: green;"><strong>✓ Webhook test successful!</strong><br>Response: ' + JSON.stringify(response) + '</div>'); 1702 }, 1703 error: function(xhr, status, error) { 1704 resultDiv.html('<div style="color: red;"><strong>✗ Webhook test failed!</strong><br>Error: ' + error + '<br>Status: ' + xhr.status + '</div>'); 1705 }, 1706 complete: function() { 1707 button.prop('disabled', false).text('Test Webhook'); 1708 } 1709 }); 1710 }); 1711 1712 function generateRandomString(length) { 1713 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 1714 let result = ''; 1715 for (let i = 0; i < length; i++) { 1716 result += chars.charAt(Math.floor(Math.random() * chars.length)); 1717 } 1718 return result; 1719 } 1720 1721 $('#provider-select').on('change', function() { 1722 $('input[type="password"]').each(function() { 1723 const field = $(this); 1724 const fieldId = field.attr('id'); 1725 const editButton = $('#edit-' + fieldId.replace('_', '-')); 1726 1727 if (editButton.length && field.val() === '••••••••••••••••') { 1728 field.prop('disabled', true) 1729 .prop('readonly', true) 1730 .css('background-color', '#f7f7f7') 1731 .css('border', '1px solid #ddd') 1732 .css('cursor', 'not-allowed'); 1733 1734 editButton.text(editButton.text().replace('Cancel', 'Edit')); 1735 } 1736 }); 1737 }); 1738 }); 1739 1740 jQuery(document).ready(function($) { 1741 $('#generate-encryption-key').on('click', function() { 1742 $.ajax({ 1743 url: ajaxurl, 1744 type: 'POST', 1745 data: { 1746 action: 'nexlifydesk_generate_encryption_key', 1747 nonce: window.nexlifydesk_generate_key_nonce || '' 1748 }, 1749 success: function(response) { 1750 if (response.success) { 1751 $('#encryption-key-output').val(response.data.key); 1752 $('#generated-key-display').show(); 1753 } else { 1754 alert('Error generating key: ' + response.data); 1755 } 1756 }, 1757 error: function() { 1758 alert('Error generating encryption key. Please try again.'); 1759 } 1760 }); 1761 }); 1762 1763 $('#copy-encryption-key').on('click', function() { 1764 const keyField = document.getElementById('encryption-key-output'); 1765 keyField.select(); 1766 keyField.setSelectionRange(0, 99999); 1767 1768 navigator.clipboard.writeText(keyField.value).then(function() { 1769 alert('Encryption key copied to clipboard!'); 1770 }, function(err) { 1771 document.execCommand('copy'); 1772 alert('Encryption key copied to clipboard!'); 1773 }); 1774 }); 1775 }); -
nexlifydesk/trunk/includes/class-nexlifydesk-admin.php
r3326104 r3330741 15 15 add_action('admin_post_nexlifydesk_delete_agent_position', array(__CLASS__, 'handle_delete_agent_position')); 16 16 add_action('admin_post_nexlifydesk_save_agent_assignments', array(__CLASS__, 'handle_save_agent_assignments')); 17 add_action('admin_post_nexlifydesk_save_imap_settings', array(__CLASS__, 'save_imap_settings')); 18 add_action('admin_init', ['NexlifyDesk_Admin', 'maybe_encrypt_existing_passwords']); 19 add_action('wp_ajax_nexlifydesk_refresh_ticket_list', array(__CLASS__, 'ajax_refresh_ticket_list')); 20 add_action('wp_ajax_nexlifydesk_mark_ticket_read', array(__CLASS__, 'ajax_mark_ticket_read')); 21 add_action('wp_ajax_nexlifydesk_clear_rate_limit', array(__CLASS__, 'ajax_clear_rate_limit')); 22 add_action('wp_ajax_nexlifydesk_check_rate_limit', array(__CLASS__, 'ajax_check_rate_limit')); 23 add_action('admin_post_nexlifydesk_deauth_google', array(__CLASS__, 'handle_deauth_google')); 24 add_action('admin_post_nexlifydesk_deauth_aws', array(__CLASS__, 'handle_deauth_aws')); 25 add_action('admin_post_nexlifydesk_deauth_custom', array(__CLASS__, 'handle_deauth_custom')); 26 add_action('admin_post_nexlifydesk_deauth_all', array(__CLASS__, 'handle_deauth_all')); 27 28 register_setting('nexlifydesk_settings', 'nexlifydesk_imap_settings', [ 29 'type' => 'array', 30 'sanitize_callback' => array(__CLASS__, 'sanitize_imap_settings'), 31 ]); 32 } 33 34 /** 35 * Sanitize IMAP settings input 36 */ 37 public static function sanitize_imap_settings($input) { 38 39 $new_input = get_option('nexlifydesk_imap_settings', []); 40 41 $new_input['enabled'] = isset($input['enabled']) ? 1 : 0; 42 $new_input['provider'] = sanitize_text_field($input['provider'] ?? 'custom'); 43 44 $fetch_interval = absint($input['fetch_interval'] ?? 5); 45 $allowed_intervals = [2, 5, 10, 15]; 46 $new_input['fetch_interval'] = in_array($fetch_interval, $allowed_intervals) ? $fetch_interval : 5; 47 48 $is_ssl_enabled = function_exists('nexlifydesk_check_ssl_enabled') ? nexlifydesk_check_ssl_enabled() : true; 49 50 $provider = $new_input['provider'] ?? 'custom'; 51 52 if ($provider === 'aws') { 53 $is_ssl_enabled = true; 54 } 55 56 $new_input['protocol'] = sanitize_text_field($input['protocol'] ?? 'imap'); 57 $new_input['host'] = sanitize_text_field($input['host'] ?? ''); 58 $new_input['port'] = absint($input['port'] ?? 993); 59 $new_input['encryption'] = sanitize_text_field($input['encryption'] ?? 'ssl'); 60 $new_input['username'] = sanitize_text_field($input['username'] ?? ''); 61 62 if (!empty($input['password']) && $input['password'] !== '••••••••••••••••') { 63 $plain = sanitize_text_field($input['password']); 64 $new_input['password'] = nexlifydesk_encrypt($plain); 65 } elseif (!empty($input['password_preserved'])) { 66 $new_input['password'] = $new_input['password'] ?? ''; 67 } else { 68 $new_input['password'] = ''; 69 } 70 71 $new_input['google_client_id'] = sanitize_text_field($input['google_client_id'] ?? ''); 72 73 if (!empty($input['google_client_secret']) && $input['google_client_secret'] !== '••••••••••••••••') { 74 $plain = sanitize_text_field($input['google_client_secret']); 75 $new_input['google_client_secret'] = nexlifydesk_encrypt($plain); 76 } elseif (!empty($input['google_client_secret_preserved'])) { 77 $new_input['google_client_secret'] = $new_input['google_client_secret'] ?? ''; 78 } else { 79 $new_input['google_client_secret'] = ''; 80 } 81 82 if (isset($input['google_access_token'])) { 83 $new_input['google_access_token'] = sanitize_text_field($input['google_access_token']); 84 } 85 if (isset($input['google_refresh_token'])) { 86 $new_input['google_refresh_token'] = sanitize_text_field($input['google_refresh_token']); 87 } 88 if (isset($input['google_account_email'])) { 89 $new_input['google_account_email'] = sanitize_email($input['google_account_email']); 90 } 91 92 $new_input['aws_region'] = sanitize_text_field($input['aws_region'] ?? ''); 93 $new_input['aws_organization_id'] = sanitize_text_field($input['aws_organization_id'] ?? ''); 94 $new_input['aws_email'] = sanitize_email($input['aws_email'] ?? ''); 95 96 if (!empty($input['aws_password']) && $input['aws_password'] !== '••••••••••••••••') { 97 $plain = sanitize_text_field($input['aws_password']); 98 $new_input['aws_password'] = nexlifydesk_encrypt($plain); 99 } elseif (!empty($input['aws_password_preserved'])) { 100 $new_input['aws_password'] = $new_input['aws_password'] ?? ''; 101 } else { 102 $new_input['aws_password'] = ''; 103 } 104 105 $new_input['aws_access_key_id'] = sanitize_text_field($input['aws_access_key_id'] ?? ''); 106 107 if (!empty($input['aws_secret_access_key']) && $input['aws_secret_access_key'] !== '••••••••••••••••') { 108 $plain = sanitize_text_field($input['aws_secret_access_key']); 109 $new_input['aws_secret_access_key'] = nexlifydesk_encrypt($plain); 110 } elseif (!empty($input['aws_secret_access_key_preserved'])) { 111 $new_input['aws_secret_access_key'] = $new_input['aws_secret_access_key'] ?? ''; 112 } else { 113 $new_input['aws_secret_access_key'] = ''; 114 } 115 116 $new_input['delete_emails_after_fetch'] = isset($input['delete_emails_after_fetch']) ? 1 : 0; 117 $new_input['block_admin_emails'] = isset($input['block_admin_emails']) ? 1 : 0; 118 $new_input['block_notification_subjects'] = isset($input['block_notification_subjects']) ? 1 : 0; 119 $new_input['block_marketing_emails'] = isset($input['block_marketing_emails']) ? 1 : 0; 120 $new_input['blocked_emails'] = sanitize_textarea_field($input['blocked_emails'] ?? ''); 121 $new_input['blocked_domains'] = sanitize_textarea_field($input['blocked_domains'] ?? ''); 122 $new_input['spam_url_filtering'] = isset($input['spam_url_filtering']) ? 1 : 0; 123 $new_input['max_links_per_email'] = absint($input['max_links_per_email'] ?? 3); 124 $new_input['blocked_keywords'] = sanitize_textarea_field($input['blocked_keywords'] ?? ''); 125 $new_input['aws_integration_type'] = sanitize_text_field($input['aws_integration_type'] ?? 'imap'); 126 if ($new_input['aws_integration_type'] === 'lambda') { 127 $new_input['aws_integration_type'] = 'imap'; 128 } 129 $new_input['lambda_webhook_secret'] = sanitize_text_field($input['lambda_webhook_secret'] ?? ''); 130 131 return $new_input; 132 } 133 134 /** 135 * Save IMAP settings from the IMAP Auth settings page 136 */ 137 public static function save_imap_settings() { 138 if (!current_user_can('manage_options')) { 139 wp_die(esc_html__('You do not have permission.', 'nexlifydesk')); 140 } 141 check_admin_referer('nexlifydesk_save_imap_settings', 'nexlifydesk_imap_settings_nonce'); 142 143 $old_settings = get_option('nexlifydesk_imap_settings', []); 144 145 $settings = array( 146 'enabled' => isset($_POST['enabled']) ? 1 : 0, 147 'provider' => sanitize_text_field(isset($_POST['provider']) ? wp_unslash($_POST['provider']) : ''), 148 'host' => sanitize_text_field(isset($_POST['host']) ? wp_unslash($_POST['host']) : ''), 149 'port' => absint(isset($_POST['port']) ? wp_unslash($_POST['port']) : 993), 150 'encryption' => sanitize_text_field(isset($_POST['encryption']) ? wp_unslash($_POST['encryption']) : ''), 151 'username' => sanitize_text_field(isset($_POST['username']) ? wp_unslash($_POST['username']) : ''), 152 'protocol' => sanitize_text_field(isset($_POST['protocol']) ? wp_unslash($_POST['protocol']) : 'imap'), 153 ); 154 155 $password_input = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : ''; 156 $password_preserved = isset($_POST['password_preserved']) ? sanitize_text_field(wp_unslash($_POST['password_preserved'])) : ''; 157 158 if (!empty($password_input)) { 159 $settings['password'] = nexlifydesk_encrypt($password_input); 160 } elseif (!empty($password_preserved)) { 161 $settings['password'] = $old_settings['password'] ?? ''; 162 } else { 163 $settings['password'] = ''; 164 } 165 166 $aws_password_input = isset($_POST['aws_password']) ? sanitize_text_field(wp_unslash($_POST['aws_password'])) : ''; 167 $aws_password_preserved = isset($_POST['aws_password_preserved']) ? sanitize_text_field(wp_unslash($_POST['aws_password_preserved'])) : ''; 168 169 if (!empty($aws_password_input)) { 170 $settings['aws_password'] = nexlifydesk_encrypt($aws_password_input); 171 } elseif (!empty($aws_password_preserved)) { 172 $settings['aws_password'] = $old_settings['aws_password'] ?? ''; 173 } else { 174 $settings['aws_password'] = ''; 175 } 176 177 $aws_secret_input = isset($_POST['aws_secret_access_key']) ? sanitize_text_field(wp_unslash($_POST['aws_secret_access_key'])) : ''; 178 $aws_secret_preserved = isset($_POST['aws_secret_access_key_preserved']) ? sanitize_text_field(wp_unslash($_POST['aws_secret_access_key_preserved'])) : ''; 179 180 if (!empty($aws_secret_input)) { 181 $settings['aws_secret_access_key'] = nexlifydesk_encrypt($aws_secret_input); 182 } elseif (!empty($aws_secret_preserved)) { 183 $settings['aws_secret_access_key'] = $old_settings['aws_secret_access_key'] ?? ''; 184 } else { 185 $settings['aws_secret_access_key'] = ''; 186 } 187 188 $google_secret_input = isset($_POST['google_client_secret']) ? sanitize_text_field(wp_unslash($_POST['google_client_secret'])) : ''; 189 $google_secret_preserved = isset($_POST['google_client_secret_preserved']) ? sanitize_text_field(wp_unslash($_POST['google_client_secret_preserved'])) : ''; 190 191 if (!empty($google_secret_input)) { 192 $settings['google_client_secret'] = nexlifydesk_encrypt($google_secret_input); 193 } elseif (!empty($google_secret_preserved)) { 194 $settings['google_client_secret'] = $old_settings['google_client_secret'] ?? ''; 195 } else { 196 $settings['google_client_secret'] = ''; 197 } 198 199 $settings['aws_integration_type'] = sanitize_text_field(isset($_POST['aws_integration_type']) ? wp_unslash($_POST['aws_integration_type']) : 'imap'); 200 if ($settings['aws_integration_type'] === 'lambda') { 201 $settings['aws_integration_type'] = 'imap'; 202 } 203 $settings['lambda_webhook_secret'] = sanitize_text_field(isset($_POST['lambda_webhook_secret']) ? wp_unslash($_POST['lambda_webhook_secret']) : ''); 204 $settings = array_merge($old_settings, $settings); 205 206 update_option('nexlifydesk_imap_settings', $settings); 207 wp_redirect(add_query_arg(array('page' => 'nexlifydesk_imap_auth', 'settings-updated' => 'true'), admin_url('admin.php'))); 208 exit; 209 } 210 211 /** 212 * Handle Google de-authentication 213 */ 214 public static function handle_deauth_google() { 215 if (!current_user_can('manage_options')) { 216 wp_die(esc_html__('You do not have permission.', 'nexlifydesk')); 217 } 218 219 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_google')) { 220 wp_die(esc_html__('Security check failed.', 'nexlifydesk')); 221 } 222 223 $settings = get_option('nexlifydesk_imap_settings', []); 224 225 unset($settings['google_access_token']); 226 unset($settings['google_refresh_token']); 227 unset($settings['google_client_id']); 228 unset($settings['google_client_secret']); 229 unset($settings['google_auth_status']); 230 unset($settings['google_account_email']); 231 unset($settings['google_token_expires_at']); 232 unset($settings['google_fetch_start_time']); 233 234 $settings['google_access_token'] = ''; 235 $settings['google_refresh_token'] = ''; 236 $settings['google_client_id'] = ''; 237 $settings['google_client_secret'] = ''; 238 $settings['google_auth_status'] = ''; 239 $settings['google_account_email'] = ''; 240 $settings['google_token_expires_at'] = ''; 241 $settings['google_fetch_start_time'] = ''; 242 243 update_option('nexlifydesk_imap_settings', $settings); 244 245 delete_option('nexlifydesk_processed_emails'); 246 247 wp_redirect(add_query_arg(array( 248 'page' => 'nexlifydesk_imap_auth', 249 'deauth' => 'google_success', 250 '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth') 251 ), admin_url('admin.php'))); 252 exit; 253 } 254 255 /** 256 * Handle AWS de-authentication 257 */ 258 public static function handle_deauth_aws() { 259 if (!current_user_can('manage_options')) { 260 wp_die(esc_html__('You do not have permission.', 'nexlifydesk')); 261 } 262 263 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_aws')) { 264 wp_die(esc_html__('Security check failed.', 'nexlifydesk')); 265 } 266 267 $settings = get_option('nexlifydesk_imap_settings', []); 268 269 unset($settings['aws_region']); 270 unset($settings['aws_organization_id']); 271 unset($settings['aws_email']); 272 unset($settings['aws_password']); 273 unset($settings['aws_access_key_id']); 274 unset($settings['aws_secret_access_key']); 275 unset($settings['aws_fetch_start_time']); 276 277 $settings['aws_region'] = ''; 278 $settings['aws_organization_id'] = ''; 279 $settings['aws_email'] = ''; 280 $settings['aws_password'] = ''; 281 $settings['aws_access_key_id'] = ''; 282 $settings['aws_secret_access_key'] = ''; 283 $settings['aws_fetch_start_time'] = ''; 284 285 update_option('nexlifydesk_imap_settings', $settings); 286 287 delete_option('nexlifydesk_aws_processed_emails'); 288 289 wp_redirect(add_query_arg(array( 290 'page' => 'nexlifydesk_imap_auth', 291 'deauth' => 'aws_success', 292 '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth') 293 ), admin_url('admin.php'))); 294 exit; 295 } 296 297 /** 298 * Handle Custom IMAP/POP3 de-authentication 299 */ 300 public static function handle_deauth_custom() { 301 if (!current_user_can('manage_options')) { 302 wp_die(esc_html__('You do not have permission.', 'nexlifydesk')); 303 } 304 305 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_custom')) { 306 wp_die(esc_html__('Security check failed.', 'nexlifydesk')); 307 } 308 309 $settings = get_option('nexlifydesk_imap_settings', []); 310 311 unset($settings['host']); 312 unset($settings['port']); 313 unset($settings['username']); 314 unset($settings['password']); 315 unset($settings['encryption']); 316 unset($settings['protocol']); 317 318 $settings['host'] = ''; 319 $settings['port'] = 993; 320 $settings['username'] = ''; 321 $settings['password'] = ''; 322 $settings['encryption'] = 'ssl'; 323 $settings['protocol'] = 'imap'; 324 325 update_option('nexlifydesk_imap_settings', $settings); 326 327 wp_redirect(add_query_arg(array( 328 'page' => 'nexlifydesk_imap_auth', 329 'deauth' => 'custom_success', 330 '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth') 331 ), admin_url('admin.php'))); 332 exit; 333 } 334 335 /** 336 * Handle complete de-authentication (all providers) 337 */ 338 public static function handle_deauth_all() { 339 if (!current_user_can('manage_options')) { 340 wp_die(esc_html__('You do not have permission.', 'nexlifydesk')); 341 } 342 343 if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_all')) { 344 wp_die(esc_html__('Security check failed.', 'nexlifydesk')); 345 } 346 347 $current_settings = get_option('nexlifydesk_imap_settings', []); 348 $credential_keys = [ 349 // Custom IMAP/POP3 credentials 350 'host', 'port', 'username', 'password', 'encryption', 'protocol', 351 352 // Google credentials 353 'google_client_id', 'google_client_secret', 'google_access_token', 354 'google_refresh_token', 'google_auth_status', 'google_account_email', 355 'google_token_expires_at', 'google_fetch_start_time', 356 357 // AWS credentials 358 'aws_region', 'aws_organization_id', 'aws_email', 'aws_password', 359 'aws_access_key_id', 'aws_secret_access_key', 'aws_fetch_start_time' 360 ]; 361 362 foreach ($credential_keys as $key) { 363 unset($current_settings[$key]); 364 } 365 366 $default_settings = array( 367 'enabled' => 0, 368 'provider' => 'custom', 369 'fetch_interval' => 5, 370 'delete_emails_after_fetch' => 1, 371 372 // Custom IMAP/POP3 settings 373 'host' => '', 374 'port' => 993, 375 'username' => '', 376 'password' => '', 377 'encryption' => 'ssl', 378 'protocol' => 'imap', 379 380 // Google settings 381 'google_client_id' => '', 382 'google_client_secret' => '', 383 'google_access_token' => '', 384 'google_refresh_token' => '', 385 'google_auth_status' => '', 386 'google_account_email' => '', 387 'google_token_expires_at' => '', 388 'google_fetch_start_time' => '', 389 390 // AWS settings 391 'aws_region' => '', 392 'aws_organization_id' => '', 393 'aws_email' => '', 394 'aws_password' => '', 395 'aws_access_key_id' => '', 396 'aws_secret_access_key' => '', 397 'aws_fetch_start_time' => '', 398 399 // Spam protection settings (preserve these) 400 'block_admin_emails' => 1, 401 'block_notification_subjects' => 1, 402 'block_marketing_emails' => 1, 403 'blocked_emails' => '', 404 'blocked_domains' => '', 405 'spam_url_filtering' => 1, 406 'max_links_per_email' => 3, 407 'blocked_keywords' => '', 408 ); 409 410 $spam_settings = [ 411 'block_admin_emails', 'block_notification_subjects', 'block_marketing_emails', 412 'blocked_emails', 'blocked_domains', 'spam_url_filtering', 'max_links_per_email', 'blocked_keywords' 413 ]; 414 415 foreach ($spam_settings as $setting) { 416 if (isset($current_settings[$setting])) { 417 $default_settings[$setting] = $current_settings[$setting]; 418 } 419 } 420 421 $final_settings = array_merge($current_settings, $default_settings); 422 423 update_option('nexlifydesk_imap_settings', $final_settings); 424 425 delete_option('nexlifydesk_processed_emails'); 426 delete_option('nexlifydesk_aws_processed_emails'); 427 428 wp_redirect(add_query_arg(array( 429 'page' => 'nexlifydesk_imap_auth', 430 'deauth' => 'all_success', 431 '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth') 432 ), admin_url('admin.php'))); 433 exit; 434 } 435 436 public static function maybe_encrypt_existing_passwords() { 437 $settings = get_option('nexlifydesk_imap_settings', []); 438 $updated = false; 439 440 $is_encrypted = function($value) { 441 if (empty($value)) return false; 442 return nexlifydesk_is_encrypted($value); 443 }; 444 445 // Custom IMAP/POP3 password 446 if (!empty($settings['password']) && !$is_encrypted($settings['password'])) { 447 $settings['password'] = nexlifydesk_encrypt($settings['password']); 448 $updated = true; 449 } 450 // AWS password 451 if (!empty($settings['aws_password']) && !$is_encrypted($settings['aws_password'])) { 452 $settings['aws_password'] = nexlifydesk_encrypt($settings['aws_password']); 453 $updated = true; 454 } 455 // Google client secret 456 if (!empty($settings['google_client_secret']) && !$is_encrypted($settings['google_client_secret'])) { 457 $settings['google_client_secret'] = nexlifydesk_encrypt($settings['google_client_secret']); 458 $updated = true; 459 } 460 // AWS secret access key 461 if (!empty($settings['aws_secret_access_key']) && !$is_encrypted($settings['aws_secret_access_key'])) { 462 $settings['aws_secret_access_key'] = nexlifydesk_encrypt($settings['aws_secret_access_key']); 463 $updated = true; 464 } 465 if ($updated) { 466 update_option('nexlifydesk_imap_settings', $settings); 467 } 17 468 } 18 469 … … 108 559 array('NexlifyDesk_Admin', 'render_support_page') 109 560 ); 561 562 // Add IMAP Auth submenu 563 add_submenu_page( 564 'nexlifydesk_tickets', 565 __('IMAP Auth', 'nexlifydesk'), 566 __('IMAP Auth', 'nexlifydesk'), 567 'manage_options', 568 'nexlifydesk_imap_auth', 569 array(__CLASS__, 'render_imap_auth_page') 570 ); 110 571 111 572 } 112 573 574 public static function render_imap_auth_page() { 575 ?> 576 <div class="wrap"> 577 <h1><?php esc_html_e('Email Piping (IMAP Auth)', 'nexlifydesk'); ?></h1> 578 579 <?php 580 // Show IMAP extension warning if not available 581 if (!extension_loaded('imap')) { 582 echo '<div class="notice notice-error"><p><strong>' . esc_html__('Warning:', 'nexlifydesk') . '</strong> ' . 583 esc_html__('IMAP extension is not installed on this server. Email piping will not work for Custom IMAP/POP3 and AWS providers. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk') . 584 '</p></div>'; 585 } 586 ?> 587 588 <?php include NEXLIFYDESK_PLUGIN_DIR . 'templates/admin/imap-auth.php'; ?> 589 </div> 590 <?php 591 } 592 593 /** 594 * AJAX handler for refreshing ticket list 595 */ 596 public static function ajax_refresh_ticket_list() { 597 if (!current_user_can('nexlifydesk_manage_tickets')) { 598 wp_die('Unauthorized'); 599 } 600 601 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 602 603 $last_refresh = isset($_POST['last_refresh']) ? intval($_POST['last_refresh']) / 1000 : 0; 604 605 if (class_exists('NexlifyDesk_Tickets')) { 606 $tickets = NexlifyDesk_Tickets::get_tickets_for_refresh($last_refresh); 607 608 wp_send_json_success(array( 609 'tickets' => $tickets, 610 'timestamp' => time() 611 )); 612 } 613 614 wp_send_json_error('Tickets system not available'); 615 } 616 617 /** 618 * AJAX handler for marking ticket as read 619 */ 620 public static function ajax_mark_ticket_read() { 621 if (!current_user_can('nexlifydesk_manage_tickets')) { 622 wp_die('Unauthorized'); 623 } 624 625 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 626 627 $ticket_id = isset($_POST['ticket_id']) ? intval($_POST['ticket_id']) : 0; 628 629 if ($ticket_id && class_exists('NexlifyDesk_Tickets')) { 630 $success = NexlifyDesk_Tickets::mark_ticket_as_read($ticket_id, get_current_user_id()); 631 632 if ($success) { 633 wp_send_json_success('Ticket marked as read'); 634 } 635 } 636 637 wp_send_json_error('Failed to mark ticket as read'); 638 } 639 640 /** 641 * AJAX handler to clear rate limit for a specific email address 642 */ 643 public static function ajax_clear_rate_limit() { 644 // Check permissions 645 if (!current_user_can('manage_options')) { 646 wp_send_json_error(__('You do not have permission to perform this action.', 'nexlifydesk')); 647 } 648 649 // Verify nonce 650 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk-ajax-nonce')) { 651 wp_send_json_error(__('Nonce verification failed.', 'nexlifydesk')); 652 } 653 654 // Get email address 655 $email_address = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; 656 657 if (empty($email_address) || !is_email($email_address)) { 658 wp_send_json_error(__('Please enter a valid email address.', 'nexlifydesk')); 659 } 660 661 // Check if user exists 662 $user = get_user_by('email', $email_address); 663 $user_id = $user ? $user->ID : 0; 664 665 // Get current status before clearing 666 $status = NexlifyDesk_Rate_Limiter::get_rate_limit_status($user_id, $email_address); 667 668 if (!$status['is_limited']) { 669 wp_send_json_error(__('This email address is not currently rate limited.', 'nexlifydesk')); 670 } 671 672 // Clear the rate limit 673 $result = NexlifyDesk_Rate_Limiter::clear_rate_limit($user_id, $email_address); 674 675 if ($result) { 676 $message = sprintf( 677 /* translators: %s: email address */ 678 __('Rate limit cleared for %s. They can now send emails again.', 'nexlifydesk'), 679 esc_html($email_address) 680 ); 681 wp_send_json_success($message); 682 } else { 683 wp_send_json_error(__('Failed to clear rate limit. Please try again.', 'nexlifydesk')); 684 } 685 } 686 687 /** 688 * AJAX handler to check rate limit status for a specific email address 689 */ 690 public static function ajax_check_rate_limit() { 691 // Check permissions 692 if (!current_user_can('manage_options')) { 693 wp_send_json_error(__('You do not have permission to perform this action.', 'nexlifydesk')); 694 } 695 696 // Verify nonce 697 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk-ajax-nonce')) { 698 wp_send_json_error(__('Nonce verification failed.', 'nexlifydesk')); 699 } 700 701 // Get email address 702 $email_address = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; 703 704 if (empty($email_address) || !is_email($email_address)) { 705 wp_send_json_error(__('Please enter a valid email address.', 'nexlifydesk')); 706 } 707 708 // Check if user exists 709 $user = get_user_by('email', $email_address); 710 $user_id = $user ? $user->ID : 0; 711 712 // Get rate limit status 713 $status = NexlifyDesk_Rate_Limiter::get_rate_limit_status($user_id, $email_address); 714 715 if ($status['is_limited']) { 716 $time_remaining = NexlifyDesk_Rate_Limiter::format_time_remaining($status['time_until_reset']); 717 $message = sprintf( 718 /* translators: 1: email address, 2: current count, 3: max allowed, 4: time remaining */ 719 __('Rate limit status for %1$s: %2$d/%3$d emails sent. Rate limit will reset in %4$s.', 'nexlifydesk'), 720 esc_html($email_address), 721 $status['current_count'], 722 $status['max_allowed'], 723 $time_remaining 724 ); 725 } else { 726 $message = sprintf( 727 /* translators: 1: email address, 2: current count, 3: max allowed */ 728 __('Rate limit status for %1$s: %2$d/%3$d emails sent. No rate limit active.', 'nexlifydesk'), 729 esc_html($email_address), 730 $status['current_count'], 731 $status['max_allowed'] 732 ); 733 } 734 735 wp_send_json_success($message); 736 } 737 738 /** 739 * Get the ticket page URL from settings 740 * 741 * @return string The ticket page URL or empty string if not set 742 */ 743 public static function get_ticket_page_url() { 744 $settings = get_option('nexlifydesk_settings', array()); 745 $ticket_page_id = isset($settings['ticket_page_id']) ? (int)$settings['ticket_page_id'] : 0; 746 747 if ($ticket_page_id > 0) { 748 $ticket_page_url = get_permalink($ticket_page_id); 749 return $ticket_page_url ? $ticket_page_url : ''; 750 } 751 752 return ''; 753 } 754 113 755 public static function render_support_page() { 114 756 if (isset($_POST['submit']) && isset($_POST['nexlify_support_nonce'])) { … … 159 801 $ticket_id = isset($_GET['ticket_id']) ? sanitize_text_field( wp_unslash( $_GET['ticket_id'] ) ) : ''; 160 802 161 if ( 162 ($page && strpos($page, 'nexlifydesk') === 0) || 163 (strpos($hook, 'nexlifydesk') !== false) || 164 ($page === 'nexlifydesk_tickets' && $ticket_id) 165 ) { 166 wp_enqueue_style( 167 'nexlifydesk-admin', 168 NEXLIFYDESK_PLUGIN_URL . 'assets/css/nexlifydesk-admin.css', 169 array(), 170 NEXLIFYDESK_VERSION 171 ); 803 if ( 804 ($page && strpos($page, 'nexlifydesk') === 0) || 805 (strpos($hook, 'nexlifydesk') !== false) 806 ) { 807 wp_enqueue_style( 808 'nexlifydesk-admin', 809 NEXLIFYDESK_PLUGIN_URL . 'assets/css/nexlifydesk-admin.css', 810 array(), 811 NEXLIFYDESK_VERSION 812 ); 813 wp_enqueue_script( 814 'nexlifydesk-admin', 815 NEXLIFYDESK_PLUGIN_URL . 'assets/js/nexlifydesk.js', 816 array('jquery'), 817 NEXLIFYDESK_VERSION, 818 true 819 ); 820 821 // Enqueue page-specific scripts 822 if ($page === 'nexlifydesk_tickets') { 172 823 wp_enqueue_script( 173 'nexlifydesk-admin ',174 NEXLIFYDESK_PLUGIN_URL . 'assets/js/ nexlifydesk.js',175 array('jquery' ),824 'nexlifydesk-admin-ticket-list', 825 NEXLIFYDESK_PLUGIN_URL . 'assets/js/admin-ticket-list.js', 826 array('jquery', 'nexlifydesk-admin'), 176 827 NEXLIFYDESK_VERSION, 177 828 true 178 829 ); 830 } 179 831 180 832 $available_capabilities = array( … … 230 882 } 231 883 884 if ($page === 'nexlifydesk_imap_auth' || $page === 'nexlifydesk_imap_settings') { 885 $is_ssl_enabled = function_exists('nexlifydesk_check_ssl_enabled') ? nexlifydesk_check_ssl_enabled() : true; 886 887 if (defined('NEXLIFYDESK_FORCE_SSL_ENABLED') && NEXLIFYDESK_FORCE_SSL_ENABLED) { 888 $is_ssl_enabled = true; 889 } 890 891 wp_add_inline_script( 892 'nexlifydesk-admin', 893 'window.nexlifydesk_ssl_enabled = ' . wp_json_encode($is_ssl_enabled) . ';' . 894 'window.nexlifydesk_fetch_emails_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk_fetch_emails_now')) . ';' . 895 'window.nexlifydesk_aws_test_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk_aws_test')) . ';' . 896 'window.nexlifydesk_google_test_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk-ajax-nonce')) . ';' . 897 'window.nexlifydesk_custom_test_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk-ajax-nonce')) . ';' . 898 'window.nexlifydesk_generate_key_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk_generate_key')) . ';', 899 'before' 900 ); 901 } 902 232 903 if ($page === 'nexlifydesk_tickets') { 233 904 wp_add_inline_script( … … 821 1492 $settings = array( 822 1493 'email_notifications' => isset($_POST['email_notifications']) ? 1 : 0, 1494 'admin_email_notifications' => isset($_POST['admin_email_notifications']) ? 1 : 0, 1495 'disable_email_notifications_for_email_tickets' => isset($_POST['disable_email_notifications_for_email_tickets']) ? 1 : 0, 823 1496 'default_priority' => isset($_POST['default_priority']) ? sanitize_text_field(wp_unslash($_POST['default_priority'])) : '', 824 1497 'auto_assign' => isset($_POST['auto_assign']) ? 1 : 0, … … 955 1628 'status_changed' => '', 956 1629 'sla_breach' => '', 1630 'email_auto_response' => '', 957 1631 )); 958 1632 … … 963 1637 'status_changed' => isset($_POST['status_changed']) ? wp_kses_post(wp_unslash($_POST['status_changed'])) : '', 964 1638 'sla_breach' => isset($_POST['sla_breach']) ? wp_kses_post(wp_unslash($_POST['sla_breach'])) : '', 1639 'email_auto_response' => isset($_POST['email_auto_response']) ? wp_kses_post(wp_unslash($_POST['email_auto_response'])) : '', 965 1640 ]; 966 1641 update_option('nexlifydesk_email_templates', $templates); … … 1030 1705 <div id="preview-status_changed" class="nexlifydesk-email-preview" style="border:1px solid #ddd; margin-top:10px; padding:10px; display:none;"></div> 1031 1706 1707 <h2><?php esc_html_e('Email Auto-Response (For Email Channel)', 'nexlifydesk'); ?></h2> 1708 <p class="description"><?php esc_html_e('This is the automatic response sent to customers when they create a ticket via email. Leave empty to use the default message.', 'nexlifydesk'); ?></p> 1709 <?php 1710 // Show default template as placeholder if empty 1711 $auto_response_content = $templates['email_auto_response']; 1712 if (empty($auto_response_content)) { 1713 $auto_response_content = "Hello {customer_name},\n\nThank you for contacting us. We have received your support request and have assigned it ticket ID #{ticket_id}.\n\nSubject: {subject}\n\nOur support team will review your request and get back to you as soon as possible. You can reference this ticket ID in any future correspondence.\n\nBest regards,\n{site_name} Support Team\n{site_url}"; 1714 } 1715 wp_editor( 1716 $auto_response_content, 1717 'email_auto_response', 1718 array( 1719 'textarea_name' => 'email_auto_response', 1720 'textarea_rows' => 10, 1721 'media_buttons' => false, 1722 'tinymce' => false, 1723 'quicktags' => true, 1724 'default_editor' => 'html', 1725 ) 1726 ); ?> 1727 <button type="button" class="button preview-email-template" data-editor="email_auto_response" style="margin-top:5px;"> 1728 <?php esc_html_e('Preview', 'nexlifydesk'); ?> 1729 </button> 1730 <div id="preview-email_auto_response" class="nexlifydesk-email-preview" style="border:1px solid #ddd; margin-top:10px; padding:10px; display:none;"></div> 1731 1032 1732 <h2><?php esc_html_e('SLA Breach', 'nexlifydesk'); ?></h2> 1033 1733 <?php wp_editor( … … 1070 1770 <li><code>{ticket_admin_url}</code> – <?php esc_html_e('Direct link to the ticket in the admin area (for agents/admins)', 'nexlifydesk'); ?></li> 1071 1771 </ul> 1772 1773 <h3><?php esc_html_e('Auto-Response Specific Placeholders', 'nexlifydesk'); ?></h3> 1774 <p class="description"><?php esc_html_e('These placeholders are specifically for the Email Auto-Response template:', 'nexlifydesk'); ?></p> 1775 <ul> 1776 <li><code>{customer_name}</code> – <?php esc_html_e('Customer Name (extracted from email)', 'nexlifydesk'); ?></li> 1777 <li><code>{site_name}</code> – <?php esc_html_e('Your Website Name', 'nexlifydesk'); ?></li> 1778 <li><code>{site_url}</code> – <?php esc_html_e('Your Website URL', 'nexlifydesk'); ?></li> 1779 <li><code>{admin_email}</code> – <?php esc_html_e('Admin Email Address', 'nexlifydesk'); ?></li> 1780 </ul> 1781 1072 1782 <p><?php esc_html_e('Copy and paste these placeholders into your templates. They will be replaced with real ticket data.', 'nexlifydesk'); ?></p> 1073 1783 </div> 1074 1784 </div> 1075 1785 <?php 1076 }1077 1078 private static function check_rate_limit($action, $user_id) {1079 $key = "nexlifydesk_rate_limit_{$action}_{$user_id}";1080 $attempts = get_transient($key);1081 1082 if ($attempts && $attempts > 5) {1083 wp_die(esc_html__('Too many attempts. Please try again later.', 'nexlifydesk'));1084 }1085 1086 set_transient($key, ($attempts + 1), 300); // 5 minutes1087 }1088 1089 public static function get_ticket_page_url() {1090 $settings = get_option('nexlifydesk_settings', array());1091 $ticket_page_id = isset($settings['ticket_page_id']) ? (int)$settings['ticket_page_id'] : 0;1092 1093 if ($ticket_page_id > 0) {1094 $url = get_permalink($ticket_page_id);1095 if ($url && wp_http_validate_url($url)) {1096 return $url;1097 }1098 }1099 1100 return home_url();1101 1786 } 1102 1787 … … 1118 1803 <?php 1119 1804 } 1120 }1121 1122 public static function save_settings() {1123 if (!current_user_can('manage_options')) {1124 wp_die(esc_html__('You do not have permission to perform this action.', 'nexlifydesk'));1125 }1126 check_admin_referer('nexlifydesk_save_settings', 'nexlifydesk_settings_nonce');1127 1128 $settings = get_option('nexlifydesk_settings', array());1129 1130 $flat_fields = array(1131 'email_notifications', 'default_priority', 'auto_assign', 'allowed_file_types', 'max_file_size',1132 'default_category', 'sla_response_time', 'ticket_page_id', 'ticket_form_page_id', 'ticket_id_prefix',1133 'ticket_id_start', 'keep_data_on_uninstall', 'status_change_notification', 'check_duplicates', 'duplicate_threshold',1134 'auto_assign_to_admin'1135 );1136 foreach ($flat_fields as $field) {1137 if (isset($_POST[$field])) {1138 $settings[$field] = is_numeric($_POST[$field]) ? (int)$_POST[$field] : sanitize_text_field(wp_unslash($_POST[$field]));1139 } elseif (in_array($field, array('email_notifications', 'auto_assign', 'keep_data_on_uninstall', 'status_change_notification', 'check_duplicates'))) {1140 $settings[$field] = 0;1141 }1142 }1143 1144 if (isset($_POST['nexlifydesk_settings']) && is_array($_POST['nexlifydesk_settings'])) {1145 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Input is unslashed and sanitized immediately after1146 $nexlifydesk_settings_raw = wp_unslash($_POST['nexlifydesk_settings']);1147 $nexlifydesk_settings = filter_var_array($nexlifydesk_settings_raw, FILTER_SANITIZE_FULL_SPECIAL_CHARS);1148 $sanitized_settings = array();1149 1150 foreach ($nexlifydesk_settings as $key => $value) {1151 $sanitized_key = sanitize_key($key);1152 $sanitized_value = is_numeric($value) ? (int)$value : sanitize_text_field($value);1153 $sanitized_settings[$sanitized_key] = $sanitized_value;1154 $settings[$sanitized_key] = $sanitized_value;1155 }1156 }1157 1158 update_option('nexlifydesk_settings', $settings);1159 add_settings_error('nexlifydesk_settings', 'settings_saved', __('Settings saved successfully!', 'nexlifydesk'), 'success');1160 wp_redirect(add_query_arg(array('settings-updated' => 'true'), admin_url('admin.php?page=nexlifydesk_settings')));1161 exit;1162 }1163 1164 public static function get_ticket_form_page_url() {1165 $settings = get_option('nexlifydesk_settings', array());1166 $ticket_form_page_id = isset($settings['ticket_form_page_id']) ? (int)$settings['ticket_form_page_id'] : 0;1167 1168 if ($ticket_form_page_id > 0) {1169 $page = get_post($ticket_form_page_id);1170 if ($page && $page->post_status === 'publish') {1171 $url = get_permalink($ticket_form_page_id);1172 if ($url && wp_http_validate_url($url)) {1173 return $url;1174 }1175 }1176 }1177 1178 $pages = get_pages(array('post_status' => 'publish'));1179 foreach ($pages as $page) {1180 if (has_shortcode($page->post_content, 'nexlifydesk_ticket_form')) {1181 $page_title = strtolower($page->post_title);1182 $page_slug = $page->post_name;1183 1184 if (strpos($page_title, 'doc') !== false ||1185 strpos($page_title, 'help') !== false ||1186 strpos($page_title, 'guide') !== false ||1187 strpos($page_slug, 'doc') !== false ||1188 strpos($page_slug, 'help') !== false ||1189 strpos($page_slug, 'guide') !== false) {1190 continue;1191 }1192 1193 return get_permalink($page->ID);1194 }1195 }1196 1197 return '';1198 1805 } 1199 1806 } -
nexlifydesk/trunk/includes/class-nexlifydesk-ajax.php
r3326104 r3330741 17 17 add_action('wp_ajax_nexlifydesk_delete_category', array(__CLASS__, 'delete_category')); 18 18 add_action('wp_ajax_nexlifydesk_add_category', array(__CLASS__, 'add_category')); 19 add_action('wp_ajax_nexlifydesk_test_aws_connection', array(__CLASS__, 'test_aws_connection')); 20 add_action('wp_ajax_nexlifydesk_manual_fetch_emails', array(__CLASS__, 'manual_fetch_emails')); 21 add_action('wp_ajax_nexlifydesk_bulk_action', array(__CLASS__, 'handle_bulk_action')); 22 add_action('wp_ajax_nexlifydesk_refresh_ticket_list', array(__CLASS__, 'refresh_ticket_list')); 23 add_action('wp_ajax_nexlifydesk_mark_ticket_read', array(__CLASS__, 'mark_ticket_read')); 24 add_action('wp_ajax_nexlifydesk_test_custom_connection', array(__CLASS__, 'test_custom_connection')); 25 add_action('wp_ajax_nexlifydesk_test_google_connection', array(__CLASS__, 'test_google_connection')); 26 add_action('wp_ajax_nexlifydesk_manual_fetch_google_emails', array(__CLASS__, 'manual_fetch_google_emails')); 19 27 } 20 28 … … 25 33 if (!is_user_logged_in()) { 26 34 wp_send_json_error(__('You must be logged in to submit a ticket.', 'nexlifydesk')); 35 } 36 37 if (isset($_FILES['attachments']) && isset($_FILES['attachments']['error']) && is_array($_FILES['attachments']['error'])) { 38 foreach ($_FILES['attachments']['error'] as $error) { 39 $error = absint($error); 40 if ($error === UPLOAD_ERR_FORM_SIZE || $error === UPLOAD_ERR_INI_SIZE) { 41 wp_send_json_error(__('One or more files are too large. Please reduce file sizes and try again.', 'nexlifydesk')); 42 } 43 if ($error === UPLOAD_ERR_PARTIAL) { 44 wp_send_json_error(__('File upload was interrupted. Please try again.', 'nexlifydesk')); 45 } 46 } 47 } 48 49 $max_post_size = self::get_max_post_size(); 50 $content_length = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0; 51 52 if ($content_length > $max_post_size) { 53 wp_send_json_error(sprintf( 54 /* translators: %s: Maximum allowed file size */ 55 __('Request too large. Maximum allowed size is %s. Please reduce file sizes or number of files.', 'nexlifydesk'), 56 size_format($max_post_size) 57 )); 27 58 } 28 59 … … 35 66 'message' => wp_kses_post(wp_unslash($_POST['message'])), 36 67 'category_id' => isset($_POST['category_id']) ? absint(wp_unslash($_POST['category_id'])) : 0, 37 'priority' => isset($_POST['priority']) ? sanitize_text_field(wp_unslash($_POST['priority'])) : 'medium' 68 'priority' => isset($_POST['priority']) ? sanitize_text_field(wp_unslash($_POST['priority'])) : 'medium', 69 'source' => 'web' 38 70 ); 39 71 … … 198 230 $data['attachments'] = $attachments; 199 231 $data['user_id'] = $current_user->ID; 232 $data['source'] = 'web'; 200 233 201 234 $reply_id = NexlifyDesk_Tickets::add_reply($data); … … 395 428 wp_send_json_error( 396 429 sprintf( 397 /* translators: % d: Maximum file size in megabytes */430 /* translators: %s: Maximum file size in megabytes */ 398 431 __('File size exceeds maximum limit of %dMB.', 'nexlifydesk'), 399 432 isset($settings['max_file_size']) ? (int)$settings['max_file_size'] : 2 … … 436 469 437 470 try { 471 438 472 $current_user = wp_get_current_user(); 439 473 $status = isset($_POST['status']) && $_POST['status'] !== 'all' ? sanitize_text_field(wp_unslash($_POST['status'])) : ''; … … 539 573 540 574 } catch (Exception $e) { 541 542 575 wp_send_json_error(__('Error loading tickets. Please try again or contact support.', 'nexlifydesk')); 543 576 } … … 725 758 } 726 759 } 760 761 /** 762 * Test AWS WorkMail connection 763 */ 764 public static function test_aws_connection() { 765 766 if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) { 767 wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk'))); 768 } 769 770 // Check if IMAP extension is available 771 if (!extension_loaded('imap')) { 772 wp_send_json_error(array('message' => __('IMAP extension is not installed on this server. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk'))); 773 } 774 775 if ( 776 !isset($_POST['nonce']) || 777 !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk_aws_test') 778 ) { 779 wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk'))); 780 } 781 782 $region = sanitize_text_field(wp_unslash($_POST['region'] ?? '')); 783 $organization_id = sanitize_text_field(wp_unslash($_POST['organization_id'] ?? '')); 784 $email = sanitize_email(wp_unslash($_POST['email'] ?? '')); 785 $password = sanitize_text_field(wp_unslash($_POST['password'] ?? '')); 786 $access_key_id = sanitize_text_field(wp_unslash($_POST['access_key_id'] ?? '')); 787 $secret_access_key = sanitize_text_field(wp_unslash($_POST['secret_access_key'] ?? '')); 788 789 if ($password === '••••••••••••••••') { 790 $current_settings = get_option('nexlifydesk_imap_settings', []); 791 if (!empty($current_settings['aws_password'])) { 792 $password = nexlifydesk_decrypt($current_settings['aws_password']); 793 } 794 } 795 796 $is_ssl_enabled = ( 797 is_ssl() || 798 (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') || 799 (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') || 800 (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') || 801 (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') || 802 (wp_parse_url(home_url(), PHP_URL_SCHEME) === 'https') 803 ); 804 805 if (defined('NEXLIFYDESK_FORCE_SSL_ENABLED') && NEXLIFYDESK_FORCE_SSL_ENABLED) { 806 $is_ssl_enabled = true; 807 } 808 809 if (!$is_ssl_enabled) { 810 wp_send_json_error(array('message' => __('SSL certificate is required for AWS WorkMail integration.', 'nexlifydesk'))); 811 } 812 813 if (empty($region) || empty($organization_id) || empty($email) || empty($password)) { 814 wp_send_json_error(array('message' => __('Please fill in all required fields: AWS Region, Organization ID, Email Address, and Email Password.', 'nexlifydesk'))); 815 } 816 817 try { 818 $messages = []; 819 820 $imap_host = "imap.mail.{$region}.awsapps.com"; 821 $imap_port = 993; 822 823 $connection = @imap_open( 824 "{{$imap_host}:{$imap_port}/imap/ssl}INBOX", 825 $email, 826 $password, 827 OP_READONLY 828 ); 829 830 if (!$connection) { 831 $error = imap_last_error(); 832 /* translators: 1: IMAP error, 2: Organization ID, 3: Email, 4: AWS region */ 833 wp_send_json_error(array('message' => sprintf(__('AWS WorkMail IMAP connection failed: %1$s. Please verify your Organization ID (%2$s), Email (%3$s), and Password are correct for region %4$s.', 'nexlifydesk'), $error, $organization_id, $email, $region))); 834 } 835 836 imap_close($connection); 837 $messages[] = 'AWS WorkMail IMAP connection successful'; 838 839 if (!empty($access_key_id) && !empty($secret_access_key)) { 840 $original_settings = get_option('nexlifydesk_imap_settings', []); 841 $test_settings = array_merge($original_settings, [ 842 'aws_region' => $region, 843 'aws_access_key_id' => $access_key_id, 844 'aws_secret_access_key' => $secret_access_key 845 ]); 846 update_option('nexlifydesk_imap_settings', $test_settings); 847 848 $aws_handler_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/providers/aws-ses/aws-handler.php'; 849 if (file_exists($aws_handler_path)) { 850 require_once $aws_handler_path; 851 852 $auth = new NexlifyDesk_AWS_Auth(); 853 $ses_result = $auth->validate_ses_credentials(); 854 855 if (is_wp_error($ses_result)) { 856 $messages[] = 'AWS SES credentials validation failed: ' . $ses_result->get_error_message(); 857 } else { 858 $messages[] = 'AWS SES credentials validated successfully'; 859 } 860 } 861 862 update_option('nexlifydesk_imap_settings', $original_settings); 863 } else { 864 $messages[] = 'AWS API credentials (Access Key ID, Secret Access Key) not provided - SES features will not be available'; 865 } 866 867 $final_message = implode('. ', $messages); 868 wp_send_json_success(array('message' => $final_message)); 869 870 } catch (Exception $e) { 871 /* translators: %s: Error message returned from the connection test */ 872 wp_send_json_error(array('message' => sprintf(__('Connection test failed: %s', 'nexlifydesk'), $e->getMessage()))); 873 } 874 } 875 876 /* 877 * Test custom IMAP connection 878 */ 879 public static function test_custom_connection() { 880 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 881 882 // Check if IMAP extension is available 883 if (!extension_loaded('imap')) { 884 wp_send_json_error(array('message' => __('IMAP extension is not installed on this server. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk'))); 885 } 886 887 $host = isset($_POST['host']) ? sanitize_text_field(wp_unslash($_POST['host'])) : ''; 888 $port = isset($_POST['port']) ? intval($_POST['port']) : 993; 889 $username = isset($_POST['username']) ? sanitize_text_field(wp_unslash($_POST['username'])) : ''; 890 $password = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : ''; 891 $encryption = isset($_POST['encryption']) ? sanitize_text_field(wp_unslash($_POST['encryption'])) : 'ssl'; 892 $protocol = isset($_POST['protocol']) ? sanitize_text_field(wp_unslash($_POST['protocol'])) : 'imap'; 893 894 if ($password === '••••••••••••••••') { 895 $settings = get_option('nexlifydesk_imap_settings', array()); 896 $password = nexlifydesk_get_safe_password($settings['password'] ?? ''); 897 } 898 899 if (empty($host) || empty($port) || empty($username) || empty($password)) { 900 wp_send_json_error('Missing required connection details.'); 901 } 902 903 $mailbox = "{" . $host . ":" . $port . "/" . $protocol . "/" . $encryption . "}INBOX"; 904 $inbox = @imap_open($mailbox, $username, $password, OP_READONLY); 905 906 if ($inbox) { 907 imap_close($inbox); 908 wp_send_json_success(['message' => 'Connection successful!']); 909 } else { 910 $error = imap_last_error(); 911 wp_send_json_error(['message' => 'Connection failed!']); 912 } 913 } 914 915 /** 916 * Manual email fetch for testing 917 */ 918 public static function manual_fetch_emails() { 919 920 if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) { 921 wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk'))); 922 } 923 924 $provider = isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : ''; 925 $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; 926 927 if ($provider === 'custom') { 928 if (!$nonce || !wp_verify_nonce($nonce, 'nexlifydesk-ajax-nonce')) { 929 wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk'))); 930 } 931 wp_send_json_success(array('message' => 'Custom email fetch test successful!')); 932 } 933 934 if (!$nonce || !wp_verify_nonce($nonce, 'nexlifydesk_aws_test')) { 935 wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk'))); 936 } 937 938 $region = sanitize_text_field(wp_unslash($_POST['region'] ?? '')); 939 $organization_id = sanitize_text_field(wp_unslash($_POST['organization_id'] ?? '')); 940 $email = sanitize_email(wp_unslash($_POST['email'] ?? '')); 941 $password = sanitize_text_field(wp_unslash($_POST['password'] ?? '')); 942 $access_key_id = sanitize_text_field(wp_unslash($_POST['access_key_id'] ?? '')); 943 $secret_access_key = sanitize_text_field(wp_unslash($_POST['secret_access_key'] ?? '')); 944 945 if ($password === '••••••••••••••••') { 946 $current_settings = get_option('nexlifydesk_imap_settings', []); 947 if (!empty($current_settings['aws_password'])) { 948 $password = nexlifydesk_decrypt($current_settings['aws_password']); 949 } 950 } 951 952 if (empty($region) || empty($organization_id) || empty($email) || empty($password)) { 953 wp_send_json_error(array('message' => __('Please fill in all required AWS fields first.', 'nexlifydesk'))); 954 } 955 956 try { 957 $current_settings = get_option('nexlifydesk_imap_settings', []); 958 $test_settings = array_merge($current_settings, [ 959 'enabled' => true, 960 'provider' => 'aws', 961 'aws_region' => $region, 962 'aws_organization_id' => $organization_id, 963 'aws_email' => $email, 964 'aws_password' => $password 965 ]); 966 967 if (!empty($access_key_id) && !empty($secret_access_key)) { 968 $test_settings['aws_access_key_id'] = $access_key_id; 969 $test_settings['aws_secret_access_key'] = $secret_access_key; 970 } 971 972 update_option('nexlifydesk_imap_settings', $test_settings); 973 974 $email_pipe_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/nexlifydesk-email-pipe.php'; 975 if (file_exists($email_pipe_path)) { 976 require_once $email_pipe_path; 977 } 978 979 $aws_handler_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/providers/aws-ses/aws-handler.php'; 980 if (file_exists($aws_handler_path)) { 981 require_once $aws_handler_path; 982 } 983 984 if (function_exists('nexlifydesk_fetch_emails')) { 985 nexlifydesk_fetch_emails(); 986 } else { 987 throw new Exception('Email fetch function not found'); 988 } 989 990 update_option('nexlifydesk_imap_settings', $current_settings); 991 992 global $wpdb; 993 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe and not user input 994 $table_name = $wpdb->prefix . 'nexlifydesk_tickets'; 995 $recent_tickets_cache_key = 'nexlifydesk_recent_email_tickets_' . md5(gmdate('Y-m-d H:i:s', strtotime('-2 minutes'))); 996 $recent_tickets = wp_cache_get($recent_tickets_cache_key); 997 998 if (false === $recent_tickets) { 999 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and not user input 1000 $recent_tickets = $wpdb->get_results( 1001 $wpdb->prepare( 1002 "SELECT ticket_id, subject, source, created_at 1003 FROM {$table_name} 1004 WHERE created_at >= %s 1005 AND source = 'email' 1006 ORDER BY created_at DESC 1007 LIMIT 10", 1008 gmdate('Y-m-d H:i:s', strtotime('-2 minutes')) 1009 ) 1010 ); 1011 wp_cache_set($recent_tickets_cache_key, $recent_tickets, '', 120); 1012 } 1013 1014 $message = "Email fetch completed successfully!\n"; 1015 1016 if (!empty($recent_tickets)) { 1017 $message .= "Found " . count($recent_tickets) . " recent email ticket(s):\n"; 1018 foreach ($recent_tickets as $ticket) { 1019 $message .= "- #{$ticket->ticket_id}: {$ticket->subject} ({$ticket->created_at})\n"; 1020 } 1021 } else { 1022 $message .= "No new email tickets found. This could mean:\n"; 1023 $message .= "- No new emails in the mailbox\n"; 1024 $message .= "- Emails are being filtered out (spam, duplicates)\n"; 1025 $message .= "- All emails have been processed already\n"; 1026 } 1027 1028 wp_send_json_success(array('message' => $message)); 1029 1030 } catch (Exception $e) { 1031 if (isset($current_settings)) { 1032 update_option('nexlifydesk_imap_settings', $current_settings); 1033 } 1034 1035 /* translators: %s: Error message returned from the email fetch */ 1036 wp_send_json_error(array('message' => sprintf(__('Email fetch failed: %s', 'nexlifydesk'), $e->getMessage()))); 1037 } 1038 } 1039 1040 /** 1041 * Handle bulk actions for tickets 1042 */ 1043 public static function handle_bulk_action() { 1044 // Check nonce 1045 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 1046 1047 // Check permissions 1048 if (!current_user_can('nexlifydesk_manage_tickets')) { 1049 wp_send_json_error(__('You do not have permission to perform this action.', 'nexlifydesk')); 1050 } 1051 1052 $bulk_action = sanitize_text_field(wp_unslash($_POST['bulk_action'] ?? '')); 1053 $ticket_ids = array_map('intval', (array) ($_POST['ticket_ids'] ?? [])); 1054 1055 if (empty($bulk_action) || empty($ticket_ids)) { 1056 wp_send_json_error(__('Invalid bulk action or no tickets selected.', 'nexlifydesk')); 1057 } 1058 1059 $updated_count = 0; 1060 $errors = []; 1061 1062 foreach ($ticket_ids as $ticket_id) { 1063 $ticket = NexlifyDesk_Tickets::get_ticket($ticket_id); 1064 if (!$ticket) { 1065 /* translators: %d: Ticket ID */ 1066 $errors[] = sprintf(__('Ticket #%d not found.', 'nexlifydesk'), $ticket_id); 1067 continue; 1068 } 1069 1070 try { 1071 switch ($bulk_action) { 1072 case 'assign': 1073 $agent_id = intval($_POST['agent_id'] ?? 0); 1074 $result = NexlifyDesk_Tickets::assign_ticket($ticket_id, $agent_id); 1075 if ($result) { 1076 $updated_count++; 1077 } else { 1078 /* translators: %d: Ticket ID */ 1079 $errors[] = sprintf(__('Failed to assign ticket #%d.', 'nexlifydesk'), $ticket_id); 1080 } 1081 break; 1082 1083 case 'status': 1084 $status = sanitize_text_field(wp_unslash($_POST['status'] ?? '')); 1085 $allowed_statuses = ['open', 'in_progress', 'pending', 'resolved', 'closed']; 1086 if (!in_array($status, $allowed_statuses)) { 1087 /* translators: 1: Status, 2: Ticket ID */ 1088 $errors[] = sprintf(__('Invalid status "%1$s" for ticket #%2$d.', 'nexlifydesk'), $status, $ticket_id); 1089 continue 2; 1090 } 1091 1092 $result = NexlifyDesk_Tickets::update_ticket_status($ticket_id, $status); 1093 if ($result) { 1094 $updated_count++; 1095 } else { 1096 /* translators: %d: Ticket ID */ 1097 $errors[] = sprintf(__('Failed to update status for ticket #%d.', 'nexlifydesk'), $ticket_id); 1098 } 1099 break; 1100 1101 case 'priority': 1102 $priority = sanitize_text_field(wp_unslash($_POST['priority'] ?? '')); 1103 $allowed_priorities = ['low', 'medium', 'high', 'urgent']; 1104 if (!in_array($priority, $allowed_priorities)) { 1105 /* translators: 1: Priority, 2: Ticket ID */ 1106 $errors[] = sprintf(__('Invalid priority "%1$s" for ticket #%2$d.', 'nexlifydesk'), $priority, $ticket_id); 1107 continue 2; 1108 } 1109 1110 $result = NexlifyDesk_Tickets::update_ticket_priority($ticket_id, $priority); 1111 if ($result) { 1112 $updated_count++; 1113 } else { 1114 /* translators: %d: Ticket ID */ 1115 $errors[] = sprintf(__('Failed to update priority for ticket #%d.', 'nexlifydesk'), $ticket_id); 1116 } 1117 break; 1118 1119 case 'delete': 1120 // Only allow admins to delete tickets 1121 if (!current_user_can('manage_options')) { 1122 /* translators: %d: Ticket ID */ 1123 $errors[] = sprintf(__('You do not have permission to delete ticket #%d.', 'nexlifydesk'), $ticket_id); 1124 continue 2; 1125 } 1126 1127 $result = NexlifyDesk_Tickets::delete_ticket($ticket_id); 1128 if ($result) { 1129 $updated_count++; 1130 } else { 1131 /* translators: %d: Ticket ID */ 1132 $errors[] = sprintf(__('Failed to delete ticket #%d.', 'nexlifydesk'), $ticket_id); 1133 } 1134 break; 1135 1136 default: 1137 /* translators: %s: Bulk action name */ 1138 $errors[] = sprintf(__('Unknown bulk action "%s".', 'nexlifydesk'), $bulk_action); 1139 break; 1140 } 1141 } catch (Exception $e) { 1142 /* translators: 1: Ticket ID, 2: Error message */ 1143 $errors[] = sprintf(__('Error processing ticket #%1$d: %2$s', 'nexlifydesk'), $ticket_id, $e->getMessage()); 1144 } 1145 } 1146 1147 $messages = []; 1148 if ($updated_count > 0) { 1149 if ($bulk_action === 'delete') { 1150 /* translators: %d: Number of tickets deleted */ 1151 $messages[] = sprintf(_n('%d ticket deleted permanently.', '%d tickets deleted permanently.', $updated_count, 'nexlifydesk'), $updated_count); 1152 } else { 1153 /* translators: %d: Number of tickets updated */ 1154 $messages[] = sprintf(_n('%d ticket updated successfully.', '%d tickets updated successfully.', $updated_count, 'nexlifydesk'), $updated_count); 1155 } 1156 } 1157 1158 if (!empty($errors)) { 1159 $messages[] = __('Errors occurred:', 'nexlifydesk') . ' ' . implode(', ', $errors); 1160 } 1161 1162 if ($updated_count > 0) { 1163 wp_send_json_success(implode(' ', $messages)); 1164 } else { 1165 wp_send_json_error(implode(' ', $messages)); 1166 } 1167 } 1168 1169 /** 1170 * Refresh ticket list - returns updated ticket data 1171 */ 1172 public static function refresh_ticket_list() { 1173 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk-ajax-nonce')) { 1174 wp_send_json_error(array( 1175 'message' => __('Session expired. Please reload the page.', 'nexlifydesk'), 1176 'code' => 'nonce_expired' 1177 )); 1178 } 1179 1180 if (!current_user_can('nexlifydesk_manage_tickets')) { 1181 wp_send_json_error(array('message' => __('You do not have permission to view tickets.', 'nexlifydesk'))); 1182 } 1183 1184 try { 1185 $last_refresh = isset($_POST['last_refresh']) ? intval($_POST['last_refresh']) : 0; 1186 1187 wp_cache_flush_group('nexlifydesk_tickets_grid'); 1188 1189 $tickets = NexlifyDesk_Tickets::get_tickets_for_grid(['per_page' => 100]); 1190 1191 if (!is_array($tickets)) { 1192 $tickets = array(); 1193 } 1194 1195 $formatted_tickets = array(); 1196 $current_user_id = get_current_user_id(); 1197 1198 foreach ($tickets as $ticket) { 1199 if (!is_object($ticket) || !isset($ticket->id)) { 1200 continue; 1201 } 1202 1203 $user = get_userdata($ticket->user_id); 1204 $assigned_agent = $ticket->assigned_to ? get_userdata($ticket->assigned_to) : null; 1205 1206 if (!$user) { 1207 $customer_details = function_exists('nexlifydesk_extract_customer_details') ? 1208 nexlifydesk_extract_customer_details($ticket->message) : 1209 ['name' => '', 'email' => '', 'message' => $ticket->message]; 1210 $customer_name = $customer_details['name'] ?: 'Guest'; 1211 $customer_email = $customer_details['email'] ?: 'N/A'; 1212 } else { 1213 $customer_name = $user->display_name; 1214 $customer_email = $user->user_email; 1215 } 1216 1217 $is_unread = 0; 1218 if (isset($ticket->is_unread)) { 1219 $is_unread = intval($ticket->is_unread); 1220 } else { 1221 global $wpdb; 1222 $cache_key = 'nexlifydesk_ticket_read_' . intval($ticket->id) . '_' . intval($current_user_id); 1223 $read_record = wp_cache_get($cache_key, '', 60); 1224 if (false === $read_record) { 1225 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1226 $read_record = $wpdb->get_row( 1227 $wpdb->prepare( 1228 "SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads 1229 WHERE ticket_id = %d AND user_id = %d", 1230 $ticket->id, 1231 $current_user_id 1232 ) 1233 ); 1234 wp_cache_set($cache_key, $read_record, '', 60); 1235 } 1236 1237 if (!$read_record) { 1238 $is_unread = 1; 1239 } else { 1240 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1241 $newer_replies = $wpdb->get_var( 1242 $wpdb->prepare( 1243 "SELECT COUNT(*) FROM {$wpdb->prefix}nexlifydesk_replies 1244 WHERE ticket_id = %d AND user_id != %d 1245 AND created_at > %s AND is_internal_note = 0", 1246 $ticket->id, 1247 $current_user_id, 1248 $read_record->read_at 1249 ) 1250 ); 1251 $is_unread = $newer_replies > 0 ? 1 : 0; 1252 } 1253 } 1254 1255 $formatted_tickets[] = array( 1256 'id' => $ticket->id, 1257 'ticket_id' => $ticket->ticket_id, 1258 'subject' => $ticket->subject, 1259 'message' => $ticket->message, 1260 'status' => $ticket->status, 1261 'priority' => $ticket->priority, 1262 'user_id' => $ticket->user_id, 1263 'user_name' => $customer_name, 1264 'user_email' => $customer_email, 1265 'assigned_to' => $ticket->assigned_to, 1266 'assigned_to_display_name' => $assigned_agent ? $assigned_agent->display_name : '', 1267 'created_at' => $ticket->created_at, 1268 'updated_at' => $ticket->updated_at, 1269 'last_reply_time' => isset($ticket->last_reply_time) ? $ticket->last_reply_time : $ticket->created_at, 1270 'is_unread' => $is_unread 1271 ); 1272 } 1273 1274 $response_data = array( 1275 'tickets' => $formatted_tickets, 1276 'new_nonce' => wp_create_nonce('nexlifydesk-ajax-nonce') 1277 ); 1278 1279 wp_send_json_success($response_data); 1280 1281 } catch (Exception $e) { 1282 wp_send_json_error(array( 1283 'message' => __('Error refreshing ticket list.', 'nexlifydesk'), 1284 'error' => $e->getMessage() 1285 )); 1286 } catch (Error $e) { 1287 wp_send_json_error(array( 1288 'message' => __('Fatal error refreshing ticket list.', 'nexlifydesk'), 1289 'error' => $e->getMessage() 1290 )); 1291 } 1292 } 1293 1294 /** 1295 * Mark ticket as read 1296 */ 1297 public static function mark_ticket_read() { 1298 // Check nonce 1299 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 1300 1301 // Check permissions 1302 if (!current_user_can('nexlifydesk_manage_tickets')) { 1303 wp_send_json_error(array('message' => __('You do not have permission to mark tickets as read.', 'nexlifydesk'))); 1304 } 1305 1306 if (!isset($_POST['ticket_id'])) { 1307 wp_send_json_error(array('message' => __('Ticket ID is required.', 'nexlifydesk'))); 1308 } 1309 1310 $ticket_id = intval($_POST['ticket_id']); 1311 1312 try { 1313 $current_user_id = get_current_user_id(); 1314 1315 global $wpdb; 1316 $reads_table = $wpdb->prefix . 'nexlifydesk_ticket_reads'; 1317 1318 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct REPLACE query 1319 $result = $wpdb->replace( 1320 $reads_table, 1321 array( 1322 'ticket_id' => $ticket_id, 1323 'user_id' => $current_user_id, 1324 'read_at' => current_time('mysql') 1325 ), 1326 array('%d', '%d', '%s') 1327 ); 1328 // Clear cache for this ticket/user read status 1329 $cache_key = 'nexlifydesk_ticket_read_' . intval($ticket_id) . '_' . intval($current_user_id); 1330 wp_cache_delete($cache_key); 1331 1332 if ($result === false) { 1333 throw new Exception('Failed to update read status in database'); 1334 } 1335 1336 wp_cache_flush_group('nexlifydesk_tickets_grid'); 1337 1338 wp_send_json_success(array('message' => __('Ticket marked as read.', 'nexlifydesk'))); 1339 1340 } catch (Exception $e) { 1341 wp_send_json_error(array('message' => __('Error marking ticket as read.', 'nexlifydesk'))); 1342 } 1343 } 1344 1345 /** 1346 * Test Google connection 1347 */ 1348 public static function test_google_connection() { 1349 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 1350 1351 if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) { 1352 wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk'))); 1353 } 1354 1355 $client_id = isset($_POST['client_id']) ? sanitize_text_field(wp_unslash($_POST['client_id'])) : ''; 1356 $client_secret = isset($_POST['client_secret']) ? sanitize_text_field(wp_unslash($_POST['client_secret'])) : ''; 1357 1358 if ($client_secret === '••••••••••••••••') { 1359 $settings = get_option('nexlifydesk_imap_settings', array()); 1360 $client_secret = !empty($settings['google_client_secret']) ? nexlifydesk_decrypt($settings['google_client_secret']) : ''; 1361 } 1362 1363 if (empty($client_id) || empty($client_secret)) { 1364 wp_send_json_error(array('message' => __('Please fill in both Google Client ID and Client Secret.', 'nexlifydesk'))); 1365 } 1366 1367 try { 1368 $email_pipe_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/nexlifydesk-email-pipe.php'; 1369 if (file_exists($email_pipe_path)) { 1370 require_once $email_pipe_path; 1371 } 1372 1373 if (function_exists('nexlifydesk_get_google_access_token')) { 1374 $access_token = nexlifydesk_get_google_access_token(); 1375 if ($access_token) { 1376 $response = wp_remote_get('https://www.googleapis.com/oauth2/v2/userinfo', [ 1377 'headers' => ['Authorization' => 'Bearer ' . $access_token] 1378 ]); 1379 1380 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { 1381 $user_info = json_decode(wp_remote_retrieve_body($response), true); 1382 $email = $user_info['email'] ?? 'Unknown'; 1383 wp_send_json_success(array('message' => "Google connection successful! Authorized as: {$email}")); 1384 } else { 1385 wp_send_json_error(array('message' => __('Google connection failed. Please re-authorize.', 'nexlifydesk'))); 1386 } 1387 } else { 1388 wp_send_json_error(array('message' => __('Google not authorized. Please complete the OAuth authorization first.', 'nexlifydesk'))); 1389 } 1390 } else { 1391 wp_send_json_error(array('message' => __('Google handler functions not available.', 'nexlifydesk'))); 1392 } 1393 } catch (Exception $e) { 1394 /* translators: %s: Error message returned from the Google connection test */ 1395 wp_send_json_error(array('message' => sprintf(__('Google connection test failed: %s', 'nexlifydesk'), $e->getMessage()))); 1396 } 1397 } 1398 1399 /** 1400 * Manual Google email fetch for testing 1401 */ 1402 public static function manual_fetch_google_emails() { 1403 check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce'); 1404 1405 if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) { 1406 wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk'))); 1407 } 1408 1409 try { 1410 $email_pipe_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/nexlifydesk-email-pipe.php'; 1411 if (file_exists($email_pipe_path)) { 1412 require_once $email_pipe_path; 1413 } 1414 1415 if (function_exists('nexlifydesk_fetch_google_emails')) { 1416 nexlifydesk_fetch_google_emails(); 1417 } else { 1418 throw new Exception('Google email fetch function not found'); 1419 } 1420 1421 global $wpdb; 1422 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe and not user input 1423 $table_name = $wpdb->prefix . 'nexlifydesk_tickets'; 1424 $recent_tickets_cache_key = 'nexlifydesk_recent_google_tickets_' . md5(gmdate('Y-m-d H:i:s', strtotime('-2 minutes'))); 1425 $recent_tickets = wp_cache_get($recent_tickets_cache_key); 1426 1427 if (false === $recent_tickets) { 1428 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and not user input 1429 $recent_tickets = $wpdb->get_results( 1430 $wpdb->prepare( 1431 "SELECT ticket_id, subject, source, created_at 1432 FROM {$table_name} 1433 WHERE created_at >= %s 1434 AND source = 'email' 1435 ORDER BY created_at DESC 1436 LIMIT 10", 1437 gmdate('Y-m-d H:i:s', strtotime('-2 minutes')) 1438 ) 1439 ); 1440 wp_cache_set($recent_tickets_cache_key, $recent_tickets, '', 120); 1441 } 1442 1443 $message = "Google email fetch completed successfully!\n"; 1444 1445 if (!empty($recent_tickets)) { 1446 $message .= "Found " . count($recent_tickets) . " recent email ticket(s):\n"; 1447 foreach ($recent_tickets as $ticket) { 1448 $message .= "- #{$ticket->ticket_id}: {$ticket->subject} ({$ticket->created_at})\n"; 1449 } 1450 } else { 1451 $message .= "No new email tickets found. This could mean:\n"; 1452 $message .= "- No new emails in Gmail\n"; 1453 $message .= "- Emails are being filtered out (spam, duplicates)\n"; 1454 $message .= "- All emails have been processed already\n"; 1455 } 1456 1457 wp_send_json_success(array('message' => $message)); 1458 1459 } catch (Exception $e) { 1460 /* translators: %s: Error message returned from the Google email fetch */ 1461 wp_send_json_error(array('message' => sprintf(__('Google email fetch failed: %s', 'nexlifydesk'), $e->getMessage()))); 1462 } 1463 } 1464 1465 /** 1466 * Get the maximum allowed POST size from server configuration 1467 * 1468 * @return int Maximum POST size in bytes 1469 */ 1470 private static function get_max_post_size() { 1471 $post_max_size = ini_get('post_max_size'); 1472 $upload_max_filesize = ini_get('upload_max_filesize'); 1473 $post_max_bytes = self::convert_to_bytes($post_max_size); 1474 $upload_max_bytes = self::convert_to_bytes($upload_max_filesize); 1475 1476 return min($post_max_bytes, $upload_max_bytes); 1477 } 1478 1479 /** 1480 * Convert PHP ini size format to bytes 1481 * 1482 * @param string $size Size string like "8M", "1G", "512K" 1483 * @return int Size in bytes 1484 */ 1485 private static function convert_to_bytes($size) { 1486 $size = trim($size); 1487 $last = strtolower($size[strlen($size) - 1]); 1488 $size = (int) $size; 1489 1490 switch ($last) { 1491 case 'g': 1492 $size *= 1024; 1493 // Fall through 1494 case 'm': 1495 $size *= 1024; 1496 // Fall through 1497 case 'k': 1498 $size *= 1024; 1499 break; 1500 } 1501 1502 return $size; 1503 } 727 1504 } -
nexlifydesk/trunk/includes/class-nexlifydesk-database.php
r3326104 r3330741 10 10 global $wpdb; 11 11 self::$tables = array( 12 'tickets' => $wpdb->prefix . 'nexlifydesk_tickets', 13 'replies' => $wpdb->prefix . 'nexlifydesk_replies', 14 'attachments' => $wpdb->prefix . 'nexlifydesk_attachments', 15 'categories' => $wpdb->prefix . 'nexlifydesk_categories' 12 'tickets' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'tickets', 13 'replies' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'replies', 14 'attachments' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'attachments', 15 'categories' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'categories', 16 'ticket_reads' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'ticket_reads' 16 17 ); 17 18 } … … 24 25 'replies' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'replies', 25 26 'attachments' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'attachments', 26 'categories' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'categories' 27 'categories' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'categories', 28 'ticket_reads' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'ticket_reads' 27 29 ); 28 30 … … 42 44 priority varchar(20) NOT NULL DEFAULT 'medium', 43 45 assigned_to bigint(20) DEFAULT NULL, 46 source varchar(20) NOT NULL DEFAULT 'web', 44 47 created_at datetime NOT NULL, 45 48 updated_at datetime NOT NULL, … … 88 91 ) $charset_collate;"; 89 92 93 // Ticket reads table 94 $sql .= "CREATE TABLE " . self::$tables['ticket_reads'] . " ( 95 id bigint(20) NOT NULL AUTO_INCREMENT, 96 ticket_id bigint(20) NOT NULL, 97 user_id bigint(20) NOT NULL, 98 read_at datetime NOT NULL, 99 PRIMARY KEY (id), 100 UNIQUE KEY ticket_user (ticket_id, user_id), 101 KEY ticket_id (ticket_id), 102 KEY user_id (user_id) 103 ) $charset_collate;"; 104 90 105 $result = dbDelta($sql); 91 106 92 // Verifying if the tables were created 93 $tables_to_check = array('tickets', 'replies', 'attachments', 'categories'); 107 $tables_to_check = array('tickets', 'replies', 'attachments', 'categories', 'ticket_reads'); 94 108 foreach ($tables_to_check as $table_key) { 95 109 $table_name = self::$tables[$table_key]; … … 107 121 add_option('nexlifydesk_settings', array( 108 122 'email_notifications' => 1, 123 'admin_email_notifications' => 1, 109 124 'default_priority' => 'medium', 110 125 'auto_assign' => 0, … … 178 193 179 194 public static function check_and_run_migrations() { 180 $current_version = get_option('nexlifydesk_db_version', '1.0. 0');195 $current_version = get_option('nexlifydesk_db_version', '1.0.1'); 181 196 $plugin_version = NEXLIFYDESK_VERSION; 182 197 183 if (version_compare($current_version, '1.0. 0', '<')) {198 if (version_compare($current_version, '1.0.1', '<')) { 184 199 self::migrate_to_1_0_1(); 185 update_option('nexlifydesk_db_version', '1.0.0'); 200 update_option('nexlifydesk_db_version', '1.0.1'); 201 } 202 203 if (version_compare($current_version, '1.0.2', '<')) { 204 self::migrate_to_1_0_2(); 205 update_option('nexlifydesk_db_version', '1.0.2'); 186 206 } 187 207 … … 194 214 private static function migrate_to_1_0_1() { 195 215 global $wpdb; 216 217 // Ensure tables array is initialized 218 if (empty(self::$tables)) { 219 self::init(); 220 } 196 221 197 222 $table_name = self::$tables['replies']; … … 229 254 } 230 255 } 256 257 private static function migrate_to_1_0_2() { 258 global $wpdb; 259 260 if (empty(self::$tables)) { 261 self::init(); 262 } 263 264 $table_name = self::$tables['ticket_reads']; 265 266 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Migration check 267 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name; 268 269 if (!$table_exists) { 270 $charset_collate = $wpdb->get_charset_collate(); 271 272 $sql = "CREATE TABLE {$table_name} ( 273 id bigint(20) NOT NULL AUTO_INCREMENT, 274 ticket_id bigint(20) NOT NULL, 275 user_id bigint(20) NOT NULL, 276 read_at datetime NOT NULL, 277 PRIMARY KEY (id), 278 UNIQUE KEY ticket_user (ticket_id, user_id), 279 KEY ticket_id (ticket_id), 280 KEY user_id (user_id) 281 ) {$charset_collate};"; 282 283 require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); 284 dbDelta($sql); 285 } 286 } 287 288 public static function upgrade_database() { 289 global $wpdb; 290 291 $current_version = get_option('nexlifydesk_version', '1.0.1'); 292 293 if ($current_version === '1.0.1' && !get_option('nexlifydesk_db_version')) { 294 return; 295 } 296 297 if (empty(self::$tables)) { 298 self::init(); 299 } 300 301 $tickets_table = self::$tables['tickets']; 302 303 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table existence check during upgrade, no caching needed 304 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tickets_table)) === $tickets_table; 305 306 if (!$table_exists) { 307 return; 308 } 309 310 if (version_compare($current_version, NEXLIFYDESK_VERSION, '<')) { 311 $tickets_table_escaped = esc_sql($tickets_table); 312 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped and safe 313 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema check during upgrade, no caching needed 314 $column_exists = $wpdb->get_results( 315 $wpdb->prepare( 316 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped and safe 317 "SHOW COLUMNS FROM `{$tickets_table_escaped}` LIKE %s", 318 'source' 319 ) 320 ); 321 322 if (empty($column_exists)) { 323 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Schema change during plugin upgrade, table name is safe and not user input, no caching needed 324 $wpdb->query("ALTER TABLE {$tickets_table} ADD COLUMN source varchar(20) NOT NULL DEFAULT 'web' AFTER assigned_to"); 325 } 326 327 // Update version 328 update_option('nexlifydesk_version', NEXLIFYDESK_VERSION); 329 } 330 } 231 331 } -
nexlifydesk/trunk/includes/class-nexlifydesk-tickets.php
r3326104 r3330741 25 25 'priority' => 'medium', 26 26 'status' => 'open', 27 'source' => 'web', 27 28 'attachments' => array() 28 29 ); … … 34 35 } 35 36 36 // Checking for duplicate tickets 37 $user_id = $data['user_id']; 38 $email_address = ''; 39 40 if ($user_id > 0) { 41 $user = get_userdata($user_id); 42 $email_address = $user ? $user->user_email : ''; 43 } else { 44 if (isset($data['email'])) { 45 $email_address = $data['email']; 46 } else { 47 if (function_exists('nexlifydesk_extract_customer_details')) { 48 $customer_details = nexlifydesk_extract_customer_details($data['message']); 49 $email_address = $customer_details['email'] ?: '[email protected]'; 50 } else { 51 $email_address = '[email protected]'; 52 } 53 } 54 } 55 56 $skip_rate_limit = false; 57 if ($user_id > 0) { 58 $user = get_userdata($user_id); 59 if ($user && (in_array('administrator', $user->roles) || in_array('nexlifydesk_agent', $user->roles))) { 60 $skip_rate_limit = true; 61 } 62 } 63 64 if (!$skip_rate_limit && class_exists('NexlifyDesk_Rate_Limiter')) { 65 if (NexlifyDesk_Rate_Limiter::is_rate_limited($user_id, $email_address)) { 66 $error_message = NexlifyDesk_Rate_Limiter::get_rate_limit_error_message($user_id, $email_address); 67 return new WP_Error('rate_limit_exceeded', $error_message); 68 } 69 } 70 71 // Add email to data array for duplicate checking 72 $data['email'] = $email_address; 73 74 // For non-registered users, add a small delay to avoid race conditions with concurrent email processing 75 if (!empty($data['source']) && $data['source'] === 'email' && $user_id == 0) { 76 // Add a micro-delay to reduce race conditions in email processing 77 usleep(100000); // 100ms delay 78 } 79 37 80 $existing_ticket = self::check_for_duplicate_ticket($data); 38 81 if ($existing_ticket) { 39 // Add new message as a reply to the existing ticket40 82 $reply_data = array( 41 83 'ticket_id' => $existing_ticket->id, … … 49 91 50 92 if (!is_wp_error($reply_id)) { 51 // Update the existing ticket's updated_at timestamp52 93 $current_time = current_time('mysql'); 53 94 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query … … 60 101 ); 61 102 62 // Return the existing ticket with a flag indicating it's a duplicate63 103 $existing_ticket->is_duplicate = true; 64 104 $existing_ticket->new_reply_id = $reply_id; … … 80 120 'priority' => sanitize_text_field($data['priority']), 81 121 'status' => sanitize_text_field($data['status']), 122 'source' => sanitize_text_field($data['source']), 82 123 'created_at' => $current_time, 83 124 'updated_at' => $current_time 84 125 ), 85 array('%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' )126 array('%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s') 86 127 ); 87 128 … … 92 133 $new_ticket_id = $wpdb->insert_id; 93 134 135 // Store customer email as post meta for both web and email tickets 136 if (!empty($data['email'])) { 137 update_post_meta($new_ticket_id, 'customer_email', $data['email']); 138 } 139 94 140 if (!empty($data['attachments'])) { 95 141 self::handle_attachments($data['attachments'], $new_ticket_id, null, $data['user_id']); … … 98 144 $ticket = self::get_ticket($new_ticket_id); 99 145 100 register_shutdown_function(function() use ($ticket) { 101 NexlifyDesk_Tickets::send_notification($ticket, 'new_ticket'); 146 if (!$skip_rate_limit && class_exists('NexlifyDesk_Rate_Limiter')) { 147 NexlifyDesk_Rate_Limiter::record_ticket_creation($user_id, $email_address); 148 } 149 150 register_shutdown_function(function() use ($ticket, $data) { 151 // Different notification handling for email vs web channels 152 if (!empty($data['source']) && $data['source'] === 'email') { 153 // Send simple auto-response for email tickets 154 NexlifyDesk_Tickets::send_email_channel_notification($ticket, 'new_ticket'); 155 } else { 156 // Send standard notifications for web tickets 157 NexlifyDesk_Tickets::send_notification($ticket, 'new_ticket'); 158 } 102 159 }); 103 160 … … 125 182 } 126 183 184 wp_cache_flush_group('nexlifydesk_tickets_grid'); 185 127 186 return $ticket; 128 187 } … … 239 298 'message' => '', 240 299 'is_admin_reply' => current_user_can('manage_options'), 241 'is_internal_note' => false 300 'is_internal_note' => false, 301 'source' => 'web' 242 302 ); 243 303 … … 246 306 if (empty($data['message'])) { 247 307 return new WP_Error('missing_message', __('Message is required.', 'nexlifydesk')); 308 } 309 310 if ($data['source'] === 'web' && $data['user_id'] > 0) { 311 $user = get_userdata($data['user_id']); 312 if ($user && (in_array('administrator', $user->roles) || in_array('nexlifydesk_agent', $user->roles))) { 313 // Admin/agent reply from web interface 314 } 248 315 } 249 316 … … 283 350 $reply_id = $wpdb->insert_id; 284 351 352 if (!empty($data['source']) && $data['source'] === 'email' && class_exists('NexlifyDesk_Rate_Limiter')) { 353 $email_address = ''; 354 if ($data['user_id'] > 0) { 355 $user = get_userdata($data['user_id']); 356 $email_address = $user ? $user->user_email : ''; 357 } else { 358 $ticket = self::get_ticket($data['ticket_id']); 359 if ($ticket) { 360 $email_address = get_post_meta($ticket->id, 'customer_email', true); 361 } 362 } 363 364 if (!empty($email_address)) { 365 NexlifyDesk_Rate_Limiter::record_email_activity($data['user_id'], $email_address); 366 } 367 } 368 285 369 if (!empty($data['attachments'])) { 286 370 self::handle_attachments($data['attachments'], $data['ticket_id'], $reply_id, $data['user_id']); … … 289 373 $ticket = self::get_ticket($data['ticket_id']); 290 374 291 // Send notification email292 375 if (!$data['is_internal_note']) { 293 register_shutdown_function(function() use ($ticket, $reply_id) { 294 NexlifyDesk_Tickets::send_notification($ticket, 'new_reply', $reply_id); 376 register_shutdown_function(function() use ($ticket, $reply_id, $data) { 377 if (!empty($data['source']) && $data['source'] === 'email') { 378 NexlifyDesk_Tickets::send_email_channel_notification($ticket, 'new_reply', $reply_id); 379 } else { 380 NexlifyDesk_Tickets::send_notification($ticket, 'new_reply', $reply_id); 381 } 295 382 }); 296 383 } 384 385 wp_cache_flush_group('nexlifydesk_tickets_grid'); 297 386 298 387 return $reply_id; … … 354 443 355 444 if (isset($attachments['name']) && is_array($attachments['name'])) { 445 // Legacy multi-file format from $_FILES 356 446 $file_count = count($attachments['name']); 357 447 … … 369 459 ); 370 460 371 self::process_single_attachment($file_data, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types);461 $result = self::process_single_attachment($file_data, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types); 372 462 } 373 463 } else { 374 if (isset($attachments['name']) && !is_array($attachments['name'])) { 375 self::process_single_attachment($attachments, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types); 376 } else { 377 foreach ($attachments as $attachment) { 378 if (is_array($attachment) && isset($attachment['name'])) { 379 self::process_single_attachment($attachment, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types); 380 } 464 // Process each attachment individually (new format from AJAX) 465 foreach ($attachments as $attachment) { 466 if (is_array($attachment) && isset($attachment['name'])) { 467 $result = self::process_single_attachment($attachment, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types); 381 468 } 382 469 } … … 385 472 386 473 private static function process_single_attachment($attachment, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types) { 474 387 475 if ($attachment['size'] > $max_size) { 388 476 return false; … … 423 511 424 512 $upload_result = wp_handle_upload($file_data, array('test_form' => false)); 425 513 426 514 if (!isset($upload_result['error']) && isset($upload_result['url'])) { 427 515 global $wpdb; 428 516 517 $table_name = NexlifyDesk_Database::get_table('attachments'); 429 518 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 430 519 $result = $wpdb->insert( 431 NexlifyDesk_Database::get_table('attachments'),520 $table_name, 432 521 array( 433 522 'ticket_id' => $ticket_id, … … 465 554 $query .= " AND reply_id IS NULL"; 466 555 } 556 467 557 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled 468 558 $results = $wpdb->get_results( … … 573 663 } 574 664 665 // Clear ticket grid cache when status is updated 666 wp_cache_flush_group('nexlifydesk_tickets_grid'); 667 575 668 return true; 576 669 } … … 585 678 $user = get_userdata($ticket->user_id); 586 679 $admin_email = get_option('admin_email'); 587 $headers = array('Content-Type: text/html; charset=UTF-8'); 680 681 // Check if admin notifications are enabled 682 $admin_notifications_enabled = !empty($settings['admin_email_notifications']); 683 684 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 685 $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest'); 686 $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: null); 687 688 $headers = array( 689 'Content-Type: text/html; charset=UTF-8', 690 'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>', 691 'Message-ID: <ticket-' . $ticket->ticket_id . '-' . time() . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>', 692 'In-Reply-To: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>', 693 'References: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>' 694 ); 588 695 589 696 $ticket_url = add_query_arg( … … 603 710 switch ($type) { 604 711 case 'new_ticket': 605 // translators: %s: Ticket ID.606 $subject = sprintf(__('New Support Ticket #%s', 'nexlifydesk'), $ticket->ticket_id);712 // translators: 1: Ticket ID, 2: Ticket subject. 713 $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 607 714 $message = self::get_email_template('new_ticket', $ticket); 608 715 $message = str_replace( … … 613 720 614 721 // Sending to customer 615 if ($user && !in_array($user->user_email, $emailed)) { 616 wp_mail($user->user_email, $subject, $message, $headers); 617 $emailed[] = $user->user_email; 618 } 619 620 // Sending to admin 621 if (!in_array($admin_email, $emailed)) { 622 wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers); 623 $emailed[] = $admin_email; 624 } 625 626 // Sending to assigned agent if exists and not already emailed 722 if ($customer_email && !in_array($customer_email, $emailed)) { 723 wp_mail($customer_email, $subject, $message, $headers); 724 $emailed[] = $customer_email; 725 } 726 727 // Sending to assigned agent if exists 627 728 if (!empty($ticket->assigned_to)) { 628 729 $agent = get_userdata($ticket->assigned_to); … … 632 733 } 633 734 } 735 736 // Send to admin only if: 737 // 1. Admin notifications are enabled AND 738 // 2. Either no agent is assigned OR the ticket is assigned to admin 739 if ($admin_notifications_enabled && !in_array($admin_email, $emailed)) { 740 $should_notify_admin = false; 741 742 if (empty($ticket->assigned_to)) { 743 // No agent assigned, notify admin 744 $should_notify_admin = true; 745 } else { 746 // Check if assigned to admin 747 $assigned_user = get_userdata($ticket->assigned_to); 748 if ($assigned_user && in_array('administrator', $assigned_user->roles)) { 749 $should_notify_admin = true; 750 } 751 } 752 753 if ($should_notify_admin) { 754 wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers); 755 $emailed[] = $admin_email; 756 } 757 } 634 758 break; 635 759 … … 644 768 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 645 769 $reply = $wpdb->get_row($wpdb->prepare( 646 "SELECT is_internal_note FROM `{$wpdb->prefix}nexlifydesk_replies` WHERE id = %d",770 "SELECT user_id, is_internal_note FROM `{$wpdb->prefix}nexlifydesk_replies` WHERE id = %d", 647 771 $reply_id 648 772 )); … … 655 779 } 656 780 657 // translators: %s: Ticket ID.658 $subject = sprintf(__(' New Reply to Ticket #%s', 'nexlifydesk'), $ticket->ticket_id);781 // translators: 1: Ticket ID, 2: Ticket subject. 782 $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 659 783 $message = self::get_email_template('new_reply', $ticket, $reply_id); 660 784 $message = str_replace( … … 664 788 ); 665 789 666 if ($ticket->user_id != get_current_user_id()) { 667 if ($user && !in_array($user->user_email, $emailed)) { 668 wp_mail($user->user_email, $subject, $message, $headers); 669 $emailed[] = $user->user_email; 790 $reply_user_id = $reply ? $reply->user_id : get_current_user_id(); 791 $reply_user = get_userdata($reply_user_id); 792 $is_agent_reply = $reply_user && (in_array('administrator', $reply_user->roles) || in_array('nexlifydesk_agent', $reply_user->roles)); 793 794 if ($is_agent_reply) { 795 if ($customer_email && !in_array($customer_email, $emailed)) { 796 wp_mail($customer_email, $subject, $message, $headers); 797 $emailed[] = $customer_email; 670 798 } 671 799 } else { … … 673 801 $agent = get_userdata($ticket->assigned_to); 674 802 if ($agent && !in_array($agent->user_email, $emailed)) { 675 wp_mail($agent->user_email, $subject, $message, $headers);803 wp_mail($agent->user_email, '[Agent] ' . $subject, $message, $headers); 676 804 $emailed[] = $agent->user_email; 677 805 } 678 } else {806 } else if ($admin_notifications_enabled) { 679 807 if (!in_array($admin_email, $emailed)) { 680 wp_mail($admin_email, $subject, $message, $headers);808 wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers); 681 809 $emailed[] = $admin_email; 682 810 } … … 686 814 687 815 case 'status_changed': 688 // translators: %s: Ticket ID.689 $subject = sprintf(__(' Ticket #%s Status Changed', 'nexlifydesk'), $ticket->ticket_id);816 // translators: 1: Ticket ID, 2: Ticket subject. 817 $subject = sprintf(__('[#%1$s] %2$s - Status Changed', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 690 818 $message = self::get_email_template('status_changed', $ticket); 691 819 $message = str_replace( … … 694 822 $message 695 823 ); 696 if ($user && !in_array($user->user_email, $emailed)) { 697 wp_mail($user->user_email, $subject, $message, $headers); 698 $emailed[] = $user->user_email; 824 825 if ($customer_email && !in_array($customer_email, $emailed)) { 826 wp_mail($customer_email, $subject, $message, $headers); 827 $emailed[] = $customer_email; 828 } 829 830 if (!empty($ticket->assigned_to)) { 831 $agent = get_userdata($ticket->assigned_to); 832 if ($agent && !in_array($agent->user_email, $emailed)) { 833 wp_mail($agent->user_email, '[Agent] ' . $subject, $message, $headers); 834 $emailed[] = $agent->user_email; 835 } 836 } 837 838 if ($admin_notifications_enabled && !in_array($admin_email, $emailed)) { 839 $should_notify_admin = false; 840 841 if (empty($ticket->assigned_to)) { 842 $should_notify_admin = true; 843 } else { 844 $assigned_user = get_userdata($ticket->assigned_to); 845 if ($assigned_user && in_array('administrator', $assigned_user->roles)) { 846 $should_notify_admin = true; 847 } 848 } 849 850 if ($should_notify_admin) { 851 wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers); 852 $emailed[] = $admin_email; 853 } 699 854 } 700 855 break; 701 856 702 857 case 'sla_breach': 703 // translators: %s: Ticket ID.704 $subject = sprintf(__('SLA Breach: Ticket #%s', 'nexlifydesk'), $ticket->ticket_id);858 // translators: 1: Ticket ID, 2: Ticket subject. 859 $subject = sprintf(__('[#%1$s] SLA Breach: %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 705 860 $message = self::get_email_template('sla_breach', $ticket); 706 861 $message = str_replace( … … 709 864 $message 710 865 ); 866 711 867 if (!in_array($admin_email, $emailed)) { 712 wp_mail($admin_email, $subject, $message, $headers);868 wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers); 713 869 $emailed[] = $admin_email; 870 } 871 872 if (!empty($ticket->assigned_to)) { 873 $agent = get_userdata($ticket->assigned_to); 874 if ($agent && !in_array($agent->user_email, $emailed)) { 875 wp_mail($agent->user_email, '[Agent] ' . $subject, $message, $headers); 876 $emailed[] = $agent->user_email; 877 } 714 878 } 715 879 break; … … 728 892 729 893 $user = get_userdata($ticket->user_id); 894 895 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 896 $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest'); 897 $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: ''); 898 $clean_message = $customer_details['message'] ?: $ticket->message; 899 730 900 $placeholders = array( 731 901 '{ticket_id}' => esc_html($ticket->ticket_id), 732 902 '{subject}' => esc_html($ticket->subject), 733 '{message}' => wp _kses_post($ticket->message),734 '{user_name}' => $user ? esc_html($user->display_name) : '',735 '{user_email}' => $user ? esc_html($user->user_email) : '',903 '{message}' => wpautop(wp_kses_post($clean_message)), 904 '{user_name}' => esc_html($customer_name), 905 '{user_email}' => esc_html($customer_email), 736 906 '{status}' => esc_html(ucfirst($ticket->status)), 737 907 '{priority}' => esc_html(ucfirst($ticket->priority)), … … 766 936 } 767 937 if ($reply) { 768 $placeholders['{reply_message}'] = $reply->message; 938 $reply_customer_details = nexlifydesk_extract_customer_details($reply->message); 939 $clean_reply_message = $reply_customer_details['message'] ?: $reply->message; 940 941 $clean_reply_message = wp_kses_post($clean_reply_message); 942 $clean_reply_message = wpautop($clean_reply_message); 943 944 $placeholders['{reply_message}'] = $clean_reply_message; 769 945 $reply_user = get_userdata($reply->user_id); 770 $placeholders['{reply_user_name}'] = $reply_user ? $reply_user->display_name : ''; 946 $reply_customer_name = $reply_user ? $reply_user->display_name : ($reply_customer_details['name'] ?: 'Guest'); 947 $placeholders['{reply_user_name}'] = esc_html($reply_customer_name); 771 948 } 772 949 } … … 775 952 776 953 return $content; 954 } 955 956 /** 957 * Send email notifications for tickets created via email channel 958 * Simple auto-response for new tickets, raw email replies for ongoing conversation 959 * 960 * @param object $ticket The ticket object 961 * @param string $type The notification type ('new_ticket' or 'new_reply') 962 * @param int $reply_id Optional reply ID for reply notifications 963 */ 964 public static function send_email_channel_notification($ticket, $type, $reply_id = null) { 965 $settings = get_option('nexlifydesk_settings'); 966 if (empty($settings['email_notifications'])) return; 967 968 $admin_email = get_option('admin_email'); 969 $admin_notifications_enabled = !empty($settings['admin_email_notifications']); 970 971 $customer_email = get_post_meta($ticket->id, 'customer_email', true); 972 if (empty($customer_email)) { 973 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 974 $customer_email = $customer_details['email'] ?: null; 975 } 976 977 if (empty($customer_email)) { 978 return; 979 } 980 981 $headers = array( 982 'Content-Type: text/html; charset=UTF-8', 983 'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>', 984 'Message-ID: <ticket-' . $ticket->ticket_id . '-' . time() . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>', 985 'In-Reply-To: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>', 986 'References: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>' 987 ); 988 989 switch ($type) { 990 case 'new_ticket': 991 self::send_auto_response($ticket, $customer_email, $headers); 992 993 self::notify_admin_about_email_ticket($ticket, $admin_email, $headers, $admin_notifications_enabled); 994 break; 995 996 case 'new_reply': 997 self::send_raw_email_reply($ticket, $reply_id, $customer_email, $headers); 998 break; 999 } 1000 } 1001 1002 /** 1003 * Send auto-response to customer for new email tickets 1004 */ 1005 private static function send_auto_response($ticket, $customer_email, $headers) { 1006 $templates = get_option('nexlifydesk_email_templates', array()); 1007 1008 $auto_response_message = isset($templates['email_auto_response']) && !empty($templates['email_auto_response']) 1009 ? $templates['email_auto_response'] 1010 : self::get_default_auto_response(); 1011 1012 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 1013 $customer_name = $customer_details['name'] ?: 'Customer'; 1014 1015 $placeholders = array( 1016 '{customer_name}' => esc_html($customer_name), 1017 '{ticket_id}' => esc_html($ticket->ticket_id), 1018 '{subject}' => esc_html($ticket->subject), 1019 '{site_name}' => esc_html(get_bloginfo('name')), 1020 '{site_url}' => esc_url(home_url()), 1021 '{admin_email}' => esc_html(get_option('admin_email')) 1022 ); 1023 1024 $message = strtr($auto_response_message, $placeholders); 1025 1026 // translators: 1: Ticket ID, 2: Ticket subject. 1027 $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 1028 1029 wp_mail($customer_email, $subject, $message, $headers); 1030 } 1031 1032 /** 1033 * Send raw email reply to customer (as if agent is replying directly) 1034 */ 1035 private static function send_raw_email_reply($ticket, $reply_id, $customer_email, $headers) { 1036 global $wpdb; 1037 1038 if (!$reply_id) return; 1039 1040 $reply_cache_key = 'nexlifydesk_reply_' . intval($reply_id); 1041 $reply = wp_cache_get($reply_cache_key); 1042 if (false === $reply) { 1043 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1044 $reply = $wpdb->get_row($wpdb->prepare( 1045 "SELECT r.*, u.display_name, u.user_email 1046 FROM `{$wpdb->prefix}nexlifydesk_replies` r 1047 LEFT JOIN {$wpdb->users} u ON r.user_id = u.ID 1048 WHERE r.id = %d", 1049 $reply_id 1050 )); 1051 wp_cache_set($reply_cache_key, $reply, '', 300); 1052 } 1053 1054 if (!$reply) return; 1055 1056 $reply_user = get_userdata($reply->user_id); 1057 $is_agent_reply = $reply_user && ( 1058 in_array('administrator', $reply_user->roles) || 1059 in_array('nexlifydesk_agent', $reply_user->roles) 1060 ); 1061 1062 if (!$is_agent_reply) { 1063 $settings = get_option('nexlifydesk_settings'); 1064 $admin_notifications_enabled = !empty($settings['admin_email_notifications']); 1065 self::notify_admin_about_email_reply($ticket, $reply, $headers, $admin_notifications_enabled); 1066 return; 1067 } 1068 1069 $clean_message = $reply->message; 1070 $clean_message = preg_replace('/\[Customer Details\].*?\[Message\]/s', '', $clean_message); 1071 $clean_message = preg_replace('/\[Customer Details\].*?\[Reply\]/s', '', $clean_message); 1072 $clean_message = trim($clean_message); 1073 1074 if (strpos($clean_message, '<') !== false) { 1075 $clean_message = wp_strip_all_tags($clean_message); 1076 $clean_message = html_entity_decode($clean_message, ENT_QUOTES, 'UTF-8'); 1077 } 1078 1079 $from_name = $reply_user->display_name ?: get_bloginfo('name'); 1080 $from_email = $reply_user->user_email ?: get_option('admin_email'); 1081 $headers[1] = 'From: ' . $from_name . ' <' . $from_email . '>'; 1082 // translators: 1: Ticket ID, 2: Ticket subject. 1083 $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 1084 1085 wp_mail($customer_email, $subject, $clean_message, $headers); 1086 } 1087 1088 /** 1089 * Notify admin/agents about new email ticket 1090 */ 1091 private static function notify_admin_about_email_ticket($ticket, $admin_email, $headers, $admin_notifications_enabled = true) { 1092 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 1093 $customer_name = $customer_details['name'] ?: 'Unknown Customer'; 1094 $customer_email = $customer_details['email'] ?: 'Unknown Email'; 1095 $clean_message = $customer_details['message'] ?: $ticket->message; 1096 1097 $is_html = false; 1098 if (is_array($headers)) { 1099 foreach ($headers as $header) { 1100 if (stripos($header, 'Content-Type: text/html') !== false) { 1101 $is_html = true; 1102 break; 1103 } 1104 } 1105 } else { 1106 $is_html = (stripos($headers, 'Content-Type: text/html') !== false); 1107 } 1108 1109 $formatted_message = $clean_message; 1110 if ($is_html) { 1111 $formatted_message = nl2br(esc_html($formatted_message)); 1112 } else { 1113 $formatted_message = str_replace(array("\r\n", "\r"), "\n", $formatted_message); 1114 } 1115 1116 if ($is_html) { 1117 $admin_message = sprintf( 1118 "<strong>New support ticket received via email:</strong><br><br>" . 1119 "<strong>Ticket ID:</strong> %s<br>" . 1120 "<strong>From:</strong> %s <%s><br>" . 1121 "<strong>Subject:</strong> %s<br>" . 1122 "<strong>Priority:</strong> %s<br><br>" . 1123 "<strong>Message:</strong><br>%s<br><br>" . 1124 "<a href=\"%s\">View ticket</a>", 1125 esc_html($ticket->ticket_id), 1126 esc_html($customer_name), 1127 esc_html($customer_email), 1128 esc_html($ticket->subject), 1129 esc_html(ucfirst($ticket->priority)), 1130 $formatted_message, 1131 esc_url(add_query_arg( 1132 array( 1133 'page' => 'nexlifydesk_tickets', 1134 'ticket_id' => $ticket->id, 1135 ), 1136 admin_url('admin.php') 1137 )) 1138 ); 1139 } else { 1140 $admin_message = sprintf( 1141 "New support ticket received via email:\n\n" . 1142 "Ticket ID: %s\n" . 1143 "From: %s <%s>\n" . 1144 "Subject: %s\n" . 1145 "Priority: %s\n\n" . 1146 "Message:\n%s\n\n" . 1147 "View ticket: %s", 1148 $ticket->ticket_id, 1149 $customer_name, 1150 $customer_email, 1151 $ticket->subject, 1152 ucfirst($ticket->priority), 1153 $formatted_message, 1154 add_query_arg( 1155 array( 1156 'page' => 'nexlifydesk_tickets', 1157 'ticket_id' => $ticket->id, 1158 ), 1159 admin_url('admin.php') 1160 ) 1161 ); 1162 } 1163 1164 // translators: 1: Ticket ID, 2: Ticket subject. 1165 $subject = sprintf(__('[New Email Ticket] [#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 1166 1167 // Notify admin only if admin notifications are enabled and conditions are met 1168 if ($admin_notifications_enabled) { 1169 $should_notify_admin = false; 1170 1171 if (empty($ticket->assigned_to)) { 1172 $should_notify_admin = true; 1173 } else { 1174 $assigned_user = get_userdata($ticket->assigned_to); 1175 if ($assigned_user && in_array('administrator', $assigned_user->roles)) { 1176 $should_notify_admin = true; 1177 } 1178 } 1179 1180 if ($should_notify_admin) { 1181 wp_mail($admin_email, $subject, $admin_message, $headers); 1182 } 1183 } 1184 1185 if (!empty($ticket->assigned_to)) { 1186 $agent = get_userdata($ticket->assigned_to); 1187 if ($agent && $agent->user_email !== $admin_email) { 1188 wp_mail($agent->user_email, '[Agent] ' . $subject, $admin_message, $headers); 1189 } 1190 } 1191 } 1192 1193 /** 1194 * Notify admin/agents about customer email reply 1195 */ 1196 private static function notify_admin_about_email_reply($ticket, $reply, $headers, $admin_notifications_enabled = true) { 1197 $admin_email = get_option('admin_email'); 1198 1199 $customer_details = nexlifydesk_extract_customer_details($reply->message); 1200 $customer_name = $customer_details['name'] ?: 'Unknown Customer'; 1201 $customer_email = $customer_details['email'] ?: 'Unknown Email'; 1202 $clean_message = $customer_details['message'] ?: $reply->message; 1203 1204 $is_html = false; 1205 if (is_array($headers)) { 1206 foreach ($headers as $header) { 1207 if (stripos($header, 'Content-Type: text/html') !== false) { 1208 $is_html = true; 1209 break; 1210 } 1211 } 1212 } else { 1213 $is_html = (stripos($headers, 'Content-Type: text/html') !== false); 1214 } 1215 1216 $formatted_message = $clean_message; 1217 if ($is_html) { 1218 $formatted_message = nl2br(esc_html($formatted_message)); 1219 } else { 1220 $formatted_message = str_replace(array("\r\n", "\r"), "\n", $formatted_message); 1221 } 1222 1223 if ($is_html) { 1224 $admin_message = sprintf( 1225 "<strong>Customer replied to ticket via email:</strong><br><br>" . 1226 "<strong>Ticket ID:</strong> %s<br>" . 1227 "<strong>From:</strong> %s <%s><br>" . 1228 "<strong>Subject:</strong> %s<br><br>" . 1229 "<strong>Reply:</strong><br>%s<br><br>" . 1230 "<a href=\"%s\">View ticket</a>", 1231 esc_html($ticket->ticket_id), 1232 esc_html($customer_name), 1233 esc_html($customer_email), 1234 esc_html($ticket->subject), 1235 $formatted_message, 1236 esc_url(add_query_arg( 1237 array( 1238 'page' => 'nexlifydesk_tickets', 1239 'ticket_id' => $ticket->id, 1240 ), 1241 admin_url('admin.php') 1242 )) 1243 ); 1244 } else { 1245 $admin_message = sprintf( 1246 "Customer replied to ticket via email:\n\n" . 1247 "Ticket ID: %s\n" . 1248 "From: %s <%s>\n" . 1249 "Subject: %s\n\n" . 1250 "Reply:\n%s\n\n" . 1251 "View ticket: %s", 1252 $ticket->ticket_id, 1253 $customer_name, 1254 $customer_email, 1255 $ticket->subject, 1256 $formatted_message, 1257 add_query_arg( 1258 array( 1259 'page' => 'nexlifydesk_tickets', 1260 'ticket_id' => $ticket->id, 1261 ), 1262 admin_url('admin.php') 1263 ) 1264 ); 1265 } 1266 1267 // translators: 1: Ticket ID, 2: Ticket subject. 1268 $subject = sprintf(__('[Customer Reply] [#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject); 1269 1270 if ($admin_notifications_enabled) { 1271 $should_notify_admin = false; 1272 1273 if (empty($ticket->assigned_to)) { 1274 $should_notify_admin = true; 1275 } else { 1276 $assigned_user = get_userdata($ticket->assigned_to); 1277 if ($assigned_user && in_array('administrator', $assigned_user->roles)) { 1278 $should_notify_admin = true; 1279 } 1280 } 1281 1282 if ($should_notify_admin) { 1283 wp_mail($admin_email, $subject, $admin_message, $headers); 1284 } 1285 } 1286 1287 if (!empty($ticket->assigned_to)) { 1288 $agent = get_userdata($ticket->assigned_to); 1289 if ($agent && $agent->user_email !== $admin_email) { 1290 wp_mail($agent->user_email, '[Agent] ' . $subject, $admin_message, $headers); 1291 } 1292 } 1293 } 1294 1295 /** 1296 * Get default auto-response message 1297 */ 1298 private static function get_default_auto_response() { 1299 return "Hello {customer_name},\n\n" . 1300 "Thank you for contacting us. We have received your support request and have assigned it ticket ID #{ticket_id}.\n\n" . 1301 "Subject: {subject}\n\n" . 1302 "Our support team will review your request and get back to you as soon as possible. You can reference this ticket ID in any future correspondence.\n\n" . 1303 "Best regards,\n" . 1304 "{site_name} Support Team\n" . 1305 "{site_url}"; 777 1306 } 778 1307 … … 1060 1589 1061 1590 /** 1062 * Check for duplicate tickets based on subject, order number, or content similarity 1591 * Check for duplicate tickets based on simple rules: 1592 * - For non-registered users: Check if they have any active ticket (any status except closed/resolved) 1593 * - For registered users: Check for subject similarity only 1063 1594 * 1064 1595 * @param array $data Ticket data to check for duplicates … … 1068 1599 global $wpdb; 1069 1600 1601 // Clear relevant caches to ensure fresh duplicate detection 1070 1602 $user_id = isset($data['user_id']) ? absint($data['user_id']) : get_current_user_id(); 1603 $email = isset($data['email']) ? sanitize_email($data['email']) : ''; 1071 1604 $subject = sanitize_text_field($data['subject']); 1072 $message = wp_kses_post($data['message']); 1073 1074 // Get settings for duplicate detection 1605 $source = isset($data['source']) ? sanitize_text_field($data['source']) : ''; 1606 1075 1607 $settings = get_option('nexlifydesk_settings', array()); 1076 1608 $check_duplicates = isset($settings['check_duplicates']) ? $settings['check_duplicates'] : true; 1077 $duplicate_threshold = isset($settings['duplicate_threshold']) ? absint($settings['duplicate_threshold']) : 80; // 80% similarity1078 1609 1079 1610 if (!$check_duplicates) { … … 1081 1612 } 1082 1613 1083 // Check for exact subject match from same user in last 30 days1084 1614 $table_name = NexlifyDesk_Database::get_table('tickets'); 1085 // Use cache to avoid repeated direct DB queries 1086 $cache_key = 'nexlifydesk_duplicate_ticket_' . intval($user_id) . '_' . md5($subject); 1087 $existing_ticket = wp_cache_get($cache_key); 1088 1089 if (false === $existing_ticket) { 1615 1616 if ($user_id == 0 && !empty($email)) { 1617 1090 1618 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1091 1619 $existing_ticket = $wpdb->get_row($wpdb->prepare( 1092 "SELECT * FROM {$table_name}1093 WHERE user_id = %d1094 AND subject = %s1095 AND status NOT IN ('closed', 'resolved')1096 AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)1097 ORDER BY created_at DESC1620 "SELECT t.* FROM {$table_name} t 1621 LEFT JOIN {$wpdb->prefix}postmeta pm ON pm.post_id = t.id AND pm.meta_key = 'customer_email' 1622 WHERE t.user_id = 0 1623 AND pm.meta_value = %s 1624 AND t.status IN ('open', 'pending', 'in_progress') 1625 ORDER BY t.created_at DESC 1098 1626 LIMIT 1", 1099 $user_id, 1100 $subject 1627 $email 1101 1628 )); 1102 wp_cache_set($cache_key, $existing_ticket, '', 300); 1103 } 1104 1105 if ($existing_ticket) { 1106 return $existing_ticket; 1107 } 1108 1109 $order_patterns = array( 1110 '/order\s*#?\s*([a-zA-Z0-9\-_]+)/i', 1111 '/order\s*id\s*:?\s*([a-zA-Z0-9\-_]+)/i', 1112 '/order\s*number\s*:?\s*([a-zA-Z0-9\-_]+)/i', 1113 '/#([a-zA-Z0-9\-_]{6,})/i', 1114 '/invoice\s*#?\s*([a-zA-Z0-9\-_]+)/i' 1115 ); 1116 1117 foreach ($order_patterns as $pattern) { 1118 if (preg_match($pattern, $subject . ' ' . $message, $matches)) { 1119 $order_identifier = sanitize_text_field($matches[1]); 1120 1629 1630 if (!$existing_ticket) { 1631 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1632 $existing_ticket = $wpdb->get_row($wpdb->prepare( 1633 "SELECT t.* FROM {$table_name} t 1634 WHERE t.user_id = 0 1635 AND t.message LIKE %s 1636 AND t.status IN ('open', 'pending', 'in_progress') 1637 ORDER BY t.created_at DESC 1638 LIMIT 1", 1639 '%Email: ' . $email . '%' 1640 )); 1641 } 1642 1643 if ($existing_ticket) { 1644 return $existing_ticket; 1645 } 1646 } 1647 1648 if ($user_id > 0) { 1649 1650 $cache_key = 'nexlifydesk_registered_user_duplicate_' . intval($user_id) . '_' . md5($subject); 1651 $existing_ticket = wp_cache_get($cache_key); 1652 1653 if (false === $existing_ticket) { 1121 1654 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1122 1655 $existing_ticket = $wpdb->get_row($wpdb->prepare( 1123 1656 "SELECT * FROM {$table_name} 1124 1657 WHERE user_id = %d 1125 AND (subject LIKE %s OR message LIKE %s)1126 AND status NOT IN ('closed', 'resolved')1127 AND created_at > DATE_SUB(NOW(), INTERVAL 60 DAY)1658 AND subject = %s 1659 AND status IN ('open', 'pending', 'in_progress') 1660 AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY) 1128 1661 ORDER BY created_at DESC 1129 1662 LIMIT 1", 1130 1663 $user_id, 1131 '%' . $wpdb->esc_like($order_identifier) . '%', 1132 '%' . $wpdb->esc_like($order_identifier) . '%' 1664 $subject 1133 1665 )); 1134 1135 if ($existing_ticket) { 1136 return $existing_ticket; 1137 } 1138 } 1139 } 1140 1141 $subject_words = array_filter(explode(' ', strtolower($subject))); 1142 $message_words = array_filter(explode(' ', strtolower($message))); 1143 1144 $stop_words = array('the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'cannot', 'cant', 'wont', 'dont', 'doesnt', 'didnt', 'hasnt', 'havent', 'hadnt', 'wouldnt', 'couldnt', 'shouldnt', 'mightnt', 'mustnt', 'isnt', 'arent', 'wasnt', 'werent', 'a', 'an', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their'); 1145 1146 $subject_keywords = array_diff($subject_words, $stop_words); 1147 $message_keywords = array_diff($message_words, $stop_words); 1148 $all_keywords = array_merge($subject_keywords, $message_keywords); 1149 1150 if (count($all_keywords) >= 3) { 1666 wp_cache_set($cache_key, $existing_ticket, '', 300); 1667 } 1668 1669 if ($existing_ticket) { 1670 return $existing_ticket; 1671 } 1672 } 1673 1674 return false; 1675 } 1676 1677 /** 1678 * Get tickets for the admin grid view. 1679 * 1680 * @param array $args 1681 * @return array 1682 */ 1683 public static function get_tickets_for_grid($args = []) { 1684 global $wpdb; 1685 1686 $defaults = [ 1687 'status' => 'all', 1688 'priority' => 'all', 1689 'search' => '', 1690 'per_page' => 20, 1691 'offset' => 0, 1692 ]; 1693 $args = wp_parse_args($args, $defaults); 1694 1695 $where_clauses = ['1=1']; 1696 $query_params = []; 1697 1698 if ($args['status'] !== 'all') { 1699 $where_clauses[] = 't.status = %s'; 1700 $query_params[] = $args['status']; 1701 } 1702 if ($args['priority'] !== 'all') { 1703 $where_clauses[] = 't.priority = %s'; 1704 $query_params[] = $args['priority']; 1705 } 1706 if (!empty($args['search'])) { 1707 $search_term = '%' . $wpdb->esc_like($args['search']) . '%'; 1708 $where_clauses[] = '(t.subject LIKE %s OR t.message LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s OR t.ticket_id LIKE %s)'; 1709 $query_params[] = $search_term; 1710 $query_params[] = $search_term; 1711 $query_params[] = $search_term; 1712 $query_params[] = $search_term; 1713 $query_params[] = $search_term; 1714 } 1715 1716 $where_sql = implode(' AND ', $where_clauses); 1717 1718 $query_params[] = (int) $args['per_page']; 1719 $query_params[] = (int) $args['offset']; 1720 1721 $current_user_id = get_current_user_id(); 1722 1723 $cache_key = 'nexlifydesk_tickets_grid_v3_' . md5(serialize($args) . $current_user_id); 1724 $results = wp_cache_get($cache_key, 'nexlifydesk_tickets_grid'); 1725 if ($results === false) { 1151 1726 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1152 $recent_tickets = $wpdb->get_results($wpdb->prepare( 1153 "SELECT * FROM {$table_name} 1154 WHERE user_id = %d 1155 AND status NOT IN ('closed', 'resolved') 1156 AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY) 1157 ORDER BY created_at DESC 1158 LIMIT 20", 1159 $user_id 1160 )); 1161 1162 foreach ($recent_tickets as $ticket) { 1163 $ticket_content = strtolower($ticket->subject . ' ' . $ticket->message); 1164 $matching_keywords = 0; 1165 1166 foreach ($all_keywords as $keyword) { 1167 if (strlen($keyword) > 2 && strpos($ticket_content, $keyword) !== false) { 1168 $matching_keywords++; 1727 $results = $wpdb->get_results( 1728 $wpdb->prepare( 1729 "SELECT t.*, 1730 u.display_name as user_name, 1731 u.user_email as user_email, 1732 a.display_name as assigned_to_display_name, 1733 COALESCE(MAX(r.created_at), t.created_at) as last_reply_time, 1734 CASE 1735 WHEN NOT EXISTS ( 1736 SELECT 1 FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr 1737 WHERE tr.ticket_id = t.id AND tr.user_id = %d 1738 ) THEN 1 1739 WHEN EXISTS ( 1740 SELECT 1 FROM {$wpdb->prefix}nexlifydesk_replies r2 1741 WHERE r2.ticket_id = t.id 1742 AND r2.user_id != %d 1743 AND r2.created_at > COALESCE( 1744 (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr2 1745 WHERE tr2.ticket_id = t.id AND tr2.user_id = %d), 1746 t.created_at 1747 ) 1748 AND r2.is_internal_note = 0 1749 ) THEN 1 1750 WHEN t.created_at > COALESCE( 1751 (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr3 1752 WHERE tr3.ticket_id = t.id AND tr3.user_id = %d), 1753 '1970-01-01' 1754 ) THEN 1 1755 ELSE 0 1756 END as is_unread 1757 FROM {$wpdb->prefix}nexlifydesk_tickets t 1758 LEFT JOIN {$wpdb->users} u ON t.user_id = u.ID 1759 LEFT JOIN {$wpdb->users} a ON t.assigned_to = a.ID 1760 LEFT JOIN {$wpdb->prefix}nexlifydesk_replies r ON t.id = r.ticket_id 1761 WHERE {$where_sql} 1762 GROUP BY t.id 1763 ORDER BY is_unread DESC, last_reply_time DESC 1764 LIMIT %d OFFSET %d", 1765 $current_user_id, 1766 $current_user_id, 1767 $current_user_id, 1768 $current_user_id, 1769 ...$query_params 1770 ) 1771 ); 1772 1773 if (!empty($results)) { 1774 foreach ($results as $ticket) { 1775 if (!$ticket->user_id || empty($ticket->user_name)) { 1776 if (function_exists('nexlifydesk_extract_customer_details')) { 1777 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 1778 $ticket->user_name = $customer_details['name'] ?: 'Guest'; 1779 $ticket->user_email = $customer_details['email'] ?: 'N/A'; 1780 } else { 1781 $ticket->user_name = 'Guest'; 1782 $ticket->user_email = 'N/A'; 1783 } 1169 1784 } 1170 1785 } 1171 1172 $similarity_score = ($matching_keywords / count($all_keywords)) * 100; 1173 1174 if ($similarity_score >= $duplicate_threshold) { 1175 return $ticket; 1176 } 1177 } 1786 } 1787 1788 wp_cache_set($cache_key, $results, 'nexlifydesk_tickets_grid', 30); 1789 } 1790 1791 return $results; 1792 } 1793 1794 /** 1795 * Update ticket priority 1796 * 1797 * @param int $ticket_id Ticket ID 1798 * @param string $priority New priority 1799 * @return bool Success 1800 */ 1801 public static function update_ticket_priority($ticket_id, $priority) { 1802 global $wpdb; 1803 1804 $allowed_priorities = array('low', 'medium', 'high', 'urgent'); 1805 if (!in_array($priority, $allowed_priorities)) { 1806 return false; 1807 } 1808 1809 $ticket_before = self::get_ticket($ticket_id); 1810 if (!$ticket_before) { 1811 return false; 1812 } 1813 1814 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled 1815 $result = $wpdb->update( 1816 NexlifyDesk_Database::get_table('tickets'), 1817 array( 1818 'priority' => $priority, 1819 'updated_at' => current_time('mysql') 1820 ), 1821 array('id' => $ticket_id), 1822 array('%s', '%s'), 1823 array('%d') 1824 ); 1825 1826 if ($result !== false) { 1827 // Clear cache 1828 wp_cache_delete('nexlifydesk_ticket_' . $ticket_id); 1829 1830 return true; 1178 1831 } 1179 1832 1180 1833 return false; 1181 1834 } 1835 1836 /** 1837 * Assign ticket to an agent 1838 * 1839 * @param int $ticket_id Ticket ID 1840 * @param int $agent_id Agent user ID (0 for unassigned) 1841 * @return bool Success 1842 */ 1843 public static function assign_ticket($ticket_id, $agent_id) { 1844 global $wpdb; 1845 1846 $ticket_before = self::get_ticket($ticket_id); 1847 if (!$ticket_before) { 1848 return false; 1849 } 1850 1851 if ($agent_id > 0) { 1852 $agent = get_userdata($agent_id); 1853 if (!$agent) { 1854 return false; 1855 } 1856 1857 if (!user_can($agent_id, 'nexlifydesk_manage_tickets') && !user_can($agent_id, 'administrator')) { 1858 return false; 1859 } 1860 } 1861 1862 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled 1863 $result = $wpdb->update( 1864 NexlifyDesk_Database::get_table('tickets'), 1865 array( 1866 'assigned_to' => $agent_id === 0 ? null : $agent_id, 1867 'updated_at' => current_time('mysql') 1868 ), 1869 array('id' => $ticket_id), 1870 array('%d', '%s'), 1871 array('%d') 1872 ); 1873 1874 if ($result !== false) { 1875 // Clear cache 1876 wp_cache_delete('nexlifydesk_ticket_' . $ticket_id); 1877 1878 wp_cache_flush_group('nexlifydesk_tickets_grid'); 1879 1880 return true; 1881 } 1882 1883 return false; 1884 } 1885 1886 /** 1887 * Get tickets for real-time refresh with unread status 1888 * 1889 * @param int $last_refresh_timestamp Unix timestamp of last refresh (unused for now) 1890 * @return array Array of ticket objects with unread status 1891 */ 1892 public static function get_tickets_for_refresh($last_refresh_timestamp = 0) { 1893 return self::get_tickets_for_grid(['per_page' => 50]); 1894 } 1895 1896 /** 1897 * Mark ticket as read for a specific user 1898 * 1899 * @param int $ticket_id Ticket ID 1900 * @param int $user_id User ID 1901 * @return bool Success 1902 */ 1903 public static function mark_ticket_as_read($ticket_id, $user_id) { 1904 global $wpdb; 1905 1906 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1907 $result = $wpdb->replace( 1908 $wpdb->prefix . 'nexlifydesk_ticket_reads', 1909 array( 1910 'ticket_id' => $ticket_id, 1911 'user_id' => $user_id, 1912 'read_at' => current_time('mysql') 1913 ), 1914 array('%d', '%d', '%s') 1915 ); 1916 1917 if ($result) { 1918 // Clear relevant caches 1919 wp_cache_delete('nexlifydesk_ticket_' . $ticket_id); 1920 return true; 1921 } 1922 1923 return false; 1924 } 1925 1926 /** 1927 * Delete a ticket permanently from the database 1928 */ 1929 public static function delete_ticket($ticket_id) { 1930 global $wpdb; 1931 1932 $ticket = self::get_ticket($ticket_id); 1933 if (!$ticket) { 1934 return false; 1935 } 1936 1937 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1938 $wpdb->query('START TRANSACTION'); 1939 1940 try { 1941 $attachments = self::get_attachments($ticket_id); 1942 if (!empty($attachments)) { 1943 foreach ($attachments as $attachment) { 1944 $file_path = wp_upload_dir()['basedir'] . '/nexlifydesk/' . $attachment->file_name; 1945 if (file_exists($file_path)) { 1946 wp_delete_file($file_path); 1947 } 1948 } 1949 } 1950 1951 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1952 $attachments_deleted = $wpdb->delete( 1953 $wpdb->prefix . 'nexlifydesk_attachments', 1954 array('ticket_id' => $ticket_id), 1955 array('%d') 1956 ); 1957 1958 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1959 $replies_deleted = $wpdb->delete( 1960 $wpdb->prefix . 'nexlifydesk_replies', 1961 array('ticket_id' => $ticket_id), 1962 array('%d') 1963 ); 1964 1965 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1966 $ticket_deleted = $wpdb->delete( 1967 $wpdb->prefix . 'nexlifydesk_tickets', 1968 array('id' => $ticket_id), 1969 array('%d') 1970 ); 1971 1972 if ($ticket_deleted === false) { 1973 throw new Exception('Failed to delete ticket from database'); 1974 } 1975 1976 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1977 $wpdb->query('COMMIT'); 1978 1979 wp_cache_delete('nexlifydesk_ticket_' . $ticket_id); 1980 wp_cache_delete('nexlifydesk_ticket_replies_' . $ticket_id); 1981 wp_cache_delete('nexlifydesk_attachments_' . $ticket_id); 1982 1983 do_action('nexlifydesk_ticket_deleted', $ticket_id, $ticket); 1984 1985 return true; 1986 1987 } catch (Exception $e) { 1988 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query 1989 $wpdb->query('ROLLBACK'); 1990 return false; 1991 } 1992 } 1993 1182 1994 } -
nexlifydesk/trunk/includes/nexlifydesk-functions.php
r3326104 r3330741 3 3 exit; 4 4 } 5 6 /** 7 * NexlifyDesk Email Filtering & Spam Protection Features 8 * 9 * This file contains functions for filtering emails and preventing spam from creating tickets. 10 * 11 * Features include: 12 * - Admin email blocking: Prevents emails from the admin email address (like notification emails) 13 * - Notification subject filtering: Blocks emails with [Admin] or [Agent] in the subject 14 * - Blocked email addresses: Custom list of email addresses to ignore 15 * - Domain blocking: Block entire domains (useful for known spam domains) 16 * - URL filtering: Blocks emails with too many links or suspicious URLs 17 * - Keyword filtering: Blocks emails containing specified spam keywords 18 * 19 * All settings can be configured in WP Admin > Settings > NexlifyDesk > Email Piping 20 */ 5 21 6 22 /** … … 17 33 return isset($statuses[$status]) ? $statuses[$status] : ucfirst($status); 18 34 } 35 36 /** 37 * Check if an email should be blocked based on spam protection settings 38 * 39 * @param string $email_address The sender's email address 40 * @param string $subject The email subject 41 * @param string $message The email message content 42 * @param array $settings The IMAP settings with spam protection options 43 * @return bool True if email should be blocked, false otherwise 44 */ 45 function nexlifydesk_should_block_email($email_address, $subject, $message, $settings) { 46 // Check if admin email blocking is enabled 47 if (!empty($settings['block_admin_emails']) && $settings['block_admin_emails']) { 48 $admin_email = get_option('admin_email'); 49 if (strtolower($email_address) === strtolower($admin_email)) { 50 return true; 51 } 52 } 53 54 if (!empty($settings['block_notification_subjects']) && $settings['block_notification_subjects']) { 55 if (preg_match('/\[(Admin|Agent)\]/i', $subject)) { 56 return true; 57 } 58 } 59 60 if (!empty($settings['blocked_emails'])) { 61 $blocked_emails = array_map('trim', array_filter(explode("\n", $settings['blocked_emails']))); 62 foreach ($blocked_emails as $blocked_email) { 63 if (strtolower($email_address) === strtolower($blocked_email)) { 64 return true; 65 } 66 } 67 } 68 69 if (!empty($settings['blocked_domains'])) { 70 $blocked_domains = array_map('trim', array_filter(explode("\n", $settings['blocked_domains']))); 71 $email_domain = substr(strrchr($email_address, '@'), 1); 72 foreach ($blocked_domains as $blocked_domain) { 73 if (strtolower($email_domain) === strtolower($blocked_domain)) { 74 return true; 75 } 76 } 77 } 78 79 if (!empty($settings['blocked_keywords'])) { 80 $blocked_keywords = array_map('trim', array_filter(explode("\n", $settings['blocked_keywords']))); 81 $full_content = $subject . ' ' . $message; 82 foreach ($blocked_keywords as $keyword) { 83 if (stripos($full_content, $keyword) !== false) { 84 return true; 85 } 86 } 87 } 88 89 if (!empty($settings['spam_url_filtering']) && $settings['spam_url_filtering']) { 90 $max_links = isset($settings['max_links_per_email']) ? (int)$settings['max_links_per_email'] : 3; 91 92 $url_pattern = '/https?:\/\/[^\s<>"{}|\\^`\[\]]+/i'; 93 preg_match_all($url_pattern, $message, $matches); 94 $url_count = count($matches[0]); 95 96 if ($url_count > $max_links) { 97 return true; 98 } 99 100 $suspicious_patterns = [ 101 '/bit\.ly|tinyurl|t\.co|goo\.gl|short\.link/i', 102 '/\.(tk|ml|ga|cf)(\s|\/|$)/i', 103 '/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/i', 104 ]; 105 106 foreach ($suspicious_patterns as $pattern) { 107 if (preg_match($pattern, $message)) { 108 return true; 109 } 110 } 111 } 112 113 if (!empty($settings['block_marketing_emails']) && $settings['block_marketing_emails']) { 114 if (function_exists('nexlifydesk_is_marketing_email') && 115 nexlifydesk_is_marketing_email($email_address, $subject, $message, $settings)) { 116 return true; 117 } 118 } 119 120 return false; 121 } 122 123 /** 124 * Get the ticket page URL from plugin settings 125 * @return string The ticket page URL or empty string if not found 126 */ 127 function nexlifydesk_get_ticket_page_url() { 128 $settings = get_option('nexlifydesk_settings', array()); 129 if (isset($settings['ticket_page_id']) && $settings['ticket_page_id']) { 130 return get_permalink($settings['ticket_page_id']); 131 } 132 return ''; 133 } -
nexlifydesk/trunk/nexlifydesk.php
r3326104 r3330741 3 3 * Plugin Name: NexlifyDesk 4 4 * Description: A modern, user-friendly support ticket system for WordPress with ticket submission, threaded replies, file attachments, agent assignment, and customizable. 5 * Version: 1.0. 05 * Version: 1.0.1 6 6 * Supported Versions: 6.2+ 7 7 * Tested up to: 6.2 < 6.8. … … 19 19 define('NEXLIFYDESK_PLUGIN_DIR', plugin_dir_path(__FILE__)); 20 20 define('NEXLIFYDESK_PLUGIN_URL', plugin_dir_url(__FILE__)); 21 define('NEXLIFYDESK_VERSION', '1.0. 0');21 define('NEXLIFYDESK_VERSION', '1.0.1'); 22 22 define('NEXLIFYDESK_TABLE_PREFIX', 'nexlifydesk_'); 23 23 define('NEXLIFYDESK_CAP_VIEW_ALL_TICKETS', 'nexlifydesk_view_all_tickets'); … … 35 35 require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/class-nexlifydesk-reports.php'; 36 36 require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/class-support.php'; 37 require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/class-nexlifydesk-rate-limiter.php'; 38 require_once NEXLIFYDESK_PLUGIN_DIR . 'email-source/nexlifydesk-email-pipe.php'; 39 require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/helpers.php'; 37 40 38 41 // Initialize the plugin … … 40 43 41 44 NexlifyDesk_Database::init(); 45 46 // Run database upgrades if needed 47 NexlifyDesk_Database::upgrade_database(); 48 42 49 NexlifyDesk_Database::check_and_run_migrations(); 43 50 NexlifyDesk_Tickets::init(); … … 232 239 'ticket_closed_text' => __('This ticket is closed. Please create a new ticket for further assistance.', 'nexlifydesk'), 233 240 'status_updated_text' => __('Status updated successfully!', 'nexlifydesk'), 234 'ticket_page_url' => NexlifyDesk_Admin::get_ticket_page_url(),241 'ticket_page_url' => nexlifydesk_get_ticket_page_url(), 235 242 )); 236 243 } … … 424 431 wp_send_json_success(implode('<br>', $purged)); 425 432 }); 433 434 add_action('nexlifydesk_fetch_emails_event', 'nexlifydesk_fetch_emails'); 435 436 // Function to get the current fetch interval setting 437 function nexlifydesk_get_fetch_interval() { 438 $settings = get_option('nexlifydesk_imap_settings', []); 439 $interval = isset($settings['fetch_interval']) ? intval($settings['fetch_interval']) : 5; 440 441 // Ensure valid interval 442 if (!in_array($interval, [2, 5, 10])) { 443 $interval = 5; // Default to 5 minutes 444 } 445 446 return $interval; 447 } 448 449 // Function to reschedule email fetch with new interval 450 function nexlifydesk_reschedule_email_fetch() { 451 $interval = nexlifydesk_get_fetch_interval(); 452 $schedule_name = "nexlifydesk_{$interval}_minutes"; 453 454 // Clear existing schedule 455 wp_clear_scheduled_hook('nexlifydesk_fetch_emails_event'); 456 457 // Schedule with new interval 458 if (!wp_next_scheduled('nexlifydesk_fetch_emails_event')) { 459 wp_schedule_event(time(), $schedule_name, 'nexlifydesk_fetch_emails_event'); 460 } 461 } 462 463 // Schedule email fetch on plugin activation 464 register_activation_hook(__FILE__, function() { 465 nexlifydesk_reschedule_email_fetch(); 466 }); 467 468 // Reschedule when settings are updated 469 add_action('update_option_nexlifydesk_imap_settings', function($old_value, $value, $option) { 470 $old_interval = isset($old_value['fetch_interval']) ? intval($old_value['fetch_interval']) : 5; 471 $new_interval = isset($value['fetch_interval']) ? intval($value['fetch_interval']) : 5; 472 473 // If interval changed, reschedule 474 if ($old_interval !== $new_interval) { 475 nexlifydesk_reschedule_email_fetch(); 476 } 477 }, 10, 3); 478 479 // Fallback: schedule on 'init' for multisite/cron edge cases 480 add_action('init', function() { 481 if (!wp_next_scheduled('nexlifydesk_fetch_emails_event')) { 482 nexlifydesk_reschedule_email_fetch(); 483 } 484 }); 485 486 // Add custom intervals 487 add_filter('cron_schedules', function($schedules) { 488 $schedules['nexlifydesk_2_minutes'] = array( 489 'interval' => 120, // 2 minutes 490 'display' => __('Every 2 Minutes (NexlifyDesk)', 'nexlifydesk') 491 ); 492 $schedules['nexlifydesk_5_minutes'] = array( 493 'interval' => 300, // 5 minutes 494 'display' => __('Every 5 Minutes (NexlifyDesk)', 'nexlifydesk') 495 ); 496 $schedules['nexlifydesk_10_minutes'] = array( 497 'interval' => 600, // 10 minutes 498 'display' => __('Every 10 Minutes (NexlifyDesk)', 'nexlifydesk') 499 ); 500 return $schedules; 501 }); 502 503 // AJAX handler for instant email fetch 504 add_action('wp_ajax_nexlifydesk_fetch_emails_now', function() { 505 check_ajax_referer('nexlifydesk_fetch_emails_now'); 506 507 // Use a more permissive permission check 508 if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) { 509 wp_send_json_error(__('You do not have permission.', 'nexlifydesk')); 510 } 511 512 if (function_exists('nexlifydesk_fetch_emails')) { 513 nexlifydesk_fetch_emails(); 514 wp_send_json_success(); 515 } else { 516 wp_send_json_error(__('Fetch function not found.', 'nexlifydesk')); 517 } 518 }); 519 520 /** 521 * Gets tickets for the initial grid view. 522 * 523 * @param array $args 524 * @return array 525 */ 526 function nexlifydesk_get_tickets_for_grid($args = []) { 527 if (!class_exists('NexlifyDesk_Tickets')) { 528 return []; 529 } 530 return NexlifyDesk_Tickets::get_tickets_for_grid($args); 531 } 532 533 /** 534 * Renders a single ticket card. 535 * 536 * @param object $ticket 537 */ 538 function nexlifydesk_render_ticket_card($ticket) { 539 if (!is_object($ticket)) { 540 return; 541 } 542 $assignee_name = !empty($ticket->assigned_to_display_name) ? $ticket->assigned_to_display_name : __('Unassigned', 'nexlifydesk'); 543 $assignee_initials = strtoupper(substr($assignee_name, 0, 2)); 544 $status_class = 'status-' . str_replace('_', '-', $ticket->status); 545 $priority_class = 'priority-' . $ticket->priority; 546 $ticket_url = admin_url('admin.php?page=nexlifydesk_tickets&ticket_id=' . $ticket->id); 547 ?> 548 <div class="ticket-card" onclick="window.location.href='<?php echo esc_url($ticket_url); ?>';" data-id="<?php echo esc_attr($ticket->id); ?>"> 549 <div class="ticket-header"> 550 <div class="ticket-id">#<?php echo esc_html($ticket->ticket_id); ?></div> 551 </div> 552 <div class="ticket-title"><?php echo esc_html($ticket->subject); ?></div> 553 <div class="ticket-description"><?php echo esc_html(wp_trim_words($ticket->message, 15)); ?></div> 554 <div class="ticket-meta"> 555 <span class="status-badge <?php echo esc_attr($status_class); ?>"><?php echo esc_html(ucwords(str_replace('_', ' ', $ticket->status))); ?></span> 556 <span class="priority-badge <?php echo esc_attr($priority_class); ?>"><?php echo esc_html(ucfirst($ticket->priority)); ?></span> 557 </div> 558 <div class="ticket-footer"> 559 <div class="assignee"> 560 <div class="avatar"><?php echo esc_html($assignee_initials); ?></div> 561 <span><?php echo esc_html($assignee_name); ?></span> 562 </div> 563 <div><?php echo esc_html(human_time_diff(strtotime($ticket->created_at))) . ' ' . esc_html__('ago', 'nexlifydesk'); ?></div> 564 </div> 565 </div> 566 <?php 567 } -
nexlifydesk/trunk/readme.txt
r3326109 r3330741 4 4 Requires at least: 6.2 5 5 Tested up to: 6.8 6 Stable tag: 1.0. 06 Stable tag: 1.0.1 7 7 License: GPLv2 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 15 15 NexlifyDesk is a comprehensive, enterprise-grade support ticketing system for WordPress that transforms your website into a professional customer service hub. Built with scalability and efficiency in mind, it provides everything you need to deliver exceptional support experiences while maintaining complete control over your customer data. 16 16 17 **Documentation**: [Full Documentation & Setup Guide](https://nexlifylabs.com/nexlify labs-plugin-documentation/nexlifydesk-documentation/)17 **Documentation**: [Full Documentation & Setup Guide](https://nexlifylabs.com/nexlifydesk-documentation/getting-started/) 18 18 19 19 == Key Features == … … 44 44 45 45 **Intelligent Automation** 46 * ** Duplicate Detection**: Advanced algorithms detect similar tickets and merge conversations automatically46 * **Enhanced Duplicate Detection**: Advanced multi-layer algorithms detect similar tickets and merge conversations automatically with improved accuracy 47 47 * **Auto-Assignment**: Smart distribution of tickets to available agents based on workload 48 48 * **SLA Monitoring**: Automatic tracking and breach notifications to maintain service standards 49 49 * **Auto-Closure**: Resolved tickets automatically close after 48 hours of inactivity with system notifications 50 51 **Email Piping & Integration** 52 * **Multi-Provider Support**: Convert emails to tickets with support for Custom IMAP/POP3, AWS WorkMail, and Google Workspace/Gmail 53 * **Flexible Email Management**: Option to keep or delete emails from inbox after ticket creation 54 * **Advanced Spam Protection**: Built-in spam filtering, email blocking, and rate limiting to prevent abuse 55 * **Intelligent Email Processing**: Automatic sender detection, thread management, and duplicate prevention 56 57 **Enhanced Admin Experience** 58 * **Real-time Ticket List**: Live-updating ticket interface with read/unread status indicators 59 * **Smart Sorting**: Automatic prioritization with unread tickets displayed first 60 * **Instant Notifications**: Real-time updates without page refresh 61 * **Bulk Operations**: Enhanced bulk actions for efficient ticket management 50 62 51 63 **Categories & Organization** … … 117 129 * Link these pages in NexlifyDesk > Settings for proper navigation 118 130 119 5. **Agent Setup** (Optional): Configure your support team: 131 5. **Email Piping Setup** (Optional): Configure email-to-ticket conversion: 132 * Choose your email provider (Custom IMAP/POP3, AWS WorkMail, or Google Workspace) 133 * Configure connection settings and authentication 134 * Set up spam protection and email filtering rules 135 * Choose whether to keep or delete emails from inbox after processing 136 137 6. **Agent Setup** (Optional): Configure your support team: 120 138 * Create user accounts for support agents 121 139 * Assign the "NexlifyDesk Agent" role … … 179 197 - **Order-based duplicate detection**: Links tickets mentioning order numbers 180 198 - **Customer context**: View order information when handling support requests 199 200 = How do I set up email piping? = 201 202 Email piping converts incoming emails into support tickets automatically. Setup varies by provider: 203 204 **Custom IMAP/POP3:** 205 1. Go to NexlifyDesk > Settings > Email Piping 206 2. Select "Custom IMAP/POP3" as your provider 207 3. Enter your mail server details (host, port, username, password) 208 4. Configure spam protection and email filtering settings 209 5. Choose whether to delete emails from inbox after processing 210 211 **AWS WorkMail:** 212 1. Ensure your site has SSL enabled (required for AWS) 213 2. Select "AWS WorkMail" as your provider 214 3. Enter your AWS region, organization ID, and email credentials 215 4. Optionally configure AWS SES for enhanced email sending 216 217 **Google Workspace/Gmail:** 218 1. Set up Google OAuth credentials in your Google Cloud Console 219 2. Select "Google Workspace" as your provider 220 3. Complete the OAuth authentication process 221 4. Configure email processing preferences 181 222 182 223 = How does automatic ticket assignment work? = … … 351 392 == Upgrade Notice == 352 393 394 = 1.0.1 = 395 Major update with email piping support! Convert emails to tickets automatically with support for Custom IMAP/POP3, AWS WorkMail, and Google Workspace. Enhanced duplicate detection, real-time ticket list with read/unread status, and advanced spam protection. Recommended for all users. 396 353 397 = 1.0.0 = 354 398 Initial release of NexlifyDesk. No upgrade steps necessary for new installations. 399 400 == Changelog == 401 402 = 1.0.1 = 403 **Major Features:** 404 * Added Email Piping support with multiple providers (Custom IMAP/POP3, AWS WorkMail, Google Workspace/Gmail) 405 * Enhanced duplicate ticket detection with improved accuracy and performance 406 * New real-time ticket list interface with read/unread status indicators 407 * Advanced spam protection with rate limiting and email blocking capabilities 408 409 **Email Piping Features:** 410 * Support for Custom IMAP/POP3 servers with SSL/TLS encryption 411 * AWS WorkMail integration with SSL requirement and SES support 412 * Google Workspace/Gmail integration with OAuth authentication 413 * Configurable option to keep or delete emails from inbox after processing 414 * Intelligent email thread detection and customer detail extraction 415 * Comprehensive spam filtering with customizable rules 416 417 **Enhanced Admin Experience:** 418 * Real-time ticket list updates without page refresh 419 * Smart sorting with unread tickets prioritized first 420 * Improved bulk operations for efficient ticket management 421 * Enhanced ticket grid with performance optimizations 422 423 **Security & Performance:** 424 * Built-in rate limiting to prevent spam and abuse 425 * Advanced email validation and sanitization 426 * Optimized database queries with proper caching 427 * Enhanced SSL detection for secure email processing 428 429 **Bug Fixes:** 430 * Improved WordPress coding standards compliance 431 * Fixed database query optimizations 432 * Enhanced error handling and logging 433 * Better mobile responsive design 434 435 = 1.0.0 = 436 * Initial release 437 * Core ticketing system with agent management 438 * Internal notes and professional communication tools 439 * WooCommerce integration 440 * SLA monitoring and reporting 441 * Duplicate detection system 442 * Email notification system 443 * Agent roles and permissions 444 * Categories and priority management 445 * File attachment support 446 * Shortcode system for frontend integration 355 447 356 448 == Support & Documentation == -
nexlifydesk/trunk/templates/admin/settings.php
r3326104 r3330741 6 6 $default_settings = array( 7 7 'email_notifications' => 1, 8 'admin_email_notifications' => 1, 8 9 'default_priority' => 'medium', 9 10 'auto_assign' => 0, … … 65 66 </tr> 66 67 <tr> 68 <th><label for="admin_email_notifications"><?php esc_html_e('Admin Email Notifications', 'nexlifydesk'); ?></label></th> 69 <td> 70 <input type="checkbox" name="admin_email_notifications" id="admin_email_notifications" value="1" <?php checked($settings['admin_email_notifications'], 1); ?>> 71 <p class="description"><?php esc_html_e('Send email notifications to admin. When disabled, only assigned agents will receive notifications (except for SLA breach notifications).', 'nexlifydesk'); ?></p> 72 </td> 73 </tr> 74 <tr> 67 75 <th><label for="default_priority"><?php esc_html_e('Default Priority', 'nexlifydesk'); ?></label></th> 68 76 <td> … … 238 246 </table> 239 247 248 <h2><?php esc_html_e('Spam Protection', 'nexlifydesk'); ?></h2> 249 <table class="form-table"> 250 <tr> 251 <th><label for=""><?php esc_html_e('Rate Limiting', 'nexlifydesk'); ?></label></th> 252 <td> 253 <p class="description"><?php esc_html_e('Prevents spam by limiting ticket creation to 5 tickets per 30 minutes per user/email address.', 'nexlifydesk'); ?></p> 254 <p class="description"><?php esc_html_e('This applies to both web submissions and email piping. Admin and agent users are exempt from rate limiting.', 'nexlifydesk'); ?></p> 255 </td> 256 </tr> 257 <tr> 258 <th><label for=""><?php esc_html_e('Clear Rate Limits', 'nexlifydesk'); ?></label></th> 259 <td> 260 <p class="description"><?php esc_html_e('Remove rate limiting restrictions for a specific user or email address.', 'nexlifydesk'); ?></p> 261 <div style="margin-top: 10px;"> 262 <input type="email" id="nexlifydesk_clear_rate_limit_email" placeholder="<?php esc_attr_e('Enter email address', 'nexlifydesk'); ?>" style="width: 300px;"> 263 <button type="button" class="button" id="nexlifydesk_clear_rate_limit"><?php esc_html_e('Clear Rate Limit', 'nexlifydesk'); ?></button> 264 </div> 265 <div id="nexlifydesk_clear_rate_limit_result" style="margin-top: 10px;"></div> 266 </td> 267 </tr> 268 <tr> 269 <th><label for=""><?php esc_html_e('Rate Limit Status', 'nexlifydesk'); ?></label></th> 270 <td> 271 <p class="description"><?php esc_html_e('Check the current rate limit status for a specific user or email address.', 'nexlifydesk'); ?></p> 272 <div style="margin-top: 10px;"> 273 <input type="email" id="nexlifydesk_check_rate_limit_email" placeholder="<?php esc_attr_e('Enter email address', 'nexlifydesk'); ?>" style="width: 300px;"> 274 <button type="button" class="button" id="nexlifydesk_check_rate_limit"><?php esc_html_e('Check Status', 'nexlifydesk'); ?></button> 275 </div> 276 <div id="nexlifydesk_check_rate_limit_result" style="margin-top: 10px;"></div> 277 </td> 278 </tr> 279 </table> 280 240 281 <h2><?php esc_html_e('Data Management', 'nexlifydesk'); ?></h2> 241 282 <table class="form-table"> -
nexlifydesk/trunk/templates/admin/ticket-single.php
r3326104 r3330741 9 9 } 10 10 11 // --- Preparing variables ---12 11 $user = get_userdata($ticket->user_id); 13 12 $assigned_agent = $ticket->assigned_to ? get_userdata($ticket->assigned_to) : null; … … 15 14 $initial_attachments = NexlifyDesk_Tickets::get_attachments($ticket->id); 16 15 $user_avatar_url = $user ? get_avatar_url($user->ID) : get_avatar_url(0); 16 17 $customer_details = function_exists('nexlifydesk_extract_customer_details') ? 18 nexlifydesk_extract_customer_details($ticket->message) : 19 ['name' => '', 'email' => '', 'message' => $ticket->message]; 20 21 $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest'); 22 $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A'); 23 $display_message = $customer_details['message']; 17 24 ?> 18 25 … … 69 76 <p><?php 70 77 /* translators: 1: requester name, 2: date and time */ 71 printf(esc_html__('Requested by %1$s on %2$s', 'nexlifydesk'), '<strong>' . esc_html($ user ? $user->display_name : 'Guest') . '</strong>', esc_html(date_i18n(get_option('date_format') . ' \a\t ' . get_option('time_format'), strtotime($ticket->created_at))));78 printf(esc_html__('Requested by %1$s on %2$s', 'nexlifydesk'), '<strong>' . esc_html($customer_name) . '</strong>', esc_html(date_i18n(get_option('date_format') . ' \a\t ' . get_option('time_format'), strtotime($ticket->created_at)))); 72 79 ?></p> 80 <?php if (!$user && $customer_email !== 'N/A') : ?> 81 <p class="customer-email"><?php echo esc_html($customer_email); ?></p> 82 <?php endif; ?> 73 83 </div> 74 84 </header> … … 118 128 <div class="message-content"> 119 129 <div class="message-header"> 120 <strong><?php echo esc_html($ user ? $user->display_name : 'Guest'); ?></strong>130 <strong><?php echo esc_html($customer_name); ?></strong> 121 131 <span class="timestamp"><?php echo esc_html(date_i18n(get_option('date_format') . ' - ' . get_option('time_format'), strtotime($ticket->created_at))); ?></span> 122 132 </div> 123 <p><?php echo nl2br(esc_html($ ticket->message)); ?></p>133 <p><?php echo nl2br(esc_html($display_message)); ?></p> 124 134 <?php if (!empty($initial_attachments)) : ?> 125 135 <div class="attachments"> … … 139 149 $is_internal_note = isset($reply->is_internal_note) && $reply->is_internal_note; 140 150 151 if (!$reply_user) { 152 $reply_customer_details = function_exists('nexlifydesk_extract_customer_details') ? 153 nexlifydesk_extract_customer_details($reply->message) : 154 ['name' => '', 'email' => '', 'message' => $reply->message]; 155 $reply_customer_name = $reply_customer_details['name'] ?: 'Guest'; 156 $reply_display_message = $reply_customer_details['message']; 157 } else { 158 $reply_customer_name = $reply_user->display_name; 159 $reply_display_message = $reply->message; 160 } 161 141 162 if ($is_internal_note) { 142 163 $message_class = 'internal-note'; … … 149 170 <div class="message-content"> 150 171 <div class="message-header"> 151 <strong><?php echo esc_html($reply_ user ? $reply_user->display_name : 'User'); ?></strong>172 <strong><?php echo esc_html($reply_customer_name); ?></strong> 152 173 <?php if ($is_internal_note) : ?> 153 174 <span class="internal-note-label"><?php esc_html_e('Internal Note', 'nexlifydesk'); ?></span> … … 155 176 <span class="timestamp"><?php echo esc_html(date_i18n(get_option('date_format') . ' - ' . get_option('time_format'), strtotime($reply->created_at))); ?></span> 156 177 </div> 157 <p><?php echo nl2br(esc_html($reply ->message)); ?></p>178 <p><?php echo nl2br(esc_html($reply_display_message)); ?></p> 158 179 <?php if (!empty($reply->attachments)) : ?> 159 180 <div class="attachments"> … … 174 195 <div class="sidebar-block"> 175 196 <h3><?php esc_html_e('About Customer', 'nexlifydesk'); ?></h3> 176 <p><strong><?php echo esc_html($ user ? $user->display_name : 'Guest'); ?></strong></p>177 <p><?php echo esc_html($ user ? $user->user_email : 'N/A'); ?></p>197 <p><strong><?php echo esc_html($customer_name); ?></strong></p> 198 <p><?php echo esc_html($customer_email); ?></p> 178 199 <?php if ($user): ?> 179 200 <a href="<?php echo esc_url(get_edit_user_link($user->ID)); ?>" target="_blank"><?php esc_html_e('View Profile', 'nexlifydesk'); ?></a> 201 <?php elseif ($customer_email !== 'N/A'): ?> 202 <p class="non-registered-notice"><?php esc_html_e('Non-registered customer', 'nexlifydesk'); ?></p> 180 203 <?php endif; ?> 181 204 </div> … … 198 221 <ul class="attachments-list"> 199 222 <?php 200 $all_attachments = array_merge($initial_attachments, ...array_column($replies, 'attachments')); 223 $all_attachments = array(); 224 225 if (!empty($initial_attachments) && is_array($initial_attachments)) { 226 $all_attachments = array_merge($all_attachments, $initial_attachments); 227 } 228 229 if (!empty($replies) && is_array($replies)) { 230 foreach ($replies as $reply) { 231 if (isset($reply->attachments) && is_array($reply->attachments) && !empty($reply->attachments)) { 232 $all_attachments = array_merge($all_attachments, $reply->attachments); 233 } 234 } 235 } 236 201 237 if (!empty($all_attachments)) { 202 238 foreach ($all_attachments as $attachment) { 203 echo '<li><a href="' . esc_url($attachment->file_path) . '" target="_blank" rel="noopener">' . esc_html($attachment->file_name) . '</a></li>'; 239 if (isset($attachment->file_path) && isset($attachment->file_name)) { 240 echo '<li><a href="' . esc_url($attachment->file_path) . '" target="_blank" rel="noopener">' . esc_html($attachment->file_name) . '</a></li>'; 241 } 204 242 } 205 243 } else { -
nexlifydesk/trunk/templates/admin/tickets-list.php
r3326104 r3330741 19 19 <h1><?php esc_html_e('Support Tickets', 'nexlifydesk'); ?></h1> 20 20 <p><?php esc_html_e('Manage and track customer support requests', 'nexlifydesk'); ?></p> 21 <button id="manual-refresh-btn" class="button" style="margin-left: 10px;"><?php esc_html_e('Refresh Now', 'nexlifydesk'); ?></button> 21 22 </div> 22 23 … … 48 49 49 50 <div class="controls"> 51 <div class="bulk-actions"> 52 <input type="checkbox" id="select-all-tickets" class="select-all-checkbox"> 53 <select id="bulk-action-select" class="bulk-action-dropdown" disabled> 54 <option value=""><?php esc_html_e('Bulk Actions', 'nexlifydesk'); ?></option> 55 <option value="assign"><?php esc_html_e('Assign to Agent', 'nexlifydesk'); ?></option> 56 <option value="status"><?php esc_html_e('Change Status', 'nexlifydesk'); ?></option> 57 <option value="priority"><?php esc_html_e('Change Priority', 'nexlifydesk'); ?></option> 58 <?php if (current_user_can('manage_options')): ?> 59 <option value="delete" class="delete-option" style="color: #d63638;"><?php esc_html_e('Delete Tickets (Admin Only)', 'nexlifydesk'); ?></option> 60 <?php endif; ?> 61 </select> 62 <button id="apply-bulk-action" class="button button-primary" disabled><?php esc_html_e('Apply', 'nexlifydesk'); ?></button> 63 </div> 64 50 65 <div class="search-bar"> 51 66 <span class="search-icon dashicons dashicons-search"></span> … … 67 82 <option value="low"><?php esc_html_e('Low', 'nexlifydesk'); ?></option> 68 83 </select> 69 70 <button id="reassign-orphaned-tickets-btn" class="button button-secondary"> 71 <?php esc_html_e('Reassign Orphaned Tickets', 'nexlifydesk'); ?> 72 </button> 73 </div> 74 75 <div id="nexlifydesk-ticket-grid" class="ticket-grid"> 76 <div class="loading-tickets"> 77 <span class="spinner is-active"></span> 78 <?php esc_html_e('Loading tickets...', 'nexlifydesk'); ?> 79 </div> 80 </div> 84 </div> 85 86 <div id="nexlifydesk-ticket-list" class="ticket-list"> 87 <div class="ticket-list-header"> 88 <div class="header-checkbox"> 89 <input type="checkbox" id="header-select-all" class="select-all-checkbox"> 90 </div> 91 <div class="header-subject"><?php esc_html_e('Subject', 'nexlifydesk'); ?></div> 92 <div class="header-customer"><?php esc_html_e('Customer', 'nexlifydesk'); ?></div> 93 <div class="header-status"><?php esc_html_e('Status', 'nexlifydesk'); ?></div> 94 <div class="header-priority"><?php esc_html_e('Priority', 'nexlifydesk'); ?></div> 95 <div class="header-assignee"><?php esc_html_e('Assignee', 'nexlifydesk'); ?></div> 96 <div class="header-date"><?php esc_html_e('Date', 'nexlifydesk'); ?></div> 97 </div> 98 99 <div class="ticket-list-body"> 100 <?php 101 if (class_exists('NexlifyDesk_Tickets')) { 102 $initial_tickets = NexlifyDesk_Tickets::get_tickets_for_grid(); 103 if (!empty($initial_tickets)) { 104 foreach ($initial_tickets as $ticket) { 105 $user = get_userdata($ticket->user_id); 106 $assigned_agent = $ticket->assigned_to ? get_userdata($ticket->assigned_to) : null; 107 108 if (!$user) { 109 $customer_details = function_exists('nexlifydesk_extract_customer_details') ? 110 nexlifydesk_extract_customer_details($ticket->message) : 111 ['name' => '', 'email' => '', 'message' => $ticket->message]; 112 $customer_name = $customer_details['name'] ?: 'Guest'; 113 $customer_email = $customer_details['email'] ?: 'N/A'; 114 } else { 115 $customer_name = $user->display_name; 116 $customer_email = $user->user_email; 117 } 118 ?> 119 <?php 120 $is_unread = isset($ticket->is_unread) ? $ticket->is_unread : false; 121 $last_reply_time = isset($ticket->last_reply_time) ? $ticket->last_reply_time : $ticket->created_at; 122 ?> 123 <div class="ticket-row <?php echo $is_unread ? 'unread' : ''; ?>" 124 data-ticket-id="<?php echo esc_attr($ticket->id); ?>" 125 data-last-reply="<?php echo esc_attr(strtotime($last_reply_time)); ?>" 126 data-is-unread="<?php echo $is_unread ? '1' : '0'; ?>" 127 data-status="<?php echo esc_attr($ticket->status); ?>" 128 data-priority="<?php echo esc_attr($ticket->priority); ?>"> 129 <div class="row-checkbox"> 130 <input type="checkbox" class="ticket-checkbox" value="<?php echo esc_attr($ticket->id); ?>"> 131 </div> 132 <div class="row-subject"> 133 <a href="<?php echo esc_url(admin_url('admin.php?page=nexlifydesk_tickets&ticket_id=' . $ticket->id)); ?>" class="ticket-link"> 134 <?php if ($is_unread): ?><span class="unread-dot"></span><?php endif; ?> 135 <span class="ticket-id">#<?php echo esc_html($ticket->ticket_id); ?></span> 136 <span class="ticket-title <?php echo $is_unread ? 'unread-title' : ''; ?>"><?php echo esc_html($ticket->subject); ?></span> 137 </a> 138 <div class="ticket-preview"><?php echo esc_html(wp_trim_words($ticket->message, 15)); ?></div> 139 </div> 140 <div class="row-customer"> 141 <span class="customer-name"><?php echo esc_html($customer_name); ?></span> 142 <span class="customer-email"><?php echo esc_html($customer_email); ?></span> 143 </div> 144 <div class="row-status"> 145 <span class="status-badge status-<?php echo esc_attr($ticket->status); ?>"> 146 <?php echo esc_html(ucfirst(str_replace('_', ' ', $ticket->status))); ?> 147 </span> 148 </div> 149 <div class="row-priority"> 150 <span class="priority-badge priority-<?php echo esc_attr($ticket->priority); ?>"> 151 <?php echo esc_html(ucfirst($ticket->priority)); ?> 152 </span> 153 </div> 154 <div class="row-assignee"> 155 <?php if ($assigned_agent): ?> 156 <div class="assignee-info"> 157 <div class="avatar"><?php echo esc_html(substr($assigned_agent->display_name, 0, 1)); ?></div> 158 <span><?php echo esc_html($assigned_agent->display_name); ?></span> 159 </div> 160 <?php else: ?> 161 <span class="unassigned"><?php esc_html_e('Unassigned', 'nexlifydesk'); ?></span> 162 <?php endif; ?> 163 </div> 164 <div class="row-date"> 165 <span class="date-time"><?php echo esc_html(date_i18n('M j, Y', strtotime($last_reply_time))); ?></span> 166 <span class="time-ago"><?php echo esc_html(human_time_diff(strtotime($last_reply_time), current_time('timestamp')) . ' ago'); ?></span> 167 </div> 168 </div> 169 <?php 170 } 171 } else { 172 echo '<div class="no-tickets">' . esc_html__('No tickets found.', 'nexlifydesk') . '</div>'; 173 } 174 } else { 175 echo '<div class="no-tickets">' . esc_html__('Tickets system not available.', 'nexlifydesk') . '</div>'; 176 } 177 ?> 178 </div> 179 </div> 180 181 <input type="hidden" id="nexlifydesk-nonce" value="<?php echo esc_attr(wp_create_nonce('nexlifydesk-ajax-nonce')); ?>"> 182 183 <div id="bulk-assign-modal" class="bulk-modal" style="display: none;"> 184 <div class="modal-content"> 185 <h3><?php esc_html_e('Assign Tickets to Agent', 'nexlifydesk'); ?></h3> 186 <select id="bulk-assign-agent"> 187 <option value=""><?php esc_html_e('Select Agent', 'nexlifydesk'); ?></option> 188 <?php 189 $agents = get_users(array('capability' => 'nexlifydesk_manage_tickets')); 190 foreach ($agents as $agent) { 191 echo '<option value="' . esc_attr($agent->ID) . '">' . esc_html($agent->display_name) . '</option>'; 192 } 193 ?> 194 </select> 195 <div class="modal-actions"> 196 <button id="cancel-bulk-assign" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button> 197 <button id="confirm-bulk-assign" class="button button-primary"><?php esc_html_e('Assign', 'nexlifydesk'); ?></button> 198 </div> 199 </div> 200 </div> 201 202 <div id="bulk-status-modal" class="bulk-modal" style="display: none;"> 203 <div class="modal-content"> 204 <h3><?php esc_html_e('Change Ticket Status', 'nexlifydesk'); ?></h3> 205 <select id="bulk-status-select"> 206 <option value="open"><?php esc_html_e('Open', 'nexlifydesk'); ?></option> 207 <option value="in_progress"><?php esc_html_e('In Progress', 'nexlifydesk'); ?></option> 208 <option value="resolved"><?php esc_html_e('Resolved', 'nexlifydesk'); ?></option> 209 <option value="closed"><?php esc_html_e('Closed', 'nexlifydesk'); ?></option> 210 </select> 211 <div class="modal-actions"> 212 <button id="cancel-bulk-status" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button> 213 <button id="confirm-bulk-status" class="button button-primary"><?php esc_html_e('Update', 'nexlifydesk'); ?></button> 214 </div> 215 </div> 216 </div> 217 218 <div id="bulk-priority-modal" class="bulk-modal" style="display: none;"> 219 <div class="modal-content"> 220 <h3><?php esc_html_e('Change Ticket Priority', 'nexlifydesk'); ?></h3> 221 <select id="bulk-priority-select"> 222 <option value="low"><?php esc_html_e('Low', 'nexlifydesk'); ?></option> 223 <option value="medium"><?php esc_html_e('Medium', 'nexlifydesk'); ?></option> 224 <option value="high"><?php esc_html_e('High', 'nexlifydesk'); ?></option> 225 </select> 226 <div class="modal-actions"> 227 <button id="cancel-bulk-priority" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button> 228 <button id="confirm-bulk-priority" class="button button-primary"><?php esc_html_e('Update', 'nexlifydesk'); ?></button> 229 </div> 230 </div> 231 </div> 232 233 <?php if (current_user_can('manage_options')): ?> 234 <div id="bulk-delete-modal" class="bulk-modal" style="display: none;"> 235 <div class="modal-content"> 236 <h3 style="color: #d63638;"><?php esc_html_e('Delete Tickets', 'nexlifydesk'); ?></h3> 237 <p><?php esc_html_e('Are you sure you want to permanently delete the selected tickets?', 'nexlifydesk'); ?></p> 238 <div class="selected-tickets-info"> 239 <?php esc_html_e('This will delete', 'nexlifydesk'); ?> <strong><span id="selected-tickets-count">0</span></strong> <?php esc_html_e('tickets permanently.', 'nexlifydesk'); ?> 240 </div> 241 <p style="color: #d63638; font-weight: 600;"><?php esc_html_e('This action cannot be undone!', 'nexlifydesk'); ?></p> 242 <div class="modal-actions"> 243 <button id="cancel-bulk-delete" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button> 244 <button id="confirm-bulk-delete" class="button button-primary" style="background-color: #d63638; border-color: #d63638;"><?php esc_html_e('Delete Permanently', 'nexlifydesk'); ?></button> 245 </div> 246 </div> 247 </div> 248 <?php endif; ?> 81 249 </div> -
nexlifydesk/trunk/templates/frontend/partials/single-reply.php
r3326104 r3330741 11 11 } 12 12 13 $reply_message = isset($reply->message) ? $reply->message : ''; 14 $reply_customer_details = nexlifydesk_extract_customer_details($reply_message); 15 $reply_customer_name = $reply_user ? $reply_user->display_name : ($reply_customer_details['name'] ?: __('Unknown', 'nexlifydesk')); 16 $clean_reply_message = $reply_customer_details['message'] ?: $reply_message; 17 13 18 if (!$reply_user || !is_object($reply_user) || !isset($reply_user->roles)) { 14 19 $reply_user = (object) array( 15 'display_name' => __('Unknown', 'nexlifydesk'),20 'display_name' => $reply_customer_name, 16 21 'ID' => 0, 17 22 'roles' => array() … … 31 36 <div class="message-content"> 32 37 <div class="message-header"> 33 <strong><?php echo esc_html($reply_ user ? $reply_user->display_name : __('Unknown', 'nexlifydesk')); ?></strong>38 <strong><?php echo esc_html($reply_customer_name); ?></strong> 34 39 <span class="timestamp"><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($reply->created_at))); ?></span> 35 40 </div> 36 41 <div class="message-body"> 37 <?php echo nl2br(esc_html($ reply->message)); ?>42 <?php echo nl2br(esc_html($clean_reply_message)); ?> 38 43 39 44 <?php if (!empty($reply->attachments)) : ?> … … 41 46 <?php foreach ($reply->attachments as $attachment) : ?> 42 47 <div class="attachment"> 43 <a href="<?php echo esc_url($attachment->file_ url); ?>" target="_blank">48 <a href="<?php echo esc_url($attachment->file_path); ?>" target="_blank"> 44 49 <span class="attachment-icon">📎</span> 45 50 <?php echo esc_html($attachment->file_name); ?> -
nexlifydesk/trunk/templates/frontend/ticket-form.php
r3326104 r3330741 103 103 <p class="file-info"> 104 104 <?php 105 // Get server limits for display 106 $server_max_size = min( 107 wp_max_upload_size(), 108 wp_convert_hr_to_bytes(ini_get('post_max_size')) 109 ); 110 $server_max_mb = round($server_max_size / 1024 / 1024, 1); 111 $plugin_max_mb = $max_file_size; 112 113 // Use the smaller of plugin setting or server limit 114 $effective_max_mb = min($plugin_max_mb, $server_max_mb); 115 105 116 printf( 106 117 /* translators: 1: Maximum file size in MB, 2: Allowed file types (comma separated) */ 107 esc_html__('Max size: %1$sMB . Allowed types: %2$s', 'nexlifydesk'),108 esc_html($ max_file_size),118 esc_html__('Max size: %1$sMB per file. Allowed types: %2$s', 'nexlifydesk'), 119 esc_html($effective_max_mb), 109 120 esc_html($allowed_types) 110 ); ?> 121 ); 122 123 // Show server limit warning if it's lower than plugin setting 124 if ($server_max_mb < $plugin_max_mb) { 125 echo '<br><small style="color: #d63638;">'; 126 printf( 127 /* translators: %s: Server maximum file size in MB */ 128 esc_html__('Server limit: %sMB', 'nexlifydesk'), 129 esc_html($server_max_mb) 130 ); 131 echo '</small>'; 132 } 133 ?> 111 134 </p> 112 135 </div> -
nexlifydesk/trunk/templates/frontend/ticket-single.php
r3326104 r3330741 66 66 $replies = NexlifyDesk_Tickets::get_replies($ticket->id); 67 67 $initial_attachments = NexlifyDesk_Tickets::get_attachments($ticket->id); 68 69 // Extract customer details from ticket content for non-registered users 70 $customer_details = nexlifydesk_extract_customer_details($ticket->message); 71 $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest'); 72 $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A'); 73 $clean_initial_message = $customer_details['message'] ?: $ticket->message; 68 74 $user_initials = $user ? strtoupper(substr($user->first_name, 0, 1) . substr($user->last_name, 0, 1)) : 'G'; 69 75 if (empty(trim($user_initials))) { 70 $user_initials = strtoupper(substr($user->display_name, 0, 2));76 $user_initials = $user ? strtoupper(substr($user->display_name, 0, 2)) : strtoupper(substr($customer_name, 0, 2)); 71 77 } 72 78 … … 95 101 </div> 96 102 </div> 97 <div class="user-avatar" title="<?php echo esc_attr($ user ? $user->display_name : 'Guest'); ?>"><?php echo esc_html($user_initials); ?></div>103 <div class="user-avatar" title="<?php echo esc_attr($customer_name); ?>"><?php echo esc_html($user_initials); ?></div> 98 104 </div> 99 105 </div> … … 103 109 <div class="section-title"><?php esc_html_e('Description', 'nexlifydesk'); ?></div> 104 110 <div class="description"> 105 <?php echo nl2br(esc_html($ ticket->message)); ?>111 <?php echo nl2br(esc_html($clean_initial_message)); ?> 106 112 </div> 107 113
Note: See TracChangeset
for help on using the changeset viewer.