Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.38% covered (warning)
78.38%
58 / 74
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Setup
78.38% covered (warning)
78.38%
58 / 74
20.00% covered (danger)
20.00%
1 / 5
42.35
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 generate_token
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 sync_attached_media
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 generate_token_on_save
70.37% covered (warning)
70.37%
19 / 27
0.00% covered (danger)
0.00%
0 / 1
24.52
 warm_social_image
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Setup class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize\Social_Image_Generator;
9
10use Automattic\Jetpack\Publicize\Jetpack_Social_Settings\Settings;
11use Automattic\Jetpack\Publicize\Publicize;
12
13/**
14 * Class for setting up Social Image Generator-related functionality.
15 */
16class Setup {
17    /**
18     * Initialise SIG-related functionality.
19     */
20    public function init() {
21        if ( ! ( new Settings() )->is_sig_available() ) {
22            return;
23        }
24
25        // Be wary of any code that you add to this file, since this function is called on plugin load.
26        // We're using the `wp_after_insert_post` hook because we need access to the updated post meta. By using the default priority
27        // of 10 we make sure that our code runs before Sync processes the post.
28        add_action( 'wp_after_insert_post', array( $this, 'generate_token_on_save' ), 10, 3 );
29        add_action( 'jetpack_social_sig_warm_image', array( $this, 'warm_social_image' ) );
30        add_action( 'rest_api_init', array( REST_Token_Controller::class, 'register' ) );
31
32        // Flagged to be removed after deprecation.
33        // @deprecated 0.38.3
34        add_action( 'rest_api_init', array( REST_Settings_Controller::class, 'register' ) );
35    }
36
37    /**
38     * Get a token from WPCOM to generate the social image for the post, and save it locally.
39     *
40     * @param Post_Settings $post_settings A Post_Settings object that can be used to save the generated token.
41     */
42    public function generate_token( $post_settings ) {
43        if ( ! $post_settings->is_enabled() ) {
44            return;
45        }
46
47        $token = fetch_token(
48            $post_settings->get_custom_text(),
49            $post_settings->get_image_url(),
50            $post_settings->get_template(),
51            $post_settings->get_font()
52        );
53
54        if ( is_wp_error( $token ) ) {
55            return;
56        }
57
58        $post_settings->update_setting( 'token', sanitize_text_field( $token ) );
59
60        $this->sync_attached_media( $post_settings->post_id );
61    }
62
63    /**
64     * Point the shared attachment at the post's current generated image.
65     *
66     * When the post shares the generated image as an attachment, attached_media holds a
67     * plain URL instead of being derived from the token at render time like the OG tag is.
68     * It therefore goes stale whenever the token changes — most visibly after a template
69     * edit, which shares the previous template's image. This runs alongside the
70     * authoritative token write so the two can't drift, including when the editor closes
71     * before its debounced preview ever produced a token.
72     *
73     * @param int $post_id Post whose attachment should follow the generated image.
74     */
75    private function sync_attached_media( $post_id ) {
76        $options = get_post_meta( $post_id, Publicize::POST_JETPACK_SOCIAL_OPTIONS, true );
77
78        if ( ! is_array( $options ) || ( $options['media_source'] ?? '' ) !== 'sig' ) {
79            return;
80        }
81
82        // A non-empty id means real library media, which must not be replaced.
83        if ( ! isset( $options['attached_media'][0] ) || ! empty( $options['attached_media'][0]['id'] ) ) {
84            return;
85        }
86
87        $url = get_image_url( $post_id );
88
89        if ( empty( $url ) || ( $options['attached_media'][0]['url'] ?? '' ) === $url ) {
90            return;
91        }
92
93        $options['attached_media'][0] = array(
94            'id'   => 0,
95            'url'  => $url,
96            'type' => 'image/png',
97        );
98
99        update_post_meta( $post_id, Publicize::POST_JETPACK_SOCIAL_OPTIONS, $options );
100    }
101
102    /**
103     * Trigger token generation for a post if SIG is enabled.
104     *
105     * @param int      $post_id     Post ID.
106     * @param \WP_Post $post        The post object being saved.
107     * @param bool     $update      Whether this is an update to a post.
108     */
109    public function generate_token_on_save( $post_id, $post, $update ) {
110        if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
111            return;
112        }
113
114        // If we're not using the block editor for this post, do not continue.
115        if ( ! use_block_editor_for_post( $post ) ) {
116            return;
117        }
118
119        global $publicize;
120
121        if ( ! $publicize->post_type_is_publicizeable( $post->post_type ) ) {
122            return;
123        }
124
125        $settings = new Settings();
126
127        if ( ! $settings->is_sig_available() ) {
128            return;
129        }
130
131        if ( wp_is_post_autosave( $post ) || wp_is_post_revision( $post_id ) ) {
132            return;
133        }
134
135        // Set SIG to be enabled by default for new posts if the toggle is on.
136        $post_settings = new Post_Settings( $post_id );
137        if (
138            ! $update &&
139            'auto-draft' === $post->post_status &&
140            ! empty( $settings->get_settings()['socialImageGeneratorSettings']['enabled'] ) &&
141            empty( $post_settings->get_settings( true ) ) &&
142            'jetpack-social-note' !== $post->post_type
143        ) {
144            $post_settings->update_setting( 'enabled', true );
145            return;
146        }
147
148        if ( $post->post_status === 'auto-draft' ) {
149            return;
150        }
151
152        if ( ! $post_settings->is_enabled() ) {
153            return;
154        }
155
156        $this->generate_token( $post_settings );
157
158        // Prime the Social Image Generator cache out-of-band right after publish.
159        // SIG renders the preview image on the first request to its URL, so a post
160        // shared immediately after publishing can race that cold render and end up
161        // with no preview image (notably on X, which does not retry). Warming the
162        // URL here means the image is already rendered and edge-cached before any
163        // crawler fetches it. Scheduled rather than inline so it never delays the
164        // publish request itself.
165        if (
166            'publish' === $post->post_status &&
167            ! wp_next_scheduled( 'jetpack_social_sig_warm_image', array( $post_id ) )
168        ) {
169            wp_schedule_single_event( time(), 'jetpack_social_sig_warm_image', array( $post_id ) );
170        }
171    }
172
173    /**
174     * Warm the edge cache for a post's generated social image.
175     *
176     * Runs from a scheduled single event (see generate_token_on_save) so it never
177     * blocks the publish request. Issues one blocking request to the same URL the
178     * Open Graph tags expose, which forces the on-demand render and lets the full
179     * response populate the edge cache before a crawler fetches it.
180     *
181     * @param int $post_id Post ID whose social image should be primed.
182     */
183    public function warm_social_image( $post_id ) {
184        $post_settings = new Post_Settings( $post_id );
185
186        if ( ! $post_settings->is_enabled() ) {
187            return;
188        }
189
190        $image_url = get_image_url( $post_id );
191
192        if ( empty( $image_url ) ) {
193            return;
194        }
195
196        // Blocking so the rendered response travels back through the edge cache and
197        // is stored; redirection is followed to the final image URL the crawler hits.
198        wp_remote_get(
199            $image_url,
200            array(
201                'timeout'     => 15,
202                'redirection' => 5,
203                'blocking'    => true,
204                'user-agent'  => 'WordPress.com Social Image Generator cache warmer',
205            )
206        );
207    }
208}