Plugin Directory

Changeset 3334167


Ignore:
Timestamp:
07/25/2025 12:22:43 PM (7 months ago)
Author:
nexlifycreator
Message:

feat: NexlifyDesk v1.0.5 - Critical Security Update + Smart Workflow Enhancements

SECURITY FIXES:
🔒 Resolved critical agent permission vulnerability preventing unauthorized ticket access
🔒 Enhanced agent role-based access control with proper assignment restrictions
🔒 Fixed agent reply permissions to prevent responses to unassigned tickets
🔒 Improved frontend ticket access control with better email verification

NEW FEATURES:
✨ Smart role-based read status system - agents handling tickets auto-marks for supervisors
✨ Professional dual-date columns (Created + Last Updated) with accurate timestamps
✨ Enhanced duplicate detection with better customer detail extraction
✨ Comprehensive cache clearing system for consistent ticket list updates

IMPROVEMENTS:
🎯 Enhanced agent position capabilities respecting "View All Tickets" permission
🎯 Improved ticket filtering in admin dashboard for proper agent visibility
🎯 Enhanced bulk action permissions restricted to assigned tickets only
🎯 Fixed ticket form redirect issues with proper URL handling
🎯 Improved frontend shortcode logic for customer vs agent access
🎯 Enhanced JavaScript form submission with server-provided redirect URLs
🎯 Better success message handling with ticket numbers and view links

BUG FIXES:
🐛 Fixed category deletion/reactivation misleading error messages
🐛 Resolved AJAX response issues returning HTML instead of JSON
🐛 Enhanced category management with proper output buffering
🐛 Improved JavaScript error handling with automatic page reload
🐛 Fixed category existence checks preventing race conditions
🐛 Enhanced admin category form processing for real-time accuracy
🐛 Optimized category cache management preventing conflicts

TECHNICAL:
⚡ Enhanced database query optimization with proper phpcs comments
⚡ Improved agent dashboard statistics showing only relevant metrics
⚡ Enhanced category cache management for deletion/recreation conflicts
⚡ Comprehensive output buffering and proper exit handling

Breaking Changes: None
Compatibility: WordPress 6.2+ | PHP 7.4+
Migration: Automatic database updates on activation

Location:
nexlifydesk
Files:
338 added
1 deleted
26 edited

Legend:

