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
← All changes | wp_actlog.php +538 -353 5.426.76 View file →
@@ -2,17 +2,103 @@
2 2
3 3 if (!defined('ABSPATH')) exit;
4 4 if (!class_exists('BVWPActLog')) :
5 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 + */
6 11 class BVWPActLog {
7 12
8 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 +
9 94 public $db;
10 95 public $settings;
11 96 public $bvinfo;
12 97 public $request_id;
13 - public $ip_header;
14 - public $ignored_events;
98 + public $config;
99 + public $pending_changes = array();
100 + public $installed_packages = array();
15 101
16 102 public function __construct($db, $settings, $info, $config) {
17 103 $this->db = $db;
18 104 $this->settings = $settings;
@@ -17,36 +103,115 @@
17 103 $this->db = $db;
18 104 $this->settings = $settings;
19 105 $this->bvinfo = $info;
20 106 $this->request_id = WPRInfo::getRequestID();
21 - $this->ip_header = array_key_exists('ip_header', $config) ? $config['ip_header'] : false;
22 - $this->ignored_events = array_key_exists('ignored_events', $config) ? $config['ignored_events'] : array();
107 + $this->config = is_array($config) ? $config : array();
23 108 }
24 109
25 110 function init() {
26 - $this->add_actions_and_listeners();
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 + }
27 117 }
28 118
29 - function get_post($post_id) {
30 - $post = get_post($post_id);
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;
31 177 $data = array('id' => $post_id);
32 - if (!empty($post)) {
33 - $data['title'] = $post->post_title;
34 - $data['status'] = $post->post_status;
35 - $data['type'] = $post->post_type;
36 - $data['url'] = get_permalink($post_id);
37 - $data['date'] = $post->post_date;
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);
38 198 }
39 199 return $data;
40 200 }
41 201
42 - function get_comment($comment_id) {
43 - $comment = get_comment($comment_id);
44 - $data = array('id' => $comment_id);
45 - if (!empty($comment)) {
46 - $data['author'] = $comment->comment_author;
47 - $data['post_id'] = $comment->comment_post_ID;
48 - }
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);
49 214 return $data;
50 215 }
51 216
52 217 function get_term($term_id) {
@@ -51,9 +216,9 @@
51 216
52 217 function get_term($term_id) {
53 218 $term = get_term($term_id);
54 219 $data = array('id' => $term_id);
55 - if (!empty($term)) {
220 + if (!empty($term) && !is_wp_error($term)) {
56 221 $data['name'] = $term->name;
57 222 $data['slug'] = $term->slug;
58 223 $data['taxonomy'] = $term->taxonomy;
59 224 }
@@ -59,65 +224,39 @@
59 224 }
60 225 return $data;
61 226 }
62 227
63 - function get_user($user_id) {
64 - $user = get_userdata($user_id);
65 - $data = array('id' => $user_id);
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);
66 231 if (!empty($user)) {
67 232 $data['username'] = $user->user_login;
68 233 $data['email'] = $user->user_email;
69 - $data['role'] = $user->roles;
234 + $data['role'] = isset($user->roles) ? $user->roles : array();
70 235 }
71 236 return $data;
72 237 }
73 238
74 - function get_blog($blog_id) {
75 - $blog = get_blog_details($blog_id);
76 - $data = array('id' => $blog_id);
77 - if (!empty($blog)) {
78 - $data['name'] = $blog->blogname;
79 - $data['url'] = $blog->path;
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');
80 244 }
81 245 return $data;
82 246 }
83 247
84 - function wc_get_attribute($attribute_id, $attribute_data = null) {
85 - $data = array('id' => $attribute_id);
86 - if (!is_null($attribute_data) && is_array($attribute_data)) {
87 - $data['name'] = $attribute_data['attribute_label'];
88 - $data['slug'] = $attribute_data['attribute_name'];
89 - } else {
90 - $attribute = wc_get_attribute($attribute_id);
91 - if (!empty($attribute)) {
92 - $data['name'] = $attribute->name;
93 - $data['slug'] = substr($attribute->slug, 3);
94 - }
95 - }
96 - return $data;
97 - }
98 -
99 - function wc_get_tax_rate($tax_rate_id, $tax_rate) {
100 - $data = array('id' => $tax_rate_id);
101 - if (!empty($tax_rate)) {
102 - $data['name'] = array_key_exists('tax_rate_name', $tax_rate) ? $tax_rate['tax_rate_name'] : '';
103 - $data['country'] = array_key_exists('tax_rate_country', $tax_rate) ? $tax_rate['tax_rate_country'] : '';
104 - $data['rate'] = array_key_exists('tax_rate', $tax_rate) ? $tax_rate['tax_rate'] : '';
105 - }
106 - return $data;
107 - }
108 -
109 248 function get_ip($ipHeader) {
110 249 $ip = '127.0.0.1';
111 250 if ($ipHeader && is_array($ipHeader)) {
112 251 if (array_key_exists($ipHeader['hdr'], $_SERVER)) {
113 - $_ips = preg_split("/(,| |\t)/", $_SERVER[$ipHeader['hdr']]);
252 + $_ips = preg_split("/(,| |\t)/", WPRHelper::getRawParam('SERVER', $ipHeader['hdr']));
114 253 if (array_key_exists(intval($ipHeader['pos']), $_ips)) {
115 254 $ip = $_ips[intval($ipHeader['pos'])];
116 255 }
117 256 }
118 257 } else if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
119 - $ip = $_SERVER['REMOTE_ADDR'];
258 + $ip = WPRHelper::getRawParam('SERVER', 'REMOTE_ADDR');
120 259 }
121 260
122 261 $ip = trim($ip);
123 262 if (WPRHelper::safePregMatch('/^\[([0-9a-fA-F:]+)\](:[0-9]+)$/', $ip, $matches)) {
@@ -128,14 +267,21 @@
128 267
129 268 return $ip;
130 269 }
131 270
132 - function add_activity($event_data) {
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;
133 279 if (!function_exists('wp_get_current_user')) {
134 280 @include_once(ABSPATH . "wp-includes/pluggable.php");
135 281 }
136 282
137 - $user = wp_get_current_user();
283 + $user = $user ? $user : wp_get_current_user();
138 284 $values = array();
139 285 if (!empty($user)) {
140 286 $values["user_id"] = $user->ID;
141 287 $values["username"] = $user->user_login;
@@ -141,388 +287,427 @@
141 287 $values["username"] = $user->user_login;
142 288 }
143 289 $values["request_id"] = $this->request_id;
144 290 $values["site_id"] = get_current_blog_id();
145 - $values["ip"] = $this->get_ip($this->ip_header);
146 - $values["event_type"] = current_filter();
147 - $values["event_data"] = maybe_serialize($event_data);
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;
148 295 $values["time"] = time();
149 - $this->db->replaceIntoBVTable(BVWPActLog::$actlog_table, $values);
296 + return $this->db->replaceIntoBVTable(BVWPActLog::$actlog_table, $values) !== false;
150 297 }
151 298
152 - function is_key_ignored($ignored_keys, $value) {
153 - $is_ignored = false;
154 - if (array_key_exists("post_types_regex", $ignored_keys)) {
155 - foreach ($ignored_keys['post_types_regex'] as $val) {
156 - if (WPRHelper::safePregMatch($val, $value)) {
157 - return true;
158 - }
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';
159 311 }
312 + $event_data[$entity] = $snapshot;
160 313 }
161 - if (array_key_exists("post_types", $ignored_keys)) {
162 - foreach ($ignored_keys['post_types'] as $val) {
163 - if ($val == $value) {
164 - return true;
165 - }
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;
166 337 }
338 + if ($update && empty($data['changed_fields'])) return;
167 339 }
168 - return $is_ignored;
340 + $this->add_activity($data);
169 341 }
170 342
171 - function user_login_handler($user_login, $user) {
172 - $event_data = array("user" => $this->get_user($user->ID));
173 - $this->add_activity($event_data);
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)));
174 346 }
175 347
176 - function user_logout_handler($user_id) {
177 - $user = $this->get_user($user_id);
178 - $event_data = array("user" => $user);
179 - $this->add_activity($event_data);
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)));
180 352 }
181 353
182 - function password_reset_handler($user, $new_pass) {
183 - if (!empty($user)) {
184 - $event_data = array("user" => $this->get_user($user->ID));
185 - $this->add_activity($event_data);
186 - }
354 + /* ---------- comments ---------- */
355 +
356 + function skip_comment($comment) {
357 + return !$comment || !$this->selected('comment_types', $comment->comment_type);
187 358 }
188 359
189 - function comment_handler($comment_id) {
190 - $comment = $this->get_comment($comment_id);
191 - $post = $this->get_post($comment['post_id']);
192 - $event_data = array(
193 - "comment" => $comment,
194 - "post" => $post
195 - );
196 - $this->add_activity($event_data);
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 + ));
197 367 }
198 368
199 - function comment_status_changed_handler($new_status, $old_status, $comment) {
200 - $post = $this->get_post($comment->comment_post_ID);
201 - $event_data = array(
202 - "comment" => $this->get_comment($comment->comment_ID),
203 - "post" => $post,
204 - "old_status" => $old_status,
205 - "new_status" => $new_status
206 - );
207 - $this->add_activity($event_data);
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 + ));
208 377 }
209 378
210 - function post_handler($post_id) {
211 - $post = $this->get_post($post_id);
212 - if ($this->is_key_ignored($this->ignored_events, $post["type"]))
213 - return;
214 - $event_data = array();
215 - if ($post["type"] === "product") {
216 - $event_data["product"] = $post;
217 - } elseif ($post["type"] === "shop_order") {
218 - $event_data["order"] = $post;
219 - } else {
220 - $event_data["post"] = $post;
221 - }
222 - $this->add_activity($event_data);
379 + /* ---------- terms, users, roles ---------- */
380 +
381 + function term_handler($term_id) {
382 + $this->add_activity(array('term' => $this->get_term($term_id)));
223 383 }
224 384
225 - function post_saved_handler($post_id, $post, $update) {
226 - $post = $this->get_post($post_id);
227 - if ($this->is_key_ignored($this->ignored_events, $post["type"]))
228 - return;
229 - $event_data = array();
230 - if ($post["type"] === "product") {
231 - $event_data["product"] = $post;
232 - } elseif ($post["type"] === "shop_order") {
233 - $event_data["order"] = $post;
234 - } else {
235 - $event_data["post"] = $post;
236 - }
237 - $event_data["updated"] = $update;
238 - $this->add_activity($event_data);
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)));
239 387 }
240 388
241 - function term_handler($term_id) {
242 - $term = $this->get_term($term_id);
243 - $event_data = array(
244 - "term" => $term,
245 - );
246 - $this->add_activity($event_data);
389 + function user_handler($user_id) {
390 + $this->add_activity(array('user' => $this->get_user($user_id)));
247 391 }
248 392
249 - function term_updation_handler($data, $term_id) {
250 - $term = $this->get_term($term_id);
251 - $event_data = array(
252 - "term" => $term,
253 - "new_term" => $data
254 - );
255 - $this->add_activity($event_data);
256 - return $data;
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)));
257 395 }
258 396
259 - function term_deletion_handler($term_id) {
260 - $event_data = array(
261 - "term" => array("id" => $term_id)
262 - );
263 - $this->add_activity($event_data);
397 + function user_deleted_handler($id, $reassign, $user) {
398 + $this->add_activity(array('user' => $this->get_user($user, $id)));
264 399 }
265 400
266 - function user_handler($user_id) {
267 - $user = $this->get_user($user_id);
268 - $event_data = array(
269 - "user" => $user,
270 - );
271 - $this->add_activity($event_data);
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));
272 403 }
273 404
274 - function user_update_handler($user_id, $old_userdata) {
275 - $new_userdata = $this->get_user($user_id);
276 - $event_data = array(
277 - "old_user" => $this->get_user($old_userdata->ID),
278 - "user" => $new_userdata,
279 - );
280 - $this->add_activity($event_data);
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);
281 411 }
282 412
283 - function plugin_action_handler($plugin) {
284 - $event_data = array("plugin" => $plugin);
285 - $this->add_activity($event_data);
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);
286 416 }
287 417
288 - function theme_action_handler($theme_name) {
289 - $event_data = array("theme" => $theme_name);
290 - $this->add_activity($event_data);
418 + function password_reset_handler($user) {
419 + if (!empty($user)) $this->add_activity(array('user' => $this->get_user($user)));
291 420 }
292 421
293 - function mu_handler($blog_id) {
294 - $blog = $this->get_blog($blog_id);
295 - $event_data = array(
296 - "blog" => $blog
297 - );
298 - $this->add_activity($event_data);
422 + /* ---------- plugins, themes, sites ---------- */
423 +
424 + function plugin_handler($plugin) {
425 + $this->add_activity(array('plugin' => $plugin));
299 426 }
300 427
301 - function mu_site_handler($blog) {
302 - $event_data = array(
303 - "blog" => $this->get_blog($blog->blog_id)
304 - );
305 - $this->add_activity($event_data);
428 + function plugin_deleted_handler($plugin, $deleted) {
429 + if ($deleted) $this->add_activity(array('plugin' => $plugin));
306 430 }
307 431
308 - function woocommerce_attribute_created_handler($attribute_id, $attribute_data) {
309 - $event_data = array(
310 - "attribute" => $this->wc_get_attribute($attribute_id, $attribute_data)
311 - );
312 - $this->add_activity($event_data);
432 + function theme_handler($theme_name) {
433 + $this->add_activity(array('theme' => $theme_name));
313 434 }
314 435
315 - function woocommerce_attribute_handler($attribute_id) {
316 - $event_data = array(
317 - "attribute" => $this->wc_get_attribute($attribute_id)
318 - );
319 - $this->add_activity($event_data);
436 + function theme_deleted_handler($stylesheet, $deleted) {
437 + if ($deleted) $this->add_activity(array('theme' => $stylesheet));
320 438 }
321 439
322 - function woocommerce_tax_rate_handler($tax_rate_id, $tax_rate) {
323 - $event_data = array(
324 - "tax_rate" => $this->wc_get_tax_rate($tax_rate_id, $tax_rate)
325 - );
326 - $this->add_activity($event_data);
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;
327 446 }
328 447
329 - function woocommerce_tax_rate_deleted_handler($tax_rate_id) {
330 - $event_data = array(
331 - "tax_rate" => array("id" => $tax_rate_id)
332 - );
333 - $this->add_activity($event_data);
448 + function site_handler($site) {
449 + $this->add_activity(array('blog' => $this->get_site($site)));
334 450 }
335 451
336 - function woocommerce_grant_product_download_access_handler($data) {
337 - $event_data = array(
338 - "download_id" => $data['download_id'],
339 - "user_id" => $data['user_id'],
340 - "order_id" => $data['order_id'],
341 - "product_id" => $data['product_id']
342 - );
343 - $this->add_activity($event_data);
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);
344 455 }
345 456
346 - function woocommerce_revoke_access_to_product_download_handler($download_id, $product_id, $order_id) {
347 - $event_data = array(
348 - "download_id" => $download_id,
349 - "product_id" => $product_id,
350 - "order_id" => $order_id
351 - );
352 - $this->add_activity($event_data);
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';
353 466 }
354 467
355 - function woocommerce_shipping_zone_method_handler($instance_id, $method_id, $zone_id) {
356 - $event_data = array(
357 - "instance_id" => absint ($instance_id),
358 - "method_id" => $method_id,
359 - "zone_id" => $zone_id
360 - );
361 - $this->add_activity($event_data);
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);
362 475 }
363 476
364 - function get_plugin_update_data($plugins) {
365 - $data = array();
366 - if (!empty($plugins) && defined('WP_PLUGIN_DIR')) {
367 - foreach ($plugins as $plugin) {
368 - $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin);
369 - $install_data = array('title' => $plugin_data['Name'], 'version' => $plugin_data['Version']);
370 - array_push($data, $install_data);
371 - }
372 - }
373 - return $data;
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);
374 482 }
375 483
376 - function get_theme_update_data($themes) {
377 - $data = array();
378 - if (!empty($themes)) {
379 - foreach ($themes as $theme) {
380 - $theme_data = wp_get_theme($theme);
381 - $install_data = array('title' => $theme_data['Name'], 'version' => $theme_data['Version']);
382 - array_push($data, $install_data);
383 - }
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;
384 496 }
385 - return $data;
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);
386 501 }
387 502
388 - function get_plugin_install_data($upgrader) {
389 - $data = array();
390 - if ($upgrader->bulk != "1") {
391 - $plugin_data = $upgrader->new_plugin_data;
392 - $install_data = array('title' => $plugin_data['Name'], 'version' => $plugin_data['Version']);
393 - array_push($data, $install_data);
394 - }
395 - return $data;
396 - }
503 + /* ---------- upgrades ---------- */
397 504
398 - function get_theme_install_data($upgrader) {
399 - $data = array();
400 - $theme_data = $upgrader->new_theme_data;
401 - $install_data = array('title' => $theme_data['Name'], 'version' => $theme_data['Version']);
402 - array_push($data, $install_data);
403 - return $data;
505 + function is_relative_package_path($file) {
506 + return is_string($file) && $file !== '' && $file[0] !== '/' && strpos($file, '..') === false;
404 507 }
405 508
406 - function get_update_data($options) {
407 - $event_data = array('action' => 'update');
408 - if ($options['type'] === 'plugin') {
409 - $event_data['type'] = 'plugin';
410 - if (array_key_exists("plugin", $options)) {
411 - $options['plugins'] = array($options['plugin']);
412 - unset($options['plugin']);
413 - }
414 - $event_data['plugins'] = $this->get_plugin_update_data($options['plugins']);
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]]);
415 517 }
416 - else if ($options['type'] === 'theme') {
417 - $event_data['type'] = 'theme';
418 - if (array_key_exists("theme", $options)) {
419 - $options['themes'] = array($options['theme']);
420 - unset($options['theme']);
421 - }
422 - $event_data['themes'] = $this->get_theme_update_data($options['themes']);
423 - }
424 - return $event_data;
518 + return $result;
425 519 }
426 520
427 - function get_install_data($upgrader, $options) {
428 - $event_data = array('action' => 'install');
429 - if ($options['type'] === 'plugin') {
430 - $event_data['type'] = 'plugin';
431 - $event_data['plugins'] = $this->get_plugin_install_data($upgrader);
432 - }
433 - else if ($options['type'] === 'theme') {
434 - $event_data['type'] = 'theme';
435 - $event_data['themes'] = $this->get_theme_install_data($upgrader);
436 - }
437 - return $event_data;
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)));
438 526 }
439 527
440 528 function upgrade_handler($upgrader, $data) {
441 - $event_data = array();
529 + if (!is_array($data) || !isset($data['action'], $data['type']) || !in_array($data['type'], array('plugin', 'theme'), true))
530 + return;
531 + $type = $data['type'];
442 532 if ($data['action'] === 'update') {
443 - if ('core' === $data['type']) {
444 - return;
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);
445 537 }
446 - $event_data = $this->get_update_data($data);
447 - } else if ($data['action'] === 'install') {
448 - $event_data = $this->get_install_data($upgrader, $data);
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))));
449 541 }
450 - $this->add_activity($event_data);
451 542 }
452 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 +
453 555 function core_upgrade_handler($new_wp_version) {
454 556 global $wp_version;
455 - $event_data = array();
456 - $event_data['type'] = 'core';
457 - $event_data['wp_core'] = array('prev_version' => $wp_version, 'new_version' => $new_wp_version);
458 - $this->add_activity($event_data);
557 + $this->add_activity(array('type' => 'core', 'wp_core' => array('prev_version' => $wp_version, 'new_version' => $new_wp_version)));
459 558 }
460 559
461 - /* ADDING ACTION AND LISTENERS FOR SENSING EVENTS. */
462 - public function add_actions_and_listeners() {
463 - /* SENSORS FOR POST AND PAGE CHANGES */
464 - add_action('pre_post_update', array($this, 'post_handler'));
465 - add_action('save_post', array($this, 'post_saved_handler'), 10, 3);
466 - add_action('post_stuck', array($this, 'post_handler'));
467 - add_action('post_unstuck', array($this, 'post_handler'));
468 - add_action('delete_post', array($this, 'post_handler'));
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 + }
469 566
470 - /* SENSORS FOR COMMENTS */
471 - add_action('comment_post', array($this, 'comment_handler'));
472 - add_action('edit_comment', array($this, 'comment_handler'));
473 - add_action('transition_comment_status', array($this, 'comment_status_changed_handler'), 10, 3);
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 + }
474 576
475 - /* SENSORS FOR TAG AND CATEGORY CHANGES */
476 - add_action('create_term', array($this, 'term_handler'));
477 - add_action('pre_delete_term', array($this, 'term_handler'));
478 - add_action('delete_term', array($this, 'term_deletion_handler'));
479 - add_filter('wp_update_term_data', array($this, 'term_updation_handler'), 10, 2);
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 + }
480 584
481 - /* SENSORS FOR USER CHANGES*/
482 - add_action('user_register', array($this, 'user_handler'));
483 - add_action('wpmu_new_user', array($this, 'user_handler'));
484 - add_action('profile_update', array($this, 'user_update_handler'), 10, 2);
485 - add_action('delete_user', array($this, 'user_handler'));
486 - add_action('wpmu_delete_user', array($this, 'user_handler'));
585 + /* ---------- WooCommerce products and orders ---------- */
487 586
488 - /* SENSORS FOR PLUGIN AND THEME*/
489 - add_action('activate_plugin', array($this, 'plugin_action_handler'));
490 - add_action('deactivate_plugin', array($this, 'plugin_action_handler'));
491 - add_action('switch_theme', array($this, 'theme_action_handler'));
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 + }
492 593
493 - /* SENSORS FOR MULTISITE CHANGES */
494 - add_action('wp_insert_site', array($this, 'mu_site_handler'));
495 - add_action('archive_blog', array($this, 'mu_handler'));
496 - add_action('unarchive_blog', array( $this, 'mu_handler'));
497 - add_action('activate_blog', array($this, 'mu_handler'));
498 - add_action('deactivate_blog', array($this, 'mu_handler'));
499 - add_action('wp_delete_site', array($this, 'mu_site_handler'));
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 + }
500 600
501 - /* SENSORS USER ACTIONS AT FRONTEND */
502 - add_action('wp_login', array($this, 'user_login_handler'), 10, 2);
503 - add_action('wp_logout', array( $this, 'user_logout_handler'), 5, 1);
504 - add_action('password_reset', array( $this, 'password_reset_handler'), 10, 2);
601 + function product_before_save_handler($product) {
602 + $this->remember_changes($product, $this->option('product_fields'));
603 + }
505 604
506 - /* SENSOR FOR PLUGIN, THEME, WPCORE UPGRADES */
507 - add_action('upgrader_process_complete', array($this, 'upgrade_handler'), 10, 2);
508 - add_action('_core_updated_successfully', array($this, 'core_upgrade_handler'), 10, 1);
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 + }
509 614
510 - /* SENSORS FOR WOOCOMMERCE EVENTS */
511 - add_action('woocommerce_attribute_added', array($this, 'woocommerce_attribute_created_handler'), 10, 2);
512 - add_action('woocommerce_attribute_updated', array($this, 'woocommerce_attribute_handler'), 10, 1);
513 - add_action('woocommerce_before_attribute_delete', array($this, 'woocommerce_attribute_handler'), 10, 1);
514 - add_action('woocommerce_attribute_deleted', array($this, 'woocommerce_attribute_handler'), 10, 1);
615 + function order_before_save_handler($order) {
616 + $this->remember_changes($order, $this->option('order_fields'));
617 + }
515 618
516 - add_action('woocommerce_tax_rate_added', array($this, 'woocommerce_tax_rate_handler'), 10, 2);
517 - add_action('woocommerce_tax_rate_deleted', array($this, 'woocommerce_tax_rate_deleted_handler'), 10, 1);
518 - add_action('woocommerce_tax_rate_updated', array($this, 'woocommerce_tax_rate_handler'), 10, 2);
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 + }
519 624
520 - add_action('woocommerce_grant_product_download_access', array($this, 'woocommerce_grant_product_download_access_handler'), 10, 1);
521 - add_action('woocommerce_ajax_revoke_access_to_product_download', array($this, 'woocommerce_revoke_access_to_product_download_handler'), 10, 3);
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 + }
522 629
523 - add_action('woocommerce_shipping_zone_method_added', array($this, 'woocommerce_shipping_zone_method_handler'), 10, 3);
524 - add_action('woocommerce_shipping_zone_method_status_toggled', array($this, 'woocommerce_shipping_zone_method_handler'), 10, 3);
525 - add_action('woocommerce_shipping_zone_method_deleted', array($this, 'woocommerce_shipping_zone_method_handler'), 10, 3);
630 + function order_deleted_handler($id) {
631 + $this->add_activity(array('order' => array('id' => $id)));
526 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 + }
527 712 }
528 -endif;
713 +endif;