PluginProbe
The WP Remote WordPress Plugin / 6.76
The WP Remote WordPress Plugin v6.76
6.76 6.72 6.69 6.65 6.62 6.48 6.47 4.87 4.97 5.05 5.09 5.16 5.22 5.24 5.25 5.38 5.41 5.42 5.45 5.47 5.53 5.56 5.65 5.68 5.72 All 54 releases
wpremote / wp_actlog.php

wp_actlog.php in The WP Remote WordPress Plugin 6.76, at wp_actlog.php

714 lines 29.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) exit;
4 if (!class_exists('BVWPActLog')) :
5
6 /*
7 * A sensor, not an interpreter. The server decides which hooks and keys are
8 * watched; each handler records what WordPress handed it and nothing more.
9 * Meaning (created, published, which setting changed) is assigned on the server.
10 */
11 class BVWPActLog {
12
13 public static $actlog_table = 'activities_store';
14 # Post content and comment bodies larger than this are omitted from the row.
15 const CONTENT_MAX_BYTES = 16384;
16 # A serialized row larger than this is stored without content, or not at all.
17 # The server refuses rows above 4 MiB and ships up to 1000 rows per request.
18 const EVENT_DATA_MAX_BYTES = 32768;
19 # Column widths for tables created by actlog wing 1.1+ and for tables
20 # widened through the alteractlogtable callback.
21 const IP_MAX_LENGTH = 50;
22 const EVENT_TYPE_MAX_LENGTH = 60;
23
24 # hook => array(handler, accepted args, priority). The server sends the hooks
25 # to register; only hooks named here can ever be bound.
26 public static $hooks = array(
27 'wp_after_insert_post' => array('post_saved_handler', 4),
28 'deleted_post' => array('post_deleted_handler', 2),
29 'post_stuck' => array('post_handler'),
30 'post_unstuck' => array('post_handler'),
31 'wp_insert_comment' => array('comment_handler', 2),
32 'edit_comment' => array('comment_handler'),
33 'transition_comment_status' => array('comment_status_handler', 3),
34 'created_term' => array('term_handler'),
35 'edited_term' => array('term_handler'),
36 'delete_term' => array('term_deleted_handler', 4),
37 'user_register' => array('user_handler'),
38 'profile_update' => array('user_updated_handler', 2),
39 'deleted_user' => array('user_deleted_handler', 3),
40 'set_user_role' => array('role_handler', 3),
41 'add_user_role' => array('role_handler', 2),
42 'remove_user_role' => array('role_handler', 2),
43 'activated_plugin' => array('plugin_handler'),
44 'deactivated_plugin' => array('plugin_handler'),
45 'deleted_plugin' => array('plugin_deleted_handler', 2),
46 'switch_theme' => array('theme_handler'),
47 'deleted_theme' => array('theme_deleted_handler', 2),
48 'wp_insert_site' => array('site_handler'),
49 'wp_delete_site' => array('site_handler'),
50 'wp_update_site' => array('site_updated_handler', 2),
51 'wp_login' => array('login_handler', 2),
52 'wp_logout' => array('logout_handler'),
53 'after_password_reset' => array('password_reset_handler'),
54 'upgrader_install_package_result' => array('package_result_handler', 2, PHP_INT_MAX),
55 'upgrader_process_complete' => array('upgrade_handler', 2),
56 'automatic_updates_complete' => array('automatic_updates_handler'),
57 '_core_updated_successfully' => array('core_upgrade_handler'),
58 'added_post_meta' => array('post_meta_handler', 4),
59 'updated_post_meta' => array('post_meta_handler', 4),
60 'deleted_post_meta' => array('post_meta_handler', 4),
61 'added_user_meta' => array('user_meta_handler', 4),
62 'updated_user_meta' => array('user_meta_handler', 4),
63 'deleted_user_meta' => array('user_meta_handler', 4),
64 'added_option' => array('option_handler', 3),
65 'updated_option' => array('option_handler', 3),
66 'deleted_option' => array('option_handler', 3),
67 # Multisite network options (sitemeta).
68 'add_site_option' => array('option_handler', 3),
69 'update_site_option' => array('option_handler', 3),
70 'delete_site_option' => array('option_handler', 3),
71 'woocommerce_before_product_object_save' => array('product_before_save_handler'),
72 'woocommerce_after_product_object_save' => array('product_saved_handler'),
73 'woocommerce_new_order' => array('order_handler', 2),
74 'woocommerce_before_order_object_save' => array('order_before_save_handler'),
75 'woocommerce_after_order_object_save' => array('order_saved_handler'),
76 'woocommerce_order_status_changed' => array('order_status_handler', 4),
77 'woocommerce_trash_order' => array('order_handler'),
78 'woocommerce_delete_order' => array('order_deleted_handler'),
79 'trashed_post' => array('order_post_handler'),
80 'untrashed_post' => array('order_post_handler'),
81 'woocommerce_attribute_added' => array('attribute_handler', 2),
82 'woocommerce_attribute_updated' => array('attribute_updated_handler', 3),
83 'woocommerce_attribute_deleted' => array('attribute_deleted_handler', 3),
84 'woocommerce_tax_rate_added' => array('tax_rate_handler', 2),
85 'woocommerce_tax_rate_updated' => array('tax_rate_handler', 2),
86 'woocommerce_tax_rate_deleted' => array('tax_rate_deleted_handler'),
87 'woocommerce_shipping_zone_method_added' => array('shipping_method_handler', 3),
88 'woocommerce_shipping_zone_method_status_toggled' => array('shipping_method_handler', 4),
89 'woocommerce_shipping_zone_method_deleted' => array('shipping_method_handler', 3),
90 'woocommerce_grant_product_download_access' => array('download_granted_handler'),
91 'woocommerce_ajax_revoke_access_to_product_download' => array('download_revoked_handler', 4)
92 );
93
94 public $db;
95 public $settings;
96 public $bvinfo;
97 public $request_id;
98 public $config;
99 public $pending_changes = array();
100 public $installed_packages = array();
101
102 public function __construct($db, $settings, $info, $config) {
103 $this->db = $db;
104 $this->settings = $settings;
105 $this->bvinfo = $info;
106 $this->request_id = WPRInfo::getRequestID();
107 $this->config = is_array($config) ? $config : array();
108 }
109
110 function init() {
111 add_action('wpr_clear_actlog_config', array($this, 'clearConfig'));
112 foreach ($this->option('hooks') as $hook) {
113 if (!is_string($hook) || !isset(self::$hooks[$hook])) continue;
114 $binding = self::$hooks[$hook];
115 $this->listen($hook, $binding[0], isset($binding[1]) ? $binding[1] : 1, isset($binding[2]) ? $binding[2] : 10);
116 }
117 }
118
119 # Fired from the plugin's uninstall hook; this module owns its own table.
120 public function clearConfig() {
121 $this->db->dropBVTable(BVWPActLog::$actlog_table);
122 }
123
124 function option($key) {
125 return isset($this->config[$key]) && is_array($this->config[$key]) ? $this->config[$key] : array();
126 }
127
128 function watching($hook) {
129 return in_array($hook, $this->option('hooks'), true);
130 }
131
132 # Everything captured by name (post types, comment types, setting keys) is
133 # selected from one server list of the same shape: allow mode captures only
134 # the listed names, deny mode everything except them.
135 function selected($list, $name) {
136 $list = $this->option($list);
137 $listed = $this->listed($list, $name);
138 return isset($list['mode']) && $list['mode'] === 'deny' ? !$listed : $listed;
139 }
140
141 function listed($list, $name) {
142 if (!is_string($name)) return false;
143 if (isset($list['names']) && in_array($name, (array) $list['names'], true)) return true;
144 foreach (isset($list['patterns']) ? (array) $list['patterns'] : array() as $pattern) {
145 if (is_string($pattern) && WPRHelper::safePregMatch($pattern, $name)) return true;
146 }
147 return false;
148 }
149
150 /*
151 * Every hook goes through this wrapper so that a WordPress or plugin API change
152 * that breaks one handler can never break the site. Filters get their first
153 * argument back untouched when the handler fails or the event is ignored.
154 */
155 function listen($hook, $handler, $accepted_args = 1, $priority = 10) {
156 $self = $this;
157 add_filter($hook, function() use ($self, $handler) {
158 $args = func_get_args();
159 $passthrough = isset($args[0]) ? $args[0] : null;
160 try {
161 $result = call_user_func_array(array($self, $handler), $args);
162 return is_null($result) ? $passthrough : $result;
163 } catch (Throwable $e) {
164 return $passthrough;
165 } catch (Exception $e) {
166 # PHP 5 does not define Throwable, so keep the legacy exception path safe.
167 return $passthrough;
168 }
169 }, $priority, $accepted_args);
170 }
171
172 /* ---------- snapshots ---------- */
173
174 function get_post($post_id, $post = null, $with_details = null) {
175 $post = is_null($post) ? get_post($post_id) : $post;
176 $with_details = is_null($with_details) ? !empty($this->config['capture_details']) : $with_details;
177 $data = array('id' => $post_id);
178 if (empty($post))
179 return $data;
180 $data['title'] = $post->post_title;
181 $data['status'] = $post->post_status;
182 $data['type'] = $post->post_type;
183 $data['parent_id'] = intval($post->post_parent);
184 $data['url'] = get_permalink($post);
185 $data['date'] = $post->post_date;
186 # The reason describes site policy, not this call: identity-only snapshots
187 # (comment context, settings, deletions) carry no reason on a site with details on.
188 if (!in_array($post->post_type, array('post', 'page'), true)) {
189 $data['detail_omission_reason'] = 'unsupported';
190 } elseif (empty($this->config['capture_details'])) {
191 $data['detail_omission_reason'] = 'disabled';
192 } elseif ($with_details) {
193 $data['slug'] = $post->post_name;
194 $data['author_id'] = intval($post->post_author);
195 $data['modified_date'] = $post->post_modified;
196 $data['excerpt'] = $post->post_excerpt;
197 $this->add_content($data, 'content', $post->post_content);
198 }
199 return $data;
200 }
201
202 function add_content(&$data, $key, $value) {
203 $value = is_scalar($value) ? (string) $value : '';
204 if (strlen($value) < self::CONTENT_MAX_BYTES)
205 $data[$key] = $value;
206 else
207 $data[$key . '_omission_reason'] = 'too_large';
208 }
209
210 function get_comment($comment) {
211 $data = array('id' => intval($comment->comment_ID), 'author' => $comment->comment_author, 'post_id' => $comment->comment_post_ID);
212 if (!empty($this->config['capture_details']))
213 $this->add_content($data, 'body', $comment->comment_content);
214 return $data;
215 }
216
217 function get_term($term_id) {
218 $term = get_term($term_id);
219 $data = array('id' => $term_id);
220 if (!empty($term) && !is_wp_error($term)) {
221 $data['name'] = $term->name;
222 $data['slug'] = $term->slug;
223 $data['taxonomy'] = $term->taxonomy;
224 }
225 return $data;
226 }
227
228 function get_user($user, $user_id = 0) {
229 $user = is_object($user) ? $user : get_userdata($user);
230 $data = array('id' => !empty($user) && isset($user->ID) ? $user->ID : $user_id);
231 if (!empty($user)) {
232 $data['username'] = $user->user_login;
233 $data['email'] = $user->user_email;
234 $data['role'] = isset($user->roles) ? $user->roles : array();
235 }
236 return $data;
237 }
238
239 function get_order($id, $order) {
240 $data = array('id' => $id);
241 if (is_object($order)) {
242 $data['title'] = 'Order #' . $order->get_order_number();
243 $data['status'] = $order->get_status('edit');
244 }
245 return $data;
246 }
247
248 function get_ip($ipHeader) {
249 $ip = '127.0.0.1';
250 if ($ipHeader && is_array($ipHeader)) {
251 if (array_key_exists($ipHeader['hdr'], $_SERVER)) {
252 $_ips = preg_split("/(,| |\t)/", WPRHelper::getRawParam('SERVER', $ipHeader['hdr']));
253 if (array_key_exists(intval($ipHeader['pos']), $_ips)) {
254 $ip = $_ips[intval($ipHeader['pos'])];
255 }
256 }
257 } else if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
258 $ip = WPRHelper::getRawParam('SERVER', 'REMOTE_ADDR');
259 }
260
261 $ip = trim($ip);
262 if (WPRHelper::safePregMatch('/^\[([0-9a-fA-F:]+)\](:[0-9]+)$/', $ip, $matches)) {
263 $ip = $matches[1];
264 } elseif (WPRHelper::safePregMatch('/^([0-9.]+)(:[0-9]+)$/', $ip, $matches)) {
265 $ip = $matches[1];
266 }
267
268 return $ip;
269 }
270
271 /* ---------- storage ---------- */
272
273 function add_activity($event_data, $user = null) {
274 $event_data['schema_version'] = 2;
275 $event_data['hook'] = current_filter();
276 $serialized = $this->fit($event_data);
277 if ($serialized === false)
278 return false;
279 if (!function_exists('wp_get_current_user')) {
280 @include_once(ABSPATH . "wp-includes/pluggable.php");
281 }
282
283 $user = $user ? $user : wp_get_current_user();
284 $values = array();
285 if (!empty($user)) {
286 $values["user_id"] = $user->ID;
287 $values["username"] = $user->user_login;
288 }
289 $values["request_id"] = $this->request_id;
290 $values["site_id"] = get_current_blog_id();
291 $ip_header = isset($this->config['ip_header']) ? $this->config['ip_header'] : false;
292 $values["ip"] = substr($this->get_ip($ip_header), 0, self::IP_MAX_LENGTH);
293 $values["event_type"] = 'bv_activity';
294 $values["event_data"] = $serialized;
295 $values["time"] = time();
296 return $this->db->replaceIntoBVTable(BVWPActLog::$actlog_table, $values) !== false;
297 }
298
299 # Content is dropped whole, never truncated, when the row would exceed the cap.
300 # Returns the serialized row, or false when even identity alone does not fit.
301 function fit($event_data) {
302 $serialized = maybe_serialize($event_data);
303 if (strlen($serialized) <= self::EVENT_DATA_MAX_BYTES)
304 return $serialized;
305 foreach ($event_data as $entity => $snapshot) {
306 if (!is_array($snapshot)) continue;
307 foreach (array('content', 'excerpt', 'body') as $key) {
308 if (!isset($snapshot[$key])) continue;
309 unset($snapshot[$key]);
310 $snapshot[$key . '_omission_reason'] = 'too_large';
311 }
312 $event_data[$entity] = $snapshot;
313 }
314 $serialized = maybe_serialize($event_data);
315 return strlen($serialized) <= self::EVENT_DATA_MAX_BYTES ? $serialized : false;
316 }
317
318 /* ---------- posts ---------- */
319
320 function skip_post($post) {
321 return !$post || $post->post_status === 'auto-draft' || !$this->selected('post_types', $post->post_type);
322 }
323
324 # Names of the standard fields that differ are recorded as evidence; the
325 # snapshots omit content the server must not see, so they cannot show it.
326 const POST_FIELDS = array('post_title', 'post_content', 'post_excerpt', 'post_status', 'post_name', 'post_author',
327 'post_parent', 'post_password', 'menu_order', 'comment_status', 'ping_status', 'post_date');
328
329 function post_saved_handler($id, $post, $update, $before) {
330 if ($this->skip_post($post)) return;
331 $data = array('post' => $this->get_post($id, $post), 'updated' => (bool) $update);
332 if ($before) {
333 $data['before'] = $this->get_post($id, $before);
334 $data['changed_fields'] = array();
335 foreach (self::POST_FIELDS as $field) {
336 if ((isset($before->$field) ? $before->$field : null) != (isset($post->$field) ? $post->$field : null)) $data['changed_fields'][] = $field;
337 }
338 if ($update && empty($data['changed_fields'])) return;
339 }
340 $this->add_activity($data);
341 }
342
343 function post_handler($post_id) {
344 $post = get_post($post_id);
345 if (!$this->skip_post($post)) $this->add_activity(array('post' => $this->get_post($post_id, $post, false)));
346 }
347
348 function post_deleted_handler($id, $post) {
349 if ($this->watching('woocommerce_delete_order') && $this->is_stored_as_post($post)) return $this->order_post_handler($id, $post);
350 if ($this->skip_post($post)) return;
351 $this->add_activity(array('post' => $this->get_post($id, $post, false)));
352 }
353
354 /* ---------- comments ---------- */
355
356 function skip_comment($comment) {
357 return !$comment || !$this->selected('comment_types', $comment->comment_type);
358 }
359
360 function comment_handler($comment_id, $comment = null) {
361 $comment = $comment ? $comment : get_comment($comment_id);
362 if ($this->skip_comment($comment)) return;
363 $this->add_activity(array(
364 'comment' => $this->get_comment($comment),
365 'post' => $this->get_post($comment->comment_post_ID, null, false)
366 ));
367 }
368
369 function comment_status_handler($new_status, $old_status, $comment) {
370 if ($new_status === $old_status || $old_status === 'new' || $this->skip_comment($comment)) return;
371 $this->add_activity(array(
372 'comment' => $this->get_comment($comment),
373 'post' => $this->get_post($comment->comment_post_ID, null, false),
374 'old_status' => $old_status,
375 'new_status' => $new_status
376 ));
377 }
378
379 /* ---------- terms, users, roles ---------- */
380
381 function term_handler($term_id) {
382 $this->add_activity(array('term' => $this->get_term($term_id)));
383 }
384
385 function term_deleted_handler($id, $tt_id, $taxonomy, $term) {
386 $this->add_activity(array('term' => array('id' => $id, 'name' => $term->name, 'slug' => $term->slug, 'taxonomy' => $taxonomy)));
387 }
388
389 function user_handler($user_id) {
390 $this->add_activity(array('user' => $this->get_user($user_id)));
391 }
392
393 function user_updated_handler($user_id, $old_userdata) {
394 $this->add_activity(array('old_user' => $this->get_user($old_userdata, $user_id), 'user' => $this->get_user($user_id)));
395 }
396
397 function user_deleted_handler($id, $reassign, $user) {
398 $this->add_activity(array('user' => $this->get_user($user, $id)));
399 }
400
401 function role_handler($id, $role, $old_roles = null) {
402 $this->add_activity(array('user' => $this->get_user($id), 'role' => $role, 'old_roles' => $old_roles));
403 }
404
405 /* ---------- sessions ---------- */
406
407 # Login and logout use the hook's subject as the actor: WordPress has already
408 # cleared the current user when the logout hook fires.
409 function login_handler($user_login, $user) {
410 $this->add_activity(array('user' => $this->get_user($user)), $user);
411 }
412
413 function logout_handler($user_id = 0) {
414 $user = get_userdata($user_id);
415 $this->add_activity(array('user' => $this->get_user($user, $user_id)), $user);
416 }
417
418 function password_reset_handler($user) {
419 if (!empty($user)) $this->add_activity(array('user' => $this->get_user($user)));
420 }
421
422 /* ---------- plugins, themes, sites ---------- */
423
424 function plugin_handler($plugin) {
425 $this->add_activity(array('plugin' => $plugin));
426 }
427
428 function plugin_deleted_handler($plugin, $deleted) {
429 if ($deleted) $this->add_activity(array('plugin' => $plugin));
430 }
431
432 function theme_handler($theme_name) {
433 $this->add_activity(array('theme' => $theme_name));
434 }
435
436 function theme_deleted_handler($stylesheet, $deleted) {
437 if ($deleted) $this->add_activity(array('theme' => $stylesheet));
438 }
439
440 function get_site($site) {
441 $data = array('id' => intval($site->blog_id), 'domain' => $site->domain, 'path' => $site->path, 'url' => 'https://' . $site->domain . $site->path);
442 foreach (array('public', 'archived', 'spam', 'deleted') as $flag)
443 $data[$flag] = isset($site->$flag) ? (bool) intval($site->$flag) : null;
444 if (isset($site->blogname)) $data['name'] = $site->blogname;
445 return $data;
446 }
447
448 function site_handler($site) {
449 $this->add_activity(array('blog' => $this->get_site($site)));
450 }
451
452 function site_updated_handler($site, $before) {
453 $data = array('blog' => $this->get_site($site), 'before' => $this->get_site($before));
454 if ($data['blog'] != $data['before']) $this->add_activity($data);
455 }
456
457 /* ---------- settings: values ride along, bounded ---------- */
458
459 # Like content, an oversized value is omitted whole and the omission recorded.
460 function add_value(&$data, $key, $value) {
461 if (is_null($value)) return;
462 if (strlen(is_scalar($value) ? (string) $value : maybe_serialize($value)) < self::CONTENT_MAX_BYTES)
463 $data[$key] = $value;
464 else
465 $data[$key . '_omission_reason'] = 'too_large';
466 }
467
468 function post_meta_handler($meta_ids, $post_id, $key, $value = null) {
469 if (!$this->selected('post_meta', $key)) return;
470 $post = get_post($post_id);
471 if ($this->skip_post($post)) return;
472 $data = array('key' => $key, 'post' => $this->get_post($post_id, $post, false));
473 $this->add_value($data, current_filter() === 'deleted_post_meta' ? 'old_value' : 'new_value', $value);
474 $this->add_activity($data);
475 }
476
477 function user_meta_handler($meta_ids, $user_id, $key, $value = null) {
478 if (!$this->selected('user_meta', $key)) return;
479 $data = array('key' => $key, 'user' => $this->get_user($user_id));
480 $this->add_value($data, current_filter() === 'deleted_user_meta' ? 'old_value' : 'new_value', $value);
481 $this->add_activity($data);
482 }
483
484 # Options and multisite network options (sitemeta). Transients are cache,
485 # never activity, whatever the server selected. updated_option passes
486 # (key, old, new) but update_site_option (key, new, old); the add hooks pass
487 # only the new value and the delete hooks none.
488 function option_handler($key, $first = null, $second = null) {
489 if (strpos($key, '_transient_') === 0 || strpos($key, '_site_transient_') === 0) return;
490 if (!$this->selected('option', $key)) return;
491 $old = $new = null;
492 switch (current_filter()) {
493 case 'updated_option': list($old, $new) = array($first, $second); break;
494 case 'update_site_option': list($new, $old) = array($first, $second); break;
495 case 'added_option': case 'add_site_option': $new = $first; break;
496 }
497 $data = array('key' => $key);
498 $this->add_value($data, 'old_value', $old);
499 $this->add_value($data, 'new_value', $new);
500 $this->add_activity($data);
501 }
502
503 /* ---------- upgrades ---------- */
504
505 function is_relative_package_path($file) {
506 return is_string($file) && $file !== '' && $file[0] !== '/' && strpos($file, '..') === false;
507 }
508
509 # Remember successful installs only. Automatic updates can still roll back
510 # after this hook, so the activity is emitted at final completion.
511 function package_result_handler($result, $extra) {
512 if (!is_array($extra)) return $result;
513 foreach (array('plugin', 'theme') as $type) {
514 if (!isset($extra[$type]) || !$this->is_relative_package_path($extra[$type])) continue;
515 if (is_array($result)) $this->installed_packages[$type][$extra[$type]] = true;
516 else unset($this->installed_packages[$type][$extra[$type]]);
517 }
518 return $result;
519 }
520
521 function completed_package($type, $file) {
522 if (!isset($this->installed_packages[$type][$file])) return;
523 unset($this->installed_packages[$type][$file]);
524 $item = $type === 'plugin' ? $this->get_plugin_data($file) : $this->get_theme_data($file);
525 $this->add_activity(array('action' => 'update', 'type' => $type, $type . 's' => array($item)));
526 }
527
528 function upgrade_handler($upgrader, $data) {
529 if (!is_array($data) || !isset($data['action'], $data['type']) || !in_array($data['type'], array('plugin', 'theme'), true))
530 return;
531 $type = $data['type'];
532 if ($data['action'] === 'update') {
533 if (isset($upgrader->skin) && $upgrader->skin instanceof Automatic_Upgrader_Skin) return;
534 $files = isset($data[$type]) ? array($data[$type]) : (isset($data[$type . 's']) ? (array) $data[$type . 's'] : array());
535 foreach ($files as $file) {
536 if ($this->is_relative_package_path($file)) $this->completed_package($type, $file);
537 }
538 } elseif ($data['action'] === 'install' && isset($upgrader->result) && is_array($upgrader->result)) {
539 $headers = $type === 'plugin' ? (isset($upgrader->new_plugin_data) ? $upgrader->new_plugin_data : array()) : (isset($upgrader->new_theme_data) ? $upgrader->new_theme_data : array());
540 $this->add_activity(array('action' => 'install', 'type' => $type, $type . 's' => array($this->title_and_version($headers))));
541 }
542 }
543
544 function automatic_updates_handler($results) {
545 foreach (array('plugin', 'theme') as $type) {
546 foreach (isset($results[$type]) ? $results[$type] : array() as $result) {
547 $file = isset($result->item->$type) ? $result->item->$type : null;
548 if (!$this->is_relative_package_path($file)) continue;
549 if ($result->result && !is_wp_error($result->result)) $this->completed_package($type, $file);
550 unset($this->installed_packages[$type][$file]);
551 }
552 }
553 }
554
555 function core_upgrade_handler($new_wp_version) {
556 global $wp_version;
557 $this->add_activity(array('type' => 'core', 'wp_core' => array('prev_version' => $wp_version, 'new_version' => $new_wp_version)));
558 }
559
560 function title_and_version($headers) {
561 return array(
562 'title' => isset($headers['Name']) ? $headers['Name'] : '',
563 'version' => isset($headers['Version']) ? $headers['Version'] : ''
564 );
565 }
566
567 function get_plugin_data($file) {
568 if (!function_exists('get_plugin_data'))
569 @include_once(ABSPATH . 'wp-admin/includes/plugin.php');
570 $headers = defined('WP_PLUGIN_DIR') && function_exists('get_plugin_data') ? get_plugin_data(WP_PLUGIN_DIR . '/' . $file, false, false) : array();
571 $item = $this->title_and_version($headers);
572 $item['file'] = $file;
573 $item['slug'] = strpos($file, '/') !== false ? dirname($file) : preg_replace('/\.php$/', '', $file);
574 return $item;
575 }
576
577 function get_theme_data($stylesheet) {
578 # Read installed headers without depending on the theme object cache.
579 $headers = get_file_data(get_theme_root($stylesheet) . '/' . $stylesheet . '/style.css', array('Name' => 'Theme Name', 'Version' => 'Version'));
580 $item = $this->title_and_version($headers);
581 $item['stylesheet'] = $stylesheet;
582 return $item;
583 }
584
585 /* ---------- WooCommerce products and orders ---------- */
586
587 # Woo exposes pending changes only before save, so the change set is held
588 # until the matching after-save hook confirms the write.
589 function remember_changes($object, $allowed) {
590 $fields = array_values(array_intersect(array_keys($object->get_changes()), $allowed));
591 $this->pending_changes[spl_object_hash($object)] = array('new' => !$object->get_id(), 'fields' => $fields);
592 }
593
594 function take_changes($object) {
595 $key = spl_object_hash($object);
596 $changes = isset($this->pending_changes[$key]) ? $this->pending_changes[$key] : null;
597 unset($this->pending_changes[$key]);
598 return $changes && $object->get_id() ? $changes : null;
599 }
600
601 function product_before_save_handler($product) {
602 $this->remember_changes($product, $this->option('product_fields'));
603 }
604
605 function product_saved_handler($product) {
606 $changes = $this->take_changes($product);
607 if (!$changes || (!$changes['new'] && empty($changes['fields']))) return;
608 $post = array(
609 'id' => $product->get_id(), 'title' => $product->get_name('edit'), 'status' => $product->get_status('edit'),
610 'type' => $product->is_type('variation') ? 'product_variation' : 'product', 'parent_id' => $product->get_parent_id('edit')
611 );
612 $this->add_activity(array('post' => $post, 'updated' => !$changes['new'], 'changed_fields' => $changes['fields']));
613 }
614
615 function order_before_save_handler($order) {
616 $this->remember_changes($order, $this->option('order_fields'));
617 }
618
619 function order_saved_handler($order) {
620 $changes = $this->take_changes($order);
621 if (!$changes || $changes['new'] || empty($changes['fields'])) return;
622 $this->add_activity(array('order' => $this->get_order($order->get_id(), $order), 'changed_fields' => $changes['fields']));
623 }
624
625 function order_handler($id, $order = null) {
626 $order = $order ? $order : (function_exists('wc_get_order') ? wc_get_order($id) : null);
627 $this->add_activity(array('order' => $this->get_order($id, $order)));
628 }
629
630 function order_deleted_handler($id) {
631 $this->add_activity(array('order' => array('id' => $id)));
632 }
633
634 function order_status_handler($id, $from, $to, $order) {
635 $this->add_activity(array('order' => $this->get_order($id, $order), 'old_status' => $from, 'new_status' => $to));
636 }
637
638 # Orders kept in the posts table are observed through WordPress trash,
639 # restore and delete, only while the order hooks are watched. With HPOS
640 # these posts are only synchronization copies.
641 function is_stored_as_post($post) {
642 return $post && $post->post_type === 'shop_order' &&
643 class_exists('\Automattic\WooCommerce\Utilities\OrderUtil') &&
644 !\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
645 }
646
647 function order_post_handler($id, $post = null) {
648 $post = is_object($post) ? $post : get_post($id);
649 if (!$this->is_stored_as_post($post)) return;
650 $this->add_activity(array('order' => array('id' => $id, 'title' => 'Order #' . $id, 'status' => preg_replace('/^wc-/', '', $post->post_status))));
651 }
652
653 /* ---------- WooCommerce configuration ---------- */
654
655 function get_attribute($attribute_id, $attribute_data) {
656 $data = array('id' => $attribute_id);
657 if (is_array($attribute_data)) {
658 $data['name'] = isset($attribute_data['attribute_label']) ? $attribute_data['attribute_label'] : '';
659 $data['slug'] = isset($attribute_data['attribute_name']) ? $attribute_data['attribute_name'] : '';
660 }
661 return $data;
662 }
663
664 function attribute_handler($attribute_id, $attribute_data) {
665 $this->add_activity(array('attribute' => $this->get_attribute($attribute_id, $attribute_data)));
666 }
667
668 function attribute_updated_handler($attribute_id, $attribute_data, $old_slug = null) {
669 $attribute = $this->get_attribute($attribute_id, $attribute_data);
670 if (is_string($old_slug)) $attribute['old_slug'] = $old_slug;
671 $this->add_activity(array('attribute' => $attribute));
672 }
673
674 function attribute_deleted_handler($attribute_id, $name = null, $taxonomy = null) {
675 $attribute = array('id' => $attribute_id);
676 if (is_string($name)) $attribute['name'] = $name;
677 if (is_string($taxonomy)) $attribute['slug'] = preg_replace('/^pa_/', '', $taxonomy);
678 $this->add_activity(array('attribute' => $attribute));
679 }
680
681 function tax_rate_handler($tax_rate_id, $tax_rate) {
682 $data = array('id' => $tax_rate_id);
683 if (is_array($tax_rate)) {
684 $data['name'] = isset($tax_rate['tax_rate_name']) ? $tax_rate['tax_rate_name'] : '';
685 $data['country'] = isset($tax_rate['tax_rate_country']) ? $tax_rate['tax_rate_country'] : '';
686 $data['rate'] = isset($tax_rate['tax_rate']) ? $tax_rate['tax_rate'] : '';
687 }
688 $this->add_activity(array('tax_rate' => $data));
689 }
690
691 function tax_rate_deleted_handler($tax_rate_id) {
692 $this->add_activity(array('tax_rate' => array('id' => $tax_rate_id)));
693 }
694
695 function shipping_method_handler($instance_id, $method_id, $zone_id, $enabled = null) {
696 $data = array('instance_id' => absint($instance_id), 'method_id' => $method_id, 'zone_id' => $zone_id);
697 if (!is_null($enabled)) $data['enabled'] = (bool) $enabled;
698 $this->add_activity($data);
699 }
700
701 function download_granted_handler($data) {
702 if (!is_array($data)) return;
703 $event_data = array();
704 foreach (array('download_id', 'user_id', 'order_id', 'product_id') as $key)
705 $event_data[$key] = isset($data[$key]) ? $data[$key] : '';
706 $this->add_activity($event_data);
707 }
708
709 function download_revoked_handler($download_id, $product_id, $order_id, $permission_id = null) {
710 $this->add_activity(array('download_id' => $download_id, 'product_id' => $product_id, 'order_id' => $order_id, 'permission_id' => $permission_id));
711 }
712 }
713 endif;
714