PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.0
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.0
3.13.3 3.13.2 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 All 261 releases
wpfunnels / includes / core / MCP / Tools / ContextTools.php

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

636 lines 23.6 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, default funnel type, 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_type, 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 'default_funnel_type' => isset( $general['funnel_type'] ) ? $general['funnel_type'] : '',
242 'permalink_base' => isset( $general['permalink'] ) ? $general['permalink'] : '',
243 'funnel_builder_mode' => isset( $general['funnel_builder_mode'] ) ? $general['funnel_builder_mode'] : 'horizontal',
244 ],
245 'guidelines' => self::guidelines( $wc, $pro ),
246 ];
247
248 set_transient( self::CACHE_KEY, $context, self::CACHE_TTL );
249
250 return $context;
251 }
252
253 /**
254 * The site's Elementor design tokens.
255 *
256 * @return array
257 */
258 public static function getSiteDesignTokens() {
259 return MCPHelper::getSiteDesignTokens();
260 }
261
262 /**
263 * Setup audit with actionable links.
264 *
265 * @return array
266 */
267 public static function checkSetupStatus() {
268 $issues = [];
269 $general = Wpfnl_functions::get_general_settings();
270 $wc = Wpfnl_functions::is_wc_active();
271
272 if ( ! $wc && ! Wpfnl_functions::is_lms_addon_active() ) {
273 $issues[] = [
274 'severity' => 'warning',
275 'issue' => 'WooCommerce is not active, so checkout, order bump and offer steps are unavailable.',
276 'fix' => 'Install and activate WooCommerce, or build lead-generation funnels instead.',
277 'url' => admin_url( 'plugin-install.php?s=woocommerce&tab=search&type=term' ),
278 ];
279 }
280
281 if ( ! get_option( 'permalink_structure' ) ) {
282 $issues[] = [
283 'severity' => 'error',
284 'issue' => 'Plain permalinks are enabled; funnel step URLs will not resolve.',
285 'fix' => 'Switch to any pretty permalink structure.',
286 'url' => admin_url( 'options-permalink.php' ),
287 ];
288 }
289
290 if ( empty( $general['builder'] ) ) {
291 $issues[] = [
292 'severity' => 'warning',
293 'issue' => 'No page builder is selected, so new steps fall back to the block editor.',
294 'fix' => 'Pick a page builder in WPFunnels settings.',
295 'url' => admin_url( 'admin.php?page=' . ( defined( 'WPFNL_GLOBAL_SETTINGS_SLUG' ) ? WPFNL_GLOBAL_SETTINGS_SLUG : 'wpfnl_settings' ) ),
296 ];
297 }
298
299 if ( ! Wpfnl_functions::is_wpfnl_pro_activated() ) {
300 $issues[] = [
301 'severity' => 'info',
302 'issue' => 'WPFunnels Pro is not active: A/B testing, analytics, one-click upsells and CRM integrations are unavailable.',
303 'fix' => 'Activate WPFunnels Pro to unlock those tools.',
304 'url' => admin_url( 'admin.php?page=wpf-license' ),
305 ];
306 }
307
308 $counts = self::funnelCounts();
309 if ( 0 === $counts['total'] ) {
310 $issues[] = [
311 'severity' => 'info',
312 'issue' => 'No funnels exist yet.',
313 'fix' => 'Create one with wpfunnels/create-funnel.',
314 'url' => admin_url( 'admin.php?page=' . ( defined( 'WPFNL_FUNNEL_PAGE_SLUG' ) ? WPFNL_FUNNEL_PAGE_SLUG : 'wp_funnels' ) ),
315 ];
316 }
317
318 return [
319 'ready' => empty(
320 array_filter(
321 $issues,
322 static function ( $issue ) {
323 return 'error' === $issue['severity'];
324 }
325 )
326 ),
327 'issues' => $issues,
328 ];
329 }
330
331 /**
332 * Merge changes into WPFunnels' general settings option.
333 *
334 * Only keys already present in `get_general_settings()`'s defaults are
335 * writable — this is the same option the Settings → General screen reads
336 * and writes, so an unrecognized key here would just be dead weight in
337 * the option, never surfaced anywhere.
338 *
339 * @param array $input Tool input.
340 * @return array|\WP_Error
341 */
342 public static function updateGlobalSettings( $input = [] ) {
343 $changes = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
344 if ( empty( $changes ) ) {
345 return MCPHelper::error( 'missing_settings', 'Provide at least one setting to update.' );
346 }
347
348 $current = Wpfnl_functions::get_general_settings();
349 $allowed = array_intersect_key( $changes, $current );
350
351 if ( empty( $allowed ) ) {
352 return MCPHelper::error(
353 'no_recognized_keys',
354 'None of the given keys are recognized general settings.',
355 [ 'recognized_keys' => array_keys( $current ) ]
356 );
357 }
358
359 $updated = array_merge( $current, $allowed );
360 Wpfnl_functions::update_admin_settings( '_wpfunnels_general_settings', $updated );
361
362 self::invalidateCache();
363
364 return [
365 'success' => true,
366 'changed' => array_keys( $allowed ),
367 'settings' => $updated,
368 ];
369 }
370
371 /**
372 * Merge changes into WPFunnels' funnel/step permalink settings.
373 *
374 * @param array $input Tool input.
375 * @return array|\WP_Error
376 */
377 public static function updatePermalinkSettings( $input = [] ) {
378 $changes = array_intersect_key( $input, array_flip( [ 'structure', 'funnel_base', 'step_base' ] ) );
379 if ( empty( $changes ) ) {
380 return MCPHelper::error( 'missing_settings', 'Provide at least one of structure, funnel_base, step_base.' );
381 }
382
383 $current = Wpfnl_functions::get_permalink_settings();
384 $updated = array_merge( $current, array_map( 'sanitize_text_field', $changes ) );
385
386 Wpfnl_functions::update_admin_settings( '_wpfunnels_permalink_settings', $updated );
387 Wpfnl_functions::update_admin_settings( '_wpfunnels_permalink_saved', true );
388 flush_rewrite_rules();
389
390 self::invalidateCache();
391
392 return [
393 'success' => true,
394 'changed' => array_keys( $changes ),
395 'settings' => $updated,
396 ];
397 }
398
399 /**
400 * Read one allow-listed WPFunnels settings group.
401 *
402 * Reuses `SettingsController::get_group_settings()` for the actual read
403 * (including its own internal allow-list check) so this stays byte-for-byte
404 * consistent with what the Settings screens themselves read.
405 *
406 * @param array $input Tool input.
407 * @return array|\WP_Error
408 */
409 public static function getSettingsGroup( $input = [] ) {
410 $group_id = isset( $input['group_id'] ) ? sanitize_text_field( $input['group_id'] ) : '';
411
412 if ( ! self::isAllowedSettingsGroup( $group_id ) ) {
413 return MCPHelper::error(
414 'invalid_settings_group',
415 'group_id is not a recognized WPFunnels settings group.',
416 [ 'allowed_groups' => self::ALLOWED_SETTINGS_GROUPS ]
417 );
418 }
419
420 $controller = new SettingsController();
421 $result = $controller->get_group_settings( $group_id );
422
423 if ( is_wp_error( $result ) ) {
424 return MCPHelper::error( 'settings_group_error', $result->get_error_message() );
425 }
426
427 if ( empty( $result ) ) {
428 // get_group_settings() only flattens array-shaped options; a scalar
429 // option (currently only _wpfunnels_google_map_api_key) foreach()es
430 // to nothing there, so fall back to the raw stored value.
431 $raw = Wpfnl_functions::get_admin_settings( $group_id );
432 if ( ! empty( $raw ) && ! is_array( $raw ) ) {
433 return [
434 'group_id' => $group_id,
435 'settings' => [ 'value' => $raw ],
436 ];
437 }
438
439 return [
440 'group_id' => $group_id,
441 'settings' => [],
442 ];
443 }
444
445 $settings = [];
446 foreach ( $result as $entry ) {
447 $settings[ $entry['id'] ] = $entry['value'];
448 }
449
450 return [
451 'group_id' => $group_id,
452 'settings' => $settings,
453 ];
454 }
455
456 /**
457 * Merge changes into one allow-listed WPFunnels settings group.
458 *
459 * Reuses `SettingsController::update_settings()` for the actual write, one
460 * key at a time (that method is built around updating a single named
461 * setting per call). `type` is forced to `ignore_activation` so this never
462 * triggers the controller's plugin-activation side effect, which is meant
463 * for the setup wizard's own flow, not a generic settings write.
464 *
465 * @param array $input Tool input.
466 * @return array|\WP_Error
467 */
468 public static function updateSettingsGroup( $input = [] ) {
469 $group_id = isset( $input['group_id'] ) ? sanitize_text_field( $input['group_id'] ) : '';
470
471 if ( ! self::isAllowedSettingsGroup( $group_id ) ) {
472 return MCPHelper::error(
473 'invalid_settings_group',
474 'group_id is not a recognized WPFunnels settings group.',
475 [ 'allowed_groups' => self::ALLOWED_SETTINGS_GROUPS ]
476 );
477 }
478
479 $settings = isset( $input['settings'] ) ? $input['settings'] : null;
480 if ( ! is_array( $settings ) || empty( $settings ) ) {
481 return MCPHelper::error( 'missing_settings', 'Provide a settings object with at least one key to update.' );
482 }
483
484 // _wpfunnels_google_map_api_key stores a plain string, not a keyed
485 // array. update_settings() assumes every group is an associative array
486 // and would corrupt a string option via `$settings['x'] = ...` (illegal
487 // string offset), so this one group is written directly instead of
488 // being looped through that method.
489 if ( '_wpfunnels_google_map_api_key' === $group_id ) {
490 $value = isset( $settings['value'] ) ? $settings['value'] : reset( $settings );
491 Wpfnl_functions::update_admin_settings( $group_id, sanitize_text_field( $value ) );
492
493 self::invalidateCache();
494
495 return [
496 'success' => true,
497 'group_id' => $group_id,
498 'changed' => [ 'value' ],
499 'settings' => [ 'value' => Wpfnl_functions::get_admin_settings( $group_id, '' ) ],
500 ];
501 }
502
503 $controller = new SettingsController();
504 $changed = [];
505
506 foreach ( $settings as $key => $value ) {
507 $key = sanitize_text_field( $key );
508
509 $result = $controller->update_settings(
510 [
511 'group_id' => $group_id,
512 'settings_id' => $key,
513 'value' => $value,
514 'type' => 'ignore_activation',
515 'slug' => '',
516 'permission' => '',
517 ]
518 );
519
520 if ( is_wp_error( $result ) ) {
521 return MCPHelper::error(
522 'update_failed',
523 $result->get_error_message(),
524 [
525 'failed_key' => $key,
526 'changed_before_failure' => $changed,
527 ]
528 );
529 }
530
531 $changed[] = $key;
532 }
533
534 self::invalidateCache();
535
536 $fresh = self::getSettingsGroup( [ 'group_id' => $group_id ] );
537
538 return [
539 'success' => true,
540 'group_id' => $group_id,
541 'changed' => $changed,
542 'settings' => is_wp_error( $fresh ) ? null : $fresh['settings'],
543 ];
544 }
545
546 /**
547 * Re-check of SettingsController::is_allowed_settings_group(): the same
548 * explicit allow-list, plus the same `_wpfunnels_` prefix fallback so
549 * add-on-registered groups keep working. See ALLOWED_SETTINGS_GROUPS for
550 * why this is a duplicate rather than a call into the real method.
551 *
552 * @param string $group_id Candidate option key.
553 * @return bool
554 */
555 private static function isAllowedSettingsGroup( $group_id ) {
556 if ( ! is_string( $group_id ) || '' === $group_id ) {
557 return false;
558 }
559
560 if ( in_array( $group_id, self::ALLOWED_SETTINGS_GROUPS, true ) ) {
561 return true;
562 }
563
564 return 0 === strpos( $group_id, '_wpfunnels_' );
565 }
566
567 /**
568 * Funnel counts by type and status.
569 *
570 * @return array
571 */
572 private static function funnelCounts() {
573 $counts = [
574 'total' => 0,
575 'published' => 0,
576 'draft' => 0,
577 'by_type' => [],
578 'store_checkout' => 0,
579 ];
580
581 $funnels = get_posts(
582 [
583 'post_type' => WPFNL_FUNNELS_POST_TYPE,
584 'post_status' => [ 'publish', 'draft' ],
585 'numberposts' => -1,
586 'fields' => 'ids',
587 ]
588 );
589
590 foreach ( $funnels as $funnel_id ) {
591 $counts['total']++;
592
593 if ( 'publish' === get_post_status( $funnel_id ) ) {
594 $counts['published']++;
595 } else {
596 $counts['draft']++;
597 }
598
599 $type = get_post_meta( $funnel_id, '_wpfnl_funnel_type', true ) ?: 'wc';
600 if ( 'store_checkout' === $type ) {
601 $counts['store_checkout']++;
602 }
603
604 $counts['by_type'][ $type ] = isset( $counts['by_type'][ $type ] ) ? $counts['by_type'][ $type ] + 1 : 1;
605 }
606
607 return $counts;
608 }
609
610 /**
611 * Build guidelines, trimmed to what this install supports.
612 *
613 * @param bool $wc WooCommerce active.
614 * @param bool $pro Pro active.
615 * @return array
616 */
617 private static function guidelines( $wc, $pro ) {
618 $lines = [
619 'Resolve names to IDs before writing: use list-funnels, list-steps and search-products. Never invent an ID.',
620 'Build order: create-funnel, then create-step per step, then assign-products-to-step on checkout/upsell/downsell steps.',
621 'Funnel types are wc (WooCommerce sales), lms, lead (opt-in) and store_checkout. A funnel keeps one type.',
622 'A funnel has at most one landing, one checkout and one thank-you step. Canonical order is landing, checkout, upsell, downsell, thank-you.',
623 'Products can only be attached to checkout, upsell and downsell steps.',
624 ];
625
626 if ( ! $wc ) {
627 $lines[] = 'WooCommerce is inactive here: build lead funnels with landing, opt-in and thank-you steps only.';
628 }
629 if ( ! $pro ) {
630 $lines[] = 'Pro is inactive here: one-click upsell/downsell, A/B testing and analytics tools are unavailable. Say so plainly rather than pretending.';
631 }
632
633 return $lines;
634 }
635 }
636