PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.2
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.2
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 / Helpers / MCPHelper.php

MCPHelper.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.2, at includes/core/MCP/Helpers/MCPHelper.php

381 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCPHelper — shared plumbing for WPFunnels abilities.
4 *
5 * Pagination, error construction, permission callbacks and the output
6 * formatters every tool implementation reuses.
7 *
8 * @package WPFunnels\MCP
9 * @since 3.13.0
10 */
11
12 namespace WPFunnels\MCP\Helpers;
13
14 defined( 'ABSPATH' ) || exit;
15
16 use WPFunnels\Wpfnl_functions;
17
18 /**
19 * Class MCPHelper
20 */
21 class MCPHelper {
22
23 /**
24 * Default page size for list tools.
25 */
26 public const DEFAULT_PER_PAGE = 20;
27
28 /**
29 * Hard ceiling on page size — protects the model's context window.
30 */
31 public const MAX_PER_PAGE = 100;
32
33 /**
34 * Build a structured error.
35 *
36 * @param string $code Machine-readable code.
37 * @param string $message Human-readable message.
38 * @param array $data Extra context.
39 * @return \WP_Error
40 */
41 public static function error( $code, $message, $data = [] ) {
42 return new \WP_Error( 'wpfunnels_' . $code, $message, $data );
43 }
44
45 /**
46 * Permission callback for a capability.
47 *
48 * Every WPFunnels capability today funnels through wpf_manage_funnels;
49 * administrators are allowed via manage_options so a site with a customized
50 * role map never locks the owner out of their own tools.
51 *
52 * @param string $capability Capability to require.
53 * @return callable
54 */
55 public static function currentUserCan( $capability = 'wpf_manage_funnels' ) {
56 return static function () use ( $capability ) {
57 return current_user_can( $capability ) || current_user_can( 'manage_options' );
58 };
59 }
60
61 /**
62 * Clamp a per_page value into the allowed range.
63 *
64 * @param mixed $per_page Requested page size.
65 * @return int
66 */
67 public static function perPage( $per_page ) {
68 $per_page = (int) $per_page;
69 if ( $per_page < 1 ) {
70 $per_page = self::DEFAULT_PER_PAGE;
71 }
72 return min( $per_page, self::MAX_PER_PAGE );
73 }
74
75 /**
76 * Standard pagination envelope.
77 *
78 * @param array $items Page of items.
79 * @param int $total Total matching items.
80 * @param int $page Current page (1-based).
81 * @param int $per_page Page size.
82 * @return array
83 */
84 public static function paginate( $items, $total, $page, $per_page ) {
85 $per_page = self::perPage( $per_page );
86 $page = max( 1, (int) $page );
87
88 return [
89 'items' => $items,
90 'pagination' => [
91 'total' => (int) $total,
92 'page' => $page,
93 'per_page' => $per_page,
94 'total_pages' => $per_page > 0 ? (int) ceil( $total / $per_page ) : 0,
95 'has_more' => ( $page * $per_page ) < (int) $total,
96 ],
97 ];
98 }
99
100 /**
101 * Shared pagination schema fragment.
102 *
103 * @return array
104 */
105 public static function paginationSchema() {
106 return [
107 'page' => [
108 'type' => 'integer',
109 'description' => 'Page number, 1-based.',
110 'minimum' => 1,
111 'default' => 1,
112 ],
113 'per_page' => [
114 'type' => 'integer',
115 'description' => 'Items per page (max ' . self::MAX_PER_PAGE . ').',
116 'minimum' => 1,
117 'maximum' => self::MAX_PER_PAGE,
118 'default' => self::DEFAULT_PER_PAGE,
119 ],
120 ];
121 }
122
123 /**
124 * Step types this install supports, as slug => label.
125 *
126 * Offer steps depend on WooCommerce; lead funnels use opt-in steps. Reported
127 * from the runtime so the model never emits a type this site cannot build.
128 *
129 * @return array
130 */
131 public static function supportedStepTypes() {
132 $types = [
133 'landing' => 'Landing page — first page visitors see',
134 'checkout' => 'Checkout page — collects payment',
135 'thankyou' => 'Thank-you page — confirmation and next steps',
136 'custom' => 'Custom page — any additional content page',
137 ];
138
139 if ( Wpfnl_functions::is_wc_active() ) {
140 $types['upsell'] = 'Upsell page — one-click offer after checkout';
141 $types['downsell'] = 'Downsell page — alternative when the upsell is declined';
142 }
143
144 $types['optin'] = 'Opt-in page — captures leads without payment';
145
146 /**
147 * Filter the step types exposed to AI tools.
148 *
149 * @since 3.13.0
150 * @param array $types Slug => description.
151 */
152 return apply_filters( 'wpfunnels/mcp_step_types', $types );
153 }
154
155 /**
156 * Site's Elementor design tokens, for widgets that don't inherit
157 * Elementor's Global Colors automatically.
158 *
159 * Only the kit's `primary` system color is reported — deliberately not
160 * the `text` color or typography: the kit's body-text color is tuned for
161 * light backgrounds and would likely be illegible as button text, and
162 * most kits carry no custom font override worth copying.
163 *
164 * @return array
165 */
166 public static function getSiteDesignTokens() {
167 if ( ! Wpfnl_functions::is_elementor_active() || ! class_exists( '\Elementor\Plugin' ) ) {
168 return [
169 'elementor_active' => false,
170 'primary_color' => null,
171 ];
172 }
173
174 $kit_id = \Elementor\Plugin::$instance->kits_manager->get_active_id();
175 $settings = $kit_id ? get_post_meta( $kit_id, '_elementor_page_settings', true ) : [];
176 $colors = isset( $settings['system_colors'] ) && is_array( $settings['system_colors'] ) ? $settings['system_colors'] : [];
177
178 $primary = null;
179 foreach ( $colors as $color ) {
180 if ( isset( $color['_id'], $color['color'] ) && 'primary' === $color['_id'] ) {
181 $primary = $color['color'];
182 break;
183 }
184 }
185
186 return [
187 'elementor_active' => true,
188 'primary_color' => $primary,
189 ];
190 }
191
192 /**
193 * Detect which page builder authored a page's content.
194 *
195 * Elementor and Bricks stamp their own meta keys on any page they've
196 * edited, checked first since either can coexist with block markup left
197 * over in `post_content`. Falls back to `has_blocks()` for Gutenberg,
198 * then to non-empty `post_content` for a builder this site doesn't
199 * specifically detect ('other'), or 'unknown' for an empty/missing page.
200 *
201 * @param int $page_id Post id.
202 * @return string One of gutenberg|elementor|bricks|other|unknown.
203 */
204 public static function detectPageBuilder( $page_id ) {
205 $page_id = (int) $page_id;
206 if ( ! $page_id ) {
207 return 'unknown';
208 }
209
210 if ( defined( 'ELEMENTOR_VERSION' ) && 'builder' === get_post_meta( $page_id, '_elementor_edit_mode', true ) ) {
211 return 'elementor';
212 }
213
214 // `_bricks_page_content_2` is Bricks' own storage key regardless of
215 // whether it's active as a theme or plugin.
216 if ( '' !== get_post_meta( $page_id, '_bricks_page_content_2', true ) ) {
217 return 'bricks';
218 }
219
220 $post = get_post( $page_id );
221 if ( ! $post ) {
222 return 'unknown';
223 }
224
225 if ( function_exists( 'has_blocks' ) && has_blocks( $post ) ) {
226 return 'gutenberg';
227 }
228
229 return '' !== trim( (string) $post->post_content ) ? 'other' : 'unknown';
230 }
231
232 /**
233 * Whether a step type can hold products.
234 *
235 * @param string $step_type Step type slug.
236 * @return bool
237 */
238 public static function stepTypeHoldsProducts( $step_type ) {
239 return in_array( $step_type, [ 'checkout', 'upsell', 'downsell' ], true );
240 }
241
242 /**
243 * Product meta key for a step type.
244 *
245 * @param string $step_type Step type slug.
246 * @return string
247 */
248 public static function productMetaKey( $step_type ) {
249 switch ( $step_type ) {
250 case 'upsell':
251 return '_wpfnl_upsell_products';
252 case 'downsell':
253 return '_wpfnl_downsell_products';
254 default:
255 return '_wpfnl_checkout_products';
256 }
257 }
258
259 /**
260 * Load a funnel post, or an error when the id is not a funnel.
261 *
262 * @param int $funnel_id Funnel post id.
263 * @return \WP_Post|\WP_Error
264 */
265 public static function requireFunnel( $funnel_id ) {
266 $funnel_id = (int) $funnel_id;
267 $post = $funnel_id ? get_post( $funnel_id ) : null;
268
269 if ( ! $post || WPFNL_FUNNELS_POST_TYPE !== $post->post_type ) {
270 return self::error(
271 'funnel_not_found',
272 sprintf( 'No funnel found with ID %d. Use wpfunnels/list-funnels to find valid IDs.', $funnel_id ),
273 [ 'funnel_id' => $funnel_id ]
274 );
275 }
276
277 return $post;
278 }
279
280 /**
281 * Load a step post, or an error when the id is not a step.
282 *
283 * @param int $step_id Step post id.
284 * @return \WP_Post|\WP_Error
285 */
286 public static function requireStep( $step_id ) {
287 $step_id = (int) $step_id;
288 $post = $step_id ? get_post( $step_id ) : null;
289
290 if ( ! $post || WPFNL_STEPS_POST_TYPE !== $post->post_type ) {
291 return self::error(
292 'step_not_found',
293 sprintf( 'No funnel step found with ID %d. Use wpfunnels/list-steps to find valid IDs.', $step_id ),
294 [ 'step_id' => $step_id ]
295 );
296 }
297
298 return $post;
299 }
300
301 /**
302 * Format a funnel for tool output. Never includes builder payloads.
303 *
304 * @param \WP_Post $funnel Funnel post.
305 * @param bool $verbose Include step summaries and settings.
306 * @return array
307 */
308 public static function formatFunnel( $funnel, $verbose = false ) {
309 $funnel_id = (int) $funnel->ID;
310 $steps = Wpfnl_functions::get_steps( $funnel_id );
311 $steps = is_array( $steps ) ? $steps : [];
312
313 $data = [
314 'id' => $funnel_id,
315 'name' => $funnel->post_title,
316 'status' => $funnel->post_status,
317 'type' => get_post_meta( $funnel_id, '_wpfnl_funnel_type', true ) ?: 'wc',
318 'step_count' => count( $steps ),
319 'step_types' => array_values(
320 array_filter(
321 array_map(
322 static function ( $step ) {
323 return isset( $step['step_type'] ) ? $step['step_type'] : null;
324 },
325 $steps
326 )
327 )
328 ),
329 'created_at' => $funnel->post_date,
330 'modified_at' => $funnel->post_modified,
331 'canvas_url' => self::canvasUrl( $funnel_id ),
332 ];
333
334 if ( $verbose ) {
335 $data['steps'] = array_map(
336 static function ( $step ) {
337 return MCPHelper::formatStepSummary( $step );
338 },
339 $steps
340 );
341 $data['ai_generated'] = (bool) get_post_meta( $funnel_id, '_wpfnl_ai_generated', true );
342 }
343
344 return $data;
345 }
346
347 /**
348 * Format one entry of the `_steps_order` array.
349 *
350 * @param array $step Step order entry.
351 * @return array
352 */
353 public static function formatStepSummary( $step ) {
354 $step_id = isset( $step['id'] ) ? (int) $step['id'] : 0;
355
356 return [
357 'id' => $step_id,
358 'name' => isset( $step['name'] ) ? $step['name'] : get_the_title( $step_id ),
359 'step_type' => isset( $step['step_type'] ) ? $step['step_type'] : '',
360 'view_url' => $step_id ? get_permalink( $step_id ) : '',
361 'edit_url' => $step_id ? get_edit_post_link( $step_id, 'raw' ) : '',
362 ];
363 }
364
365 /**
366 * Funnel canvas (builder) URL.
367 *
368 * @param int $funnel_id Funnel id.
369 * @return string
370 */
371 public static function canvasUrl( $funnel_id ) {
372 return add_query_arg(
373 [
374 'page' => 'edit_funnel',
375 'id' => (int) $funnel_id,
376 ],
377 admin_url( 'admin.php' )
378 );
379 }
380 }
381