Changeset 3334167
- Timestamp:
- 07/25/2025 12:22:43 PM (7 months ago)
- Location:
- nexlifydesk
- Files:
-
- 338 added
- 1 deleted
- 26 edited
-
tags/1.0.5 (added)
-
tags/1.0.5/assets (added)
-
tags/1.0.5/assets/css (added)
-
tags/1.0.5/assets/css/nexlifydesk-admin.css (added)
-
tags/1.0.5/assets/css/nexlifydesk.css (added)
-
tags/1.0.5/assets/images (added)
-
tags/1.0.5/assets/images/dashboard-icon.png (added)
-
tags/1.0.5/assets/images/file-types (added)
-
tags/1.0.5/assets/images/file-types/document.png (added)
-
tags/1.0.5/assets/images/file-types/image.png (added)
-
tags/1.0.5/assets/images/file-types/pdf.png (added)
-
tags/1.0.5/assets/images/nexlifydesk-logo-small.png (added)
-
tags/1.0.5/assets/images/nexlifydesk-logo.png (added)
-
tags/1.0.5/assets/images/priority (added)
-
tags/1.0.5/assets/images/priority/high.png (added)
-
tags/1.0.5/assets/images/priority/low.png (added)
-
tags/1.0.5/assets/images/priority/medium.png (added)
-
tags/1.0.5/assets/images/status (added)
-
tags/1.0.5/assets/images/status/closed.png (added)
-
tags/1.0.5/assets/images/status/open.png (added)
-
tags/1.0.5/assets/images/status/pending.png (added)
-
tags/1.0.5/assets/images/status/resolved.png (added)
-
tags/1.0.5/assets/images/support-icon.png (added)
-
tags/1.0.5/assets/images/ticket-icon.png (added)
-
tags/1.0.5/assets/js (added)
-
tags/1.0.5/assets/js/admin-ticket-list.js (added)
-
tags/1.0.5/assets/js/nexlifydesk.js (added)
-
tags/1.0.5/email-source (added)
-
tags/1.0.5/email-source/nexlifydesk-email-pipe.php (added)
-
tags/1.0.5/email-source/providers (added)
-
tags/1.0.5/email-source/providers/aws-ses (added)
-
tags/1.0.5/email-source/providers/aws-ses/aws-handler.php (added)
-
tags/1.0.5/email-source/providers/google (added)
-
tags/1.0.5/email-source/providers/google/google-handler.php (added)
-
tags/1.0.5/email-source/providers/outlook (added)
-
tags/1.0.5/includes (added)
-
tags/1.0.5/includes/class-nexlifydesk-admin.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-ajax.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-database.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-rate-limiter.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-reports.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-shortcodes.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-tickets.php (added)
-
tags/1.0.5/includes/class-nexlifydesk-users.php (added)
-
tags/1.0.5/includes/class-support.php (added)
-
tags/1.0.5/includes/helpers.php (added)
-
tags/1.0.5/includes/nexlifydesk-functions.php (added)
-
tags/1.0.5/includes/textanalysis (added)
-
tags/1.0.5/includes/textanalysis/Comparisons (added)
-
tags/1.0.5/includes/textanalysis/Comparisons/CosineSimilarityComparison.php (added)
-
tags/1.0.5/includes/textanalysis/Corpus (added)
-
tags/1.0.5/includes/textanalysis/Corpus/WordnetCorpus.php (added)
-
tags/1.0.5/includes/textanalysis/Documents (added)
-
tags/1.0.5/includes/textanalysis/Documents/DocumentAbstract.php (added)
-
tags/1.0.5/includes/textanalysis/Documents/TokensDocument.php (added)
-
tags/1.0.5/includes/textanalysis/Interfaces (added)
-
tags/1.0.5/includes/textanalysis/Interfaces/IDistance.php (added)
-
tags/1.0.5/includes/textanalysis/Interfaces/IExtractStrategy.php (added)
-
tags/1.0.5/includes/textanalysis/Interfaces/ISimilarity.php (added)
-
tags/1.0.5/includes/textanalysis/Interfaces/IStemmer.php (added)
-
tags/1.0.5/includes/textanalysis/Interfaces/ITokenTransformation.php (added)
-
tags/1.0.5/includes/textanalysis/SECURITY.md (added)
-
tags/1.0.5/includes/textanalysis/Tokenizers (added)
-
tags/1.0.5/includes/textanalysis/Tokenizers/GeneralTokenizer.php (added)
-
tags/1.0.5/includes/textanalysis/Tokenizers/TokenizerAbstract.php (added)
-
tags/1.0.5/includes/textanalysis/Tokenizers/WhitespaceTokenizer.php (added)
-
tags/1.0.5/languages (added)
-
tags/1.0.5/languages/nexlifydesk-de_DE_formal.mo (added)
-
tags/1.0.5/languages/nexlifydesk-de_DE_formal.po (added)
-
tags/1.0.5/languages/nexlifydesk-es_ES.mo (added)
-
tags/1.0.5/languages/nexlifydesk-es_ES.po (added)
-
tags/1.0.5/languages/nexlifydesk-fr_FR.mo (added)
-
tags/1.0.5/languages/nexlifydesk-fr_FR.po (added)
-
tags/1.0.5/languages/nexlifydesk-it_IT.mo (added)
-
tags/1.0.5/languages/nexlifydesk-it_IT.po (added)
-
tags/1.0.5/languages/nexlifydesk-ja.mo (added)
-
tags/1.0.5/languages/nexlifydesk-ja.po (added)
-
tags/1.0.5/languages/nexlifydesk-pt_BR.mo (added)
-
tags/1.0.5/languages/nexlifydesk-pt_BR.po (added)
-
tags/1.0.5/languages/nexlifydesk-pt_PT.mo (added)
-
tags/1.0.5/languages/nexlifydesk-pt_PT.po (added)
-
tags/1.0.5/languages/nexlifydesk-ru_RU.mo (added)
-
tags/1.0.5/languages/nexlifydesk-ru_RU.po (added)
-
tags/1.0.5/languages/nexlifydesk-zh_CN.mo (added)
-
tags/1.0.5/languages/nexlifydesk-zh_CN.po (added)
-
tags/1.0.5/languages/nexlifydesk.pot (added)
-
tags/1.0.5/license.txt (added)
-
tags/1.0.5/nexlifydesk.php (added)
-
tags/1.0.5/readme.txt (added)
-
tags/1.0.5/templates (added)
-
tags/1.0.5/templates/admin (added)
-
tags/1.0.5/templates/admin/imap-auth.php (added)
-
tags/1.0.5/templates/admin/partials (added)
-
tags/1.0.5/templates/admin/partials/single-reply.php (added)
-
tags/1.0.5/templates/admin/reports.php (added)
-
tags/1.0.5/templates/admin/settings.php (added)
-
tags/1.0.5/templates/admin/ticket-single.php (added)
-
tags/1.0.5/templates/admin/tickets-list.php (added)
-
tags/1.0.5/templates/emails (added)
-
tags/1.0.5/templates/emails/new_reply.php (added)
-
tags/1.0.5/templates/emails/new_ticket.php (added)
-
tags/1.0.5/templates/emails/sla_breach.php (added)
-
tags/1.0.5/templates/emails/status_changed.php (added)
-
tags/1.0.5/templates/frontend (added)
-
tags/1.0.5/templates/frontend/partials (added)
-
tags/1.0.5/templates/frontend/partials/single-reply.php (added)
-
tags/1.0.5/templates/frontend/ticket-form.php (added)
-
tags/1.0.5/templates/frontend/ticket-list.php (added)
-
tags/1.0.5/templates/frontend/ticket-single.php (added)
-
tags/1.0.5/uninstall.php (added)
-
tags/1.0.5/vendor (added)
-
tags/1.0.5/vendor/freemius (added)
-
tags/1.0.5/vendor/freemius/LICENSE.txt (added)
-
tags/1.0.5/vendor/freemius/README.md (added)
-
tags/1.0.5/vendor/freemius/assets (added)
-
tags/1.0.5/vendor/freemius/assets/css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/account.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/add-ons.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/affiliation.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/checkout.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/clone-resolution.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/common.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/connect.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/debug.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/dialog-boxes.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/gdpr-optin-notice.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/index.php (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/optout.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/admin/plugins.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/customizer.css (added)
-
tags/1.0.5/vendor/freemius/assets/css/index.php (added)
-
tags/1.0.5/vendor/freemius/assets/img (added)
-
tags/1.0.5/vendor/freemius/assets/img/index.php (added)
-
tags/1.0.5/vendor/freemius/assets/img/nexlifydesk.png (added)
-
tags/1.0.5/vendor/freemius/assets/img/plugin-icon.png (added)
-
tags/1.0.5/vendor/freemius/assets/img/theme-icon.png (added)
-
tags/1.0.5/vendor/freemius/assets/index.php (added)
-
tags/1.0.5/vendor/freemius/assets/js (added)
-
tags/1.0.5/vendor/freemius/assets/js/index.php (added)
-
tags/1.0.5/vendor/freemius/assets/js/jquery.form.js (added)
-
tags/1.0.5/vendor/freemius/assets/js/nojquery.ba-postmessage.js (added)
-
tags/1.0.5/vendor/freemius/assets/js/postmessage.js (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/14fb1bd5b7c41648488b06147f50a0dc.svg (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/178afa6030e76635dbe835e111d2c507.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/27b5a722a5553d9de0170325267fccec.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/4375c4a3ddc6f637c2ab9a2d7220f91e.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/4529cac82a2d1f300d3c4702b7b5e8f3.svg (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/5480ed23b199531a8cbc05924f26952b.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/b4f3b958f4a019862d81b15f3f8eee3a.svg (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/c03f665db27af43971565560adfba594.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/cb5fc4f6ec7ada72e986f6e7dde365bf.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/dd89563360f0272635c8f0ab7d7f1402.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/e366d70661d8ad2493bd6afbd779f125.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/f18006f6535a1a6e9c6bfbffafe6f18a.svg (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/f3aac72a8e63997d6bb888f816457e9b.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/f928f1be99776af83e8e6be4baf8ffe7.svg (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/fde48e4609a6ddc11d639fc2421f2afd.png (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/freemius-pricing.js (added)
-
tags/1.0.5/vendor/freemius/assets/js/pricing/freemius-pricing.js.LICENSE.txt (added)
-
tags/1.0.5/vendor/freemius/composer.json (added)
-
tags/1.0.5/vendor/freemius/config.php (added)
-
tags/1.0.5/vendor/freemius/includes (added)
-
tags/1.0.5/vendor/freemius/includes/class-freemius-abstract.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-freemius.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-admin-notices.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-api.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-garbage-collector.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-lock.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-logger.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-options.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-plugin-updater.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-security.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-storage.php (added)
-
tags/1.0.5/vendor/freemius/includes/class-fs-user-lock.php (added)
-
tags/1.0.5/vendor/freemius/includes/customizer (added)
-
tags/1.0.5/vendor/freemius/includes/customizer/class-fs-customizer-support-section.php (added)
-
tags/1.0.5/vendor/freemius/includes/customizer/class-fs-customizer-upsell-control.php (added)
-
tags/1.0.5/vendor/freemius/includes/customizer/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/debug (added)
-
tags/1.0.5/vendor/freemius/includes/debug/class-fs-debug-bar-panel.php (added)
-
tags/1.0.5/vendor/freemius/includes/debug/debug-bar-start.php (added)
-
tags/1.0.5/vendor/freemius/includes/debug/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-affiliate-terms.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-affiliate.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-billing.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-entity.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-payment.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-plugin-info.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-plugin-license.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-plugin-plan.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-plugin-tag.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-plugin.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-pricing.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-scope-entity.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-site.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-subscription.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/class-fs-user.php (added)
-
tags/1.0.5/vendor/freemius/includes/entities/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/fs-core-functions.php (added)
-
tags/1.0.5/vendor/freemius/includes/fs-essential-functions.php (added)
-
tags/1.0.5/vendor/freemius/includes/fs-html-escaping-functions.php (added)
-
tags/1.0.5/vendor/freemius/includes/fs-plugin-info-dialog.php (added)
-
tags/1.0.5/vendor/freemius/includes/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/l10n.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-admin-menu-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-admin-notice-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-cache-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-checkout-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-clone-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-contact-form-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-debug-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-gdpr-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-key-value-storage.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-license-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-option-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-permission-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-plan-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/class-fs-plugin-manager.php (added)
-
tags/1.0.5/vendor/freemius/includes/managers/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions/ArgumentNotExistException.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions/EmptyArgumentException.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions/Exception.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions/InvalidArgumentException.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions/OAuthException.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/Exceptions/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/FreemiusBase.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/FreemiusWordPress.php (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/LICENSE.txt (added)
-
tags/1.0.5/vendor/freemius/includes/sdk/index.php (added)
-
tags/1.0.5/vendor/freemius/includes/supplements (added)
-
tags/1.0.5/vendor/freemius/includes/supplements/fs-essential-functions-1.1.7.1.php (added)
-
tags/1.0.5/vendor/freemius/includes/supplements/fs-essential-functions-2.2.1.php (added)
-
tags/1.0.5/vendor/freemius/includes/supplements/fs-migration-2.5.1.php (added)
-
tags/1.0.5/vendor/freemius/includes/supplements/index.php (added)
-
tags/1.0.5/vendor/freemius/index.php (added)
-
tags/1.0.5/vendor/freemius/languages (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-cs_CZ.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-da_DK.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-de_DE.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-es_ES.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-fr_FR.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-he_IL.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-hu_HU.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-it_IT.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-ja.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-nl_NL.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-ru_RU.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-ta.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius-zh_CN.mo (added)
-
tags/1.0.5/vendor/freemius/languages/freemius.pot (added)
-
tags/1.0.5/vendor/freemius/languages/index.php (added)
-
tags/1.0.5/vendor/freemius/require.php (added)
-
tags/1.0.5/vendor/freemius/start.php (added)
-
tags/1.0.5/vendor/freemius/templates (added)
-
tags/1.0.5/vendor/freemius/templates/account (added)
-
tags/1.0.5/vendor/freemius/templates/account.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/billing.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials/activate-license-button.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials/addon.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials/deactivate-license-button.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials/disconnect-button.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/partials/site.php (added)
-
tags/1.0.5/vendor/freemius/templates/account/payments.php (added)
-
tags/1.0.5/vendor/freemius/templates/add-ons.php (added)
-
tags/1.0.5/vendor/freemius/templates/add-trial-to-pricing.php (added)
-
tags/1.0.5/vendor/freemius/templates/admin-notice.php (added)
-
tags/1.0.5/vendor/freemius/templates/ajax-loader.php (added)
-
tags/1.0.5/vendor/freemius/templates/api-connectivity-message-js.php (added)
-
tags/1.0.5/vendor/freemius/templates/auto-installation.php (added)
-
tags/1.0.5/vendor/freemius/templates/checkout (added)
-
tags/1.0.5/vendor/freemius/templates/checkout.php (added)
-
tags/1.0.5/vendor/freemius/templates/checkout/frame.php (added)
-
tags/1.0.5/vendor/freemius/templates/checkout/process-redirect.php (added)
-
tags/1.0.5/vendor/freemius/templates/checkout/redirect.php (added)
-
tags/1.0.5/vendor/freemius/templates/clone-resolution-js.php (added)
-
tags/1.0.5/vendor/freemius/templates/connect (added)
-
tags/1.0.5/vendor/freemius/templates/connect.php (added)
-
tags/1.0.5/vendor/freemius/templates/connect/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/connect/permission.php (added)
-
tags/1.0.5/vendor/freemius/templates/connect/permissions-group.php (added)
-
tags/1.0.5/vendor/freemius/templates/contact.php (added)
-
tags/1.0.5/vendor/freemius/templates/debug (added)
-
tags/1.0.5/vendor/freemius/templates/debug.php (added)
-
tags/1.0.5/vendor/freemius/templates/debug/api-calls.php (added)
-
tags/1.0.5/vendor/freemius/templates/debug/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/debug/logger.php (added)
-
tags/1.0.5/vendor/freemius/templates/debug/plugins-themes-sync.php (added)
-
tags/1.0.5/vendor/freemius/templates/debug/scheduled-crons.php (added)
-
tags/1.0.5/vendor/freemius/templates/email.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms (added)
-
tags/1.0.5/vendor/freemius/templates/forms/affiliation.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/data-debug-mode.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/deactivation (added)
-
tags/1.0.5/vendor/freemius/templates/forms/deactivation/contact.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/deactivation/form.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/deactivation/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/deactivation/retry-skip.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/email-address-update.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/license-activation.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/optout.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/premium-versions-upgrade-handler.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/premium-versions-upgrade-metadata.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/resend-key.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/subscription-cancellation.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/trial-start.php (added)
-
tags/1.0.5/vendor/freemius/templates/forms/user-change.php (added)
-
tags/1.0.5/vendor/freemius/templates/gdpr-optin-js.php (added)
-
tags/1.0.5/vendor/freemius/templates/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/js (added)
-
tags/1.0.5/vendor/freemius/templates/js/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/js/jquery.content-change.php (added)
-
tags/1.0.5/vendor/freemius/templates/js/open-license-activation.php (added)
-
tags/1.0.5/vendor/freemius/templates/js/permissions.php (added)
-
tags/1.0.5/vendor/freemius/templates/js/style-premium-theme.php (added)
-
tags/1.0.5/vendor/freemius/templates/partials (added)
-
tags/1.0.5/vendor/freemius/templates/partials/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/partials/network-activation.php (added)
-
tags/1.0.5/vendor/freemius/templates/plugin-icon.php (added)
-
tags/1.0.5/vendor/freemius/templates/plugin-info (added)
-
tags/1.0.5/vendor/freemius/templates/plugin-info/description.php (added)
-
tags/1.0.5/vendor/freemius/templates/plugin-info/features.php (added)
-
tags/1.0.5/vendor/freemius/templates/plugin-info/index.php (added)
-
tags/1.0.5/vendor/freemius/templates/plugin-info/screenshots.php (added)
-
tags/1.0.5/vendor/freemius/templates/pricing.php (added)
-
tags/1.0.5/vendor/freemius/templates/secure-https-header.php (added)
-
tags/1.0.5/vendor/freemius/templates/sticky-admin-notice-js.php (added)
-
tags/1.0.5/vendor/freemius/templates/tabs-capture-js.php (added)
-
tags/1.0.5/vendor/freemius/templates/tabs.php (added)
-
trunk/assets/css/nexlifydesk-admin.css (modified) (10 diffs)
-
trunk/assets/css/nexlifydesk.css (modified) (1 diff)
-
trunk/assets/js/admin-ticket-list.js (modified) (1 diff)
-
trunk/assets/js/nexlifydesk.js (modified) (8 diffs)
-
trunk/email-source/nexlifydesk-email-pipe.php (modified) (5 diffs)
-
trunk/email-source/providers/aws-ses/aws-handler.php (modified) (2 diffs)
-
trunk/email-source/providers/google/google-handler.php (modified) (4 diffs)
-
trunk/includes/class-nexlifydesk-admin.php (modified) (10 diffs)
-
trunk/includes/class-nexlifydesk-ajax.php (modified) (18 diffs)
-
trunk/includes/class-nexlifydesk-database.php (modified) (3 diffs)
-
trunk/includes/class-nexlifydesk-reports.php (modified) (10 diffs)
-
trunk/includes/class-nexlifydesk-shortcodes.php (modified) (7 diffs)
-
trunk/includes/class-nexlifydesk-tickets.php (modified) (25 diffs)
-
trunk/includes/class-nexlifydesk-users.php (modified) (1 diff)
-
trunk/includes/helpers.php (modified) (10 diffs)
-
trunk/includes/nexlifydesk-functions.php (modified) (11 diffs)
-
trunk/includes/textanalysis/Console (deleted)
-
trunk/includes/textanalysis/SECURITY.md (modified) (1 diff)
-
trunk/nexlifydesk.php (modified) (14 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/templates/admin/imap-auth.php (modified) (1 diff)
-
trunk/templates/admin/settings.php (modified) (6 diffs)
-
trunk/templates/admin/ticket-single.php (modified) (3 diffs)
-
trunk/templates/admin/tickets-list.php (modified) (3 diffs)
-
trunk/templates/frontend/ticket-form.php (modified) (5 diffs)
-
trunk/templates/frontend/ticket-single.php (modified) (1 diff)
-
trunk/uninstall.php (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
-
nexlifydesk/trunk/assets/css/nexlifydesk-admin.css
r3333095 r3334167 352 352 } 353 353 354 /* Gmail-style ticket list */355 354 .nexlifydesk-admin-ticket-list-ui .bulk-actions { 356 355 display: flex; … … 373 372 border: 1px solid #e2e8f0; 374 373 overflow: hidden; 374 min-width: auto; 375 overflow-x: auto; 375 376 } 376 377 377 378 .nexlifydesk-admin-ticket-list-ui .ticket-list-header { 378 379 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; 380 381 gap: 12px; 381 382 padding: 12px 16px; … … 389 390 } 390 391 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 391 401 .nexlifydesk-admin-ticket-list-ui .ticket-row { 392 402 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; 394 404 gap: 12px; 395 405 padding: 16px; … … 407 417 .nexlifydesk-admin-ticket-list-ui .ticket-list-header, 408 418 .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; 410 420 gap: 8px; 411 421 } … … 415 425 .nexlifydesk-admin-ticket-list-ui .ticket-list-header, 416 426 .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; 418 428 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; 419 455 } 420 456 } … … 615 651 } 616 652 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 { 618 656 display: flex; 619 657 flex-direction: column; 620 658 gap: 2px; 659 min-width: 0; 660 white-space: nowrap; 661 overflow: hidden; 621 662 } 622 663 … … 625 666 color: #1e293b; 626 667 font-weight: 500; 668 white-space: nowrap; 669 overflow: hidden; 670 text-overflow: ellipsis; 627 671 } 628 672 … … 630 674 font-size: 11px; 631 675 color: #64748b; 676 white-space: nowrap; 677 overflow: hidden; 678 text-overflow: ellipsis; 632 679 } 633 680 … … 1351 1398 } 1352 1399 1353 /* IMAP Authentication Spam Protection Styles*/1400 /* IMAP Authentication Spam Protection */ 1354 1401 .nexlifydesk-spam-protection { 1355 1402 border-top: 1px solid #ccd0d4; … … 1377 1424 } 1378 1425 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 790 790 } 791 791 792 /* --- Frontend Ticket List Styles (Table Design)--- */792 /* --- Frontend Ticket List Styles --- */ 793 793 .nexlifydesk-table-container { 794 794 width: 100%; -
nexlifydesk/trunk/assets/js/admin-ticket-list.js
r3330751 r3334167 509 509 ` : '<span class="unassigned">Unassigned</span>'} 510 510 </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> 514 518 </div> 515 519 </div> -
nexlifydesk/trunk/assets/js/nexlifydesk.js
r3333214 r3334167 21 21 $('.page-title-action').on('click', function(e) { 22 22 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 }); 24 44 }); 25 45 … … 27 47 e.preventDefault(); 28 48 if (!isPluginValid) return; 29 if (!confirm(nexlifydesk_admin_vars.confirm_delete)) return; 30 49 31 50 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 32 57 $.ajax({ 33 58 url: nexlifydesk_admin_vars.ajaxurl, … … 40 65 success: function(response) { 41 66 if (response.success) { 67 alert(response.data); 42 68 location.reload(); 43 69 } 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 } 45 75 } 76 }, 77 error: function() { 78 alert('An error occurred while deleting the category.'); 46 79 } 47 80 }); 48 81 }); 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 } 49 149 50 150 $('#nexlifydesk-tickets-filter').on('submit', function(e) { … … 106 206 var form = $(this); 107 207 var formData = new FormData(form[0]); 208 209 formData.append('current_url', window.location.href); 210 108 211 var submitBtn = $('#submit-ticket-btn'); 109 212 var buttonText = submitBtn.find('.button-text'); … … 126 229 return; 127 230 } 231 128 232 $('#nexlifydesk-message') 129 233 .removeClass('error') … … 131 235 .text(response.data.message || nexlifydesk_vars.ticket_submitted_text) 132 236 .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 }147 237 148 238 form[0].reset(); … … 190 280 var $form = $(this); 191 281 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'); 197 304 198 305 var formData = { 199 306 action: 'nexlifydesk_add_category', 200 307 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(), 203 310 submit_category: true 204 311 }; 205 312 206 313 $.ajax({ 207 url: nexlifydesk_admin_vars.ajaxurl, 314 url: nexlifydesk_admin_vars.ajaxurl, 208 315 type: 'POST', 209 316 data: formData, 317 timeout: 10000, 210 318 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 213 329 setTimeout(function() { 214 location.reload(); 215 }, 1000); 330 $form.slideUp(300, function() { 331 location.reload(); 332 }); 333 }, 1500); 216 334 } 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(); 218 341 } 219 342 }, 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(); 222 352 }, 223 353 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'); 225 358 } 226 359 }); … … 1959 2092 } 1960 2093 1961 // Make functions globally available1962 2094 window.nexlifydesk_update_templates = nexlifydesk_update_templates; 1963 2095 window.nexlifydesk_dismiss_notice = nexlifydesk_dismiss_notice; -
nexlifydesk/trunk/email-source/nexlifydesk-email-pipe.php
r3333214 r3334167 62 62 63 63 /** 64 * Clean email subject for better duplicate detection65 * Removes Re:, Fwd:, etc. prefixes and normalizes the subject66 64 * 67 65 * @param string $subject The email subject line … … 91 89 $table_name = $wpdb->prefix . 'nexlifydesk_tickets'; 92 90 93 // For non-registered users (user_id = 0)94 91 if ($user_id == 0 && !empty($email)) { 95 92 … … 164 161 165 162 function nexlifydesk_fetch_custom_emails() { 166 // Check if IMAP extension is available167 163 if (!extension_loaded('imap')) { 168 164 add_action('admin_notices', function() { … … 186 182 $delete_emails = isset($settings['delete_emails_after_fetch']) ? $settings['delete_emails_after_fetch'] : 1; 187 183 188 // Validate required settings189 184 if (empty($host) || empty($port) || empty($username) || empty($password)) { 190 185 return array('error' => 'Custom IMAP/POP3 credentials not configured. Please configure Host, Port, Username, and Password.'); 191 186 } 192 187 193 // Initialize tracking variables194 188 $tickets_created = 0; 195 189 $replies_added = 0; … … 231 225 $date = isset($overview->date) ? $overview->date : ''; 232 226 233 // Decode MIME-encoded subject first, then apply general email content decoding234 227 if (function_exists('nexlifydesk_decode_email_subject')) { 235 228 $subject = nexlifydesk_decode_email_subject($subject); -
nexlifydesk/trunk/email-source/providers/aws-ses/aws-handler.php
r3333214 r3334167 315 315 316 316 /** 317 * AWS SES Authentication Handler318 317 * Based on WP Mail SMTP Pro structure but adapted for NexlifyDesk 319 318 */ … … 333 332 334 333 /** 335 * Check if SSL is enabled for the site336 334 * Uses centralized SSL detection from helpers.php 337 335 */ -
nexlifydesk/trunk/email-source/providers/google/google-handler.php
r3333214 r3334167 3 3 if (!defined('ABSPATH')) exit; 4 4 5 // Ensure helpers are available for email decoding functions6 5 if (!function_exists('nexlifydesk_decode_email_subject')) { 7 6 require_once dirname(__FILE__) . '/../../includes/helpers.php'; … … 150 149 $delete_emails = isset($settings['delete_emails_after_fetch']) ? $settings['delete_emails_after_fetch'] : 1; 151 150 152 // Check if Google credentials are configured153 151 if (empty($settings['google_client_id']) || empty($settings['google_client_secret']) || empty($settings['google_refresh_token'])) { 154 152 return array('error' => 'Google credentials not configured. Please authorize with Google first.'); … … 225 223 $subject_header = array_values(array_filter($headers, fn($h) => $h['name'] === 'Subject'))[0]['value'] ?? '(No Subject)'; 226 224 227 // Decode MIME-encoded subject to fix encoding issues like "=?UTF-8?Q?..."228 225 if (function_exists('nexlifydesk_decode_email_subject')) { 229 226 $subject_header = nexlifydesk_decode_email_subject($subject_header); … … 438 435 439 436 /** 440 * Parses the payload of a Gmail message to find the email body.441 437 * 442 438 * @param array $payload The payload from the Gmail API message. -
nexlifydesk/trunk/includes/class-nexlifydesk-admin.php
r3333214 r3334167 578 578 579 579 <?php 580 // Show IMAP extension warning if not available581 580 if (!extension_loaded('imap')) { 582 581 echo '<div class="notice notice-error"><p><strong>' . esc_html__('Warning:', 'nexlifydesk') . '</strong> ' . … … 628 627 629 628 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'); 635 631 } 636 632 … … 851 847 'save_text' => __('Save', 'nexlifydesk'), 852 848 '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'), 853 854 'delete_confirm' => __('Are you sure you want to delete this position?', 'nexlifydesk'), 854 855 'loading_tickets_text' => __('Loading tickets...', 'nexlifydesk'), … … 939 940 if (!current_user_can('nexlifydesk_view_all_tickets')) { 940 941 $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()) { 942 955 wp_die(esc_html__('You do not have permission to view this ticket.', 'nexlifydesk')); 943 956 } … … 964 977 $ticket = NexlifyDesk_Tickets::get_ticket($ticket_id); 965 978 if ($ticket) { 979 if (method_exists('NexlifyDesk_Tickets', 'mark_ticket_read')) { 980 NexlifyDesk_Tickets::mark_ticket_read($ticket_id); 981 } 966 982 include NEXLIFYDESK_PLUGIN_DIR . 'templates/admin/ticket-single.php'; 967 983 } else { … … 1506 1522 'default_category' => isset($_POST['default_category']) ? absint($_POST['default_category']) : 0, 1507 1523 '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(), 1510 1526 'ticket_id_prefix' => isset($_POST['ticket_id_prefix']) ? sanitize_text_field(wp_unslash($_POST['ticket_id_prefix'])) : '', 1511 1527 'ticket_id_start' => isset($_POST['ticket_id_start']) ? absint($_POST['ticket_id_start']) : 0, … … 1544 1560 1545 1561 $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 ); 1559 1571 1560 1572 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 } 1565 1613 } 1566 1614 … … 1594 1642 1595 1643 wp_cache_delete($cache_key); 1644 1645 self::clear_all_category_caches($wpdb->insert_id); 1596 1646 1597 1647 if (defined('DOING_AJAX') && DOING_AJAX) { … … 1628 1678 1629 1679 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; 1630 1760 } 1631 1761 … … 1746 1876 <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> 1747 1877 <?php 1748 // Show default template as placeholder if empty1749 1878 $auto_response_content = isset($templates['email_auto_response']) ? $templates['email_auto_response'] : ''; 1750 1879 if (empty($auto_response_content)) { 1751 // Default HTML formatted auto-response template1752 1880 $auto_response_content = '<p>Hello {customer_name},</p> 1753 1881 -
nexlifydesk/trunk/includes/class-nexlifydesk-ajax.php
r3333214 r3334167 37 37 38 38 if (isset($_FILES['attachments']) && isset($_FILES['attachments']['error']) && is_array($_FILES['attachments']['error'])) { 39 // Sanitize the error array by casting each value to integer40 39 $attachment_errors = array_map('intval', $_FILES['attachments']['error']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via array_map with intval 41 40 … … 126 125 } else { 127 126 $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]; 131 136 $ticket_page_url = get_permalink($ticket_page_id); 132 } else { 137 } 138 139 if (!$ticket_page_url || !wp_http_validate_url($ticket_page_url)) { 133 140 $ticket_page_url = isset($_POST['current_url']) ? esc_url_raw(wp_unslash($_POST['current_url'])) : home_url(); 134 141 } … … 153 160 $redirect_url = add_query_arg(array( 154 161 'ticket_id' => $ticket->ticket_id, 162 'ticket_number' => $ticket->ticket_id, 155 163 'ticket_submitted' => 1 156 164 ), $ticket_page_url); … … 158 166 wp_send_json_success(array( 159 167 '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 161 171 )); 162 172 } … … 192 202 $can_reply = false; 193 203 194 if (current_user_can(' nexlifydesk_manage_tickets') || current_user_can('manage_options')) {204 if (current_user_can('manage_options')) { 195 205 $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 } 196 217 } 197 218 … … 312 333 if (!$ticket) { 313 334 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 } 314 343 } 315 344 … … 671 700 672 701 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 } 674 717 675 718 if (!current_user_can('manage_options')) { 719 ob_end_clean(); 676 720 wp_send_json_error(__('You do not have permission to add categories.', 'nexlifydesk')); 721 exit; 677 722 } 678 723 … … 680 725 681 726 if (empty($_POST['category_name'])) { 727 ob_end_clean(); 682 728 wp_send_json_error(__('Category name is required.', 'nexlifydesk')); 729 exit; 683 730 } 684 731 … … 686 733 $category_description = isset($_POST['category_description']) ? 687 734 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 } 689 742 690 743 $table_name = NexlifyDesk_Database::get_table('categories'); 691 744 692 745 $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 ); 705 755 706 756 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 } 708 796 } 709 797 … … 721 809 722 810 if ($result === false) { 811 ob_end_clean(); 723 812 wp_send_json_error( 724 813 sprintf( 725 814 /* translators: %s: Database error message */ 726 815 __('Failed to add category: %s', 'nexlifydesk'), 727 $wpdb->last_error 816 $wpdb->last_error ?: 'Unknown database error' 728 817 ) 729 818 ); 730 } 731 819 exit; 820 } 821 822 // Clear caches 823 NexlifyDesk_Admin::clear_all_category_caches($wpdb->insert_id); 824 825 ob_end_clean(); 732 826 wp_send_json_success(array( 733 827 'message' => __('Category added successfully!', 'nexlifydesk') 734 828 )); 829 exit; 735 830 } 736 831 … … 752 847 wp_send_json_error(__('Invalid category ID.', 'nexlifydesk')); 753 848 } 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 755 907 $result = $wpdb->update( 756 908 NexlifyDesk_Database::get_table('categories'), … … 762 914 763 915 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); 767 928 } else { 768 929 wp_send_json_error(__('Could not delete category.', 'nexlifydesk')); … … 988 1149 } 989 1150 990 // Call the actual custom email fetch function to test it991 1151 $result = nexlifydesk_fetch_custom_emails(); 992 1152 … … 1147 1307 /* translators: %d: Ticket ID */ 1148 1308 $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); 1149 1317 continue; 1150 1318 } … … 1200 1368 1201 1369 case 'delete': 1202 // Only allow admins to delete tickets1203 1370 if (!current_user_can('manage_options')) { 1204 1371 /* translators: %d: Ticket ID */ … … 1498 1665 $result = nexlifydesk_fetch_google_emails(); 1499 1666 1500 // Check if there was an error1501 1667 if (is_array($result) && isset($result['error'])) { 1502 1668 wp_send_json_error(array('message' => $result['error'])); … … 1504 1670 } 1505 1671 1506 // If we have a success result with details1507 1672 if (is_array($result) && isset($result['success']) && $result['success']) { 1508 1673 wp_send_json_success(array('message' => $result['message'])); … … 1513 1678 } 1514 1679 1515 // Fallback for legacy behavior - check for recent tickets1516 1680 global $wpdb; 1517 1681 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe and not user input -
nexlifydesk/trunk/includes/class-nexlifydesk-database.php
r3333214 r3334167 193 193 194 194 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'); 196 196 $plugin_version = NEXLIFYDESK_VERSION; 197 197 198 if (version_compare($current_version, '1.0. 4', '<')) {198 if (version_compare($current_version, '1.0.5', '<')) { 199 199 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', '<')) { 204 204 self::migrate_to_1_0_2(); 205 update_option('nexlifydesk_db_version', '1.0. 4');205 update_option('nexlifydesk_db_version', '1.0.5'); 206 206 } 207 207 … … 293 293 global $wpdb; 294 294 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')) { 298 298 return; 299 299 } … … 371 371 } 372 372 373 // Save updated settings if any passwords were encrypted374 373 if ($updated) { 375 374 update_option('nexlifydesk_imap_settings', $imap_settings); -
nexlifydesk/trunk/includes/class-nexlifydesk-reports.php
r3326104 r3334167 9 9 global $wpdb; 10 10 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'); 12 21 $stats = wp_cache_get($cache_key); 13 22 … … 15 24 $stats = array(); 16 25 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 17 34 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 18 35 // 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 } 22 46 23 47 // 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 } 28 59 29 60 // 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 } 34 72 35 73 // 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 } 40 85 41 86 $stats['status_breakdown'] = array(); … … 45 90 46 91 // 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 } 51 103 // phpcs:enable 52 104 … … 57 109 58 110 // 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); 60 112 61 113 // 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); 63 115 64 116 // 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); 66 118 67 119 // 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); 69 121 70 122 wp_cache_set($cache_key, $stats, 'nexlifydesk', HOUR_IN_SECONDS); … … 74 126 } 75 127 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 : ''); 80 132 $avg_response = wp_cache_get($cache_key); 81 133 82 134 if (false === $avg_response) { 83 135 // 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 } 96 165 // phpcs:enable 97 166 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'; 100 169 } else { 101 170 $avg_response = 'N/A'; … … 108 177 } 109 178 110 private static function get_monthly_ticket_data( ) {179 private static function get_monthly_ticket_data($agent_id = null) { 111 180 global $wpdb; 112 181 … … 115 184 for ($i = 29; $i >= 0; $i--) { 116 185 $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 : ''); 118 187 $count = wp_cache_get($cache_key, 'nexlifydesk'); 119 188 if (false === $count) { 120 189 // 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 } 127 205 // phpcs:enable 128 206 wp_cache_set($cache_key, $count, 'nexlifydesk', HOUR_IN_SECONDS); … … 138 216 } 139 217 140 private static function get_agent_performance( ) {218 private static function get_agent_performance($agent_id = null) { 141 219 global $wpdb; 142 220 … … 195 273 ); 196 274 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'; 198 276 199 277 $performance[] = array( … … 209 287 } 210 288 211 private static function get_recent_activity( ) {289 private static function get_recent_activity($agent_id = null) { 212 290 global $wpdb; 213 291 -
nexlifydesk/trunk/includes/class-nexlifydesk-shortcodes.php
r3333095 r3334167 16 16 ), $atts, 'nexlifydesk_ticket_form'); 17 17 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]'; 20 20 } 21 21 … … 38 38 ), $atts, 'nexlifydesk_ticket_list'); 39 39 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]'; 42 42 } 43 43 … … 84 84 $current_user = wp_get_current_user(); 85 85 86 if (current_user_can('manage_options') || current_user_can('nexlifydesk_view_all_tickets')) {86 if (current_user_can('manage_options')) { 87 87 return true; 88 88 } … … 90 90 $ticket = NexlifyDesk_Tickets::get_ticket_by_ticket_id($ticket_id_param); 91 91 92 if (!$ticket && is_numeric($ticket_id_param)) { 93 $ticket = NexlifyDesk_Tickets::get_ticket(absint($ticket_id_param)); 94 } 95 92 96 if (!$ticket) { 93 97 return false; 94 98 } 95 99 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; 97 112 } 98 113 … … 118 133 119 134 $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); 124 144 if ($page && $page->post_status === 'publish') { 125 $url = get_permalink($ ticket_form_page_id);145 $url = get_permalink($page_id); 126 146 if ($url && wp_http_validate_url($url)) { 127 147 return $url; … … 153 173 154 174 /** 155 * Documentation page shortcode check175 * Check if current page is one of the configured ticket form pages 156 176 * 157 177 * @return bool 158 178 */ 159 private static function is_ documentation_page() {179 private static function is_configured_form_page() { 160 180 global $post; 161 181 … … 164 184 } 165 185 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 212 198 * 213 199 * @return bool 214 200 */ 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 216 208 $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)); 261 216 } 262 217 } -
nexlifydesk/trunk/includes/class-nexlifydesk-tickets.php
r3333214 r3334167 381 381 } 382 382 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 } 384 423 385 424 return $reply_id; … … 606 645 global $wpdb; 607 646 608 $allowed_statuses = array('open', 'pending', 'in_progress', 'resolved', 'closed'); // <-- Add in_progress here647 $allowed_statuses = array('open', 'pending', 'in_progress', 'resolved', 'closed'); 609 648 if (!in_array($status, $allowed_statuses)) { 610 649 return false; … … 628 667 629 668 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 631 673 wp_cache_delete('nexlifydesk_ticket_' . intval($ticket_id)); 632 674 … … 647 689 wp_cache_delete('nexlifydesk_assigned_tickets_' . intval($ticket_before->assigned_to) . '_resolved'); 648 690 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 652 700 $ticket = self::get_ticket($ticket_id); 653 701 … … 658 706 }); 659 707 } 660 661 // Clear ticket grid cache when status is updated662 wp_cache_flush_group('nexlifydesk_tickets_grid');663 708 664 709 return true; … … 706 751 ); 707 752 708 // Sending to customer709 753 if ($customer_email && !in_array($customer_email, $emailed)) { 710 754 wp_mail($customer_email, $subject, $message, $headers); … … 712 756 } 713 757 714 // Sending to assigned agent if exists715 758 if (!empty($ticket->assigned_to)) { 716 759 $agent = get_userdata($ticket->assigned_to); … … 872 915 $template_content = ob_get_clean(); 873 916 874 // Fallback if template file is empty or doesn't exist875 917 if (empty($template_content)) { 876 918 $template_content = self::get_fallback_email_template($template, $ticket, $reply_id); … … 947 989 */ 948 990 private static function get_fallback_email_template($template, $ticket, $reply_id = null) { 949 // Generate a basic fallback template if template files are not available950 991 $user = get_userdata($ticket->user_id); 951 992 $customer_details = nexlifydesk_extract_customer_details($ticket->message); … … 967 1008 968 1009 /** 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 975 1012 * @param object $ticket The ticket object 976 1013 * @return array Email headers array … … 983 1020 ); 984 1021 985 // Check if SMTP plugins are active that might add their own Message-ID headers986 1022 $smtp_plugins_active = ( 987 1023 is_plugin_active('wp-mail-smtp/wp_mail_smtp.php') || … … 994 1030 ); 995 1031 996 // Only add Message-ID and threading headers if no SMTP plugin is detected997 1032 if (!$smtp_plugins_active) { 998 1033 $domain = wp_parse_url(home_url(), PHP_URL_HOST); … … 1006 1041 1007 1042 /** 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 1011 1044 * @param object $ticket The ticket object 1012 1045 * @param string $type The notification type ('new_ticket' or 'new_reply') … … 1556 1589 return $reassigned_count; 1557 1590 } 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 } 1558 1716 1559 1717 /** … … 1635 1793 1636 1794 /** 1637 * Check for duplicate tickets using improved user-scoped logic:1638 * - For registered users: Check ONLY within their own tickets, never cross-user matching1639 * - For unregistered users: Consolidate by email address only, no content matching needed1640 *1641 1795 * @param array $data Ticket data to check for duplicates 1642 1796 * @return object|false Returns existing ticket if duplicate found, false otherwise 1643 1797 */ 1644 1798 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 1646 1806 if (!function_exists('nexlifydesk_find_duplicate_ticket')) { 1647 1807 require_once dirname(__FILE__) . '/nexlifydesk-functions.php'; 1648 1808 } 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')) { 1652 1811 return false; 1653 1812 } 1813 1654 1814 return nexlifydesk_find_duplicate_ticket($data); 1655 1815 } … … 1675 1835 $where_clauses = ['1=1']; 1676 1836 $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 } 1677 1849 1678 1850 if ($args['status'] !== 'all') { … … 1701 1873 $current_user_id = get_current_user_id(); 1702 1874 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')); 1704 1883 $results = wp_cache_get($cache_key, 'nexlifydesk_tickets_grid'); 1705 1884 if ($results === false) { … … 1712 1891 a.display_name as assigned_to_display_name, 1713 1892 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, 1714 1894 CASE 1715 1895 WHEN NOT EXISTS ( … … 1718 1898 ) THEN 1 1719 1899 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' 1727 1906 ) 1728 AND r 2.is_internal_note = 01907 AND r_new.is_internal_note = 0 1729 1908 ) THEN 1 1730 1909 WHEN t.created_at > COALESCE( 1731 (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr 31732 WHERE tr 3.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), 1733 1912 '1970-01-01' 1734 1913 ) THEN 1 … … 1746 1925 $current_user_id, 1747 1926 $current_user_id, 1748 $current_user_id,1749 1927 ...$query_params 1750 1928 ) … … 1805 1983 1806 1984 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 1808 1989 wp_cache_delete('nexlifydesk_ticket_' . $ticket_id); 1809 1990 … … 1853 2034 1854 2035 if ($result !== false) { 1855 // Clear cache 2036 self::clear_all_ticket_caches(); 2037 1856 2038 wp_cache_delete('nexlifydesk_ticket_' . $ticket_id); 1857 2039 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 } 1859 2043 1860 2044 return true; … … 1865 2049 1866 2050 /** 1867 * Get tickets for real-time refresh with unread status1868 2051 * 1869 2052 * @param int $last_refresh_timestamp Unix timestamp of last refresh (unused for now) … … 1875 2058 1876 2059 /** 1877 * Mark ticket as read for a specific user1878 2060 * 1879 2061 * @param int $ticket_id Ticket ID … … 1972 2154 } 1973 2155 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 1974 2174 } -
nexlifydesk/trunk/includes/class-nexlifydesk-users.php
r3326104 r3334167 21 21 'edit_posts' => true, 22 22 'nexlifydesk_manage_tickets' => true, 23 'nexlifydesk_view_all_tickets' => true,23 'nexlifydesk_view_all_tickets' => false, 24 24 'nexlifydesk_assign_tickets' => true, 25 25 'nexlifydesk_manage_categories' => true, -
nexlifydesk/trunk/includes/helpers.php
r3333095 r3334167 100 100 } 101 101 102 if (strlen($value) < 32) { // Encrypted passwords should be at least 32 chars102 if (strlen($value) < 32) { 103 103 return false; 104 104 } … … 116 116 117 117 /** 118 * Parse email "From" header to extract name and email address119 *120 118 * @param string $from_header The full "From" header string 121 119 * @return array Array with 'name' and 'email' keys … … 181 179 182 180 /** 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 187 182 * @param string $subject The encoded email subject 188 183 * @return string The decoded subject … … 195 190 $subject = trim($subject); 196 191 197 // Check if subject contains MIME encoding198 192 if (strpos($subject, '=?') !== false && strpos($subject, '?=') !== false) { 199 // Use imap_mime_header_decode to properly decode the subject200 193 $decoded_parts = imap_mime_header_decode($subject); 201 194 … … 209 202 } 210 203 211 // If no encoding detected or decoding failed, return original subject212 204 return $subject; 213 205 } … … 387 379 $actual_message = $clean_message; 388 380 389 // Handle structured format with [Customer Details] and [Message]/[Reply] sections390 381 if (preg_match('/\[Customer Details\]\s*(.*?)\s*\[(?:Message|Reply)\]\s*(.*)/s', $clean_message, $matches)) { 391 382 $customer_section = trim($matches[1]); 392 383 $actual_message = trim($matches[2]); 393 384 394 // Extract name and email from customer details section395 385 if (preg_match('/Name:\s*([^\n\r]+)/i', $customer_section, $name_matches)) { 396 386 $name = trim($name_matches[1]); … … 400 390 } 401 391 } else { 402 // Fallback to original logic for other formats403 392 if (preg_match('/From:\s*([^<\n]+)\s*<([^>]+)>/', $clean_message, $matches)) { 404 393 $name = trim($matches[1]); … … 413 402 } 414 403 415 // Generate name from email if needed416 404 if (empty($name) && !empty($email)) { 417 405 $email_parts = explode('@', $email); … … 425 413 'name' => $name, 426 414 'email' => $email, 427 'message' => $actual_message // Now returns clean message without customer details415 'message' => $actual_message 428 416 ]; 429 417 } … … 663 651 664 652 /** 665 * Handles Quoted-Printable, Base64, and other common email encodings666 *667 653 * @param string $content The email content to decode 668 654 * @return string The decoded content -
nexlifydesk/trunk/includes/nexlifydesk-functions.php
r3333214 r3334167 154 154 $order_numbers = array(); 155 155 if (empty($text)) return $order_numbers; 156 // Match patterns like Order #1234, Order number 1234, #1234, 1234157 156 $patterns = array( 158 '/order\s*(number)?\s*#?\s*(\d{4,})/i', // Order #1234, Order number 1234159 '/#(\d{4,})/', // #1234160 '/\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/' 161 160 ); 162 161 foreach ($patterns as $pattern) { … … 196 195 197 196 /** 198 * Global duplicate ticket detection for all channels199 * Implements correct logic per user requirements:200 * - Registered users: Check ONLY within their own tickets, never cross-user matching201 * - Unregistered users: Consolidate by email address only, no content matching needed202 197 * 203 198 * @param array $ticket_data (user_id, email, subject, message, source) … … 212 207 $table_name = $wpdb->prefix . 'nexlifydesk_tickets'; 213 208 214 // ========================215 209 // A. REGISTERED USER LOGIC 216 // ========================217 210 if ($user_id > 0) { 218 // Step 1: Check if this registered user has ANY existing ticket (open, pending, in_progress)219 211 $cache_key = 'nexlifydesk_user_has_tickets_' . $user_id; 220 212 $user_has_tickets = wp_cache_get($cache_key); … … 231 223 } 232 224 233 // Step 2: If user has no existing tickets, create new ticket (no duplicate possible)234 225 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 239 229 $order_numbers = array(); 240 230 if (!empty($subject)) { … … 246 236 $order_numbers = array_unique($order_numbers); 247 237 248 // Check for order number matches within user's own tickets249 238 if (!empty($order_numbers)) { 250 239 $order_regex = implode('|', array_map('preg_quote', $order_numbers)); … … 264 253 } 265 254 266 // Check for exact subject match within user's own tickets267 255 if (!empty($subject)) { 268 256 $cache_key = 'nexlifydesk_user_subject_match_' . md5($user_id . $subject); … … 281 269 } 282 270 283 // Semantic similarity check within user's own tickets only284 271 if (class_exists('TextAnalysis\\Comparisons\\CosineSimilarityComparison') && class_exists('TextAnalysis\\Tokenizers\\GeneralTokenizer')) { 285 272 $similarity_threshold = 0.12; // Optimized threshold for better matching … … 298 285 299 286 if (!empty($user_tickets)) { 300 // Manual keyword mapping for semantic matching301 287 $keyword_map = array( 302 288 'profile' => 'account', 'dashboard' => 'account', 'user' => 'account', 'panel' => 'account', … … 333 319 ); 334 320 if ($similarity >= $similarity_threshold) { 335 return $ticket; // Found duplicate within user's own tickets321 return $ticket; 336 322 } 337 323 } … … 339 325 } 340 326 341 // No duplicate found within user's tickets = create new ticket for this user342 327 return null; 343 328 } 344 329 345 // ==========================346 330 // B. UNREGISTERED USER LOGIC 347 // ==========================348 331 if ($user_id == 0 && !empty($email)) { 349 // For unregistered users, we simply check if there's ANY existing ticket for this email350 // Content matching is NOT required - we want to consolidate all communication by email351 332 $cache_key = 'nexlifydesk_email_consolidation_' . md5($email); 352 333 $existing_ticket = wp_cache_get($cache_key); 353 334 354 335 if (false === $existing_ticket) { 355 // Find the most recent ticket for this email address (any status except closed)356 336 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe. 357 337 $existing_ticket = $wpdb->get_row( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query. … … 366 346 } 367 347 368 return $existing_ticket; // Return existing ticket for email consolidation, or null to create new348 return $existing_ticket; 369 349 } 370 350 371 // No valid user_id or email = create new ticket372 351 return null; 373 352 } -
nexlifydesk/trunk/includes/textanalysis/SECURITY.md
r3333095 r3334167 6 6 - `Documents/` - Data structures 7 7 - `Interfaces/` - Class contracts 8 - `Corpus/` - Text corpus management 8 9 9 10 ## 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 12 14 13 15 ## Security Status 14 16 ✅ No file system operations 15 ✅ No remote downloads 17 ✅ No remote downloads 16 18 ✅ No external dependencies 19 ✅ No command line interfaces 17 20 ✅ Pure computational library 21 ✅ Clean directory structure 18 22 19 23 This subset provides semantic similarity analysis without security vulnerabilities. 24 All potentially dangerous components have been properly removed. -
nexlifydesk/trunk/nexlifydesk.php
r3333214 r3334167 3 3 * Plugin Name: NexlifyDesk 4 4 * Description: A modern, user-friendly support ticket system for WordPress with ticket submission, threaded replies, file attachments, agent assignment, and customizable. 5 * Version: 1.0. 45 * Version: 1.0.5 6 6 * Author URI: https://nexlifylabs.com 7 7 * Supported Versions: 6.2+ … … 18 18 19 19 if ( ! function_exists( 'nexlifydesk' ) ) { 20 // Create a helper function for easy SDK access.21 20 function nexlifydesk() { 22 21 global $nexlifydesk; 23 22 24 23 if ( ! isset( $nexlifydesk ) ) { 25 // Include Freemius SDK.26 24 require_once dirname( __FILE__ ) . '/vendor/freemius/start.php'; 27 25 $nexlifydesk = fs_dynamic_init( array( … … 44 42 } 45 43 46 // Init Freemius.47 44 nexlifydesk(); 48 // Signal that SDK was initiated.45 49 46 do_action( 'nexlifydesk_loaded' ); 50 47 51 // Register Freemius uninstall handler52 48 nexlifydesk()->add_action('after_uninstall', 'nexlifydesk_freemius_uninstall_cleanup'); 53 49 } … … 55 51 define('NEXLIFYDESK_PLUGIN_DIR', plugin_dir_path(__FILE__)); 56 52 define('NEXLIFYDESK_PLUGIN_URL', plugin_dir_url(__FILE__)); 57 define('NEXLIFYDESK_VERSION', '1.0. 4');53 define('NEXLIFYDESK_VERSION', '1.0.5'); 58 54 define('NEXLIFYDESK_TABLE_PREFIX', 'nexlifydesk_'); 59 55 define('NEXLIFYDESK_CAP_VIEW_ALL_TICKETS', 'nexlifydesk_view_all_tickets'); … … 94 90 } 95 91 add_action('plugins_loaded', 'nexlifydesk_init'); 96 97 // Add plugin action links (donate button, etc.)98 92 add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'nexlifydesk_plugin_action_links'); 99 93 add_filter('plugin_row_meta', 'nexlifydesk_plugin_row_meta', 10, 2); … … 117 111 } 118 112 119 // Add admin notice and action to update email templates for existing installations120 113 add_action('admin_notices', 'nexlifydesk_show_template_update_notice'); 121 114 add_action('wp_ajax_nexlifydesk_update_email_templates', 'nexlifydesk_ajax_update_email_templates'); … … 123 116 124 117 function nexlifydesk_show_template_update_notice() { 125 // Only show to admins on NexlifyDesk pages126 118 if (!current_user_can('manage_options')) { 127 119 return; 128 120 } 129 121 130 // Check if notice was dismissed131 122 if (get_option('nexlifydesk_template_notice_dismissed', false)) { 132 123 return; … … 138 129 } 139 130 140 // Check if templates need updating (contain old placeholder format)141 131 $existing_templates = get_option('nexlifydesk_email_templates', array()); 142 132 $needs_update = false; … … 181 171 } 182 172 183 // Clear existing templates to force use of template files184 173 $templates = array( 185 174 'new_ticket' => '', … … 203 192 } 204 193 205 // Set the dismissal flag206 194 update_option('nexlifydesk_template_notice_dismissed', true); 207 195 … … 231 219 */ 232 220 function nexlifydesk_load_default_email_templates() { 233 // Only load if no templates exist yet (fresh installation)234 221 $existing_templates = get_option('nexlifydesk_email_templates', array()); 235 222 236 // Check if templates are empty or contain old placeholder-style templates237 223 $needs_update = empty($existing_templates) || 238 224 (isset($existing_templates['new_reply']) && … … 240 226 241 227 if ($needs_update) { 242 // Set placeholders indicating that template files are being used243 228 $templates = array( 244 229 'new_ticket' => '', … … 434 419 435 420 /** 436 * Freemius uninstall cleanup handler437 421 * This is called by Freemius when the plugin is uninstalled through their system 438 422 */ 439 423 function nexlifydesk_freemius_uninstall_cleanup() { 440 // Include the uninstall script to handle cleanup441 424 $uninstall_file = plugin_dir_path(__FILE__) . 'uninstall.php'; 442 425 if (file_exists($uninstall_file)) { 443 // Define the constant that uninstall.php expects444 426 if (!defined('WP_UNINSTALL_PLUGIN')) { 445 427 define('WP_UNINSTALL_PLUGIN', true); … … 449 431 } 450 432 451 // Register the uninstall hook to use the uninstall.php file452 433 register_uninstall_hook(__FILE__, 'nexlifydesk_freemius_uninstall_cleanup'); 453 434 -
nexlifydesk/trunk/readme.txt
r3333214 r3334167 4 4 Requires at least: 6.2 5 5 Tested up to: 6.8 6 Stable tag: 1.0. 46 Stable tag: 1.0.5 7 7 License: GPLv2 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 349 349 == Changelog == 350 350 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 351 377 = 1.0.4 = 352 378 - Enhanced email provider validation to prevent false success messages when credentials are not configured … … 402 428 == Upgrade Notice == 403 429 430 = 1.0.5 = 431 CRITICAL 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 404 433 = 1.0.4 = 405 434 Important 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 37 37 $auth_url = wp_nonce_url(admin_url('admin.php?action=nexlifydesk_google_auth_init'), 'google-auth-init'); 38 38 39 // SSL detection - AWS will only work with HTTPS39 // SSL detection 40 40 $is_ssl_enabled = function_exists('nexlifydesk_check_ssl_enabled') ? nexlifydesk_check_ssl_enabled() : is_ssl(); 41 41 -
nexlifydesk/trunk/templates/admin/settings.php
r3333095 r3334167 14 14 'default_category' => 1, 15 15 '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(), 18 18 'ticket_id_prefix' => 'T', 19 19 'ticket_id_start' => 1001, … … 33 33 if (in_array($key, array('default_priority', 'allowed_file_types', 'ticket_id_prefix',), true)) { 34 34 $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)) { 36 36 $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 } 37 43 } elseif (in_array($key, array('check_duplicates', 'keep_data_on_uninstall'), true)) { 38 44 $settings[$key] = $value ? 1 : 0; … … 99 105 </tr> 100 106 <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;"> 105 110 <?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 ?> 106 117 <?php foreach ($pages as $page): ?> 107 118 <?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' : ''; ?>> 109 120 <?php echo esc_html(wp_strip_all_tags($page->post_title)); ?> 110 121 </option> … … 115 126 <?php endif; ?> 116 127 </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;"> 125 138 <?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 ?> 126 145 <?php foreach ($pages as $page): ?> 127 146 <?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' : ''; ?>> 129 148 <?php echo esc_html(wp_strip_all_tags($page->post_title)); ?> 130 149 </option> … … 135 154 <?php endif; ?> 136 155 </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> 138 160 </td> 139 161 </tr> … … 308 330 }); 309 331 310 // Show warning if already unchecked on page load311 332 if (!$('#keep_data_on_uninstall').is(':checked')) { 312 333 $('#data-deletion-warning').show(); -
nexlifydesk/trunk/templates/admin/ticket-single.php
r3333095 r3334167 15 15 $user_avatar_url = $user ? get_avatar_url($user->ID) : get_avatar_url(0); 16 16 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 19 18 $customer_details = function_exists('nexlifydesk_extract_customer_details') ? 20 19 nexlifydesk_extract_customer_details($ticket->message) : … … 23 22 $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest'); 24 23 $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A'); 25 // Use clean message without embedded customer details for admin panel display26 24 $display_message = $customer_details['message']; 27 25 ?> … … 154 152 155 153 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 158 155 $reply_customer_details = function_exists('nexlifydesk_extract_customer_details') ? 159 156 nexlifydesk_extract_customer_details($reply->message) : 160 157 ['name' => '', 'email' => '', 'message' => $reply->message]; 161 158 $reply_customer_name = $reply_customer_details['name'] ?: 'Guest'; 162 // Use clean message without embedded customer details for admin panel display163 159 $reply_display_message = $reply_customer_details['message']; 164 160 } else { -
nexlifydesk/trunk/templates/admin/tickets-list.php
r3333095 r3334167 100 100 <div class="header-priority"><?php esc_html_e('Priority', 'nexlifydesk'); ?></div> 101 101 <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> 103 104 </div> 104 105 … … 126 127 $is_unread = isset($ticket->is_unread) ? $ticket->is_unread : false; 127 128 $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; 128 130 ?> 129 131 <div class="ticket-row <?php echo $is_unread ? 'unread' : ''; ?>" … … 168 170 <?php endif; ?> 169 171 </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"> 171 177 <span class="date-time"><?php echo esc_html(date_i18n('M j, Y', strtotime($last_reply_time))); ?></span> 172 178 <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 103 103 <p class="file-info"> 104 104 <?php 105 // Get server limits for display106 105 $server_max_size = min( 107 106 wp_max_upload_size(), … … 111 110 $plugin_max_mb = $max_file_size; 112 111 113 // Use the smaller of plugin setting or server limit114 112 $effective_max_mb = min($plugin_max_mb, $server_max_mb); 115 113 … … 121 119 ); 122 120 123 // Show server limit warning if it's lower than plugin setting124 121 if ($server_max_mb < $plugin_max_mb) { 125 122 echo '<br><small style="color: #d63638;">'; … … 170 167 $ticket_number = ''; 171 168 $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'])) : ''; 181 178 } 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 182 187 if ($ticket_submitted === 1) : 183 188 ?> … … 195 200 </div> 196 201 <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 ?> 201 250 </div> 202 251 </div> -
nexlifydesk/trunk/templates/frontend/ticket-single.php
r3333095 r3334167 44 44 45 45 $current_user = wp_get_current_user(); 46 // SECURITY FIX: Frontend access should ONLY be for ticket owners47 // Agents and administrators should ONLY access tickets from admin panel48 46 $is_ticket_owner = ($ticket && (int)$ticket->user_id === (int)$current_user->ID); 49 47 50 // Handle unregistered user tickets (user_id = 0) by checking customer_email51 48 if (!$is_ticket_owner && $ticket && (int)$ticket->user_id === 0) { 52 49 $customer_email = get_post_meta($ticket->id, 'customer_email', true); -
nexlifydesk/trunk/uninstall.php
r3333214 r3334167 23 23 global $wpdb; 24 24 25 // Get settings to check if data should be kept26 25 $settings = get_option('nexlifydesk_settings', array()); 27 26 $keep_data = isset($settings['keep_data_on_uninstall']) ? (bool)$settings['keep_data_on_uninstall'] : true; 28 27 29 // Always remove scheduled hooks and transients (cleanup)30 28 wp_clear_scheduled_hook('nexlifydesk_sla_check'); 31 29 wp_clear_scheduled_hook('nexlifydesk_auto_close_tickets'); … … 35 33 delete_transient('nexlifydesk_google_oauth_state'); 36 34 37 // Clear any cached data38 35 wp_cache_flush(); 39 36 40 // If user wants to keep data, only do minimal cleanup41 37 if ($keep_data) { 42 // Remove only version option but keep all user data43 38 delete_option('nexlifydesk_db_version'); 44 39 return; 45 40 } 46 41 47 // User wants complete data removal - proceed with full cleanup48 49 // Remove all plugin options and settings50 42 $options_to_remove = array( 51 43 'nexlifydesk_settings', … … 61 53 foreach ($options_to_remove as $option) { 62 54 delete_option($option); 63 // Also remove from network options if multisite64 55 if (is_multisite()) { 65 56 delete_network_option(null, $option); … … 67 58 } 68 59 69 // Remove custom database tables70 60 $table_names = array( 71 61 $wpdb->prefix . 'nexlifydesk_tickets', … … 80 70 } 81 71 82 // Remove custom user roles83 72 remove_role('nexlifydesk_agent'); 84 73 remove_role('nexlifydesk_supervisor'); 85 74 86 // Remove custom capabilities from administrator role87 75 $admin_role = get_role('administrator'); 88 76 if ($admin_role) { … … 100 88 } 101 89 102 // Remove uploaded files directory103 90 $upload_dir = wp_upload_dir(); 104 91 $plugin_upload_dir = trailingslashit($upload_dir['basedir']) . 'nexlifydesk/'; … … 108 95 } 109 96 110 // Remove any user meta related to the plugin111 97 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct deletion is required for uninstall cleanup 112 98 $wpdb->query($wpdb->prepare( … … 114 100 'nexlifydesk_%' 115 101 )); 116 // Optionally clear usermeta cache after deletion117 102 wp_cache_flush(); 118 103 119 // Clean up any remaining cache entries and transients120 104 $cache_keys_patterns = array( 121 105 'nexlifydesk_ticket_', … … 128 112 ); 129 113 130 // Attempt to clean up cache entries (this is a best-effort cleanup)131 114 global $wp_object_cache; 132 115 if (isset($wp_object_cache->cache)) { … … 170 153 nexlifydesk_recursive_delete_directory($path); 171 154 } else { 172 // Use WordPress file deletion function for security173 155 wp_delete_file($path); 174 156 } 175 157 } 176 158 177 // Use WP_Filesystem for directory removal178 159 global $wp_filesystem; 179 160 if (empty($wp_filesystem)) { … … 184 165 } 185 166 186 / / Execute the uninstall process167 /** Execute the uninstall process */ 187 168 nexlifydesk_execute_uninstall();
Note: See TracChangeset
for help on using the changeset viewer.