PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 2.8.17 2.8.18 All 259 releases
wpfunnels / includes / core / MCP / Tools / ContextTools.php

ContextTools.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at includes/core/MCP/Tools/ContextTools.php

635 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ContextTools — discovery and setup abilities.
4 *
5 * `get-funnel-context` is the first call any agent should make: it reports what
6 * this install actually supports so the model never proposes a step type,
7 * builder or offer this site cannot build.
8 *
9 * @package WPFunnels\MCP
10 * @since 3.13.0
11 */
12
13 namespace WPFunnels\MCP\Tools;
14
15 defined( 'ABSPATH' ) || exit;
16
17 use WPFunnels\MCP\Helpers\MCPHelper;
18 use WPFunnels\Wpfnl_functions;
19 use WPFunnels\Rest\Controllers\SettingsController;
20
21 /**
22 * Class ContextTools
23 */
24 class ContextTools {
25
26 /**
27 * Transient holding the cached context payload.
28 */
29 private const CACHE_KEY = '_wpfnl_mcp_context_cache';
30
31 /**
32 * Cache lifetime in seconds. Short: funnels change while an agent works.
33 */
34 private const CACHE_TTL = 60;
35
36 /**
37 * WPFunnels-owned option keys the generic settings mechanism may read/write.
38 *
39 * This is a verbatim copy of the explicit allow-list inside
40 * `SettingsController::is_allowed_settings_group()`. That method is a
41 * security boundary: group_id is passed straight into
42 * get_option()/update_option(), so an unguarded value could target
43 * arbitrary WordPress options (e.g. `wp_user_roles`). The source method is
44 * `protected` with no legitimate inheritance path from this class, so the
45 * list is duplicated rather than called into — keep it byte-for-byte in
46 * sync with the controller if that list ever changes.
47 *
48 * @see \WPFunnels\Rest\Controllers\SettingsController::is_allowed_settings_group()
49 */
50 private const ALLOWED_SETTINGS_GROUPS = [
51 '_wpfunnels_general_settings',
52 '_wpfunnels_permalink_settings',
53 '_wpfunnels_optin_settings',
54 '_wpfunnels_advanced_settings',
55 '_wpfunnels_integrations',
56 '_wpfunnels_notification_settings',
57 '_wpfunnels_lms_settings',
58 '_wpfunnels_recaptcha_setting',
59 '_wpfunnels_google_map_api_key',
60 '_wpfunnels_user_roles',
61 ];
62
63 /**
64 * Ability definitions for this domain.
65 *
66 * @return array
67 */
68 public static function definitions() {
69 return [
70 'wpfunnels/get-funnel-context' => [
71 'label' => __( 'Get Funnel Context', 'wpfnl' ),
72 'description' => 'Snapshot of this WPFunnels install: plugin versions, licence tier, active page builder, WooCommerce and Mail Mint availability, currency, funnel counts by type, supported step types, and the guidelines for building funnels here. Call this FIRST in any session — it tells you what this site can actually do.',
73 'input_schema' => [
74 'type' => 'object',
75 'properties' => [],
76 ],
77 'execute_callback' => [ __CLASS__, 'getFunnelContext' ],
78 'permission_callback' => MCPHelper::currentUserCan(),
79 'annotations' => [ 'readonly' ],
80 ],
81 'wpfunnels/get-site-design-tokens' => [
82 'label' => __( 'Get Site Design Tokens', 'wpfnl' ),
83 'description' => 'The site\'s Elementor design tokens (currently just the kit\'s primary brand color, if Elementor is active). compose-step-layout already applies this automatically to the widgets it builds — call this only to answer a direct question about the site\'s brand color, not before composing a page.',
84 'input_schema' => [
85 'type' => 'object',
86 'properties' => [],
87 ],
88 'execute_callback' => [ __CLASS__, 'getSiteDesignTokens' ],
89 'permission_callback' => MCPHelper::currentUserCan(),
90 'annotations' => [ 'readonly' ],
91 ],
92 'wpfunnels/check-setup-status' => [
93 'label' => __( 'Check Setup Status', 'wpfnl' ),
94 'description' => 'Audit whether this install is ready to run funnels: setup wizard completion, page builder availability, WooCommerce state, permalink structure, global settings and licence. Returns a list of issues with the admin URL to fix each one.',
95 'input_schema' => [
96 'type' => 'object',
97 'properties' => [],
98 ],
99 'execute_callback' => [ __CLASS__, 'checkSetupStatus' ],
100 'permission_callback' => MCPHelper::currentUserCan(),
101 'annotations' => [ 'readonly' ],
102 ],
103 'wpfunnels/update-global-settings' => [
104 'label' => __( 'Update Global Settings', 'wpfnl' ),
105 'description' => 'Update WPFunnels\' site-wide settings (default page builder, order bump enable, A/B testing enable, funnel builder mode, and similar toggles from Settings → General). Only recognized keys are written; unknown keys are ignored. This is a merge, not a full replace — existing settings not mentioned are left alone.',
106 'input_schema' => [
107 'type' => 'object',
108 'properties' => [
109 'settings' => [
110 'type' => 'object',
111 'description' => 'Key/value pairs matching the keys returned by get-funnel-context\'s settings section (e.g. builder, order_bump, ab_testing, funnel_builder_mode).',
112 ],
113 ],
114 'required' => [ 'settings' ],
115 ],
116 'execute_callback' => [ __CLASS__, 'updateGlobalSettings' ],
117 'permission_callback' => MCPHelper::currentUserCan(),
118 'annotations' => [ 'destructive' ],
119 ],
120 'wpfunnels/update-permalink-settings' => [
121 'label' => __( 'Update Permalink Settings', 'wpfnl' ),
122 'description' => 'Update WPFunnels\' funnel/step URL structure: the permalink style and the URL base segments used for funnel and step pages. Changes site URL structure — every existing funnel/step link changes if the base segments change.',
123 'input_schema' => [
124 'type' => 'object',
125 'properties' => [
126 'structure' => [
127 'type' => 'string',
128 'description' => 'Permalink style for funnel/step URLs.',
129 ],
130 'funnel_base' => [
131 'type' => 'string',
132 'description' => 'URL base segment for funnels.',
133 ],
134 'step_base' => [
135 'type' => 'string',
136 'description' => 'URL base segment for steps.',
137 ],
138 ],
139 ],
140 'execute_callback' => [ __CLASS__, 'updatePermalinkSettings' ],
141 'permission_callback' => MCPHelper::currentUserCan( 'manage_options' ),
142 'annotations' => [ 'destructive' ],
143 ],
144 'wpfunnels/get-settings-group' => [
145 'label' => __( 'Get Settings Group', 'wpfnl' ),
146 'description' => 'Read one WPFunnels site-wide settings group by its option-group ID. Covers tabs with no dedicated ability: _wpfunnels_optin_settings (global opt-in email defaults — sender_name, sender_email, email_subject), _wpfunnels_lms_settings (lms_provider and other LMS defaults), _wpfunnels_notification_settings (revenue report emails — enable_revenue_report, revenue_report_frequency, revenue_report_recipient, revenue_report_subject, send_time), _wpfunnels_recaptcha_setting (enable_recaptcha, recaptcha_site_key, recaptcha_site_secret), _wpfunnels_advanced_settings (show_supported_payment_gateway), _wpfunnels_google_map_api_key (a plain string, returned as {value: "..."}), _wpfunnels_user_roles (map of role slug to "yes"/"no" funnel-management access), and _wpfunnels_integrations (reserved for a future integrations tab; not currently populated by core). _wpfunnels_general_settings and _wpfunnels_permalink_settings also work here but get-funnel-context / update-global-settings / update-permalink-settings are the better-typed way to read/write those. Only WPFunnels-owned _wpfunnels_* option keys are accepted; anything else is rejected.',
147 'input_schema' => [
148 'type' => 'object',
149 'properties' => [
150 'group_id' => [
151 'type' => 'string',
152 'description' => 'Settings group / option key to read, e.g. _wpfunnels_optin_settings, _wpfunnels_notification_settings, _wpfunnels_recaptcha_setting, _wpfunnels_lms_settings, _wpfunnels_advanced_settings, _wpfunnels_google_map_api_key, _wpfunnels_user_roles, _wpfunnels_integrations, _wpfunnels_general_settings, _wpfunnels_permalink_settings.',
153 ],
154 ],
155 'required' => [ 'group_id' ],
156 ],
157 'execute_callback' => [ __CLASS__, 'getSettingsGroup' ],
158 'permission_callback' => MCPHelper::currentUserCan(),
159 'annotations' => [ 'readonly' ],
160 ],
161 'wpfunnels/update-settings-group' => [
162 'label' => __( 'Update Settings Group', 'wpfnl' ),
163 'description' => 'Update one or more keys in a WPFunnels site-wide settings group (see get-settings-group for the field names each group holds). This is a merge into the existing group, applied one key at a time — a later key failing does not roll back keys already applied. Only WPFunnels-owned _wpfunnels_* option keys are accepted, the same allow-list get-settings-group uses; anything else is rejected. Changes here are site-wide and can affect every funnel (e.g. disabling reCAPTCHA, changing revenue report recipients, changing the LMS provider) — use narrower abilities like update-global-settings or update-permalink-settings when they already cover the field you need.',
164 'input_schema' => [
165 'type' => 'object',
166 'properties' => [
167 'group_id' => [
168 'type' => 'string',
169 'description' => 'Settings group / option key to update. See get-settings-group for the allowed groups and their fields.',
170 ],
171 'settings' => [
172 'type' => 'object',
173 'description' => 'Key/value pairs to merge into this group, e.g. {"enable_recaptcha": "yes", "recaptcha_site_key": "..."}. For the scalar _wpfunnels_google_map_api_key group, pass {"value": "..."}.',
174 ],
175 ],
176 'required' => [ 'group_id', 'settings' ],
177 ],
178 'execute_callback' => [ __CLASS__, 'updateSettingsGroup' ],
179 'permission_callback' => MCPHelper::currentUserCan( 'manage_options' ),
180 'annotations' => [ 'destructive' ],
181 ],
182 ];
183 }
184
185 /**
186 * Invalidate the cached context. Wired to funnel/step mutations.
187 *
188 * @return void
189 */
190 public static function invalidateCache() {
191 delete_transient( self::CACHE_KEY );
192 }
193
194 /**
195 * Build (or serve cached) install context.
196 *
197 * @return array
198 */
199 public static function getFunnelContext() {
200 $cached = get_transient( self::CACHE_KEY );
201 if ( is_array( $cached ) ) {
202 return $cached;
203 }
204
205 $general = Wpfnl_functions::get_general_settings();
206 $wc = Wpfnl_functions::is_wc_active();
207 $pro = Wpfnl_functions::is_wpfnl_pro_activated();
208 $counts = self::funnelCounts();
209 $builder = Wpfnl_functions::get_builder_type();
210
211 $context = [
212 'site' => [
213 'name' => get_bloginfo( 'name' ),
214 'url' => home_url(),
215 'admin_url' => admin_url(),
216 'locale' => get_locale(),
217 'timezone' => wp_timezone_string(),
218 'today' => current_time( 'Y-m-d' ),
219 ],
220 'plugin' => [
221 'version' => defined( 'WPFNL_VERSION' ) ? WPFNL_VERSION : '',
222 'pro_active' => $pro,
223 'pro_version' => defined( 'WPFNL_PRO_VERSION' ) ? WPFNL_PRO_VERSION : '',
224 ],
225 'capabilities' => [
226 'page_builder' => $builder ? $builder : 'gutenberg',
227 'elementor_active' => Wpfnl_functions::is_elementor_active(),
228 'woocommerce' => $wc,
229 'currency' => $wc && function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '',
230 'currency_symbol' => $wc && function_exists( 'get_woocommerce_currency_symbol' ) ? html_entity_decode( get_woocommerce_currency_symbol() ) : '',
231 'mail_mint' => class_exists( '\Mint\MRM\Internal\AI\AIInit' ) || defined( 'MAIL_MINT_VERSION' ),
232 'lms' => Wpfnl_functions::is_lms_addon_active(),
233 'supported_step_types' => MCPHelper::supportedStepTypes(),
234 'offer_steps' => $wc && $pro,
235 'order_bump' => $wc,
236 'ab_testing' => $pro,
237 'analytics' => $pro,
238 ],
239 'funnels' => $counts,
240 'settings' => [
241 'permalink_base' => isset( $general['permalink'] ) ? $general['permalink'] : '',
242 'funnel_builder_mode' => isset( $general['funnel_builder_mode'] ) ? $general['funnel_builder_mode'] : 'horizontal',
243 ],
244 'guidelines' => self::guidelines( $wc, $pro ),
245 ];
246
247 set_transient( self::CACHE_KEY, $context, self::CACHE_TTL );
248
249 return $context;
250 }
251
252 /**
253 * The site's Elementor design tokens.
254 *
255 * @return array
256 */
257 public static function getSiteDesignTokens() {
258 return MCPHelper::getSiteDesignTokens();
259 }
260
261 /**
262 * Setup audit with actionable links.
263 *
264 * @return array
265 */
266 public static function checkSetupStatus() {
267 $issues = [];
268 $general = Wpfnl_functions::get_general_settings();
269 $wc = Wpfnl_functions::is_wc_active();
270
271 if ( ! $wc && ! Wpfnl_functions::is_lms_addon_active() ) {
272 $issues[] = [
273 'severity' => 'warning',
274 'issue' => 'WooCommerce is not active, so checkout, order bump and offer steps are unavailable.',
275 'fix' => 'Install and activate WooCommerce, or build lead-generation funnels instead.',
276 'url' => admin_url( 'plugin-install.php?s=woocommerce&tab=search&type=term' ),
277 ];
278 }
279
280 if ( ! get_option( 'permalink_structure' ) ) {
281 $issues[] = [
282 'severity' => 'error',
283 'issue' => 'Plain permalinks are enabled; funnel step URLs will not resolve.',
284 'fix' => 'Switch to any pretty permalink structure.',
285 'url' => admin_url( 'options-permalink.php' ),
286 ];
287 }
288
289 if ( empty( $general['builder'] ) ) {
290 $issues[] = [
291 'severity' => 'warning',
292 'issue' => 'No page builder is selected, so new steps fall back to the block editor.',
293 'fix' => 'Pick a page builder in WPFunnels settings.',
294 'url' => admin_url( 'admin.php?page=' . ( defined( 'WPFNL_GLOBAL_SETTINGS_SLUG' ) ? WPFNL_GLOBAL_SETTINGS_SLUG : 'wpfnl_settings' ) ),
295 ];
296 }
297
298 if ( ! Wpfnl_functions::is_wpfnl_pro_activated() ) {
299 $issues[] = [
300 'severity' => 'info',
301 'issue' => 'WPFunnels Pro is not active: A/B testing, analytics, one-click upsells and CRM integrations are unavailable.',
302 'fix' => 'Activate WPFunnels Pro to unlock those tools.',
303 'url' => admin_url( 'admin.php?page=wpf-license' ),
304 ];
305 }
306
307 $counts = self::funnelCounts();
308 if ( 0 === $counts['total'] ) {
309 $issues[] = [
310 'severity' => 'info',
311 'issue' => 'No funnels exist yet.',
312 'fix' => 'Create one with wpfunnels/create-funnel.',
313 'url' => admin_url( 'admin.php?page=' . ( defined( 'WPFNL_FUNNEL_PAGE_SLUG' ) ? WPFNL_FUNNEL_PAGE_SLUG : 'wp_funnels' ) ),
314 ];
315 }
316
317 return [
318 'ready' => empty(
319 array_filter(
320 $issues,
321 static function ( $issue ) {
322 return 'error' === $issue['severity'];
323 }
324 )
325 ),
326 'issues' => $issues,
327 ];
328 }
329
330 /**
331 * Merge changes into WPFunnels' general settings option.
332 *
333 * Only keys already present in `get_general_settings()`'s defaults are
334 * writable — this is the same option the Settings → General screen reads
335 * and writes, so an unrecognized key here would just be dead weight in
336 * the option, never surfaced anywhere.
337 *
338 * @param array $input Tool input.
339 * @return array|\WP_Error
340 */
341 public static function updateGlobalSettings( $input = [] ) {
342 $changes = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
343 if ( empty( $changes ) ) {
344 return MCPHelper::error( 'missing_settings', 'Provide at least one setting to update.' );
345 }
346
347 $current = Wpfnl_functions::get_general_settings();
348 $allowed = array_intersect_key( $changes, $current );
349
350 if ( empty( $allowed ) ) {
351 return MCPHelper::error(
352 'no_recognized_keys',
353 'None of the given keys are recognized general settings.',
354 [ 'recognized_keys' => array_keys( $current ) ]
355 );
356 }
357
358 $updated = array_merge( $current, $allowed );
359 Wpfnl_functions::update_admin_settings( '_wpfunnels_general_settings', $updated );
360
361 self::invalidateCache();
362
363 return [
364 'success' => true,
365 'changed' => array_keys( $allowed ),
366 'settings' => $updated,
367 ];
368 }
369
370 /**
371 * Merge changes into WPFunnels' funnel/step permalink settings.
372 *
373 * @param array $input Tool input.
374 * @return array|\WP_Error
375 */
376 public static function updatePermalinkSettings( $input = [] ) {
377 $changes = array_intersect_key( $input, array_flip( [ 'structure', 'funnel_base', 'step_base' ] ) );
378 if ( empty( $changes ) ) {
379 return MCPHelper::error( 'missing_settings', 'Provide at least one of structure, funnel_base, step_base.' );
380 }
381
382 $current = Wpfnl_functions::get_permalink_settings();
383 $updated = array_merge( $current, array_map( 'sanitize_text_field', $changes ) );
384
385 Wpfnl_functions::update_admin_settings( '_wpfunnels_permalink_settings', $updated );
386 Wpfnl_functions::update_admin_settings( '_wpfunnels_permalink_saved', true );
387 flush_rewrite_rules();
388
389 self::invalidateCache();
390
391 return [
392 'success' => true,
393 'changed' => array_keys( $changes ),
394 'settings' => $updated,
395 ];
396 }
397
398 /**
399 * Read one allow-listed WPFunnels settings group.
400 *
401 * Reuses `SettingsController::get_group_settings()` for the actual read
402 * (including its own internal allow-list check) so this stays byte-for-byte
403 * consistent with what the Settings screens themselves read.
404 *
405 * @param array $input Tool input.
406 * @return array|\WP_Error
407 */
408 public static function getSettingsGroup( $input = [] ) {
409 $group_id = isset( $input['group_id'] ) ? sanitize_text_field( $input['group_id'] ) : '';
410
411 if ( ! self::isAllowedSettingsGroup( $group_id ) ) {
412 return MCPHelper::error(
413 'invalid_settings_group',
414 'group_id is not a recognized WPFunnels settings group.',
415 [ 'allowed_groups' => self::ALLOWED_SETTINGS_GROUPS ]
416 );
417 }
418
419 $controller = new SettingsController();
420 $result = $controller->get_group_settings( $group_id );
421
422 if ( is_wp_error( $result ) ) {
423 return MCPHelper::error( 'settings_group_error', $result->get_error_message() );
424 }
425
426 if ( empty( $result ) ) {
427 // get_group_settings() only flattens array-shaped options; a scalar
428 // option (currently only _wpfunnels_google_map_api_key) foreach()es
429 // to nothing there, so fall back to the raw stored value.
430 $raw = Wpfnl_functions::get_admin_settings( $group_id );
431 if ( ! empty( $raw ) && ! is_array( $raw ) ) {
432 return [
433 'group_id' => $group_id,
434 'settings' => [ 'value' => $raw ],
435 ];
436 }
437
438 return [
439 'group_id' => $group_id,
440 'settings' => [],
441 ];
442 }
443
444 $settings = [];
445 foreach ( $result as $entry ) {
446 $settings[ $entry['id'] ] = $entry['value'];
447 }
448
449 return [
450 'group_id' => $group_id,
451 'settings' => $settings,
452 ];
453 }
454
455 /**
456 * Merge changes into one allow-listed WPFunnels settings group.
457 *
458 * Reuses `SettingsController::update_settings()` for the actual write, one
459 * key at a time (that method is built around updating a single named
460 * setting per call). `type` is forced to `ignore_activation` so this never
461 * triggers the controller's plugin-activation side effect, which is meant
462 * for the setup wizard's own flow, not a generic settings write.
463 *
464 * @param array $input Tool input.
465 * @return array|\WP_Error
466 */
467 public static function updateSettingsGroup( $input = [] ) {
468 $group_id = isset( $input['group_id'] ) ? sanitize_text_field( $input['group_id'] ) : '';
469
470 if ( ! self::isAllowedSettingsGroup( $group_id ) ) {
471 return MCPHelper::error(
472 'invalid_settings_group',
473 'group_id is not a recognized WPFunnels settings group.',
474 [ 'allowed_groups' => self::ALLOWED_SETTINGS_GROUPS ]
475 );
476 }
477
478 $settings = isset( $input['settings'] ) ? $input['settings'] : null;
479 if ( ! is_array( $settings ) || empty( $settings ) ) {
480 return MCPHelper::error( 'missing_settings', 'Provide a settings object with at least one key to update.' );
481 }
482
483 // _wpfunnels_google_map_api_key stores a plain string, not a keyed
484 // array. update_settings() assumes every group is an associative array
485 // and would corrupt a string option via `$settings['x'] = ...` (illegal
486 // string offset), so this one group is written directly instead of
487 // being looped through that method.
488 if ( '_wpfunnels_google_map_api_key' === $group_id ) {
489 $value = isset( $settings['value'] ) ? $settings['value'] : reset( $settings );
490 Wpfnl_functions::update_admin_settings( $group_id, sanitize_text_field( $value ) );
491
492 self::invalidateCache();
493
494 return [
495 'success' => true,
496 'group_id' => $group_id,
497 'changed' => [ 'value' ],
498 'settings' => [ 'value' => Wpfnl_functions::get_admin_settings( $group_id, '' ) ],
499 ];
500 }
501
502 $controller = new SettingsController();
503 $changed = [];
504
505 foreach ( $settings as $key => $value ) {
506 $key = sanitize_text_field( $key );
507
508 $result = $controller->update_settings(
509 [
510 'group_id' => $group_id,
511 'settings_id' => $key,
512 'value' => $value,
513 'type' => 'ignore_activation',
514 'slug' => '',
515 'permission' => '',
516 ]
517 );
518
519 if ( is_wp_error( $result ) ) {
520 return MCPHelper::error(
521 'update_failed',
522 $result->get_error_message(),
523 [
524 'failed_key' => $key,
525 'changed_before_failure' => $changed,
526 ]
527 );
528 }
529
530 $changed[] = $key;
531 }
532
533 self::invalidateCache();
534
535 $fresh = self::getSettingsGroup( [ 'group_id' => $group_id ] );
536
537 return [
538 'success' => true,
539 'group_id' => $group_id,
540 'changed' => $changed,
541 'settings' => is_wp_error( $fresh ) ? null : $fresh['settings'],
542 ];
543 }
544
545 /**
546 * Re-check of SettingsController::is_allowed_settings_group(): the same
547 * explicit allow-list, plus the same `_wpfunnels_` prefix fallback so
548 * add-on-registered groups keep working. See ALLOWED_SETTINGS_GROUPS for
549 * why this is a duplicate rather than a call into the real method.
550 *
551 * @param string $group_id Candidate option key.
552 * @return bool
553 */
554 private static function isAllowedSettingsGroup( $group_id ) {
555 if ( ! is_string( $group_id ) || '' === $group_id ) {
556 return false;
557 }
558
559 if ( in_array( $group_id, self::ALLOWED_SETTINGS_GROUPS, true ) ) {
560 return true;
561 }
562
563 return 0 === strpos( $group_id, '_wpfunnels_' );
564 }
565
566 /**
567 * Funnel counts by type and status.
568 *
569 * @return array
570 */
571 private static function funnelCounts() {
572 $counts = [
573 'total' => 0,
574 'published' => 0,
575 'draft' => 0,
576 'by_type' => [],
577 'store_checkout' => 0,
578 ];
579
580 $funnels = get_posts(
581 [
582 'post_type' => WPFNL_FUNNELS_POST_TYPE,
583 'post_status' => [ 'publish', 'draft' ],
584 'numberposts' => -1,
585 'fields' => 'ids',
586 ]
587 );
588
589 foreach ( $funnels as $funnel_id ) {
590 $counts['total']++;
591
592 if ( 'publish' === get_post_status( $funnel_id ) ) {
593 $counts['published']++;
594 } else {
595 $counts['draft']++;
596 }
597
598 $type = get_post_meta( $funnel_id, '_wpfnl_funnel_type', true ) ?: 'wc';
599 if ( 'store_checkout' === $type ) {
600 $counts['store_checkout']++;
601 }
602
603 $counts['by_type'][ $type ] = isset( $counts['by_type'][ $type ] ) ? $counts['by_type'][ $type ] + 1 : 1;
604 }
605
606 return $counts;
607 }
608
609 /**
610 * Build guidelines, trimmed to what this install supports.
611 *
612 * @param bool $wc WooCommerce active.
613 * @param bool $pro Pro active.
614 * @return array
615 */
616 private static function guidelines( $wc, $pro ) {
617 $lines = [
618 'Resolve names to IDs before writing: use list-funnels, list-steps and search-products. Never invent an ID.',
619 'Build order: create-funnel, then create-step per step, then assign-products-to-step on checkout/upsell/downsell steps.',
620 'Funnel types are wc (WooCommerce sales), lms, lead (opt-in) and store_checkout. A funnel keeps one type.',
621 'A funnel has at most one landing, one checkout and one thank-you step. Canonical order is landing, checkout, upsell, downsell, thank-you.',
622 'Products can only be attached to checkout, upsell and downsell steps.',
623 ];
624
625 if ( ! $wc ) {
626 $lines[] = 'WooCommerce is inactive here: build lead funnels with landing, opt-in and thank-you steps only.';
627 }
628 if ( ! $pro ) {
629 $lines[] = 'Pro is inactive here: one-click upsell/downsell, A/B testing and analytics tools are unavailable. Say so plainly rather than pretending.';
630 }
631
632 return $lines;
633 }
634 }
635