Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.29% covered (warning)
52.29%
343 / 656
30.00% covered (danger)
30.00%
15 / 50
CRAP
0.00% covered (danger)
0.00%
0 / 1
Automattic\Jetpack\Publicize\publicize_calypso_url
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Publicize\add_theme_post_thumbnails_support
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Publicize_Base
52.53% covered (warning)
52.53%
343 / 653
31.25% covered (danger)
31.25%
15 / 48
6041.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
1
 get_services
n/a
0 / 0
n/a
0 / 0
0
 has_feature_flag
n/a
0 / 0
n/a
0 / 0
1
 is_enabled
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 connect_url
n/a
0 / 0
n/a
0 / 0
0
 refresh_url
n/a
0 / 0
n/a
0 / 0
0
 disconnect_url
n/a
0 / 0
n/a
0 / 0
0
 get_service_label
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
90
 get_connections
n/a
0 / 0
n/a
0 / 0
0
 get_connection
n/a
0 / 0
n/a
0 / 0
0
 get_connection_id
n/a
0 / 0
n/a
0 / 0
0
 get_connection_unique_id
n/a
0 / 0
n/a
0 / 0
0
 get_connection_meta
n/a
0 / 0
n/a
0 / 0
0
 disconnect
n/a
0 / 0
n/a
0 / 0
0
 globalize_connection
n/a
0 / 0
n/a
0 / 0
0
 unglobalize_connection
n/a
0 / 0
n/a
0 / 0
0
 get_profile_link
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
552
 get_display_name
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 get_username
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_external_handle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 get_profile_picture
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 show_options_popup
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
210
 is_global_connection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_valid_facebook_connection
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 is_invalid_linkedin_connection
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_connecting_connection
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 parse_connection_error_code
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_publicize_conns_test_results
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
182
 test_connection
n/a
0 / 0
n/a
0 / 0
0
 get_filtered_connection_data
92.31% covered (success)
92.31%
48 / 52
0.00% covered (danger)
0.00%
0 / 1
18.15
 post_is_done_sharing
n/a
0 / 0
n/a
0 / 0
0
 get_available_service_data
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 user_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 blog_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flag_post_for_publicize
n/a
0 / 0
n/a
0 / 0
0
 add_post_type_support
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 current_user_can_access_publicize_data
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 message_meta_auth_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register_post_meta
100.00% covered (success)
100.00%
173 / 173
100.00% covered (success)
100.00%
1 / 1
5
 image_focal_point_auth_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 any_connection_has_custom_template
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 should_submit_post_pre_checks
63.16% covered (warning)
63.16%
12 / 19
0.00% covered (danger)
0.00%
0 / 1
23.80
 save_meta
61.54% covered (warning)
61.54%
24 / 39
0.00% covered (danger)
0.00%
0 / 1
46.09
 update_published_message
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
132
 get_publicizing_services
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 post_is_publicizeable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 post_type_is_publicizeable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 publicize_checkbox_default
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_attached_media_image
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
6.35
 get_social_opengraph_image
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 get_remote_filesize
n/a
0 / 0
n/a
0 / 0
1
 get_resized_image_url
n/a
0 / 0
n/a
0 / 0
1
 compress_and_scale_og_image
n/a
0 / 0
n/a
0 / 0
1
 reduce_file_size
n/a
0 / 0
n/a
0 / 0
1
 jetpack_social_open_graph_filter
n/a
0 / 0
n/a
0 / 0
1
 add_jetpack_social_og_image
n/a
0 / 0
n/a
0 / 0
1
 add_jetpack_social_og_images
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 build_sprintf
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 publicize_connections_url
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_api_data
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 is_enhanced_publishing_enabled
n/a
0 / 0
n/a
0 / 0
1
 has_enhanced_publishing_feature
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_social_image_generator_enabled
n/a
0 / 0
n/a
0 / 0
1
 has_social_image_generator_feature
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_social_auto_conversion_feature
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 has_connection_feature
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 has_connections_management_feature
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_supported_additional_connections
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 has_paid_plan
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 has_paid_features
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_dismissed_notices
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 can_manage_connection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Publicize_Base class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8// phpcs:disable WordPress.NamingConventions.ValidVariableName
9
10namespace Automattic\Jetpack\Publicize;
11
12use Automattic\Jetpack\Connection\Client;
13use Automattic\Jetpack\Current_Plan;
14use Automattic\Jetpack\Paths;
15use Automattic\Jetpack\Redirect;
16use Automattic\Jetpack\Status;
17use WP_Error;
18use WP_Post;
19
20/**
21 * Base class for Publicize.
22 */
23abstract class Publicize_Base {
24
25    /**
26     * Services that are currently connected to the given user
27     * through Publicize.
28     *
29     * @var array
30     */
31    public $connected_services = array();
32
33    /**
34     * Services that are supported by publicize. They don't
35     * necessarily need to be connected to the current user.
36     *
37     * @var array
38     */
39    public $services;
40
41    /**
42     * Post meta key for admin page.
43     *
44     * @var string
45     */
46    public $ADMIN_PAGE = 'wpas';
47
48    /**
49     * Post meta key for post message.
50     *
51     * @var string
52     */
53    public $POST_MESS = '_wpas_mess';
54
55    /**
56     * Post meta key for the flagging when the post share feature is disabled.
57     *
58     * @var string
59     */
60    const POST_PUBLICIZE_FEATURE_ENABLED = '_wpas_feature_enabled';
61
62    /**
63     * Post meta key for Jetpack Social options.
64     *
65     * @var string
66     */
67    const POST_JETPACK_SOCIAL_OPTIONS = '_wpas_options';
68
69    /**
70     * Post meta key for per-connection customization overrides.
71     *
72     * @var string
73     */
74    const POST_CONNECTION_OVERRIDES = '_wpas_connection_overrides';
75
76    /**
77     * Post meta key for enabling per-network customization.
78     *
79     * @var string
80     */
81    const POST_CUSTOMIZE_PER_NETWORK = '_wpas_customize_per_network';
82
83    /**
84     * Attachment meta key for the social image focal point.
85     *
86     * Stored on the image (attachment), not the post, so a focal point set once is
87     * shared by every post that uses the image.
88     *
89     * @var string
90     */
91    const ATTACHMENT_IMAGE_FOCAL_POINT = '_jetpack_social_image_focal_point';
92
93    // Skip meta keys. We used to rely on _wpas_skip_ appended with the token_id to skip posts. But to support
94    // multiple connections for the same token, we are going to use the _wpas_skip_publicize_ which
95    // will be appended with the connection_id.
96    /**
97     * Token ID appended to indicate that a connection should NOT be publicized to.
98     *
99     * @var string
100     */
101    public $POST_SKIP = '_wpas_skip_';
102
103    /**
104     * Connection ID appended to indicate that a connection should NOT be publicized to.
105     *
106     * @var string
107     */
108    public $POST_SKIP_PUBLICIZE = '_wpas_skip_publicize_';
109
110    // Done meta keys.
111    /**
112     * Token ID appended to indicate a connection has already been publicized to.
113     *
114     * @var string
115     */
116    public $POST_DONE = '_wpas_done_';
117
118    /**
119     * Prefix for user authorization (used in publicize-wpcom.php)
120     *
121     * @var string
122     */
123    public $USER_AUTH = 'wpas_authorize';
124
125    /**
126     * Prefix for user opt.
127     *
128     * @var string
129     */
130    public $USER_OPT = 'wpas_';
131
132    /**
133     * Ready for Publicize to do its thing.
134     *
135     * @var string
136     */
137    public $PENDING = '_publicize_pending';
138
139    /**
140     * Array of external IDs where we've Publicized.
141     *
142     * @var string
143     */
144    public $POST_SERVICE_DONE = '_publicize_done_external';
145
146    /**
147     * Option key for dismissing Jetpack Social notices.
148     *
149     * @var string
150     */
151    const OPTION_JETPACK_SOCIAL_DISMISSED_NOTICES = 'jetpack_social_dismissed_notices';
152
153    /**
154     * The maximum size of an image that can be used as an Open Graph image.
155     *
156     * @var string
157     */
158    const OG_IMAGE_MAX_FILESIZE = 2000000; // 2MB.
159
160    /**
161     * Default pieces of the message used in constructing the
162     * content pushed out to other social networks.
163     */
164
165    /**
166     * Default prefix.
167     *
168     * @var string
169     */
170    public $default_prefix = '';
171
172    /**
173     * Default message.
174     *
175     * @var string
176     */
177    public $default_message = '%title%';
178
179    /**
180     * Default suffix.
181     *
182     * @var string
183     */
184    public $default_suffix = ' ';
185
186    /**
187     * What WP capability is require to create/delete global connections?
188     * All users with this cap can un-globalize all other global connections, and globalize any of their own
189     * Globalized connections cannot be unselected by users without this capability when publishing
190     *
191     * @var string
192     */
193    public $GLOBAL_CAP = 'publish_posts';
194
195    /**
196     * Sets up the basics of Publicize.
197     */
198    public function __construct() {
199        $this->default_message = self::build_sprintf(
200            array(
201                /**
202                 * Filter the default Publicize message.
203                 *
204                 * @since 0.1.0
205                 * @since-jetpack 2.0.0
206                 *
207                 * @param string $this->default_message Publicize's default message. Default is the post title.
208                 */
209                apply_filters( 'wpas_default_message', $this->default_message ),
210                'title',
211                'url',
212            )
213        );
214
215        $this->default_prefix = self::build_sprintf(
216            array(
217                /**
218                 * Filter the message prepended to the Publicize custom message.
219                 *
220                 * @since 0.1.0
221                 * @since-jetpack 2.0.0
222                 *
223                 * @param string $this->default_prefix String prepended to the Publicize custom message.
224                 */
225                apply_filters( 'wpas_default_prefix', $this->default_prefix ),
226                'url',
227            )
228        );
229
230        $this->default_suffix = self::build_sprintf(
231            array(
232                /**
233                 * Filter the message appended to the Publicize custom message.
234                 *
235                 * @since 0.1.0
236                 * @since-jetpack 2.0.0
237                 *
238                 * @param string $this->default_suffix String appended to the Publicize custom message.
239                 */
240                apply_filters( 'wpas_default_suffix', $this->default_suffix ),
241                'url',
242            )
243        );
244
245        /**
246         * Filter the capability to change global Publicize connection options.
247         *
248         * All users with this cap can un-globalize all other global connections, and globalize any of their own
249         * Globalized connections cannot be unselected by users without this capability when publishing.
250         *
251         * @since 0.1.0
252         * @since-jetpack 2.2.1
253         *
254         * @param string $this->GLOBAL_CAP default capability in control of global Publicize connection options. Default to edit_others_posts.
255         */
256        $this->GLOBAL_CAP = apply_filters( 'jetpack_publicize_global_connections_cap', $this->GLOBAL_CAP );
257
258        // stage 1 and 2 of 3-stage Publicize. Flag for Publicize on creation, save meta,
259        // then check meta and publicize based on that. stage 3 implemented on wpcom.
260        add_action( 'transition_post_status', array( $this, 'flag_post_for_publicize' ), 10, 3 );
261        add_action( 'save_post', array( $this, 'save_meta' ), 20, 2 );
262
263        // Default checkbox state for each Connection.
264        add_filter( 'publicize_checkbox_default', array( $this, 'publicize_checkbox_default' ), 10, 2 );
265
266        // Add images generated by Social Image Generator to the Open Graph tags on singular posts.
267        add_filter( 'jetpack_open_graph_tags', array( $this, 'add_jetpack_social_og_images' ), 12, 1 ); // $priority = 12, to run after the Twitter_Cards class adds its tags.
268
269        // Alter the "Post Publish" admin notice to mention the Connections we Publicized to.
270        add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 );
271
272        // Custom priority to ensure post type support is added prior to thumbnail support being added to the theme.
273        add_action( 'init', array( $this, 'add_post_type_support' ), 8 );
274        add_action( 'init', array( $this, 'register_post_meta' ), 20 );
275
276        // The custom priority for this action ensures that any existing code that
277        // removes post-thumbnails support during 'init' continues to work.
278        add_action( 'init', __NAMESPACE__ . '\add_theme_post_thumbnails_support', 8 );
279    }
280
281    /**
282     * Services: Facebook, Twitter, etc.
283     */
284
285    /**
286     * Get services for the given blog and user.
287     *
288     * Can return all available services or just the ones with an active connection.
289     *
290     * @param string    $filter Type of filter.
291     *        'all' (default) - Get all services available for connecting.
292     *        'connected'     - Get all services currently connected.
293     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
294     * @param false|int $_user_id The user ID. Use false (default) for the current user.
295     * @return array
296     */
297    abstract public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false );
298
299    /**
300     * Whether the site has the feature flag enabled.
301     *
302     * @deprecated 0.69.1 Use Current_Plan::supports() directly instead.
303     *
304     * @todo Remove this method After March 2026.
305     *
306     * @param string $flag_name The feature flag to check. Will be prefixed with 'jetpack_social_has_' for the option.
307     * @param string $feature_name The feature name to check for for the Current_Plan check, without the social- prefix.
308     * @return bool
309     */
310    public function has_feature_flag( $flag_name, $feature_name ): bool {
311        return Publicize_Script_Data::has_feature_flag( $feature_name );
312    }
313
314    /**
315     * Does the given user have a connection to the service on the given blog?
316     *
317     * @param string    $service_name 'facebook', 'twitter', etc.
318     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
319     * @param false|int $_user_id The user ID. Use false (default) for the current user.
320     * @return bool
321     */
322    public function is_enabled( $service_name, $_blog_id = false, $_user_id = false ) {
323        if ( ! $_blog_id ) {
324            $_blog_id = $this->blog_id();
325        }
326
327        if ( ! $_user_id ) {
328            $_user_id = $this->user_id();
329        }
330
331        $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
332        return is_array( $connections ) && count( $connections ) > 0;
333    }
334
335    /**
336     * Generates a connection URL.
337     *
338     * This is the URL, which, when visited by the user, starts the authentication
339     * process required to forge a connection.
340     *
341     * @param string $service_name 'facebook', 'twitter', etc.
342     * @return string
343     */
344    abstract public function connect_url( $service_name );
345
346    /**
347     * Generates a Connection refresh URL.
348     *
349     * This is the URL, which, when visited by the user, re-authenticates their
350     * connection to the service.
351     *
352     * @param string $service_name 'facebook', 'twitter', etc.
353     * @return string
354     */
355    abstract public function refresh_url( $service_name );
356
357    /**
358     * Generates a disconnection URL.
359     *
360     * This is the URL, which, when visited by the user, breaks their connection
361     * with the service.
362     *
363     * @param string $service_name 'facebook', 'twitter', etc.
364     * @param string $connection_id Connection ID.
365     * @return string
366     */
367    abstract public function disconnect_url( $service_name, $connection_id );
368
369    /**
370     * Returns a display name for the Service
371     *
372     * @param string $service_name 'facebook', 'twitter', etc.
373     * @return string
374     */
375    public static function get_service_label( $service_name ) {
376        switch ( $service_name ) {
377            case 'linkedin':
378                return 'LinkedIn';
379            case 'google_drive': // google-drive used to be called google_drive.
380            case 'google-drive':
381                return 'Google Drive';
382            case 'instagram-business':
383                return 'Instagram';
384            case 'twitter':
385            case 'facebook':
386            case 'tumblr':
387            default:
388                return ucfirst( $service_name );
389        }
390    }
391
392    /**
393     * Connections: For each Service, there can be multiple connections
394     * for a given user. For example, one user could be connected to Twitter
395     * as both @jetpack and as @wordpressdotcom
396     *
397     * For historical reasons, Connections are represented as an object
398     * on WordPress.com and as an array in Jetpack.
399     */
400
401    /**
402     * Get the active Connections of a Service
403     *
404     * @param string    $service_name 'facebook', 'twitter', etc.
405     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
406     * @param false|int $_user_id The user ID. Use false (default) for the current user.
407     * @return false|object[]|array[] false if no connections exist
408     */
409    abstract public function get_connections( $service_name, $_blog_id = false, $_user_id = false );
410
411    /**
412     * Get a single Connection of a Service
413     *
414     * @param string    $service_name 'facebook', 'twitter', etc.
415     * @param string    $connection_id Connection ID.
416     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
417     * @param false|int $_user_id The user ID. Use false (default) for the current user.
418     * @return false|object[]|array[] false if no connections exist
419     */
420    abstract public function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false );
421
422    /**
423     * Get the Connection ID.
424     *
425     * Note that this is different than the Connection's uniqueid.
426     *
427     * Via a quirk of history, ID is globally unique and unique_id
428     * is only unique per site.
429     *
430     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
431     * @return string
432     */
433    abstract public function get_connection_id( $connection );
434
435    /**
436     * Get the Connection unique_id
437     *
438     * Note that this is different than the Connections ID.
439     *
440     * Via a quirk of history, ID is globally unique and unique_id
441     * is only unique per site.
442     *
443     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
444     * @return string
445     */
446    abstract public function get_connection_unique_id( $connection );
447
448    /**
449     * Get the Connection's Meta data
450     *
451     * @param object|array $connection Connection.
452     * @return array Connection Meta
453     */
454    abstract public function get_connection_meta( $connection );
455
456    /**
457     * Disconnect a Connection
458     *
459     * @param string    $service_name 'facebook', 'twitter', etc.
460     * @param string    $connection_id Connection ID.
461     * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
462     * @param false|int $_user_id The user ID. Use false (default) for the current user.
463     * @param bool      $force_delete Whether to skip permissions checks.
464     * @return false|void False on failure. Void on success.
465     */
466    abstract public function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false );
467
468    /**
469     * Globalizes a Connection
470     *
471     * @param string $connection_id Connection ID.
472     * @return bool Falsey on failure. Truthy on success.
473     */
474    abstract public function globalize_connection( $connection_id );
475
476    /**
477     * Unglobalizes a Connection
478     *
479     * @param string $connection_id Connection ID.
480     * @return bool Falsey on failure. Truthy on success.
481     */
482    abstract public function unglobalize_connection( $connection_id );
483
484    /**
485     * Returns an external URL to the Connection's profile
486     *
487     * @param string       $service_name 'facebook', 'twitter', etc.
488     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
489     * @return false|string False on failure. URL on success.
490     */
491    public function get_profile_link( $service_name, $connection ) {
492        $cmeta = $this->get_connection_meta( $connection );
493
494        if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
495            if ( 'facebook' === $service_name && str_starts_with( wp_parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
496                // App-scoped Facebook user IDs are not usable profile links.
497                return false;
498            }
499
500            return $cmeta['connection_data']['meta']['link'];
501        }
502
503        if ( 'facebook' === $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
504            return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
505        }
506
507        if ( 'instagram-business' === $service_name && isset( $cmeta['connection_data']['meta']['username'] ) ) {
508            return 'https://instagram.com/' . $cmeta['connection_data']['meta']['username'];
509        }
510
511        if ( 'linkedin' === $service_name ) {
512
513            $entity_type = $cmeta['connection_data']['meta']['entity_type'] ?? 'person';
514
515            if ( 'organization' === $entity_type && ! empty( $cmeta['connection_data']['meta']['external_name'] ) ) {
516                return 'https://www.linkedin.com/company/' . $cmeta['connection_data']['meta']['external_name'];
517            }
518
519            if ( 'person' === $entity_type && ! empty( $cmeta['external_name'] ) ) {
520                return 'https://www.linkedin.com/in/' . $cmeta['external_name'];
521            }
522        }
523
524        if ( 'threads' === $service_name && isset( $cmeta['external_name'] ) ) {
525            return 'https://www.threads.net/@' . $cmeta['external_name'];
526        }
527
528        if ( 'mastodon' === $service_name && isset( $cmeta['external_name'] ) ) {
529            return 'https://mastodon.social/@' . $cmeta['external_name'];
530        }
531
532        if ( 'nextdoor' === $service_name && isset( $cmeta['external_id'] ) ) {
533            return 'https://nextdoor.com/profile/' . $cmeta['external_id'];
534        }
535
536        if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
537            return 'https://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
538        }
539
540        if ( 'twitter' === $service_name ) {
541            return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'.
542        }
543
544        if ( 'bluesky' === $service_name ) {
545            return 'https://bsky.app/profile/' . $cmeta['external_id'];
546        }
547
548        return false; // no fallback. we just won't link it.
549    }
550
551    /**
552     * Returns a display name for the Connection
553     *
554     * @param string       $service_name 'facebook', 'twitter', etc.
555     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
556     * @return string
557     */
558    public function get_display_name( $service_name, $connection ) {
559        $cmeta = $this->get_connection_meta( $connection );
560
561        if ( 'mastodon' === $service_name && isset( $cmeta['external_display'] ) ) {
562            return $cmeta['external_display'];
563        }
564
565        if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
566            return $cmeta['connection_data']['meta']['display_name'];
567        }
568
569        if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
570            return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
571        }
572
573        if ( 'twitter' === $service_name ) {
574            return $cmeta['external_display'];
575        }
576
577        $connection_display = $cmeta['external_display'];
578
579        if ( empty( $connection_display ) ) {
580            $connection_display = $cmeta['external_name'];
581        }
582
583        return $connection_display;
584    }
585
586    /**
587     * Returns the account name for the Connection. For services like Instagram, we need both the username and the account's name.
588     *
589     * @param string       $service_name 'facebook', 'linkedin', etc.
590     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
591     * @return string
592     */
593    public function get_username( $service_name, $connection ) {
594        $handle = $this->get_external_handle( $service_name, $connection );
595
596        return $handle ?? $this->get_display_name( $service_name, $connection );
597    }
598
599    /**
600     * Returns the external handle for the Connection.
601     *
602     * @param string       $service_name 'facebook', 'linkedin', etc.
603     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
604     * @return string|null
605     */
606    public function get_external_handle( $service_name, $connection ) {
607        $cmeta = $this->get_connection_meta( $connection );
608
609        switch ( $service_name ) {
610            case 'mastodon':
611                return $cmeta['external_display'] ?? null;
612
613            case 'bluesky':
614            case 'threads':
615                return $cmeta['external_name'] ?? null;
616
617            case 'instagram-business':
618                return $cmeta['connection_data']['meta']['username'] ?? null;
619
620            default:
621                return null;
622        }
623    }
624
625    /**
626     * Returns a profile picture for the Connection
627     *
628     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
629     * @return string
630     */
631    public function get_profile_picture( $connection ) {
632        $cmeta = $this->get_connection_meta( $connection );
633
634        if ( isset( $cmeta['profile_picture'] ) ) {
635            return $cmeta['profile_picture'];
636        }
637
638        return '';
639    }
640
641    /**
642     * Whether the user needs to select additional options after connecting
643     *
644     * @param string       $service_name 'facebook', 'twitter', etc.
645     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
646     * @return bool
647     */
648    public function show_options_popup( $service_name, $connection ) {
649        $cmeta = $this->get_connection_meta( $connection );
650
651        // Always show if no selection has been made for Facebook.
652        if ( 'facebook' === $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
653            return true;
654        }
655
656        // Always show if no selection has been made for Tumblr.
657        if ( 'tumblr' === $service_name && empty( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
658            return true;
659        }
660
661        // if we have the specific connection info..
662        $id = ! empty( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
663
664        if ( $id ) {
665            if ( $cmeta['connection_data']['id'] === $id ) {
666                return true;
667            }
668        } else {
669            // Otherwise, just show if this is the completed step / first load.
670            // phpcs:disable WordPress.Security.NonceVerification.Recommended
671            $is_completed = ! empty( $_GET['action'] ) && 'completed' === $_GET['action'];
672            $service      = ! empty( $_GET['service'] ) ? sanitize_text_field( wp_unslash( $_GET['service'] ) ) : false;
673            // phpcs:enable WordPress.Security.NonceVerification.Recommended
674
675            if ( $is_completed && $service_name === $service && ! in_array( $service, array( 'facebook', 'tumblr' ), true ) ) {
676                return true;
677            }
678        }
679
680        return false;
681    }
682
683    /**
684     * Check if a connection is global
685     *
686     * @param array $connection Connection data.
687     * @return bool Whether the connection is global.
688     */
689    public function is_global_connection( $connection ) {
690        return empty( $connection['connection_data']['user_id'] );
691    }
692
693    /**
694     * Whether the Connection is "valid" wrt Facebook's requirements.
695     *
696     * Must be connected to a Page (not a Profile).
697     * (Also returns true if we're in the middle of the connection process)
698     *
699     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
700     * @return bool
701     */
702    public function is_valid_facebook_connection( $connection ) {
703        if ( $this->is_connecting_connection( $connection ) ) {
704            return true;
705        }
706        $connection_meta = $this->get_connection_meta( $connection );
707        $connection_data = $connection_meta['connection_data'];
708        return isset( $connection_data['meta']['facebook_page'] );
709    }
710
711    /**
712     * LinkedIn needs to be reauthenticated to use v2 of their API.
713     * If it's using LinkedIn old API, it's an 'invalid' connection
714     *
715     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
716     * @return bool
717     */
718    public function is_invalid_linkedin_connection( $connection ) {
719        // LinkedIn API v1 included the profile link in the connection data.
720        $connection_meta = $this->get_connection_meta( $connection );
721        return isset( $connection_meta['connection_data']['meta']['profile_url'] );
722    }
723
724    /**
725     * Whether the Connection currently being connected
726     *
727     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
728     * @return bool
729     */
730    public function is_connecting_connection( $connection ) {
731        $connection_meta = $this->get_connection_meta( $connection );
732        $connection_data = $connection_meta['connection_data'];
733        return isset( $connection_data['meta']['options_responses'] );
734    }
735
736    /**
737     * Parse the error code returned by the XML-RPC API call.
738     *
739     * @param string $code Error code in numerical format.
740     *
741     * @return string Error code.
742     */
743    public function parse_connection_error_code( $code ) {
744        if ( 1 === $code ) {
745            return 'unsupported';
746        }
747
748        return 'broken';
749    }
750
751    /**
752     * Run connection tests on all Connections
753     *
754     * @return array {
755     *     Array of connection test results.
756     *
757     *     @type string 'connectionID'          Connection identifier string that is unique for each connection
758     *     @type string 'serviceName'           Slug of the connection's service (facebook, twitter, ...)
759     *     @type bool   'connectionTestPassed'  Whether the connection test was successful
760     *     @type string 'connectionTestMessage' Test success or error message
761     *     @type bool   'userCanRefresh'        Whether the user can re-authenticate their connection to the service
762     *     @type string 'refreshText'           Message instructing user to re-authenticate their connection to the service
763     *     @type string 'refreshURL'            URL, which, when visited by the user, re-authenticates their connection to the service.
764     *     @type string 'unique_id'             ID string representing connection
765     * }
766     */
767    public function get_publicize_conns_test_results() {
768        $test_results = array();
769
770        foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
771            foreach ( $connections as $connection ) {
772
773                $id = $this->get_connection_id( $connection );
774
775                $connection_test_error_code = '';
776                $connection_test_passed     = true;
777                $connection_test_message    = __( 'This connection is working correctly.', 'jetpack-publicize-pkg' );
778                $user_can_refresh           = false;
779                $refresh_text               = '';
780                $refresh_url                = '';
781
782                $connection_test_result = true;
783                if ( method_exists( $this, 'test_connection' ) ) {
784                    $connection_test_result = $this->test_connection( $service_name, $connection );
785                }
786
787                if ( is_wp_error( $connection_test_result ) ) {
788                    $connection_test_passed  = false;
789                    $connection_test_message = $connection_test_result->get_error_message();
790                    $error_data              = $connection_test_result->get_error_data();
791                    $error_code              = $connection_test_result->get_error_code();
792
793                    if ( ! empty( $error_data ) ) {
794                        $user_can_refresh = $error_data['user_can_refresh'];
795                        $refresh_text     = $error_data['refresh_text'];
796                        $refresh_url      = $error_data['refresh_url'];
797                    }
798                    $connection_test_error_code = $connection_test_passed ? '' : $this->parse_connection_error_code( $error_code );
799                }
800                // Mark Facebook profiles as deprecated.
801                if ( 'facebook' === $service_name ) {
802                    if ( ! $this->is_valid_facebook_connection( $connection ) ) {
803                        $connection_test_passed  = false;
804                        $user_can_refresh        = false;
805                        $connection_test_message = __( 'Please select a Facebook Page to publish updates.', 'jetpack-publicize-pkg' );
806                    }
807                }
808
809                // LinkedIn needs reauthentication to be compatible with v2 of their API.
810                if ( 'linkedin' === $service_name && $this->is_invalid_linkedin_connection( $connection ) ) {
811                    $connection_test_passed  = 'must_reauth';
812                    $user_can_refresh        = false;
813                    $connection_test_message = esc_html__( 'Your LinkedIn connection needs to be reauthenticated to continue working â€“ head to Sharing to take care of it.', 'jetpack-publicize-pkg' );
814                }
815
816                $unique_id = null;
817
818                if ( ! empty( $connection->unique_id ) ) {
819                    $unique_id = $connection->unique_id;
820                } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) {
821                    $unique_id = $connection['connection_data']['token_id'];
822                }
823
824                $test_results[] = array(
825                    'connectionID'            => $id,
826                    'serviceName'             => $service_name,
827                    'connectionTestPassed'    => $connection_test_passed,
828                    'connectionTestErrorCode' => $connection_test_error_code,
829                    'connectionTestMessage'   => esc_attr( $connection_test_message ),
830                    'userCanRefresh'          => $user_can_refresh,
831                    'refreshText'             => esc_attr( $refresh_text ),
832                    'refreshURL'              => $refresh_url,
833                    'unique_id'               => $unique_id,
834                );
835            }
836        }
837
838        return $test_results;
839    }
840
841    /**
842     * Run the connection test for the Connection
843     *
844     * @param string       $service_name $service_name 'facebook', 'twitter', etc.
845     * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
846     * @return WP_Error|true WP_Error on failure. True on success
847     */
848    abstract public function test_connection( $service_name, $connection );
849
850    /**
851     * Retrieves current list of connections and applies filters.
852     *
853     * Retrieves current available connections and checks if the connections
854     * have already been used to share current post. Finally, the checkbox
855     * form UI fields are calculated. This function exposes connection form
856     * data directly as array so it can be retrieved for static HTML generation
857     * or JSON consumption.
858     *
859     * @since 0.1.0
860     * @since-jetpack 6.7.0
861     *
862     * @param integer $selected_post_id Optional. Post ID to query connection status for.
863     *
864     * @return array {
865     *     Array of UI setup data for connection list form.
866     *
867     *     @type string 'unique_id'        ID string representing connection
868     *     @type string 'service_name'     Slug of the connection's service (facebook, twitter, ...)
869     *     @type string 'service_label'    Service Label (Facebook, Twitter, ...)
870     *     @type string 'display_name'     Connection's human-readable Username: "@jetpack"
871     *     @type string 'profile_picture'  Connection profile picture.
872     *     @type bool   'enabled'          Default value for the connection (e.g., for a checkbox).
873     *     @type bool   'done'             Has this connection already been publicized to?
874     *     @type bool   'toggleable'       Is the user allowed to change the value for the connection?
875     *     @type bool   'global'           Is this connection a global one?
876     *     @type string 'external_id'      External ID for the connection.
877     * }
878     */
879    public function get_filtered_connection_data( $selected_post_id = null ) {
880        $connection_list = array();
881
882        $post = get_post( $selected_post_id ); // Defaults to current post if $post_id is null.
883        // Handle case where there is no current post.
884        if ( ! empty( $post ) ) {
885            $post_id = $post->ID;
886        } else {
887            $post_id = null;
888        }
889
890        // We don't allow Publicizing to the same external id twice, to prevent spam.
891        $service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true );
892
893        $connections = Connections::get_all_for_user();
894
895        if ( ! empty( $connections ) ) {
896
897            foreach ( $connections as $connection ) {
898                $service_name  = $connection['service_name'];
899                $unique_id     = $connection['id'];
900                $connection_id = $connection['connection_id'];
901                // Was this connection (OR, old-format service) already Publicized to?
902                $done = ! empty( $post ) && (
903                    // Flags based on token_id.
904                    1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true )
905                    ||
906                    // Old flags.
907                    1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $service_name, true )
908                );
909
910                /**
911                 * Filter whether a post should be publicized to a given service.
912                 *
913                 * @since 0.1.0
914                 * @since-jetpack 2.0.0
915                 *
916                 * @param bool true Should the post be publicized to a given service? Default to true.
917                 * @param int $post_id Post ID.
918                 * @param string $service_name Service name.
919                 * @param array $connection The connection data.
920                 */
921                /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */
922                if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection ) ) {
923                    continue;
924                }
925
926                // Should we be skipping this one?
927                $skip = (
928                    (
929                        ! empty( $post )
930                        &&
931                        in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true )
932                        &&
933                        (
934                            // Flags based on token_id.
935                            get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true )
936                            ||
937                            // Flags based on  the connection_id.
938                            get_post_meta( $post->ID, $this->POST_SKIP_PUBLICIZE . $connection_id, true )
939                            ||
940                            // Old flags.
941                            get_post_meta( $post->ID, $this->POST_SKIP . $service_name, false )
942                        )
943                    )
944                    ||
945                    (
946                        isset( $connection['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection['external_id'] ] )
947                    )
948                );
949
950                // Determine the state of the checkbox (on/off) and allow filtering.
951                $enabled = $done || ! $skip;
952                /**
953                 * Filter the checkbox state of each Publicize connection appearing in the post editor.
954                 *
955                 * @since 0.1.0
956                 * @since-jetpack 2.0.1
957                 *
958                 * @param bool $enabled Should the Publicize checkbox be enabled for a given service.
959                 * @param int $post_id Post ID.
960                 * @param string $service_name Service name.
961                 * @param array $connection Array of connection details.
962                 */
963                $enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection );
964
965                /**
966                 * If this is a shared connection and this user doesn't have enough permissions to modify.
967                 */
968                if ( ! $done && Connections::is_shared( $connection ) && ! current_user_can( $this->GLOBAL_CAP ) ) {
969                    /**
970                     * Filters the checkboxes for global connections with non-prilvedged users.
971                     *
972                     * @since 0.1.0
973                     * @since-jetpack 3.7.0
974                     *
975                     * @param bool   $enabled Indicates if this connection should be enabled. Default true.
976                     * @param int    $post_id ID of the current post
977                     * @param string $service_name Name of the connection (Facebook, Twitter, etc)
978                     * @param array  $connection Array of data about the connection.
979                     */
980                    $enabled = apply_filters( 'publicize_checkbox_global_default', $enabled, $post_id, $service_name, $connection );
981                }
982
983                // Force the checkbox to be checked if the post was DONE, regardless of what the filter does.
984                if ( $done ) {
985                    $enabled = true;
986                }
987
988                $connection_list[] = array_merge(
989                    $connection,
990                    array(
991                        'enabled' => $enabled,
992                        'done'    => $done,
993                    )
994                );
995            }
996        }
997
998        return $connection_list;
999    }
1000
1001    /**
1002     * Checks if post has already been shared by Publicize in the past.
1003     *
1004     * @since 0.1.0
1005     * @since-jetpack 6.7.0
1006     *
1007     * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
1008     *
1009     * @return bool True if post has already been shared by Publicize, false otherwise.
1010     */
1011    abstract public function post_is_done_sharing( $post_id = null );
1012
1013    /**
1014     * Retrieves full list of available Publicize connection services.
1015     *
1016     * Retrieves current available publicize service connections
1017     * with associated labels and URLs.
1018     *
1019     * @since 0.1.0
1020     * @since-jetpack 6.7.0
1021     *
1022     * @return array {
1023     *     Array of UI service connection data for all services
1024     *
1025     *     @type string 'name'  Name of service.
1026     *     @type string 'label' Display label for service.
1027     *     @type string 'url'   URL for adding connection to service.
1028     * }
1029     */
1030    public function get_available_service_data() {
1031        $available_services     = $this->get_services( 'all' );
1032        $available_service_data = array();
1033
1034        foreach ( $available_services as $service_name => $service ) {
1035            $available_service_data[] = array(
1036                'name'  => $service_name,
1037                'label' => static::get_service_label( $service_name ),
1038                'url'   => $this->connect_url( $service_name ),
1039            );
1040        }
1041
1042        return $available_service_data;
1043    }
1044
1045    /**
1046     * Site Data
1047     */
1048
1049    /**
1050     * Get user ID.
1051     *
1052     * @return int The current user's ID, or 0 if no user is logged in.
1053     */
1054    public function user_id() {
1055        return get_current_user_id();
1056    }
1057
1058    /**
1059     * Get site ID.
1060     *
1061     * @return int Site ID.
1062     */
1063    public function blog_id() {
1064        return get_current_blog_id();
1065    }
1066
1067    /**
1068     * Posts
1069     */
1070
1071    /**
1072     * Checks old and new status to see if the post should be flagged as
1073     * ready to Publicize.
1074     *
1075     * Attached to the `transition_post_status` filter.
1076     *
1077     * @param string  $new_status New status.
1078     * @param string  $old_status Old status.
1079     * @param WP_Post $post Post object.
1080     * @return void
1081     */
1082    abstract public function flag_post_for_publicize( $new_status, $old_status, $post );
1083
1084    /**
1085     * Ensures the Post internal post-type supports `publicize`
1086     *
1087     * This feature support flag is used by the REST API.
1088     */
1089    public function add_post_type_support() {
1090        add_post_type_support( 'post', 'publicize' );
1091    }
1092
1093    /**
1094     * Can the current user access Publicize Data.
1095     *
1096     * @param int $post_id 0 for general access. Post_ID for specific access.
1097     * @return bool
1098     */
1099    public function current_user_can_access_publicize_data( $post_id = 0 ) {
1100        /**
1101         * Filter what user capability is required to use the publicize form on the edit post page. Useful if publish post capability has been removed from role.
1102         *
1103         * @since 0.1.0
1104         * @since-jetpack 4.1.0
1105         *
1106         * @param string $capability User capability needed to use publicize
1107         */
1108        $capability = apply_filters( 'jetpack_publicize_capability', 'publish_posts' );
1109
1110        if ( 'publish_posts' === $capability && $post_id ) {
1111            return current_user_can( 'publish_post', $post_id );
1112        }
1113
1114        return current_user_can( $capability );
1115    }
1116
1117    /**
1118     * Auth callback for the protected ->POST_MESS post_meta
1119     *
1120     * @param int $object_id Post ID.
1121     * @return bool
1122     */
1123    public function message_meta_auth_callback( $object_id ) {
1124        return $this->current_user_can_access_publicize_data( $object_id );
1125    }
1126
1127    /**
1128     * Registers the post_meta for use in the REST API.
1129     *
1130     * Registers for each post type that with `publicize` feature support.
1131     */
1132    public function register_post_meta() {
1133        /*
1134         * Default the share-message meta to the saved global template
1135         */
1136        $message_default = Current_Plan::supports( 'social-message-templates' )
1137            ? ( new Jetpack_Social_Settings\Settings() )->get_message_template()
1138            : '';
1139
1140        $message_args = array(
1141            'type'          => 'string',
1142            'description'   => __( 'The message to use instead of the title when sharing to Jetpack Social services', 'jetpack-publicize-pkg' ),
1143            'single'        => true,
1144            'default'       => $message_default,
1145            'show_in_rest'  => array(
1146                'name' => 'jetpack_publicize_message',
1147            ),
1148            'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1149        );
1150
1151        $publicize_feature_enable_args = array(
1152            'type'          => 'boolean',
1153            'description'   => __( 'Whether or not the Share Post feature is enabled.', 'jetpack-publicize-pkg' ),
1154            'single'        => true,
1155            'default'       => true,
1156            'show_in_rest'  => array(
1157                'name' => 'jetpack_publicize_feature_enabled',
1158            ),
1159            'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1160        );
1161
1162        $already_shared_flag_args = array(
1163            'type'          => 'boolean',
1164            'description'   => __( 'Whether or not the post has already been shared.', 'jetpack-publicize-pkg' ),
1165            'single'        => true,
1166            'default'       => false,
1167            'show_in_rest'  => array(
1168                'name' => 'jetpack_social_post_already_shared',
1169            ),
1170            'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1171        );
1172
1173        $jetpack_social_options_args = array(
1174            'type'          => 'object',
1175            'description'   => __( 'Post options related to Jetpack Social.', 'jetpack-publicize-pkg' ),
1176            'single'        => true,
1177            'default'       => array(
1178                'image_generator_settings' => array(
1179                    'template'         => ( new Jetpack_Social_Settings\Settings() )->sig_get_default_template(),
1180                    'default_image_id' => ( new Jetpack_Social_Settings\Settings() )->sig_get_default_image_id(),
1181                    'font'             => ( new Jetpack_Social_Settings\Settings() )->sig_get_default_font(),
1182                    'enabled'          => false,
1183                ),
1184                'version'                  => 2,
1185            ),
1186            'show_in_rest'  => array(
1187                'name'   => 'jetpack_social_options',
1188                'schema' => array(
1189                    'type'       => 'object',
1190                    'properties' => array(
1191                        'version'                  => array(
1192                            'type' => 'number',
1193                        ),
1194                        'attached_media'           => array(
1195                            'type'  => 'array',
1196                            'items' => array(
1197                                'type'       => 'object',
1198                                'properties' => array(
1199                                    'id'   => array(
1200                                        'type' => 'number',
1201                                    ),
1202                                    'url'  => array(
1203                                        'type' => 'string',
1204                                    ),
1205                                    'type' => array(
1206                                        'type' => 'string',
1207                                    ),
1208                                ),
1209                            ),
1210                        ),
1211                        'image_generator_settings' => array(
1212                            'type'       => 'object',
1213                            'properties' => array(
1214                                'enabled'          => array(
1215                                    'type' => 'boolean',
1216                                ),
1217                                'custom_text'      => array(
1218                                    'type' => 'string',
1219                                ),
1220                                'image_type'       => array(
1221                                    'type' => 'string',
1222                                ),
1223                                'image_id'         => array(
1224                                    'type' => 'number',
1225                                ),
1226                                'template'         => array(
1227                                    'type' => 'string',
1228                                ),
1229                                'font'             => array(
1230                                    'type' => 'string',
1231                                ),
1232                                'token'            => array(
1233                                    'type' => 'string',
1234                                ),
1235                                'default_image_id' => array(
1236                                    'type' => 'number',
1237                                ),
1238                            ),
1239                        ),
1240                        'media_source'             => array(
1241                            'type' => 'string',
1242                            'enum' => array( 'featured-image', 'sig', 'media-library', 'upload-video', 'none' ),
1243                        ),
1244                    ),
1245                ),
1246            ),
1247            'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1248        );
1249
1250        $image_focal_point_args = array(
1251            'type'          => 'object',
1252            'description'   => __( 'The focal point of the image, used to crop social share variants.', 'jetpack-publicize-pkg' ),
1253            'single'        => true,
1254            'default'       => array(
1255                'x' => 0.5,
1256                'y' => 0.5,
1257            ),
1258            'show_in_rest'  => array(
1259                'schema' => array(
1260                    'type'                 => 'object',
1261                    'required'             => array( 'x', 'y' ),
1262                    'properties'           => array(
1263                        'x' => array(
1264                            'type'    => 'number',
1265                            'minimum' => 0,
1266                            'maximum' => 1,
1267                        ),
1268                        'y' => array(
1269                            'type'    => 'number',
1270                            'minimum' => 0,
1271                            'maximum' => 1,
1272                        ),
1273                    ),
1274                    'additionalProperties' => false,
1275                ),
1276            ),
1277            'auth_callback' => array( $this, 'image_focal_point_auth_callback' ),
1278        );
1279
1280        $connection_overrides_args = array(
1281            'type'          => 'object',
1282            'description'   => __( 'Per-connection customizations for message and media.', 'jetpack-publicize-pkg' ),
1283            'single'        => true,
1284            'default'       => array(),
1285            'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1286        );
1287
1288        $customize_per_network_default = (
1289            Current_Plan::supports( 'social-message-templates' )
1290            && $this->any_connection_has_custom_template()
1291        );
1292
1293        $customize_per_network_args = array(
1294            'type'          => 'boolean',
1295            'description'   => __( 'Whether to enable per-network customization.', 'jetpack-publicize-pkg' ),
1296            'single'        => true,
1297            'default'       => $customize_per_network_default,
1298            'show_in_rest'  => $this->has_paid_features(),
1299            'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1300        );
1301
1302        foreach ( get_post_types() as $post_type ) {
1303            if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1304                continue;
1305            }
1306
1307            $message_args['object_subtype']                  = $post_type;
1308            $publicize_feature_enable_args['object_subtype'] = $post_type;
1309            $already_shared_flag_args['object_subtype']      = $post_type;
1310            $jetpack_social_options_args['object_subtype']   = $post_type;
1311            $connection_overrides_args['object_subtype']     = $post_type;
1312            $customize_per_network_args['object_subtype']    = $post_type;
1313
1314            register_meta( 'post', $this->POST_MESS, $message_args );
1315            register_meta( 'post', self::POST_PUBLICIZE_FEATURE_ENABLED, $publicize_feature_enable_args );
1316            register_meta( 'post', $this->POST_DONE . 'all', $already_shared_flag_args );
1317            register_meta( 'post', self::POST_JETPACK_SOCIAL_OPTIONS, $jetpack_social_options_args );
1318            register_meta( 'post', self::POST_CONNECTION_OVERRIDES, $connection_overrides_args );
1319            register_meta( 'post', self::POST_CUSTOMIZE_PER_NETWORK, $customize_per_network_args );
1320        }
1321
1322        // The focal point lives on the image (attachment), not the post, so it is shared
1323        // by every post that uses the image. Registered once, not per publicizeable type.
1324        register_post_meta( 'attachment', self::ATTACHMENT_IMAGE_FOCAL_POINT, $image_focal_point_args );
1325    }
1326
1327    /**
1328     * Auth callback for the image focal point attachment meta.
1329     *
1330     * Writing the focal point edits the image, so it requires edit rights on the attachment.
1331     *
1332     * @param bool   $allowed   Whether the user can edit the meta. Unused; recomputed here.
1333     * @param string $meta_key  The meta key. Unused.
1334     * @param int    $object_id The attachment ID.
1335     * @return bool
1336     */
1337    public function image_focal_point_auth_callback( $allowed, $meta_key, $object_id ) {
1338        return current_user_can( 'edit_post', $object_id );
1339    }
1340
1341    /**
1342     * Whether any connection available to the current user has a custom message template.
1343     *
1344     * @return bool
1345     */
1346    protected function any_connection_has_custom_template() {
1347        foreach ( Connections::get_all_for_user() as $connection ) {
1348            if ( '' !== trim( (string) ( $connection['template'] ?? '' ) ) ) {
1349                return true;
1350            }
1351        }
1352
1353        return false;
1354    }
1355
1356    /**
1357     * Helper function to allow us to not publicize posts in certain contexts.
1358     *
1359     * @param WP_Post $post Post object.
1360     */
1361    public function should_submit_post_pre_checks( $post ) {
1362        $submit_post = true;
1363
1364        if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
1365            $submit_post = false;
1366        }
1367
1368        if (
1369            defined( 'DOING_AUTOSAVE' )
1370        &&
1371            DOING_AUTOSAVE
1372        ) {
1373            $submit_post = false;
1374        }
1375
1376        // To stop quick edits from getting publicized.
1377        if ( did_action( 'wp_ajax_inline-save' ) ) {
1378            $submit_post = false;
1379        }
1380
1381        // phpcs:disable WordPress.Security.NonceVerification.Recommended
1382        if ( ! empty( $_GET['bulk_edit'] ) ) {
1383            $submit_post = false;
1384        }
1385        // phpcs:enable WordPress.Security.NonceVerification.Recommended
1386
1387        // - API/XML-RPC Test Posts
1388        if (
1389            (
1390            ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
1391            ||
1392            ( defined( 'APP_REQUEST' ) && constant( 'APP_REQUEST' ) )
1393            )
1394        &&
1395            str_starts_with( $post->post_title, 'Temporary Post Used For Theme Detection' )
1396        ) {
1397            $submit_post = false;
1398        }
1399
1400        // Only work with certain statuses (avoids inherits, auto drafts etc).
1401        if ( ! in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true ) ) {
1402            $submit_post = false;
1403        }
1404
1405        // Don't publish password protected posts.
1406        if ( '' !== $post->post_password ) {
1407            $submit_post = false;
1408        }
1409
1410        return $submit_post;
1411    }
1412
1413    /**
1414     * Fires when a post is saved, checks conditions and saves state in postmeta so that it
1415     * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
1416     *
1417     * Attached to the `save_post` action.
1418     *
1419     * @param int     $post_id Post ID.
1420     * @param WP_Post $post Post object.
1421     */
1422    public function save_meta( $post_id, $post ) {
1423        $cron_user   = null;
1424        $submit_post = true;
1425
1426        if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
1427            return;
1428        }
1429
1430        $submit_post = $this->should_submit_post_pre_checks( $post );
1431
1432        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We're only checking if a value is set
1433        $admin_page = $_POST[ $this->ADMIN_PAGE ] ?? null;
1434
1435        // Did this request happen via wp-admin?
1436        $from_web = isset( $_SERVER['REQUEST_METHOD'] )
1437            &&
1438            'post' === strtolower( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) )
1439            &&
1440            ! empty( $admin_page );
1441
1442        // phpcs:ignore WordPress.Security.NonceVerification.Missing
1443        $title = isset( $_POST['wpas_title'] ) ? sanitize_textarea_field( wp_unslash( $_POST['wpas_title'] ) ) : null;
1444
1445        if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && ! empty( $title ) ) {
1446            update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $title ) ) );
1447        }
1448
1449        // Change current user to provide context for get_services() if we're running during cron.
1450        if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1451            $cron_user = (int) $GLOBALS['user_ID'];
1452            wp_set_current_user( $post->post_author );
1453        }
1454
1455        /**
1456         * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
1457         * it will Publicize to everything *except* those marked for skipping.
1458         */
1459        foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1460            foreach ( $connections as $connection ) {
1461                $connection_data = '';
1462                if ( is_object( $connection ) && method_exists( $connection, 'get_meta' ) ) {
1463                    $connection_data = $connection->get_meta( 'connection_data' );
1464                } elseif ( ! empty( $connection['connection_data'] ) ) {
1465                    $connection_data = $connection['connection_data'];
1466                }
1467
1468                /** This action is documented in modules/publicize/ui.php */
1469                /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */
1470                if ( false === apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
1471                    delete_post_meta( $post_id, $this->PENDING );
1472                    continue;
1473                }
1474
1475                // This was a wp-admin request, so we need to check the state of checkboxes.
1476                if ( $from_web ) {
1477                    $connection_id = $this->get_connection_id( $connection );
1478                    // Delete stray service-based post meta.
1479                    delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
1480
1481                    // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition.
1482                    if ( empty( $admin_page['submit'][ $connection_id ] ) ) {
1483                        // Also make sure that the service-specific input isn't there.
1484                        // If the user connected to a new service 'in-page' then a hidden field with the service
1485                        // name is added, so we just assume they wanted to Publicize to that service.
1486                        if ( empty( $admin_page['submit'][ $service_name ] ) ) {
1487                            // Nothing seems to be checked, so we're going to mark this one to be skipped.
1488                            update_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id, 1 );
1489                            continue;
1490                        } else {
1491                            // Clean up any stray post meta.
1492                            delete_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id );
1493                        }
1494                    } else {
1495                        // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it.
1496                        delete_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id );
1497                    }
1498                }
1499
1500                /**
1501                 * Fires right before the post is processed for Publicize.
1502                 * Users may hook in here and do anything else they need to after meta is written,
1503                 * and before the post is processed for Publicize.
1504                 *
1505                 * @since 0.1.0
1506                 * @since-jetpack 2.1.2
1507                 *
1508                 * @param bool $submit_post Should the post be publicized.
1509                 * @param int $post->ID Post ID.
1510                 * @param string $service_name Service name.
1511                 * @param array $connection Array of connection details.
1512                 */
1513                do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
1514            }
1515        }
1516
1517        if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1518            wp_set_current_user( $cron_user );
1519        }
1520
1521        // Next up will be ::publicize_post().
1522    }
1523
1524    /**
1525     * Alters the "Post Published" message to include information about where the post
1526     * was Publicized to.
1527     *
1528     * Attached to the `post_updated_messages` filter
1529     *
1530     * @param string[] $messages Array of messages.
1531     * @return string[]
1532     */
1533    public function update_published_message( $messages ) {
1534        global $post_type, $post_type_object, $post;
1535        if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1536            return $messages;
1537        }
1538
1539        // Bail early if the post is private.
1540        if ( 'publish' !== $post->post_status ) {
1541            return $messages;
1542        }
1543
1544        $view_post_link_html = '';
1545        $viewable            = is_post_type_viewable( $post_type_object );
1546        if ( $viewable ) {
1547            /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain, WordPress.Utils.I18nTextDomainFixer.MissingArgDomain */
1548            $view_text = esc_html__( 'View post' ); // Intentionally omitted domain.
1549
1550            if ( 'jetpack-portfolio' === $post_type ) {
1551                $view_text = esc_html__( 'View project', 'jetpack-publicize-pkg' );
1552            }
1553
1554            $view_post_link_html = sprintf(
1555                ' <a href="%1$s">%2$s</a>',
1556                esc_url( get_permalink( $post ) ),
1557                $view_text
1558            );
1559        }
1560
1561        $services = $this->get_publicizing_services( $post->ID );
1562        if ( empty( $services ) ) {
1563            return $messages;
1564        }
1565
1566        $labels = array();
1567        foreach ( $services as $service_name => $display_names ) {
1568            $labels[] = sprintf(
1569                /* translators: Service name is %1$s, and account name is %2$s. */
1570                esc_html__( '%1$s (%2$s)', 'jetpack-publicize-pkg' ),
1571                esc_html( $service_name ),
1572                esc_html( is_array( $display_names ) ? implode( ', ', $display_names ) : $display_names )
1573            );
1574        }
1575
1576        $messages['post'][6] = sprintf(
1577            /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1578            esc_html__( 'Post published and sharing on %1$s.', 'jetpack-publicize-pkg' ),
1579            implode( ', ', $labels )
1580        ) . $view_post_link_html;
1581
1582        if ( 'post' === $post_type && class_exists( 'Jetpack_Subscriptions' ) ) {
1583            $subscription = \Jetpack_Subscriptions::init();
1584            if ( $subscription->should_email_post_to_subscribers( $post ) ) {
1585                $messages['post'][6] = sprintf(
1586                    /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1587                    esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack-publicize-pkg' ),
1588                    implode( ', ', $labels )
1589                ) . $view_post_link_html;
1590            }
1591        }
1592
1593        $messages['jetpack-portfolio'][6] = sprintf(
1594            /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1595            esc_html__( 'Project published and sharing project on %1$s.', 'jetpack-publicize-pkg' ),
1596            implode( ', ', $labels )
1597        ) . $view_post_link_html;
1598
1599        return $messages;
1600    }
1601
1602    /**
1603     * Get the Connections the Post was just Publicized to.
1604     *
1605     * Only reliable just after the Post was published.
1606     *
1607     * @param int $post_id Post ID.
1608     * @return string[] Array of Service display name => Connection display name
1609     */
1610    public function get_publicizing_services( $post_id ) {
1611        $services = array();
1612
1613        foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1614            // services have multiple connections.
1615            foreach ( $connections as $connection ) {
1616                $connection_id = $this->get_connection_id( $connection );
1617                // Did we skip this connection?
1618                if ( get_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id, true ) ) {
1619                    continue;
1620                }
1621                $services[ static::get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
1622            }
1623        }
1624
1625        return $services;
1626    }
1627
1628    /**
1629     * Is the post Publicize-able?
1630     *
1631     * Only valid prior to Publicizing a Post.
1632     *
1633     * @param WP_Post $post Post to check.
1634     * @return bool
1635     */
1636    public function post_is_publicizeable( $post ) {
1637        if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
1638            return false;
1639        }
1640
1641        // This is more a precaution. To only publicize posts that are published. (Mostly relevant for Jetpack sites).
1642        if ( 'publish' !== $post->post_status ) {
1643            return false;
1644        }
1645
1646        // If it's not flagged as ready, then abort. @see ::flag_post_for_publicize().
1647        if ( ! get_post_meta( $post->ID, $this->PENDING, true ) ) {
1648            return false;
1649        }
1650
1651        return true;
1652    }
1653
1654    /**
1655     * Is a given post type Publicize-able?
1656     *
1657     * Not every CPT lends itself to Publicize-ation.  Allow CPTs to register by adding their CPT via
1658     * the publicize_post_types array filter.
1659     *
1660     * @param string $post_type The post type to check.
1661     * @return bool True if the post type can be Publicized.
1662     */
1663    public function post_type_is_publicizeable( $post_type ) {
1664        if ( 'post' === $post_type ) {
1665            return true;
1666        }
1667
1668        return post_type_supports( $post_type, 'publicize' );
1669    }
1670
1671    /**
1672     * Already-published posts should not be Publicized by default. This filter sets checked to
1673     * false if a post has already been published.
1674     *
1675     * Attached to the `publicize_checkbox_default` filter
1676     *
1677     * @param bool $checked True if checkbox is checked, false otherwise.
1678     * @param int  $post_id Post ID to set checkbox for.
1679     * @return bool
1680     */
1681    public function publicize_checkbox_default( $checked, $post_id ) {
1682        if ( 'publish' === get_post_status( $post_id ) ) {
1683            return false;
1684        }
1685
1686        return $checked;
1687    }
1688
1689    /**
1690     * Get the attached image for a post.
1691     *
1692     * @param int $post_id ID of the post to get attached media for.
1693     * @return array {
1694     *     Array of image data, or empty array if no image is available.
1695     *
1696     *     @type string $url Image source URL.
1697     *     @type int    $width Image width in pixels.
1698     *     @type int    $height Image height in pixels.
1699     * }
1700     */
1701    public function get_attached_media_image( $post_id ) {
1702        $options = get_post_meta( $post_id, self::POST_JETPACK_SOCIAL_OPTIONS, true );
1703
1704        if ( ! is_array( $options ) || empty( $options['attached_media'] ) || empty( $options['attached_media'][0]['id'] ) ) {
1705            return array();
1706        }
1707
1708        $media_id = $options['attached_media'][0]['id'];
1709
1710        if ( ! wp_attachment_is_image( $media_id ) ) {
1711            return array();
1712        }
1713
1714        $image = wp_get_attachment_image_src( $media_id, array( 1200, 1200 ) );
1715
1716        if ( ! $image ) {
1717            return array();
1718        }
1719
1720        return array(
1721            'url'    => $image[0],
1722            'width'  => $image[1],
1723            'height' => $image[2],
1724        );
1725    }
1726
1727    /**
1728     * Get the OpenGraph image for a post.
1729     *
1730     * @param int $post_id ID of the post to get OpenGraph image for.
1731     * @return array {
1732     *     Array of image data, or empty array if no image is available.
1733     *
1734     *     @type string $url Image source URL.
1735     *     @type int    $width Image width in pixels.
1736     *     @type int    $height Image height in pixels.
1737     * }
1738     */
1739    public function get_social_opengraph_image( $post_id ) {
1740        $generated_image_url = Social_Image_Generator\get_image_url( $post_id );
1741
1742        if ( ! empty( $generated_image_url ) ) {
1743            return array(
1744                'url'    => $generated_image_url,
1745                'width'  => 1200,
1746                'height' => 630,
1747            );
1748        }
1749
1750        $attached_media = $this->get_attached_media_image( $post_id );
1751
1752        if ( $attached_media ) {
1753            return $attached_media;
1754        }
1755
1756        $featured_image_id = get_post_thumbnail_id( $post_id );
1757
1758        if ( $featured_image_id && Current_Plan::supports( 'social-image-focal-point' ) ) {
1759            $featured_image = Focal_Point::get_cropped_image( $featured_image_id );
1760
1761            if ( $featured_image ) {
1762                return $featured_image;
1763            }
1764        }
1765
1766        return array();
1767    }
1768
1769    /**
1770     * Returns the image size in bytes of a remote image.
1771     *
1772     * @deprecated 0.66.3
1773     *
1774     * @param  string $image_url       Image URL.
1775     * @return integer|null $bytes      Image size in bytes, or null if request failed.
1776     */
1777    public function get_remote_filesize( $image_url ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1778        _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1779
1780        return null;
1781    }
1782
1783    /**
1784     * Returns the resized Photon URL for a given image.
1785     *
1786     * @deprecated 0.66.3
1787     *
1788     * @param string $image_url Image URL.
1789     * @param int    $width Image width.
1790     * @param int    $height Image height.
1791     * @return string
1792     */
1793    public function get_resized_image_url( $image_url, $width, $height ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1794        _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1795
1796        return $image_url;
1797    }
1798
1799    /**
1800     * This function runs the image through Site Accelerator to compress it, and also scales it down if needed.
1801     *
1802     * @deprecated 0.66.3
1803     *
1804     * @param string $url Image URL.
1805     * @param int    $width Image width.
1806     * @param int    $height Image height.
1807     * @return array The compressed image data.
1808     */
1809    public function compress_and_scale_og_image( $url, $width, $height ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1810        _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1811
1812        return array();
1813    }
1814
1815    /**
1816     * Reduce the filesize of an image by reducing the dimensions. Uses Photon.
1817     * Returns null if the image cannot be reduced enough.
1818     *
1819     * @deprecated 0.66.3
1820     *
1821     * @param string $url Image URL.
1822     * @param int    $width Image width.
1823     * @param int    $height Image height.
1824     * @param int    $filesize Image filesize.
1825     * @param int    $tries Number of times to try reducing the image size. Default is 5.
1826     * @return array|null
1827     */
1828    public function reduce_file_size( $url, $width, $height, $filesize, $tries = 5 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1829        _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1830
1831        return null;
1832    }
1833
1834    /**
1835     * Hooks into jetpack_open_graph_tags to add the Jetpack Social images to the OpenGraph tags,
1836     * or to make the Jetpack open graph images pass restrictions.
1837     *
1838     * @deprecated 0.66.3 use add_jetpack_social_og_images instead.
1839     *
1840     * @param array $tags Current tags.
1841     *
1842     * @return array The modified tags.
1843     */
1844    public function jetpack_social_open_graph_filter( $tags ) {
1845        _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1846        return $tags;
1847    }
1848
1849    /**
1850     * Add the Jetpack Social images (attached media, SIG image) to the OpenGraph tags.
1851     *
1852     * @deprecated 0.66.3 use add_jetpack_social_og_images instead.
1853     *
1854     * @param array $tags Current tags.
1855     * @param array $opengraph_image The Jetpack Social image data.
1856     *
1857     * @return array The modified tags.
1858     */
1859    public function add_jetpack_social_og_image( $tags, $opengraph_image ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1860        _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1861
1862        return $tags;
1863    }
1864
1865    /**
1866     * Add the Jetpack Social images (attached media, SIG image) to the OpenGraph tags.
1867     *
1868     * @param array $tags Current tags.
1869     */
1870    public function add_jetpack_social_og_images( $tags ) {
1871        if ( ! is_singular() ) {
1872            return $tags;
1873        }
1874
1875        $post_id = get_the_ID();
1876        if ( ! $post_id ) {
1877            return $tags;
1878        }
1879
1880        if ( ! post_type_supports( get_post_type( $post_id ), 'publicize' ) ) {
1881            return $tags;
1882        }
1883
1884        $opengraph_image = $this->get_social_opengraph_image( $post_id );
1885        if ( empty( $opengraph_image ) ) {
1886            return $tags;
1887        }
1888
1889        // If this code is running in Jetpack, we need to add Twitter cards.
1890        // Some active plugins disable Jetpack's Twitter Cards, so we need
1891        // to check if the class was instantiated before adding the cards.
1892        $needs_twitter_cards = class_exists( \Automattic\Jetpack\Post_Media\Twitter_Cards::class );
1893
1894        return array_merge(
1895            $tags,
1896            array(
1897                'og:image'        => $opengraph_image['url'],
1898                'og:image:width'  => $opengraph_image['width'],
1899                'og:image:height' => $opengraph_image['height'],
1900            ),
1901            $needs_twitter_cards ? array(
1902                'twitter:image' => $opengraph_image['url'],
1903                'twitter:card'  => 'summary_large_image',
1904            ) : array()
1905        );
1906    }
1907
1908    /**
1909     * Util
1910     */
1911
1912    /**
1913     * Converts a Publicize message template string into a sprintf format string
1914     *
1915     * @param string[] $args Array of arguments.
1916     *               0 - The Publicize message template: 'Check out my post: %title% @ %url'
1917     *             ... - The template tags 'title', 'url', etc.
1918     * @return string
1919     */
1920    protected static function build_sprintf( $args ) {
1921        $string  = null;
1922        $search  = array();
1923        $replace = array();
1924        foreach ( $args as $k => $arg ) {
1925            if ( 0 === $k ) {
1926                $string = $arg;
1927                continue;
1928            }
1929            $search[]  = "%$arg%";
1930            $replace[] = "%$k\$s";
1931        }
1932        return str_replace( $search, $replace, $string );
1933    }
1934
1935    /**
1936     * Get the URL to the connections management page.
1937     *
1938     * @return string
1939     */
1940    public function publicize_connections_url() {
1941        $has_social_admin_page = defined( 'JETPACK_SOCIAL_PLUGIN_DIR' );
1942
1943        $page = $has_social_admin_page ? 'jetpack-social' : 'jetpack#/sharing';
1944
1945        return ( new Paths() )->admin_url( array( 'page' => $page ) );
1946    }
1947
1948    /**
1949     * Get the Jetpack Social info from the API.
1950     *
1951     * @param int $blog_id The WPCOM blog_id for the current blog.
1952     * @return array
1953     */
1954    public function get_api_data( $blog_id ) {
1955        static $api_data_response = null;
1956        $key                      = 'jetpack_social_api_data';
1957
1958        if ( isset( $api_data_response ) ) {
1959            return ! is_wp_error( $api_data_response ) ? $api_data_response : array();
1960        }
1961
1962        $rest_controller   = new REST_Controller();
1963        $response          = Client::wpcom_json_api_request_as_blog(
1964            sprintf( 'sites/%d/jetpack-social', absint( $blog_id ) ),
1965            '2',
1966            array(
1967                'headers' => array( 'content-type' => 'application/json' ),
1968                'method'  => 'GET',
1969            ),
1970            null,
1971            'wpcom'
1972        );
1973        $api_data_response = $rest_controller->make_proper_response( $response );
1974
1975        if ( ! is_wp_error( $api_data_response ) ) {
1976            set_transient( $key, $api_data_response, DAY_IN_SECONDS );
1977            return $api_data_response;
1978        }
1979
1980        $cached_response = get_transient( $key );
1981        if ( ! empty( $cached_response ) ) {
1982            return $cached_response;
1983        }
1984
1985        return array();
1986    }
1987
1988    /**
1989     * Check if enhanced publishing is enabled.
1990     *
1991     * @deprecated $$next-version use Automattic\Jetpack\Publicize\Publicize_Base\has_enhanced_publishing_feature instead.
1992     * @param int $blog_id The blog ID for the current blog.
1993     * @return bool
1994     */
1995    public function is_enhanced_publishing_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1996        return $this->has_enhanced_publishing_feature();
1997    }
1998
1999    /**
2000     * Check if the enhanced publishing feature is enabled.
2001     *
2002     * @return bool
2003     */
2004    public function has_enhanced_publishing_feature() {
2005        return Current_Plan::supports( 'social-enhanced-publishing' );
2006    }
2007
2008    /**
2009     * Check if the social image generator is enabled.
2010     *
2011     * @deprecated 0.24.2 use Automattic\Jetpack\Publicize\Publicize_Base\has_social_image_generator_feature instead.
2012     * @param int $blog_id The blog ID for the current blog.
2013     * @return bool
2014     */
2015    public function is_social_image_generator_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2016        return $this->has_social_image_generator_feature();
2017    }
2018
2019    /**
2020     * Check if the social image generator is enabled.
2021     *
2022     * @return bool
2023     */
2024    public function has_social_image_generator_feature() {
2025        return Current_Plan::supports( 'social-image-generator' );
2026    }
2027
2028    /**
2029     * Check if the auto-conversion feature is one of the active features.
2030     *
2031     * TODO: Remove this after certain releases of Jetpack v15.
2032     *
2033     * @param string $type Whether image or video.
2034     *
2035     * @return bool
2036     */
2037    public function has_social_auto_conversion_feature( $type ) {
2038        return Current_Plan::supports( "social-$type-auto-convert" );
2039    }
2040
2041    /**
2042     * Check if a connection is enabled.
2043     *
2044     * @param string $connection The connection name like 'instagram', 'mastodon', 'nextdoor' etc.
2045     *
2046     * @return bool
2047     */
2048    public function has_connection_feature( $connection ) {
2049        return Current_Plan::supports( "social-$connection-connection" );
2050    }
2051
2052    /**
2053     * Check if the new connections management is enabled is enabled.
2054     *
2055     * @return bool
2056     */
2057    public function has_connections_management_feature() {
2058        return Current_Plan::supports( 'social-connections-management' );
2059    }
2060
2061    /**
2062     * Get a list of additional connections that are supported by the current plan.
2063     *
2064     * @return array
2065     */
2066    public function get_supported_additional_connections() {
2067        $additional_connections = array();
2068
2069        if ( $this->has_connection_feature( 'threads' ) ) {
2070            $additional_connections[] = 'threads';
2071        }
2072
2073        return $additional_connections;
2074    }
2075
2076    /**
2077     * Check if we have a paid Jetpack Social plan.
2078     *
2079     * @param bool $refresh_from_wpcom Whether to force refresh the plan check.
2080     *
2081     * @return bool True if we have a paid plan, false otherwise.
2082     */
2083    public function has_paid_plan( $refresh_from_wpcom = false ) {
2084        static $has_paid_plan = null;
2085        if ( null === $has_paid_plan ) {
2086            $has_paid_plan = Current_Plan::supports( 'social-shares-1000', $refresh_from_wpcom );
2087        }
2088        return $has_paid_plan;
2089    }
2090
2091    /**
2092     * Check if we have paid features enabled.
2093     *
2094     * @return bool True if we have paid features, false otherwise.
2095     */
2096    public function has_paid_features() {
2097        return $this->has_enhanced_publishing_feature();
2098    }
2099
2100    /**
2101     * Get an array with all dismissed notices.
2102     *
2103     * @return array
2104     */
2105    public function get_dismissed_notices() {
2106        $dismissed_notices = get_option( self::OPTION_JETPACK_SOCIAL_DISMISSED_NOTICES );
2107
2108        if ( ! is_array( $dismissed_notices ) ) {
2109            return array();
2110        }
2111
2112        return $dismissed_notices;
2113    }
2114
2115    /**
2116     * Whether the current user can manage a connection.
2117     *
2118     * @param array $connection_data The connection data.
2119     *
2120     * @return bool
2121     */
2122    public static function can_manage_connection( $connection_data ) {
2123        return current_user_can( 'edit_others_posts' ) || get_current_user_id() === (int) $connection_data['user_id'];
2124    }
2125}
2126
2127// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
2128
2129/**
2130 * Get Calypso URL for Publicize connections.
2131 *
2132 * @return string
2133 */
2134function publicize_calypso_url() {
2135    _deprecated_function( __METHOD__, '0.2.0', 'Publicize::publicize_connections_url' );
2136    return Redirect::get_url( 'calypso-marketing-connections', array( 'site' => ( new Status() )->get_site_suffix() ) );
2137}
2138
2139/**
2140 * Adds support for the post-thumbnails feature, regardless of underlying theme support.
2141 *
2142 * This ensures the featured image UI appears in the editor, allowing the user to
2143 * explicitly set an image for their social media post.
2144 */
2145function add_theme_post_thumbnails_support() {
2146    add_theme_support( 'post-thumbnails', get_post_types_by_support( 'publicize' ) );
2147}