PluginProbe
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity / 3.3.9
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity v3.3.9
3.3.9 3.3.8 trunk 1.0 1.1.0 1.10.0 1.11.0 1.11.1 1.12.0 1.13.0 1.14.0 1.15.0 1.16.0 1.17.0 1.17.1 1.18.0 1.19.0 1.2.0 1.20.0 1.20.1 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 All 67 releases
← All changes | Loggers/Core/Logtivity_Plugin.php +194 -21 3.3.8 → 3.3.9 View file →
@@ -24,11 +24,15 @@
24 24
25 25 class Logtivity_Plugin extends Logtivity_Abstract_Logger
26 26 {
27 27 /**
28 + * Plugin data captured before a destructive file operation — a deletion, or
29 + * an update that overwrites the files — keyed by plugin slug. Read back once
30 + * the operation completes, when the on-disk copy is gone or already replaced.
31 + *
28 32 * @var array
29 33 */
30 - protected array $savedPluginData = [];
34 + protected array $capturedPluginData = [];
31 35
32 36 /**
33 37 * @inheritDoc
34 38 */
@@ -33,13 +37,24 @@
33 37 * @inheritDoc
34 38 */
35 39 protected function registerHooks(): void
36 40 {
37 - add_action('activated_plugin', [$this, 'pluginActivated'], 10, 2);
38 - add_action('deactivated_plugin', [$this, 'pluginDeactivated'], 10, 2);
41 + /*
42 + * Track the active plugin lists directly rather than the activated_plugin/
43 + * deactivated_plugin actions. Those actions are skipped when a plugin is
44 + * (de)activated silently (the $silent argument to activate_plugin() /
45 + * deactivate_plugins()), whereas the underlying option is always written.
46 + * Diffing the option therefore catches every status change, silent or not.
47 + */
48 + add_action('update_option_active_plugins', [$this, 'activePluginsUpdated'], 10, 2);
49 + add_action('add_option_active_plugins', [$this, 'activePluginsAdded'], 10, 2);
50 + add_action('update_site_option_active_sitewide_plugins', [$this, 'networkPluginsUpdated'], 10, 3);
51 + add_action('add_site_option_active_sitewide_plugins', [$this, 'networkPluginsAdded'], 10, 2);
52 +
39 53 add_action('delete_plugin', [$this, 'savePluginData'], 10, 1);
40 54 add_action('deleted_plugin', [$this, 'pluginDeleted'], 10, 2);
41 55
56 + add_filter('upgrader_pre_install', [$this, 'savePreUpdateVersion'], 10, 2);
42 57 add_action('upgrader_process_complete', [$this, 'upgradeProcessComplete'], 10, 2);
43 58
44 59 add_filter('editable_extensions', [$this, 'pluginFileModified'], 10, 2);
45 60 }
@@ -52,10 +67,9 @@
52 67 protected function getPluginData($plugin): ?array
53 68 {
54 69 if (is_string($plugin)) {
55 70 $slug = $plugin;
56 - $pluginData = $this->savedPluginData[$plugin]
57 - ?? get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin, false, false);
71 + $pluginData = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin, false, false);
58 72
59 73 } elseif ($plugin instanceof Plugin_Upgrader) {
60 74 if ($slug = $plugin->plugin_info()) {
61 75 $pluginData = get_plugin_data($plugin->result['local_destination'] . '/' . $slug, false, false);
@@ -96,27 +110,158 @@
96 110 }
97 111 }
98 112
99 113 /**
114 + * The active_plugins option was updated (single site).
115 + *
116 + * @param mixed $oldValue
117 + * @param mixed $value
118 + *
119 + * @return void
120 + */
121 + public function activePluginsUpdated($oldValue, $value): void
122 + {
123 + $this->logPluginListChanges((array)$oldValue, (array)$value, false);
124 + }
125 +
126 + /**
127 + * The active_plugins option was created (single site, first activation).
128 + *
129 + * @param string $option
130 + * @param mixed $value
131 + *
132 + * @return void
133 + */
134 + public function activePluginsAdded(string $option, $value): void
135 + {
136 + $this->logPluginListChanges([], (array)$value, false);
137 + }
138 +
139 + /**
140 + * The active_sitewide_plugins network option was updated (multisite).
141 + *
142 + * The value is keyed by plugin slug, so compare against the keys.
143 + *
144 + * @param string $option
145 + * @param mixed $value
146 + * @param mixed $oldValue
147 + *
148 + * @return void
149 + */
150 + public function networkPluginsUpdated(string $option, $value, $oldValue): void
151 + {
152 + $this->logPluginListChanges(
153 + array_keys((array)$oldValue),
154 + array_keys((array)$value),
155 + true
156 + );
157 + }
158 +
159 + /**
160 + * The active_sitewide_plugins network option was created (multisite).
161 + *
162 + * @param string $option
163 + * @param mixed $value
164 + *
165 + * @return void
166 + */
167 + public function networkPluginsAdded(string $option, $value): void
168 + {
169 + $this->logPluginListChanges([], array_keys((array)$value), true);
170 + }
171 +
172 + /**
173 + * Diff a previous and current list of active plugin slugs and log each
174 + * activation/deactivation. A set-based diff means a pure reordering of the
175 + * list (e.g. Logtivity moving itself to load first) produces no events.
176 + *
177 + * @param string[] $previous
178 + * @param string[] $current
179 + * @param bool $networkWide
180 + *
181 + * @return void
182 + */
183 + protected function logPluginListChanges(array $previous, array $current, bool $networkWide): void
184 + {
185 + foreach (array_diff($current, $previous) as $slug) {
186 + $this->pluginStateChanged('Activated', $slug, $networkWide);
187 + }
188 +
189 + foreach (array_diff($previous, $current) as $slug) {
190 + /*
191 + * A plugin whose files no longer exist was deleted, not merely
192 + * deactivated. Deleting an active plugin drops it from the active
193 + * list too, but that deletion is already reported via the
194 + * deleted_plugin hook, so skip the redundant deactivation event.
195 + */
196 + if ($this->pluginFileExists($slug) == false) {
197 + continue;
198 + }
199 +
200 + $this->pluginStateChanged('Deactivated', $slug, $networkWide);
201 + }
202 + }
203 +
204 + /**
205 + * Whether the plugin's main file still exists on disk.
206 + *
100 207 * @param string $slug
101 - * @param bool $networkWide
102 208 *
209 + * @return bool
210 + */
211 + protected function pluginFileExists(string $slug): bool
212 + {
213 + return is_readable(WP_PLUGIN_DIR . '/' . $slug);
214 + }
215 +
216 + /**
217 + * Capture a plugin's data before its files are removed or overwritten, so it
218 + * can still be reported once the on-disk copy is gone or replaced. Keyed by
219 + * slug and captured at most once per plugin per request.
220 + *
221 + * @param string $slug
222 + *
103 223 * @return void
104 224 */
105 - public function pluginActivated(string $slug, bool $networkWide): void
225 + protected function capturePluginData(string $slug): void
106 226 {
107 - $this->pluginStateChanged('Activated', $slug, $networkWide);
227 + if (
228 + isset($this->capturedPluginData[$slug]) == false
229 + && $this->pluginFileExists($slug)
230 + ) {
231 + if ($pluginData = $this->getPluginData($slug)) {
232 + $this->capturedPluginData[$slug] = $pluginData;
233 + }
234 + }
108 235 }
109 236
110 237 /**
111 - * @param string $slug
112 - * @param bool $networkDeactivating
238 + * Snapshot the version of every installed plugin. Used when an install does
239 + * not name the plugin being written (an upload, including one overwriting an
240 + * existing plugin), so the prior version can still be resolved by slug once
241 + * the install completes. Runs while the old files are still on disk, and
242 + * never overwrites data already captured for a specific plugin.
113 243 *
114 244 * @return void
115 245 */
116 - public function pluginDeactivated(string $slug, bool $networkDeactivating): void
246 + protected function captureAllPluginData(): void
117 247 {
118 - $this->pluginStateChanged('Deactivated', $slug, $networkDeactivating);
248 + if (function_exists('get_plugins') == false) {
249 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
250 + }
251 +
252 + foreach (get_plugins() as $slug => $pluginData) {
253 + if (isset($this->capturedPluginData[$slug]) == false) {
254 + $this->capturedPluginData[$slug] = array_merge(
255 + [
256 + 'Name' => 'Not Set',
257 + 'Slug' => $slug,
258 + 'Version' => 'Not Set',
259 + ],
260 + $pluginData
261 + );
262 + }
263 + }
119 264 }
120 265
121 266 /**
122 267 * @param string $plugin
@@ -124,13 +269,9 @@
124 269 * @return void
125 270 */
126 271 public function savePluginData(string $plugin): void
127 272 {
128 - if (empty($this->savedPluginData)) {
129 - if ($pluginData = $this->getPluginData($plugin)) {
130 - $this->savedPluginData[$plugin] = $pluginData;
131 - }
132 - }
273 + $this->capturePluginData($plugin);
133 274 }
134 275
135 276 /**
136 277 * @param string $pluginFile
@@ -139,9 +280,11 @@
139 280 * @return void
140 281 */
141 282 public function pluginDeleted(string $pluginFile, bool $deleted): void
142 283 {
143 - if ($plugin = $this->getPluginData($pluginFile)) {
284 + $plugin = $this->capturedPluginData[$pluginFile] ?? $this->getPluginData($pluginFile);
285 +
286 + if ($plugin) {
144 287 Logtivity::log()
145 288 ->setAction('Plugin Deleted')
146 289 ->setContext($plugin['Name'])
147 290 ->addMeta('Slug', $plugin['Slug'])
@@ -151,8 +294,32 @@
151 294 }
152 295 }
153 296
154 297 /**
298 + * Capture plugin data before an install/update overwrites the files, so the
299 + * old version can be logged once it completes. Fires while the old files are
300 + * still on disk. Updates name the plugin in $hookExtra, so only that one is
301 + * captured; uploads (including one overwriting an existing plugin) do not,
302 + * so every installed plugin is snapshotted and the replaced one resolved by
303 + * slug at completion. Registered as a filter, so the value is returned as-is.
304 + *
305 + * @param bool|WP_Error $response
306 + * @param array $hookExtra
307 + *
308 + * @return bool|WP_Error
309 + */
310 + public function savePreUpdateVersion($response, array $hookExtra)
311 + {
312 + if ($slug = $hookExtra['plugin'] ?? null) {
313 + $this->capturePluginData($slug);
314 + } else {
315 + $this->captureAllPluginData();
316 + }
317 +
318 + return $response;
319 + }
320 +
321 + /**
155 322 * @param WP_Upgrader $upgrader
156 323 * @param array $options
157 324 *
158 325 * @return void
@@ -201,14 +368,20 @@
201 368 $plugins = $this->getPluginList($upgrader, $options);
202 369
203 370 foreach ($plugins as $plugin) {
204 371 if ($pluginData = $this->getPluginData($plugin)) {
205 - Logtivity::log()
372 + $log = Logtivity::log()
206 373 ->setAction('Plugin ' . $action)
207 374 ->setContext($pluginData['Name'])
208 375 ->addMeta('Slug', $pluginData['Slug'])
209 - ->addMeta('Version', $pluginData['Version'])
210 - ->addMeta('Bulk', $bulk ? 'Yes' : 'No')
376 + ->addMeta('Version', $pluginData['Version']);
377 +
378 + $oldVersion = $this->capturedPluginData[$pluginData['Slug']]['Version'] ?? null;
379 + if ($oldVersion) {
380 + $log->addMeta('Old Version', $oldVersion);
381 + }
382 +
383 + $log->addMeta('Bulk', $bulk ? 'Yes' : 'No')
211 384 ->send();
212 385 }
213 386 }
214 387 }