PluginProbe ʕ •ᴥ•ʔ
OttoKit: All-in-One Automation Platform / 1.1.32
OttoKit: All-in-One Automation Platform v1.1.32
1.1.33 1.1.32 1.1.31 1.1.30 1.1.29 1.1.28 1.1.27 1.1.9 trunk 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 1.0.39 1.0.40 1.0.41 1.0.42 1.0.43 1.0.44 1.0.45 1.0.46 1.0.47 1.0.48 1.0.49 1.0.50 1.0.51 1.0.52 1.0.53 1.0.54 1.0.55 1.0.56 1.0.57 1.0.58 1.0.59 1.0.60 1.0.61 1.0.62 1.0.63 1.0.64 1.0.65 1.0.66 1.0.67 1.0.68 1.0.69 1.0.7 1.0.70 1.0.71 1.0.72 1.0.73 1.0.74 1.0.75 1.0.76 1.0.77 1.0.78 1.0.79 1.0.8 1.0.80 1.0.81 1.0.82 1.0.83 1.0.84 1.0.85 1.0.86 1.0.87 1.0.88 1.0.89 1.0.9 1.0.90 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.20 1.1.21 1.1.22 1.1.23 1.1.24 1.1.25 1.1.26 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8
suretriggers / src / Abilities / AbilitiesController.php
suretriggers / src / Abilities Last commit date
AbilitiesController.php 4 months ago
AbilitiesController.php
1029 lines
1 <?php
2 /**
3 * AbilitiesController — WordPress Abilities API registration.
4 * php version 5.6
5 *
6 * @category Abilities
7 * @package SureTriggers
8 * @author BSF <username@example.com>
9 * @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
10 * @link https://www.brainstormforce.com/
11 * @since 1.1.21
12 */
13
14 namespace SureTriggers\Abilities;
15
16 use SureTriggers\Controllers\EventController;
17 use SureTriggers\Controllers\IntegrationsController;
18 use SureTriggers\Controllers\OptionController;
19 use SureTriggers\Controllers\WebhookRequestsController;
20 use SureTriggers\Models\SaasApiToken;
21 use SureTriggers\Traits\SingletonLoader;
22
23 /**
24 * AbilitiesController
25 *
26 * Registers OttoKit categories and abilities with the WordPress Abilities API
27 * (WP 6.9+). All registrations are guarded with function_exists() so the plugin
28 * remains compatible with WP 5.4+.
29 *
30 * Categories:
31 * - ottokit-connection (connection status, settings, system health)
32 * - ottokit-automation (triggers, integrations, events, webhook logs)
33 *
34 * Abilities (8):
35 * - ottokit/get-connection-status
36 * - ottokit/get-settings
37 * - ottokit/get-system-status
38 * - ottokit/get-active-triggers
39 * - ottokit/get-active-integrations
40 * - ottokit/get-all-integrations
41 * - ottokit/get-integration-events
42 * - ottokit/get-webhook-logs
43 *
44 * @category Abilities
45 * @package SureTriggers
46 * @author BSF <username@example.com>
47 * @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
48 * @link https://www.brainstormforce.com/
49 * @since 1.1.21
50 */
51 class AbilitiesController {
52
53 use SingletonLoader;
54
55 /**
56 * Constructor — hooks into Abilities API lifecycle if WP 6.9+ is present.
57 *
58 * @since 1.1.21
59 */
60 public function __construct() {
61 if ( ! function_exists( 'wp_register_ability_category' ) ) {
62 return;
63 }
64
65 add_action( 'wp_abilities_api_categories_init', [ $this, 'register_categories' ] );
66 add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
67 }
68
69 // -------------------------------------------------------------------------
70 // Category registration
71 // -------------------------------------------------------------------------
72
73 /**
74 * Register ability categories.
75 *
76 * @return void
77 */
78 public function register_categories() {
79 wp_register_ability_category(
80 'ottokit-connection',
81 [
82 'label' => __( 'OttoKit Connection', 'suretriggers' ),
83 'description' => __( 'Abilities for checking OttoKit connection status, settings, and system health.', 'suretriggers' ),
84 ]
85 );
86
87 wp_register_ability_category(
88 'ottokit-automation',
89 [
90 'label' => __( 'OttoKit Automation', 'suretriggers' ),
91 'description' => __( 'Abilities for inspecting automation triggers, integrations, events, and webhook logs.', 'suretriggers' ),
92 ]
93 );
94 }
95
96 // -------------------------------------------------------------------------
97 // Ability registration
98 // -------------------------------------------------------------------------
99
100 /**
101 * Register all OttoKit abilities.
102 *
103 * @return void
104 */
105 public function register_abilities() {
106 $this->register_get_connection_status();
107 $this->register_get_settings();
108 $this->register_get_system_status();
109 $this->register_get_active_triggers();
110 $this->register_get_active_integrations();
111 $this->register_get_all_integrations();
112 $this->register_get_integration_events();
113 $this->register_get_webhook_logs();
114 }
115
116 // -------------------------------------------------------------------------
117 // ottokit/get-connection-status
118 // -------------------------------------------------------------------------
119
120 /**
121 * Register the get-connection-status ability.
122 *
123 * @return void
124 */
125 private function register_get_connection_status() {
126 wp_register_ability(
127 'ottokit/get-connection-status',
128 [
129 'label' => __( 'Get OttoKit connection status', 'suretriggers' ),
130 'description' => __( 'Reads the live OttoKit SaaS connection state from WordPress options. Returns: connected (bool), status string (suretriggers_connection_successful | suretriggers_connection_error | suretriggers_connection_wp_error | empty), the admin email used to connect, and the billing plan ID. Use this first when the user reports automations are not running or the connection appears broken — never assume connection state.', 'suretriggers' ),
131 'category' => 'ottokit-connection',
132 'permission_callback' => [ $this, 'permission_manage_options' ],
133 'input_schema' => [
134 'type' => 'object',
135 'properties' => [],
136 ],
137 'output_schema' => [
138 'type' => 'object',
139 'properties' => [
140 'connected' => [
141 'type' => 'boolean',
142 'description' => __( 'Whether OttoKit is connected to the SaaS platform.', 'suretriggers' ),
143 ],
144 'status' => [
145 'type' => 'string',
146 'description' => __( 'Verified connection status. One of: suretriggers_connection_successful, suretriggers_connection_error, suretriggers_connection_wp_error, or empty.', 'suretriggers' ),
147 ],
148 'connected_email' => [
149 'type' => [ 'string', 'null' ],
150 'description' => __( 'Email address used to connect to OttoKit SaaS. Null if not connected.', 'suretriggers' ),
151 ],
152 'plan_id' => [
153 'type' => [ 'string', 'null' ],
154 'description' => __( 'Current OttoKit plan identifier (e.g. free, pro). Null if unknown.', 'suretriggers' ),
155 ],
156 ],
157 ],
158 'execute_callback' => [ $this, 'execute_get_connection_status' ],
159 'meta' => [
160 'show_in_rest' => true,
161 'annotations' => [
162 'readonly' => true,
163 'destructive' => false,
164 'idempotent' => true,
165 'priority' => 1.0,
166 'instructions' => 'Call this first when the user reports automations are not running or the connection appears broken. Never assume connection state — always read it. Explain each returned field clearly to the user, especially the status string. Suggest re-connecting if status is empty or shows an error.',
167 ],
168 'mcp' => [ 'public' => true ],
169 ],
170 ]
171 );
172 }
173
174 /**
175 * Execute get-connection-status.
176 *
177 * @param array $input Ability input (unused).
178 * @return array
179 */
180 public function execute_get_connection_status( $input ) {
181 $token = SaasApiToken::get();
182 $connected = ! empty( $token ) && 'connection-denied' !== $token;
183 $status = get_option( 'suretriggers_verify_connection', '' );
184 $connected_email = OptionController::get_option( 'connected_email_key' );
185 $plan_data = get_option( 'suretriggers_lifetime_user_plan_data' );
186 $plan_id = ( is_array( $plan_data ) && isset( $plan_data['plan_id'] ) ) ? $plan_data['plan_id'] : null;
187
188 return [
189 'connected' => $connected,
190 'status' => is_string( $status ) ? $status : '',
191 'connected_email' => ( is_string( $connected_email ) && '' !== $connected_email ) ? $connected_email : null,
192 'plan_id' => is_string( $plan_id ) ? $plan_id : null,
193 ];
194 }
195
196 // -------------------------------------------------------------------------
197 // ottokit/get-settings
198 // -------------------------------------------------------------------------
199
200 /**
201 * Register the get-settings ability.
202 *
203 * @return void
204 */
205 private function register_get_settings() {
206 wp_register_ability(
207 'ottokit/get-settings',
208 [
209 'label' => __( 'Get OttoKit access control settings', 'suretriggers' ),
210 'description' => __( 'Reads the OttoKit access-control settings from WordPress options. Returns: extra user IDs allowed to access the OttoKit dashboard (enabled_users), role slugs with access (enabled_user_roles), and the installation source type. Use this to audit who can access OttoKit or to diagnose access-denied complaints.', 'suretriggers' ),
211 'category' => 'ottokit-connection',
212 'permission_callback' => [ $this, 'permission_manage_options' ],
213 'input_schema' => [
214 'type' => 'object',
215 'properties' => [],
216 ],
217 'output_schema' => [
218 'type' => 'object',
219 'properties' => [
220 'enabled_users' => [
221 'type' => 'array',
222 'items' => [ 'type' => 'integer' ],
223 'description' => __( 'User IDs that have access to OttoKit (in addition to administrators).', 'suretriggers' ),
224 ],
225 'enabled_user_roles' => [
226 'type' => 'array',
227 'items' => [ 'type' => 'string' ],
228 'description' => __( 'WordPress user role slugs that have access to OttoKit.', 'suretriggers' ),
229 ],
230 'source_type' => [
231 'type' => [ 'string', 'null' ],
232 'description' => __( 'Source type identifier for this OttoKit installation.', 'suretriggers' ),
233 ],
234 ],
235 ],
236 'execute_callback' => [ $this, 'execute_get_settings' ],
237 'meta' => [
238 'show_in_rest' => true,
239 'annotations' => [
240 'readonly' => true,
241 'destructive' => false,
242 'idempotent' => true,
243 'priority' => 1.0,
244 'instructions' => 'Use this to audit access control. If a user reports they cannot access the OttoKit dashboard, check whether their user ID appears in enabled_users or their role slug appears in enabled_user_roles.',
245 ],
246 'mcp' => [ 'public' => true ],
247 ],
248 ]
249 );
250 }
251
252 /**
253 * Execute get-settings.
254 *
255 * @param array $input Ability input (unused).
256 * @return array
257 */
258 public function execute_get_settings( $input ) {
259 $enabled_users = get_option( 'suretriggers_enabled_users', [] );
260 $enabled_users = is_array( $enabled_users ) ? array_values( array_map( 'absint', $enabled_users ) ) : [];
261
262 $enabled_roles = get_option( 'suretriggers_enabled_user_roles', [] );
263 $enabled_roles = is_array( $enabled_roles ) ? array_values( array_map( 'strval', $enabled_roles ) ) : [];
264
265 $source_type = get_option( 'suretriggers_source' );
266
267 return [
268 'enabled_users' => $enabled_users,
269 'enabled_user_roles' => $enabled_roles,
270 'source_type' => ( is_string( $source_type ) && '' !== $source_type ) ? $source_type : null,
271 ];
272 }
273
274 // -------------------------------------------------------------------------
275 // ottokit/get-system-status
276 // -------------------------------------------------------------------------
277
278 /**
279 * Register the get-system-status ability.
280 *
281 * @return void
282 */
283 private function register_get_system_status() {
284 wp_register_ability(
285 'ottokit/get-system-status',
286 [
287 'label' => __( 'Get OttoKit system health status', 'suretriggers' ),
288 'description' => __( 'Performs a full health check of the OttoKit installation. Returns: last SaaS connection status string, whether all three background cron jobs are scheduled (retry-failed-requests every 30 min, cleanup-logs daily, verify-api-connection every 6 hours), whether the webhook log DB table exists, and log counts by status (success, failed, pending). A missing cron or absent DB table indicates a broken installation that needs the plugin deactivated and reactivated.', 'suretriggers' ),
289 'category' => 'ottokit-connection',
290 'permission_callback' => [ $this, 'permission_manage_options' ],
291 'input_schema' => [
292 'type' => 'object',
293 'properties' => [],
294 ],
295 'output_schema' => [
296 'type' => 'object',
297 'properties' => [
298 'connection_status' => [
299 'type' => 'string',
300 'description' => __( 'Last verified OttoKit connection status string.', 'suretriggers' ),
301 ],
302 'cron_retry_scheduled' => [
303 'type' => 'boolean',
304 'description' => __( 'Whether the failed-request retry cron (every 30 min) is scheduled.', 'suretriggers' ),
305 ],
306 'cron_cleanup_scheduled' => [
307 'type' => 'boolean',
308 'description' => __( 'Whether the daily log cleanup cron is scheduled.', 'suretriggers' ),
309 ],
310 'cron_verify_scheduled' => [
311 'type' => 'boolean',
312 'description' => __( 'Whether the 6-hourly connection-verify cron is scheduled.', 'suretriggers' ),
313 ],
314 'webhook_table_exists' => [
315 'type' => 'boolean',
316 'description' => __( 'Whether the suretriggers_webhook_requests DB table exists.', 'suretriggers' ),
317 ],
318 'total_logs' => [
319 'type' => 'integer',
320 'description' => __( 'Total webhook log entries.', 'suretriggers' ),
321 ],
322 'success_logs' => [
323 'type' => 'integer',
324 'description' => __( 'Count of successful webhook log entries.', 'suretriggers' ),
325 ],
326 'failed_logs' => [
327 'type' => 'integer',
328 'description' => __( 'Count of failed webhook log entries.', 'suretriggers' ),
329 ],
330 'pending_logs' => [
331 'type' => 'integer',
332 'description' => __( 'Count of pending webhook log entries.', 'suretriggers' ),
333 ],
334 ],
335 ],
336 'execute_callback' => [ $this, 'execute_get_system_status' ],
337 'meta' => [
338 'show_in_rest' => true,
339 'annotations' => [
340 'readonly' => true,
341 'destructive' => false,
342 'idempotent' => true,
343 'priority' => 1.0,
344 'instructions' => 'Use this to generate a full health report. Highlight any unscheduled cron jobs or a missing webhook table as critical issues. If crons are missing or the DB table is absent, advise the user to deactivate and reactivate the plugin. Explain all fields to the user.',
345 ],
346 'mcp' => [ 'public' => true ],
347 ],
348 ]
349 );
350 }
351
352 /**
353 * Execute get-system-status.
354 *
355 * @param array $input Ability input (unused).
356 * @return array
357 */
358 public function execute_get_system_status( $input ) {
359 global $wpdb;
360
361 $connection_status = get_option( 'suretriggers_verify_connection', '' );
362
363 $table_name = WebhookRequestsController::get_table_name();
364 $table_exists = (bool) $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
365 $wpdb->prepare(
366 'SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = %s AND table_name = %s LIMIT 1',
367 DB_NAME,
368 $table_name
369 )
370 );
371
372 $total_logs = 0;
373 $success_logs = 0;
374 $failed_logs = 0;
375 $pending_logs = 0;
376
377 if ( $table_exists ) {
378 $total_logs = (int) $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
379 "SELECT COUNT(*) FROM {$table_name}" //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
380 );
381 $success_logs = (int) $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
382 $wpdb->prepare(
383 "SELECT COUNT(*) FROM {$table_name} WHERE status = %s", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
384 'success'
385 )
386 );
387 $failed_logs = (int) $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
388 $wpdb->prepare(
389 "SELECT COUNT(*) FROM {$table_name} WHERE status = %s", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
390 'failed'
391 )
392 );
393 $pending_logs = (int) $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
394 $wpdb->prepare(
395 "SELECT COUNT(*) FROM {$table_name} WHERE status = %s", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
396 'pending'
397 )
398 );
399 }
400
401 return [
402 'connection_status' => is_string( $connection_status ) ? $connection_status : '',
403 'cron_retry_scheduled' => (bool) wp_next_scheduled( 'suretriggers_retry_failed_requests' ),
404 'cron_cleanup_scheduled' => (bool) wp_next_scheduled( 'suretriggers_webhook_requests_cleanup_logs' ),
405 'cron_verify_scheduled' => (bool) wp_next_scheduled( 'suretriggers_verify_api_connection' ),
406 'webhook_table_exists' => $table_exists,
407 'total_logs' => $total_logs,
408 'success_logs' => $success_logs,
409 'failed_logs' => $failed_logs,
410 'pending_logs' => $pending_logs,
411 ];
412 }
413
414 // -------------------------------------------------------------------------
415 // ottokit/get-active-triggers
416 // -------------------------------------------------------------------------
417
418 /**
419 * Register the get-active-triggers ability.
420 *
421 * @return void
422 */
423 private function register_get_active_triggers() {
424 wp_register_ability(
425 'ottokit/get-active-triggers',
426 [
427 'label' => __( 'Get configured automation triggers', 'suretriggers' ),
428 'description' => __( 'Returns all automation triggers currently saved in OttoKit settings. Each entry shows the integration slug (e.g. WooCommerce) and the trigger event identifier. An empty list means no automations have been configured via the OttoKit SaaS dashboard yet. Use this to show the user which events are actively being watched on their site.', 'suretriggers' ),
429 'category' => 'ottokit-automation',
430 'permission_callback' => [ $this, 'permission_manage_options' ],
431 'input_schema' => [
432 'type' => 'object',
433 'properties' => [],
434 ],
435 'output_schema' => [
436 'type' => 'object',
437 'properties' => [
438 'triggers' => [
439 'type' => 'array',
440 'description' => __( 'List of configured trigger automations.', 'suretriggers' ),
441 'items' => [
442 'type' => 'object',
443 'properties' => [
444 'integration' => [
445 'type' => 'string',
446 'description' => __( 'Integration slug (e.g. WooCommerce, FluentCRM).', 'suretriggers' ),
447 ],
448 'trigger' => [
449 'type' => 'string',
450 'description' => __( 'Trigger event identifier.', 'suretriggers' ),
451 ],
452 ],
453 ],
454 ],
455 'count' => [
456 'type' => 'integer',
457 'description' => __( 'Total number of configured triggers.', 'suretriggers' ),
458 ],
459 ],
460 ],
461 'execute_callback' => [ $this, 'execute_get_active_triggers' ],
462 'meta' => [
463 'show_in_rest' => true,
464 'annotations' => [
465 'readonly' => true,
466 'destructive' => false,
467 'idempotent' => true,
468 'priority' => 1.0,
469 'instructions' => 'If the triggers list is empty, tell the user they have not set up any automations via the OttoKit SaaS dashboard yet. Use this before suggesting any setup steps.',
470 ],
471 'mcp' => [ 'public' => true ],
472 ],
473 ]
474 );
475 }
476
477 /**
478 * Execute get-active-triggers.
479 *
480 * @param array $input Ability input (unused).
481 * @return array
482 */
483 public function execute_get_active_triggers( $input ) {
484 $raw = OptionController::get_option( 'triggers' );
485 if ( ! is_array( $raw ) ) {
486 $raw = [];
487 }
488
489 $triggers = [];
490 foreach ( $raw as $item ) {
491 if ( ! is_array( $item ) ) {
492 continue;
493 }
494 $triggers[] = [
495 'integration' => isset( $item['integration'] ) ? (string) $item['integration'] : '',
496 'trigger' => isset( $item['trigger'] ) ? (string) $item['trigger'] : '',
497 ];
498 }
499
500 return [
501 'triggers' => $triggers,
502 'count' => count( $triggers ),
503 ];
504 }
505
506 // -------------------------------------------------------------------------
507 // ottokit/get-active-integrations
508 // -------------------------------------------------------------------------
509
510 /**
511 * Register the get-active-integrations ability.
512 *
513 * @return void
514 */
515 private function register_get_active_integrations() {
516 wp_register_ability(
517 'ottokit/get-active-integrations',
518 [
519 'label' => __( 'Get active OttoKit integrations', 'suretriggers' ),
520 'description' => __( 'Returns the slugs of all OttoKit-supported integrations whose plugin is currently installed and active on this WordPress site. An integration appears here only if its plugin is both installed and activated. Use this to confirm which plugins OttoKit can trigger or act upon right now.', 'suretriggers' ),
521 'category' => 'ottokit-automation',
522 'permission_callback' => [ $this, 'permission_manage_options' ],
523 'input_schema' => [
524 'type' => 'object',
525 'properties' => [],
526 ],
527 'output_schema' => [
528 'type' => 'object',
529 'properties' => [
530 'integrations' => [
531 'type' => 'array',
532 'items' => [ 'type' => 'string' ],
533 'description' => __( 'Integration slugs that are active (plugin installed and enabled).', 'suretriggers' ),
534 ],
535 'count' => [
536 'type' => 'integer',
537 'description' => __( 'Number of active integrations.', 'suretriggers' ),
538 ],
539 ],
540 ],
541 'execute_callback' => [ $this, 'execute_get_active_integrations' ],
542 'meta' => [
543 'show_in_rest' => true,
544 'annotations' => [
545 'readonly' => true,
546 'destructive' => false,
547 'idempotent' => true,
548 'priority' => 1.0,
549 'instructions' => 'If an integration the user expects is missing, its plugin is not installed or not activated. Suggest using ottokit/get-all-integrations with status=inactive to see what OttoKit-supported plugins are missing.',
550 ],
551 'mcp' => [ 'public' => true ],
552 ],
553 ]
554 );
555 }
556
557 /**
558 * Execute get-active-integrations.
559 *
560 * @param array $input Ability input (unused).
561 * @return array
562 */
563 public function execute_get_active_integrations( $input ) {
564 $integrations = IntegrationsController::get_activated_integrations();
565 if ( ! is_array( $integrations ) ) {
566 $integrations = [];
567 }
568
569 return [
570 'integrations' => array_values( $integrations ),
571 'count' => count( $integrations ),
572 ];
573 }
574
575 // -------------------------------------------------------------------------
576 // ottokit/get-all-integrations
577 // -------------------------------------------------------------------------
578
579 /**
580 * Register the get-all-integrations ability.
581 *
582 * @return void
583 */
584 private function register_get_all_integrations() {
585 wp_register_ability(
586 'ottokit/get-all-integrations',
587 [
588 'label' => __( 'Get all OttoKit integrations with status', 'suretriggers' ),
589 'description' => __( 'Returns all OttoKit-supported integrations with their enabled status. Filter by status: active (plugin installed and running), inactive (plugin missing or deactivated), or all. Always includes active_count and inactive_count totals regardless of the filter. Use status=inactive to help the user discover which OttoKit-supported plugins they could install.', 'suretriggers' ),
590 'category' => 'ottokit-automation',
591 'permission_callback' => [ $this, 'permission_manage_options' ],
592 'input_schema' => [
593 'type' => 'object',
594 'properties' => [
595 'status' => [
596 'type' => 'string',
597 'enum' => [ 'all', 'active', 'inactive' ],
598 'description' => __( 'Filter integrations by status. Defaults to all.', 'suretriggers' ),
599 'default' => 'all',
600 ],
601 ],
602 ],
603 'output_schema' => [
604 'type' => 'object',
605 'properties' => [
606 'integrations' => [
607 'type' => 'array',
608 'description' => __( 'List of integrations with their ID and enabled status.', 'suretriggers' ),
609 'items' => [
610 'type' => 'object',
611 'properties' => [
612 'id' => [
613 'type' => 'string',
614 'description' => __( 'Integration slug.', 'suretriggers' ),
615 ],
616 'enabled' => [
617 'type' => 'boolean',
618 'description' => __( 'Whether the integration plugin is installed and active.', 'suretriggers' ),
619 ],
620 ],
621 ],
622 ],
623 'total' => [
624 'type' => 'integer',
625 'description' => __( 'Total integrations in the filtered result.', 'suretriggers' ),
626 ],
627 'active_count' => [
628 'type' => 'integer',
629 'description' => __( 'Number of active integrations across all supported integrations.', 'suretriggers' ),
630 ],
631 'inactive_count' => [
632 'type' => 'integer',
633 'description' => __( 'Number of inactive integrations across all supported integrations.', 'suretriggers' ),
634 ],
635 ],
636 ],
637 'execute_callback' => [ $this, 'execute_get_all_integrations' ],
638 'meta' => [
639 'show_in_rest' => true,
640 'annotations' => [
641 'readonly' => true,
642 'destructive' => false,
643 'idempotent' => true,
644 'priority' => 1.0,
645 'instructions' => 'Use status=active to show available integrations. Use status=inactive to help the user discover OttoKit-supported plugins they could install. Always include active_count and inactive_count in your response.',
646 ],
647 'mcp' => [ 'public' => true ],
648 ],
649 ]
650 );
651 }
652
653 /**
654 * Execute get-all-integrations.
655 *
656 * @param array $input Ability input.
657 * @return array
658 */
659 public function execute_get_all_integrations( $input ) {
660 $all_classes = IntegrationsController::get_integrations();
661 $status_filter = isset( $input['status'] ) ? (string) $input['status'] : 'all';
662 $allowed = [ 'all', 'active', 'inactive' ];
663 if ( ! in_array( $status_filter, $allowed, true ) ) {
664 $status_filter = 'all';
665 }
666
667 $all_integrations = [];
668 $active_count = 0;
669 $inactive_count = 0;
670
671 foreach ( $all_classes as $id => $class ) {
672 $enabled = $class->is_enabled();
673
674 if ( $enabled ) {
675 $active_count++;
676 } else {
677 $inactive_count++;
678 }
679
680 if ( 'all' === $status_filter ||
681 ( 'active' === $status_filter && $enabled ) ||
682 ( 'inactive' === $status_filter && ! $enabled )
683 ) {
684 $all_integrations[] = [
685 'id' => (string) $id,
686 'enabled' => $enabled,
687 ];
688 }
689 }
690
691 return [
692 'integrations' => $all_integrations,
693 'total' => count( $all_integrations ),
694 'active_count' => $active_count,
695 'inactive_count' => $inactive_count,
696 ];
697 }
698
699 // -------------------------------------------------------------------------
700 // ottokit/get-integration-events
701 // -------------------------------------------------------------------------
702
703 /**
704 * Register the get-integration-events ability.
705 *
706 * @return void
707 */
708 private function register_get_integration_events() {
709 wp_register_ability(
710 'ottokit/get-integration-events',
711 [
712 'label' => __( 'Get triggers and actions for an integration', 'suretriggers' ),
713 'description' => __( 'Returns all registered triggers and actions for a named OttoKit integration. Triggers fire when an event occurs in the plugin (e.g. a WooCommerce order is placed). Actions perform operations in that plugin (e.g. enrol a user in a course). Requires an exact integration slug — call ottokit/get-active-integrations first to get valid slugs. Returns trigger_count and action_count.', 'suretriggers' ),
714 'category' => 'ottokit-automation',
715 'permission_callback' => [ $this, 'permission_manage_options' ],
716 'input_schema' => [
717 'type' => 'object',
718 'required' => [ 'integration' ],
719 'properties' => [
720 'integration' => [
721 'type' => 'string',
722 'description' => __( 'Integration slug (e.g. WooCommerce, FluentCRM, WordPress). Use get-active-integrations to list valid IDs.', 'suretriggers' ),
723 ],
724 ],
725 ],
726 'output_schema' => [
727 'type' => 'object',
728 'properties' => [
729 'integration' => [
730 'type' => 'string',
731 'description' => __( 'The requested integration slug.', 'suretriggers' ),
732 ],
733 'triggers' => [
734 'type' => 'object',
735 'description' => __( 'Registered triggers keyed by trigger identifier.', 'suretriggers' ),
736 ],
737 'actions' => [
738 'type' => 'object',
739 'description' => __( 'Registered actions keyed by action identifier.', 'suretriggers' ),
740 ],
741 'trigger_count' => [
742 'type' => 'integer',
743 'description' => __( 'Number of registered triggers for this integration.', 'suretriggers' ),
744 ],
745 'action_count' => [
746 'type' => 'integer',
747 'description' => __( 'Number of registered actions for this integration.', 'suretriggers' ),
748 ],
749 ],
750 ],
751 'execute_callback' => [ $this, 'execute_get_integration_events' ],
752 'meta' => [
753 'show_in_rest' => true,
754 'annotations' => [
755 'readonly' => true,
756 'destructive' => false,
757 'idempotent' => true,
758 'priority' => 1.0,
759 'instructions' => 'Always call ottokit/get-active-integrations first to confirm the integration slug is valid. Integration slugs are case-sensitive. If the integration is not found, show the user the valid slugs from get-active-integrations.',
760 ],
761 'mcp' => [ 'public' => true ],
762 ],
763 ]
764 );
765 }
766
767 /**
768 * Execute get-integration-events.
769 *
770 * @param array $input Ability input.
771 * @return array|\WP_Error
772 */
773 public function execute_get_integration_events( $input ) {
774 if ( empty( $input['integration'] ) || ! is_string( $input['integration'] ) ) {
775 return new \WP_Error(
776 'missing_integration',
777 __( 'The integration parameter is required.', 'suretriggers' )
778 );
779 }
780
781 $integration_id = sanitize_text_field( $input['integration'] );
782 $event_ctrl = EventController::get_instance();
783
784 $triggers = isset( $event_ctrl->triggers[ $integration_id ] ) && is_array( $event_ctrl->triggers[ $integration_id ] )
785 ? $event_ctrl->triggers[ $integration_id ]
786 : [];
787
788 $actions = isset( $event_ctrl->actions[ $integration_id ] ) && is_array( $event_ctrl->actions[ $integration_id ] )
789 ? $event_ctrl->actions[ $integration_id ]
790 : [];
791
792 if ( empty( $triggers ) && empty( $actions ) ) {
793 $valid_ids = array_keys( $event_ctrl->triggers );
794 return new \WP_Error(
795 'integration_not_found',
796 sprintf(
797 /* translators: %s: integration slug */
798 __( 'No events found for integration "%s". Use get-active-integrations to get valid integration IDs.', 'suretriggers' ),
799 $integration_id
800 ),
801 defined( 'WP_DEBUG' ) && WP_DEBUG ? [ 'available_integrations' => $valid_ids ] : []
802 );
803 }
804
805 return [
806 'integration' => $integration_id,
807 'triggers' => $triggers,
808 'actions' => $actions,
809 'trigger_count' => count( $triggers ),
810 'action_count' => count( $actions ),
811 ];
812 }
813
814 // -------------------------------------------------------------------------
815 // ottokit/get-webhook-logs
816 // -------------------------------------------------------------------------
817
818 /**
819 * Register the get-webhook-logs ability.
820 *
821 * @return void
822 */
823 private function register_get_webhook_logs() {
824 wp_register_ability(
825 'ottokit/get-webhook-logs',
826 [
827 'label' => __( 'Get webhook execution logs', 'suretriggers' ),
828 'description' => __( 'Returns paginated webhook execution logs from the OttoKit database. Filter by status (success, failed, pending, all), date_after, and date_before. Each entry includes: HTTP response_code, error_info message, retry_attempts count, and timestamps. A 401 or 403 response_code means an authentication problem. Use this to debug failed automation triggers.', 'suretriggers' ),
829 'category' => 'ottokit-automation',
830 'permission_callback' => [ $this, 'permission_manage_options' ],
831 'input_schema' => [
832 'type' => 'object',
833 'properties' => [
834 'status' => [
835 'type' => 'string',
836 'enum' => [ 'all', 'success', 'failed', 'pending' ],
837 'description' => __( 'Filter logs by execution status. Defaults to all.', 'suretriggers' ),
838 'default' => 'all',
839 ],
840 'date_after' => [
841 'type' => 'string',
842 'description' => __( 'Return logs created after this datetime (ISO 8601 or MySQL format, e.g. "2026-03-05 00:00:00"). Optional.', 'suretriggers' ),
843 ],
844 'date_before' => [
845 'type' => 'string',
846 'description' => __( 'Return logs created before this datetime (ISO 8601 or MySQL format, e.g. "2026-03-06 23:59:59"). Optional.', 'suretriggers' ),
847 ],
848 'per_page' => [
849 'type' => 'integer',
850 'description' => __( 'Number of logs per page. Min 1, max 100. Defaults to 20.', 'suretriggers' ),
851 'default' => 20,
852 'minimum' => 1,
853 'maximum' => 100,
854 ],
855 'page' => [
856 'type' => 'integer',
857 'description' => __( 'Page number (1-indexed). Defaults to 1.', 'suretriggers' ),
858 'default' => 1,
859 'minimum' => 1,
860 ],
861 ],
862 ],
863 'output_schema' => [
864 'type' => 'object',
865 'properties' => [
866 'logs' => [
867 'type' => 'array',
868 'description' => __( 'Webhook log entries.', 'suretriggers' ),
869 'items' => [
870 'type' => 'object',
871 'properties' => [
872 'id' => [ 'type' => 'integer' ],
873 'status' => [ 'type' => 'string' ],
874 'response_code' => [ 'type' => 'integer' ],
875 'error_info' => [ 'type' => 'string' ],
876 'retry_attempts' => [ 'type' => 'integer' ],
877 'created_at' => [ 'type' => 'string' ],
878 'updated_at' => [ 'type' => [ 'string', 'null' ] ],
879 ],
880 ],
881 ],
882 'total' => [
883 'type' => 'integer',
884 'description' => __( 'Total matching log entries.', 'suretriggers' ),
885 ],
886 'page' => [
887 'type' => 'integer',
888 'description' => __( 'Current page number.', 'suretriggers' ),
889 ],
890 'per_page' => [
891 'type' => 'integer',
892 'description' => __( 'Logs per page.', 'suretriggers' ),
893 ],
894 'total_pages' => [
895 'type' => 'integer',
896 'description' => __( 'Total number of pages.', 'suretriggers' ),
897 ],
898 ],
899 ],
900 'execute_callback' => [ $this, 'execute_get_webhook_logs' ],
901 'meta' => [
902 'show_in_rest' => true,
903 'annotations' => [
904 'readonly' => true,
905 'destructive' => false,
906 'idempotent' => true,
907 'priority' => 1.0,
908 'instructions' => 'When investigating failures, always filter by status=failed. Report the total count, the most recent failure (created_at, error_info, response_code), and retry_attempts. A 401 or 403 response_code means an authentication issue — suggest running ottokit/get-connection-status. Use date_after to limit to recent failures.',
909 ],
910 'mcp' => [ 'public' => true ],
911 ],
912 ]
913 );
914 }
915
916 /**
917 * Execute get-webhook-logs.
918 *
919 * @param array $input Ability input.
920 * @return array|\WP_Error
921 */
922 public function execute_get_webhook_logs( $input ) {
923 global $wpdb;
924
925 $allowed_statuses = [ 'success', 'failed', 'pending' ];
926 $status = isset( $input['status'] ) ? (string) $input['status'] : 'all';
927 if ( 'all' !== $status && ! in_array( $status, $allowed_statuses, true ) ) {
928 $status = 'all';
929 }
930
931 $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 20;
932 $per_page = max( 1, min( 100, $per_page ) );
933
934 $page = isset( $input['page'] ) ? (int) $input['page'] : 1;
935 $page = max( 1, $page );
936 $offset = ( $page - 1 ) * $per_page;
937
938 // Date filters — validate by attempting to parse via strtotime.
939 $date_after = isset( $input['date_after'] ) ? sanitize_text_field( (string) $input['date_after'] ) : '';
940 $date_before = isset( $input['date_before'] ) ? sanitize_text_field( (string) $input['date_before'] ) : '';
941
942 if ( '' !== $date_after ) {
943 $ts = strtotime( $date_after );
944 if ( false === $ts ) {
945 $date_after = '';
946 } else {
947 $date_after = gmdate( 'Y-m-d H:i:s', $ts );
948 }
949 }
950
951 if ( '' !== $date_before ) {
952 $ts = strtotime( $date_before );
953 if ( false === $ts ) {
954 $date_before = '';
955 } else {
956 $date_before = gmdate( 'Y-m-d H:i:s', $ts );
957 }
958 }
959
960 $table = WebhookRequestsController::get_table_name();
961
962 $total = (int) $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
963 $wpdb->prepare(
964 "SELECT COUNT(*) FROM {$table} WHERE (%s = 'all' OR status = %s) AND (%s = '' OR created_at >= %s) AND (%s = '' OR created_at <= %s)", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
965 $status,
966 $status,
967 $date_after,
968 $date_after,
969 $date_before,
970 $date_before
971 )
972 );
973 $rows = $wpdb->get_results( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
974 $wpdb->prepare(
975 "SELECT id, status, response_code, error_info, retry_attempts, created_at, updated_at FROM {$table} WHERE (%s = 'all' OR status = %s) AND (%s = '' OR created_at >= %s) AND (%s = '' OR created_at <= %s) ORDER BY id DESC LIMIT %d OFFSET %d", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
976 $status,
977 $status,
978 $date_after,
979 $date_after,
980 $date_before,
981 $date_before,
982 $per_page,
983 $offset
984 ),
985 ARRAY_A
986 );
987
988 if ( ! is_array( $rows ) ) {
989 $rows = [];
990 }
991
992 $logs = [];
993 foreach ( $rows as $row ) {
994 $logs[] = [
995 'id' => (int) $row['id'],
996 'status' => (string) $row['status'],
997 'response_code' => (int) $row['response_code'],
998 'error_info' => (string) $row['error_info'],
999 'retry_attempts' => (int) $row['retry_attempts'],
1000 'created_at' => (string) $row['created_at'],
1001 'updated_at' => isset( $row['updated_at'] ) && ! empty( $row['updated_at'] ) ? (string) $row['updated_at'] : null,
1002 ];
1003 }
1004
1005 $total_pages = ( $per_page > 0 ) ? (int) ceil( $total / $per_page ) : 0;
1006
1007 return [
1008 'logs' => $logs,
1009 'total' => $total,
1010 'page' => $page,
1011 'per_page' => $per_page,
1012 'total_pages' => $total_pages,
1013 ];
1014 }
1015
1016 // -------------------------------------------------------------------------
1017 // Public permission callbacks (must be public for add_action compatibility)
1018 // -------------------------------------------------------------------------
1019
1020 /**
1021 * Public permission callback — manage_options capability.
1022 *
1023 * @return bool
1024 */
1025 public function permission_manage_options() {
1026 return current_user_can( 'manage_options' );
1027 }
1028 }
1029