Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PreviewsService
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 5
156
0.00% covered (danger)
0.00%
0 / 1
 generate_snapshot
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 validate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 publish_page
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 capture_screenshot
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 trash_preview_pages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace NewfoldLabs\WP\Module\Onboarding\Services;
4
5use NewfoldLabs\WP\Module\Onboarding\Data\Options;
6
7/**
8 * Class for generating snapshots.
9 */
10class PreviewsService {
11    /**
12     * The API that processes block rendering.
13     *
14     * @var string
15     */
16    protected static $screenshot_service_api = 'https://hiive.cloud/workers/screenshot-service/';
17
18    /**
19     * Generate the snapshot.
20     * 
21     * @param string $content The content to render.
22     * @param string $slug The slug of the page.
23     * @param ?string $custom_styles The custom styles to apply to the page.
24     * @return array|\WP_Error
25     */
26    public static function generate_snapshot( string $content, string $slug, ?string $custom_styles = null ): array | \WP_Error {
27        if ( ! self::validate( $content ) ) {
28            return new \WP_Error( 'invalid_pattern_content', 'Invalid pattern content.' );
29        }
30
31        // Initial response array.
32        $response = [
33            'screenshot' => null,
34            'post_url'   => null,
35            'post_id'    => null,
36        ];
37        
38        // Publish the page (to be crawled by the screenshot service or used as a fallback iframe).
39        $post                 = self::publish_page( $content, $slug, $custom_styles );
40        $response['post_url'] = $post['post_url'];
41        $response['post_id']  = $post['post_id'];
42        
43        // Generate the screenshot.
44        /**
45         * We'll be using the iframe as the main snapshot source.
46         * So we'll skip the screenshot generation for now.
47         */
48        // $screenshot = self::capture_screenshot( url: $post['post_url'], key: $slug );
49        // if ( ! is_wp_error( $screenshot ) ) {
50        //     $response['screenshot'] = $screenshot;
51        // }
52
53        return $response;
54    }
55    
56    /**
57     * Validate the pattern content.
58     * 
59     * @param string $content The content to validate.
60     * @return bool
61     */
62    private static function validate( string $content ): bool {
63        $blocks_content = $content;
64
65        try {
66            // Parse and render the pattern.
67            $parsed = parse_blocks( $blocks_content );
68            if ( ! isset( $parsed[0] ) || empty($parsed[0] ) ) {
69                throw new \Exception( 'Invalid pattern content.' );
70            }
71            render_block( $parsed[0] );
72
73            return true;
74        } catch ( \Exception $e ) {
75            return false;
76        }
77    }
78
79    /**
80     * Publish the page.
81     * 
82     * @param string $content The content to render.
83     * @param string $slug The slug of the page.
84     * @return array
85     */
86    private static function publish_page( string $content, string $slug, $custom_styles = null ): array {
87        // Inject custom styles if provided
88        if ( $custom_styles ) {
89            $styles = '<style>';
90            // Disable cursor events.
91            $styles .= 'body * { cursor: default !important; }';
92            // Remove unintended content margin added by the style/script block.
93            $styles .= '.entry-content > :not(style):not(script):first-of-type {margin-top: 0 !important;}';
94            // Hide preview pages links from nav
95            $styles .= '.wp-block-pages-list__item:has(a[href*="home-version"]) { display: none !important; }';
96            $styles .= '.wp-block-pages-list__item:has(a[href*="home-homepage"]) { display: none !important; }';
97            // Add custom styles
98            $styles .= $custom_styles;
99            $styles .= '</style>';
100        }
101
102        // Inject iframe script
103        $iframe_script = '<script>
104            document.addEventListener("DOMContentLoaded", function() {
105                // Check if page is loaded in an iframe
106                if (typeof window !== "undefined" && window.self !== window.top) {
107                    // Hide the admin bar
108                    const nfdOnboardingPreviewAdminBar = document.getElementById("wpadminbar");
109                    if (nfdOnboardingPreviewAdminBar) {
110                        nfdOnboardingPreviewAdminBar.style.display = "none";
111                        // Remove the admin bar reserved space
112                        document.documentElement.style.setProperty("margin-top", "0px", "important");
113                    }
114
115                    // Prevent click events
116                    document.addEventListener("click", function(e) {
117                        e.preventDefault();
118                        e.stopPropagation();
119                        return false;
120                    }, true);
121
122                    // Prevent context menu (right click)
123                    document.addEventListener("contextmenu", function(e) {
124                        e.preventDefault();
125                        return false;
126                    });
127                }
128            });
129        </script>';
130
131        // Remove hooks that can slow down the operation.
132        remove_all_actions('wp_insert_post');
133        remove_all_actions('save_post');
134        // Insert the page.
135        $post_id = wp_insert_post( array(
136            'post_title'    => 'Home-' . $slug,
137            'post_name'     => 'home-' . $slug,
138            'post_content'  => $styles . $iframe_script . $content,
139            'post_status'   => 'publish',
140            'post_type'     => 'page',
141            'page_template' => 'blank',
142        ) );
143        $post_url = get_permalink( $post_id );
144
145        // Add the preview page id to 'sitegen_previews' option to be cleaned up later
146        $sitegen_previews = get_option( Options::get_option_name( 'sitegen_previews' ), array() );
147        $sitegen_previews[] = $post_id;
148        update_option( Options::get_option_name( 'sitegen_previews' ), $sitegen_previews );
149
150        return [
151            'post_url' => $post_url,
152            'post_id' => $post_id,
153        ];
154    }
155
156    /**
157     * Generate the screenshot.
158     * 
159     * @link https://github.com/newfold-labs/cf-worker-screenshot-service — Documentation.
160     * @param string $url The URL of the page.
161     * @param string $key cache key.
162     * @return string|WP_Error
163     */
164    public static function capture_screenshot( string $url, string $key ): string | \WP_Error {
165        $body = array(
166            'url'   => $url,
167            'key'  => $key,
168            'quality' => 50,
169        );
170        $args = array(
171            'body'    => wp_json_encode( $body ),
172            'headers' => array(
173                'Content-Type' => 'application/json',
174            ),
175            'timeout' => 30,
176        );
177
178        $response = wp_remote_post( self::$screenshot_service_api, $args );
179        $status   = wp_remote_retrieve_response_code( $response );
180        if ( 200 !== $status ) {
181            return new \WP_Error(
182                'nfd_onboarding_error',
183                __( 'Unable to generate screenshot.', 'wp-module-onboarding-data' ),
184                array(
185                    'status' => '500',
186                )
187            );
188        }
189
190        // Get the image data and convert it to base64.
191        $img_binary = wp_remote_retrieve_body( $response );
192        $img_base64 = base64_encode( $img_binary );
193        $img        = 'data:image/webp;base64,' . $img_base64;
194        return $img;
195    }
196
197    /**
198     * Trash the preview pages.
199     * 
200     * @return void
201     */
202    public static function trash_preview_pages(): void {
203        $sitegen_previews = get_option( Options::get_option_name( 'sitegen_previews' ), array() );
204        foreach ( $sitegen_previews as $post_id ) {
205            wp_delete_post( $post_id, true );
206        }
207        delete_option( Options::get_option_name( 'sitegen_previews' ) );
208    }
209}