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