| @@ -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; | |