Changeset 3469978
- Timestamp:
- 02/26/2026 07:56:02 AM (5 weeks ago)
- Location:
- divewp-boost-site-performance/trunk
- Files:
-
- 15 edited
-
README.txt (modified) (3 diffs)
-
assets/css/features/cron-jobs.css (modified) (2 diffs)
-
assets/css/features/user-events.css (modified) (2 diffs)
-
assets/js/divewp-admin.js (modified) (1 diff)
-
assets/js/divewp-cron-jobs.js (modified) (6 diffs)
-
assets/js/divewp-plugins-management.js (modified) (1 diff)
-
divewp.php (modified) (4 diffs)
-
includes/admin/templates/admin-left-sidebar.php (modified) (2 diffs)
-
includes/class-divewp-database.php (modified) (2 diffs)
-
includes/class-divewp-db-access.php (modified) (4 diffs)
-
includes/class-divewp-main.php (modified) (1 diff)
-
includes/features/cron-jobs/ajax-handlers.php (modified) (4 diffs)
-
includes/features/cron-jobs/class-cron-jobs.php (modified) (1 diff)
-
includes/features/user-events/class-event-logger.php (modified) (9 diffs)
-
includes/features/user-events/class-user-events.php (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
divewp-boost-site-performance/trunk/README.txt
r3467419 r3469978 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.2 7 Stable tag: 2.3. 17 Stable tag: 2.3.3 8 8 License: GPLv2 or later 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 269 269 == Changelog == 270 270 271 = 2.3.0 = 271 272 = 2.3.2 = 273 * **FIXED**: Plugin Check – nonce verification for theme/plugin file edit logging 274 * **FIXED**: Plugin Check – PreparedSQL and DirectQuery phpcs ignores for DB/migration code 275 * **FIXED**: Cron execution log – RUNS column now shows correct per-hook totals (SQL GROUP BY) 276 * **FIXED**: Hook history modal – Total Runs uses authoritative backend count, not capped at 50 277 * **FIXED**: Hook filter – exact match instead of partial LIKE for accurate counts 278 * **FIXED**: Clear logs – result modal shows success count or "No logs older than X days" 279 * **FIXED**: Clear older button – prompts to select time period when none selected 280 * **FIXED**: Result modal close – uses top-right X only (consistent with other cron modals) 281 * **FIXED**: PHPCS – PreparedSQL.InterpolatedNotPrepared warnings in DB access layer 282 * **IMPROVED**: User Events – structured context (WHO/WHERE/VIA-WHAT/WHEN) 283 * Added: Additive schema migration for user_events (actor_snapshot, channel, source_context, target_context, request_metadata) 284 * Added: Event details modal – click any event row to see Who, Via, When, Details, and technical context 285 * Added: Structured logging – who initiated, where (screen/route), via what (admin UI, AJAX, REST app password) 286 * Fixed: Timeline "Today" label – now correctly compares with site timezone instead of always showing "Today" for first group 287 * Fixed: Removed inline styles from User Events markup (moved to user-events.css) 288 * Compatibility: Existing events remain visible; new fields are optional with fallbacks. No destructive changes. 289 290 = 2.3.1 = 272 291 * **NEW**: Plugins Management feature 273 292 * Added: Plugins Management dashboard – list all installed plugins with status pills (Active, Inactive, Update Available, Up to date) … … 352 371 == Upgrade Notice == 353 372 373 = 2.3.3 = 374 User Events upgrade: structured context (Who/Where/Via/When), event details modal, and timeline fixes. Additive DB migration—existing data preserved. Recommended for all users. 375 376 = 2.3.2 = 377 Bug fixes for Cron execution log (correct RUNS counts, clear logs feedback, PHPCS compliance). Recommended for all users. 378 354 379 = 2.3.0 = 355 380 New Plugins Management feature: view and manage all installed plugins, see update status, and use the Abilities API (divewp/plugins-management) for AI-assisted plugin management. Recommended for all users. -
divewp-boost-site-performance/trunk/assets/css/features/cron-jobs.css
r3467366 r3469978 943 943 } 944 944 945 .divewp-modal-status--success { 946 background: #dcfce7; 947 color: #166534; 948 } 949 950 .divewp-modal-status--danger { 951 background: #fee2e2; 952 color: #b91c1c; 953 } 954 955 /* Result modal: notice with status variant */ 956 .divewp-modal-result.divewp-modal-status--success { 957 background: #dcfce7; 958 border-color: #bbf7d0; 959 color: #166534; 960 } 961 962 .divewp-modal-result.divewp-modal-status--success .dashicons { 963 color: #16a34a; 964 } 965 966 .divewp-modal-result.divewp-modal-status--danger { 967 background: #fee2e2; 968 border-color: #fecaca; 969 color: #b91c1c; 970 } 971 972 .divewp-modal-result.divewp-modal-status--danger .dashicons { 973 color: #dc2626; 974 } 975 945 976 /* Hook name - prominent display */ 946 977 .divewp-modal-hook { … … 1622 1653 height: 14px; 1623 1654 opacity: 0.6; 1655 } 1656 1657 /* ========================================================================== 1658 Execution Log Retention & Clear Controls 1659 ========================================================================== */ 1660 1661 .divewp-cron-log-execution-wrap { 1662 padding: 16px; 1663 } 1664 1665 .divewp-cron-log-retention-notice { 1666 display: flex; 1667 align-items: center; 1668 gap: 8px; 1669 padding: 10px 14px; 1670 background: #e0f2fe; 1671 border: 1px solid #bae6fd; 1672 border-radius: 8px; 1673 color: #075985; 1674 font-size: 13px; 1675 margin-bottom: 12px; 1676 } 1677 1678 .divewp-cron-log-retention-notice .dashicons { 1679 font-size: 18px; 1680 width: 18px; 1681 height: 18px; 1682 flex-shrink: 0; 1683 } 1684 1685 .divewp-cron-log-toolbar { 1686 display: flex; 1687 align-items: center; 1688 gap: 8px; 1689 margin-bottom: 16px; 1690 flex-wrap: wrap; 1691 } 1692 1693 .divewp-cron-log-toolbar .divewp-cron-clear-select { 1694 padding: 6px 12px; 1695 border: 1px solid #dcdcde; 1696 border-radius: 4px; 1697 font-size: 13px; 1698 background: #fff; 1699 min-width: 180px; 1700 } 1701 1702 .divewp-cron-log-toolbar .divewp-cron-clear-apply, 1703 .divewp-cron-log-toolbar .divewp-cron-clear-all { 1704 font-size: 12px; 1705 padding: 6px 12px; 1706 height: auto; 1707 } 1708 1709 .divewp-cron-log-toolbar .divewp-cron-clear-all { 1710 color: #d63638; 1711 border-color: #d63638; 1712 } 1713 1714 .divewp-cron-log-toolbar .divewp-cron-clear-all:hover { 1715 background: #d63638; 1716 color: #fff; 1717 border-color: #d63638; 1624 1718 } 1625 1719 -
divewp-boost-site-performance/trunk/assets/css/features/user-events.css
r3278673 r3469978 29 29 display: flex; 30 30 gap: 10px; 31 } 32 33 #divewp-refresh-logs { 34 margin-right: 10px; 31 35 } 32 36 … … 240 244 } 241 245 246 /* Event details modal (centered, matches Cron Jobs / Plugins Management) */ 247 .divewp-event-drawer { 248 position: fixed; 249 top: 0; 250 left: 0; 251 width: 100vw; 252 height: 100vh; 253 background: rgba(0, 0, 0, 0.5); 254 z-index: 100000; 255 display: flex; 256 justify-content: center; 257 align-items: center; 258 visibility: hidden; 259 opacity: 0; 260 transition: visibility 0s 0.3s, opacity 0.3s; 261 } 262 263 .divewp-event-drawer.open { 264 visibility: visible; 265 opacity: 1; 266 transition: visibility 0s, opacity 0.3s; 267 } 268 269 .divewp-event-drawer__overlay { 270 position: absolute; 271 top: 0; 272 right: 0; 273 bottom: 0; 274 left: 0; 275 cursor: pointer; 276 } 277 278 .divewp-event-drawer__panel { 279 position: relative; 280 background: #fff; 281 border: 2px solid #e5e7eb; 282 border-radius: 16px; 283 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25); 284 overflow: hidden; 285 max-width: 600px; 286 width: 92%; 287 max-height: 85vh; 288 display: flex; 289 flex-direction: column; 290 animation: divewp-event-modal-fadein 0.3s ease-out; 291 } 292 293 @keyframes divewp-event-modal-fadein { 294 from { 295 opacity: 0; 296 transform: scale(0.95); 297 } 298 to { 299 opacity: 1; 300 transform: scale(1); 301 } 302 } 303 304 .divewp-event-drawer__header { 305 display: flex; 306 align-items: center; 307 justify-content: space-between; 308 padding: 16px 20px; 309 background: #f8f9fa; 310 border-bottom: 1px solid #e1e5e9; 311 } 312 313 .divewp-event-drawer__title { 314 margin: 0; 315 font-family: 'Literata', serif; 316 font-size: 18px; 317 font-weight: 600; 318 color: #1e293b; 319 } 320 321 .divewp-event-drawer__close { 322 display: flex; 323 align-items: center; 324 justify-content: center; 325 width: 32px; 326 height: 32px; 327 padding: 0; 328 background: #fff; 329 border: 1px solid #dcdcde; 330 border-radius: 6px; 331 cursor: pointer; 332 color: #646970; 333 transition: all 0.2s; 334 } 335 336 .divewp-event-drawer__close:hover { 337 background: #f0f0f1; 338 color: #d63638; 339 border-color: #d63638; 340 } 341 342 .divewp-event-drawer__content { 343 flex: 1; 344 overflow-y: auto; 345 padding: 20px; 346 } 347 348 /* Event type / action header row inside modal */ 349 .divewp-event-type-row { 350 display: flex; 351 align-items: center; 352 justify-content: space-between; 353 padding-bottom: 14px; 354 border-bottom: 1px solid #e2e8f0; 355 margin-bottom: 4px; 356 } 357 358 .divewp-event-type-row__label { 359 font-size: 14px; 360 font-weight: 600; 361 color: #1e293b; 362 } 363 364 .divewp-event-row--clickable { 365 cursor: pointer; 366 } 367 368 .divewp-event-row--clickable:hover { 369 background: #f6f7f7; 370 } 371 242 372 #divewp-refresh-email-logs:hover { 243 373 background-color: #3182ce; -
divewp-boost-site-performance/trunk/assets/js/divewp-admin.js
r3448398 r3469978 520 520 }); 521 521 522 // User Events: open details drawer on row click 523 $(document).on('click', '.divewp-event-row--clickable', function (e) { 524 var eventId = $(this).data('event-id'); 525 if (!eventId) return; 526 527 var $drawer = $('.divewp-event-drawer'); 528 var $content = $drawer.find('.divewp-event-drawer__content'); 529 $content.html('<p>' + (divewpData.loading || 'Loading...') + '</p>'); 530 $drawer.addClass('open').attr('aria-hidden', 'false'); 531 532 $.ajax({ 533 url: divewpData.ajaxurl, 534 type: 'POST', 535 data: { 536 action: 'divewp_get_event_details', 537 nonce: divewpData.nonce, 538 event_id: eventId 539 }, 540 success: function (response) { 541 if (response.success) { 542 var d = response.data; 543 function esc(s) { 544 if (s == null || s === '') return '-'; 545 return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 546 } 547 var who = (d.actor && (d.actor.user_login || d.actor.user_email)) ? (d.actor.user_login || d.actor.user_email) : '-'; 548 var desc = (d.event && d.event.description) ? d.event.description : '-'; 549 var actionToClass = { created: 'success', creation: 'success', login: 'success', activated: 'success', installed: 'success', restored: 'success', approved: 'success', updated: 'info', update: 'info', logout: 'info', edited: 'info', customized: 'info', authenticated: 'info', core_updated: 'info', role_changed: 'info', trashed: 'danger', unpublished: 'warning', deactivated: 'warning', deletion: 'danger', deleted: 'danger', deleted_permanently: 'danger', failed_login: 'warning', file_edited: 'warning', password_reset: 'warning' }; 550 var statusMap = { success: 'success', danger: 'danger', warning: 'warning', info: 'info' }; 551 var action = (d.event && d.event.event_action) ? d.event.event_action : ''; 552 var status = (d.event && d.event.status) ? d.event.status : ''; 553 var pillClass = (action && actionToClass[action]) ? 'status-pill-' + actionToClass[action] : ((status && statusMap[status]) ? 'status-pill-' + statusMap[status] : 'status-pill-info'); 554 555 var html = '<div class="divewp-modal-details">'; 556 html += '<div class="divewp-event-type-row">'; 557 html += '<span class="divewp-event-type-row__label">' + esc(d.event_type_label) + '</span>'; 558 html += '<span class="status-pill ' + pillClass + '">' + esc(d.action_label) + '</span>'; 559 html += '</div>'; 560 html += '<div class="divewp-modal-grid">'; 561 html += '<div class="divewp-modal-item">'; 562 html += '<div class="divewp-modal-item__icon"><span class="dashicons dashicons-admin-users"></span></div>'; 563 html += '<div class="divewp-modal-item__content">'; 564 html += '<span class="divewp-modal-item__label">' + (divewpData.eventDetailsWho || 'Who') + '</span>'; 565 html += '<span class="divewp-modal-item__value">' + esc(who) + '</span>'; 566 html += '</div></div>'; 567 html += '<div class="divewp-modal-item">'; 568 html += '<div class="divewp-modal-item__icon"><span class="dashicons dashicons-admin-site"></span></div>'; 569 html += '<div class="divewp-modal-item__content">'; 570 html += '<span class="divewp-modal-item__label">' + (divewpData.eventDetailsVia || 'Via') + '</span>'; 571 html += '<span class="divewp-modal-item__value">' + esc(d.channel) + '</span>'; 572 html += '</div></div>'; 573 html += '<div class="divewp-modal-item">'; 574 html += '<div class="divewp-modal-item__icon"><span class="dashicons dashicons-calendar-alt"></span></div>'; 575 html += '<div class="divewp-modal-item__content">'; 576 html += '<span class="divewp-modal-item__label">' + (divewpData.eventDetailsWhen || 'When') + '</span>'; 577 html += '<span class="divewp-modal-item__value">' + esc(d.created_at_local) + '</span>'; 578 html += '</div></div>'; 579 html += '<div class="divewp-modal-item">'; 580 html += '<div class="divewp-modal-item__icon"><span class="dashicons dashicons-media-text"></span></div>'; 581 html += '<div class="divewp-modal-item__content">'; 582 html += '<span class="divewp-modal-item__label">' + (divewpData.eventDetailsWhat || 'Details') + '</span>'; 583 html += '<span class="divewp-modal-item__value">' + esc(desc) + '</span>'; 584 html += '</div></div>'; 585 html += '</div>'; 586 if (d.source_context && Object.keys(d.source_context).length) { 587 html += '<div class="divewp-modal-args">'; 588 html += '<div class="divewp-modal-args__header">'; 589 html += '<span class="dashicons dashicons-location"></span>'; 590 html += '<span>' + (divewpData.eventDetailsWhere || 'Context') + '</span>'; 591 html += '</div>'; 592 html += '<pre class="divewp-modal-args__code">' + esc(JSON.stringify(d.source_context, null, 2)) + '</pre>'; 593 html += '</div>'; 594 } 595 if (d.target_context && Object.keys(d.target_context).length) { 596 html += '<div class="divewp-modal-args">'; 597 html += '<div class="divewp-modal-args__header">'; 598 html += '<span class="dashicons dashicons-editor-code"></span>'; 599 html += '<span>' + (divewpData.eventDetailsTarget || 'Target') + '</span>'; 600 html += '</div>'; 601 html += '<pre class="divewp-modal-args__code">' + esc(JSON.stringify(d.target_context, null, 2)) + '</pre>'; 602 html += '</div>'; 603 } 604 html += '</div>'; 605 $content.html(html); 606 } else { 607 $content.html('<p>' + (response.data && response.data.message ? response.data.message : 'Failed to load event details.') + '</p>'); 608 } 609 }, 610 error: function () { 611 $content.html('<p>' + (divewpData.error || 'Failed to load event details.') + '</p>'); 612 } 613 }); 614 }); 615 616 $(document).on('click', '.divewp-event-drawer__close, .divewp-event-drawer__overlay', function (e) { 617 e.preventDefault(); 618 $('.divewp-event-drawer').removeClass('open').attr('aria-hidden', 'true'); 619 }); 620 621 $(document).on('keydown', function (e) { 622 if (e.key === 'Escape' && $('.divewp-event-drawer').hasClass('open')) { 623 $('.divewp-event-drawer').removeClass('open').attr('aria-hidden', 'true'); 624 } 625 }); 626 522 627 // Handle new feature highlights 523 628 (function () { -
divewp-boost-site-performance/trunk/assets/js/divewp-cron-jobs.js
r3448398 r3469978 181 181 self.loadMoreWpCronEvents(); 182 182 } 183 }); 184 185 // Execution log clear controls 186 $(document).on('click', '.divewp-cron-clear-apply', function(e) { 187 e.preventDefault(); 188 self.handleClearLogsOlderThan(); 189 }); 190 $(document).on('click', '.divewp-cron-clear-all', function(e) { 191 e.preventDefault(); 192 self.handleClearAllLogs(); 183 193 }); 184 194 }, … … 647 657 var self = this; 648 658 var $panel = $('.divewp-cron-panel[data-panel="execution-log"]'); 659 var retentionDays = (divewpCronData && divewpCronData.retention_days) ? divewpCronData.retention_days : 30; 660 var retentionText = (this.config.strings.retentionNotice || 'Logs are auto-cleaned daily. Retention: %d days.').replace('%d', retentionDays); 649 661 650 662 if (!hooks || hooks.length === 0) { 651 $panel.html('<div class="divewp-cron-empty"><span class="dashicons dashicons-media-text"></span><p>' + this.config.strings.noLogs + '</p></div>'); 652 return; 653 } 654 655 var html = '<div class="divewp-cron-log-grouped">'; 663 var emptyHtml = '<div class="divewp-cron-log-execution-wrap">'; 664 emptyHtml += '<div class="divewp-cron-log-retention-notice">'; 665 emptyHtml += '<span class="dashicons dashicons-info"></span>'; 666 emptyHtml += '<span>' + this.escapeHtml(retentionText) + '</span>'; 667 emptyHtml += '</div>'; 668 emptyHtml += '<div class="divewp-cron-log-toolbar">'; 669 emptyHtml += '<select class="divewp-cron-clear-select">'; 670 emptyHtml += '<option value="">' + this.escapeHtml(this.config.strings.clearLogsOlderThan || 'Clear logs older than...') + '</option>'; 671 emptyHtml += '<option value="7">7 days</option>'; 672 emptyHtml += '<option value="30">30 days</option>'; 673 emptyHtml += '<option value="90">90 days</option>'; 674 emptyHtml += '</select>'; 675 emptyHtml += '<button type="button" class="button divewp-cron-clear-apply">' + this.escapeHtml(this.config.strings.clearOlder || 'Clear older') + '</button>'; 676 emptyHtml += '<button type="button" class="button divewp-cron-clear-all">' + this.escapeHtml(this.config.strings.clearAllLogs || 'Clear All Logs') + '</button>'; 677 emptyHtml += '</div>'; 678 emptyHtml += '<div class="divewp-cron-empty"><span class="dashicons dashicons-media-text"></span><p>' + this.config.strings.noLogs + '</p></div>'; 679 emptyHtml += '</div>'; 680 $panel.html(emptyHtml); 681 return; 682 } 683 684 var html = '<div class="divewp-cron-log-execution-wrap">'; 685 html += '<div class="divewp-cron-log-retention-notice">'; 686 html += '<span class="dashicons dashicons-info"></span>'; 687 html += '<span>' + this.escapeHtml(retentionText) + '</span>'; 688 html += '</div>'; 689 html += '<div class="divewp-cron-log-toolbar">'; 690 html += '<select class="divewp-cron-clear-select">'; 691 html += '<option value="">' + this.escapeHtml(this.config.strings.clearLogsOlderThan || 'Clear logs older than...') + '</option>'; 692 html += '<option value="7">7 days</option>'; 693 html += '<option value="30">30 days</option>'; 694 html += '<option value="90">90 days</option>'; 695 html += '</select>'; 696 html += '<button type="button" class="button divewp-cron-clear-apply">' + this.escapeHtml(this.config.strings.clearOlder || 'Clear older') + '</button>'; 697 html += '<button type="button" class="button divewp-cron-clear-all">' + this.escapeHtml(this.config.strings.clearAllLogs || 'Clear All Logs') + '</button>'; 698 html += '</div>'; 699 html += '<div class="divewp-cron-log-grouped">'; 656 700 html += '<div class="divewp-cron-log-header">'; 657 701 html += '<div class="divewp-cron-log-header__check"><input type="checkbox" class="divewp-cron-select-all"></div>'; … … 681 725 } 682 726 683 html += '</div> ';727 html += '</div></div>'; 684 728 $panel.html(html); 685 729 this.updateBulkActions(); … … 1084 1128 1085 1129 /** 1130 * Clear execution logs older than selected days 1131 */ 1132 handleClearLogsOlderThan: function() { 1133 var self = this; 1134 var days = parseInt($('.divewp-cron-clear-select').val(), 10); 1135 if (!days || days <= 0) { 1136 alert(this.config.strings.errorSelectDays || 'Please select a time period first.'); 1137 return; 1138 } 1139 var msg = (this.config.strings.confirmClearOlderThan || 'Delete execution logs older than %d days?').replace('%d', days); 1140 if (!confirm(msg)) { 1141 return; 1142 } 1143 $('.divewp-cron-clear-apply').prop('disabled', true); 1144 $.ajax({ 1145 url: this.config.ajaxurl, 1146 type: 'POST', 1147 data: { 1148 action: 'divewp_cron_clear_logs', 1149 nonce: this.config.nonce, 1150 days: days 1151 }, 1152 success: function(response) { 1153 $('.divewp-cron-clear-apply').prop('disabled', false); 1154 $('.divewp-cron-clear-select').val(''); 1155 var msg = response.success ? (response.data.message || '') : (response.data.message || self.config.strings.error); 1156 self.showResultModal(msg, response.success ? 'success' : 'error'); 1157 if (response.success && self.state.currentTab === 'execution-log') { 1158 self.loadExecutionLogContent(); 1159 } 1160 }, 1161 error: function() { 1162 $('.divewp-cron-clear-apply').prop('disabled', false); 1163 self.showResultModal(self.config.strings.error, 'error'); 1164 } 1165 }); 1166 }, 1167 1168 /** 1169 * Clear all execution logs 1170 */ 1171 handleClearAllLogs: function() { 1172 var self = this; 1173 if (!confirm(this.config.strings.confirmClearAllLogs || 'Delete ALL execution logs? This cannot be undone.')) { 1174 return; 1175 } 1176 $('.divewp-cron-clear-all').prop('disabled', true); 1177 $.ajax({ 1178 url: this.config.ajaxurl, 1179 type: 'POST', 1180 data: { 1181 action: 'divewp_cron_clear_logs', 1182 nonce: this.config.nonce, 1183 days: 0 1184 }, 1185 success: function(response) { 1186 $('.divewp-cron-clear-all').prop('disabled', false); 1187 var msg = response.success ? (response.data.message || '') : (response.data.message || self.config.strings.error); 1188 self.showResultModal(msg, response.success ? 'success' : 'error'); 1189 if (response.success && self.state.currentTab === 'execution-log') { 1190 self.loadExecutionLogContent(); 1191 } 1192 }, 1193 error: function() { 1194 $('.divewp-cron-clear-all').prop('disabled', false); 1195 self.showResultModal(self.config.strings.error, 'error'); 1196 } 1197 }); 1198 }, 1199 1200 /** 1086 1201 * Filter table by search term 1087 1202 */ … … 1484 1599 var self = this; 1485 1600 1601 // Handle close action 1602 if (action === 'close') { 1603 this.closeDrawer(); 1604 return; 1605 } 1606 1486 1607 // Add Task modal actions (not tied to a row) 1487 1608 if (action === 'add-cancel') { … … 1796 1917 1797 1918 /** 1798 * Show notice 1919 * Show notice (uses global showNotice if available; otherwise alert for errors only) 1799 1920 */ 1800 1921 showNotice: function(message, type) { 1801 // Use existing DiveWP notice system if available1802 1922 if (typeof divewpData !== 'undefined' && typeof divewpData.showNotice === 'function') { 1803 1923 divewpData.showNotice(message, type); 1804 1924 return; 1805 1925 } 1806 1807 // Fallback to simple alert 1808 if (type === 'error') { 1926 if (type === 'error' && message) { 1809 1927 alert('Error: ' + message); 1810 1928 } 1929 }, 1930 1931 /** 1932 * Show result modal (drawer) with message - used for clear logs feedback 1933 */ 1934 showResultModal: function(message, type) { 1935 if (!message) { 1936 return; 1937 } 1938 var $drawer = $('.divewp-cron-drawer'); 1939 var $title = $drawer.find('.divewp-cron-drawer__title'); 1940 var $content = $drawer.find('.divewp-cron-drawer__content'); 1941 var $footer = $drawer.find('.divewp-cron-drawer__footer'); 1942 1943 this.state.currentRow = null; 1944 $title.text(this.config.strings.clearResultTitle || 'Clear Logs Result'); 1945 1946 var iconClass = type === 'error' ? 'dashicons-warning' : 'dashicons-yes-alt'; 1947 var statusClass = type === 'error' ? 'divewp-modal-status--danger' : 'divewp-modal-status--success'; 1948 var html = '<div class="divewp-modal-details">'; 1949 html += '<div class="divewp-modal-notice divewp-modal-result ' + statusClass + '">'; 1950 html += '<span class="dashicons ' + iconClass + '"></span>'; 1951 html += '<span>' + this.escapeHtml(message) + '</span>'; 1952 html += '</div>'; 1953 html += '</div>'; 1954 1955 $content.html(html); 1956 $footer.empty().hide(); 1957 $drawer.addClass('open'); 1811 1958 }, 1812 1959 -
divewp-boost-site-performance/trunk/assets/js/divewp-plugins-management.js
r3467419 r3469978 385 385 } 386 386 387 // Update button visibility 387 // Reset and update button visibility 388 updateBtn.stop(true, true); 389 updateBtn.prop('disabled', false); 390 updateBtn.removeClass('button-success button-danger').addClass('button-primary'); 391 updateBtn.find('.update-label').text(divewpPluginsData.strings.updatePlugin); 392 388 393 if (hasUpdate) { 389 394 updateBtn.show(); -
divewp-boost-site-performance/trunk/divewp.php
r3467499 r3469978 4 4 * Plugin URI: https://wordpress.org/plugins/divewp-boost-site-performance/ 5 5 * Description: Learn WordPress Best Practices Through Your Own Site! Get clear insights about Performance, Security, and Best Practices – explained in plain English. 6 * Version: 2.3. 16 * Version: 2.3.3 7 7 * Requires at least: 6.8 8 8 * Requires PHP: 7.2 … … 30 30 31 31 // Define plugin constants first 32 define('DIVEWP_VERSION', '2.3. 0');32 define('DIVEWP_VERSION', '2.3.3'); 33 33 define('DIVEWP_PLUGIN_DIR', plugin_dir_path(__FILE__)); 34 34 define('DIVEWP_PLUGIN_URL', plugin_dir_url(__FILE__)); … … 120 120 throw new Exception('Failed to initialize database tables'); 121 121 } 122 update_option('divewp_db_version', '2. 0.1');122 update_option('divewp_db_version', '2.1.0'); 123 123 divewp_debug_log('Database tables created successfully with benchmark support', 'info'); 124 124 return true; … … 171 171 if (version_compare($current_db_version, '2.0.1', '<')) { 172 172 if (DiveWP_Database::init_tables()) { 173 update_option('divewp_db_version', '2.0.1'); 174 divewp_debug_log('Database updated to version 2.0.1 with benchmark results table', 'info'); 173 update_option('divewp_db_version', '2.1.0'); 174 divewp_debug_log('Database updated to version 2.1.0', 'info'); 175 } 176 } 177 if (version_compare($current_db_version, '2.1.0', '<')) { 178 if (DiveWP_Database::migrate_user_events_structured_context()) { 179 update_option('divewp_db_version', '2.1.0'); 180 divewp_debug_log('User events schema migrated to 2.1.0 (structured context)', 'info'); 175 181 } 176 182 } -
divewp-boost-site-performance/trunk/includes/admin/templates/admin-left-sidebar.php
r3467366 r3469978 62 62 <i class="dashicons dashicons-admin-site-alt3"></i> 63 63 <?php esc_html_e('Hosting Benchmark', 'divewp-boost-site-performance'); ?> 64 <span class="new-feature-highlight-pill" data-feature-id="hosting"><?php esc_html_e('NEW', 'divewp-boost-site-performance'); ?></span>65 64 </li> 66 65 <li data-tab="cron-jobs" data-feature="cron-jobs"> 67 66 <i class="dashicons dashicons-clock"></i> 68 67 <?php esc_html_e('Cron Job Manager', 'divewp-boost-site-performance'); ?> 69 <span class="new-feature-highlight-pill" data-feature-id="cron-jobs"><?php esc_html_e('NEW', 'divewp-boost-site-performance'); ?></span>70 68 </li> 71 69 <li data-tab="user-events" data-feature="user-events"> 72 70 <i class="dashicons dashicons-groups"></i> 73 71 <?php esc_html_e('User Events', 'divewp-boost-site-performance'); ?> 74 <span class="new-feature-highlight-pill" data-feature-id="user-events"><?php esc_html_e('NEW', 'divewp-boost-site-performance'); ?></span>75 72 </li> 76 73 <li data-tab="email" data-feature="email-insights"> … … 81 78 <i class="dashicons dashicons-rest-api"></i> 82 79 <?php esc_html_e('AI Capabilities', 'divewp-boost-site-performance'); ?> 83 <span class="new-feature-highlight-pill" data-feature-id="ai-capabilities"><?php esc_html_e('NEW', 'divewp-boost-site-performance'); ?></span>84 80 </li> 85 81 <li data-tab="plugins-management" data-feature="plugins-management"> 86 82 <i class="dashicons dashicons-admin-plugins"></i> 87 83 <?php esc_html_e('Plugins Management', 'divewp-boost-site-performance'); ?> 84 <span class="new-feature-highlight-pill" data-feature-id="plugins-management"><?php esc_html_e('NEW', 'divewp-boost-site-performance'); ?></span> 88 85 </li> 89 86 </ul> -
divewp-boost-site-performance/trunk/includes/class-divewp-database.php
r3448398 r3469978 268 268 status varchar(20) NOT NULL, 269 269 created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 270 actor_snapshot text, 271 channel varchar(50), 272 source_context text, 273 target_context text, 274 request_metadata text, 270 275 PRIMARY KEY (id) 271 276 ) {$charset_collate};"; … … 411 416 412 417 /** 418 * Migrate user_events table additively for structured context fields (v2.1.0). 419 * Idempotent: safe to rerun; adds missing columns only. 420 * 421 * @since 2.3.3 422 * @return bool True on success, false on failure 423 */ 424 public static function migrate_user_events_structured_context() { 425 global $wpdb; 426 427 if (!current_user_can('manage_options')) { 428 return false; 429 } 430 431 $table_name = $wpdb->prefix . self::$tables['user_events']; 432 433 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Migration table check; caching inappropriate for schema verification 434 if ($wpdb->get_var($wpdb->prepare( 435 "SHOW TABLES LIKE %s", 436 $table_name 437 )) !== $table_name) { 438 return true; 439 } 440 441 $charset_collate = $wpdb->get_charset_collate(); 442 443 // dbDelta adds missing columns; does not drop or rename existing ones 444 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name and charset are admin-defined 445 $sql = "CREATE TABLE {$table_name} ( 446 id bigint(20) NOT NULL AUTO_INCREMENT, 447 event_type varchar(50) NOT NULL, 448 event_action varchar(50) NOT NULL, 449 user_id bigint(20) NOT NULL, 450 description text NOT NULL, 451 status varchar(20) NOT NULL, 452 created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 453 actor_snapshot text, 454 channel varchar(50), 455 source_context text, 456 target_context text, 457 request_metadata text, 458 PRIMARY KEY (id) 459 ) {$charset_collate};"; 460 461 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 462 dbDelta($sql); 463 464 if (defined('DIVEWP_DEBUG') && DIVEWP_DEBUG) { 465 divewp_debug_log(esc_html__('User events migration to v2.1.0 completed', 'divewp-boost-site-performance'), 'info'); 466 } 467 468 return true; 469 } 470 471 /** 413 472 * Verify existence of required database tables 414 473 * -
divewp-boost-site-performance/trunk/includes/class-divewp-db-access.php
r3448398 r3469978 162 162 * No caching as this is a write operation requiring immediate effect. 163 163 * Protected by capability checks. 164 * Supports optional structured context (actor_snapshot, channel, source_context, target_context, request_metadata). 165 * 166 * @since 2.3.3 164 167 */ 165 168 public function log_event($data) { 166 if (!current_user_can('manage_options')) { 167 return false; 168 } 169 170 global $wpdb; 171 172 // Ensure we store the timestamp in UTC 169 $is_failed_login = isset($data['event_action']) && $data['event_action'] === 'failed_login' 170 && isset($data['user_id']) && (int) $data['user_id'] === 0; 171 if (!$is_failed_login && !current_user_can('manage_options')) { 172 return false; 173 } 174 175 global $wpdb; 176 177 $insert_data = array( 178 'event_type' => sanitize_text_field($data['event_type']), 179 'event_action' => sanitize_text_field($data['event_action']), 180 'user_id' => absint($data['user_id']), 181 'description' => sanitize_text_field($data['description']), 182 'status' => sanitize_text_field($data['status']), 183 ); 184 173 185 $utc_now = new DateTime('now', new DateTimeZone('UTC')); 174 175 // Direct insert required for immediate logging 186 $insert_data['created_at'] = $utc_now->format('Y-m-d H:i:s'); 187 188 $format = array('%s', '%s', '%d', '%s', '%s', '%s'); 189 190 if (isset($data['actor_snapshot'])) { 191 $insert_data['actor_snapshot'] = wp_json_encode($data['actor_snapshot']); 192 $format[] = '%s'; 193 } 194 if (isset($data['channel'])) { 195 $insert_data['channel'] = sanitize_text_field($data['channel']); 196 $format[] = '%s'; 197 } 198 if (isset($data['source_context'])) { 199 $insert_data['source_context'] = wp_json_encode($data['source_context']); 200 $format[] = '%s'; 201 } 202 if (isset($data['target_context'])) { 203 $insert_data['target_context'] = wp_json_encode($data['target_context']); 204 $format[] = '%s'; 205 } 206 if (isset($data['request_metadata'])) { 207 $insert_data['request_metadata'] = wp_json_encode($data['request_metadata']); 208 $format[] = '%s'; 209 } 210 176 211 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Write operation for admin-only logging feature 177 return $wpdb->insert( 178 $wpdb->prefix . 'divewp_user_events', 179 array( 180 'event_type' => sanitize_text_field($data['event_type']), 181 'event_action' => sanitize_text_field($data['event_action']), 182 'user_id' => absint($data['user_id']), 183 'description' => sanitize_text_field($data['description']), 184 'status' => sanitize_text_field($data['status']), 185 'created_at' => $utc_now->format('Y-m-d H:i:s') 186 ), 187 array('%s', '%s', '%d', '%s', '%s', '%s') 188 ); 212 return $wpdb->insert($wpdb->prefix . 'divewp_user_events', $insert_data, $format); 189 213 } 190 214 … … 225 249 226 250 return $results ?: array(); 251 } 252 253 /** 254 * Get a single user event by ID with user login. 255 * Returns structured fields (actor_snapshot, channel, etc.) when present; null for missing. 256 * 257 * @since 2.3.3 258 * @param int $event_id Event ID 259 * @return object|null Event object or null if not found 260 */ 261 public function get_event_by_id($event_id) { 262 if (!current_user_can('manage_options')) { 263 return null; 264 } 265 266 $event_id = absint($event_id); 267 if (!$event_id) { 268 return null; 269 } 270 271 global $wpdb; 272 $events_table = esc_sql($this->user_events_table); 273 $users_table = esc_sql($wpdb->users); 274 275 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin-only data retrieval 276 $event = $wpdb->get_row( 277 $wpdb->prepare( 278 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names escaped with esc_sql(), not user input 279 "SELECT e.*, u.user_login FROM $events_table e LEFT JOIN $users_table u ON e.user_id = u.ID WHERE e.id = %d", 280 $event_id 281 ) 282 ); 283 284 return $event ?: null; 227 285 } 228 286 … … 858 916 859 917 if (!empty($hook)) { 860 $where_parts[] = 'hook LIKE%s';861 $prepare_values[] = '%' . $wpdb->esc_like(sanitize_text_field($hook)) . '%';918 $where_parts[] = 'hook = %s'; 919 $prepare_values[] = sanitize_text_field($hook); 862 920 } 863 921 … … 942 1000 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped 943 1001 "SELECT COUNT(*) FROM $table" 1002 ); 1003 } 1004 1005 /** 1006 * Get total cron logs count for a specific hook (exact match). 1007 * 1008 * @since 2.2.0 1009 * @param string $hook Hook name (exact match) 1010 * @return int Total count for that hook 1011 */ 1012 public function get_cron_logs_count_for_hook($hook) { 1013 if (!current_user_can('manage_options')) { 1014 return 0; 1015 } 1016 if (empty($hook)) { 1017 return 0; 1018 } 1019 global $wpdb; 1020 $table = esc_sql($this->cron_logs_table); 1021 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin-only monitoring 1022 return (int) $wpdb->get_var( 1023 $wpdb->prepare( 1024 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql() and not user input 1025 "SELECT COUNT(*) FROM $table WHERE hook = %s", 1026 sanitize_text_field($hook) 1027 ) 1028 ); 1029 } 1030 1031 /** 1032 * Get cron logs grouped by hook with accurate per-hook totals. 1033 * Uses SQL GROUP BY for correct counts regardless of table size. 1034 * 1035 * @since 2.2.0 1036 * @param int $limit Max number of distinct hooks to return (by last_run DESC) 1037 * @param string $status Optional status filter 1038 * @return array Array of hook summaries with total_runs, success_count, etc. 1039 */ 1040 public function get_cron_logs_grouped_by_hook($limit = 500, $status = '') { 1041 if (!current_user_can('manage_options')) { 1042 return array(); 1043 } 1044 global $wpdb; 1045 $limit = absint($limit); 1046 $table = esc_sql($this->cron_logs_table); 1047 $status_clause = ''; 1048 $prepare_values = array(); 1049 if (!empty($status)) { 1050 $status_clause = 'WHERE status = %s'; 1051 $prepare_values[] = sanitize_text_field($status); 1052 } 1053 $prepare_values[] = $limit; 1054 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber 1055 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter 1056 $rows = $wpdb->get_results( 1057 $wpdb->prepare( 1058 "SELECT 1059 hook, 1060 COUNT(*) as total_runs, 1061 SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count, 1062 SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count, 1063 SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) as warning_count, 1064 SUM(COALESCE(duration_ms, 0)) as total_duration, 1065 MIN(duration_ms) as min_duration, 1066 MAX(duration_ms) as max_duration, 1067 MAX(started_at) as last_run, 1068 SUBSTRING_INDEX(GROUP_CONCAT(status ORDER BY started_at DESC SEPARATOR '|'), '|', 1) as last_status 1069 FROM $table 1070 $status_clause 1071 GROUP BY hook 1072 ORDER BY last_run DESC 1073 LIMIT %d", 1074 ...$prepare_values 1075 ), 1076 ARRAY_A 1077 ); 1078 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber 1079 if (empty($rows)) { 1080 return array(); 1081 } 1082 $display_format = get_option('date_format') . ' ' . get_option('time_format'); 1083 $grouped = array(); 1084 foreach ($rows as $row) { 1085 $total_runs = (int) $row['total_runs']; 1086 $success_count = (int) $row['success_count']; 1087 $grouped[] = array( 1088 'hook' => $row['hook'], 1089 'total_runs' => $total_runs, 1090 'success_count' => $success_count, 1091 'error_count' => (int) $row['error_count'], 1092 'warning_count' => (int) $row['warning_count'], 1093 'avg_duration_ms' => $total_runs > 0 ? round((float) $row['total_duration'] / $total_runs, 1) : 0, 1094 'min_duration' => $row['min_duration'] !== null ? (int) $row['min_duration'] : null, 1095 'max_duration' => $row['max_duration'] !== null ? (int) $row['max_duration'] : null, 1096 'last_run' => $row['last_run'], 1097 'last_run_local' => get_date_from_gmt($row['last_run'], $display_format), 1098 'last_status' => $row['last_status'], 1099 'success_rate' => $total_runs > 0 ? round(($success_count / $total_runs) * 100, 0) : 0, 1100 ); 1101 } 1102 return $grouped; 1103 } 1104 1105 /** 1106 * Get summary stats for a specific hook (exact match). 1107 * 1108 * @since 2.2.0 1109 * @param string $hook Hook name (exact match) 1110 * @return array Summary with total_runs, success_count, error_count, etc. 1111 */ 1112 public function get_cron_log_summary_for_hook($hook) { 1113 if (!current_user_can('manage_options')) { 1114 return array( 1115 'total_runs' => 0, 1116 'success_count' => 0, 1117 'error_count' => 0, 1118 'warning_count' => 0, 1119 'success_rate' => 0, 1120 'avg_duration_ms' => 0, 1121 'min_duration_ms' => null, 1122 'max_duration_ms' => null, 1123 ); 1124 } 1125 if (empty($hook)) { 1126 return array( 1127 'total_runs' => 0, 1128 'success_count' => 0, 1129 'error_count' => 0, 1130 'warning_count' => 0, 1131 'success_rate' => 0, 1132 'avg_duration_ms' => 0, 1133 'min_duration_ms' => null, 1134 'max_duration_ms' => null, 1135 ); 1136 } 1137 global $wpdb; 1138 $table = esc_sql($this->cron_logs_table); 1139 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter 1140 $row = $wpdb->get_row( 1141 $wpdb->prepare( 1142 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped with esc_sql() and not user input 1143 "SELECT 1144 COUNT(*) as total_runs, 1145 SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count, 1146 SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count, 1147 SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) as warning_count, 1148 SUM(COALESCE(duration_ms, 0)) as total_duration, 1149 MIN(duration_ms) as min_duration, 1150 MAX(duration_ms) as max_duration 1151 FROM $table 1152 WHERE hook = %s", 1153 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 1154 sanitize_text_field($hook) 1155 ), 1156 ARRAY_A 1157 ); 1158 if (!$row || (int) $row['total_runs'] === 0) { 1159 return array( 1160 'total_runs' => 0, 1161 'success_count' => 0, 1162 'error_count' => 0, 1163 'warning_count' => 0, 1164 'success_rate' => 0, 1165 'avg_duration_ms' => 0, 1166 'min_duration_ms' => null, 1167 'max_duration_ms' => null, 1168 ); 1169 } 1170 $total_runs = (int) $row['total_runs']; 1171 $success_count = (int) $row['success_count']; 1172 return array( 1173 'total_runs' => $total_runs, 1174 'success_count' => $success_count, 1175 'error_count' => (int) $row['error_count'], 1176 'warning_count' => (int) $row['warning_count'], 1177 'success_rate' => round(($success_count / $total_runs) * 100, 0), 1178 'avg_duration_ms' => round((float) $row['total_duration'] / $total_runs, 1), 1179 'min_duration_ms' => $row['min_duration'] !== null ? (int) $row['min_duration'] : null, 1180 'max_duration_ms' => $row['max_duration'] !== null ? (int) $row['max_duration'] : null, 944 1181 ); 945 1182 } -
divewp-boost-site-performance/trunk/includes/class-divewp-main.php
r3467366 r3469978 443 443 'nonce' => wp_create_nonce('divewp_nonce'), 444 444 'testEmailFailed' => __('Failed to send test email', 'divewp-boost-site-performance'), 445 'loading' => __('Loading...', 'divewp-boost-site-performance'), 446 'error' => __('Failed to load event details.', 'divewp-boost-site-performance'), 447 'eventDetailsWho' => __('Who', 'divewp-boost-site-performance'), 448 'eventDetailsVia' => __('Via', 'divewp-boost-site-performance'), 449 'eventDetailsWhen' => __('When', 'divewp-boost-site-performance'), 450 'eventDetailsWhat' => __('Details', 'divewp-boost-site-performance'), 451 'eventDetailsWhere' => __('Context', 'divewp-boost-site-performance'), 452 'eventDetailsTarget' => __('Target', 'divewp-boost-site-performance'), 445 453 'preloader' => array( 446 454 'minDuration' => 1000, -
divewp-boost-site-performance/trunk/includes/features/cron-jobs/ajax-handlers.php
r3448398 r3469978 767 767 768 768 $db = DiveWP_DB_Access::get_instance(); 769 $ logs = $db->get_recent_cron_logs($limit, 0, $status, '');769 $hooks = $db->get_cron_logs_grouped_by_hook($limit, $status); 770 770 $total = $db->get_total_cron_logs($status); 771 771 $stats = $db->get_cron_log_stats(); 772 $display_format = get_option('date_format') . ' ' . get_option('time_format');773 774 // Group logs by hook name775 $grouped = array();776 foreach ($logs as $log) {777 $hook = $log->hook;778 if (!isset($grouped[$hook])) {779 $grouped[$hook] = array(780 'hook' => $hook,781 'total_runs' => 0,782 'success_count' => 0,783 'error_count' => 0,784 'warning_count' => 0,785 'total_duration' => 0,786 'min_duration' => null,787 'max_duration' => null,788 'last_run' => null,789 'last_status' => null,790 'trigger_sources' => array(),791 );792 }793 794 $grouped[$hook]['total_runs']++;795 796 // Count by status797 if ($log->status === 'success') {798 $grouped[$hook]['success_count']++;799 } elseif ($log->status === 'error') {800 $grouped[$hook]['error_count']++;801 } elseif ($log->status === 'warning') {802 $grouped[$hook]['warning_count']++;803 }804 805 // Duration stats806 $duration = isset($log->duration_ms) ? (int) $log->duration_ms : 0;807 $grouped[$hook]['total_duration'] += $duration;808 if ($grouped[$hook]['min_duration'] === null || $duration < $grouped[$hook]['min_duration']) {809 $grouped[$hook]['min_duration'] = $duration;810 }811 if ($grouped[$hook]['max_duration'] === null || $duration > $grouped[$hook]['max_duration']) {812 $grouped[$hook]['max_duration'] = $duration;813 }814 815 // Track trigger sources816 if (!empty($log->trigger_source) && !in_array($log->trigger_source, $grouped[$hook]['trigger_sources'], true)) {817 $grouped[$hook]['trigger_sources'][] = $log->trigger_source;818 }819 820 // Last run (logs are ordered by started_at DESC, so first occurrence is latest)821 if ($grouped[$hook]['last_run'] === null) {822 $grouped[$hook]['last_run'] = $log->started_at;823 $grouped[$hook]['last_run_local'] = get_date_from_gmt($log->started_at, $display_format);824 $grouped[$hook]['last_status'] = $log->status;825 }826 }827 828 // Calculate averages and success rate829 foreach ($grouped as $hook => &$data) {830 $data['avg_duration_ms'] = $data['total_runs'] > 0831 ? round($data['total_duration'] / $data['total_runs'], 1)832 : 0;833 $data['success_rate'] = $data['total_runs'] > 0834 ? round(($data['success_count'] / $data['total_runs']) * 100, 0)835 : 0;836 unset($data['total_duration']); // Don't send raw total837 }838 unset($data);839 840 // Convert to indexed array and sort by last_run841 $hooks = array_values($grouped);842 usort($hooks, function($a, $b) {843 return strcmp($b['last_run'], $a['last_run']);844 });845 772 846 773 wp_send_json_success(array( … … 873 800 $db = DiveWP_DB_Access::get_instance(); 874 801 $logs = $db->get_recent_cron_logs($limit, 0, '', $hook); 875 876 // Format the executions 802 $summary = $db->get_cron_log_summary_for_hook($hook); 803 804 // Format the executions (timeline preview, limited) 877 805 $executions = array(); 878 806 $display_format = get_option('date_format') . ' ' . get_option('time_format'); … … 890 818 } 891 819 892 // Calculate summary stats893 $total_runs = count($executions);894 $success_count = 0;895 $error_count = 0;896 $total_duration = 0;897 $min_duration = null;898 $max_duration = null;899 900 foreach ($executions as $exec) {901 if ($exec['status'] === 'success') {902 $success_count++;903 } elseif ($exec['status'] === 'error') {904 $error_count++;905 }906 $duration = (int) $exec['duration_ms'];907 $total_duration += $duration;908 if ($min_duration === null || $duration < $min_duration) {909 $min_duration = $duration;910 }911 if ($max_duration === null || $duration > $max_duration) {912 $max_duration = $duration;913 }914 }915 916 820 wp_send_json_success(array( 917 821 'hook' => $hook, 918 822 'executions' => $executions, 919 'summary' => array( 920 'total_runs' => $total_runs, 921 'success_count' => $success_count, 922 'error_count' => $error_count, 923 'success_rate' => $total_runs > 0 ? round(($success_count / $total_runs) * 100, 0) : 0, 924 'avg_duration_ms' => $total_runs > 0 ? round($total_duration / $total_runs, 1) : 0, 925 'min_duration_ms' => $min_duration, 926 'max_duration_ms' => $max_duration, 927 ), 823 'summary' => $summary, 928 824 )); 929 825 } … … 984 880 if ($days > 0) { 985 881 $deleted = $db->cleanup_cron_logs($days); 882 $deleted = $deleted === false ? 0 : (int) $deleted; 883 if ($deleted > 0) { 884 $message = sprintf( 885 /* translators: 1: Number of log entries deleted, 2: Number of days */ 886 _n( 887 '%1$d log entry deleted (older than %2$d days).', 888 '%1$d log entries deleted (older than %2$d days).', 889 $deleted, 890 'divewp-boost-site-performance' 891 ), 892 $deleted, 893 $days 894 ); 895 } else { 896 $message = sprintf( 897 /* translators: %d: Number of days */ 898 __('No logs older than %d days found.', 'divewp-boost-site-performance'), 899 $days 900 ); 901 } 902 } else { 903 $total_before = $db->get_total_cron_logs(); 904 $db->delete_all_cron_logs(); 986 905 $message = sprintf( 987 /* translators: %d: Number of days */ 988 __('Logs older than %d days have been deleted.', 'divewp-boost-site-performance'), 989 $days 906 /* translators: %d: Number of log entries deleted */ 907 _n( 908 '%d log entry deleted.', 909 '%d log entries deleted.', 910 $total_before, 911 'divewp-boost-site-performance' 912 ), 913 $total_before 990 914 ); 991 } else {992 $db->delete_all_cron_logs();993 $message = __('All logs have been deleted.', 'divewp-boost-site-performance');994 915 } 995 916 -
divewp-boost-site-performance/trunk/includes/features/cron-jobs/class-cron-jobs.php
r3448398 r3469978 187 187 /* translators: %d: Number of overdue tasks (plural) */ 188 188 'tasks_overdue' => __('%d tasks overdue', 'divewp-boost-site-performance'), 189 /* translators: %d: Retention period in days */ 190 'retentionNotice' => __('Logs are auto-cleaned daily. Retention: %d days.', 'divewp-boost-site-performance'), 191 'clearLogsOlderThan' => __('Clear logs older than...', 'divewp-boost-site-performance'), 192 'clearOlder' => __('Clear older', 'divewp-boost-site-performance'), 193 'clearAllLogs' => __('Clear All Logs', 'divewp-boost-site-performance'), 194 /* translators: %d: Number of days */ 195 'confirmClearOlderThan' => __('Delete execution logs older than %d days?', 'divewp-boost-site-performance'), 196 'confirmClearAllLogs' => __('Delete ALL execution logs? This cannot be undone.', 'divewp-boost-site-performance'), 197 'clearResultTitle' => __('Clear Logs Result', 'divewp-boost-site-performance'), 198 'close' => __('Close', 'divewp-boost-site-performance'), 189 199 ), 200 'retention_days' => absint(apply_filters('divewp_cron_log_retention_days', 30)), 190 201 )); 191 202 } -
divewp-boost-site-performance/trunk/includes/features/user-events/class-event-logger.php
r3448398 r3469978 165 165 // REST API authenticated requests (used for method + route context). 166 166 add_filter('rest_request_before_callbacks', array($this, 'log_rest_api_access'), 10, 3); 167 168 // New events 169 add_action('set_user_role', array($this, 'log_user_role_change'), 10, 3); 170 add_action('wp_login_failed', array($this, 'log_failed_login'), 10, 2); 171 add_action('before_delete_post', array($this, 'log_post_permanent_deletion'), 10, 2); 172 add_action('upgrader_process_complete', array($this, 'log_core_update'), 10, 2); 173 add_action('wp_ajax_edit-theme-plugin-file', array($this, 'log_file_edit'), 5); 167 174 } 168 175 … … 178 185 * @type string $description Description of event (max 255 chars) 179 186 * @type string $status Status of event (success/warning/error/info) 187 * @type array $actor_snapshot Optional. Structured who (user_id, user_login, user_email) 188 * @type string $channel Optional. Via what (admin_ui, ajax, rest_app_password) 189 * @type array $source_context Optional. Where (screen, route, etc.) 190 * @type array $target_context Optional. Target of action (post_id, plugin_slug, etc.) 191 * @type array $request_metadata Optional. Extra metadata 180 192 * } 181 193 * @return bool|int False on failure, event ID on success … … 186 198 throw new Exception(__('Invalid event data', 'divewp-boost-site-performance')); 187 199 } 188 200 201 $data['description'] = substr(sanitize_text_field($data['description']), 0, 2000); 202 203 $context = $this->build_structured_context($data); 204 $data = array_merge($context, $data); 205 189 206 $result = $this->db->log_event($data); 190 207 if (!$result) { 191 208 throw new Exception(__('Failed to insert event', 'divewp-boost-site-performance')); 192 209 } 193 210 194 211 return $result; 195 212 } catch (Exception $e) { … … 202 219 return false; 203 220 } 221 } 222 223 /** 224 * Build structured context (who, where, via what) for event logging. 225 * Preserves existing description; adds normalized fields when available. 226 * 227 * @since 2.3.3 228 * @param array $data Event data (user_id, etc.) 229 * @return array actor_snapshot, channel, source_context, target_context, request_metadata 230 */ 231 private function build_structured_context($data) { 232 $user_id = isset($data['user_id']) ? absint($data['user_id']) : get_current_user_id(); 233 $user = $user_id ? get_userdata($user_id) : null; 234 235 $actor_snapshot = array( 236 'user_id' => $user_id, 237 'user_login' => $user ? sanitize_text_field($user->user_login) : '', 238 'user_email' => $user ? sanitize_email($user->user_email) : '', 239 ); 240 241 $channel = 'admin_ui'; 242 if (defined('REST_REQUEST') && REST_REQUEST && $this->rest_app_password_authenticated) { 243 $channel = 'rest_app_password'; 244 } elseif (defined('DOING_AJAX') && DOING_AJAX) { 245 $channel = 'ajax'; 246 } 247 248 $source_context = array(); 249 if (function_exists('get_current_screen') && is_admin()) { 250 $screen = get_current_screen(); 251 if ($screen) { 252 $source_context['screen'] = sanitize_text_field($screen->id); 253 $source_context['screen_base'] = sanitize_text_field($screen->base); 254 } 255 } 256 if (empty($source_context)) { 257 $source_context['context'] = is_admin() ? 'admin' : 'front'; 258 } 259 260 return array( 261 'actor_snapshot' => $actor_snapshot, 262 'channel' => $channel, 263 'source_context' => $source_context, 264 ); 204 265 } 205 266 … … 298 359 wp_get_current_user()->user_login 299 360 ), 300 'status' => $this->get_status_for_action($action) 361 'status' => $this->get_status_for_action($action), 362 'target_context' => array( 363 'post_id' => $post->ID, 364 'post_type' => $post->post_type, 365 'post_title' => sanitize_text_field($post->post_title), 366 ), 301 367 ]); 302 368 } … … 742 808 wp_get_current_user()->user_login 743 809 ), 744 'status' => 'success' 810 'status' => 'success', 811 'target_context' => array( 812 'plugin' => sanitize_text_field($plugin), 813 'plugin_name' => sanitize_text_field($plugin_name), 814 'version' => sanitize_text_field($plugin_version), 815 ), 745 816 ]); 746 817 } … … 885 956 wp_get_current_user()->user_login 886 957 ), 887 'status' => 'success' 958 'status' => 'success', 959 'target_context' => array( 960 'from_theme' => sanitize_text_field($old_theme->get('Name')), 961 'to_theme' => sanitize_text_field($new_theme->get('Name')), 962 'to_version' => sanitize_text_field($new_theme->get('Version')), 963 ), 888 964 ]); 889 965 } … … 1180 1256 $user->user_login 1181 1257 ), 1182 'status' => 'info' 1258 'status' => 'info', 1259 'source_context' => array( 1260 'route' => sanitize_text_field($route), 1261 'method' => sanitize_text_field($method), 1262 ), 1183 1263 ]); 1184 1264 1185 1265 $logged = true; 1186 1266 return $response; 1267 } 1268 1269 /** 1270 * Log user role change 1271 * 1272 * @param int $user_id User ID. 1273 * @param string $new_role New role. 1274 * @param array $old_roles Old roles. 1275 */ 1276 public function log_user_role_change($user_id, $new_role, $old_roles) { 1277 if (!$this->is_admin_user()) { 1278 return; 1279 } 1280 1281 $user = get_userdata($user_id); 1282 if (!$user) { 1283 return; 1284 } 1285 1286 $old_role = is_array($old_roles) && !empty($old_roles) ? reset($old_roles) : ''; 1287 1288 $this->insert([ 1289 'event_type' => 'user_management', 1290 'event_action' => 'role_changed', 1291 'user_id' => get_current_user_id(), 1292 'description' => sprintf( 1293 'User "%s" role changed from "%s" to "%s" by Administrator %s', 1294 $user->user_login, 1295 $old_role ? translate_user_role($old_role) : '-', 1296 $new_role ? translate_user_role($new_role) : '-', 1297 wp_get_current_user()->user_login 1298 ), 1299 'status' => 'info', 1300 'target_context' => array( 1301 'user_login' => sanitize_text_field($user->user_login), 1302 'old_role' => sanitize_text_field($old_role), 1303 'new_role' => sanitize_text_field($new_role), 1304 ), 1305 ]); 1306 } 1307 1308 /** 1309 * Log failed login attempt 1310 * 1311 * @param string $username Username attempted. 1312 * @param object $error WP_Error object. 1313 */ 1314 public function log_failed_login($username, $error) { 1315 $throttle_key = 'divewp_failed_login_' . md5(sanitize_text_field($username)); 1316 if (get_transient($throttle_key)) { 1317 return; 1318 } 1319 set_transient($throttle_key, 1, MINUTE_IN_SECONDS); 1320 1321 $this->insert([ 1322 'event_type' => 'admin', 1323 'event_action' => 'failed_login', 1324 'user_id' => 0, 1325 'description' => sprintf( 1326 'Failed login attempt for username "%s"', 1327 sanitize_text_field($username) 1328 ), 1329 'status' => 'warning', 1330 'target_context' => array( 1331 'attempted_username' => sanitize_text_field($username), 1332 ), 1333 ]); 1334 } 1335 1336 /** 1337 * Log post permanent deletion 1338 * 1339 * @param int $post_id Post ID. 1340 * @param object $post Post object. 1341 */ 1342 public function log_post_permanent_deletion($post_id, $post) { 1343 if (!$this->is_admin_user()) { 1344 return; 1345 } 1346 1347 if (!is_object($post)) { 1348 $post = get_post($post_id); 1349 } 1350 if (!$post || wp_is_post_revision($post_id) || (isset($post->post_status) && $post->post_status === 'auto-draft')) { 1351 return; 1352 } 1353 1354 $post_type = get_post_type_object($post->post_type); 1355 $post_type_label = $post_type ? $post_type->labels->singular_name : $post->post_type; 1356 1357 $this->insert([ 1358 'event_type' => 'content', 1359 'event_action' => 'deleted_permanently', 1360 'user_id' => get_current_user_id(), 1361 'description' => sprintf( 1362 '%s "%s" was permanently deleted by Administrator %s', 1363 $post_type_label, 1364 $post->post_title, 1365 wp_get_current_user()->user_login 1366 ), 1367 'status' => 'danger', 1368 'target_context' => array( 1369 'post_id' => $post_id, 1370 'post_type' => sanitize_text_field($post->post_type), 1371 'post_title' => sanitize_text_field($post->post_title), 1372 ), 1373 ]); 1374 } 1375 1376 /** 1377 * Log WordPress core update 1378 * 1379 * @param object $upgrader Upgrader instance. 1380 * @param array $options Upgrade options. 1381 */ 1382 public function log_core_update($upgrader_object, $options) { 1383 static $logged = false; 1384 if ($logged) { 1385 return; 1386 } 1387 1388 if (!$this->is_admin_user() || 1389 !isset($options['type']) || $options['type'] !== 'core' || 1390 !isset($options['action']) || $options['action'] !== 'update') { 1391 return; 1392 } 1393 1394 $version = get_bloginfo('version'); 1395 1396 $this->insert([ 1397 'event_type' => 'admin', 1398 'event_action' => 'core_updated', 1399 'user_id' => get_current_user_id(), 1400 'description' => sprintf( 1401 'WordPress core updated to v%s by Administrator %s', 1402 $version, 1403 wp_get_current_user()->user_login 1404 ), 1405 'status' => 'success', 1406 'target_context' => array( 1407 'version' => sanitize_text_field($version), 1408 ), 1409 ]); 1410 1411 $logged = true; 1412 } 1413 1414 /** 1415 * Log file edit via theme/plugin editor 1416 */ 1417 public function log_file_edit() { 1418 if (!$this->is_admin_user()) { 1419 return; 1420 } 1421 1422 if (!isset($_POST['file'])) { 1423 return; 1424 } 1425 1426 $file = sanitize_text_field(wp_unslash($_POST['file'])); 1427 if (empty($file)) { 1428 return; 1429 } 1430 1431 $editor_type = isset($_POST['plugin']) ? 'plugin' : 'theme'; 1432 if ('plugin' === $editor_type) { 1433 $nonce_action = 'edit-plugin_' . $file; 1434 } else { 1435 $stylesheet = isset($_POST['theme']) ? sanitize_text_field(wp_unslash($_POST['theme'])) : ''; 1436 if (empty($stylesheet)) { 1437 return; 1438 } 1439 $nonce_action = 'edit-theme_' . $stylesheet . '_' . $file; 1440 } 1441 $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; 1442 if (!wp_verify_nonce($nonce, $nonce_action)) { 1443 return; 1444 } 1445 1446 $this->insert([ 1447 'event_type' => 'admin', 1448 'event_action' => 'file_edited', 1449 'user_id' => get_current_user_id(), 1450 'description' => sprintf( 1451 '%s file "%s" was edited by Administrator %s', 1452 ucfirst($editor_type), 1453 $file, 1454 wp_get_current_user()->user_login 1455 ), 1456 'status' => 'warning', 1457 'target_context' => array( 1458 'file' => $file, 1459 'editor_type' => $editor_type, 1460 ), 1461 ]); 1187 1462 } 1188 1463 … … 1213 1488 */ 1214 1489 private function validate_event_data($data) { 1215 // Sanitize input data 1216 $data = array_map('sanitize_text_field', $data); 1217 1218 // Validate event type 1219 if (!in_array($data['event_type'], self::VALID_EVENT_TYPES)) { 1490 if (!is_array($data) || empty($data['event_type']) || empty($data['description'])) { 1220 1491 return false; 1221 1492 } 1222 1223 // Truncate description if too long 1224 $data['description'] = substr($data['description'], 0, 255); 1225 1493 1494 $event_type = sanitize_text_field($data['event_type']); 1495 if (!in_array($event_type, self::VALID_EVENT_TYPES)) { 1496 return false; 1497 } 1498 1226 1499 return true; 1227 1500 } -
divewp-boost-site-performance/trunk/includes/features/user-events/class-user-events.php
r3448398 r3469978 60 60 add_action('wp_ajax_divewp_load_more_events', array($this, 'ajax_load_more_events')); 61 61 add_action('wp_ajax_divewp_load_recent_timeline', array($this, 'ajax_load_recent_timeline')); 62 add_action('wp_ajax_divewp_get_event_details', array($this, 'ajax_get_event_details')); 62 63 63 64 // Admin scripts and styles … … 73 74 } 74 75 75 // Enqueue User Events CSS 76 // Enqueue User Events CSS (depends on cron-jobs for shared modal-details styles) 76 77 wp_enqueue_style( 77 78 'divewp-user-events', 78 79 DIVEWP_PLUGIN_URL . 'assets/css/features/user-events.css', 79 array( ),80 array('divewp-cron-jobs'), 80 81 DIVEWP_VERSION 81 82 ); … … 154 155 echo '<div class="divewp-actions">'; 155 156 /* translators: Button text to refresh the activity logs */ 156 echo '<button id="divewp-refresh-logs" class="button button-primary" style="margin-right: 10px;">' .157 esc_html__('Refresh Logs', 'divewp-boost-site-performance') . 157 echo '<button id="divewp-refresh-logs" class="button button-primary">' . 158 esc_html__('Refresh Logs', 'divewp-boost-site-performance') . 158 159 '</button>'; 159 160 /* translators: Button text to delete all activity logs */ … … 194 195 195 196 foreach ($user_events_data as $event) { 196 echo '<tr>'; 197 echo '<td>' . esc_html($event->user_login) . '</td>'; 197 $event_id = isset($event->id) ? absint($event->id) : 0; 198 echo '<tr class="divewp-event-row' . ( $event_id ? ' divewp-event-row--clickable' : '' ) . '"' . ( $event_id ? ' data-event-id="' . esc_attr($event_id) . '"' : '' ) . '>'; 199 echo '<td>' . esc_html($event->user_login ?? '') . '</td>'; 198 200 echo '<td>' . esc_html($this->get_event_type_label($event->event_type)) . '</td>'; 199 201 echo '<td>'; … … 227 229 } 228 230 231 $this->render_event_details_drawer(); 229 232 echo '</div>'; // #divewp-logs-container 233 } 234 235 /** 236 * Render event details drawer (modal) markup. 237 * 238 * @since 2.3.3 239 */ 240 private function render_event_details_drawer() { 241 ?> 242 <div class="divewp-event-drawer" aria-hidden="true"> 243 <div class="divewp-event-drawer__overlay"></div> 244 <div class="divewp-event-drawer__panel"> 245 <div class="divewp-event-drawer__header"> 246 <h4 class="divewp-event-drawer__title"><?php esc_html_e('Event Details', 'divewp-boost-site-performance'); ?></h4> 247 <button type="button" class="divewp-event-drawer__close" aria-label="<?php esc_attr_e('Close', 'divewp-boost-site-performance'); ?>"> 248 <span class="dashicons dashicons-no-alt"></span> 249 </button> 250 </div> 251 <div class="divewp-event-drawer__content"> 252 <!-- Content loaded dynamically --> 253 </div> 254 </div> 255 </div> 256 <?php 230 257 } 231 258 … … 363 390 'edited' => __('Edited', 'divewp-boost-site-performance'), 364 391 /* translators: Action label when content is published */ 365 'published' => __('Published', 'divewp-boost-site-performance') 392 'published' => __('Published', 'divewp-boost-site-performance'), 393 394 // New events 395 /* translators: Action label when user role is changed */ 396 'role_changed' => __('Role Changed', 'divewp-boost-site-performance'), 397 /* translators: Action label for failed login attempt */ 398 'failed_login' => __('Failed Login', 'divewp-boost-site-performance'), 399 /* translators: Action label when content is permanently deleted */ 400 'deleted_permanently' => __('Deleted', 'divewp-boost-site-performance'), 401 /* translators: Action label when WordPress core is updated */ 402 'core_updated' => __('Core Updated', 'divewp-boost-site-performance'), 403 /* translators: Action label when theme/plugin file is edited */ 404 'file_edited' => __('File Edited', 'divewp-boost-site-performance') 366 405 ); 367 406 … … 470 509 471 510 foreach ($events as $event) { 472 echo '<tr>'; 473 echo '<td>' . esc_html($event->user_login) . '</td>'; 511 $event_id = isset($event->id) ? absint($event->id) : 0; 512 echo '<tr class="divewp-event-row' . ( $event_id ? ' divewp-event-row--clickable' : '' ) . '"' . ( $event_id ? ' data-event-id="' . esc_attr($event_id) . '"' : '' ) . '>'; 513 echo '<td>' . esc_html($event->user_login ?? '') . '</td>'; 474 514 echo '<td>' . esc_html($this->get_event_type_label($event->event_type)) . '</td>'; 475 515 echo '<td>'; … … 509 549 echo '<div class="divewp-timeline">'; 510 550 $current_date = ''; 551 $today_local = wp_date('Y-m-d'); 511 552 foreach ($recent_events as $event) { 512 // Get the site's timezone513 553 $timezone = wp_timezone(); 514 515 // Convert UTC timestamp to site's timezone516 554 $datetime = new DateTime($event->created_at, new DateTimeZone('UTC')); 517 555 $datetime->setTimezone($timezone); 518 519 556 $event_date = $datetime->format('Y-m-d'); 520 557 $event_time = $datetime->format('H:i'); 521 558 522 559 if ($event_date !== $current_date) { 523 if ($current_date === '') { 524 echo '<div class="divewp-timeline-date today">'; 525 echo '<span class="date-label">' . esc_html__('Today', 'divewp-boost-site-performance') . '</span>'; 526 } else { 527 echo '<div class="divewp-timeline-date">'; 528 echo '<span class="date-label">' . esc_html($datetime->format('F j')) . '</span>'; 529 } 560 echo '<div class="divewp-timeline-date' . ( $event_date === $today_local ? ' today' : '' ) . '">'; 561 echo '<span class="date-label">'; 562 echo $event_date === $today_local 563 ? esc_html__('Today', 'divewp-boost-site-performance') 564 : esc_html($datetime->format('F j')); 565 echo '</span>'; 530 566 echo '</div>'; 531 567 $current_date = $event_date; … … 566 602 567 603 /** 604 * AJAX handler for event details (modal) 605 * 606 * @since 2.3.3 607 */ 608 public function ajax_get_event_details() { 609 if (!isset($_REQUEST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'])), 'divewp_nonce')) { 610 wp_send_json_error(array('message' => esc_html__('Security verification failed', 'divewp-boost-site-performance'))); 611 return; 612 } 613 if (!current_user_can('manage_options')) { 614 wp_send_json_error(array('message' => esc_html__('Permission denied', 'divewp-boost-site-performance'))); 615 return; 616 } 617 618 $event_id = isset($_POST['event_id']) ? absint($_POST['event_id']) : 0; 619 if (!$event_id) { 620 wp_send_json_error(array('message' => esc_html__('Invalid event', 'divewp-boost-site-performance'))); 621 return; 622 } 623 624 $db = DiveWP_DB_Access::get_instance(); 625 $event = $db->get_event_by_id($event_id); 626 if (!$event) { 627 wp_send_json_error(array('message' => esc_html__('Event not found', 'divewp-boost-site-performance'))); 628 return; 629 } 630 631 $display_format = get_option('date_format') . ' ' . get_option('time_format'); 632 $timezone = wp_timezone(); 633 $datetime = new DateTime($event->created_at, new DateTimeZone('UTC')); 634 $datetime->setTimezone($timezone); 635 $created_at_local = $datetime->format($display_format); 636 637 $actor = array( 638 'user_login' => $event->user_login ?? '', 639 'user_id' => $event->user_id ?? 0, 640 ); 641 if (!empty($event->actor_snapshot)) { 642 $decoded = json_decode($event->actor_snapshot, true); 643 if (is_array($decoded)) { 644 $actor = array_merge($actor, $decoded); 645 } 646 } 647 648 $channel_labels = array( 649 'admin_ui' => __('Admin dashboard', 'divewp-boost-site-performance'), 650 'ajax' => __('AJAX request', 'divewp-boost-site-performance'), 651 'rest_app_password' => __('REST API (Application Password)', 'divewp-boost-site-performance'), 652 ); 653 $channel = isset($channel_labels[ $event->channel ?? '' ]) ? $channel_labels[ $event->channel ] : ($event->channel ?? __('Unknown', 'divewp-boost-site-performance')); 654 655 $source_context = array(); 656 if (!empty($event->source_context)) { 657 $decoded = json_decode($event->source_context, true); 658 if (is_array($decoded)) { 659 $source_context = $decoded; 660 } 661 } 662 663 $target_context = array(); 664 if (!empty($event->target_context)) { 665 $decoded = json_decode($event->target_context, true); 666 if (is_array($decoded)) { 667 $target_context = $decoded; 668 } 669 } 670 671 $plain_summary = sprintf( 672 /* translators: 1: who, 2: what, 3: when */ 673 __('%1$s performed this action on %2$s.', 'divewp-boost-site-performance'), 674 esc_html($actor['user_login'] ?: __('Unknown user', 'divewp-boost-site-performance')), 675 esc_html($created_at_local) 676 ); 677 678 wp_send_json_success(array( 679 'event' => $event, 680 'plain_summary' => $plain_summary, 681 'created_at_local' => $created_at_local, 682 'actor' => $actor, 683 'channel' => $channel, 684 'source_context' => $source_context, 685 'target_context' => $target_context, 686 'event_type_label' => $this->get_event_type_label($event->event_type), 687 'action_label' => $this->get_action_label($event->event_action, $event->event_type), 688 )); 689 } 690 691 /** 568 692 * Get action class for event styling 569 693 * … … 573 697 */ 574 698 private function get_action_class($action) { 575 // Document the action-class mapping 699 // Document the action-class mapping (danger = red, warning = orange, success = green, info = blue) 576 700 $classes = array( 577 'created' => 'success', // Creation actions 578 'creation' => 'success', // User creation 579 'login' => 'success', // Login events 580 'activated' => 'success', // Plugin/theme activation 581 'installed' => 'success', // Installation events 582 'restored' => 'success', // Restoration events 583 'updated' => 'info', // Update actions 584 'update' => 'info', // General updates 585 'logout' => 'info', // Logout events 586 'trashed' => 'warning', // Trash actions 587 'unpublished' => 'warning', // Unpublish events 588 'deactivated' => 'warning', // Deactivation events 589 'deletion' => 'danger', // Deletion events 590 'deleted' => 'danger', // Delete actions 591 'authenticated' => 'info' // REST API authenticated access 701 'created' => 'success', // Creation actions 702 'creation' => 'success', // User creation 703 'login' => 'success', // Login events 704 'activated' => 'success', // Plugin/theme activation 705 'installed' => 'success', // Installation events 706 'restored' => 'success', // Restoration events 707 'approved' => 'success', // Comment approved 708 'updated' => 'info', // Update actions 709 'update' => 'info', // General updates 710 'logout' => 'info', // Logout events 711 'edited' => 'info', // Edit actions 712 'customized' => 'info', // Theme customization 713 'authenticated' => 'info', // REST API authenticated access 714 'core_updated' => 'info', // Core update 715 'role_changed' => 'info', // Role change 716 'trashed' => 'danger', // Trash (destructive) 717 'unpublished' => 'warning', // Unpublish events 718 'deactivated' => 'warning', // Deactivation events 719 'deletion' => 'danger', // User deletion 720 'deleted' => 'danger', // Delete actions 721 'deleted_permanently' => 'danger', // Permanent post deletion 722 'failed_login' => 'warning', // Failed login attempt 723 'file_edited' => 'warning', // File edit (security-sensitive) 724 'password_reset' => 'warning', // Password reset 592 725 ); 593 726
Note: See TracChangeset
for help on using the changeset viewer.