Unmodified
Added
Removed
  • nexlifydesk/trunk/assets/css/nexlifydesk-admin.css

    r3333095 r3334167  
    352352}
    353353
    354 /* Gmail-style ticket list */
    355354.nexlifydesk-admin-ticket-list-ui .bulk-actions {
    356355    display: flex;
     
    373372    border: 1px solid #e2e8f0;
    374373    overflow: hidden;
     374    min-width: auto;
     375    overflow-x: auto;
    375376}
    376377
    377378.nexlifydesk-admin-ticket-list-ui .ticket-list-header {
    378379    display: grid;
    379     grid-template-columns: 40px 3fr 2fr 80px 80px 120px 100px;
     380    grid-template-columns: 40px 2.5fr 1.8fr 85px 85px 130px 120px 120px;
    380381    gap: 12px;
    381382    padding: 12px 16px;
     
    389390}
    390391
     392.nexlifydesk-admin-ticket-list-ui .ticket-list-header > div {
     393    white-space: nowrap;
     394    overflow: hidden;
     395    text-overflow: ellipsis;
     396    display: flex;
     397    align-items: center;
     398    min-width: 0;
     399}
     400
    391401.nexlifydesk-admin-ticket-list-ui .ticket-row {
    392402    display: grid;
    393     grid-template-columns: 40px 3fr 2fr 80px 80px 120px 100px;
     403    grid-template-columns: 40px 2.5fr 1.8fr 85px 85px 130px 120px 120px;
    394404    gap: 12px;
    395405    padding: 16px;
     
    407417    .nexlifydesk-admin-ticket-list-ui .ticket-list-header,
    408418    .nexlifydesk-admin-ticket-list-ui .ticket-row {
    409         grid-template-columns: 40px 4fr 2fr 70px 70px 100px 90px;
     419        grid-template-columns: 40px 3fr 1.5fr 80px 80px 110px 110px 110px;
    410420        gap: 8px;
    411421    }
     
    415425    .nexlifydesk-admin-ticket-list-ui .ticket-list-header,
    416426    .nexlifydesk-admin-ticket-list-ui .ticket-row {
    417         grid-template-columns: 40px 5fr 2fr 60px 60px 80px 80px;
     427        grid-template-columns: 40px 4fr 2fr 70px 70px 90px 100px 100px;
    418428        gap: 6px;
     429    }
     430}
     431
     432@media (max-width: 768px) {
     433    .nexlifydesk-admin-ticket-list-ui .ticket-list {
     434        min-width: 800px;
     435    }
     436   
     437    .nexlifydesk-admin-ticket-list-ui .ticket-list-header,
     438    .nexlifydesk-admin-ticket-list-ui .ticket-row {
     439        grid-template-columns: 30px 3fr 1.5fr 60px 60px 80px 90px 90px;
     440        gap: 4px;
     441        padding: 12px 8px;
     442        font-size: 11px;
     443    }
     444   
     445    .nexlifydesk-admin-ticket-list-ui .ticket-list-header {
     446        font-size: 10px;
     447    }
     448   
     449    .nexlifydesk-admin-ticket-list-ui .date-time {
     450        font-size: 10px;
     451    }
     452   
     453    .nexlifydesk-admin-ticket-list-ui .time-ago {
     454        font-size: 9px;
    419455    }
    420456}
     
    615651}
    616652
    617 .nexlifydesk-admin-ticket-list-ui .row-date {
     653.nexlifydesk-admin-ticket-list-ui .row-date,
     654.nexlifydesk-admin-ticket-list-ui .row-created,
     655.nexlifydesk-admin-ticket-list-ui .row-updated {
    618656    display: flex;
    619657    flex-direction: column;
    620658    gap: 2px;
     659    min-width: 0;
     660    white-space: nowrap;
     661    overflow: hidden;
    621662}
    622663
     
    625666    color: #1e293b;
    626667    font-weight: 500;
     668    white-space: nowrap;
     669    overflow: hidden;
     670    text-overflow: ellipsis;
    627671}
    628672
     
    630674    font-size: 11px;
    631675    color: #64748b;
     676    white-space: nowrap;
     677    overflow: hidden;
     678    text-overflow: ellipsis;
    632679}
    633680
     
    13511398}
    13521399
    1353 /* IMAP Authentication Spam Protection Styles */
     1400/* IMAP Authentication Spam Protection */
    13541401.nexlifydesk-spam-protection {
    13551402    border-top: 1px solid #ccd0d4;
     
    13771424}
    13781425
     1426/* Category Form Improvements */
     1427.nexlifydesk-form-messages {
     1428    margin: 15px 0;
     1429    padding: 12px 16px;
     1430    border-radius: 4px;
     1431    font-weight: 500;
     1432    display: none;
     1433}
     1434
     1435.nexlifydesk-form-messages:not(:empty) {
     1436    display: block;
     1437}
     1438
     1439.nexlifydesk-form-messages.success {
     1440    background-color: #d1e7dd;
     1441    border: 1px solid #badbcc;
     1442    color: #0f5132;
     1443}
     1444
     1445.nexlifydesk-form-messages.error {
     1446    background-color: #f8d7da;
     1447    border: 1px solid #f5c6cb;
     1448    color: #721c24;
     1449}
     1450
     1451.nexlifydesk-form-messages.notice {
     1452    background-color: #fff3cd;
     1453    border: 1px solid #ffecb5;
     1454    color: #664d03;
     1455}
     1456
     1457/* Loading state for forms */
     1458.nexlifydesk-form-loading {
     1459    opacity: 0.7;
     1460    pointer-events: none;
     1461    position: relative;
     1462}
     1463
     1464.nexlifydesk-form-loading::before {
     1465    content: "";
     1466    position: absolute;
     1467    top: 0;
     1468    left: 0;
     1469    right: 0;
     1470    bottom: 0;
     1471    background-color: rgba(255, 255, 255, 0.8);
     1472    z-index: 1;
     1473}
     1474
     1475.nexlifydesk-form-loading::after {
     1476    content: "";
     1477    position: absolute;
     1478    top: 50%;
     1479    left: 50%;
     1480    width: 20px;
     1481    height: 20px;
     1482    margin: -10px 0 0 -10px;
     1483    border: 2px solid #ccc;
     1484    border-top-color: #0073aa;
     1485    border-radius: 50%;
     1486    animation: nexlifydesk-spin 1s linear infinite;
     1487    z-index: 2;
     1488}
     1489
     1490@keyframes nexlifydesk-spin {
     1491    to {
     1492        transform: rotate(360deg);
     1493    }
     1494}
     1495
     1496/* Category form specific improvements */
     1497#nexlifydesk-category-form {
     1498    background-color: #fff;
     1499    border: 1px solid #ccd0d4;
     1500    border-radius: 4px;
     1501    padding: 20px;
     1502    margin: 20px 0;
     1503    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
     1504}
     1505
     1506#nexlifydesk-category-form .form-table th {
     1507    width: 200px;
     1508    padding: 15px 10px 15px 0;
     1509}
     1510
     1511#nexlifydesk-category-form .form-table td {
     1512    padding: 15px 10px;
     1513}
     1514
     1515#nexlifydesk-category-form input[type="text"],
     1516#nexlifydesk-category-form textarea {
     1517    width: 100%;
     1518    max-width: 500px;
     1519}
     1520
     1521#nexlifydesk-category-form input[type="text"]:disabled,
     1522#nexlifydesk-category-form textarea:disabled {
     1523    background-color: #f7f7f7;
     1524    cursor: not-allowed;
     1525}
     1526
     1527/* Category Button */
     1528.page-title-action {
     1529    text-decoration: none !important;
     1530}
     1531
     1532/* Category Management Table */
     1533.wp-list-table .column-actions {
     1534    width: 150px;
     1535}
     1536
     1537.wp-list-table .delete-category {
     1538    color: #d63638;
     1539}
     1540
     1541.wp-list-table .delete-category:hover {
     1542    color: #d63638;
     1543    text-decoration: underline;
     1544}
     1545
  • nexlifydesk/trunk/assets/css/nexlifydesk.css

    r3333095 r3334167  
    790790}
    791791
    792 /* --- Frontend Ticket List Styles (Table Design) --- */
     792/* --- Frontend Ticket List Styles --- */
    793793.nexlifydesk-table-container {
    794794    width: 100%;
  • nexlifydesk/trunk/assets/js/admin-ticket-list.js

    r3330751 r3334167  
    509509                ` : '<span class="unassigned">Unassigned</span>'}
    510510            </div>
    511             <div class="row-date">
    512                 <span class="date-time">${formatDate(lastReplyTime)}</span>
    513                 <span class="time-ago">${getTimeAgo(lastReplyTime)}</span>
     511            <div class="row-created">
     512                <span class="date-time">${formatDate(ticket.created_at)}</span>
     513                <span class="time-ago">${getTimeAgo(ticket.created_at)}</span>
     514            </div>
     515            <div class="row-updated">
     516                <span class="date-time">${formatDate(ticket.last_reply_time || lastReplyTime)}</span>
     517                <span class="time-ago">${getTimeAgo(ticket.last_reply_time || lastReplyTime)}</span>
    514518            </div>
    515519        </div>
  • nexlifydesk/trunk/assets/js/nexlifydesk.js

    r3333214 r3334167  
    2121        $('.page-title-action').on('click', function(e) {
    2222            e.preventDefault();
    23             $('#nexlifydesk-category-form').slideToggle();
     23            var $form = $('#nexlifydesk-category-form');
     24            var $button = $(this);
     25           
     26            $form.slideToggle(300, function() {
     27                if ($form.is(':visible')) {
     28                    var cancelText = (typeof nexlifydesk_admin_vars !== 'undefined' && nexlifydesk_admin_vars.cancel_text) ||
     29                                   (typeof nexlifydesk_vars !== 'undefined' && nexlifydesk_vars.cancel_text) || 'Cancel';
     30                    $button.text(cancelText);
     31                    $form.find('#category_name').focus();
     32                   
     33                    $form.find('.nexlifydesk-form-messages').removeClass('success error notice').empty();
     34                   
     35                    if ($form.find('#category_name').val() || $form.find('#category_description').val()) {
     36                        $form[0].reset();
     37                    }
     38                } else {
     39                    var addNewText = (typeof nexlifydesk_admin_vars !== 'undefined' && nexlifydesk_admin_vars.add_new_text) ||
     40                                   (typeof nexlifydesk_vars !== 'undefined' && nexlifydesk_vars.add_new_text) || 'Add New';
     41                    $button.text(addNewText);
     42                }
     43            });
    2444        });
    2545
     
    2747            e.preventDefault();
    2848            if (!isPluginValid) return;
    29             if (!confirm(nexlifydesk_admin_vars.confirm_delete)) return;
    30 
     49           
    3150            var categoryId = $(this).data('id');
     51            var $button = $(this);
     52           
     53            if (!confirm('Are you sure you want to delete this category? This action cannot be undone.')) {
     54                return;
     55            }
     56           
    3257            $.ajax({
    3358                url: nexlifydesk_admin_vars.ajaxurl,
     
    4065                success: function(response) {
    4166                    if (response.success) {
     67                        alert(response.data);
    4268                        location.reload();
    4369                    } else {
    44                         alert(response.data);
     70                        if (response.data && typeof response.data === 'object' && response.data.type === 'reassignment_required') {
     71                            showReassignmentModal(categoryId, response.data);
     72                        } else {
     73                            alert(response.data || 'Failed to delete category.');
     74                        }
    4575                    }
     76                },
     77                error: function() {
     78                    alert('An error occurred while deleting the category.');
    4679                }
    4780            });
    4881        });
     82
     83        function showReassignmentModal(categoryId, data) {
     84            var modalHtml = '<div id="category-reassignment-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 10000; display: flex; align-items: center; justify-content: center;">';
     85            modalHtml += '<div style="background: white; padding: 30px; border-radius: 8px; max-width: 500px; width: 90%;">';
     86            modalHtml += '<h2 style="margin-top: 0; color: #d63638;">⚠️ Warning: Category Deletion</h2>';
     87            modalHtml += '<p><strong>' + data.message + '</strong></p>';
     88            modalHtml += '<p style="color: #666; margin: 15px 0;">This action cannot be undone. All tickets will be moved to the selected category.</p>';
     89            modalHtml += '<div style="margin: 20px 0;">';
     90            modalHtml += '<label for="reassign-category" style="display: block; margin-bottom: 8px; font-weight: bold;">Select destination category:</label>';
     91            modalHtml += '<select id="reassign-category" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">';
     92            modalHtml += '<option value="">-- Select Category --</option>';
     93           
     94            if (data.available_categories) {
     95                data.available_categories.forEach(function(category) {
     96                    modalHtml += '<option value="' + category.id + '">' + category.name + '</option>';
     97                });
     98            }
     99           
     100            modalHtml += '</select>';
     101            modalHtml += '</div>';
     102            modalHtml += '<div style="text-align: right; margin-top: 25px;">';
     103            modalHtml += '<button id="cancel-reassignment" style="margin-right: 10px; padding: 8px 16px; border: 1px solid #ddd; background: #f7f7f7; border-radius: 4px; cursor: pointer;">Cancel</button>';
     104            modalHtml += '<button id="confirm-reassignment" style="padding: 8px 16px; background: #d63638; color: white; border: none; border-radius: 4px; cursor: pointer;">Delete Category & Reassign Tickets</button>';
     105            modalHtml += '</div>';
     106            modalHtml += '</div>';
     107            modalHtml += '</div>';
     108           
     109            $('body').append(modalHtml);
     110           
     111            $('#cancel-reassignment, #category-reassignment-modal').on('click', function(e) {
     112                if (e.target === this) {
     113                    $('#category-reassignment-modal').remove();
     114                }
     115            });
     116           
     117            $('#confirm-reassignment').on('click', function() {
     118                var reassignTo = $('#reassign-category').val();
     119                if (!reassignTo) {
     120                    alert('Please select a category for reassignment.');
     121                    return;
     122                }
     123               
     124                $.ajax({
     125                    url: nexlifydesk_admin_vars.ajaxurl,
     126                    type: 'POST',
     127                    data: {
     128                        action: 'nexlifydesk_delete_category',
     129                        nonce: nexlifydesk_admin_vars.nonce,
     130                        category_id: categoryId,
     131                        reassign_to: reassignTo
     132                    },
     133                    success: function(response) {
     134                        $('#category-reassignment-modal').remove();
     135                        if (response.success) {
     136                            alert(response.data);
     137                            location.reload();
     138                        } else {
     139                            alert(response.data || 'Failed to delete category.');
     140                        }
     141                    },
     142                    error: function() {
     143                        $('#category-reassignment-modal').remove();
     144                        alert('An error occurred while deleting the category.');
     145                    }
     146                });
     147            });
     148        }
    49149
    50150        $('#nexlifydesk-tickets-filter').on('submit', function(e) {
     
    106206            var form = $(this);
    107207            var formData = new FormData(form[0]);
     208           
     209            formData.append('current_url', window.location.href);
     210           
    108211            var submitBtn = $('#submit-ticket-btn');
    109212            var buttonText = submitBtn.find('.button-text');
     
    126229                            return;
    127230                        }
     231                       
    128232                        $('#nexlifydesk-message')
    129233                            .removeClass('error')
     
    131235                            .text(response.data.message || nexlifydesk_vars.ticket_submitted_text)
    132236                            .show();
    133                        
    134                         if (response.data.ticket_id && response.data.ticket_number) {
    135                             var redirectUrl = window.location.href;
    136                             if (redirectUrl.indexOf('?') !== -1) {
    137                                 redirectUrl += '&';
    138                             } else {
    139                                 redirectUrl += '?';
    140                             }
    141                             redirectUrl += 'ticket_submitted=1&ticket_id=' + response.data.ticket_id + '&ticket_number=' + response.data.ticket_number;
    142                            
    143                             setTimeout(function() {
    144                                 window.location.href = redirectUrl;
    145                             }, 1500);
    146                         }
    147237                       
    148238                        form[0].reset();
     
    190280            var $form = $(this);
    191281            var $submitButton = $form.find('input[type="submit"]');
    192             var $messageContainer = $('<div class="nexlifydesk-form-messages"></div>').prependTo($form);
    193 
    194             $messageContainer.empty();
    195 
    196             $submitButton.prop('disabled', true).val(nexlifydesk_vars.adding_text);
     282            var $categoryName = $form.find('#category_name');
     283            var $categoryDescription = $form.find('#category_description');
     284            var vars = (typeof nexlifydesk_admin_vars !== 'undefined') ? nexlifydesk_admin_vars :
     285                      (typeof nexlifydesk_vars !== 'undefined') ? nexlifydesk_vars : {};
     286           
     287            var $messageContainer = $form.find('.nexlifydesk-form-messages');
     288            if ($messageContainer.length === 0) {
     289                $messageContainer = $('<div class="nexlifydesk-form-messages"></div>').prependTo($form);
     290            }
     291            $messageContainer.removeClass('success error notice').empty();
     292
     293            var categoryName = $categoryName.val().trim();
     294            if (!categoryName) {
     295                $messageContainer.addClass('error').text(vars.required_fields_text || 'Category name is required.');
     296                $categoryName.focus();
     297                return;
     298            }
     299
     300            $submitButton.prop('disabled', true).val(vars.adding_text || 'Adding...');
     301            $categoryName.prop('disabled', true);
     302            $categoryDescription.prop('disabled', true);
     303            $form.addClass('nexlifydesk-form-loading');
    197304
    198305            var formData = {
    199306                action: 'nexlifydesk_add_category',
    200307                nexlifydesk_category_nonce: $form.find('input[name="nexlifydesk_category_nonce"]').val(),
    201                 category_name: $form.find('#category_name').val(),
    202                 category_description: $form.find('#category_description').val(),
     308                category_name: categoryName,
     309                category_description: $categoryDescription.val().trim(),
    203310                submit_category: true
    204311            };
    205312
    206313            $.ajax({
    207                 url: nexlifydesk_admin_vars.ajaxurl,
     314                url: nexlifydesk_admin_vars.ajaxurl, 
    208315                type: 'POST',
    209316                data: formData,
     317                timeout: 10000,
    210318                success: function(response) {
    211                     if (response.success) {
    212                         $messageContainer.addClass('success').text(response.data.message);
     319                    if (typeof response === 'string' && response.trim().startsWith('<!DOCTYPE html>')) {
     320                        location.reload();
     321                        return;
     322                    }
     323                   
     324                    if (response && response.success) {
     325                        $messageContainer.addClass('success').html('<strong>Success!</strong> ' + (response.data.message || 'Category added successfully.'));
     326                       
     327                        $form[0].reset();
     328                       
    213329                        setTimeout(function() {
    214                             location.reload();
    215                         }, 1000);
     330                            $form.slideUp(300, function() {
     331                                location.reload();
     332                            });
     333                        }, 1500);
    216334                    } else {
    217                         $messageContainer.addClass('error').text(response.data);
     335                        var errorMessage = 'Failed to add category.';
     336                        if (response && response.data) {
     337                            errorMessage = typeof response.data === 'string' ? response.data : (response.data.message || errorMessage);
     338                        }
     339                        $messageContainer.addClass('error').html('<strong>Error:</strong> ' + errorMessage);
     340                        $categoryName.focus();
    218341                    }
    219342                },
    220                 error: function() {
    221                     $messageContainer.addClass('error').text(nexlifydesk_vars.error_occurred_text);
     343                error: function(xhr, status, error) {
     344                    var errorText = vars.error_occurred_text || 'An error occurred. Please try again.';
     345                    if (status === 'timeout') {
     346                        errorText = 'Request timed out. Please try again.';
     347                    } else if (xhr.status === 403) {
     348                        errorText = 'Access denied. Please refresh the page and try again.';
     349                    }
     350                    $messageContainer.addClass('error').html('<strong>Error:</strong> ' + errorText);
     351                    $categoryName.focus();
    222352                },
    223353                complete: function() {
    224                     $submitButton.prop('disabled', false).val(nexlifydesk_vars.add_category_text);
     354                    $submitButton.prop('disabled', false).val(vars.add_category_text || 'Add Category');
     355                    $categoryName.prop('disabled', false);
     356                    $categoryDescription.prop('disabled', false);
     357                    $form.removeClass('nexlifydesk-form-loading');
    225358                }
    226359            });
     
    19592092}
    19602093
    1961 // Make functions globally available
    19622094window.nexlifydesk_update_templates = nexlifydesk_update_templates;
    19632095window.nexlifydesk_dismiss_notice = nexlifydesk_dismiss_notice;
  • nexlifydesk/trunk/email-source/nexlifydesk-email-pipe.php

    r3333214 r3334167  
    6262
    6363/**
    64  * Clean email subject for better duplicate detection
    65  * Removes Re:, Fwd:, etc. prefixes and normalizes the subject
    6664 *
    6765 * @param string $subject The email subject line
     
    9189    $table_name = $wpdb->prefix . 'nexlifydesk_tickets';
    9290
    93     // For non-registered users (user_id = 0)
    9491    if ($user_id == 0 && !empty($email)) {
    9592       
     
    164161
    165162function nexlifydesk_fetch_custom_emails() {
    166     // Check if IMAP extension is available
    167163    if (!extension_loaded('imap')) {
    168164        add_action('admin_notices', function() {
     
    186182    $delete_emails = isset($settings['delete_emails_after_fetch']) ? $settings['delete_emails_after_fetch'] : 1;
    187183   
    188     // Validate required settings
    189184    if (empty($host) || empty($port) || empty($username) || empty($password)) {
    190185        return array('error' => 'Custom IMAP/POP3 credentials not configured. Please configure Host, Port, Username, and Password.');
    191186    }
    192187
    193     // Initialize tracking variables
    194188    $tickets_created = 0;
    195189    $replies_added = 0;
     
    231225        $date = isset($overview->date) ? $overview->date : '';
    232226       
    233         // Decode MIME-encoded subject first, then apply general email content decoding
    234227        if (function_exists('nexlifydesk_decode_email_subject')) {
    235228            $subject = nexlifydesk_decode_email_subject($subject);
  • nexlifydesk/trunk/email-source/providers/aws-ses/aws-handler.php

    r3333214 r3334167  
    315315
    316316/**
    317  * AWS SES Authentication Handler
    318317 * Based on WP Mail SMTP Pro structure but adapted for NexlifyDesk
    319318 */
     
    333332   
    334333    /**
    335      * Check if SSL is enabled for the site
    336334     * Uses centralized SSL detection from helpers.php
    337335     */
  • nexlifydesk/trunk/email-source/providers/google/google-handler.php

    r3333214 r3334167  
    33if (!defined('ABSPATH')) exit;
    44
    5 // Ensure helpers are available for email decoding functions
    65if (!function_exists('nexlifydesk_decode_email_subject')) {
    76    require_once dirname(__FILE__) . '/../../includes/helpers.php';
     
    150149    $delete_emails = isset($settings['delete_emails_after_fetch']) ? $settings['delete_emails_after_fetch'] : 1;
    151150
    152     // Check if Google credentials are configured
    153151    if (empty($settings['google_client_id']) || empty($settings['google_client_secret']) || empty($settings['google_refresh_token'])) {
    154152        return array('error' => 'Google credentials not configured. Please authorize with Google first.');
     
    225223        $subject_header = array_values(array_filter($headers, fn($h) => $h['name'] === 'Subject'))[0]['value'] ?? '(No Subject)';
    226224
    227         // Decode MIME-encoded subject to fix encoding issues like "=?UTF-8?Q?..."
    228225        if (function_exists('nexlifydesk_decode_email_subject')) {
    229226            $subject_header = nexlifydesk_decode_email_subject($subject_header);
     
    438435
    439436/**
    440  * Parses the payload of a Gmail message to find the email body.
    441437 *
    442438 * @param array $payload The payload from the Gmail API message.
  • nexlifydesk/trunk/includes/class-nexlifydesk-admin.php

    r3333214 r3334167  
    578578           
    579579            <?php
    580             // Show IMAP extension warning if not available
    581580            if (!extension_loaded('imap')) {
    582581                echo '<div class="notice notice-error"><p><strong>' . esc_html__('Warning:', 'nexlifydesk') . '</strong> ' .
     
    628627       
    629628        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             }
     629            NexlifyDesk_Tickets::mark_ticket_read($ticket_id, get_current_user_id());
     630            wp_send_json_success('Ticket marked as read');
    635631        }
    636632       
     
    851847                    'save_text' => __('Save', 'nexlifydesk'),
    852848                    'cancel_text' => __('Cancel', 'nexlifydesk'),
     849                    'add_new_text' => __('Add New', 'nexlifydesk'),
     850                    'adding_text' => __('Adding...', 'nexlifydesk'),
     851                    'add_category_text' => __('Add Category', 'nexlifydesk'),
     852                    'error_occurred_text' => __('An error occurred. Please try again.', 'nexlifydesk'),
     853                    'required_fields_text' => __('Please fill in all required fields.', 'nexlifydesk'),
    853854                    'delete_confirm' => __('Are you sure you want to delete this position?', 'nexlifydesk'),
    854855                    'loading_tickets_text' => __('Loading tickets...', 'nexlifydesk'),
     
    939940        if (!current_user_can('nexlifydesk_view_all_tickets')) {
    940941            $ticket = NexlifyDesk_Tickets::get_ticket($ticket_id);
    941             if (!$ticket || (int)$ticket->user_id !== get_current_user_id()) {
     942            $current_user = wp_get_current_user();
     943            $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     944           
     945            if (!$ticket) {
     946                wp_die(esc_html__('Ticket not found.', 'nexlifydesk'));
     947            }
     948           
     949            if ($is_agent) {
     950                $can_view_all = NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', get_current_user_id());
     951                if (!$can_view_all && (int)$ticket->assigned_to !== get_current_user_id()) {
     952                    wp_die(esc_html__('You do not have permission to view this ticket.', 'nexlifydesk'));
     953                }
     954            } elseif (!$is_agent && (int)$ticket->user_id !== get_current_user_id()) {
    942955                wp_die(esc_html__('You do not have permission to view this ticket.', 'nexlifydesk'));
    943956            }
     
    964977            $ticket = NexlifyDesk_Tickets::get_ticket($ticket_id);
    965978            if ($ticket) {
     979                if (method_exists('NexlifyDesk_Tickets', 'mark_ticket_read')) {
     980                    NexlifyDesk_Tickets::mark_ticket_read($ticket_id);
     981                }
    966982                include NEXLIFYDESK_PLUGIN_DIR . 'templates/admin/ticket-single.php';
    967983            } else {
     
    15061522                'default_category' => isset($_POST['default_category']) ? absint($_POST['default_category']) : 0,
    15071523                'sla_response_time' => isset($_POST['sla_response_time']) ? absint($_POST['sla_response_time']) : 0,
    1508                 'ticket_page_id' => isset($_POST['ticket_page_id']) ? absint($_POST['ticket_page_id']) : 0,
    1509                 'ticket_form_page_id' => isset($_POST['ticket_form_page_id']) ? absint($_POST['ticket_form_page_id']) : 0,
     1524                'ticket_page_id' => isset($_POST['ticket_page_id']) && is_array($_POST['ticket_page_id']) ? array_map('absint', wp_unslash($_POST['ticket_page_id'])) : array(),
     1525                'ticket_form_page_id' => isset($_POST['ticket_form_page_id']) && is_array($_POST['ticket_form_page_id']) ? array_map('absint', wp_unslash($_POST['ticket_form_page_id'])) : array(),
    15101526                'ticket_id_prefix' => isset($_POST['ticket_id_prefix']) ? sanitize_text_field(wp_unslash($_POST['ticket_id_prefix'])) : '',
    15111527                'ticket_id_start' => isset($_POST['ticket_id_start']) ? absint($_POST['ticket_id_start']) : 0,
     
    15441560
    15451561            $cache_key = 'nexlifydesk_category_slug_check_admin_add_' . md5($slug . '_' . get_current_user_id());
    1546             $existing = wp_cache_get($cache_key);
    1547 
    1548             if (false === $existing) {
    1549                 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled, query is prepared
    1550                 $existing = $wpdb->get_var(
    1551                     $wpdb->prepare(
    1552                         "SELECT id FROM `" . esc_sql($table_name) . "` WHERE slug = %s AND is_active = %d",
    1553                         $slug,
    1554                         1
    1555                     )
    1556                 );
    1557                 wp_cache_set($cache_key, $existing, '', 300);
    1558             }
     1562            wp_cache_delete($cache_key);
     1563
     1564            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is safe and controlled, query is prepared, no caching needed for real-time category existence check to prevent race conditions
     1565            $existing = $wpdb->get_row(
     1566                $wpdb->prepare(
     1567                    "SELECT id, is_active FROM `" . esc_sql($table_name) . "` WHERE slug = %s",
     1568                    $slug
     1569                )
     1570            );
    15591571
    15601572            if ($existing) {
    1561                 add_action('admin_notices', function() {
    1562                     echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__('A category with this name already exists.', 'nexlifydesk') . '</p></div>';
    1563                 });
    1564                 return;
     1573                if ($existing->is_active == 1) {
     1574                    add_action('admin_notices', function() {
     1575                        echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__('A category with this name already exists.', 'nexlifydesk') . '</p></div>';
     1576                    });
     1577                    return;
     1578                } else {
     1579                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
     1580                    $result = $wpdb->update(
     1581                        $table_name,
     1582                        array(
     1583                            'name' => $category_name,
     1584                            'description' => $category_description,
     1585                            'is_active' => 1
     1586                        ),
     1587                        array('id' => $existing->id),
     1588                        array('%s', '%s', '%d'),
     1589                        array('%d')
     1590                    );
     1591
     1592                    if ($result === false) {
     1593                        add_action('admin_notices', function() use ($wpdb) {
     1594                            echo '<div class="notice notice-error is-dismissible"><p>' .
     1595                                sprintf(
     1596                                    /* translators: %s: Database error message */
     1597                                    esc_html__('Failed to reactivate category: %s', 'nexlifydesk'),
     1598                                    esc_html($wpdb->last_error ?: 'Unknown database error')
     1599                                ) .
     1600                            '</p></div>';
     1601                        });
     1602                        return;
     1603                    }
     1604
     1605                    self::clear_all_category_caches($existing->id);
     1606
     1607                    wp_redirect(add_query_arg(array(
     1608                        'page' => 'nexlifydesk_categories',
     1609                        'category_reactivated' => 'true'
     1610                    ), admin_url('admin.php')));
     1611                    exit;
     1612                }
    15651613            }
    15661614
     
    15941642
    15951643            wp_cache_delete($cache_key);
     1644           
     1645            self::clear_all_category_caches($wpdb->insert_id);
    15961646
    15971647            if (defined('DOING_AJAX') && DOING_AJAX) {
     
    16281678       
    16291679        return !empty($valid_types) ? implode(',', array_unique($valid_types)) : 'jpg,jpeg,png,pdf';
     1680    }
     1681
     1682    /**
     1683     * Clear all category-related caches globally
     1684     *
     1685     * @param int $category_id Optional specific category ID
     1686     */
     1687    public static function clear_all_category_caches($category_id = null) {
     1688        wp_cache_delete('nexlifydesk_categories');
     1689        wp_cache_delete('nexlifydesk_categories_admin_page');
     1690        wp_cache_flush_group('nexlifydesk_categories');
     1691       
     1692        if ($category_id) {
     1693            wp_cache_delete('nexlifydesk_category_' . $category_id);
     1694        }
     1695       
     1696        global $wp_object_cache;
     1697        if (is_object($wp_object_cache) && method_exists($wp_object_cache, 'cache')) {
     1698            foreach ($wp_object_cache->cache as $group => $cache_group) {
     1699                if (is_array($cache_group)) {
     1700                    foreach ($cache_group as $key => $value) {
     1701                        if (strpos($key, 'nexlifydesk_category_slug_check_') === 0) {
     1702                            wp_cache_delete($key, $group);
     1703                        }
     1704                    }
     1705                }
     1706            }
     1707        }
     1708       
     1709        if (function_exists('wp_cache_flush')) {
     1710            wp_cache_flush();
     1711        }
     1712    }
     1713
     1714    /**
     1715     * Get ticket count for a category
     1716     *
     1717     * @param int $category_id
     1718     * @return int Number of tickets in this category
     1719     */
     1720    public static function get_category_ticket_count($category_id) {
     1721        global $wpdb;
     1722       
     1723        $cache_key = 'nexlifydesk_category_ticket_count_' . $category_id;
     1724        $count = wp_cache_get($cache_key);
     1725       
     1726        if (false === $count) {
     1727            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Safe query for counting
     1728            $count = $wpdb->get_var(
     1729                $wpdb->prepare(
     1730                    "SELECT COUNT(*) FROM {$wpdb->prefix}nexlifydesk_tickets WHERE category_id = %d",
     1731                    $category_id
     1732                )
     1733            );
     1734            wp_cache_set($cache_key, $count, '', 300);
     1735        }
     1736       
     1737        return (int) $count;
     1738    }
     1739
     1740    /**
     1741     * Get all categories for reassignment dropdown
     1742     *
     1743     * @param int $exclude_category_id Category ID to exclude
     1744     * @return array Available categories
     1745     */
     1746    public static function get_categories_for_reassignment($exclude_category_id) {
     1747        global $wpdb;
     1748       
     1749        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is safe and controlled, query is prepared, no caching needed for real-time category existence check to prevent race conditions
     1750        $categories = $wpdb->get_results(
     1751            $wpdb->prepare(
     1752                "SELECT id, name FROM {$wpdb->prefix}nexlifydesk_categories
     1753                 WHERE is_active = 1 AND id != %d
     1754                 ORDER BY name ASC",
     1755                $exclude_category_id
     1756            )
     1757        );
     1758       
     1759        return $categories;
    16301760    }
    16311761
     
    17461876                <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>
    17471877                <?php
    1748                 // Show default template as placeholder if empty
    17491878                $auto_response_content = isset($templates['email_auto_response']) ? $templates['email_auto_response'] : '';
    17501879                if (empty($auto_response_content)) {
    1751                     // Default HTML formatted auto-response template
    17521880                    $auto_response_content = '<p>Hello {customer_name},</p>
    17531881
  • nexlifydesk/trunk/includes/class-nexlifydesk-ajax.php

    r3333214 r3334167  
    3737       
    3838        if (isset($_FILES['attachments']) && isset($_FILES['attachments']['error']) && is_array($_FILES['attachments']['error'])) {
    39             // Sanitize the error array by casting each value to integer
    4039            $attachment_errors = array_map('intval', $_FILES['attachments']['error']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via array_map with intval
    4140           
     
    126125        } else {
    127126            $settings = get_option('nexlifydesk_settings', array());
    128             $ticket_page_id = isset($settings['ticket_page_id']) ? (int)$settings['ticket_page_id'] : 0;
    129            
    130             if ($ticket_page_id > 0) {
     127            $ticket_page_ids = isset($settings['ticket_page_id']) ? $settings['ticket_page_id'] : array();
     128           
     129            if (!is_array($ticket_page_ids)) {
     130                $ticket_page_ids = $ticket_page_ids ? array($ticket_page_ids) : array();
     131            }
     132           
     133            $ticket_page_url = '';
     134            if (!empty($ticket_page_ids)) {
     135                $ticket_page_id = (int)$ticket_page_ids[0];
    131136                $ticket_page_url = get_permalink($ticket_page_id);
    132             } else {
     137            }
     138           
     139            if (!$ticket_page_url || !wp_http_validate_url($ticket_page_url)) {
    133140                $ticket_page_url = isset($_POST['current_url']) ? esc_url_raw(wp_unslash($_POST['current_url'])) : home_url();
    134141            }
     
    153160                $redirect_url = add_query_arg(array(
    154161                    'ticket_id' => $ticket->ticket_id,
     162                    'ticket_number' => $ticket->ticket_id,
    155163                    'ticket_submitted' => 1
    156164                ), $ticket_page_url);
     
    158166                wp_send_json_success(array(
    159167                    'message' => __('Ticket submitted successfully!', 'nexlifydesk'),
    160                     'redirect' => $redirect_url
     168                    'redirect' => $redirect_url,
     169                    'ticket_id' => $ticket->ticket_id,
     170                    'ticket_number' => $ticket->ticket_id
    161171                ));
    162172            }
     
    192202        $can_reply = false;
    193203       
    194         if (current_user_can('nexlifydesk_manage_tickets') || current_user_can('manage_options')) {
     204        if (current_user_can('manage_options')) {
    195205            $can_reply = true;
     206        }
     207        elseif (current_user_can('nexlifydesk_manage_tickets')) {
     208            $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     209            if ($is_agent) {
     210                $can_view_all = NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $current_user->ID);
     211                if ($can_view_all || (int)$ticket->assigned_to === (int)$current_user->ID) {
     212                    $can_reply = true;
     213                }
     214            } else {
     215                $can_reply = true;
     216            }
    196217        }
    197218       
     
    312333        if (!$ticket) {
    313334            wp_send_json_error(__('Ticket not found.', 'nexlifydesk'));
     335        }
     336
     337        $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     338        if ($is_agent) {
     339            $can_view_all = NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $current_user->ID);
     340            if (!$can_view_all && (int)$ticket->assigned_to !== (int)$current_user->ID) {
     341                wp_send_json_error(__('You can only add notes to tickets assigned to you.', 'nexlifydesk'));
     342            }
    314343        }
    315344
     
    671700
    672701    public static function add_category() {
    673         check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     702        if (ob_get_level()) {
     703            ob_end_clean();
     704        }
     705        ob_start();
     706       
     707        if (!headers_sent()) {
     708            header('Content-Type: application/json; charset=utf-8');
     709        }
     710       
     711        if (!isset($_POST['nexlifydesk_category_nonce']) ||
     712            !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nexlifydesk_category_nonce'])), 'nexlifydesk_save_category')) {
     713            ob_end_clean();
     714            wp_send_json_error(__('Security check failed. Please refresh the page and try again.', 'nexlifydesk'));
     715            exit;
     716        }
    674717
    675718        if (!current_user_can('manage_options')) {
     719            ob_end_clean();
    676720            wp_send_json_error(__('You do not have permission to add categories.', 'nexlifydesk'));
     721            exit;
    677722        }
    678723
     
    680725
    681726        if (empty($_POST['category_name'])) {
     727            ob_end_clean();
    682728            wp_send_json_error(__('Category name is required.', 'nexlifydesk'));
     729            exit;
    683730        }
    684731
     
    686733        $category_description = isset($_POST['category_description']) ?
    687734            sanitize_textarea_field(wp_unslash($_POST['category_description'])) : '';
    688         $slug = sanitize_title($category_name);
     735        $slug = sanitize_title($category_name);
     736
     737        if (empty($slug)) {
     738            ob_end_clean();
     739            wp_send_json_error(__('Invalid category name - unable to generate slug.', 'nexlifydesk'));
     740            exit;
     741        }
    689742
    690743        $table_name = NexlifyDesk_Database::get_table('categories');
    691744
    692745        $cache_key = 'nexlifydesk_category_slug_check_' . md5($slug);
    693         $existing = wp_cache_get($cache_key);
    694 
    695         if (false === $existing) {
    696             $query = "SELECT id FROM " . $table_name . " WHERE slug = %s AND is_active = 1";
    697             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
    698             $existing = $wpdb->get_var(
    699                 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is safe and controlled
    700                 $wpdb->prepare($query, $slug)
    701             );
    702            
    703             wp_cache_set($cache_key, $existing, '', 300);
    704         }
     746        wp_cache_delete($cache_key);
     747
     748        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is safe and controlled, no caching needed for real-time category existence check to prevent race conditions
     749        $existing = $wpdb->get_row(
     750            $wpdb->prepare(
     751                "SELECT id, is_active FROM {$table_name} WHERE slug = %s",
     752                $slug
     753            )
     754        );
    705755
    706756        if ($existing) {
    707             wp_send_json_error(__('A category with this name already exists.', 'nexlifydesk'));
     757            if ($existing->is_active == 1) {
     758                ob_end_clean();
     759                wp_send_json_error(__('A category with this name already exists.', 'nexlifydesk'));
     760                exit;
     761            } else {
     762                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
     763                $result = $wpdb->update(
     764                    NexlifyDesk_Database::get_table('categories'),
     765                    array(
     766                        'name' => $category_name,
     767                        'description' => $category_description,
     768                        'is_active' => 1
     769                    ),
     770                    array('id' => $existing->id),
     771                    array('%s', '%s', '%d'),
     772                    array('%d')
     773                );
     774
     775                if ($result === false) {
     776                    ob_end_clean();
     777                    wp_send_json_error(
     778                        sprintf(
     779                            /* translators: %s: Database error message */
     780                            __('Failed to reactivate category: %s', 'nexlifydesk'),
     781                            $wpdb->last_error ?: 'Unknown database error'
     782                        )
     783                    );
     784                    exit;
     785                }
     786
     787                NexlifyDesk_Admin::clear_all_category_caches($existing->id);
     788
     789                ob_end_clean();
     790                wp_send_json_success(array(
     791                    'message' => __('Category reactivated successfully!', 'nexlifydesk'),
     792                    'category_id' => $existing->id
     793                ));
     794                exit;
     795            }
    708796        }
    709797
     
    721809
    722810        if ($result === false) {
     811            ob_end_clean();
    723812            wp_send_json_error(
    724813                sprintf(
    725814                    /* translators: %s: Database error message */
    726815                    __('Failed to add category: %s', 'nexlifydesk'),
    727                     $wpdb->last_error
     816                    $wpdb->last_error ?: 'Unknown database error'
    728817                )
    729818            );
    730         }
    731 
     819            exit;
     820        }
     821
     822        // Clear caches
     823        NexlifyDesk_Admin::clear_all_category_caches($wpdb->insert_id);
     824
     825        ob_end_clean();
    732826        wp_send_json_success(array(
    733827            'message' => __('Category added successfully!', 'nexlifydesk')
    734828        ));
     829        exit;
    735830    }
    736831
     
    752847            wp_send_json_error(__('Invalid category ID.', 'nexlifydesk'));
    753848        }
    754         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
     849
     850        $ticket_count = NexlifyDesk_Admin::get_category_ticket_count($category_id);
     851       
     852        if ($ticket_count > 0) {
     853            $available_categories = NexlifyDesk_Admin::get_categories_for_reassignment($category_id);
     854           
     855            if (empty($available_categories)) {
     856                wp_send_json_error(
     857                    sprintf(
     858                        /* translators: %d: Number of tickets */
     859                        __('Cannot delete category. It contains %d tickets and there are no other categories available for reassignment. Please create another category first.', 'nexlifydesk'),
     860                        $ticket_count
     861                    )
     862                );
     863            }
     864           
     865            $reassign_to = isset($_POST['reassign_to']) ? absint($_POST['reassign_to']) : 0;
     866           
     867            if (!$reassign_to) {
     868                wp_send_json_error(array(
     869                    'type' => 'reassignment_required',
     870                    'message' => sprintf(
     871                        /* translators: %d: Number of tickets */
     872                        __('This category contains %d tickets. Please select a category to reassign them to:', 'nexlifydesk'),
     873                        $ticket_count
     874                    ),
     875                    'ticket_count' => $ticket_count,
     876                    'available_categories' => $available_categories
     877                ));
     878            }
     879           
     880            $valid_reassignment = false;
     881            foreach ($available_categories as $cat) {
     882                if ($cat->id == $reassign_to) {
     883                    $valid_reassignment = true;
     884                    break;
     885                }
     886            }
     887           
     888            if (!$valid_reassignment) {
     889                wp_send_json_error(__('Invalid reassignment category selected.', 'nexlifydesk'));
     890            }
     891           
     892            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is safe and controlled, no caching needed for real-time category existence check to prevent race conditions
     893            $reassign_result = $wpdb->update(
     894                $wpdb->prefix . 'nexlifydesk_tickets',
     895                array('category_id' => $reassign_to),
     896                array('category_id' => $category_id),
     897                array('%d'),
     898                array('%d')
     899            );
     900           
     901            if ($reassign_result === false) {
     902                wp_send_json_error(__('Failed to reassign tickets. Category deletion cancelled.', 'nexlifydesk'));
     903            }
     904        }
     905       
     906        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is safe and controlled, no caching needed for real-time category existence check to prevent race conditions
    755907        $result = $wpdb->update(
    756908            NexlifyDesk_Database::get_table('categories'),
     
    762914   
    763915        if ($result) {
    764             wp_cache_delete('nexlifydesk_categories');
    765             wp_cache_delete('nexlifydesk_category_' . intval($category_id));
    766             wp_send_json_success(__('Category deleted successfully!', 'nexlifydesk'));
     916            NexlifyDesk_Admin::clear_all_category_caches($category_id);
     917           
     918            $success_message = __('Category deleted successfully!', 'nexlifydesk');
     919            if ($ticket_count > 0) {
     920                $success_message = sprintf(
     921                    /* translators: 1: Number of tickets */
     922                    __('Category deleted successfully! %1$d tickets were reassigned to the selected category.', 'nexlifydesk'),
     923                    $ticket_count
     924                );
     925            }
     926           
     927            wp_send_json_success($success_message);
    767928        } else {
    768929            wp_send_json_error(__('Could not delete category.', 'nexlifydesk'));
     
    9881149            }
    9891150           
    990             // Call the actual custom email fetch function to test it
    9911151            $result = nexlifydesk_fetch_custom_emails();
    9921152           
     
    11471307                /* translators: %d: Ticket ID */
    11481308                $errors[] = sprintf(__('Ticket #%d not found.', 'nexlifydesk'), $ticket_id);
     1309                continue;
     1310            }
     1311           
     1312            $current_user = wp_get_current_user();
     1313            $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     1314            if ($is_agent && (int)$ticket->assigned_to !== (int)$current_user->ID) {
     1315                /* translators: %d: Ticket ID */
     1316                $errors[] = sprintf(__('You cannot perform actions on ticket #%d as it is not assigned to you.', 'nexlifydesk'), $ticket_id);
    11491317                continue;
    11501318            }
     
    12001368                       
    12011369                    case 'delete':
    1202                         // Only allow admins to delete tickets
    12031370                        if (!current_user_can('manage_options')) {
    12041371                            /* translators: %d: Ticket ID */
     
    14981665                $result = nexlifydesk_fetch_google_emails();
    14991666               
    1500                 // Check if there was an error
    15011667                if (is_array($result) && isset($result['error'])) {
    15021668                    wp_send_json_error(array('message' => $result['error']));
     
    15041670                }
    15051671               
    1506                 // If we have a success result with details
    15071672                if (is_array($result) && isset($result['success']) && $result['success']) {
    15081673                    wp_send_json_success(array('message' => $result['message']));
     
    15131678            }
    15141679           
    1515             // Fallback for legacy behavior - check for recent tickets
    15161680            global $wpdb;
    15171681            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe and not user input
  • nexlifydesk/trunk/includes/class-nexlifydesk-database.php

    r3333214 r3334167  
    193193   
    194194    public static function check_and_run_migrations() {
    195         $current_version = get_option('nexlifydesk_db_version', '1.0.4');
     195        $current_version = get_option('nexlifydesk_db_version', '1.0.5');
    196196        $plugin_version = NEXLIFYDESK_VERSION;
    197197       
    198         if (version_compare($current_version, '1.0.4', '<')) {
     198        if (version_compare($current_version, '1.0.5', '<')) {
    199199            self::migrate_to_1_0_1();
    200             update_option('nexlifydesk_db_version', '1.0.4');
    201         }
    202        
    203         if (version_compare($current_version, '1.0.4', '<')) {
     200            update_option('nexlifydesk_db_version', '1.0.5');
     201        }
     202       
     203        if (version_compare($current_version, '1.0.5', '<')) {
    204204            self::migrate_to_1_0_2();
    205             update_option('nexlifydesk_db_version', '1.0.4');
     205            update_option('nexlifydesk_db_version', '1.0.5');
    206206        }
    207207       
     
    293293        global $wpdb;
    294294       
    295         $current_version = get_option('nexlifydesk_version', '1.0.4');
    296        
    297         if ($current_version === '1.0.4' && !get_option('nexlifydesk_db_version')) {
     295        $current_version = get_option('nexlifydesk_version', '1.0.5');
     296       
     297        if ($current_version === '1.0.5' && !get_option('nexlifydesk_db_version')) {
    298298            return;
    299299        }
     
    371371        }
    372372       
    373         // Save updated settings if any passwords were encrypted
    374373        if ($updated) {
    375374            update_option('nexlifydesk_imap_settings', $imap_settings);
  • nexlifydesk/trunk/includes/class-nexlifydesk-reports.php

    r3326104 r3334167  
    99        global $wpdb;
    1010       
    11         $cache_key = 'nexlifydesk_dashboard_stats_' . gmdate('Y-m-d-H');
     11        $current_user = wp_get_current_user();
     12        $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     13       
     14        // Check if agent can view all tickets through their position
     15        $agent_can_view_all = false;
     16        if ($is_agent) {
     17            $agent_can_view_all = NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $current_user->ID);
     18        }
     19       
     20        $cache_key = 'nexlifydesk_dashboard_stats_' . gmdate('Y-m-d-H') . ($is_agent ? '_agent_' . $current_user->ID . ($agent_can_view_all ? '_all' : '_assigned') : '_admin');
    1221        $stats = wp_cache_get($cache_key);
    1322       
     
    1524            $stats = array();
    1625           
     26            // Build WHERE clause for agents who can't view all tickets
     27            $where_clause = '';
     28            $params = array();
     29            if ($is_agent && !$agent_can_view_all) {
     30                $where_clause = ' WHERE assigned_to = %d';
     31                $params[] = $current_user->ID;
     32            }
     33           
    1734            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
    1835            // Total tickets
    19             $stats['total_tickets'] = $wpdb->get_var(
    20                 $wpdb->prepare("SELECT COUNT(*) FROM %i", $wpdb->prefix . 'nexlifydesk_tickets')
    21             );
     36            if ($is_agent && !$agent_can_view_all) {
     37                $stats['total_tickets'] = $wpdb->get_var(
     38                    $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE assigned_to = %d",
     39                    $wpdb->prefix . 'nexlifydesk_tickets', $current_user->ID)
     40                );
     41            } else {
     42                $stats['total_tickets'] = $wpdb->get_var(
     43                    $wpdb->prepare("SELECT COUNT(*) FROM %i", $wpdb->prefix . 'nexlifydesk_tickets')
     44                );
     45            }
    2246           
    2347            // Active tickets (open + pending + in_progress)
    24             $stats['active_tickets'] = $wpdb->get_var(
    25                 $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status IN ('open', 'pending', 'in_progress')",
    26                 $wpdb->prefix . 'nexlifydesk_tickets')
    27             );
     48            if ($is_agent && !$agent_can_view_all) {
     49                $stats['active_tickets'] = $wpdb->get_var(
     50                    $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status IN ('open', 'pending', 'in_progress') AND assigned_to = %d",
     51                    $wpdb->prefix . 'nexlifydesk_tickets', $current_user->ID)
     52                );
     53            } else {
     54                $stats['active_tickets'] = $wpdb->get_var(
     55                    $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status IN ('open', 'pending', 'in_progress')",
     56                    $wpdb->prefix . 'nexlifydesk_tickets')
     57                );
     58            }
    2859           
    2960            // Closed tickets
    30             $stats['closed_tickets'] = $wpdb->get_var(
    31                 $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status = 'closed'",
    32                 $wpdb->prefix . 'nexlifydesk_tickets')
    33             );
     61            if ($is_agent && !$agent_can_view_all) {
     62                $stats['closed_tickets'] = $wpdb->get_var(
     63                    $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status = 'closed' AND assigned_to = %d",
     64                    $wpdb->prefix . 'nexlifydesk_tickets', $current_user->ID)
     65                );
     66            } else {
     67                $stats['closed_tickets'] = $wpdb->get_var(
     68                    $wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status = 'closed'",
     69                    $wpdb->prefix . 'nexlifydesk_tickets')
     70                );
     71            }
    3472           
    3573            // Status breakdown
    36             $status_results = $wpdb->get_results(
    37                 $wpdb->prepare("SELECT status, COUNT(*) as count FROM %i GROUP BY status",
    38                 $wpdb->prefix . 'nexlifydesk_tickets')
    39             );
     74            if ($is_agent && !$agent_can_view_all) {
     75                $status_results = $wpdb->get_results(
     76                    $wpdb->prepare("SELECT status, COUNT(*) as count FROM %i WHERE assigned_to = %d GROUP BY status",
     77                    $wpdb->prefix . 'nexlifydesk_tickets', $current_user->ID)
     78                );
     79            } else {
     80                $status_results = $wpdb->get_results(
     81                    $wpdb->prepare("SELECT status, COUNT(*) as count FROM %i GROUP BY status",
     82                    $wpdb->prefix . 'nexlifydesk_tickets')
     83                );
     84            }
    4085           
    4186            $stats['status_breakdown'] = array();
     
    4590           
    4691            // Priority breakdown
    47             $priority_results = $wpdb->get_results(
    48                 $wpdb->prepare("SELECT priority, COUNT(*) as count FROM %i GROUP BY priority",
    49                 $wpdb->prefix . 'nexlifydesk_tickets')
    50             );
     92            if ($is_agent && !$agent_can_view_all) {
     93                $priority_results = $wpdb->get_results(
     94                    $wpdb->prepare("SELECT priority, COUNT(*) as count FROM %i WHERE assigned_to = %d GROUP BY priority",
     95                    $wpdb->prefix . 'nexlifydesk_tickets', $current_user->ID)
     96                );
     97            } else {
     98                $priority_results = $wpdb->get_results(
     99                    $wpdb->prepare("SELECT priority, COUNT(*) as count FROM %i GROUP BY priority",
     100                    $wpdb->prefix . 'nexlifydesk_tickets')
     101                );
     102            }
    51103            // phpcs:enable
    52104           
     
    57109           
    58110            // Average response time
    59             $stats['avg_response_time'] = self::calculate_avg_response_time();
     111            $stats['avg_response_time'] = self::calculate_avg_response_time(($is_agent && !$agent_can_view_all) ? $current_user->ID : null);
    60112           
    61113            // Monthly data for chart
    62             $stats['monthly_data'] = self::get_monthly_ticket_data();
     114            $stats['monthly_data'] = self::get_monthly_ticket_data(($is_agent && !$agent_can_view_all) ? $current_user->ID : null);
    63115           
    64116            // Agent performance
    65             $stats['agent_performance'] = self::get_agent_performance();
     117            $stats['agent_performance'] = self::get_agent_performance(($is_agent && !$agent_can_view_all) ? $current_user->ID : null);
    66118           
    67119            // Recent activity
    68             $stats['recent_activity'] = self::get_recent_activity();
     120            $stats['recent_activity'] = self::get_recent_activity(($is_agent && !$agent_can_view_all) ? $current_user->ID : null);
    69121           
    70122            wp_cache_set($cache_key, $stats, 'nexlifydesk', HOUR_IN_SECONDS);
     
    74126    }
    75127   
    76     private static function calculate_avg_response_time() {
    77         global $wpdb;
    78        
    79         $cache_key = 'nexlifydesk_avg_response_time';
     128    private static function calculate_avg_response_time($agent_id = null) {
     129        global $wpdb;
     130       
     131        $cache_key = 'nexlifydesk_avg_response_time' . ($agent_id ? '_agent_' . $agent_id : '');
    80132        $avg_response = wp_cache_get($cache_key);
    81133       
    82134        if (false === $avg_response) {
    83135            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    84             $avg_seconds = $wpdb->get_var(
    85                 $wpdb->prepare(
    86                     "SELECT AVG(TIMESTAMPDIFF(SECOND, t.created_at, r.created_at))
    87                     FROM %i t
    88                     INNER JOIN %i r ON t.id = r.ticket_id
    89                     WHERE r.is_admin_reply = 1
    90                     AND r.id = (SELECT MIN(id) FROM %i WHERE ticket_id = t.id AND is_admin_reply = 1)",
    91                     $wpdb->prefix . 'nexlifydesk_tickets',
    92                     $wpdb->prefix . 'nexlifydesk_replies',
    93                     $wpdb->prefix . 'nexlifydesk_replies'
    94                 )
    95             );
     136            if ($agent_id) {
     137                $avg_seconds = $wpdb->get_var(
     138                    $wpdb->prepare(
     139                        "SELECT AVG(TIMESTAMPDIFF(SECOND, t.created_at, r.created_at))
     140                        FROM %i t
     141                        INNER JOIN %i r ON t.id = r.ticket_id
     142                        WHERE r.is_admin_reply = 1
     143                        AND t.assigned_to = %d
     144                        AND r.id = (SELECT MIN(id) FROM %i WHERE ticket_id = t.id AND is_admin_reply = 1)",
     145                        $wpdb->prefix . 'nexlifydesk_tickets',
     146                        $wpdb->prefix . 'nexlifydesk_replies',
     147                        $agent_id,
     148                        $wpdb->prefix . 'nexlifydesk_replies'
     149                    )
     150                );
     151            } else {
     152                $avg_seconds = $wpdb->get_var(
     153                    $wpdb->prepare(
     154                        "SELECT AVG(TIMESTAMPDIFF(SECOND, t.created_at, r.created_at))
     155                        FROM %i t
     156                        INNER JOIN %i r ON t.id = r.ticket_id
     157                        WHERE r.is_admin_reply = 1
     158                        AND r.id = (SELECT MIN(id) FROM %i WHERE ticket_id = t.id AND is_admin_reply = 1)",
     159                        $wpdb->prefix . 'nexlifydesk_tickets',
     160                        $wpdb->prefix . 'nexlifydesk_replies',
     161                        $wpdb->prefix . 'nexlifydesk_replies'
     162                    )
     163                );
     164            }
    96165            // phpcs:enable
    97166            if ($avg_seconds) {
    98                 $hours = round($avg_seconds / 3600, 1);
    99                 $avg_response = $hours . 'h';
     167                $minutes = round($avg_seconds / 60, 1);
     168                $avg_response = $minutes . 'm';
    100169            } else {
    101170                $avg_response = 'N/A';
     
    108177    }
    109178   
    110     private static function get_monthly_ticket_data() {
     179    private static function get_monthly_ticket_data($agent_id = null) {
    111180        global $wpdb;
    112181       
     
    115184        for ($i = 29; $i >= 0; $i--) {
    116185            $date = gmdate('Y-m-d', strtotime("-$i days"));
    117             $cache_key = 'nexlifydesk_tickets_count_' . $date;
     186            $cache_key = 'nexlifydesk_tickets_count_' . $date . ($agent_id ? '_agent_' . $agent_id : '');
    118187            $count = wp_cache_get($cache_key, 'nexlifydesk');
    119188            if (false === $count) {
    120189                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
    121                 $count = $wpdb->get_var(
    122                     $wpdb->prepare(
    123                         "SELECT COUNT(*) FROM {$wpdb->prefix}nexlifydesk_tickets WHERE DATE(created_at) = %s",
    124                         $date
    125                     )
    126                 );
     190                if ($agent_id) {
     191                    $count = $wpdb->get_var(
     192                        $wpdb->prepare(
     193                            "SELECT COUNT(*) FROM {$wpdb->prefix}nexlifydesk_tickets WHERE DATE(created_at) = %s AND assigned_to = %d",
     194                            $date, $agent_id
     195                        )
     196                    );
     197                } else {
     198                    $count = $wpdb->get_var(
     199                        $wpdb->prepare(
     200                            "SELECT COUNT(*) FROM {$wpdb->prefix}nexlifydesk_tickets WHERE DATE(created_at) = %s",
     201                            $date
     202                        )
     203                    );
     204                }
    127205                // phpcs:enable
    128206                wp_cache_set($cache_key, $count, 'nexlifydesk', HOUR_IN_SECONDS);
     
    138216    }
    139217   
    140     private static function get_agent_performance() {
     218    private static function get_agent_performance($agent_id = null) {
    141219        global $wpdb;
    142220       
     
    195273            );
    196274           
    197             $avg_response_formatted = $avg_response ? round($avg_response / 3600, 1) . 'h' : 'N/A';
     275            $avg_response_formatted = $avg_response ? round($avg_response / 60, 1) . 'm' : 'N/A';
    198276           
    199277            $performance[] = array(
     
    209287    }
    210288   
    211     private static function get_recent_activity() {
     289    private static function get_recent_activity($agent_id = null) {
    212290        global $wpdb;
    213291       
  • nexlifydesk/trunk/includes/class-nexlifydesk-shortcodes.php

    r3333095 r3334167  
    1616        ), $atts, 'nexlifydesk_ticket_form');
    1717       
    18         if (self::is_documentation_page() && !self::is_actual_support_page()) {
    19             return '<div class="nexlifydesk-shortcode-placeholder" style="background: #f0f0f0; padding: 15px; border: 1px dashed #ccc; border-radius: 4px; margin: 10px 0; text-align: center; color: #666;"><strong>Shortcode:</strong> <code>[nexlifydesk_ticket_form]</code><br><small>This shortcode displays the ticket submission form for customers.</small></div>';
     18        if (!self::is_configured_form_page()) {
     19            return '[nexlifydesk_ticket_form]';
    2020        }
    2121       
     
    3838        ), $atts, 'nexlifydesk_ticket_list');
    3939       
    40         if (self::is_documentation_page() && !self::is_actual_support_page()) {
    41             return '<div class="nexlifydesk-shortcode-placeholder" style="background: #f0f0f0; padding: 15px; border: 1px dashed #ccc; border-radius: 4px; margin: 10px 0; text-align: center; color: #666;"><strong>Shortcode:</strong> <code>[nexlifydesk_ticket_list]</code><br><small>This shortcode displays the user\'s ticket history and allows them to view their support tickets.</small></div>';
     40        if (!self::is_configured_ticket_page()) {
     41            return '[nexlifydesk_ticket_list]';
    4242        }
    4343       
     
    8484        $current_user = wp_get_current_user();
    8585
    86         if (current_user_can('manage_options') || current_user_can('nexlifydesk_view_all_tickets')) {
     86        if (current_user_can('manage_options')) {
    8787            return true;
    8888        }
     
    9090        $ticket = NexlifyDesk_Tickets::get_ticket_by_ticket_id($ticket_id_param);
    9191
     92        if (!$ticket && is_numeric($ticket_id_param)) {
     93            $ticket = NexlifyDesk_Tickets::get_ticket(absint($ticket_id_param));
     94        }
     95
    9296        if (!$ticket) {
    9397            return false;
    9498        }
    9599
    96         return ((int)$ticket->user_id === (int)$current_user->ID);
     100        if ((int)$ticket->user_id === (int)$current_user->ID) {
     101            return true;
     102        }
     103
     104        if ((int)$ticket->user_id === 0) {
     105            $customer_email = get_post_meta($ticket->id, 'customer_email', true);
     106            if ($customer_email && $customer_email === $current_user->user_email) {
     107                return true;
     108            }
     109        }
     110
     111        return false;
    97112    }
    98113   
     
    118133       
    119134        $settings = get_option('nexlifydesk_settings', array());
    120         $ticket_form_page_id = isset($settings['ticket_form_page_id']) ? (int)$settings['ticket_form_page_id'] : 0;
    121        
    122         if ($ticket_form_page_id > 0) {
    123             $page = get_post($ticket_form_page_id);
     135        $ticket_form_page_ids = isset($settings['ticket_form_page_id']) ? $settings['ticket_form_page_id'] : array();
     136       
     137        if (!is_array($ticket_form_page_ids)) {
     138            $ticket_form_page_ids = $ticket_form_page_ids ? array($ticket_form_page_ids) : array();
     139        }
     140       
     141        if (!empty($ticket_form_page_ids)) {
     142            $page_id = $ticket_form_page_ids[0];
     143            $page = get_post($page_id);
    124144            if ($page && $page->post_status === 'publish') {
    125                 $url = get_permalink($ticket_form_page_id);
     145                $url = get_permalink($page_id);
    126146                if ($url && wp_http_validate_url($url)) {
    127147                    return $url;
     
    153173   
    154174    /**
    155      * Documentation page shortcode check
     175     * Check if current page is one of the configured ticket form pages
    156176     *
    157177     * @return bool
    158178     */
    159     private static function is_documentation_page() {
     179    private static function is_configured_form_page() {
    160180        global $post;
    161181       
     
    164184        }
    165185       
    166         $current_url = get_permalink();
    167         $page_slug = $post->post_name;
    168         $page_title = $post->post_title;
    169         $doc_keywords = array(
    170             'doc',
    171             'documentation',
    172             'guide',
    173             'help',
    174             'manual',
    175             'nexlifydesk-documentation',
    176             'nexlifydesk-guide',
    177             'nexlifydesk-help',
    178             'nexlifydesk-manual',
    179             'knowledge-base',
    180             'nexlifydesk',
    181             'kb',
    182             'faq',
    183             'getting-started',
    184             'configuration',
    185             'features-guide',
    186             'shortcodes-integration',
    187             'security-performance'
    188         );
    189        
    190         foreach ($doc_keywords as $keyword) {
    191             if (stripos($current_url, $keyword) !== false ||
    192                 stripos($page_slug, $keyword) !== false ||
    193                 stripos($page_title, $keyword) !== false) {
    194                 return true;
    195             }
    196         }
    197        
    198         if ($post->post_content) {
    199             $doc_shortcode_count = substr_count($post->post_content, 'nexlifydesk-shortcode-placeholder');
    200             $actual_shortcode_count = substr_count($post->post_content, '[nexlifydesk_');
    201            
    202             if ($doc_shortcode_count > 0 && $doc_shortcode_count >= $actual_shortcode_count) {
    203                 return true;
    204             }
    205         }
    206        
    207         return false;
    208     }
    209    
    210     /**
    211      * Check if this is an actual support page (form or list) that users interact with
     186        $settings = get_option('nexlifydesk_settings', array());
     187        $ticket_form_page_ids = isset($settings['ticket_form_page_id']) ? $settings['ticket_form_page_id'] : array();
     188       
     189        if (!is_array($ticket_form_page_ids)) {
     190            $ticket_form_page_ids = $ticket_form_page_ids ? array($ticket_form_page_ids) : array();
     191        }
     192       
     193        return !empty($ticket_form_page_ids) && in_array($post->ID, array_map('intval', $ticket_form_page_ids));
     194    }
     195   
     196    /**
     197     * Check if current page is one of the configured ticket list pages
    212198     *
    213199     * @return bool
    214200     */
    215     private static function is_actual_support_page() {
     201    private static function is_configured_ticket_page() {
     202        global $post;
     203       
     204        if (!$post) {
     205            return false;
     206        }
     207       
    216208        $settings = get_option('nexlifydesk_settings', array());
    217         $ticket_form_page_id = isset($settings['ticket_form_page_id']) ? (int)$settings['ticket_form_page_id'] : 0;
    218         $ticket_page_id = isset($settings['ticket_page_id']) ? (int)$settings['ticket_page_id'] : 0;
    219        
    220         global $post;
    221         if (!$post) {
    222             return false;
    223         }
    224        
    225         $current_page_id = $post->ID;
    226        
    227         if ($current_page_id === $ticket_form_page_id || $current_page_id === $ticket_page_id) {
    228             return true;
    229         }
    230        
    231         $page_title = strtolower($post->post_title);
    232         $page_slug = strtolower($post->post_name);
    233        
    234         $support_keywords = array(
    235             'submit',
    236             'ticket',
    237             'support',
    238             'contact',
    239             'help-desk',
    240             'helpdesk',
    241             'create-ticket',
    242             'new-ticket',
    243             'support-form',
    244             'ticket-form',
    245             'my-tickets',
    246             'view-tickets'
    247         );
    248        
    249         foreach ($support_keywords as $keyword) {
    250             if (strpos($page_title, $keyword) !== false || strpos($page_slug, $keyword) !== false) {
    251                 if (strpos($page_title, 'documentation') === false &&
    252                     strpos($page_title, 'guide') === false &&
    253                     strpos($page_slug, 'documentation') === false &&
    254                     strpos($page_slug, 'guide') === false) {
    255                     return true;
    256                 }
    257             }
    258         }
    259        
    260         return false;
     209        $ticket_page_ids = isset($settings['ticket_page_id']) ? $settings['ticket_page_id'] : array();
     210       
     211        if (!is_array($ticket_page_ids)) {
     212            $ticket_page_ids = $ticket_page_ids ? array($ticket_page_ids) : array();
     213        }
     214       
     215        return !empty($ticket_page_ids) && in_array($post->ID, array_map('intval', $ticket_page_ids));
    261216    }
    262217}
  • nexlifydesk/trunk/includes/class-nexlifydesk-tickets.php

    r3333214 r3334167  
    381381        }
    382382
    383         wp_cache_flush_group('nexlifydesk_tickets_grid');
     383        self::clear_all_ticket_caches();
     384       
     385        $ticket = self::get_ticket($data['ticket_id']);
     386        if ($ticket && !$data['is_internal_note']) {
     387            $users_to_mark_unread = array();
     388           
     389            if ($ticket->assigned_to && $ticket->assigned_to != $data['user_id']) {
     390                $users_to_mark_unread[] = $ticket->assigned_to;
     391            }
     392           
     393            $admin_users = get_users(array('role' => 'administrator'));
     394            foreach ($admin_users as $admin_user) {
     395                if ($admin_user->ID != $data['user_id'] && !in_array($admin_user->ID, $users_to_mark_unread)) {
     396                    $users_to_mark_unread[] = $admin_user->ID;
     397                }
     398            }
     399           
     400            if (class_exists('NexlifyDesk_Users')) {
     401                $all_agents = get_users(array('role' => 'nexlifydesk_agent'));
     402                foreach ($all_agents as $agent) {
     403                    if ($agent->ID != $data['user_id'] &&
     404                        !in_array($agent->ID, $users_to_mark_unread) &&
     405                        NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $agent->ID)) {
     406                        $users_to_mark_unread[] = $agent->ID;
     407                    }
     408                }
     409            }
     410           
     411            if (!empty($users_to_mark_unread)) {
     412                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     413                $wpdb->query(
     414                    $wpdb->prepare(
     415                        "DELETE FROM {$wpdb->prefix}nexlifydesk_ticket_reads
     416                         WHERE ticket_id = %d AND user_id IN (" . implode(',', array_fill(0, count($users_to_mark_unread), '%d')) . ")",
     417                        $data['ticket_id'],
     418                        ...$users_to_mark_unread
     419                    )
     420                );
     421            }
     422        }
    384423
    385424        return $reply_id;
     
    606645        global $wpdb;
    607646       
    608         $allowed_statuses = array('open', 'pending', 'in_progress', 'resolved', 'closed'); // <-- Add in_progress here
     647        $allowed_statuses = array('open', 'pending', 'in_progress', 'resolved', 'closed');
    609648        if (!in_array($status, $allowed_statuses)) {
    610649            return false;
     
    628667       
    629668        if ($result) {
    630             // Invalidate all relevant caches
     669            // Clear all ticket-related caches to ensure fresh data
     670            self::clear_all_ticket_caches();
     671           
     672            // Invalidate specific ticket cache
    631673            wp_cache_delete('nexlifydesk_ticket_' . intval($ticket_id));
    632674           
     
    647689                wp_cache_delete('nexlifydesk_assigned_tickets_' . intval($ticket_before->assigned_to) . '_resolved');
    648690                wp_cache_delete('nexlifydesk_assigned_tickets_' . intval($ticket_before->assigned_to) . '_closed');
    649             }
    650            
    651             // Re-get the ticket after update
     691               
     692                $was_closed = in_array($ticket_before->status, array('closed', 'resolved'));
     693                $is_reopened = !in_array($status, array('closed', 'resolved')) && $was_closed;
     694               
     695                if ($is_reopened) {
     696                    self::mark_ticket_unread_for_assignee($ticket_id, $ticket_before->assigned_to, 'status_change');
     697                }
     698            }
     699           
    652700            $ticket = self::get_ticket($ticket_id);
    653701
     
    658706                    });
    659707                }
    660 
    661             // Clear ticket grid cache when status is updated
    662             wp_cache_flush_group('nexlifydesk_tickets_grid');
    663708
    664709            return true;
     
    706751                );
    707752
    708                 // Sending to customer
    709753                if ($customer_email && !in_array($customer_email, $emailed)) {
    710754                    wp_mail($customer_email, $subject, $message, $headers);
     
    712756                }
    713757
    714                 // Sending to assigned agent if exists
    715758                if (!empty($ticket->assigned_to)) {
    716759                    $agent = get_userdata($ticket->assigned_to);
     
    872915            $template_content = ob_get_clean();
    873916           
    874             // Fallback if template file is empty or doesn't exist
    875917            if (empty($template_content)) {
    876918                $template_content = self::get_fallback_email_template($template, $ticket, $reply_id);
     
    947989     */
    948990    private static function get_fallback_email_template($template, $ticket, $reply_id = null) {
    949         // Generate a basic fallback template if template files are not available
    950991        $user = get_userdata($ticket->user_id);
    951992        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     
    9671008
    9681009    /**
    969      *
    970      * This function detects common SMTP plugins and avoids adding Message-ID,
    971      * In-Reply-To, and References headers if an SMTP plugin is active, preventing
    972      * the "multiple Message-ID headers" error that causes emails to be blocked
    973      * by Gmail and other providers.
    974      *
     1010     * Get email headers for the ticket
     1011     * @since 1.0.1
    9751012     * @param object $ticket The ticket object
    9761013     * @return array Email headers array
     
    9831020        );
    9841021
    985         // Check if SMTP plugins are active that might add their own Message-ID headers
    9861022        $smtp_plugins_active = (
    9871023            is_plugin_active('wp-mail-smtp/wp_mail_smtp.php') ||
     
    9941030        );
    9951031
    996         // Only add Message-ID and threading headers if no SMTP plugin is detected
    9971032        if (!$smtp_plugins_active) {
    9981033            $domain = wp_parse_url(home_url(), PHP_URL_HOST);
     
    10061041
    10071042    /**
    1008      * Send email notifications for tickets created via email channel
    1009      * Simple auto-response for new tickets, raw email replies for ongoing conversation
    1010      *
     1043     * Send email channel notification for new tickets or replies
    10111044     * @param object $ticket The ticket object
    10121045     * @param string $type The notification type ('new_ticket' or 'new_reply')
     
    15561589        return $reassigned_count;
    15571590    }
     1591   
     1592    /**
     1593     * Mark ticket as unread for specific user(s) when reassigned or status changes
     1594     *
     1595     * @param int $ticket_id Ticket ID
     1596     * @param int $new_assignee_id New assignee user ID
     1597     * @param string $reason Reason for marking unread (reassignment, status_change, customer_reply)
     1598     */
     1599    public static function mark_ticket_unread_for_assignee($ticket_id, $new_assignee_id, $reason = 'reassignment') {
     1600        global $wpdb;
     1601       
     1602        if (!$ticket_id || !$new_assignee_id) {
     1603            return;
     1604        }
     1605       
     1606        // Remove read status for the new assignee so they see it as unread
     1607        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1608        $wpdb->delete(
     1609            NexlifyDesk_Database::get_table('ticket_reads'),
     1610            array(
     1611                'ticket_id' => $ticket_id,
     1612                'user_id' => $new_assignee_id
     1613            ),
     1614            array('%d', '%d')
     1615        );
     1616       
     1617        // Clear cache
     1618        wp_cache_delete('nexlifydesk_ticket_reads_' . $ticket_id);
     1619        wp_cache_flush_group('nexlifydesk_tickets_grid');
     1620    }
     1621   
     1622    /**
     1623     * Mark ticket as read for current user with smart role-based logic
     1624     *
     1625     * @param int $ticket_id Ticket ID
     1626     * @param int $user_id User ID (optional, defaults to current user)
     1627     */
     1628    public static function mark_ticket_read($ticket_id, $user_id = null) {
     1629        global $wpdb;
     1630       
     1631        if (!$ticket_id) {
     1632            return;
     1633        }
     1634       
     1635        $user_id = $user_id ?: get_current_user_id();
     1636        if (!$user_id) {
     1637            return;
     1638        }
     1639       
     1640        // Get ticket details
     1641        $ticket = self::get_ticket($ticket_id);
     1642        if (!$ticket) {
     1643            return;
     1644        }
     1645       
     1646        $current_user = get_userdata($user_id);
     1647        if (!$current_user) {
     1648            return;
     1649        }
     1650       
     1651        $users_to_mark_read = array($user_id);
     1652       
     1653        $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     1654       
     1655        if ($is_agent) {
     1656           
     1657            $admin_users = get_users(array('role' => 'administrator'));
     1658            foreach ($admin_users as $admin_user) {
     1659                if (!in_array($admin_user->ID, $users_to_mark_read)) {
     1660                    $users_to_mark_read[] = $admin_user->ID;
     1661                }
     1662            }
     1663           
     1664            if (class_exists('NexlifyDesk_Users')) {
     1665                $all_agents = get_users(array('role' => 'nexlifydesk_agent'));
     1666                foreach ($all_agents as $agent) {
     1667                    if ($agent->ID != $user_id &&
     1668                        !in_array($agent->ID, $users_to_mark_read) &&
     1669                        NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $agent->ID)) {
     1670                        $users_to_mark_read[] = $agent->ID;
     1671                    }
     1672                }
     1673            }
     1674        }
     1675       
     1676        foreach ($users_to_mark_read as $mark_user_id) {
     1677            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1678            $wpdb->replace(
     1679                NexlifyDesk_Database::get_table('ticket_reads'),
     1680                array(
     1681                    'ticket_id' => $ticket_id,
     1682                    'user_id' => $mark_user_id,
     1683                    'read_at' => current_time('mysql')
     1684                ),
     1685                array('%d', '%d', '%s')
     1686            );
     1687        }
     1688       
     1689        // Clear cache
     1690        wp_cache_delete('nexlifydesk_ticket_reads_' . $ticket_id);
     1691        self::clear_all_ticket_caches();
     1692    }
     1693   
     1694    /**
     1695     * Check if user is a supervisor (can view all tickets)
     1696     *
     1697     * @param int $user_id User ID (optional, defaults to current user)
     1698     * @return bool True if user is supervisor
     1699     */
     1700    public static function is_supervisor($user_id = null) {
     1701        $user_id = $user_id ?: get_current_user_id();
     1702        if (!$user_id) {
     1703            return false;
     1704        }
     1705       
     1706        if (user_can($user_id, 'manage_options')) {
     1707            return true;
     1708        }
     1709       
     1710        if (class_exists('NexlifyDesk_Users')) {
     1711            return NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $user_id);
     1712        }
     1713       
     1714        return false;
     1715    }
    15581716
    15591717    /**
     
    16351793   
    16361794    /**
    1637      * Check for duplicate tickets using improved user-scoped logic:
    1638      * - For registered users: Check ONLY within their own tickets, never cross-user matching
    1639      * - For unregistered users: Consolidate by email address only, no content matching needed
    1640      *
    16411795     * @param array $data Ticket data to check for duplicates
    16421796     * @return object|false Returns existing ticket if duplicate found, false otherwise
    16431797     */
    16441798    public static function check_for_duplicate_ticket($data) {
    1645         // Use the new global duplicate detection function for all channels and user types
     1799        $settings = get_option('nexlifydesk_settings', array());
     1800        $check_duplicates = isset($settings['check_duplicates']) ? $settings['check_duplicates'] : false;
     1801       
     1802        if (!$check_duplicates) {
     1803            return false;
     1804        }
     1805       
    16461806        if (!function_exists('nexlifydesk_find_duplicate_ticket')) {
    16471807            require_once dirname(__FILE__) . '/nexlifydesk-functions.php';
    16481808        }
    1649         $settings = get_option('nexlifydesk_settings', array());
    1650         $check_duplicates = isset($settings['check_duplicates']) ? $settings['check_duplicates'] : true;
    1651         if (!$check_duplicates) {
     1809       
     1810        if (!function_exists('nexlifydesk_find_duplicate_ticket')) {
    16521811            return false;
    16531812        }
     1813       
    16541814        return nexlifydesk_find_duplicate_ticket($data);
    16551815    }
     
    16751835        $where_clauses = ['1=1'];
    16761836        $query_params = [];
     1837       
     1838        $current_user = wp_get_current_user();
     1839        $is_agent = in_array('nexlifydesk_agent', $current_user->roles) && !in_array('administrator', $current_user->roles);
     1840       
     1841        if ($is_agent) {
     1842            $can_view_all = NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $current_user->ID);
     1843           
     1844            if (!$can_view_all) {
     1845                $where_clauses[] = 't.assigned_to = %d';
     1846                $query_params[] = $current_user->ID;
     1847            }
     1848        }
    16771849
    16781850        if ($args['status'] !== 'all') {
     
    17011873        $current_user_id = get_current_user_id();
    17021874       
    1703         $cache_key = 'nexlifydesk_tickets_grid_v3_' . md5(serialize($args) . $current_user_id);
     1875        $agent_view_all = false;
     1876        if ($is_agent) {
     1877            $agent_view_all = NexlifyDesk_Users::agent_can('nexlifydesk_view_all_tickets', $current_user_id);
     1878        }
     1879       
     1880        $is_supervisor = $agent_view_all || current_user_can('manage_options') ? 1 : 0;
     1881       
     1882        $cache_key = 'nexlifydesk_tickets_grid_v7_' . md5(serialize($args) . $current_user_id . ($is_agent ? '_agent_' . ($agent_view_all ? 'all' : 'assigned') : '_admin'));
    17041883        $results = wp_cache_get($cache_key, 'nexlifydesk_tickets_grid');
    17051884        if ($results === false) {
     
    17121891                           a.display_name as assigned_to_display_name,
    17131892                           COALESCE(MAX(r.created_at), t.created_at) as last_reply_time,
     1893                           COALESCE(MAX(r.created_at), t.created_at) as last_updated_time,
    17141894                           CASE
    17151895                               WHEN NOT EXISTS (
     
    17181898                               ) THEN 1
    17191899                               WHEN EXISTS (
    1720                                    SELECT 1 FROM {$wpdb->prefix}nexlifydesk_replies r2
    1721                                    WHERE r2.ticket_id = t.id
    1722                                    AND r2.user_id != %d
    1723                                    AND r2.created_at > COALESCE(
    1724                                        (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr2
    1725                                         WHERE tr2.ticket_id = t.id AND tr2.user_id = %d),
    1726                                        t.created_at
     1900                                   SELECT 1 FROM {$wpdb->prefix}nexlifydesk_replies r_new
     1901                                   WHERE r_new.ticket_id = t.id
     1902                                   AND r_new.created_at > COALESCE(
     1903                                       (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr_read
     1904                                        WHERE tr_read.ticket_id = t.id AND tr_read.user_id = %d),
     1905                                       '1970-01-01'
    17271906                                   )
    1728                                    AND r2.is_internal_note = 0
     1907                                   AND r_new.is_internal_note = 0
    17291908                               ) THEN 1
    17301909                               WHEN t.created_at > COALESCE(
    1731                                    (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr3
    1732                                     WHERE tr3.ticket_id = t.id AND tr3.user_id = %d),
     1910                                   (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr_created
     1911                                    WHERE tr_created.ticket_id = t.id AND tr_created.user_id = %d),
    17331912                                   '1970-01-01'
    17341913                               ) THEN 1
     
    17461925                    $current_user_id,
    17471926                    $current_user_id,
    1748                     $current_user_id,
    17491927                    ...$query_params
    17501928                )
     
    18051983       
    18061984        if ($result !== false) {
    1807             // Clear cache
     1985            // Clear all ticket-related caches to ensure fresh data
     1986            self::clear_all_ticket_caches();
     1987           
     1988            // Clear specific ticket cache
    18081989            wp_cache_delete('nexlifydesk_ticket_' . $ticket_id);
    18091990           
     
    18532034       
    18542035        if ($result !== false) {
    1855             // Clear cache
     2036            self::clear_all_ticket_caches();
     2037           
    18562038            wp_cache_delete('nexlifydesk_ticket_' . $ticket_id);
    18572039           
    1858             wp_cache_flush_group('nexlifydesk_tickets_grid');
     2040            if ($agent_id > 0 && $ticket_before->assigned_to != $agent_id) {
     2041                self::mark_ticket_unread_for_assignee($ticket_id, $agent_id, 'reassignment');
     2042            }
    18592043           
    18602044            return true;
     
    18652049   
    18662050    /**
    1867      * Get tickets for real-time refresh with unread status
    18682051     *
    18692052     * @param int $last_refresh_timestamp Unix timestamp of last refresh (unused for now)
     
    18752058   
    18762059    /**
    1877      * Mark ticket as read for a specific user
    18782060     *
    18792061     * @param int $ticket_id Ticket ID
     
    19722154    }
    19732155
     2156    /**
     2157     * Clear all ticket-related caches to ensure fresh data
     2158     */
     2159    public static function clear_all_ticket_caches() {
     2160        // Clear ticket list caches for different contexts
     2161        wp_cache_delete('nexlifydesk_tickets_grid');
     2162        wp_cache_delete('nexlifydesk_tickets_admin_grid');
     2163        wp_cache_delete('nexlifydesk_tickets_agent_grid');
     2164        wp_cache_delete('nexlifydesk_tickets_count');
     2165        wp_cache_delete('nexlifydesk_unread_count');
     2166       
     2167        // Clear general caches that might contain ticket data
     2168        wp_cache_flush_group('nexlifydesk');
     2169       
     2170        // Trigger action for other plugins/components to clear their caches
     2171        do_action('nexlifydesk_clear_ticket_caches');
     2172    }
     2173
    19742174}
  • nexlifydesk/trunk/includes/class-nexlifydesk-users.php

    r3326104 r3334167  
    2121            'edit_posts' => true,
    2222            'nexlifydesk_manage_tickets' => true,
    23             'nexlifydesk_view_all_tickets' => true,
     23            'nexlifydesk_view_all_tickets' => false,
    2424            'nexlifydesk_assign_tickets' => true,
    2525            'nexlifydesk_manage_categories' => true,
  • nexlifydesk/trunk/includes/helpers.php

    r3333095 r3334167  
    100100    }
    101101   
    102     if (strlen($value) < 32) { // Encrypted passwords should be at least 32 chars
     102    if (strlen($value) < 32) {
    103103        return false;
    104104    }
     
    116116
    117117/**
    118  * Parse email "From" header to extract name and email address
    119  *
    120118 * @param string $from_header The full "From" header string
    121119 * @return array Array with 'name' and 'email' keys
     
    181179
    182180/**
    183  * Decode MIME-encoded email subjects to human-readable format
    184  * Fixes issue where subjects like "=?UTF-8?Q?Issue_with_Order_#5628_–_Missing_Items?="
    185  * appear encoded in tickets instead of being properly decoded
    186  *
     181 * Decode email subject from MIME encoding
    187182 * @param string $subject The encoded email subject
    188183 * @return string The decoded subject
     
    195190    $subject = trim($subject);
    196191   
    197     // Check if subject contains MIME encoding
    198192    if (strpos($subject, '=?') !== false && strpos($subject, '?=') !== false) {
    199         // Use imap_mime_header_decode to properly decode the subject
    200193        $decoded_parts = imap_mime_header_decode($subject);
    201194       
     
    209202    }
    210203   
    211     // If no encoding detected or decoding failed, return original subject
    212204    return $subject;
    213205}
     
    387379    $actual_message = $clean_message;
    388380   
    389     // Handle structured format with [Customer Details] and [Message]/[Reply] sections
    390381    if (preg_match('/\[Customer Details\]\s*(.*?)\s*\[(?:Message|Reply)\]\s*(.*)/s', $clean_message, $matches)) {
    391382        $customer_section = trim($matches[1]);
    392383        $actual_message = trim($matches[2]);
    393384       
    394         // Extract name and email from customer details section
    395385        if (preg_match('/Name:\s*([^\n\r]+)/i', $customer_section, $name_matches)) {
    396386            $name = trim($name_matches[1]);
     
    400390        }
    401391    } else {
    402         // Fallback to original logic for other formats
    403392        if (preg_match('/From:\s*([^<\n]+)\s*<([^>]+)>/', $clean_message, $matches)) {
    404393            $name = trim($matches[1]);
     
    413402    }
    414403   
    415     // Generate name from email if needed
    416404    if (empty($name) && !empty($email)) {
    417405        $email_parts = explode('@', $email);
     
    425413        'name' => $name,
    426414        'email' => $email,
    427         'message' => $actual_message  // Now returns clean message without customer details
     415        'message' => $actual_message
    428416    ];
    429417}
     
    663651
    664652/**
    665  * Handles Quoted-Printable, Base64, and other common email encodings
    666  *
    667653 * @param string $content The email content to decode
    668654 * @return string The decoded content
  • nexlifydesk/trunk/includes/nexlifydesk-functions.php

    r3333214 r3334167  
    154154    $order_numbers = array();
    155155    if (empty($text)) return $order_numbers;
    156     // Match patterns like Order #1234, Order number 1234, #1234, 1234
    157156    $patterns = array(
    158         '/order\s*(number)?\s*#?\s*(\d{4,})/i', // Order #1234, Order number 1234
    159         '/#(\d{4,})/', // #1234
    160         '/\b(\d{4,})\b/' // 1234 (standalone, 4+ digits)
     157        '/order\s*(number)?\s*#?\s*(\d{4,})/i',
     158        '/#(\d{4,})/',
     159        '/\b(\d{4,})\b/'
    161160    );
    162161    foreach ($patterns as $pattern) {
     
    196195
    197196/**
    198  * Global duplicate ticket detection for all channels
    199  * Implements correct logic per user requirements:
    200  * - Registered users: Check ONLY within their own tickets, never cross-user matching
    201  * - Unregistered users: Consolidate by email address only, no content matching needed
    202197 *
    203198 * @param array $ticket_data (user_id, email, subject, message, source)
     
    212207    $table_name = $wpdb->prefix . 'nexlifydesk_tickets';
    213208   
    214     // ========================
    215209    // A. REGISTERED USER LOGIC
    216     // ========================
    217210    if ($user_id > 0) {
    218         // Step 1: Check if this registered user has ANY existing ticket (open, pending, in_progress)
    219211        $cache_key = 'nexlifydesk_user_has_tickets_' . $user_id;
    220212        $user_has_tickets = wp_cache_get($cache_key);
     
    231223        }
    232224       
    233         // Step 2: If user has no existing tickets, create new ticket (no duplicate possible)
    234225        if (!$user_has_tickets) {
    235             return null; // No existing tickets = create new ticket
    236         }
    237        
    238         // Step 3: User has existing tickets - perform content-based duplicate detection ONLY within this user's tickets
     226            return null;
     227        }
     228       
    239229        $order_numbers = array();
    240230        if (!empty($subject)) {
     
    246236        $order_numbers = array_unique($order_numbers);
    247237       
    248         // Check for order number matches within user's own tickets
    249238        if (!empty($order_numbers)) {
    250239            $order_regex = implode('|', array_map('preg_quote', $order_numbers));
     
    264253        }
    265254       
    266         // Check for exact subject match within user's own tickets
    267255        if (!empty($subject)) {
    268256            $cache_key = 'nexlifydesk_user_subject_match_' . md5($user_id . $subject);
     
    281269        }
    282270       
    283         // Semantic similarity check within user's own tickets only
    284271        if (class_exists('TextAnalysis\\Comparisons\\CosineSimilarityComparison') && class_exists('TextAnalysis\\Tokenizers\\GeneralTokenizer')) {
    285272            $similarity_threshold = 0.12; // Optimized threshold for better matching
     
    298285           
    299286            if (!empty($user_tickets)) {
    300                 // Manual keyword mapping for semantic matching
    301287                $keyword_map = array(
    302288                    'profile' => 'account', 'dashboard' => 'account', 'user' => 'account', 'panel' => 'account',
     
    333319                    );
    334320                    if ($similarity >= $similarity_threshold) {
    335                         return $ticket; // Found duplicate within user's own tickets
     321                        return $ticket;
    336322                    }
    337323                }
     
    339325        }
    340326       
    341         // No duplicate found within user's tickets = create new ticket for this user
    342327        return null;
    343328    }
    344329
    345     // ==========================
    346330    // B. UNREGISTERED USER LOGIC
    347     // ==========================
    348331    if ($user_id == 0 && !empty($email)) {
    349         // For unregistered users, we simply check if there's ANY existing ticket for this email
    350         // Content matching is NOT required - we want to consolidate all communication by email
    351332        $cache_key = 'nexlifydesk_email_consolidation_' . md5($email);
    352333        $existing_ticket = wp_cache_get($cache_key);
    353334
    354335        if (false === $existing_ticket) {
    355             // Find the most recent ticket for this email address (any status except closed)
    356336            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
    357337            $existing_ticket = $wpdb->get_row( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query.
     
    366346        }
    367347       
    368         return $existing_ticket; // Return existing ticket for email consolidation, or null to create new
     348        return $existing_ticket;
    369349    }
    370350   
    371     // No valid user_id or email = create new ticket
    372351    return null;
    373352}
  • nexlifydesk/trunk/includes/textanalysis/SECURITY.md

    r3333095 r3334167  
    66- `Documents/` - Data structures
    77- `Interfaces/` - Class contracts
     8- `Corpus/` - Text corpus management
    89
    910## What We Removed (SECURITY RISKS)
    10 - `Console/` - Contained file system operations
    11 - `Downloaders/` - Contained remote file access
     11- `Console/` - Contained file system operations and command line tools
     12- `Downloaders/` - Contained remote file access capabilities
     13- `NltkPackageInstallCommand.php` - Package installation commands
    1214
    1315## Security Status
    1416✅ No file system operations
    15 ✅ No remote downloads
     17✅ No remote downloads 
    1618✅ No external dependencies
     19✅ No command line interfaces
    1720✅ Pure computational library
     21✅ Clean directory structure
    1822
    1923This subset provides semantic similarity analysis without security vulnerabilities.
     24All potentially dangerous components have been properly removed.
  • nexlifydesk/trunk/nexlifydesk.php

    r3333214 r3334167  
    33 * Plugin Name: NexlifyDesk
    44 * 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.4
     5 * Version: 1.0.5
    66 * Author URI: https://nexlifylabs.com
    77 * Supported Versions: 6.2+
     
    1818
    1919if ( ! function_exists( 'nexlifydesk' ) ) {
    20     // Create a helper function for easy SDK access.
    2120    function nexlifydesk() {
    2221        global $nexlifydesk;
    2322
    2423        if ( ! isset( $nexlifydesk ) ) {
    25             // Include Freemius SDK.
    2624            require_once dirname( __FILE__ ) . '/vendor/freemius/start.php';
    2725            $nexlifydesk = fs_dynamic_init( array(
     
    4442    }
    4543
    46     // Init Freemius.
    4744    nexlifydesk();
    48     // Signal that SDK was initiated.
     45   
    4946    do_action( 'nexlifydesk_loaded' );
    5047   
    51     // Register Freemius uninstall handler
    5248    nexlifydesk()->add_action('after_uninstall', 'nexlifydesk_freemius_uninstall_cleanup');
    5349}
     
    5551define('NEXLIFYDESK_PLUGIN_DIR', plugin_dir_path(__FILE__));
    5652define('NEXLIFYDESK_PLUGIN_URL', plugin_dir_url(__FILE__));
    57 define('NEXLIFYDESK_VERSION', '1.0.4');
     53define('NEXLIFYDESK_VERSION', '1.0.5');
    5854define('NEXLIFYDESK_TABLE_PREFIX', 'nexlifydesk_');
    5955define('NEXLIFYDESK_CAP_VIEW_ALL_TICKETS', 'nexlifydesk_view_all_tickets');
     
    9490}
    9591add_action('plugins_loaded', 'nexlifydesk_init');
    96 
    97 // Add plugin action links (donate button, etc.)
    9892add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'nexlifydesk_plugin_action_links');
    9993add_filter('plugin_row_meta', 'nexlifydesk_plugin_row_meta', 10, 2);
     
    117111}
    118112
    119 // Add admin notice and action to update email templates for existing installations
    120113add_action('admin_notices', 'nexlifydesk_show_template_update_notice');
    121114add_action('wp_ajax_nexlifydesk_update_email_templates', 'nexlifydesk_ajax_update_email_templates');
     
    123116
    124117function nexlifydesk_show_template_update_notice() {
    125     // Only show to admins on NexlifyDesk pages
    126118    if (!current_user_can('manage_options')) {
    127119        return;
    128120    }
    129121   
    130     // Check if notice was dismissed
    131122    if (get_option('nexlifydesk_template_notice_dismissed', false)) {
    132123        return;
     
    138129    }
    139130   
    140     // Check if templates need updating (contain old placeholder format)
    141131    $existing_templates = get_option('nexlifydesk_email_templates', array());
    142132    $needs_update = false;
     
    181171    }
    182172   
    183     // Clear existing templates to force use of template files
    184173    $templates = array(
    185174        'new_ticket' => '',
     
    203192    }
    204193   
    205     // Set the dismissal flag
    206194    update_option('nexlifydesk_template_notice_dismissed', true);
    207195   
     
    231219 */
    232220function nexlifydesk_load_default_email_templates() {
    233     // Only load if no templates exist yet (fresh installation)
    234221    $existing_templates = get_option('nexlifydesk_email_templates', array());
    235222   
    236     // Check if templates are empty or contain old placeholder-style templates
    237223    $needs_update = empty($existing_templates) ||
    238224                   (isset($existing_templates['new_reply']) &&
     
    240226   
    241227    if ($needs_update) {
    242         // Set placeholders indicating that template files are being used
    243228        $templates = array(
    244229            'new_ticket' => '',
     
    434419
    435420/**
    436  * Freemius uninstall cleanup handler
    437421 * This is called by Freemius when the plugin is uninstalled through their system
    438422 */
    439423function nexlifydesk_freemius_uninstall_cleanup() {
    440     // Include the uninstall script to handle cleanup
    441424    $uninstall_file = plugin_dir_path(__FILE__) . 'uninstall.php';
    442425    if (file_exists($uninstall_file)) {
    443         // Define the constant that uninstall.php expects
    444426        if (!defined('WP_UNINSTALL_PLUGIN')) {
    445427            define('WP_UNINSTALL_PLUGIN', true);
     
    449431}
    450432
    451 // Register the uninstall hook to use the uninstall.php file
    452433register_uninstall_hook(__FILE__, 'nexlifydesk_freemius_uninstall_cleanup');
    453434
  • nexlifydesk/trunk/readme.txt

    r3333214 r3334167  
    44Requires at least: 6.2
    55Tested up to: 6.8
    6 Stable tag: 1.0.4
     6Stable tag: 1.0.5
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    349349== Changelog ==
    350350
     351= 1.0.5 =
     352- **SECURITY FIX**: Resolved critical agent permission vulnerability where agents could view and reply to tickets not assigned to them
     353- **NEW**: Implemented smart role-based read status system - when agents handle tickets, they're automatically marked as read for supervisors/admins to eliminate review backlog
     354- **NEW**: Added professional dual-date columns (Created + Last Updated) to ticket list with accurate timestamp display based on actual reply activity, not system background processes
     355- Enhanced agent role-based access control to properly restrict agents to only their assigned tickets
     356- Fixed agent position capabilities to correctly respect "View All Tickets" permission for supervisor roles
     357- Improved ticket filtering in admin dashboard to enforce proper agent-specific ticket visibility
     358- Enhanced frontend ticket access control with better customer email verification for unregistered users
     359- Fixed agent reply permissions to prevent unauthorized responses to unassigned tickets
     360- Improved agent dashboard statistics to show only relevant metrics for assigned tickets
     361- Enhanced bulk action permissions to restrict agents from performing actions on unassigned tickets
     362- Fixed ticket form redirect issues that caused incorrect URL redirection after submission
     363- Improved frontend shortcode logic to properly handle customer vs agent access permissions
     364- Enhanced duplicate detection with better customer detail extraction for unregistered users
     365- Fixed JavaScript form submission to use proper server-provided redirect URLs
     366- Improved success message handling with proper ticket number display and view links
     367- Fixed category deletion and reactivation bug that caused misleading error messages
     368- Resolved AJAX response issues where HTML was returned instead of JSON during category operations
     369- Enhanced category management with comprehensive output buffering and proper exit handling
     370- Improved JavaScript error handling with automatic page reload for better user experience
     371- Enhanced database query optimization with proper phpcs ignore comments for intentional non-caching
     372- Fixed category existence checks to prevent race conditions and stale cache data issues
     373- Enhanced admin category form processing with consistent non-caching approach for real-time accuracy
     374- Optimized category cache management to prevent deletion/recreation conflicts
     375- Implemented comprehensive cache clearing system for consistent ticket list updates across all user roles
     376
    351377= 1.0.4 =
    352378- Enhanced email provider validation to prevent false success messages when credentials are not configured
     
    402428== Upgrade Notice ==
    403429
     430= 1.0.5 =
     431CRITICAL SECURITY UPDATE: Fixes agent permission vulnerability. NEW: Smart read status system eliminates supervisor backlog + professional dual-date columns with accurate timestamps. Update immediately for security and enhanced workflow efficiency.
     432
    404433= 1.0.4 =
    405434Important update: Enhanced email provider validation prevents false success messages, improved AWS connectivity across multiple server configurations, and strengthened JavaScript coding standards compliance.
  • nexlifydesk/trunk/templates/admin/imap-auth.php

    r3333095 r3334167  
    3737$auth_url = wp_nonce_url(admin_url('admin.php?action=nexlifydesk_google_auth_init'), 'google-auth-init');
    3838
    39 // SSL detection - AWS will only work with HTTPS
     39// SSL detection
    4040$is_ssl_enabled = function_exists('nexlifydesk_check_ssl_enabled') ? nexlifydesk_check_ssl_enabled() : is_ssl();
    4141
  • nexlifydesk/trunk/templates/admin/settings.php

    r3333095 r3334167  
    1414        'default_category' => 1,
    1515        'sla_response_time' => 24,
    16         'ticket_page_id' => 0,
    17         'ticket_form_page_id' => 0,
     16        'ticket_page_id' => array(),
     17        'ticket_form_page_id' => array(),
    1818        'ticket_id_prefix' => 'T',
    1919        'ticket_id_start' => 1001,
     
    3333        if (in_array($key, array('default_priority', 'allowed_file_types', 'ticket_id_prefix',), true)) {
    3434            $settings[$key] = sanitize_text_field($value);
    35         } elseif (in_array($key, array('max_file_size', 'default_category', 'sla_response_time', 'ticket_page_id', 'ticket_id_start', 'duplicate_threshold'), true)) {
     35        } elseif (in_array($key, array('max_file_size', 'default_category', 'sla_response_time', 'ticket_id_start', 'duplicate_threshold'), true)) {
    3636            $settings[$key] = (int)$value;
     37        } elseif (in_array($key, array('ticket_page_id', 'ticket_form_page_id'), true)) {
     38            if (!is_array($value)) {
     39                $settings[$key] = $value ? array($value) : array();
     40            } else {
     41                $settings[$key] = array_map('intval', array_filter($value));
     42            }
    3743        } elseif (in_array($key, array('check_duplicates', 'keep_data_on_uninstall'), true)) {
    3844            $settings[$key] = $value ? 1 : 0;
     
    99105            </tr>
    100106            <tr>
    101                 <th><label for="ticket_page_id"><?php esc_html_e('Ticket List Page', 'nexlifydesk'); ?></label></th>
    102                 <td>
    103                     <select name="ticket_page_id" id="ticket_page_id">
    104                         <option value="0"><?php esc_html_e('Select a page', 'nexlifydesk'); ?></option>
     107                <th><label for="ticket_page_id"><?php esc_html_e('Ticket List Pages', 'nexlifydesk'); ?></label></th>
     108                <td>
     109                    <select name="ticket_page_id[]" id="ticket_page_id" multiple="multiple" style="width: 100%; height: 120px;">
    105110                        <?php if (!empty($pages) && is_array($pages)): ?>
     111                            <?php
     112                            $selected_pages = isset($settings['ticket_page_id']) ? $settings['ticket_page_id'] : array();
     113                            if (!is_array($selected_pages)) {
     114                                $selected_pages = $selected_pages ? array($selected_pages) : array();
     115                            }
     116                            ?>
    106117                            <?php foreach ($pages as $page): ?>
    107118                                <?php if (isset($page->ID) && isset($page->post_title)): ?>
    108                                     <option value="<?php echo esc_attr((int)$page->ID); ?>" <?php selected((int)$settings['ticket_page_id'], (int)$page->ID); ?>>
     119                                    <option value="<?php echo esc_attr((int)$page->ID); ?>" <?php echo in_array((int)$page->ID, array_map('intval', $selected_pages)) ? 'selected' : ''; ?>>
    109120                                        <?php echo esc_html(wp_strip_all_tags($page->post_title)); ?>
    110121                                    </option>
     
    115126                        <?php endif; ?>
    116127                    </select>
    117                     <p class="description"><?php esc_html_e('Select the page where the ticket list shortcode [nexlifydesk_ticket_list] is embedded.', 'nexlifydesk'); ?></p>
    118                 </td>
    119             </tr>
    120             <tr>
    121                 <th><label for="ticket_form_page_id"><?php esc_html_e('Ticket Form Page', 'nexlifydesk'); ?></label></th>
    122                 <td>
    123                     <select name="ticket_form_page_id" id="ticket_form_page_id">
    124                         <option value="0"><?php esc_html_e('Select a page', 'nexlifydesk'); ?></option>
     128                    <p class="description">
     129                        <?php esc_html_e('Select one or more pages where the ticket list shortcode [nexlifydesk_ticket_list] should render the actual ticket list. Hold Ctrl (Windows) or Cmd (Mac) to select multiple pages.', 'nexlifydesk'); ?><br>
     130                        <strong><?php esc_html_e('Note:', 'nexlifydesk'); ?></strong> <?php esc_html_e('On pages not selected here, the shortcode will display as plain text for documentation purposes.', 'nexlifydesk'); ?>
     131                    </p>
     132                </td>
     133            </tr>
     134            <tr>
     135                <th><label for="ticket_form_page_id"><?php esc_html_e('Ticket Form Pages', 'nexlifydesk'); ?></label></th>
     136                <td>
     137                    <select name="ticket_form_page_id[]" id="ticket_form_page_id" multiple="multiple" style="width: 100%; height: 120px;">
    125138                        <?php if (!empty($pages) && is_array($pages)): ?>
     139                            <?php
     140                            $selected_form_pages = isset($settings['ticket_form_page_id']) ? $settings['ticket_form_page_id'] : array();
     141                            if (!is_array($selected_form_pages)) {
     142                                $selected_form_pages = $selected_form_pages ? array($selected_form_pages) : array();
     143                            }
     144                            ?>
    126145                            <?php foreach ($pages as $page): ?>
    127146                                <?php if (isset($page->ID) && isset($page->post_title)): ?>
    128                                     <option value="<?php echo esc_attr((int)$page->ID); ?>" <?php selected((int)$settings['ticket_form_page_id'], (int)$page->ID); ?>>
     147                                    <option value="<?php echo esc_attr((int)$page->ID); ?>" <?php echo in_array((int)$page->ID, array_map('intval', $selected_form_pages)) ? 'selected' : ''; ?>>
    129148                                        <?php echo esc_html(wp_strip_all_tags($page->post_title)); ?>
    130149                                    </option>
     
    135154                        <?php endif; ?>
    136155                    </select>
    137                     <p class="description"><?php esc_html_e('Select the page where the ticket form shortcode [nexlifydesk_ticket_form] is embedded.', 'nexlifydesk'); ?></p>
     156                    <p class="description">
     157                        <?php esc_html_e('Select one or more pages where the ticket form shortcode [nexlifydesk_ticket_form] should render the actual form. Hold Ctrl (Windows) or Cmd (Mac) to select multiple pages.', 'nexlifydesk'); ?><br>
     158                        <strong><?php esc_html_e('Note:', 'nexlifydesk'); ?></strong> <?php esc_html_e('On pages not selected here, the shortcode will display as plain text for documentation purposes.', 'nexlifydesk'); ?>
     159                    </p>
    138160                </td>
    139161            </tr>
     
    308330    });
    309331
    310     // Show warning if already unchecked on page load
    311332    if (!$('#keep_data_on_uninstall').is(':checked')) {
    312333        $('#data-deletion-warning').show();
  • nexlifydesk/trunk/templates/admin/ticket-single.php

    r3333095 r3334167  
    1515$user_avatar_url = $user ? get_avatar_url($user->ID) : get_avatar_url(0);
    1616
    17 // Extract customer details for non-registered users, but use clean message for display
    18 // to avoid redundancy with the "About Customer" sidebar section
     17// Extract customer details for non-registered users
    1918$customer_details = function_exists('nexlifydesk_extract_customer_details') ?
    2019    nexlifydesk_extract_customer_details($ticket->message) :
     
    2322$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
    2423$customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A');
    25 // Use clean message without embedded customer details for admin panel display
    2624$display_message = $customer_details['message'];
    2725?>
     
    154152                   
    155153                    if (!$reply_user) {
    156                         // For non-registered customers: Extract clean message without customer details
    157                         // to avoid redundancy with the "About Customer" sidebar section
     154                        // For non-registered customers
    158155                        $reply_customer_details = function_exists('nexlifydesk_extract_customer_details') ?
    159156                            nexlifydesk_extract_customer_details($reply->message) :
    160157                            ['name' => '', 'email' => '', 'message' => $reply->message];
    161158                        $reply_customer_name = $reply_customer_details['name'] ?: 'Guest';
    162                         // Use clean message without embedded customer details for admin panel display
    163159                        $reply_display_message = $reply_customer_details['message'];
    164160                    } else {
  • nexlifydesk/trunk/templates/admin/tickets-list.php

    r3333095 r3334167  
    100100            <div class="header-priority"><?php esc_html_e('Priority', 'nexlifydesk'); ?></div>
    101101            <div class="header-assignee"><?php esc_html_e('Assignee', 'nexlifydesk'); ?></div>
    102             <div class="header-date"><?php esc_html_e('Date', 'nexlifydesk'); ?></div>
     102            <div class="header-created"><?php esc_html_e('Created', 'nexlifydesk'); ?></div>
     103            <div class="header-updated"><?php esc_html_e('Last Updated', 'nexlifydesk'); ?></div>
    103104        </div>
    104105       
     
    126127                        $is_unread = isset($ticket->is_unread) ? $ticket->is_unread : false;
    127128                        $last_reply_time = isset($ticket->last_reply_time) ? $ticket->last_reply_time : $ticket->created_at;
     129                        $last_updated_time = isset($ticket->last_updated_time) ? $ticket->last_updated_time : $ticket->updated_at;
    128130                        ?>
    129131                        <div class="ticket-row <?php echo $is_unread ? 'unread' : ''; ?>"
     
    168170                                <?php endif; ?>
    169171                            </div>
    170                             <div class="row-date">
     172                            <div class="row-created">
     173                                <span class="date-time"><?php echo esc_html(date_i18n('M j, Y', strtotime($ticket->created_at))); ?></span>
     174                                <span class="time-ago"><?php echo esc_html(human_time_diff(strtotime($ticket->created_at), current_time('timestamp')) . ' ago'); ?></span>
     175                            </div>
     176                            <div class="row-updated">
    171177                                <span class="date-time"><?php echo esc_html(date_i18n('M j, Y', strtotime($last_reply_time))); ?></span>
    172178                                <span class="time-ago"><?php echo esc_html(human_time_diff(strtotime($last_reply_time), current_time('timestamp')) . ' ago'); ?></span>
  • nexlifydesk/trunk/templates/frontend/ticket-form.php

    r3330741 r3334167  
    103103                        <p class="file-info">
    104104                            <?php
    105                             // Get server limits for display
    106105                            $server_max_size = min(
    107106                                wp_max_upload_size(),
     
    111110                            $plugin_max_mb = $max_file_size;
    112111                           
    113                             // Use the smaller of plugin setting or server limit
    114112                            $effective_max_mb = min($plugin_max_mb, $server_max_mb);
    115113                           
     
    121119                            );
    122120                           
    123                             // Show server limit warning if it's lower than plugin setting
    124121                            if ($server_max_mb < $plugin_max_mb) {
    125122                                echo '<br><small style="color: #d63638;">';
     
    170167        $ticket_number = '';
    171168        $ticket_id = 0;
    172         if (
    173             isset($_GET['ticket_submitted'], $_GET['_wpnonce'])
    174             && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'nexlifydesk-ajax-nonce')
    175         ) {
    176             $ticket_submitted = absint($_GET['ticket_submitted']);
    177             if ($ticket_submitted === 1) {
    178                 $ticket_number = isset($_GET['ticket_number']) ? sanitize_text_field(wp_unslash($_GET['ticket_number'])) : '';
    179                 $ticket_id = isset($_GET['ticket_id']) ? absint($_GET['ticket_id']) : 0;
    180             }
     169        $duplicate_detected = 0;
     170       
     171        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET parameters for display purposes only, no sensitive data processing
     172        if (isset($_GET['ticket_submitted']) && absint($_GET['ticket_submitted']) === 1) {
     173            $ticket_submitted = 1;
     174            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET parameters for display purposes only
     175            $ticket_number = isset($_GET['ticket_number']) ? sanitize_text_field(wp_unslash($_GET['ticket_number'])) : '';
     176            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET parameters for display purposes only
     177            $ticket_id = isset($_GET['ticket_id']) ? sanitize_text_field(wp_unslash($_GET['ticket_id'])) : '';
    181178        }
     179       
     180        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET parameters for display purposes only, no sensitive data processing
     181        if (isset($_GET['duplicate_detected']) && absint($_GET['duplicate_detected']) === 1) {
     182            $duplicate_detected = 1;
     183            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET parameters for display purposes only
     184            $ticket_id = isset($_GET['ticket_id']) ? sanitize_text_field(wp_unslash($_GET['ticket_id'])) : '';
     185        }
     186       
    182187        if ($ticket_submitted === 1) :
    183188        ?>
     
    195200            </div>
    196201            <div class="success-actions">
    197                 <a href="<?php echo esc_url(add_query_arg('ticket_id', $ticket_id, get_permalink(get_option('nexlifydesk_settings')['ticket_page_id']))); ?>"
    198                    class="view-ticket-btn">
    199                     <?php esc_html_e('View Your Ticket', 'nexlifydesk'); ?>
    200                 </a>
     202                <?php
     203                $settings = get_option('nexlifydesk_settings', array());
     204                $ticket_page_ids = isset($settings['ticket_page_id']) ? $settings['ticket_page_id'] : array();
     205                if (!is_array($ticket_page_ids)) {
     206                    $ticket_page_ids = $ticket_page_ids ? array($ticket_page_ids) : array();
     207                }
     208               
     209                if (!empty($ticket_page_ids)) {
     210                    $ticket_page_url = get_permalink($ticket_page_ids[0]);
     211                    if ($ticket_page_url) {
     212                        $view_ticket_url = add_query_arg('ticket_id', $ticket_id, $ticket_page_url);
     213                        echo '<a href="' . esc_url($view_ticket_url) . '" class="view-ticket-btn">' . esc_html__('View Your Ticket', 'nexlifydesk') . '</a>';
     214                    }
     215                }
     216                ?>
     217            </div>
     218        </div>
     219        <?php endif; ?>
     220       
     221        <?php if ($duplicate_detected === 1) : ?>
     222        <div class="form-card success-message" style="margin-top: 30px; border-color: #ff9800;">
     223            <div class="form-header success-header">
     224                <h1><?php esc_html_e('Message Added to Existing Ticket', 'nexlifydesk'); ?></h1>
     225                <p>
     226                    <?php
     227                    printf(
     228                        /* translators: %s: Ticket ID */
     229                        esc_html__('We found a similar ticket from you. Your message has been added to ticket %s.', 'nexlifydesk'),
     230                        '<strong>#' . esc_html($ticket_id) . '</strong>'
     231                    ); ?>
     232                </p>
     233            </div>
     234            <div class="success-actions">
     235                <?php
     236                $settings = get_option('nexlifydesk_settings', array());
     237                $ticket_page_ids = isset($settings['ticket_page_id']) ? $settings['ticket_page_id'] : array();
     238                if (!is_array($ticket_page_ids)) {
     239                    $ticket_page_ids = $ticket_page_ids ? array($ticket_page_ids) : array();
     240                }
     241               
     242                if (!empty($ticket_page_ids)) {
     243                    $ticket_page_url = get_permalink($ticket_page_ids[0]);
     244                    if ($ticket_page_url) {
     245                        $view_ticket_url = add_query_arg('ticket_id', $ticket_id, $ticket_page_url);
     246                        echo '<a href="' . esc_url($view_ticket_url) . '" class="view-ticket-btn">' . esc_html__('View Your Ticket', 'nexlifydesk') . '</a>';
     247                    }
     248                }
     249                ?>
    201250            </div>
    202251        </div>
  • nexlifydesk/trunk/templates/frontend/ticket-single.php

    r3333095 r3334167  
    4444
    4545$current_user = wp_get_current_user();
    46 // SECURITY FIX: Frontend access should ONLY be for ticket owners
    47 // Agents and administrators should ONLY access tickets from admin panel
    4846$is_ticket_owner = ($ticket && (int)$ticket->user_id === (int)$current_user->ID);
    4947
    50 // Handle unregistered user tickets (user_id = 0) by checking customer_email
    5148if (!$is_ticket_owner && $ticket && (int)$ticket->user_id === 0) {
    5249    $customer_email = get_post_meta($ticket->id, 'customer_email', true);
  • nexlifydesk/trunk/uninstall.php

    r3333214 r3334167  
    2323    global $wpdb;
    2424
    25     // Get settings to check if data should be kept
    2625    $settings = get_option('nexlifydesk_settings', array());
    2726    $keep_data = isset($settings['keep_data_on_uninstall']) ? (bool)$settings['keep_data_on_uninstall'] : true;
    2827
    29     // Always remove scheduled hooks and transients (cleanup)
    3028    wp_clear_scheduled_hook('nexlifydesk_sla_check');
    3129    wp_clear_scheduled_hook('nexlifydesk_auto_close_tickets');
     
    3533    delete_transient('nexlifydesk_google_oauth_state');
    3634   
    37     // Clear any cached data
    3835    wp_cache_flush();
    3936
    40     // If user wants to keep data, only do minimal cleanup
    4137    if ($keep_data) {
    42         // Remove only version option but keep all user data
    4338        delete_option('nexlifydesk_db_version');
    4439        return;
    4540    }
    4641
    47     // User wants complete data removal - proceed with full cleanup
    48    
    49     // Remove all plugin options and settings
    5042    $options_to_remove = array(
    5143        'nexlifydesk_settings',
     
    6153    foreach ($options_to_remove as $option) {
    6254        delete_option($option);
    63         // Also remove from network options if multisite
    6455        if (is_multisite()) {
    6556            delete_network_option(null, $option);
     
    6758    }
    6859
    69     // Remove custom database tables
    7060    $table_names = array(
    7161        $wpdb->prefix . 'nexlifydesk_tickets',
     
    8070    }
    8171
    82     // Remove custom user roles
    8372    remove_role('nexlifydesk_agent');
    8473    remove_role('nexlifydesk_supervisor');
    8574
    86     // Remove custom capabilities from administrator role
    8775    $admin_role = get_role('administrator');
    8876    if ($admin_role) {
     
    10088    }
    10189
    102     // Remove uploaded files directory
    10390    $upload_dir = wp_upload_dir();
    10491    $plugin_upload_dir = trailingslashit($upload_dir['basedir']) . 'nexlifydesk/';
     
    10895    }
    10996
    110     // Remove any user meta related to the plugin
    11197    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct deletion is required for uninstall cleanup
    11298    $wpdb->query($wpdb->prepare(
     
    114100        'nexlifydesk_%'
    115101    ));
    116     // Optionally clear usermeta cache after deletion
    117102    wp_cache_flush();
    118103
    119     // Clean up any remaining cache entries and transients
    120104    $cache_keys_patterns = array(
    121105        'nexlifydesk_ticket_',
     
    128112    );
    129113   
    130     // Attempt to clean up cache entries (this is a best-effort cleanup)
    131114    global $wp_object_cache;
    132115    if (isset($wp_object_cache->cache)) {
     
    170153            nexlifydesk_recursive_delete_directory($path);
    171154        } else {
    172             // Use WordPress file deletion function for security
    173155            wp_delete_file($path);
    174156        }
    175157    }
    176158   
    177     // Use WP_Filesystem for directory removal
    178159    global $wp_filesystem;
    179160    if (empty($wp_filesystem)) {
     
    184165}
    185166
    186 // Execute the uninstall process
     167/** Execute the uninstall process */
    187168nexlifydesk_execute_uninstall();
Note: See TracChangeset for help on using the changeset viewer.