enable_rest_api(); $this->enable_wp_cli(); add_action('init', [$this, 'register_all_events']); add_action('wp_ajax_wu_get_event_payload_preview', [$this, 'event_payload_preview']); add_action('rest_api_init', [$this, 'hooks_endpoint']); add_action('wu_model_post_save', [$this, 'log_transitions'], 10, 4); add_action('wu_daily', [$this, 'clean_old_events']); } /** * 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 ('event' === $model) { return; } /* * 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', [ '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 ]); } /** * If empty, go home. */ if (empty($diff)) { return; } $changed = []; /** * Loop changed data. */ foreach ($diff as $key => $new_value) { $old_value = wu_get_isset($original, $key, ''); if ('id' === $key && intval($old_value) === 0) { return; } if (empty(json_encode($old_value)) && empty(json_encode($new_value))) { return; } $changed[ $key ] = [ 'old_value' => $old_value, 'new_value' => $new_value, ]; } $event_data = [ 'severity' => Event::SEVERITY_INFO, 'slug' => 'changed', 'object_type' => $model, 'object_id' => $object->get_id(), 'payload' => $changed, ]; } else { $event_data = [ 'severity' => Event::SEVERITY_INFO, 'slug' => 'created', 'object_type' => $model, 'object_id' => $object->get_id(), 'payload' => [], ]; } if ( ! empty($_POST) && is_user_logged_in()) { $event_data['initiator'] = 'manual'; $event_data['author_id'] = get_current_user_id(); } return wu_create_event($event_data); } /** * Returns the payload to be displayed in the payload preview field. * * @since 2.0.0 * @return void */ public function event_payload_preview(): void { if ( ! wu_request('event')) { wp_send_json_error(new \WP_Error('error', __('No event was selected.', 'wp-ultimo'))); } $slug = wu_request('event'); if ( ! $slug) { wp_send_json_error(new \WP_Error('not-found', __('Event was not found.', 'wp-ultimo'))); } $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); } } /** * 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', []); $types = [ '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; } /** * 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 ['error' => 'Event not found']; } $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 ['error' => 'Param required:' . $diff_key]; } } $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); } /** * 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; } /** * Returns the list of available webhook events. * * @since 2.0.0 * @return array $events with all events. */ public function get_events() { return $this->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; } } } return false; } /** * 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): void { $event = new Event( [ '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(); } /** * Registers the list of default events. * * @since 2.0.0 * @return void */ public function register_all_events(): void { /** * Payment Received. */ wu_register_event_type( 'payment_received', [ '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' => [ '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', [ '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' => [], ] ); /** * Confirm Email Address */ wu_register_event_type( 'confirm_email_address', [ '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( [ 'verification_link' => 'https://linktoverifyemail.com', ], wu_generate_event_payload('customer') ), 'deprecated_args' => [], ] ); /** * Domain Mapping Added */ wu_register_event_type( 'domain_created', [ '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' => [ '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', [ 'name' => __('New Renewal Payment Created', 'wp-ultimo'), 'desc' => __('This event is fired every time a new renewal payment is created by WP Multisite WaaS.', 'wp-ultimo'), 'payload' => fn() => array_merge( [ 'default_payment_url' => 'https://linktopayment.com', ], wu_generate_event_payload('payment'), wu_generate_event_payload('membership'), wu_generate_event_payload('customer') ), 'deprecated_args' => [], ] ); $models = $this->models_events; foreach ($models as $model => $params) { foreach ($params['types'] as $type) { wu_register_event_type( $model . '_' . $type, [ '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 Multisite WaaS.', 'wp-ultimo'), $params['label'], $type), 'deprecated_args' => [], 'payload' => fn() => $this->get_model_payload($model), ] ); } add_action("wu_{$model}_post_save", [$this, 'dispatch_base_model_event'], 10, 3); } do_action('wu_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 ] = [ 'label' => $label, 'types' => $event_types, ]; } /** * 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; } $payload = $this->get_model_payload($model, $obj); wu_do_event($model . '_' . $type, $payload); } /** * 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) ); } if (method_exists($obj, 'get_customer')) { $customer = $model_object ? $obj->get_customer() : false; $payload = array_merge( $payload, wu_generate_event_payload('customer', $customer) ); } if (method_exists($obj, 'get_billing_address') || method_exists($obj, 'get_membership')) { if (null !== $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( fn() => '', \WP_Ultimo\Objects\Billing_Address::fields() ) ); } } return $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; } $events_to_remove = wu_get_events( [ 'number' => 100, 'date_query' => [ '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; } } 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; } /** * Create a endpoint to retrieve all available event hooks. * * @since 2.0.0 * * @return mixed */ public function hooks_endpoint(): void { if ( ! wu_get_setting('enable_api', true)) { return; } $api = \WP_Ultimo\API::get_instance(); register_rest_route( $api->get_namespace(), '/hooks', [ 'methods' => 'GET', 'callback' => [$this, 'get_hooks_rest'], 'permission_callback' => [$api, 'check_authorization'], ] ); } /** * 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(); } } return rest_ensure_response($response); } }