Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 242
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
DesignController
0.00% covered (danger)
0.00%
0 / 242
0.00% covered (danger)
0.00%
0 / 11
2652
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 register_routes
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
56
 get_color_palettes
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 get_font_pairs
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 get_color_palettes_from_options
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
20
 get_font_pairs_from_options
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 get_color_palettes_from_hiive
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 get_font_pairs_from_hiive
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 fetch_from_hiive
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 get_color_palettes_from_theme_json
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
110
 paginate_response
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Design Controller for handling color palettes and font pairings.
4 *
5 * @package NewfoldLabs\WP\Module\Onboarding\RestApi
6 */
7
8namespace NewfoldLabs\WP\Module\Onboarding\RestApi;
9
10use NewfoldLabs\WP\Module\Onboarding\Permissions;
11use WP_REST_Controller;
12use WP_REST_Server;
13use WP_REST_Response;
14use WP_Error;
15
16/**
17 * Class DesignController
18 */
19class DesignController extends WP_REST_Controller {
20
21    /**
22     * The namespace of this controller's route.
23     *
24     * @var string
25     */
26    protected $namespace = 'newfold-onboarding/v1';
27
28    /**
29     * The base of this controller's route.
30     *
31     * @var string
32     */
33    protected $rest_base = 'design';
34
35    /**
36     * Hiive API base endpoint
37     *
38     * @var string
39     */
40    protected $hiive_api_base;
41
42    /**
43     * Constructor.
44     */
45    public function __construct() {
46        $this->hiive_api_base = defined( 'NFD_DATA_WB_DEV_MODE' ) && NFD_DATA_WB_DEV_MODE
47            ? 'http://patterns-platform.test/api/v1'
48            : 'https://paterns.hiive.cloud/api/v1';
49    }
50
51    /**
52     * Register the routes for this controller
53     */
54    public function register_routes() {
55        // Add route for paginated color palettes
56        register_rest_route(
57            $this->namespace,
58            '/' . $this->rest_base . '/color-palettes',
59            array(
60                array(
61                    'methods'             => WP_REST_Server::READABLE,
62                    'callback'            => array( $this, 'get_color_palettes' ),
63                    'permission_callback' => array( Permissions::class, 'rest_is_authorized_admin' ),
64                    'args'                => array(
65                        'page'     => array(
66                            'default'           => 1,
67                            'sanitize_callback' => 'absint',
68                            'validate_callback' => function ( $param ) {
69                                return is_numeric( $param ) && $param > 0;
70                            },
71                        ),
72                        'per_page' => array(
73                            'default'           => 10,
74                            'sanitize_callback' => 'absint',
75                            'validate_callback' => function ( $param ) {
76                                return is_numeric( $param ) && $param > 0 && $param <= 100;
77                            },
78                        ),
79                    ),
80                ),
81            )
82        );
83
84        // Add route for paginated font pairs
85        register_rest_route(
86            $this->namespace,
87            '/' . $this->rest_base . '/font-pairs',
88            array(
89                array(
90                    'methods'             => WP_REST_Server::READABLE,
91                    'callback'            => array( $this, 'get_font_pairs' ),
92                    'permission_callback' => array( Permissions::class, 'rest_is_authorized_admin' ),
93                    'args'                => array(
94                        'page'     => array(
95                            'default'           => 1,
96                            'sanitize_callback' => 'absint',
97                            'validate_callback' => function ( $param ) {
98                                return is_numeric( $param ) && $param > 0;
99                            },
100                        ),
101                        'per_page' => array(
102                            'default'           => 10,
103                            'sanitize_callback' => 'absint',
104                            'validate_callback' => function ( $param ) {
105                                return is_numeric( $param ) && $param > 0 && $param <= 100;
106                            },
107                        ),
108                    ),
109                ),
110            )
111        );
112    }
113
114    /**
115     * Get paginated color palettes
116     *
117     * @param \WP_REST_Request $request Full data about the request.
118     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
119     */
120    public function get_color_palettes( $request ) {
121        $referer  = $request->get_header( 'Referer' );
122        $page     = $request->get_param( 'page' );
123        $per_page = $request->get_param( 'per_page' );
124
125        if ( ! $referer ) {
126            return new WP_Error(
127                'invalid_referer',
128                'Invalid referer provided.',
129                array( 'status' => 400 )
130            );
131        }
132
133        // Check if referer contains nfd-onboarding
134        if ( strpos( $referer, 'nfd-onboarding' ) !== false ) {
135            $result = $this->get_color_palettes_from_options( $page, $per_page );
136            // If no palettes found in options, fallback to theme.json palettes
137            if ( is_wp_error( $result ) && $result->get_error_code() === 'no_color_palettes' ) {
138                return $this->get_color_palettes_from_theme_json( $page, $per_page );
139            }
140            return $result;
141        }
142
143        // For all other referers (including nfd-plugin), fetch from Hiive
144        $result = $this->get_color_palettes_from_hiive( $page, $per_page );
145        // If hiive palettes are empty, fallback to theme.json palettes
146        if ( is_wp_error( $result ) || ( isset( $result->data['data'] ) && empty( $result->data['data'] ) ) ) {
147            return $this->get_color_palettes_from_theme_json( $page, $per_page );
148        }
149        return $result;
150    }
151
152    /**
153     * Get paginated font pairs
154     *
155     * @param \WP_REST_Request $request Full data about the request.
156     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
157     */
158    public function get_font_pairs( $request ) {
159        $referer  = $request->get_header( 'Referer' );
160        $page     = $request->get_param( 'page' );
161        $per_page = $request->get_param( 'per_page' );
162
163        if ( ! $referer ) {
164            return new WP_Error(
165                'invalid_referer',
166                'Invalid referer provided.',
167                array( 'status' => 400 )
168            );
169        }
170
171        // Check if referer contains nfd-onboarding
172        if ( strpos( $referer, 'nfd-onboarding' ) !== false ) {
173            return $this->get_font_pairs_from_options( $page, $per_page );
174        }
175
176        // For all other referers (including nfd-plugin), fetch from Hiive
177        return $this->get_font_pairs_from_hiive( $page, $per_page );
178    }
179
180    /**
181     * Get color palettes from WordPress options table with pagination
182     *
183     * @param int $page The current page number
184     * @param int $per_page Number of items per page
185     * @return WP_REST_Response|WP_Error
186     */
187    protected function get_color_palettes_from_options( $page, $per_page ) {
188        $color_palettes = get_option( 'nfd_module_onboarding_editor_colorpalette' );
189
190        if ( false === $color_palettes ) {
191            return new WP_Error(
192                'no_color_palettes',
193                'No color palettes found in options.',
194                array( 'status' => 404 )
195            );
196        }
197
198        $formatted_palettes = array();
199        foreach ( $color_palettes as $index => $palette ) {
200            $formatted_palette = array();
201
202            foreach ( $palette as $key => $value ) {
203                $formatted_palette[] = array(
204                    'name'  => $key,
205                    'slug'  => $key,
206                    'color' => $value,
207                );
208            }
209
210            $formatted_palettes[] = array(
211                // translators: %d is the index of the palette
212                'name'          => sprintf( __( 'Palette %d', 'wp-module-onboarding' ), $index + 1 ),
213                'displayColors' => array(
214                    array(
215                        'color' => $palette['base'],
216                        'name'  => 'Base',
217                        'slug'  => 'base',
218                    ),
219                    array(
220                        'color' => $palette['contrast'],
221                        'name'  => 'Contrast',
222                        'slug'  => 'contrast',
223                    ),
224                    array(
225                        'color' => $palette['accent_2'],
226                        'name'  => 'Primary',
227                        'slug'  => 'accent-2',
228                    ),
229                    array(
230                        'color' => $palette['accent_5'],
231                        'name'  => 'Secondary',
232                        'slug'  => 'accent-5',
233                    ),
234                ),
235                'palette'       => $formatted_palette,
236            );
237        }
238
239        return $this->paginate_response( $formatted_palettes, $page, $per_page );
240    }
241
242    /**
243     * Get font pairs from WordPress options table with pagination
244     *
245     * @param int $page The current page number
246     * @param int $per_page Number of items per page
247     * @return WP_REST_Response|WP_Error
248     */
249    protected function get_font_pairs_from_options( $page, $per_page ) {
250        $font_pairs = get_option( 'nfd_module_onboarding_editor_fontpair' );
251
252        if ( false === $font_pairs ) {
253            return new WP_Error(
254                'no_font_pairs',
255                'No font pairs found in options.',
256                array( 'status' => 404 )
257            );
258        }
259
260        return $this->paginate_response( $font_pairs, $page, $per_page );
261    }
262
263    /**
264     * Get color palettes from Hiive API with pagination
265     *
266     * @param int $page The current page number
267     * @param int $per_page Number of items per page
268     * @return WP_REST_Response|WP_Error
269     */
270    protected function get_color_palettes_from_hiive( $page, $per_page ) {
271        // Try to get cached data first
272        $cached_data = get_transient( 'nfd_hiive_color_palettes' );
273        if ( false !== $cached_data ) {
274            return $this->paginate_response( $cached_data, $page, $per_page );
275        }
276
277        $color_palettes = $this->fetch_from_hiive( '/colors' );
278        if ( is_wp_error( $color_palettes ) ) {
279            return $color_palettes;
280        }
281
282        // Filter out invalid palettes and add displayColors
283        $formatted_palettes = array_filter(
284            array_map(
285                function ( $palette ) {
286                    // Skip if required properties are missing
287                    if ( ! isset( $palette['name'] ) || ! isset( $palette['palette'] ) || count( $palette['palette'] ) < 4 ) {
288                        return null;
289                    }
290
291                    // Create standardized displayColors with specific slugs
292                    $palette['displayColors'] = array(
293                        $palette['palette'][0],
294                        $palette['palette'][1],
295                        $palette['palette'][2],
296                        $palette['palette'][3],
297                    );
298                    return $palette;
299                },
300                $color_palettes['data']
301            ),
302            function ( $palette ) {
303                return null !== $palette;
304            }
305        );
306
307        // Re-index array to ensure sequential keys
308        $formatted_palettes = array_values( $formatted_palettes );
309
310        // Cache the formatted palettes for 24 hours
311        set_transient( 'nfd_hiive_color_palettes', $formatted_palettes, DAY_IN_SECONDS );
312
313        return $this->paginate_response( $formatted_palettes, $page, $per_page );
314    }
315
316    /**
317     * Get font pairs from Hiive API with pagination
318     *
319     * @param int $page The current page number
320     * @param int $per_page Number of items per page
321     * @return WP_REST_Response|WP_Error
322     */
323    protected function get_font_pairs_from_hiive( $page, $per_page ) {
324        // Try to get cached data first
325        $cached_data = get_transient( 'nfd_hiive_font_pairs' );
326        if ( false !== $cached_data ) {
327            return $this->paginate_response( $cached_data, $page, $per_page );
328        }
329
330        $font_pairs = $this->fetch_from_hiive( '/fonts' );
331        if ( is_wp_error( $font_pairs ) ) {
332            return $font_pairs;
333        }
334
335        // Cache the font pairs for 24 hours
336        set_transient( 'nfd_hiive_font_pairs', $font_pairs['data'], DAY_IN_SECONDS );
337
338        return $this->paginate_response( $font_pairs['data'], $page, $per_page );
339    }
340
341    /**
342     * Helper function to fetch data from Hiive API
343     *
344     * @param string $endpoint The endpoint to fetch from
345     * @return array|WP_Error The decoded response data or WP_Error on failure
346     */
347    protected function fetch_from_hiive( $endpoint ) {
348        // Try to get cached endpoint data first
349        $cache_key   = 'nfd_hiive_' . sanitize_key( $endpoint );
350        $cached_data = get_transient( $cache_key );
351        if ( false !== $cached_data ) {
352            return $cached_data;
353        }
354
355        $response = wp_remote_get(
356            $this->hiive_api_base . $endpoint,
357            array(
358                'timeout' => 15,
359                'headers' => array(
360                    'Accept' => 'application/json',
361                ),
362            )
363        );
364
365        if ( is_wp_error( $response ) ) {
366            return new WP_Error(
367                'hiive_api_error',
368                'Error fetching data from Hiive: ' . $response->get_error_message(),
369                array( 'status' => 502 )
370            );
371        }
372
373        $body = wp_remote_retrieve_body( $response );
374        $data = json_decode( $body, true );
375
376        if ( json_last_error() !== JSON_ERROR_NONE ) {
377            return new WP_Error(
378                'json_parse_error',
379                'Error parsing Hiive API response',
380                array( 'status' => 502 )
381            );
382        }
383
384        // Cache the endpoint data for 24 hours
385        set_transient( $cache_key, $data, DAY_IN_SECONDS );
386
387        return $data;
388    }
389
390    /**
391     * Get color palettes from theme.json
392     *
393     * @param int $page The current page number
394     * @param int $per_page Number of items per page
395     * @return WP_REST_Response|WP_Error
396     */
397    protected function get_color_palettes_from_theme_json( $page, $per_page ) {
398        $theme_palettes = array();
399
400        // Method 1: Using WP_Theme_JSON_Resolver to get style variations (includes styles/colors folder)
401        if ( class_exists( 'WP_Theme_JSON_Resolver' ) ) {
402            // Get all style variations from styles/ folder (includes colors/ subfolder)
403            $style_variations = \WP_Theme_JSON_Resolver::get_style_variations();
404
405            // Process each style variation (from styles/colors folder)
406            foreach ( $style_variations as $variation ) {
407                $variation_data = $variation['settings'] ?? array();
408
409                // Check if this variation has color palette
410                if ( isset( $variation_data['color']['palette'] ) && is_array( $variation_data['color']['palette'] ) ) {
411                    $all_palette_sources = $variation['settings']['color']['palette'];
412
413                    foreach ( $all_palette_sources as $source_name => $colors ) {
414                        if ( is_array( $colors ) && ! empty( $colors ) ) {
415                            $formatted_colors = array();
416                            $display_colors   = array();
417
418                            foreach ( $colors as $index => $color ) {
419                                $formatted_colors[] = $color;
420
421                                if ( in_array( $color['slug'], array( 'base', 'contrast', 'accent-2', 'accent-5' ), true ) ) {
422                                    $display_colors[] = $color;
423                                }
424                            }
425
426                            $theme_palettes[] = array(
427                                'name'          => $variation['title'],
428                                'displayColors' => $display_colors,
429                                'palette'       => $formatted_colors,
430                            );
431                        }
432                    }
433                }
434            }
435        }
436
437        return $this->paginate_response( $theme_palettes, $page, $per_page );
438    }
439
440    /**
441     * Helper function to paginate response data
442     *
443     * @param array $items The array of items to paginate
444     * @param int   $page The current page number
445     * @param int   $per_page Number of items per page
446     * @return WP_REST_Response
447     */
448    protected function paginate_response( $items, $page, $per_page ) {
449        $total_items = count( $items );
450        $total_pages = ceil( $total_items / $per_page );
451        $offset      = ( $page - 1 ) * $per_page;
452
453        $paginated_items = array_slice( $items, $offset, $per_page );
454
455        $response = new WP_REST_Response(
456            array(
457                'data'       => $paginated_items,
458                'pagination' => array(
459                    'total_items'  => $total_items,
460                    'total_pages'  => $total_pages,
461                    'current_page' => $page,
462                    'per_page'     => $per_page,
463                ),
464            )
465        );
466
467        // Add pagination headers
468        $response->header( 'X-WP-Total', $total_items );
469        $response->header( 'X-WP-TotalPages', $total_pages );
470
471        return $response;
472    }
473}