feat: Implement Intelligent Plugin Conflict Detector WordPress plugin #3835
21 changed files+5270−2
ModifiedREADME.md+118−2View fileUnifiedSplit
@@ -1,2 +1,118 @@
1# Intelligent-Plugin-Conflict-Detector
21. Intelligent Plugin Conflict Detector Store owners deal with their shops breaking whenever they update WooCommerce or install new plugins, and have to waste hours deactivating plugins one by one to find the culprit. They need an intelligent conflict detector that runs in the background, tests plugin combinations in a safe environment
1# Intelligent Plugin Conflict Detector
2
3A WordPress plugin that intelligently detects plugin conflicts, tests plugin
4combinations in a safe background environment, alerts store owners *before*
5conflicts cause downtime, and provides one-click rollback to a previously
6known-good state.
7
8## Features
9
10| Feature | Details |
11|---|---|
12| **Background Testing** | WP-Cron tests run every 6 hours (configurable) and also immediately after any plugin is activated or updated |
13| **Conflict Detection** | Probes the site home URL and the admin health endpoint for HTTP 5xx errors; scans the PHP error log for fatal errors introduced by recent plugin changes |
14| **Snapshot Manager** | Automatically captures the active-plugin list before every change; supports manual snapshots from the dashboard |
15| **One-Click Rollback** | Restore any snapshot with a single button; the current state is auto-snapshotted first so the rollback itself is reversible |
16| **Admin Dashboard** | Visual conflict list with severity badges, snapshot table, rollback history, and a "Run Test Now" button |
17| **Email Alerts** | Optional email notifications to the site admin whenever conflicts are detected |
18| **Auto Rollback** | Optional setting to automatically roll back when a critical conflict is detected |
19| **Plugins List Integration** | Highlights plugins with active conflicts directly on the Plugins screen |
20
21## Requirements
22
23* WordPress 5.8+
24* PHP 7.4+
25* WooCommerce (optional – the plugin works with any combination of plugins)
26
27## Installation
28
291. Upload the `intelligent-plugin-conflict-detector` folder to `/wp-content/plugins/`.
302. Activate the plugin through **Plugins → Installed Plugins**.
313. Navigate to **Tools → Conflict Detector** to view the dashboard.
32
33## Usage
34
35### Dashboard
36
37Open **Tools → Conflict Detector** in the WordPress admin:
38
39* **Conflicts tab** – lists all active (unresolved) conflicts with severity, plugin name, type, and message. Each conflict can be individually resolved. A *Clear All* button removes the entire log.
40* **Snapshots tab** – lists all captured plugin-state snapshots. Click **Rollback** next to any snapshot to restore that state.
41* **Rollback History tab** – audit trail of every rollback that has been performed.
42* **Settings link** – navigates to the settings page.
43
44### Settings
45
46Open **Tools → Conflict Detector → Settings** (or click the Settings tab):
47
48| Option | Default | Description |
49|---|---|---|
50| Email Notifications | Off | Send an email when conflicts are detected |
51| Alert Email Address | Admin email | Recipient for conflict alert emails |
52| Auto Rollback | Off | Automatically restore the previous snapshot on a critical conflict |
53| Scan Interval | 6 hours | How often the background scanner runs |
54
55### Running Tests Manually
56
57Click **Run Test Now** on the dashboard status bar. The detector will immediately probe the site home URL and admin health endpoint for HTTP errors and scan the error log, then display any new conflicts.
58
59### Creating Snapshots Manually
60
61Click **Create Snapshot** on the dashboard status bar. Enter an optional label and the current active-plugin list will be captured.
62
63## Developer Hooks
64
65| Hook | Type | Description |
66|---|---|---|
67| `ipcd_conflict_recorded` | Action | Fires after a conflict is recorded. Args: `$conflict` (array), `$conflict_id` (string) |
68| `ipcd_conflicts_detected` | Action | Fires after a test run finds conflicts. Args: `$plugin_basename`, `$event`, `$conflict_ids[]` |
69| `ipcd_before_test` | Action | Fires before a background test run. Args: `$plugin_basename`, `$event` |
70| `ipcd_after_test` | Action | Fires after a background test run. Args: `$plugin_basename`, `$event`, `$conflict_ids[]` |
71| `ipcd_rollback_complete` | Action | Fires after a successful rollback. Args: `$snapshot_id`, `$pre_rollback_id` |
72
73## Development
74
75```bash
76# Install PHP dependencies (PHPUnit + Brain\Monkey)
77cd intelligent-plugin-conflict-detector
78composer install
79
80# Run all unit tests
81./vendor/bin/phpunit
82```
83
84The test suite uses [Brain\Monkey](https://brain-wp.github.io/BrainMonkey/) to mock WordPress functions so no live WordPress installation is required.
85
86## File Structure
87
88```
89intelligent-plugin-conflict-detector/
90├── intelligent-plugin-conflict-detector.php Main plugin bootstrap
91├── includes/
92│ ├── class-plugin-state-manager.php Snapshot capture & restore
93│ ├── class-conflict-detector.php Conflict recording & retrieval
94│ ├── class-background-tester.php WP-Cron based background testing
95│ ├── class-rollback-manager.php One-click rollback
96│ └── class-notification-manager.php Admin notices & email alerts
97├── admin/
98│ ├── class-admin.php Admin menus & AJAX handlers
99│ ├── views/
100│ │ ├── dashboard.php Dashboard view
101│ │ └── settings.php Settings view
102│ └── assets/
103│ ├── css/admin.css
104│ └── js/admin.js
105├── tests/
106│ ├── bootstrap.php
107│ └── Unit/
108│ ├── ConflictDetectorTest.php
109│ ├── RollbackManagerTest.php
110│ ├── BackgroundTesterTest.php
111│ └── NotificationManagerTest.php
112├── composer.json
113└── phpunit.xml
114```
115
116## License
117
118GPL-2.0-or-later – see [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html).
Addedintelligent-plugin-conflict-detector/.gitignore+2−0View fileUnifiedSplit
@@ -0,0 +1,2 @@
1vendor/
2.phpunit.result.cache
Addedintelligent-plugin-conflict-detector/admin/assets/css/admin.css+240−0View fileUnifiedSplit
@@ -0,0 +1,240 @@
1/* ==========================================================================
2 Intelligent Plugin Conflict Detector — Admin Styles
3 ========================================================================== */
4
5/* Layout ------------------------------------------------------------------ */
6.ipcd-wrap {
7 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
8}
9
10.ipcd-page-title {
11 display: flex;
12 align-items: center;
13 gap: 8px;
14 font-size: 1.6rem;
15 margin-bottom: 16px;
16}
17
18.ipcd-logo {
19 font-size: 1.8rem;
20 line-height: 1;
21}
22
23.ipcd-version {
24 font-size: 0.75rem;
25 font-weight: 400;
26 color: #888;
27 background: #f0f0f0;
28 border-radius: 12px;
29 padding: 2px 8px;
30}
31
32/* Tabs -------------------------------------------------------------------- */
33.ipcd-tabs {
34 display: flex;
35 gap: 4px;
36 border-bottom: 2px solid #ddd;
37 margin-bottom: 24px;
38}
39
40.ipcd-tab {
41 padding: 8px 16px;
42 text-decoration: none;
43 color: #555;
44 border-radius: 4px 4px 0 0;
45 border: 2px solid transparent;
46 border-bottom: none;
47 margin-bottom: -2px;
48 display: inline-flex;
49 align-items: center;
50 gap: 6px;
51 font-weight: 500;
52 transition: color 0.15s, background 0.15s;
53}
54
55.ipcd-tab:hover {
56 color: #2271b1;
57 background: #f6f7f7;
58}
59
60.ipcd-tab.active {
61 color: #2271b1;
62 background: #fff;
63 border-color: #ddd #ddd #fff;
64}
65
66.ipcd-tab-content {
67 display: none;
68}
69
70.ipcd-tab-content.active {
71 display: block;
72}
73
74/* Status bar -------------------------------------------------------------- */
75.ipcd-status-bar {
76 display: flex;
77 align-items: center;
78 gap: 24px;
79 background: #f6f7f7;
80 border: 1px solid #ddd;
81 border-radius: 6px;
82 padding: 12px 16px;
83 margin-bottom: 20px;
84 flex-wrap: wrap;
85}
86
87.ipcd-status-item {
88 display: flex;
89 align-items: center;
90 gap: 6px;
91 font-size: 0.875rem;
92}
93
94.ipcd-status-actions {
95 margin-left: auto;
96 display: flex;
97 gap: 8px;
98 flex-wrap: wrap;
99}
100
101/* Pills ------------------------------------------------------------------- */
102.ipcd-pill {
103 display: inline-block;
104 padding: 2px 10px;
105 border-radius: 12px;
106 font-size: 0.75rem;
107 font-weight: 600;
108 text-transform: uppercase;
109 letter-spacing: 0.03em;
110}
111
112.ipcd-pill--green { background: #d8f3dc; color: #1a7431; }
113.ipcd-pill--red { background: #fce4e4; color: #c0392b; }
114.ipcd-pill--yellow { background: #fff3cd; color: #856404; }
115
116/* Badges ------------------------------------------------------------------ */
117.ipcd-badge {
118 display: inline-flex;
119 align-items: center;
120 justify-content: center;
121 min-width: 18px;
122 height: 18px;
123 padding: 0 5px;
124 border-radius: 9px;
125 background: #ddd;
126 color: #555;
127 font-size: 0.7rem;
128 font-weight: 700;
129}
130
131.ipcd-badge--error {
132 background: #d63638;
133 color: #fff;
134}
135
136/* Cards ------------------------------------------------------------------- */
137.ipcd-card {
138 background: #fff;
139 border: 1px solid #ddd;
140 border-radius: 6px;
141 margin-bottom: 20px;
142 overflow: hidden;
143}
144
145.ipcd-card-header {
146 display: flex;
147 align-items: center;
148 justify-content: space-between;
149 padding: 14px 16px;
150 border-bottom: 1px solid #eee;
151 background: #fafafa;
152}
153
154.ipcd-card-header h2 {
155 margin: 0;
156 font-size: 1rem;
157}
158
159.ipcd-card > table {
160 margin: 0;
161}
162
163/* Tables ------------------------------------------------------------------ */
164.ipcd-table th,
165.ipcd-table td {
166 padding: 10px 12px ;
167 vertical-align: middle ;
168}
169
170.ipcd-actions-cell {
171 display: flex;
172 gap: 6px;
173 align-items: center;
174}
175
176/* Severity rows ----------------------------------------------------------- */
177.ipcd-severity-critical td:first-child {
178 border-left: 4px solid #d63638;
179}
180
181.ipcd-severity-warning td:first-child {
182 border-left: 4px solid #f0b849;
183}
184
185.ipcd-severity-info td:first-child {
186 border-left: 4px solid #2271b1;
187}
188
189.ipcd-resolved-row {
190 opacity: 0.6;
191}
192
193/* Context pre ------------------------------------------------------------- */
194.ipcd-context-pre {
195 background: #f6f7f7;
196 border: 1px solid #ddd;
197 border-radius: 4px;
198 padding: 8px 12px;
199 font-size: 0.8rem;
200 overflow-x: auto;
201 margin: 6px 0 0;
202}
203
204/* Empty state ------------------------------------------------------------- */
205.ipcd-empty-state {
206 display: flex;
207 flex-direction: column;
208 align-items: center;
209 justify-content: center;
210 padding: 48px 24px;
211 text-align: center;
212 color: #888;
213}
214
215.ipcd-empty-icon {
216 font-size: 3rem;
217 margin-bottom: 12px;
218}
219
220/* Notice area ------------------------------------------------------------- */
221#ipcd-notice-area .notice {
222 margin: 0 0 16px;
223}
224
225/* Spinner ----------------------------------------------------------------- */
226.ipcd-spinner {
227 display: inline-block;
228 width: 14px;
229 height: 14px;
230 border: 2px solid #ccc;
231 border-top-color: #2271b1;
232 border-radius: 50%;
233 animation: ipcd-spin 0.6s linear infinite;
234 vertical-align: middle;
235 margin-right: 4px;
236}
237
238@keyframes ipcd-spin {
239 to { transform: rotate(360deg); }
240}
Addedintelligent-plugin-conflict-detector/admin/assets/js/admin.js+261−0View fileUnifiedSplit
@@ -0,0 +1,261 @@
1/* global jQuery, ipcdData */
2( function ( $ ) {
3 'use strict';
4
5 // -------------------------------------------------------------------------
6 // Tab switching
7 // -------------------------------------------------------------------------
8 $( '.ipcd-tab' ).on( 'click', function ( e ) {
9 var href = $( this ).attr( 'href' );
10 if ( ! href || href.charAt( 0 ) !== '#' ) {
11 return; // External link (settings page) – let it through.
12 }
13 e.preventDefault();
14 $( '.ipcd-tab' ).removeClass( 'active' );
15 $( this ).addClass( 'active' );
16 $( '.ipcd-tab-content' ).removeClass( 'active' );
17 $( href ).addClass( 'active' );
18 } );
19
20 // -------------------------------------------------------------------------
21 // Helpers
22 // -------------------------------------------------------------------------
23
24 /**
25 * Show a notice in the notice area.
26 *
27 * @param {string} message HTML message.
28 * @param {string} type 'success' | 'error' | 'warning' | 'info'.
29 */
30 function showNotice( message, type ) {
31 type = type || 'info';
32 var $notice = $( '<div class="notice notice-' + type + ' is-dismissible ipcd-notice"><p>' + message + '</p></div>' );
33 var $area = $( '#ipcd-notice-area' );
34 $area.empty().append( $notice );
35 // Scroll to the notice.
36 $( 'html, body' ).animate( { scrollTop: $area.offset().top - 40 }, 300 );
37 // Auto-dismiss after 5 s.
38 setTimeout( function () { $notice.fadeOut( 300, function () { $( this ).remove(); } ); }, 5000 );
39 }
40
41 /**
42 * Standard AJAX call wrapper.
43 *
44 * @param {string} action WP AJAX action.
45 * @param {Object} data Additional POST data.
46 * @param {Function} onSuccess Success callback (response.data).
47 * @param {Function} onError Error callback (response.data.message or WP_Error).
48 */
49 function ipcdAjax( action, data, onSuccess, onError ) {
50 var payload = $.extend( {}, data, {
51 action: action,
52 nonce: ipcdData.nonce,
53 } );
54
55 $.post( ipcdData.ajaxUrl, payload )
56 .done( function ( response ) {
57 if ( response && response.success ) {
58 if ( typeof onSuccess === 'function' ) {
59 onSuccess( response.data );
60 }
61 } else {
62 var msg = ( response && response.data && response.data.message )
63 ? response.data.message
64 : ipcdData.strings.error;
65 if ( typeof onError === 'function' ) {
66 onError( msg );
67 } else {
68 showNotice( msg, 'error' );
69 }
70 }
71 } )
72 .fail( function () {
73 var msg = ipcdData.strings.error;
74 if ( typeof onError === 'function' ) {
75 onError( msg );
76 } else {
77 showNotice( msg, 'error' );
78 }
79 } );
80 }
81
82 // -------------------------------------------------------------------------
83 // Rollback
84 // -------------------------------------------------------------------------
85 $( document ).on( 'click', '.ipcd-rollback-btn', function () {
86 var snapshotId = $( this ).data( 'snapshot-id' );
87 if ( ! window.confirm( ipcdData.strings.confirmRollback ) ) {
88 return;
89 }
90
91 var $btn = $( this );
92 $btn.prop( 'disabled', true ).html( '<span class="ipcd-spinner"></span>' + ipcdData.strings.rollingBack );
93
94 ipcdAjax(
95 'ipcd_rollback',
96 { snapshot_id: snapshotId },
97 function ( data ) {
98 showNotice( data.message, 'success' );
99 $btn.prop( 'disabled', false ).text( '⏪ Rollback' );
100 },
101 function ( msg ) {
102 showNotice( msg, 'error' );
103 $btn.prop( 'disabled', false ).text( '⏪ Rollback' );
104 }
105 );
106 } );
107
108 // -------------------------------------------------------------------------
109 // Resolve conflict
110 // -------------------------------------------------------------------------
111 $( document ).on( 'click', '.ipcd-resolve-btn', function () {
112 var conflictId = $( this ).data( 'conflict-id' );
113 var $row = $( this ).closest( 'tr' );
114 var $btn = $( this );
115
116 $btn.prop( 'disabled', true );
117
118 ipcdAjax(
119 'ipcd_resolve_conflict',
120 { conflict_id: conflictId },
121 function ( data ) {
122 showNotice( data.message, 'success' );
123 $row.fadeOut( 300, function () { $( this ).remove(); } );
124 // Update badge count.
125 var $badge = $( '.ipcd-tab[href="#tab-conflicts"] .ipcd-badge--error' );
126 var count = parseInt( $badge.text(), 10 ) - 1;
127 if ( count > 0 ) {
128 $badge.text( count );
129 } else {
130 $badge.remove();
131 }
132 },
133 function ( msg ) {
134 showNotice( msg, 'error' );
135 $btn.prop( 'disabled', false );
136 }
137 );
138 } );
139
140 // -------------------------------------------------------------------------
141 // Run test now
142 // -------------------------------------------------------------------------
143 $( '#ipcd-run-test-btn' ).on( 'click', function () {
144 var $btn = $( this );
145 $btn.prop( 'disabled', true ).html( '<span class="ipcd-spinner"></span>' + ipcdData.strings.runningTest );
146
147 ipcdAjax(
148 'ipcd_run_test',
149 { plugin: '', event: 'manual' },
150 function ( data ) {
151 showNotice( data.message, 'success' );
152 $btn.prop( 'disabled', false ).text( 'Run Test Now' );
153 },
154 function ( msg ) {
155 showNotice( msg, 'error' );
156 $btn.prop( 'disabled', false ).text( 'Run Test Now' );
157 }
158 );
159 } );
160
161 // -------------------------------------------------------------------------
162 // Create snapshot
163 // -------------------------------------------------------------------------
164 $( '#ipcd-create-snapshot-btn' ).on( 'click', function () {
165 var label = window.prompt( 'Enter a label for this snapshot (optional):', 'manual_snapshot' );
166 if ( null === label ) {
167 return; // Cancelled.
168 }
169
170 var $btn = $( this );
171 $btn.prop( 'disabled', true );
172
173 ipcdAjax(
174 'ipcd_create_snapshot',
175 { label: label || 'manual_snapshot' },
176 function ( data ) {
177 showNotice( data.message + ' ID: ' + data.snapshot_id, 'success' );
178 $btn.prop( 'disabled', false );
179 // Reload to show the new snapshot.
180 setTimeout( function () { window.location.reload(); }, 1500 );
181 },
182 function ( msg ) {
183 showNotice( msg, 'error' );
184 $btn.prop( 'disabled', false );
185 }
186 );
187 } );
188
189 // -------------------------------------------------------------------------
190 // Delete snapshot
191 // -------------------------------------------------------------------------
192 $( document ).on( 'click', '.ipcd-delete-snapshot-btn', function () {
193 if ( ! window.confirm( 'Delete this snapshot? This cannot be undone.' ) ) {
194 return;
195 }
196
197 var snapshotId = $( this ).data( 'snapshot-id' );
198 var $row = $( this ).closest( 'tr' );
199
200 ipcdAjax(
201 'ipcd_delete_snapshot',
202 { snapshot_id: snapshotId },
203 function ( data ) {
204 showNotice( data.message, 'success' );
205 $row.fadeOut( 300, function () { $( this ).remove(); } );
206 }
207 );
208 } );
209
210 // -------------------------------------------------------------------------
211 // Clear all conflicts
212 // -------------------------------------------------------------------------
213 $( '#ipcd-clear-conflicts-btn, #ipcd-clear-all-conflicts-btn' ).on( 'click', function () {
214 if ( ! window.confirm( ipcdData.strings.confirmClear ) ) {
215 return;
216 }
217
218 ipcdAjax(
219 'ipcd_clear_conflicts',
220 {},
221 function ( data ) {
222 showNotice( data.message, 'success' );
223 setTimeout( function () { window.location.reload(); }, 1500 );
224 }
225 );
226 } );
227
228 // -------------------------------------------------------------------------
229 // Save settings
230 // -------------------------------------------------------------------------
231 $( '#ipcd-settings-form' ).on( 'submit', function ( e ) {
232 e.preventDefault();
233
234 var $btn = $( '#ipcd-save-settings-btn' );
235 var data = {};
236
237 $( this ).serializeArray().forEach( function ( item ) {
238 data[ item.name ] = item.value;
239 } );
240
241 // Include unchecked checkboxes as 0.
242 if ( ! data.email_notifications ) { data.email_notifications = 0; }
243 if ( ! data.auto_rollback ) { data.auto_rollback = 0; }
244
245 $btn.prop( 'disabled', true ).text( ipcdData.strings.saving );
246
247 ipcdAjax(
248 'ipcd_save_settings',
249 data,
250 function ( resp ) {
251 showNotice( resp.message, 'success' );
252 $btn.prop( 'disabled', false ).text( 'Save Settings' );
253 },
254 function ( msg ) {
255 showNotice( msg, 'error' );
256 $btn.prop( 'disabled', false ).text( 'Save Settings' );
257 }
258 );
259 } );
260
261} )( jQuery );
Addedintelligent-plugin-conflict-detector/admin/class-admin.php+347−0View fileUnifiedSplit
@@ -0,0 +1,347 @@
1
2/**
3 * Admin Class
4 *
5 * Registers admin menus, AJAX handlers and settings page for the IPCD plugin.
6 *
7 * @package IPCD
8 */
9
10if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12}
13
14/**
15 * Class IPCD_Admin
16 */
17class IPCD_Admin {
18
19 /** @var IPCD_Conflict_Detector */
20 private $detector;
21
22 /** @var IPCD_Rollback_Manager */
23 private $rollback;
24
25 /** @var IPCD_Notification_Manager */
26 private $notifications;
27
28 /** @var IPCD_Background_Tester */
29 private $tester;
30
31 /** @var IPCD_Plugin_State_Manager */
32 private $state_manager;
33
34 /**
35 * Constructor – wire admin hooks.
36 *
37 * @param IPCD_Conflict_Detector $detector Conflict detector.
38 * @param IPCD_Rollback_Manager $rollback Rollback manager.
39 * @param IPCD_Notification_Manager $notifications Notification manager.
40 * @param IPCD_Background_Tester $tester Background tester.
41 * @param IPCD_Plugin_State_Manager $state_manager State manager.
42 */
43 public function __construct(
44 IPCD_Conflict_Detector $detector,
45 IPCD_Rollback_Manager $rollback,
46 IPCD_Notification_Manager $notifications,
47 IPCD_Background_Tester $tester,
48 IPCD_Plugin_State_Manager $state_manager
49 ) {
50 $this->detector = $detector;
51 $this->rollback = $rollback;
52 $this->notifications = $notifications;
53 $this->tester = $tester;
54 $this->state_manager = $state_manager;
55
56 add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
57 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
58
59 // AJAX handlers.
60 add_action( 'wp_ajax_ipcd_rollback', array( $this, 'ajax_rollback' ) );
61 add_action( 'wp_ajax_ipcd_resolve_conflict', array( $this, 'ajax_resolve_conflict' ) );
62 add_action( 'wp_ajax_ipcd_run_test', array( $this, 'ajax_run_test' ) );
63 add_action( 'wp_ajax_ipcd_save_settings', array( $this, 'ajax_save_settings' ) );
64 add_action( 'wp_ajax_ipcd_create_snapshot', array( $this, 'ajax_create_snapshot' ) );
65 add_action( 'wp_ajax_ipcd_delete_snapshot', array( $this, 'ajax_delete_snapshot' ) );
66 add_action( 'wp_ajax_ipcd_clear_conflicts', array( $this, 'ajax_clear_all_conflicts' ) );
67
68 // Lightweight health-ping endpoint (no auth required – used by the background tester).
69 add_action( 'wp_ajax_nopriv_ipcd_health_ping', array( $this, 'health_ping' ) );
70 add_action( 'wp_ajax_ipcd_health_ping', array( $this, 'health_ping' ) );
71
72 // Admin columns on Plugins page.
73 add_filter( 'plugin_action_links', array( $this, 'add_plugin_action_links' ), 10, 2 );
74 }
75
76 // -------------------------------------------------------------------------
77 // Menu & Assets
78 // -------------------------------------------------------------------------
79
80 /**
81 * Register the admin menu pages.
82 */
83 public function register_admin_menu(): void {
84 add_management_page(
85 __( 'Plugin Conflict Detector', 'ipcd' ),
86 __( 'Conflict Detector', 'ipcd' ),
87 'manage_options',
88 'ipcd-dashboard',
89 array( $this, 'render_dashboard' )
90 );
91
92 add_submenu_page(
93 null, // Hidden submenu (accessed via tab on dashboard page).
94 __( 'IPCD Settings', 'ipcd' ),
95 __( 'Settings', 'ipcd' ),
96 'manage_options',
97 'ipcd-settings',
98 array( $this, 'render_settings' )
99 );
100 }
101
102 /**
103 * Enqueue admin CSS and JS on IPCD pages.
104 *
105 * @param string $hook Current admin page hook.
106 */
107 public function enqueue_assets( string $hook ): void {
108 $ipcd_pages = array( 'tools_page_ipcd-dashboard', 'admin_page_ipcd-settings' );
109 if ( ! in_array( $hook, $ipcd_pages, true ) ) {
110 return;
111 }
112
113 wp_enqueue_style(
114 'ipcd-admin',
115 IPCD_PLUGIN_URL . 'admin/assets/css/admin.css',
116 array(),
117 IPCD_VERSION
118 );
119
120 wp_enqueue_script(
121 'ipcd-admin',
122 IPCD_PLUGIN_URL . 'admin/assets/js/admin.js',
123 array( 'jquery' ),
124 IPCD_VERSION,
125 true
126 );
127
128 wp_localize_script(
129 'ipcd-admin',
130 'ipcdData',
131 array(
132 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
133 'nonce' => wp_create_nonce( 'ipcd_nonce' ),
134 'strings' => array(
135 'confirmRollback' => __( 'Are you sure you want to rollback to this snapshot? Your current plugin state will be saved first.', 'ipcd' ),
136 'confirmClear' => __( 'Are you sure you want to clear all conflict records?', 'ipcd' ),
137 'rollingBack' => __( 'Rolling back…', 'ipcd' ),
138 'runningTest' => __( 'Running test…', 'ipcd' ),
139 'saving' => __( 'Saving…', 'ipcd' ),
140 'done' => __( 'Done!', 'ipcd' ),
141 'error' => __( 'An error occurred. Please try again.', 'ipcd' ),
142 ),
143 )
144 );
145 }
146
147 // -------------------------------------------------------------------------
148 // View renderers
149 // -------------------------------------------------------------------------
150
151 /**
152 * Render the dashboard page.
153 */
154 public function render_dashboard(): void {
155 if ( ! current_user_can( 'manage_options' ) ) {
156 wp_die( esc_html__( 'You do not have permission to access this page.', 'ipcd' ) );
157 }
158 include IPCD_PLUGIN_DIR . 'admin/views/dashboard.php';
159 }
160
161 /**
162 * Render the settings page.
163 */
164 public function render_settings(): void {
165 if ( ! current_user_can( 'manage_options' ) ) {
166 wp_die( esc_html__( 'You do not have permission to access this page.', 'ipcd' ) );
167 }
168 include IPCD_PLUGIN_DIR . 'admin/views/settings.php';
169 }
170
171 // -------------------------------------------------------------------------
172 // AJAX handlers
173 // -------------------------------------------------------------------------
174
175 /**
176 * AJAX: Perform a rollback.
177 */
178 public function ajax_rollback(): void {
179 $this->verify_ajax_nonce();
180
181 $snapshot_id = sanitize_text_field( $_POST['snapshot_id'] ?? '' );
182 if ( ! $snapshot_id ) {
183 wp_send_json_error( array( 'message' => __( 'Missing snapshot ID.', 'ipcd' ) ) );
184 }
185
186 $result = $this->rollback->rollback( $snapshot_id );
187 if ( $result['success'] ) {
188 wp_send_json_success( $result );
189 } else {
190 wp_send_json_error( $result );
191 }
192 }
193
194 /**
195 * AJAX: Resolve a conflict.
196 */
197 public function ajax_resolve_conflict(): void {
198 $this->verify_ajax_nonce();
199
200 $conflict_id = sanitize_text_field( $_POST['conflict_id'] ?? '' );
201 if ( ! $conflict_id ) {
202 wp_send_json_error( array( 'message' => __( 'Missing conflict ID.', 'ipcd' ) ) );
203 }
204
205 $resolved = $this->detector->resolve_conflict( $conflict_id );
206 if ( $resolved ) {
207 wp_send_json_success( array( 'message' => __( 'Conflict marked as resolved.', 'ipcd' ) ) );
208 } else {
209 wp_send_json_error( array( 'message' => __( 'Conflict not found.', 'ipcd' ) ) );
210 }
211 }
212
213 /**
214 * AJAX: Manually trigger a background test run.
215 */
216 public function ajax_run_test(): void {
217 $this->verify_ajax_nonce();
218
219 $plugin = sanitize_text_field( $_POST['plugin'] ?? '' );
220 $event = sanitize_key( $_POST['event'] ?? 'manual' );
221
222 if ( ! $plugin ) {
223 // Run a full-site health probe with no specific plugin attributed.
224 $plugin = 'manual-check';
225 }
226
227 $this->tester->run_single_test( $plugin, $event );
228
229 wp_send_json_success( array( 'message' => __( 'Test completed. Refresh the page to see results.', 'ipcd' ) ) );
230 }
231
232 /**
233 * AJAX: Save settings.
234 */
235 public function ajax_save_settings(): void {
236 $this->verify_ajax_nonce();
237
238 $this->notifications->save_settings( $_POST );
239 wp_send_json_success( array( 'message' => __( 'Settings saved.', 'ipcd' ) ) );
240 }
241
242 /**
243 * AJAX: Create a manual snapshot.
244 */
245 public function ajax_create_snapshot(): void {
246 $this->verify_ajax_nonce();
247
248 $label = sanitize_text_field( $_POST['label'] ?? 'manual_snapshot' );
249 $snapshot_id = $this->state_manager->capture_snapshot( $label );
250 wp_send_json_success(
251 array(
252 'snapshot_id' => $snapshot_id,
253 'message' => __( 'Snapshot created successfully.', 'ipcd' ),
254 )
255 );
256 }
257
258 /**
259 * AJAX: Delete a snapshot.
260 */
261 public function ajax_delete_snapshot(): void {
262 $this->verify_ajax_nonce();
263
264 $snapshot_id = sanitize_text_field( $_POST['snapshot_id'] ?? '' );
265 if ( ! $snapshot_id ) {
266 wp_send_json_error( array( 'message' => __( 'Missing snapshot ID.', 'ipcd' ) ) );
267 }
268
269 $deleted = $this->state_manager->delete_snapshot( $snapshot_id );
270 if ( $deleted ) {
271 wp_send_json_success( array( 'message' => __( 'Snapshot deleted.', 'ipcd' ) ) );
272 } else {
273 wp_send_json_error( array( 'message' => __( 'Snapshot not found.', 'ipcd' ) ) );
274 }
275 }
276
277 /**
278 * AJAX: Clear all conflict records.
279 */
280 public function ajax_clear_all_conflicts(): void {
281 $this->verify_ajax_nonce();
282
283 $this->detector->clear_all_conflicts();
284 wp_send_json_success( array( 'message' => __( 'All conflicts cleared.', 'ipcd' ) ) );
285 }
286
287 /**
288 * Health ping endpoint – returns 200 OK with a JSON body.
289 */
290 public function health_ping(): void {
291 wp_send_json_success( array( 'status' => 'ok' ) );
292 }
293
294 // -------------------------------------------------------------------------
295 // Plugins list enhancement
296 // -------------------------------------------------------------------------
297
298 /**
299 * Add "View Conflicts" link to plugin action links if conflicts exist.
300 *
301 * @param array $actions Existing action links.
302 * @param string $plugin_file Plugin basename.
303 *
304 * @return array
305 */
306 public function add_plugin_action_links( array $actions, string $plugin_file ): array {
307 $conflicts = array_filter(
308 $this->detector->get_active_conflicts(),
309 static function ( $c ) use ( $plugin_file ) {
310 return $c['plugin'] === $plugin_file;
311 }
312 );
313
314 if ( ! empty( $conflicts ) ) {
315 $count = count( $conflicts );
316 $url = admin_url( 'tools.php?page=ipcd-dashboard' );
317 $actions['ipcd'] = sprintf(
318 '<a href="%s" style="color:#d63638;font-weight:600;">⚠ %s</a>',
319 esc_url( $url ),
320 sprintf(
321 /* translators: %d: number of conflicts */
322 _n( '%d Conflict', '%d Conflicts', $count, 'ipcd' ),
323 $count
324 )
325 );
326 }
327
328 return $actions;
329 }
330
331 // -------------------------------------------------------------------------
332 // Private helpers
333 // -------------------------------------------------------------------------
334
335 /**
336 * Verify the AJAX nonce and capability, die on failure.
337 */
338 private function verify_ajax_nonce(): void {
339 if (
340 ! check_ajax_referer( 'ipcd_nonce', 'nonce', false ) ||
341 ! current_user_can( 'manage_options' )
342 ) {
343 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'ipcd' ) ), 403 );
344 wp_die();
345 }
346 }
347}
Addedintelligent-plugin-conflict-detector/admin/views/dashboard.php+267−0View fileUnifiedSplit
@@ -0,0 +1,267 @@
1
2/**
3 * Dashboard view
4 *
5 * @package IPCD
6 * @var IPCD_Conflict_Detector $detector
7 * @var IPCD_Rollback_Manager $rollback
8 * @var IPCD_Background_Tester $tester
9 * @var IPCD_Plugin_State_Manager $state_manager
10 */
11
12if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14}
15
16// Make objects available inside the view.
17$detector = ipcd()->conflict_detector;
18$rollback = ipcd()->rollback_manager;
19$tester = ipcd()->background_tester;
20$state_manager = ipcd()->state_manager;
21
22$active_conflicts = $detector->get_active_conflicts();
23$all_conflicts = $detector->get_conflicts();
24$snapshots = $state_manager->get_snapshots();
25$rollback_history = $rollback->get_history();
26$next_run = $tester->get_next_run();
27$queue = $tester->get_queue();
28
29$severity_icons = array(
30 'critical' => '🔴',
31 'warning' => '🟡',
32 'info' => '🔵',
33);
34
35<div class="wrap ipcd-wrap">
36 <h1 class="ipcd-page-title">
37 <span class="ipcd-logo">🔍</span>
38 <?php esc_html_e( 'Intelligent Plugin Conflict Detector', 'ipcd' ); ?>
39 <span class="ipcd-version">v<?php echo esc_html( IPCD_VERSION ); ?></span>
40 </h1>
41
42 <nav class="ipcd-tabs">
43 <a href="#tab-conflicts" class="ipcd-tab active"><?php esc_html_e( 'Conflicts', 'ipcd' ); ?>
44 <?php if ( ! empty( $active_conflicts ) ) : ?>
45 <span class="ipcd-badge ipcd-badge--error"><?php echo esc_html( count( $active_conflicts ) ); ?></span>
46 <?php endif; ?>
47 </a>
48 <a href="#tab-snapshots" class="ipcd-tab"><?php esc_html_e( 'Snapshots', 'ipcd' ); ?>
49 <span class="ipcd-badge"><?php echo esc_html( count( $snapshots ) ); ?></span>
50 </a>
51 <a href="#tab-history" class="ipcd-tab"><?php esc_html_e( 'Rollback History', 'ipcd' ); ?></a>
52 <a href="<?php echo esc_url( admin_url( 'admin.php?page=ipcd-settings' ) ); ?>" class="ipcd-tab"><?php esc_html_e( 'Settings', 'ipcd' ); ?></a>
53 </nav>
54
55 <!-- STATUS BAR -->
56 <div class="ipcd-status-bar">
57 <div class="ipcd-status-item">
58 <strong><?php esc_html_e( 'Background Testing:', 'ipcd' ); ?></strong>
59 <span class="ipcd-pill ipcd-pill--green"><?php esc_html_e( 'Active', 'ipcd' ); ?></span>
60 </div>
61 <div class="ipcd-status-item">
62 <strong><?php esc_html_e( 'Next Scan:', 'ipcd' ); ?></strong>
63 <?php echo $next_run ? esc_html( human_time_diff( time(), $next_run ) . ' ' . __( 'from now', 'ipcd' ) ) : esc_html__( 'Not scheduled', 'ipcd' ); ?>
64 </div>
65 <div class="ipcd-status-item">
66 <strong><?php esc_html_e( 'Queue:', 'ipcd' ); ?></strong>
67 <?php
68 $queue_count = count( $queue );
69 /* translators: %d: number of pending tests */
70 echo esc_html( sprintf( _n( '%d pending test', '%d pending tests', $queue_count, 'ipcd' ), $queue_count ) );
71 ?>
72 </div>
73 <div class="ipcd-status-actions">
74 <button id="ipcd-run-test-btn" class="button button-secondary" data-plugin="" data-event="manual">
75 <?php esc_html_e( 'Run Test Now', 'ipcd' ); ?>
76 </button>
77 <button id="ipcd-create-snapshot-btn" class="button button-secondary">
78 <?php esc_html_e( 'Create Snapshot', 'ipcd' ); ?>
79 </button>
80 </div>
81 </div>
82
83 <div id="ipcd-notice-area"></div>
84
85 <!-- TAB: CONFLICTS -->
86 <div id="tab-conflicts" class="ipcd-tab-content active">
87 <div class="ipcd-card">
88 <div class="ipcd-card-header">
89 <h2><?php esc_html_e( 'Active Conflicts', 'ipcd' ); ?></h2>
90 <?php if ( ! empty( $active_conflicts ) ) : ?>
91 <button id="ipcd-clear-conflicts-btn" class="button button-link-delete">
92 <?php esc_html_e( 'Clear All', 'ipcd' ); ?>
93 </button>
94 <?php endif; ?>
95 </div>
96
97 <?php if ( empty( $active_conflicts ) ) : ?>
98 <div class="ipcd-empty-state">
99 <span class="ipcd-empty-icon">✅</span>
100 <p><?php esc_html_e( 'No active conflicts detected. Your site looks healthy!', 'ipcd' ); ?></p>
101 </div>
102 <?php else : ?>
103 <table class="wp-list-table widefat fixed striped ipcd-table">
104 <thead>
105 <tr>
106 <th><?php esc_html_e( 'Severity', 'ipcd' ); ?></th>
107 <th><?php esc_html_e( 'Plugin', 'ipcd' ); ?></th>
108 <th><?php esc_html_e( 'Type', 'ipcd' ); ?></th>
109 <th><?php esc_html_e( 'Message', 'ipcd' ); ?></th>
110 <th><?php esc_html_e( 'Detected', 'ipcd' ); ?></th>
111 <th><?php esc_html_e( 'Actions', 'ipcd' ); ?></th>
112 </tr>
113 </thead>
114 <tbody>
115 <?php foreach ( $active_conflicts as $conflict ) : ?>
116 <tr class="ipcd-conflict-row ipcd-severity-<?php echo esc_attr( $conflict['severity'] ); ?>" data-conflict-id="<?php echo esc_attr( $conflict['id'] ); ?>">
117 <td>
118 <?php echo esc_html( $severity_icons[ $conflict['severity'] ] ?? '⚪' ); ?>
119 <strong><?php echo esc_html( ucfirst( $conflict['severity'] ) ); ?></strong>
120 </td>
121 <td><code><?php echo esc_html( $conflict['plugin'] ); ?></code></td>
122 <td><code><?php echo esc_html( $conflict['type'] ); ?></code></td>
123 <td><?php echo esc_html( $conflict['message'] ); ?></td>
124 <td><?php echo esc_html( human_time_diff( $conflict['time'], time() ) . ' ago' ); ?></td>
125 <td>
126 <button class="button button-small ipcd-resolve-btn" data-conflict-id="<?php echo esc_attr( $conflict['id'] ); ?>">
127 <?php esc_html_e( 'Resolve', 'ipcd' ); ?>
128 </button>
129 </td>
130 </tr>
131 <?php if ( ! empty( $conflict['context'] ) ) : ?>
132 <tr class="ipcd-context-row">
133 <td colspan="6">
134 <details>
135 <summary><?php esc_html_e( 'Context', 'ipcd' ); ?></summary>
136 <pre class="ipcd-context-pre"><?php echo esc_html( wp_json_encode( $conflict['context'], JSON_PRETTY_PRINT ) ); ?></pre>
137 </details>
138 </td>
139 </tr>
140 <?php endif; ?>
141 <?php endforeach; ?>
142 </tbody>
143 </table>
144 <?php endif; ?>
145 </div>
146
147 <?php if ( count( $all_conflicts ) > count( $active_conflicts ) ) : ?>
148 <div class="ipcd-card">
149 <div class="ipcd-card-header">
150 <h2><?php esc_html_e( 'Resolved Conflicts', 'ipcd' ); ?></h2>
151 </div>
152 <table class="wp-list-table widefat fixed striped ipcd-table">
153 <thead>
154 <tr>
155 <th><?php esc_html_e( 'Plugin', 'ipcd' ); ?></th>
156 <th><?php esc_html_e( 'Type', 'ipcd' ); ?></th>
157 <th><?php esc_html_e( 'Message', 'ipcd' ); ?></th>
158 <th><?php esc_html_e( 'Detected', 'ipcd' ); ?></th>
159 </tr>
160 </thead>
161 <tbody>
162 <?php foreach ( $all_conflicts as $conflict ) : ?>
163 <?php if ( ! empty( $conflict['resolved'] ) ) : ?>
164 <tr class="ipcd-resolved-row">
165 <td><code><?php echo esc_html( $conflict['plugin'] ); ?></code></td>
166 <td><code><?php echo esc_html( $conflict['type'] ); ?></code></td>
167 <td><?php echo esc_html( $conflict['message'] ); ?></td>
168 <td><?php echo esc_html( human_time_diff( $conflict['time'], time() ) . ' ago' ); ?></td>
169 </tr>
170 <?php endif; ?>
171 <?php endforeach; ?>
172 </tbody>
173 </table>
174 </div>
175 <?php endif; ?>
176 </div>
177
178 <!-- TAB: SNAPSHOTS -->
179 <div id="tab-snapshots" class="ipcd-tab-content">
180 <div class="ipcd-card">
181 <div class="ipcd-card-header">
182 <h2><?php esc_html_e( 'Plugin State Snapshots', 'ipcd' ); ?></h2>
183 <p class="description"><?php esc_html_e( 'Snapshots capture the active plugin list at a point in time. Use them to roll back if a plugin causes issues.', 'ipcd' ); ?></p>
184 </div>
185
186 <?php if ( empty( $snapshots ) ) : ?>
187 <div class="ipcd-empty-state">
188 <span class="ipcd-empty-icon">📷</span>
189 <p><?php esc_html_e( 'No snapshots yet. Snapshots are created automatically when plugins are activated or updated, or you can create one manually.', 'ipcd' ); ?></p>
190 </div>
191 <?php else : ?>
192 <table class="wp-list-table widefat fixed striped ipcd-table">
193 <thead>
194 <tr>
195 <th><?php esc_html_e( 'Label', 'ipcd' ); ?></th>
196 <th><?php esc_html_e( 'Snapshot ID', 'ipcd' ); ?></th>
197 <th><?php esc_html_e( 'Created', 'ipcd' ); ?></th>
198 <th><?php esc_html_e( 'Actions', 'ipcd' ); ?></th>
199 </tr>
200 </thead>
201 <tbody>
202 <?php foreach ( $snapshots as $snapshot ) : ?>
203 <tr>
204 <td><?php echo esc_html( $snapshot['label'] ?: __( '(manual)', 'ipcd' ) ); ?></td>
205 <td><code><?php echo esc_html( $snapshot['snapshot_id'] ); ?></code></td>
206 <td><?php echo esc_html( $snapshot['created_at'] ); ?></td>
207 <td class="ipcd-actions-cell">
208 <button class="button button-primary ipcd-rollback-btn"
209 data-snapshot-id="<?php echo esc_attr( $snapshot['snapshot_id'] ); ?>">
210 ⏪ <?php esc_html_e( 'Rollback', 'ipcd' ); ?>
211 </button>
212 <button class="button button-link-delete ipcd-delete-snapshot-btn"
213 data-snapshot-id="<?php echo esc_attr( $snapshot['snapshot_id'] ); ?>">
214 <?php esc_html_e( 'Delete', 'ipcd' ); ?>
215 </button>
216 </td>
217 </tr>
218 <?php endforeach; ?>
219 </tbody>
220 </table>
221 <?php endif; ?>
222 </div>
223 </div>
224
225 <!-- TAB: ROLLBACK HISTORY -->
226 <div id="tab-history" class="ipcd-tab-content">
227 <div class="ipcd-card">
228 <div class="ipcd-card-header">
229 <h2><?php esc_html_e( 'Rollback History', 'ipcd' ); ?></h2>
230 </div>
231
232 <?php if ( empty( $rollback_history ) ) : ?>
233 <div class="ipcd-empty-state">
234 <span class="ipcd-empty-icon">📋</span>
235 <p><?php esc_html_e( 'No rollbacks have been performed yet.', 'ipcd' ); ?></p>
236 </div>
237 <?php else : ?>
238 <table class="wp-list-table widefat fixed striped ipcd-table">
239 <thead>
240 <tr>
241 <th><?php esc_html_e( 'Restored Snapshot', 'ipcd' ); ?></th>
242 <th><?php esc_html_e( 'Safety Snapshot', 'ipcd' ); ?></th>
243 <th><?php esc_html_e( 'Status', 'ipcd' ); ?></th>
244 <th><?php esc_html_e( 'Time', 'ipcd' ); ?></th>
245 </tr>
246 </thead>
247 <tbody>
248 <?php foreach ( $rollback_history as $entry ) : ?>
249 <tr>
250 <td><code><?php echo esc_html( $entry['snapshot_id'] ); ?></code></td>
251 <td><code><?php echo esc_html( $entry['pre_rollback_id'] ); ?></code></td>
252 <td>
253 <?php if ( 'success' === $entry['status'] ) : ?>
254 <span class="ipcd-pill ipcd-pill--green"><?php esc_html_e( 'Success', 'ipcd' ); ?></span>
255 <?php else : ?>
256 <span class="ipcd-pill ipcd-pill--red"><?php esc_html_e( 'Failed', 'ipcd' ); ?></span>
257 <?php endif; ?>
258 </td>
259 <td><?php echo esc_html( human_time_diff( $entry['time'], time() ) . ' ago' ); ?></td>
260 </tr>
261 <?php endforeach; ?>
262 </tbody>
263 </table>
264 <?php endif; ?>
265 </div>
266 </div>
267</div>
Addedintelligent-plugin-conflict-detector/admin/views/settings.php+116−0View fileUnifiedSplit
@@ -0,0 +1,116 @@
1
2/**
3 * Settings view
4 *
5 * @package IPCD
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10}
11
12$settings = ipcd()->notification_manager->get_settings();
13
14<div class="wrap ipcd-wrap">
15 <h1 class="ipcd-page-title">
16 <span class="ipcd-logo">⚙️</span>
17 <?php esc_html_e( 'Conflict Detector — Settings', 'ipcd' ); ?>
18 </h1>
19
20 <div id="ipcd-notice-area"></div>
21
22 <div class="ipcd-card" style="max-width:700px;">
23 <form id="ipcd-settings-form">
24 <table class="form-table">
25 <tbody>
26 <tr>
27 <th scope="row">
28 <label for="ipcd_email_notifications">
29 <?php esc_html_e( 'Email Notifications', 'ipcd' ); ?>
30 </label>
31 </th>
32 <td>
33 <label>
34 <input type="checkbox"
35 id="ipcd_email_notifications"
36 name="email_notifications"
37 value="1"
38 <?php checked( $settings['email_notifications'] ); ?> />
39 <?php esc_html_e( 'Send email alerts when conflicts are detected', 'ipcd' ); ?>
40 </label>
41 </td>
42 </tr>
43 <tr>
44 <th scope="row">
45 <label for="ipcd_email_address">
46 <?php esc_html_e( 'Alert Email Address', 'ipcd' ); ?>
47 </label>
48 </th>
49 <td>
50 <input type="email"
51 id="ipcd_email_address"
52 name="email_address"
53 class="regular-text"
54 value="<?php echo esc_attr( $settings['email_address'] ); ?>" />
55 <p class="description"><?php esc_html_e( 'Defaults to the WordPress admin email.', 'ipcd' ); ?></p>
56 </td>
57 </tr>
58 <tr>
59 <th scope="row">
60 <label for="ipcd_auto_rollback">
61 <?php esc_html_e( 'Auto Rollback', 'ipcd' ); ?>
62 </label>
63 </th>
64 <td>
65 <label>
66 <input type="checkbox"
67 id="ipcd_auto_rollback"
68 name="auto_rollback"
69 value="1"
70 <?php checked( $settings['auto_rollback'] ); ?> />
71 <?php esc_html_e( 'Automatically roll back when a critical conflict is detected', 'ipcd' ); ?>
72 </label>
73 <p class="description">
74 <?php esc_html_e( 'Use with caution. This will restore the previous plugin state immediately after a critical conflict is found.', 'ipcd' ); ?>
75 </p>
76 </td>
77 </tr>
78 <tr>
79 <th scope="row">
80 <label for="ipcd_scan_interval">
81 <?php esc_html_e( 'Scan Interval (hours)', 'ipcd' ); ?>
82 </label>
83 </th>
84 <td>
85 <input type="number"
86 id="ipcd_scan_interval"
87 name="scan_interval_hours"
88 class="small-text"
89 min="1"
90 max="168"
91 value="<?php echo esc_attr( $settings['scan_interval_hours'] ); ?>" />
92 <p class="description"><?php esc_html_e( 'How often to run scheduled background tests. Minimum 1 hour.', 'ipcd' ); ?></p>
93 </td>
94 </tr>
95 </tbody>
96 </table>
97
98 <p class="submit">
99 <button id="ipcd-save-settings-btn" type="submit" class="button button-primary">
100 <?php esc_html_e( 'Save Settings', 'ipcd' ); ?>
101 </button>
102 <a href="<?php echo esc_url( admin_url( 'tools.php?page=ipcd-dashboard' ) ); ?>" class="button button-secondary">
103 <?php esc_html_e( '← Back to Dashboard', 'ipcd' ); ?>
104 </a>
105 </p>
106 </form>
107 </div>
108
109 <div class="ipcd-card" style="max-width:700px;margin-top:20px;">
110 <h2><?php esc_html_e( 'Danger Zone', 'ipcd' ); ?></h2>
111 <p><?php esc_html_e( 'These actions cannot be undone.', 'ipcd' ); ?></p>
112 <button id="ipcd-clear-conflicts-btn" class="button button-link-delete">
113 <?php esc_html_e( 'Clear All Conflict Records', 'ipcd' ); ?>
114 </button>
115 </div>
116</div>
Addedintelligent-plugin-conflict-detector/composer.json+29−0View fileUnifiedSplit
@@ -0,0 +1,29 @@
1{
2 "name": "ccantynz-alt/intelligent-plugin-conflict-detector",
3 "description": "WordPress plugin that intelligently detects plugin conflicts, tests in a safe environment, alerts users, and provides one-click rollback.",
4 "type": "wordpress-plugin",
5 "license": "GPL-2.0-or-later",
6 "require": {
7 "php": ">=7.4"
8 },
9 "require-dev": {
10 "phpunit/phpunit": "^9.6",
11 "brain/monkey": "^2.6",
12 "mockery/mockery": "^1.5"
13 },
14 "autoload-dev": {
15 "psr-4": {
16 "IPCD\\Tests\\": "tests/"
17 }
18 },
19 "scripts": {
20 "test": "phpunit"
21 },
22 "config": {
23 "optimize-autoloader": true,
24 "sort-packages": true,
25 "allow-plugins": {
26 "dealerdirect/phpcodesniffer-composer-installer": true
27 }
28 }
29}
Addedintelligent-plugin-conflict-detector/composer.lock+2070−0View fileUnifiedSplit
Large file (2,070 lines). Load full file
Addedintelligent-plugin-conflict-detector/includes/class-background-tester.php+187−0View fileUnifiedSplit
@@ -0,0 +1,187 @@
1
2/**
3 * Background Tester
4 *
5 * Schedules and executes background test runs that probe the site for
6 * conflicts introduced by a plugin activation or update. Tests run via
7 * WordPress Cron so they never block a real page request.
8 *
9 * @package IPCD
10 */
11
12if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14}
15
16/**
17 * Class IPCD_Background_Tester
18 */
19class IPCD_Background_Tester {
20
21 /** Cron action name. */
22 const CRON_HOOK = 'ipcd_background_test';
23
24 /** Option key that stores the queue of pending tests. */
25 const QUEUE_OPTION = 'ipcd_test_queue';
26
27 /** @var IPCD_Conflict_Detector */
28 private $detector;
29
30 /** @var IPCD_Plugin_State_Manager */
31 private $state_manager;
32
33 /**
34 * Constructor.
35 *
36 * @param IPCD_Conflict_Detector $detector Conflict detector instance.
37 * @param IPCD_Plugin_State_Manager $state_manager State manager instance.
38 */
39 public function __construct(
40 IPCD_Conflict_Detector $detector,
41 IPCD_Plugin_State_Manager $state_manager
42 ) {
43 $this->detector = $detector;
44 $this->state_manager = $state_manager;
45
46 add_action( self::CRON_HOOK, array( $this, 'run_queued_tests' ) );
47 }
48
49 /**
50 * Register the custom cron schedule (every 6 hours).
51 */
52 public static function register_cron_schedule(): void {
53 add_filter(
54 'cron_schedules',
55 static function ( array $schedules ): array {
56 $schedules['ipcd_every_6_hours'] = array(
57 'interval' => 6 * HOUR_IN_SECONDS,
58 'display' => __( 'Every 6 Hours (IPCD)', 'ipcd' ),
59 );
60 return $schedules;
61 }
62 );
63 }
64
65 /**
66 * Add a test to the queue.
67 *
68 * @param string $plugin_basename Plugin that was changed.
69 * @param string $event 'activated' | 'updated'.
70 */
71 public function schedule_test( string $plugin_basename, string $event ): void {
72 $queue = $this->get_queue();
73 $queue[] = array(
74 'plugin' => $plugin_basename,
75 'event' => $event,
76 'queued_at' => time(),
77 );
78 update_option( self::QUEUE_OPTION, $queue );
79
80 // Trigger immediately via a one-off cron event.
81 if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
82 wp_schedule_single_event( time() + 5, self::CRON_HOOK );
83 }
84 }
85
86 /**
87 * Process all queued tests. Called by WP-Cron.
88 */
89 public function run_queued_tests(): void {
90 $queue = $this->get_queue();
91 if ( empty( $queue ) ) {
92 return;
93 }
94
95 // Clear the queue before processing so parallel runs don't double-test.
96 delete_option( self::QUEUE_OPTION );
97
98 foreach ( $queue as $item ) {
99 $this->run_single_test( $item['plugin'], $item['event'] );
100 }
101 }
102
103 /**
104 * Run tests for a single plugin change.
105 *
106 * Tests performed:
107 * 1. Probe the site home URL for HTTP 5xx.
108 * 2. Probe the WP admin URL for HTTP 5xx (uses nonce-free admin-ajax ping).
109 * 3. Scan the PHP error log for fatals introduced since the change.
110 *
111 * @param string $plugin_basename Plugin being tested.
112 * @param string $event 'activated' | 'updated'.
113 */
114 public function run_single_test( string $plugin_basename, string $event ): void {
115 $since = time();
116
117 /**
118 * Fires before a background test run starts.
119 *
120 * @param string $plugin_basename
121 * @param string $event
122 */
123 do_action( 'ipcd_before_test', $plugin_basename, $event );
124
125 $conflict_ids = array();
126
127 // 1. Probe home URL.
128 $home_conflict = $this->detector->probe_url( home_url( '/' ), $plugin_basename );
129 if ( $home_conflict ) {
130 $conflict_ids[] = $home_conflict;
131 }
132
133 // 2. Probe admin-ajax health check (lightweight, no auth needed).
134 $ajax_conflict = $this->detector->probe_url(
135 admin_url( 'admin-ajax.php?action=ipcd_health_ping' ),
136 $plugin_basename
137 );
138 if ( $ajax_conflict ) {
139 $conflict_ids[] = $ajax_conflict;
140 }
141
142 // 3. Scan PHP error log.
143 $log_conflicts = $this->detector->scan_error_log( $plugin_basename, $since );
144 $conflict_ids = array_merge( $conflict_ids, $log_conflicts );
145
146 /**
147 * Fires after a background test run completes.
148 *
149 * @param string $plugin_basename
150 * @param string $event
151 * @param array $conflict_ids IDs of any newly recorded conflicts.
152 */
153 do_action( 'ipcd_after_test', $plugin_basename, $event, $conflict_ids );
154
155 // If conflicts were found, notify the admin.
156 if ( ! empty( $conflict_ids ) ) {
157 do_action( 'ipcd_conflicts_detected', $plugin_basename, $event, $conflict_ids );
158 }
159 }
160
161 /**
162 * Return the current test queue.
163 *
164 * @return array
165 */
166 public function get_queue(): array {
167 $queue = get_option( self::QUEUE_OPTION, array() );
168 return is_array( $queue ) ? $queue : array();
169 }
170
171 /**
172 * Clear the test queue.
173 */
174 public function clear_queue(): void {
175 delete_option( self::QUEUE_OPTION );
176 }
177
178 /**
179 * Return the timestamp of the next scheduled cron run, or null.
180 *
181 * @return int|null
182 */
183 public function get_next_run(): ?int {
184 $next = wp_next_scheduled( self::CRON_HOOK );
185 return $next ?: null;
186 }
187}
Addedintelligent-plugin-conflict-detector/includes/class-conflict-detector.php+275−0View fileUnifiedSplit
@@ -0,0 +1,275 @@
1
2/**
3 * Conflict Detector
4 *
5 * Records, retrieves and clears plugin conflicts. A "conflict" is anything
6 * that causes an observable failure after a plugin change: a PHP fatal, a
7 * JavaScript console error on admin pages, or an HTTP 500 response from the
8 * site front-end.
9 *
10 * @package IPCD
11 */
12
13if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15}
16
17/**
18 * Class IPCD_Conflict_Detector
19 */
20class IPCD_Conflict_Detector {
21
22 /** Option key used to store the active conflict log. */
23 const OPTION_KEY = 'ipcd_conflicts';
24
25 /** Maximum number of conflict records to keep. */
26 const MAX_RECORDS = 200;
27
28 /**
29 * Severity levels.
30 */
31 const SEVERITY_CRITICAL = 'critical';
32 const SEVERITY_WARNING = 'warning';
33 const SEVERITY_INFO = 'info';
34
35 /**
36 * Return all stored conflicts, newest first.
37 *
38 * @return array
39 */
40 public function get_conflicts(): array {
41 $conflicts = get_option( self::OPTION_KEY, array() );
42 return is_array( $conflicts ) ? $conflicts : array();
43 }
44
45 /**
46 * Return only unresolved (active) conflicts.
47 *
48 * @return array
49 */
50 public function get_active_conflicts(): array {
51 return array_filter(
52 $this->get_conflicts(),
53 static function ( $conflict ) {
54 return empty( $conflict['resolved'] );
55 }
56 );
57 }
58
59 /**
60 * Record a new conflict.
61 *
62 * @param string $plugin_basename The plugin that caused / is involved in the conflict.
63 * @param string $type Type of conflict (e.g. 'php_fatal', 'js_error', 'http_error').
64 * @param string $message Human-readable description.
65 * @param string $severity One of the SEVERITY_* constants.
66 * @param array $context Optional extra data (stack trace, URL, etc.).
67 *
68 * @return string Conflict ID.
69 */
70 public function record_conflict(
71 string $plugin_basename,
72 string $type,
73 string $message,
74 string $severity = self::SEVERITY_WARNING,
75 array $context = array()
76 ): string {
77 $conflicts = $this->get_conflicts();
78 $conflict_id = uniqid( 'ipcd_conflict_', true );
79
80 array_unshift(
81 $conflicts,
82 array(
83 'id' => $conflict_id,
84 'plugin' => sanitize_text_field( $plugin_basename ),
85 'type' => sanitize_key( $type ),
86 'message' => sanitize_text_field( $message ),
87 'severity' => $this->sanitize_severity( $severity ),
88 'context' => $context,
89 'resolved' => false,
90 'time' => time(),
91 )
92 );
93
94 // Trim to max records.
95 if ( count( $conflicts ) > self::MAX_RECORDS ) {
96 $conflicts = array_slice( $conflicts, 0, self::MAX_RECORDS );
97 }
98
99 update_option( self::OPTION_KEY, $conflicts );
100
101 /**
102 * Fired after a conflict has been recorded.
103 *
104 * @param array $conflict The full conflict record.
105 * @param string $conflict_id The generated conflict ID.
106 */
107 do_action( 'ipcd_conflict_recorded', $conflicts[0], $conflict_id );
108
109 return $conflict_id;
110 }
111
112 /**
113 * Mark a conflict as resolved.
114 *
115 * @param string $conflict_id Conflict ID to resolve.
116 *
117 * @return bool
118 */
119 public function resolve_conflict( string $conflict_id ): bool {
120 $conflicts = $this->get_conflicts();
121 $updated = false;
122
123 foreach ( $conflicts as &$conflict ) {
124 if ( isset( $conflict['id'] ) && $conflict['id'] === $conflict_id ) {
125 $conflict['resolved'] = true;
126 $conflict['resolved_at'] = time();
127 $updated = true;
128 break;
129 }
130 }
131 unset( $conflict );
132
133 if ( $updated ) {
134 update_option( self::OPTION_KEY, $conflicts );
135 }
136
137 return $updated;
138 }
139
140 /**
141 * Remove all stored conflicts for a specific plugin.
142 *
143 * @param string $plugin_basename Plugin basename.
144 */
145 public function clear_conflicts_for_plugin( string $plugin_basename ): void {
146 $conflicts = $this->get_conflicts();
147 $filtered = array_filter(
148 $conflicts,
149 static function ( $conflict ) use ( $plugin_basename ) {
150 return $conflict['plugin'] !== $plugin_basename;
151 }
152 );
153
154 update_option( self::OPTION_KEY, array_values( $filtered ) );
155 }
156
157 /**
158 * Delete all stored conflicts.
159 */
160 public function clear_all_conflicts(): void {
161 delete_option( self::OPTION_KEY );
162 }
163
164 /**
165 * Analyse the PHP error log for fatal errors introduced by a plugin change.
166 *
167 * @param string $plugin_basename Plugin to attribute errors to.
168 * @param int $since_timestamp Only look at log entries after this timestamp.
169 *
170 * @return array Array of new conflict IDs.
171 */
172 public function scan_error_log( string $plugin_basename, int $since_timestamp = 0 ): array {
173 $conflict_ids = array();
174
175 $log_file = ini_get( 'error_log' );
176 if ( ! $log_file || ! file_exists( $log_file ) || ! is_readable( $log_file ) ) {
177 return $conflict_ids;
178 }
179
180 $lines = file( $log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
181 if ( false === $lines ) {
182 return $conflict_ids;
183 }
184
185 // Only consider lines after the given timestamp.
186 foreach ( array_reverse( $lines ) as $line ) {
187 // Log lines typically start with "[13-Apr-2026 12:00:00 UTC]"
188 if ( preg_match( '/^\[(\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2} [A-Z]+)\]/', $line, $m ) ) {
189 $line_time = strtotime( $m[1] );
190 if ( $line_time && $line_time < $since_timestamp ) {
191 break;
192 }
193 }
194
195 // Detect PHP fatal / parse errors.
196 if ( preg_match( '/PHP (Fatal error|Parse error|Error):(.*)/i', $line, $matches ) ) {
197 $conflict_ids[] = $this->record_conflict(
198 $plugin_basename,
199 'php_fatal',
200 trim( $matches[2] ),
201 self::SEVERITY_CRITICAL,
202 array( 'raw_log_line' => $line )
203 );
204 }
205 }
206
207 return $conflict_ids;
208 }
209
210 /**
211 * Probe a URL and report HTTP errors.
212 *
213 * @param string $url URL to probe.
214 * @param string $plugin_basename Plugin to attribute the error to.
215 *
216 * @return string|null Conflict ID if an error was detected, null otherwise.
217 */
218 public function probe_url( string $url, string $plugin_basename ): ?string {
219 $response = wp_remote_get(
220 $url,
221 array(
222 'timeout' => 15,
223 'user-agent' => 'IPCD-Conflict-Probe/1.0',
224 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
225 )
226 );
227
228 if ( is_wp_error( $response ) ) {
229 return $this->record_conflict(
230 $plugin_basename,
231 'http_error',
232 $response->get_error_message(),
233 self::SEVERITY_CRITICAL,
234 array( 'url' => $url )
235 );
236 }
237
238 $status_code = wp_remote_retrieve_response_code( $response );
239 if ( $status_code >= 500 ) {
240 return $this->record_conflict(
241 $plugin_basename,
242 'http_error',
243 sprintf(
244 /* translators: 1: HTTP status code, 2: URL */
245 __( 'HTTP %1$d response detected at %2$s', 'ipcd' ),
246 $status_code,
247 $url
248 ),
249 self::SEVERITY_CRITICAL,
250 array(
251 'url' => $url,
252 'status_code' => $status_code,
253 )
254 );
255 }
256
257 return null;
258 }
259
260 // -------------------------------------------------------------------------
261 // Private helpers
262 // -------------------------------------------------------------------------
263
264 /**
265 * Ensure severity is one of the defined constants.
266 *
267 * @param string $severity Raw severity string.
268 *
269 * @return string
270 */
271 private function sanitize_severity( string $severity ): string {
272 $allowed = array( self::SEVERITY_CRITICAL, self::SEVERITY_WARNING, self::SEVERITY_INFO );
273 return in_array( $severity, $allowed, true ) ? $severity : self::SEVERITY_WARNING;
274 }
275}
Addedintelligent-plugin-conflict-detector/includes/class-notification-manager.php+221−0View fileUnifiedSplit
@@ -0,0 +1,221 @@
1
2/**
3 * Notification Manager
4 *
5 * Handles alerting the site admin when conflicts are detected. Supports
6 * both WordPress admin-notice banners and optional email alerts.
7 *
8 * @package IPCD
9 */
10
11if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13}
14
15/**
16 * Class IPCD_Notification_Manager
17 */
18class IPCD_Notification_Manager {
19
20 /** Option key for pending admin notices. */
21 const NOTICES_OPTION = 'ipcd_pending_notices';
22
23 /** Option key for plugin settings. */
24 const SETTINGS_OPTION = 'ipcd_settings';
25
26 /**
27 * Constructor – wire hooks.
28 */
29 public function __construct() {
30 add_action( 'admin_notices', array( $this, 'display_admin_notices' ) );
31 add_action( 'ipcd_conflicts_detected', array( $this, 'on_conflicts_detected' ), 10, 3 );
32 }
33
34 /**
35 * Called when the background tester finds new conflicts.
36 *
37 * @param string $plugin_basename Plugin that caused the conflict.
38 * @param string $event 'activated' | 'updated'.
39 * @param array $conflict_ids IDs of new conflicts.
40 */
41 public function on_conflicts_detected(
42 string $plugin_basename,
43 string $event,
44 array $conflict_ids
45 ): void {
46 $plugin_name = $this->get_plugin_name( $plugin_basename );
47 $count = count( $conflict_ids );
48
49 $message = sprintf(
50 /* translators: 1: plugin name, 2: event type, 3: number of conflicts */
51 _n(
52 '<strong>IPCD:</strong> %1$d conflict detected after %3$s <em>%2$s</em>. <a href="%4$s">View details</a>.',
53 '<strong>IPCD:</strong> %1$d conflicts detected after %3$s <em>%2$s</em>. <a href="%4$s">View details</a>.',
54 $count,
55 'ipcd'
56 ),
57 $count,
58 esc_html( $plugin_name ),
59 esc_html( $event ),
60 esc_url( admin_url( 'tools.php?page=ipcd-dashboard' ) )
61 );
62
63 $this->queue_notice( $message, 'error' );
64
65 // Send email notification if enabled.
66 $settings = $this->get_settings();
67 if ( ! empty( $settings['email_notifications'] ) ) {
68 $this->send_email_alert( $plugin_name, $event, $count );
69 }
70 }
71
72 /**
73 * Display queued admin notices.
74 */
75 public function display_admin_notices(): void {
76 $notices = $this->get_queued_notices();
77 if ( empty( $notices ) ) {
78 return;
79 }
80
81 foreach ( $notices as $notice ) {
82 $type = in_array( $notice['type'], array( 'error', 'warning', 'success', 'info' ), true )
83 ? $notice['type'] : 'info';
84 $message = wp_kses(
85 $notice['message'],
86 array(
87 'strong' => array(),
88 'em' => array(),
89 'a' => array( 'href' => array() ),
90 )
91 );
92 printf(
93 '<div class="notice notice-%s is-dismissible ipcd-notice"><p>%s</p></div>',
94 esc_attr( $type ),
95 $message // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already kses'd above
96 );
97 }
98
99 // Clear notices after display.
100 delete_option( self::NOTICES_OPTION );
101 }
102
103 /**
104 * Queue a notice to be displayed on the next admin page load.
105 *
106 * @param string $message Notice message (may contain allowed HTML).
107 * @param string $type 'error' | 'warning' | 'success' | 'info'.
108 */
109 public function queue_notice( string $message, string $type = 'info' ): void {
110 $notices = $this->get_queued_notices();
111 $notices[] = array(
112 'message' => $message,
113 'type' => $type,
114 );
115 update_option( self::NOTICES_OPTION, $notices );
116 }
117
118 /**
119 * Return queued notices.
120 *
121 * @return array
122 */
123 public function get_queued_notices(): array {
124 $notices = get_option( self::NOTICES_OPTION, array() );
125 return is_array( $notices ) ? $notices : array();
126 }
127
128 /**
129 * Return plugin settings.
130 *
131 * @return array
132 */
133 public function get_settings(): array {
134 $defaults = array(
135 'email_notifications' => false,
136 'email_address' => get_option( 'admin_email' ),
137 'auto_rollback' => false,
138 'scan_interval_hours' => 6,
139 );
140
141 $saved = get_option( self::SETTINGS_OPTION, array() );
142 return wp_parse_args( is_array( $saved ) ? $saved : array(), $defaults );
143 }
144
145 /**
146 * Save plugin settings.
147 *
148 * @param array $settings New settings values.
149 */
150 public function save_settings( array $settings ): void {
151 $sanitized = array(
152 'email_notifications' => ! empty( $settings['email_notifications'] ),
153 'email_address' => sanitize_email( $settings['email_address'] ?? '' ),
154 'auto_rollback' => ! empty( $settings['auto_rollback'] ),
155 'scan_interval_hours' => absint( $settings['scan_interval_hours'] ?? 6 ),
156 );
157 update_option( self::SETTINGS_OPTION, $sanitized );
158 }
159
160 // -------------------------------------------------------------------------
161 // Private helpers
162 // -------------------------------------------------------------------------
163
164 /**
165 * Send an email alert about detected conflicts.
166 *
167 * @param string $plugin_name Human-readable plugin name.
168 * @param string $event 'activated' | 'updated'.
169 * @param int $count Number of conflicts.
170 */
171 private function send_email_alert( string $plugin_name, string $event, int $count ): void {
172 $settings = $this->get_settings();
173 $recipient = $settings['email_address'] ?: get_option( 'admin_email' );
174 $site_name = get_bloginfo( 'name' );
175
176 $subject = sprintf(
177 /* translators: 1: site name, 2: plugin name */
178 __( '[%1$s] Plugin conflict detected — %2$s', 'ipcd' ),
179 $site_name,
180 $plugin_name
181 );
182
183 $body = sprintf(
184 /* translators: 1: number of conflicts, 2: plugin name, 3: event, 4: dashboard URL */
185 __(
186 "Hello,\n\n%1\$d conflict(s) were detected on %5\$s after %2\$s was %3\$s.\n\nPlease visit your IPCD dashboard to review the conflicts and roll back if necessary:\n%4\$s\n\nThis message was sent automatically by the Intelligent Plugin Conflict Detector.",
187 'ipcd'
188 ),
189 $count,
190 $plugin_name,
191 $event,
192 admin_url( 'tools.php?page=ipcd-dashboard' ),
193 $site_name
194 );
195
196 wp_mail( $recipient, $subject, $body );
197 }
198
199 /**
200 * Get a human-readable plugin name from its basename.
201 *
202 * @param string $plugin_basename Plugin basename (e.g. woocommerce/woocommerce.php).
203 *
204 * @return string
205 */
206 private function get_plugin_name( string $plugin_basename ): string {
207 if ( ! function_exists( 'get_plugin_data' ) ) {
208 require_once ABSPATH . 'wp-admin/includes/plugin.php';
209 }
210
211 $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_basename;
212 if ( file_exists( $plugin_file ) ) {
213 $data = get_plugin_data( $plugin_file, false, false );
214 if ( ! empty( $data['Name'] ) ) {
215 return $data['Name'];
216 }
217 }
218
219 return $plugin_basename;
220 }
221}
Addedintelligent-plugin-conflict-detector/includes/class-plugin-state-manager.php+191−0View fileUnifiedSplit
@@ -0,0 +1,191 @@
1
2/**
3 * Plugin State Manager
4 *
5 * Captures and restores snapshots of the active-plugin list and relevant
6 * WordPress/WooCommerce option values so the Rollback Manager can restore
7 * any saved state.
8 *
9 * @package IPCD
10 */
11
12if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14}
15
16/**
17 * Class IPCD_Plugin_State_Manager
18 */
19class IPCD_Plugin_State_Manager {
20
21 /** Database table name (without prefix). */
22 const TABLE_NAME = 'ipcd_snapshots';
23
24 /**
25 * Create the custom DB table on activation.
26 */
27 public static function create_tables(): void {
28 global $wpdb;
29
30 $table_name = $wpdb->prefix . self::TABLE_NAME;
31 $charset_collate = $wpdb->get_charset_collate();
32
33 $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
34 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
35 snapshot_id VARCHAR(191) NOT NULL,
36 label VARCHAR(255) NOT NULL DEFAULT '',
37 active_plugins LONGTEXT NOT NULL,
38 options_data LONGTEXT NOT NULL,
39 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
40 PRIMARY KEY (id),
41 UNIQUE KEY snapshot_id (snapshot_id)
42 ) {$charset_collate};";
43
44 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
45 dbDelta( $sql );
46 }
47
48 /**
49 * Capture a snapshot of the current plugin/option state.
50 *
51 * @param string $label Human-readable label (e.g. "before_activate_woocommerce").
52 * @param array $extra_option_keys Additional option keys to snapshot beyond the defaults.
53 *
54 * @return string The unique snapshot ID.
55 */
56 public function capture_snapshot( string $label = '', array $extra_option_keys = array() ): string {
57 global $wpdb;
58
59 $snapshot_id = uniqid( 'ipcd_', true );
60 $active_plugins = get_option( 'active_plugins', array() );
61
62 $default_option_keys = array(
63 'siteurl',
64 'blogname',
65 'active_plugins',
66 'template',
67 'stylesheet',
68 'woocommerce_version',
69 'db_version',
70 );
71
72 $options_data = array();
73 foreach ( array_unique( array_merge( $default_option_keys, $extra_option_keys ) ) as $key ) {
74 $options_data[ $key ] = get_option( $key );
75 }
76
77 $table = $wpdb->prefix . self::TABLE_NAME;
78
79 $wpdb->insert(
80 $table,
81 array(
82 'snapshot_id' => $snapshot_id,
83 'label' => sanitize_text_field( $label ),
84 'active_plugins' => wp_json_encode( $active_plugins ),
85 'options_data' => wp_json_encode( $options_data ),
86 'created_at' => current_time( 'mysql' ),
87 ),
88 array( '%s', '%s', '%s', '%s', '%s' )
89 );
90
91 return $snapshot_id;
92 }
93
94 /**
95 * Return all stored snapshots, newest first.
96 *
97 * @return array
98 */
99 public function get_snapshots(): array {
100 global $wpdb;
101
102 $table = $wpdb->prefix . self::TABLE_NAME;
103 $rows = $wpdb->get_results( "SELECT * FROM {$table} ORDER BY created_at DESC", ARRAY_A );
104
105 return $rows ?: array();
106 }
107
108 /**
109 * Return a single snapshot by its ID.
110 *
111 * @param string $snapshot_id Snapshot ID.
112 *
113 * @return array|null
114 */
115 public function get_snapshot( string $snapshot_id ): ?array {
116 global $wpdb;
117
118 $table = $wpdb->prefix . self::TABLE_NAME;
119 $row = $wpdb->get_row(
120 $wpdb->prepare( "SELECT * FROM {$table} WHERE snapshot_id = %s", $snapshot_id ),
121 ARRAY_A
122 );
123
124 return $row ?: null;
125 }
126
127 /**
128 * Restore the active_plugins list from a snapshot.
129 * Does NOT update other options – only the plugin list.
130 *
131 * @param string $snapshot_id Snapshot ID to restore.
132 *
133 * @return bool True on success, false if snapshot not found.
134 */
135 public function restore_snapshot( string $snapshot_id ): bool {
136 $snapshot = $this->get_snapshot( $snapshot_id );
137 if ( ! $snapshot ) {
138 return false;
139 }
140
141 $active_plugins = json_decode( $snapshot['active_plugins'], true );
142 if ( ! is_array( $active_plugins ) ) {
143 return false;
144 }
145
146 // Only keep plugins that still exist on disk.
147 $valid_plugins = array_filter( $active_plugins, static function ( $plugin ) {
148 return file_exists( WP_PLUGIN_DIR . '/' . $plugin );
149 } );
150
151 update_option( 'active_plugins', array_values( $valid_plugins ) );
152
153 return true;
154 }
155
156 /**
157 * Delete a snapshot by ID.
158 *
159 * @param string $snapshot_id Snapshot ID to delete.
160 *
161 * @return bool
162 */
163 public function delete_snapshot( string $snapshot_id ): bool {
164 global $wpdb;
165
166 $table = $wpdb->prefix . self::TABLE_NAME;
167 $result = $wpdb->delete( $table, array( 'snapshot_id' => $snapshot_id ), array( '%s' ) );
168
169 return (bool) $result;
170 }
171
172 /**
173 * Prune snapshots older than a given number of days.
174 *
175 * @param int $days Number of days to keep.
176 *
177 * @return int Number of rows deleted.
178 */
179 public function prune_old_snapshots( int $days = 30 ): int {
180 global $wpdb;
181
182 $table = $wpdb->prefix . self::TABLE_NAME;
183
184 return (int) $wpdb->query(
185 $wpdb->prepare(
186 "DELETE FROM {$table} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
187 $days
188 )
189 );
190 }
191}
Addedintelligent-plugin-conflict-detector/includes/class-rollback-manager.php+133−0View fileUnifiedSplit
@@ -0,0 +1,133 @@
1
2/**
3 * Rollback Manager
4 *
5 * Provides one-click rollback to any previously captured plugin state snapshot.
6 *
7 * @package IPCD
8 */
9
10if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12}
13
14/**
15 * Class IPCD_Rollback_Manager
16 */
17class IPCD_Rollback_Manager {
18
19 /** Option key that stores rollback history. */
20 const HISTORY_OPTION = 'ipcd_rollback_history';
21
22 /** @var IPCD_Plugin_State_Manager */
23 private $state_manager;
24
25 /**
26 * Constructor.
27 *
28 * @param IPCD_Plugin_State_Manager $state_manager State manager instance.
29 */
30 public function __construct( IPCD_Plugin_State_Manager $state_manager ) {
31 $this->state_manager = $state_manager;
32 }
33
34 /**
35 * Perform a rollback to the given snapshot.
36 *
37 * The current state is automatically snapshotted first so the rollback
38 * itself can be undone if needed.
39 *
40 * @param string $snapshot_id ID of the snapshot to restore.
41 *
42 * @return array Result array with 'success' (bool) and 'message' (string).
43 */
44 public function rollback( string $snapshot_id ): array {
45 // Safety snapshot of current state before rolling back.
46 $pre_rollback_id = $this->state_manager->capture_snapshot(
47 'pre_rollback_' . $snapshot_id
48 );
49
50 $success = $this->state_manager->restore_snapshot( $snapshot_id );
51
52 if ( $success ) {
53 $this->add_history_entry(
54 $snapshot_id,
55 $pre_rollback_id,
56 'success'
57 );
58
59 /**
60 * Fires after a successful rollback.
61 *
62 * @param string $snapshot_id The restored snapshot.
63 * @param string $pre_rollback_id The safety snapshot taken before restoring.
64 */
65 do_action( 'ipcd_rollback_complete', $snapshot_id, $pre_rollback_id );
66
67 return array(
68 'success' => true,
69 'message' => __( 'Rollback completed successfully. Active plugins have been restored.', 'ipcd' ),
70 'pre_rollback_snapshot' => $pre_rollback_id,
71 );
72 }
73
74 $this->add_history_entry( $snapshot_id, $pre_rollback_id, 'failed' );
75
76 return array(
77 'success' => false,
78 'message' => __( 'Rollback failed: snapshot not found or could not be restored.', 'ipcd' ),
79 );
80 }
81
82 /**
83 * Return rollback history, newest first.
84 *
85 * @return array
86 */
87 public function get_history(): array {
88 $history = get_option( self::HISTORY_OPTION, array() );
89 return is_array( $history ) ? $history : array();
90 }
91
92 /**
93 * Clear all rollback history entries.
94 */
95 public function clear_history(): void {
96 delete_option( self::HISTORY_OPTION );
97 }
98
99 // -------------------------------------------------------------------------
100 // Private helpers
101 // -------------------------------------------------------------------------
102
103 /**
104 * Append an entry to the rollback history log.
105 *
106 * @param string $snapshot_id The snapshot that was restored.
107 * @param string $pre_rollback_id The safety snapshot taken before rollback.
108 * @param string $status 'success' | 'failed'.
109 */
110 private function add_history_entry(
111 string $snapshot_id,
112 string $pre_rollback_id,
113 string $status
114 ): void {
115 $history = $this->get_history();
116 array_unshift(
117 $history,
118 array(
119 'snapshot_id' => $snapshot_id,
120 'pre_rollback_id' => $pre_rollback_id,
121 'status' => $status,
122 'time' => time(),
123 )
124 );
125
126 // Keep at most 50 history entries.
127 if ( count( $history ) > 50 ) {
128 $history = array_slice( $history, 0, 50 );
129 }
130
131 update_option( self::HISTORY_OPTION, $history );
132 }
133}
Addedintelligent-plugin-conflict-detector/intelligent-plugin-conflict-detector.php+184−0View fileUnifiedSplit
@@ -0,0 +1,184 @@
1
2/**
3 * Plugin Name: Intelligent Plugin Conflict Detector
4 * Plugin URI: https://github.com/ccantynz-alt/Intelligent-Plugin-Conflict-Detector
5 * Description: Automatically detects plugin conflicts in a safe background environment, alerts store owners before problems occur, and provides one-click rollback.
6 * Version: 1.0.0
7 * Requires at least: 5.8
8 * Requires PHP: 7.4
9 * Author: Intelligent Plugin Conflict Detector Contributors
10 * License: GPL-2.0-or-later
11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12 * Text Domain: ipcd
13 * Domain Path: /languages
14 *
15 * @package IPCD
16 */
17
18if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20}
21
22// Plugin constants.
23define( 'IPCD_VERSION', '1.0.0' );
24define( 'IPCD_PLUGIN_FILE', __FILE__ );
25define( 'IPCD_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
26define( 'IPCD_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
27define( 'IPCD_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
28
29// Required includes.
30require_once IPCD_PLUGIN_DIR . 'includes/class-plugin-state-manager.php';
31require_once IPCD_PLUGIN_DIR . 'includes/class-conflict-detector.php';
32require_once IPCD_PLUGIN_DIR . 'includes/class-background-tester.php';
33require_once IPCD_PLUGIN_DIR . 'includes/class-rollback-manager.php';
34require_once IPCD_PLUGIN_DIR . 'includes/class-notification-manager.php';
35
36if ( is_admin() ) {
37 require_once IPCD_PLUGIN_DIR . 'admin/class-admin.php';
38}
39
40/**
41 * Returns the main plugin instance (singleton).
42 *
43 * @return IPCD_Plugin
44 */
45function ipcd() {
46 return IPCD_Plugin::instance();
47}
48
49/**
50 * Main plugin class.
51 */
52final class IPCD_Plugin {
53
54 /** @var IPCD_Plugin|null */
55 private static $instance = null;
56
57 /** @var IPCD_Plugin_State_Manager */
58 public $state_manager;
59
60 /** @var IPCD_Conflict_Detector */
61 public $conflict_detector;
62
63 /** @var IPCD_Background_Tester */
64 public $background_tester;
65
66 /** @var IPCD_Rollback_Manager */
67 public $rollback_manager;
68
69 /** @var IPCD_Notification_Manager */
70 public $notification_manager;
71
72 /** @var IPCD_Admin|null */
73 public $admin;
74
75 /**
76 * Returns/creates singleton.
77 *
78 * @return IPCD_Plugin
79 */
80 public static function instance(): IPCD_Plugin {
81 if ( null === self::$instance ) {
82 self::$instance = new self();
83 }
84 return self::$instance;
85 }
86
87 /**
88 * Constructor – wire up hooks.
89 */
90 private function __construct() {
91 $this->state_manager = new IPCD_Plugin_State_Manager();
92 $this->conflict_detector = new IPCD_Conflict_Detector();
93 $this->background_tester = new IPCD_Background_Tester( $this->conflict_detector, $this->state_manager );
94 $this->rollback_manager = new IPCD_Rollback_Manager( $this->state_manager );
95 $this->notification_manager = new IPCD_Notification_Manager();
96
97 if ( is_admin() ) {
98 $this->admin = new IPCD_Admin(
99 $this->conflict_detector,
100 $this->rollback_manager,
101 $this->notification_manager,
102 $this->background_tester,
103 $this->state_manager
104 );
105 }
106
107 add_action( 'init', array( $this, 'load_textdomain' ) );
108 add_action( 'activated_plugin', array( $this, 'on_plugin_activated' ), 10, 2 );
109 add_action( 'deactivated_plugin', array( $this, 'on_plugin_deactivated' ), 10, 2 );
110 add_action( 'upgrader_process_complete', array( $this, 'on_upgrade_complete' ), 10, 2 );
111 }
112
113 /**
114 * Load plugin text domain.
115 */
116 public function load_textdomain(): void {
117 load_plugin_textdomain( 'ipcd', false, dirname( IPCD_PLUGIN_BASENAME ) . '/languages' );
118 }
119
120 /**
121 * Triggered when any plugin is activated – snapshot state and schedule a test run.
122 *
123 * @param string $plugin Plugin basename.
124 * @param bool $network_wide Whether activated network-wide.
125 */
126 public function on_plugin_activated( string $plugin, bool $network_wide ): void {
127 if ( $plugin === IPCD_PLUGIN_BASENAME ) {
128 return;
129 }
130 $this->state_manager->capture_snapshot( 'before_activate_' . sanitize_key( $plugin ) );
131 $this->background_tester->schedule_test( $plugin, 'activated' );
132 }
133
134 /**
135 * Triggered when any plugin is deactivated – clear stored conflict data for it.
136 *
137 * @param string $plugin Plugin basename.
138 * @param bool $network_wide Whether deactivated network-wide.
139 */
140 public function on_plugin_deactivated( string $plugin, bool $network_wide ): void {
141 $this->conflict_detector->clear_conflicts_for_plugin( $plugin );
142 }
143
144 /**
145 * Triggered after a plugin or WooCommerce update – capture snapshot and schedule test.
146 *
147 * @param \WP_Upgrader $upgrader Upgrader instance.
148 * @param array $options Upgrade options.
149 */
150 public function on_upgrade_complete( $upgrader, array $options ): void {
151 if ( isset( $options['type'] ) && 'plugin' === $options['type'] ) {
152 $plugins = $options['plugins'] ?? array();
153 foreach ( $plugins as $plugin ) {
154 $this->state_manager->capture_snapshot( 'before_update_' . sanitize_key( $plugin ) );
155 $this->background_tester->schedule_test( $plugin, 'updated' );
156 }
157 }
158 }
159}
160
161// Activation / deactivation / uninstall hooks.
162register_activation_hook( __FILE__, 'ipcd_activate' );
163register_deactivation_hook( __FILE__, 'ipcd_deactivate' );
164
165/**
166 * Plugin activation: create DB tables and schedule cron.
167 */
168function ipcd_activate(): void {
169 IPCD_Plugin_State_Manager::create_tables();
170 IPCD_Background_Tester::register_cron_schedule();
171 if ( ! wp_next_scheduled( 'ipcd_background_test' ) ) {
172 wp_schedule_event( time(), 'ipcd_every_6_hours', 'ipcd_background_test' );
173 }
174}
175
176/**
177 * Plugin deactivation: clear cron events.
178 */
179function ipcd_deactivate(): void {
180 wp_clear_scheduled_hook( 'ipcd_background_test' );
181}
182
183// Boot the plugin.
184ipcd();
Addedintelligent-plugin-conflict-detector/phpunit.xml+21−0View fileUnifiedSplit
@@ -0,0 +1,21 @@
1
2<phpunit
3 bootstrap="tests/bootstrap.php"
4 colors="true"
5 testdox="true"
6 stopOnFailure="false"
7 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
8 xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
9>
10 <testsuites>
11 <testsuite name="IPCD Unit Tests">
12 <directory>tests/Unit</directory>
13 </testsuite>
14 </testsuites>
15 <coverage>
16 <include>
17 <directory suffix=".php">includes</directory>
18 <directory suffix=".php">admin</directory>
19 </include>
20 </coverage>
21</phpunit>
Addedintelligent-plugin-conflict-detector/tests/Unit/BackgroundTesterTest.php+126−0View fileUnifiedSplit
@@ -0,0 +1,126 @@
1
2/**
3 * Unit tests for IPCD_Background_Tester.
4 *
5 * @package IPCD\Tests\Unit
6 */
7
8namespace IPCD\Tests\Unit;
9
10use Brain\Monkey;
11use Brain\Monkey\Functions;
12use IPCD_Background_Tester;
13use IPCD_Conflict_Detector;
14use IPCD_Plugin_State_Manager;
15use Mockery;
16use PHPUnit\Framework\TestCase;
17
18/**
19 * Class BackgroundTesterTest
20 */
21class BackgroundTesterTest extends TestCase {
22
23 protected function setUp(): void {
24 parent::setUp();
25 Monkey\setUp();
26
27 Functions\when( 'get_option' )->justReturn( array() );
28 Functions\when( 'update_option' )->justReturn( true );
29 Functions\when( 'delete_option' )->justReturn( true );
30 Functions\when( 'add_action' )->justReturn( null );
31 Functions\when( 'add_filter' )->justReturn( null );
32 Functions\when( 'do_action' )->justReturn( null );
33 Functions\when( 'wp_next_scheduled' )->justReturn( false );
34 Functions\when( 'wp_schedule_single_event' )->justReturn( true );
35 Functions\when( '__' )->returnArg( 1 );
36 }
37
38 protected function tearDown(): void {
39 Mockery::close();
40 Monkey\tearDown();
41 parent::tearDown();
42 }
43
44 // -------------------------------------------------------------------------
45 // schedule_test
46 // -------------------------------------------------------------------------
47
48 public function test_schedule_test_adds_to_queue(): void {
49 $detector_mock = Mockery::mock( IPCD_Conflict_Detector::class );
50 $state_manager_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
51
52 $saved_queue = null;
53 Functions\when( 'update_option' )
54 ->alias( static function ( $key, $value ) use ( &$saved_queue ) {
55 if ( IPCD_Background_Tester::QUEUE_OPTION === $key ) {
56 $saved_queue = $value;
57 }
58 return true;
59 } );
60
61 $tester = new IPCD_Background_Tester( $detector_mock, $state_manager_mock );
62 $tester->schedule_test( 'test-plugin/test-plugin.php', 'activated' );
63
64 $this->assertIsArray( $saved_queue );
65 $this->assertCount( 1, $saved_queue );
66 $this->assertSame( 'test-plugin/test-plugin.php', $saved_queue[0]['plugin'] );
67 $this->assertSame( 'activated', $saved_queue[0]['event'] );
68 }
69
70 // -------------------------------------------------------------------------
71 // run_queued_tests – empty queue
72 // -------------------------------------------------------------------------
73
74 public function test_run_queued_tests_does_nothing_when_queue_is_empty(): void {
75 $detector_mock = Mockery::mock( IPCD_Conflict_Detector::class );
76 $state_manager_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
77
78 $tester = new IPCD_Background_Tester( $detector_mock, $state_manager_mock );
79
80 // Should not throw – and no AJAX calls / conflict detections happen.
81 $tester->run_queued_tests();
82 $this->assertTrue( true );
83 }
84
85 // -------------------------------------------------------------------------
86 // get_queue
87 // -------------------------------------------------------------------------
88
89 public function test_get_queue_returns_array(): void {
90 $detector_mock = Mockery::mock( IPCD_Conflict_Detector::class );
91 $state_manager_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
92
93 $tester = new IPCD_Background_Tester( $detector_mock, $state_manager_mock );
94 $queue = $tester->get_queue();
95
96 $this->assertIsArray( $queue );
97 }
98
99 // -------------------------------------------------------------------------
100 // get_next_run
101 // -------------------------------------------------------------------------
102
103 public function test_get_next_run_returns_null_when_not_scheduled(): void {
104 // wp_next_scheduled already stubbed to return false in setUp.
105 $detector_mock = Mockery::mock( IPCD_Conflict_Detector::class );
106 $state_manager_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
107
108 $tester = new IPCD_Background_Tester( $detector_mock, $state_manager_mock );
109 $next = $tester->get_next_run();
110
111 $this->assertNull( $next );
112 }
113
114 public function test_get_next_run_returns_timestamp_when_scheduled(): void {
115 // Override the setUp stub for this test.
116 Functions\when( 'wp_next_scheduled' )->justReturn( 1_700_000_000 );
117
118 $detector_mock = Mockery::mock( IPCD_Conflict_Detector::class );
119 $state_manager_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
120
121 $tester = new IPCD_Background_Tester( $detector_mock, $state_manager_mock );
122 $next = $tester->get_next_run();
123
124 $this->assertSame( 1_700_000_000, $next );
125 }
126}
Addedintelligent-plugin-conflict-detector/tests/Unit/ConflictDetectorTest.php+188−0View fileUnifiedSplit
@@ -0,0 +1,188 @@
1
2/**
3 * Unit tests for IPCD_Conflict_Detector.
4 *
5 * @package IPCD\Tests\Unit
6 */
7
8namespace IPCD\Tests\Unit;
9
10use Brain\Monkey;
11use Brain\Monkey\Functions;
12use IPCD_Conflict_Detector;
13use PHPUnit\Framework\TestCase;
14
15/**
16 * Class ConflictDetectorTest
17 */
18class ConflictDetectorTest extends TestCase {
19
20 protected function setUp(): void {
21 parent::setUp();
22 Monkey\setUp();
23 }
24
25 protected function tearDown(): void {
26 Monkey\tearDown();
27 parent::tearDown();
28 }
29
30 /**
31 * Stub the WordPress functions that every test needs.
32 * Called at the start of each test so we can also add test-specific stubs
33 * before or after without conflicts.
34 */
35 private function stub_wp_functions(): void {
36 Functions\when( 'get_option' )->justReturn( array() );
37 Functions\when( 'update_option' )->justReturn( true );
38 Functions\when( 'delete_option' )->justReturn( true );
39 Functions\when( 'do_action' )->justReturn( null );
40 Functions\when( '__' )->returnArg( 1 );
41 Functions\when( 'sanitize_text_field' )->returnArg( 1 );
42 Functions\when( 'sanitize_key' )->returnArg( 1 );
43 }
44
45 // -------------------------------------------------------------------------
46 // record_conflict
47 // -------------------------------------------------------------------------
48
49 public function test_record_conflict_returns_string_id(): void {
50 $this->stub_wp_functions();
51
52 $detector = new IPCD_Conflict_Detector();
53 $id = $detector->record_conflict(
54 'some-plugin/plugin.php',
55 'php_fatal',
56 'Call to undefined function foo()',
57 IPCD_Conflict_Detector::SEVERITY_CRITICAL
58 );
59
60 $this->assertIsString( $id );
61 $this->assertStringStartsWith( 'ipcd_conflict_', $id );
62 }
63
64 public function test_record_conflict_falls_back_to_warning_for_invalid_severity(): void {
65 $recorded = array();
66
67 Functions\when( 'get_option' )->justReturn( array() );
68 Functions\when( 'do_action' )->justReturn( null );
69 Functions\when( '__' )->returnArg( 1 );
70 Functions\when( 'sanitize_text_field' )->returnArg( 1 );
71 Functions\when( 'sanitize_key' )->returnArg( 1 );
72 Functions\when( 'update_option' )
73 ->alias( static function ( $key, $value ) use ( &$recorded ) {
74 $recorded = $value;
75 return true;
76 } );
77
78 $detector = new IPCD_Conflict_Detector();
79 $detector->record_conflict(
80 'bad-plugin/bad.php',
81 'http_error',
82 'HTTP 500',
83 'nonsense_severity'
84 );
85
86 $this->assertNotEmpty( $recorded );
87 $this->assertSame( 'warning', $recorded[0]['severity'] );
88 }
89
90 // -------------------------------------------------------------------------
91 // get_conflicts / get_active_conflicts
92 // -------------------------------------------------------------------------
93
94 public function test_get_conflicts_returns_array_when_option_is_empty(): void {
95 Functions\when( 'get_option' )->justReturn( array() );
96
97 $detector = new IPCD_Conflict_Detector();
98 $conflicts = $detector->get_conflicts();
99 $this->assertIsArray( $conflicts );
100 $this->assertCount( 0, $conflicts );
101 }
102
103 public function test_get_active_conflicts_excludes_resolved(): void {
104 $stored = array(
105 array( 'id' => 'c1', 'plugin' => 'foo/foo.php', 'type' => 'php_fatal', 'message' => 'Error', 'severity' => 'critical', 'context' => array(), 'resolved' => false, 'time' => time() ),
106 array( 'id' => 'c2', 'plugin' => 'bar/bar.php', 'type' => 'http_error', 'message' => 'HTTP 500', 'severity' => 'critical', 'context' => array(), 'resolved' => true, 'time' => time() ),
107 );
108
109 Functions\when( 'get_option' )->justReturn( $stored );
110
111 $detector = new IPCD_Conflict_Detector();
112 $active = $detector->get_active_conflicts();
113
114 $this->assertCount( 1, $active );
115 $this->assertSame( 'c1', array_values( $active )[0]['id'] );
116 }
117
118 // -------------------------------------------------------------------------
119 // resolve_conflict
120 // -------------------------------------------------------------------------
121
122 public function test_resolve_conflict_returns_true_for_known_id(): void {
123 $stored = array(
124 array( 'id' => 'abc123', 'plugin' => 'foo/foo.php', 'type' => 'php_fatal', 'message' => 'err', 'severity' => 'critical', 'context' => array(), 'resolved' => false, 'time' => time() ),
125 );
126
127 Functions\when( 'get_option' )->justReturn( $stored );
128 Functions\when( 'update_option' )->justReturn( true );
129
130 $detector = new IPCD_Conflict_Detector();
131 $result = $detector->resolve_conflict( 'abc123' );
132
133 $this->assertTrue( $result );
134 }
135
136 public function test_resolve_conflict_returns_false_for_unknown_id(): void {
137 Functions\when( 'get_option' )->justReturn( array() );
138
139 $detector = new IPCD_Conflict_Detector();
140 $result = $detector->resolve_conflict( 'nonexistent_id' );
141
142 $this->assertFalse( $result );
143 }
144
145 // -------------------------------------------------------------------------
146 // clear_conflicts_for_plugin
147 // -------------------------------------------------------------------------
148
149 public function test_clear_conflicts_for_plugin_removes_matching_entries(): void {
150 $stored = array(
151 array( 'id' => '1', 'plugin' => 'target/target.php', 'type' => 't', 'message' => 'm', 'severity' => 'info', 'context' => array(), 'resolved' => false, 'time' => time() ),
152 array( 'id' => '2', 'plugin' => 'other/other.php', 'type' => 't', 'message' => 'm', 'severity' => 'info', 'context' => array(), 'resolved' => false, 'time' => time() ),
153 );
154
155 $saved = null;
156 Functions\when( 'get_option' )->justReturn( $stored );
157 Functions\when( 'update_option' )
158 ->alias( static function ( $key, $value ) use ( &$saved ) {
159 $saved = $value;
160 return true;
161 } );
162
163 $detector = new IPCD_Conflict_Detector();
164 $detector->clear_conflicts_for_plugin( 'target/target.php' );
165
166 $this->assertIsArray( $saved );
167 $this->assertCount( 1, $saved );
168 $this->assertSame( 'other/other.php', $saved[0]['plugin'] );
169 }
170
171 // -------------------------------------------------------------------------
172 // clear_all_conflicts
173 // -------------------------------------------------------------------------
174
175 public function test_clear_all_conflicts_clears_conflicts(): void {
176 $deleted_key = null;
177 Functions\when( 'delete_option' )
178 ->alias( static function ( $key ) use ( &$deleted_key ) {
179 $deleted_key = $key;
180 return true;
181 } );
182
183 $detector = new IPCD_Conflict_Detector();
184 $detector->clear_all_conflicts();
185
186 $this->assertSame( IPCD_Conflict_Detector::OPTION_KEY, $deleted_key );
187 }
188}
Addedintelligent-plugin-conflict-detector/tests/Unit/NotificationManagerTest.php+115−0View fileUnifiedSplit
@@ -0,0 +1,115 @@
1
2/**
3 * Unit tests for IPCD_Notification_Manager.
4 *
5 * @package IPCD\Tests\Unit
6 */
7
8namespace IPCD\Tests\Unit;
9
10use Brain\Monkey;
11use Brain\Monkey\Functions;
12use IPCD_Notification_Manager;
13use PHPUnit\Framework\TestCase;
14
15/**
16 * Class NotificationManagerTest
17 */
18class NotificationManagerTest extends TestCase {
19
20protected function setUp(): void {
21parent::setUp();
22Monkey\setUp();
23
24Functions\when( 'add_action' )->justReturn( null );
25Functions\when( '__' )->returnArg( 1 );
26Functions\when( '_n' )->returnArg( 1 );
27Functions\when( 'wp_parse_args' )->alias( static function ( $args, $defaults ) {
28return array_merge( $defaults, (array) $args );
29} );
30Functions\when( 'sanitize_email' )->returnArg( 1 );
31Functions\when( 'absint' )->alias( 'abs' );
32Functions\when( 'get_bloginfo' )->justReturn( 'Test Site' );
33}
34
35protected function tearDown(): void {
36Monkey\tearDown();
37parent::tearDown();
38}
39
40// -------------------------------------------------------------------------
41// queue_notice / get_queued_notices
42// -------------------------------------------------------------------------
43
44public function test_queue_notice_stores_notice(): void {
45$stored = null;
46Functions\when( 'get_option' )->justReturn( array() );
47Functions\when( 'update_option' )
48->alias( static function ( $key, $value ) use ( &$stored ) {
49$stored = $value;
50return true;
51} );
52
53$manager = new IPCD_Notification_Manager();
54$manager->queue_notice( 'Test message', 'error' );
55
56$this->assertIsArray( $stored );
57$this->assertCount( 1, $stored );
58$this->assertSame( 'Test message', $stored[0]['message'] );
59$this->assertSame( 'error', $stored[0]['type'] );
60}
61
62public function test_get_queued_notices_returns_array(): void {
63Functions\when( 'get_option' )->justReturn( array() );
64
65$manager = new IPCD_Notification_Manager();
66$notices = $manager->get_queued_notices();
67$this->assertIsArray( $notices );
68}
69
70// -------------------------------------------------------------------------
71// get_settings / save_settings
72// -------------------------------------------------------------------------
73
74public function test_get_settings_returns_defaults_when_no_saved_settings(): void {
75Functions\when( 'get_option' )->alias( static function ( $key, $default = false ) {
76if ( 'admin_email' === $key ) {
77return 'admin@example.com';
78}
79return array();
80} );
81
82$manager = new IPCD_Notification_Manager();
83$settings = $manager->get_settings();
84
85$this->assertArrayHasKey( 'email_notifications', $settings );
86$this->assertArrayHasKey( 'email_address', $settings );
87$this->assertArrayHasKey( 'auto_rollback', $settings );
88$this->assertArrayHasKey( 'scan_interval_hours', $settings );
89$this->assertSame( false, $settings['email_notifications'] );
90$this->assertSame( 6, $settings['scan_interval_hours'] );
91}
92
93public function test_save_settings_persists_sanitized_values(): void {
94$saved_value = null;
95Functions\when( 'update_option' )
96->alias( static function ( $key, $value ) use ( &$saved_value ) {
97$saved_value = $value;
98return true;
99} );
100
101$manager = new IPCD_Notification_Manager();
102$manager->save_settings( array(
103'email_notifications' => '1',
104'email_address' => 'owner@shop.com',
105'auto_rollback' => '0',
106'scan_interval_hours' => '12',
107) );
108
109$this->assertIsArray( $saved_value );
110$this->assertTrue( $saved_value['email_notifications'] );
111$this->assertSame( 'owner@shop.com', $saved_value['email_address'] );
112$this->assertFalse( $saved_value['auto_rollback'] );
113$this->assertSame( 12, $saved_value['scan_interval_hours'] );
114}
115}
Addedintelligent-plugin-conflict-detector/tests/Unit/RollbackManagerTest.php+124−0View fileUnifiedSplit
@@ -0,0 +1,124 @@
1
2/**
3 * Unit tests for IPCD_Rollback_Manager.
4 *
5 * @package IPCD\Tests\Unit
6 */
7
8namespace IPCD\Tests\Unit;
9
10use Brain\Monkey;
11use Brain\Monkey\Functions;
12use IPCD_Plugin_State_Manager;
13use IPCD_Rollback_Manager;
14use Mockery;
15use PHPUnit\Framework\TestCase;
16
17/**
18 * Class RollbackManagerTest
19 */
20class RollbackManagerTest extends TestCase {
21
22 protected function setUp(): void {
23 parent::setUp();
24 Monkey\setUp();
25
26 Functions\when( 'get_option' )->justReturn( array() );
27 Functions\when( 'update_option' )->justReturn( true );
28 Functions\when( 'delete_option' )->justReturn( true );
29 Functions\when( 'do_action' )->justReturn( null );
30 Functions\when( '__' )->returnArg( 1 );
31 Functions\when( 'sanitize_text_field' )->returnArg( 1 );
32 Functions\when( 'current_time' )->justReturn( '2026-01-01 00:00:00' );
33 Functions\when( 'wp_json_encode' )->alias( 'json_encode' );
34 }
35
36 protected function tearDown(): void {
37 Mockery::close();
38 Monkey\tearDown();
39 parent::tearDown();
40 }
41
42 // -------------------------------------------------------------------------
43 // rollback – success path
44 // -------------------------------------------------------------------------
45
46 public function test_rollback_returns_success_when_snapshot_restored(): void {
47 $state_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
48
49 $state_mock->shouldReceive( 'capture_snapshot' )
50 ->once()
51 ->andReturn( 'pre_snapshot_123' );
52
53 $state_mock->shouldReceive( 'restore_snapshot' )
54 ->once()
55 ->with( 'target_snapshot' )
56 ->andReturn( true );
57
58 // update_option is called to store rollback history.
59 Functions\when( 'update_option' )->justReturn( true );
60 Functions\when( 'get_option' )->justReturn( array() );
61
62 $rollback = new IPCD_Rollback_Manager( $state_mock );
63 $result = $rollback->rollback( 'target_snapshot' );
64
65 $this->assertTrue( $result['success'] );
66 $this->assertArrayHasKey( 'pre_rollback_snapshot', $result );
67 }
68
69 // -------------------------------------------------------------------------
70 // rollback – failure path
71 // -------------------------------------------------------------------------
72
73 public function test_rollback_returns_failure_when_snapshot_not_found(): void {
74 $state_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
75
76 $state_mock->shouldReceive( 'capture_snapshot' )
77 ->once()
78 ->andReturn( 'pre_snapshot_456' );
79
80 $state_mock->shouldReceive( 'restore_snapshot' )
81 ->once()
82 ->andReturn( false );
83
84 Functions\when( 'update_option' )->justReturn( true );
85 Functions\when( 'get_option' )->justReturn( array() );
86
87 $rollback = new IPCD_Rollback_Manager( $state_mock );
88 $result = $rollback->rollback( 'missing_snapshot' );
89
90 $this->assertFalse( $result['success'] );
91 }
92
93 // -------------------------------------------------------------------------
94 // get_history
95 // -------------------------------------------------------------------------
96
97 public function test_get_history_returns_array(): void {
98 $state_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
99 $rollback = new IPCD_Rollback_Manager( $state_mock );
100
101 $history = $rollback->get_history();
102 $this->assertIsArray( $history );
103 }
104
105 // -------------------------------------------------------------------------
106 // clear_history
107 // -------------------------------------------------------------------------
108
109 public function test_clear_history_deletes_option(): void {
110 $state_mock = Mockery::mock( IPCD_Plugin_State_Manager::class );
111 $deleted_key = null;
112
113 Functions\when( 'delete_option' )
114 ->alias( static function ( $key ) use ( &$deleted_key ) {
115 $deleted_key = $key;
116 return true;
117 } );
118
119 $rollback = new IPCD_Rollback_Manager( $state_mock );
120 $rollback->clear_history();
121
122 $this->assertSame( IPCD_Rollback_Manager::HISTORY_OPTION, $deleted_key );
123 }
124}
Addedintelligent-plugin-conflict-detector/tests/bootstrap.php+55−0View fileUnifiedSplit
@@ -0,0 +1,55 @@
1
2/**
3 * PHPUnit bootstrap file for IPCD tests.
4 *
5 * Uses Brain\Monkey to mock WordPress functions so tests can run without
6 * a live WordPress installation.
7 *
8 * @package IPCD\Tests
9 */
10
11// Composer autoloader.
12$autoloader = __DIR__ . '/../vendor/autoload.php';
13if ( ! file_exists( $autoloader ) ) {
14 echo "Composer autoloader not found. Run `composer install` inside the plugin directory.\n";
15 exit( 1 );
16}
17require_once $autoloader;
18
19// Define ABSPATH so plugin files don't bail out.
20if ( ! defined( 'ABSPATH' ) ) {
21 define( 'ABSPATH', sys_get_temp_dir() . '/wordpress/' );
22}
23
24// Stub WP_PLUGIN_DIR.
25if ( ! defined( 'WP_PLUGIN_DIR' ) ) {
26 define( 'WP_PLUGIN_DIR', ABSPATH . 'wp-content/plugins' );
27}
28
29// Constants that the plugin defines.
30if ( ! defined( 'IPCD_VERSION' ) ) {
31 define( 'IPCD_VERSION', '1.0.0' );
32}
33if ( ! defined( 'IPCD_PLUGIN_DIR' ) ) {
34 define( 'IPCD_PLUGIN_DIR', dirname( __DIR__ ) . '/' );
35}
36if ( ! defined( 'IPCD_PLUGIN_URL' ) ) {
37 define( 'IPCD_PLUGIN_URL', 'https://example.com/wp-content/plugins/intelligent-plugin-conflict-detector/' );
38}
39if ( ! defined( 'IPCD_PLUGIN_FILE' ) ) {
40 define( 'IPCD_PLUGIN_FILE', dirname( __DIR__ ) . '/intelligent-plugin-conflict-detector.php' );
41}
42if ( ! defined( 'IPCD_PLUGIN_BASENAME' ) ) {
43 define( 'IPCD_PLUGIN_BASENAME', 'intelligent-plugin-conflict-detector/intelligent-plugin-conflict-detector.php' );
44}
45if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
46 define( 'HOUR_IN_SECONDS', 3600 );
47}
48
49// Load the plugin classes under test (without loading the main plugin file
50// which has side-effects like hooking into WP and calling ipcd()).
51require_once IPCD_PLUGIN_DIR . 'includes/class-plugin-state-manager.php';
52require_once IPCD_PLUGIN_DIR . 'includes/class-conflict-detector.php';
53require_once IPCD_PLUGIN_DIR . 'includes/class-background-tester.php';
54require_once IPCD_PLUGIN_DIR . 'includes/class-rollback-manager.php';
55require_once IPCD_PLUGIN_DIR . 'includes/class-notification-manager.php';
056
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts