enable_rest_api(); $this->enable_wp_cli(); add_action('plugins_loaded', array($this, 'register_all_events')); add_action('wp_ajax_wu_get_event_payload_preview', array($this, 'event_payload_preview')); add_action('rest_api_init', array($this, 'hooks_endpoint')); add_action('wu_model_post_save', array($this, 'log_transitions'), 10, 4); add_action('wu_daily', array($this, 'clean_old_events')); } // end init; /** * Returns the payload to be displayed in the payload preview field. * Log model transitions. * * @since 2.0.0 * * @param string $model The model name. * @param array $data The data being saved, serialized. * @param array $data_unserialized The data being saved, un-serialized. * @param Base_Model $object The object being saved. * @return void */ public function log_transitions($model, $data, $data_unserialized, $object) { if ($model === 'event') { return; } // end if; /* * Editing Model */ if (wu_get_isset($data_unserialized, 'id')) { $original = $object->_get_original(); $diff = wu_array_recursive_diff($data_unserialized, $original); $keys_to_remove = apply_filters('wu_exclude_transitions_keys', array( 'meta', 'last_login', 'ips', 'query_class', 'settings', '_compiled_product_list', '_gateway_info', '_limitations', )); foreach ($keys_to_remove as $key_to_remove) { unset($diff[$key_to_remove]); } // end foreach; /** * If empty, go home. */ if (empty($diff)) { return; } // end if; $changed = array(); /** * Loop changed data. */ foreach ($diff as $key => $new_value) { $old_value = wu_get_isset($original, $key, ''); if ($key === 'id' && intval($old_value) === 0) { return; } // end if; if (empty(json_encode($old_value)) && empty(json_encode($new_value))) { return; } // end if; $changed[$key] = array( 'old_value' => $old_value, 'new_value' => $new_value, ); } // end foreach; $event_data = array( 'severity' => Event::SEVERITY_INFO, 'slug' => 'changed', 'object_type' => $model, 'object_id' => $object->get_id(), 'payload' => $changed, ); } else { $event_data = array( 'severity' => Event::SEVERITY_INFO, 'slug' => 'created', 'object_type' => $model, 'object_id' => $object->get_id(), 'payload' => array(), ); } // end if; if (!empty($_POST) && is_user_logged_in()) { $event_data['initiator'] = 'manual'; $event_data['author_id'] = get_current_user_id(); } // end if; return wu_create_event($event_data); } // end log_transitions; /** * Returns the payload to be displayed in the payload preview field. * * @since 2.0.0 * @return void */ public function event_payload_preview() { if (!wu_request('event')) { wp_send_json_error(new \WP_Error('error', __('No event was selected.', 'wp-ultimo'))); } // end if; $slug = wu_request('event'); if (!$slug) { wp_send_json_error(new \WP_Error('not-found', __('Event was not found.', 'wp-ultimo'))); } // end if; $event = wu_get_event_type($slug); if (!$event) { wp_send_json_error(new \WP_Error('not-found', __('Data not found.', 'wp-ultimo'))); } else { $payload = isset($event['payload']) ? wu_maybe_lazy_load_payload($event['payload']) : '{}'; $payload = array_map('htmlentities2', $payload); wp_send_json_success($payload); } // end if; } // end event_payload_preview; /** * Returns the list of event types to register. * * @since 2.0.0 * @return array */ public function get_event_type_as_options() { /* * We use this to order the options. */ $event_type_settings = wu_get_setting('saving_type', array()); $types = array( 'id' => '$id', 'title' => '$title', 'desc' => '$desc', 'class_name' => '$class_name', 'active' => 'in_array($id, $active_gateways, true)', 'active' => 'in_array($id, $active_gateways, true)', 'gateway' => '$class_name', // Deprecated. 'hidden' => false, ); $types = array_filter($types, fn($item) => $item['hidden'] === false); return $types; } // end get_event_type_as_options; /** * Add a new event. * * @since 2.0.0 * * @param string $slug The slug of the event. Something like payment_received. * @param array $payload with the events information. * * @return array with returns message for now. */ public function do_event($slug, $payload) { $registered_event = $this->get_event($slug); if (!$registered_event) { return array('error' => 'Event not found'); } // end if; $payload_diff = array_diff_key(wu_maybe_lazy_load_payload($registered_event['payload']), $payload); if (isset($payload_diff[0])) { foreach ($payload_diff[0] as $diff_key => $diff_value) { return array('error' => 'Param required:' . $diff_key); } // end foreach; } // end if; $payload['wu_version'] = wu_get_version(); do_action('wu_event', $slug, $payload); do_action("wu_event_{$slug}", $payload); /** * Saves in the database */ $this->save_event($slug, $payload); } // end do_event; /** * Register a new event to be used as param. * * @since 2.0.0 * * @param string $slug The slug of the event. Something like payment_received. * @param array $args with the events information. * * @return true */ public function register_event($slug, $args): bool { $this->events[$slug] = $args; return true; } // end register_event; /** * Returns the list of available webhook events. * * @since 2.0.0 * @return array $events with all events. */ public function get_events() { return $this->events; } // end get_events; /** * Returns the list of available webhook events. * * @since 2.0.0 * * @param string $slug of the event. * @return array $event with event params. */ public function get_event($slug) { $events = $this->get_events(); if ($events) { foreach ($events as $key => $event) { if ($key === $slug) { return $event; } // end if; } // end foreach; } // end if; return false; } // end get_event; /** * Saves event in the database. * * @param string $slug of the event. * @param array $payload with event params. * @return void. */ public function save_event($slug, $payload) { $event = new Event(array( 'object_id' => wu_get_isset($payload, 'object_id', ''), 'object_type' => wu_get_isset($payload, 'object_type', ''), 'severity' => wu_get_isset($payload, 'type', Event::SEVERITY_INFO), 'date_created' => wu_get_current_time('mysql', true), 'slug' => strtolower($slug), 'payload' => $payload, )); $event->save(); } // end save_event; /** * Registers the list of default events. * * @since 2.0.0 * @return void */ public function register_all_events() { /** * Payment Received. */ wu_register_event_type('payment_received', array( 'name' => __('Payment Received', 'wp-ultimo'), 'desc' => __('This event is fired every time a new payment is received, regardless of the payment status.', 'wp-ultimo'), 'payload' => fn() => array_merge( wu_generate_event_payload('payment'), wu_generate_event_payload('membership'), wu_generate_event_payload('customer') ), 'deprecated_args' => array( 'user_id' => 'customer_user_id', 'amount' => 'payment_total', 'gateway' => 'payment_gateway', 'status' => 'payment_status', 'date' => 'payment_date_created', ), )); /** * Site Published. */ wu_register_event_type('site_published', array( 'name' => __('Site Published', 'wp-ultimo'), 'desc' => __('This event is fired every time a new site is created tied to a membership, or transitions from a pending state to a published state.', 'wp-ultimo'), 'payload' => fn() => array_merge( wu_generate_event_payload('site'), wu_generate_event_payload('customer'), wu_generate_event_payload('membership') ), 'deprecated_args' => array(), )); /** * Confirm Email Address */ wu_register_event_type('confirm_email_address', array( 'name' => __('Email Verification Needed', 'wp-ultimo'), 'desc' => __('This event is fired every time a new customer is added with an email verification status of pending.', 'wp-ultimo'), 'payload' => fn() => array_merge( array( 'verification_link' => 'https://linktoverifyemail.com', ), wu_generate_event_payload('customer') ), 'deprecated_args' => array(), )); /** * Domain Mapping Added */ wu_register_event_type('domain_created', array( 'name' => __('New Domain Mapping Added', 'wp-ultimo'), 'desc' => __('This event is fired every time a new domain mapping is added by a customer.', 'wp-ultimo'), 'payload' => fn() => array_merge( wu_generate_event_payload('domain'), wu_generate_event_payload('site'), wu_generate_event_payload('membership'), wu_generate_event_payload('customer') ), 'deprecated_args' => array( 'user_id' => 1, 'user_site_id' => 1, 'mapped_domain' => 'mydomain.com', 'user_site_url' => 'http://test.mynetwork.com/', 'network_ip' => '125.399.3.23', ), )); /** * Renewal payment created */ wu_register_event_type('renewal_payment_created', array( 'name' => __('New Renewal Payment Created', 'wp-ultimo'), 'desc' => __('This event is fired every time a new renewal payment is created by WP Ultimo.', 'wp-ultimo'), 'payload' => fn() => array_merge( array( 'default_payment_url' => 'https://linktopayment.com', ), wu_generate_event_payload('payment'), wu_generate_event_payload('membership'), wu_generate_event_payload('customer') ), 'deprecated_args' => array(), )); $models = $this->models_events; foreach ($models as $model => $params) { foreach ($params['types'] as $type) { wu_register_event_type($model . '_' . $type, array( 'name' => sprintf(__('%1$s %2$s', 'wp-ultimo'), $params['label'], ucfirst($type)), 'desc' => sprintf(__('This event is fired every time a %1$s is %2$s by WP Ultimo.', 'wp-ultimo'), $params['label'], $type), 'deprecated_args' => array(), 'payload' => fn() => $this->get_model_payload($model), )); } // end foreach; add_action("wu_{$model}_post_save", array($this, 'dispatch_base_model_event'), 10, 3); } // end foreach; do_action('wu_register_all_events'); } // end register_all_events; /** * Register models events * * @param string $slug slug of event. * @param string $label label of event. * @param array $event_types event types allowed. * @since 2.1.4 */ public static function register_model_events(string $slug, string $label, array $event_types): void { $instance = self::get_instance(); $instance->models_events[$slug] = array( 'label' => $label, 'types' => $event_types, ); } // end register_model_events; /** * Dispatch registered model events * * @param array $data Data. * @param mixed $obj Object. * @param bool $new New. * * @since 2.1.4 */ public function dispatch_base_model_event(array $data, $obj, bool $new): void { $model = $obj->model; $type = $new ? 'created' : 'updated'; $registered_model = wu_get_isset($this->models_events, $model); if (!$registered_model || !in_array($type, $registered_model['types'], true)) { return; } // end if; $payload = $this->get_model_payload($model, $obj); wu_do_event($model . '_' . $type, $payload); } // end dispatch_base_model_event; /** * Returns the full payload for a given model. * * @param string $model The model name. * @param object|null $model_object The model object. * @return array * * @since 2.3.0 */ public function get_model_payload(string $model, ?object $model_object = null) { $obj = $model_object ?? call_user_func("wu_mock_{$model}"); $payload = wu_generate_event_payload($model, $obj); if (method_exists($obj, 'get_membership')) { $membership = $model_object ? $obj->get_membership() : false; $payload = array_merge( $payload, wu_generate_event_payload('membership', $membership) ); } // end if; if (method_exists($obj, 'get_customer')) { $customer = $model_object ? $obj->get_customer() : false; $payload = array_merge( $payload, wu_generate_event_payload('customer', $customer) ); } // end if; if (method_exists($obj, 'get_billing_address') || method_exists($obj, 'get_membership')) { if ($model_object) { $payload = method_exists($obj, 'get_billing_address') ? array_merge( $payload, $obj->get_billing_address()->to_array() ) : array_merge( $payload, $obj->get_membership()->get_billing_address()->to_array() ); } else { $payload = array_merge( $payload, array_map(function () { return ''; }, \WP_Ultimo\Objects\Billing_Address::fields()) ); } // end if; } // end if; return $payload; } // end get_model_payload; /** * Every day, deletes old events that we don't want to keep. * * @since 2.0.0 */ public function clean_old_events(): bool { /* * Add a filter setting this to 0 or false * to prevent old events from being ever deleted. */ $threshold_days = apply_filters('wu_events_threshold_days', 1); if (empty($threshold_days)) { return false; } // end if; $events_to_remove = wu_get_events(array( 'number' => 100, 'date_query' => array( 'column' => 'date_created', 'before' => "-{$threshold_days} days", 'inclusive' => true, ), )); $success_count = 0; foreach ($events_to_remove as $event) { $status = $event->delete(); if (!is_wp_error($status) && $status) { $success_count++; } // end if; } // end foreach; wu_log_add('wu-cron', sprintf(__('Removed %1$d events successfully. Failed to remove %2$d events.', 'wp-ultimo'), $success_count, count($events_to_remove) - $success_count)); return true; } // end clean_old_events; /** * Create a endpoint to retrieve all available event hooks. * * @since 2.0.0 * * @return mixed */ public function hooks_endpoint() { if (!wu_get_setting('enable_api', true)) { return; } // end if; $api = \WP_Ultimo\API::get_instance(); register_rest_route($api->get_namespace(), '/hooks', array( 'methods' => 'GET', 'callback' => array($this, 'get_hooks_rest'), 'permission_callback' => array($api, 'check_authorization'), )); } // end hooks_endpoint; /** * Return all event types for the REST API request. * * @since 2.0.0 * * @param WP_REST_Request $request The request sent. * @return mixed */ public function get_hooks_rest($request) { $response = wu_get_event_types(); foreach ($response as $key => $value) { $payload = wu_get_isset($value, 'payload'); if (is_callable($payload)) { $response[$key]['payload'] = $payload(); } // end if; } // end foreach; return rest_ensure_response($response); } // end get_hooks_rest; } // end class Event_Manager;