PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / class-publicize-base.php

class-publicize-base.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-base.php

2,147 lines 67.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Publicize_Base class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8 // phpcs:disable WordPress.NamingConventions.ValidVariableName
9
10 namespace Automattic\Jetpack\Publicize;
11
12 use Automattic\Jetpack\Connection\Client;
13 use Automattic\Jetpack\Current_Plan;
14 use Automattic\Jetpack\Paths;
15 use Automattic\Jetpack\Redirect;
16 use Automattic\Jetpack\Status;
17 use WP_Error;
18 use WP_Post;
19
20 /**
21 * Base class for Publicize.
22 */
23 abstract 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 = $this->has_paid_features()
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 $this->has_paid_features()
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 $response = Client::wpcom_json_api_request_as_blog(
1963 sprintf( 'sites/%d/jetpack-social', absint( $blog_id ) ),
1964 '2',
1965 array(
1966 'headers' => array( 'content-type' => 'application/json' ),
1967 'method' => 'GET',
1968 ),
1969 null,
1970 'wpcom'
1971 );
1972 $api_data_response = Publicize_Utils::make_proper_response( $response );
1973
1974 if ( ! is_wp_error( $api_data_response ) ) {
1975 set_transient( $key, $api_data_response, DAY_IN_SECONDS );
1976 return $api_data_response;
1977 }
1978
1979 $cached_response = get_transient( $key );
1980 if ( ! empty( $cached_response ) ) {
1981 return $cached_response;
1982 }
1983
1984 return array();
1985 }
1986
1987 /**
1988 * Check if enhanced publishing is enabled.
1989 *
1990 * @deprecated $$next-version use Automattic\Jetpack\Publicize\Publicize_Base\has_enhanced_publishing_feature instead.
1991 * @param int $blog_id The blog ID for the current blog.
1992 * @return bool
1993 */
1994 public function is_enhanced_publishing_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1995 return $this->has_enhanced_publishing_feature();
1996 }
1997
1998 /**
1999 * Check if the enhanced publishing feature is enabled.
2000 *
2001 * @return bool
2002 */
2003 public function has_enhanced_publishing_feature() {
2004 return Current_Plan::supports( 'social-enhanced-publishing' );
2005 }
2006
2007 /**
2008 * Check if the social image generator is enabled.
2009 *
2010 * @deprecated 0.24.2 use Automattic\Jetpack\Publicize\Publicize_Base\has_social_image_generator_feature instead.
2011 * @param int $blog_id The blog ID for the current blog.
2012 * @return bool
2013 */
2014 public function is_social_image_generator_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2015 return $this->has_social_image_generator_feature();
2016 }
2017
2018 /**
2019 * Check if the social image generator is enabled.
2020 *
2021 * @return bool
2022 */
2023 public function has_social_image_generator_feature() {
2024 return Current_Plan::supports( 'social-image-generator' );
2025 }
2026
2027 /**
2028 * Check if the auto-conversion feature is one of the active features.
2029 *
2030 * TODO: Remove this after certain releases of Jetpack v15.
2031 *
2032 * @param string $type Whether image or video.
2033 *
2034 * @return bool
2035 */
2036 public function has_social_auto_conversion_feature( $type ) {
2037 return Current_Plan::supports( "social-$type-auto-convert" );
2038 }
2039
2040 /**
2041 * Check if a connection is enabled.
2042 *
2043 * @param string $connection The connection name like 'instagram', 'mastodon', 'nextdoor' etc.
2044 *
2045 * @return bool
2046 */
2047 public function has_connection_feature( $connection ) {
2048 return Current_Plan::supports( "social-$connection-connection" );
2049 }
2050
2051 /**
2052 * Check if the new connections management is enabled is enabled.
2053 *
2054 * @return bool
2055 */
2056 public function has_connections_management_feature() {
2057 return Current_Plan::supports( 'social-connections-management' );
2058 }
2059
2060 /**
2061 * Get a list of additional connections that are supported by the current plan.
2062 *
2063 * @return array
2064 */
2065 public function get_supported_additional_connections() {
2066 $additional_connections = array();
2067
2068 if ( $this->has_connection_feature( 'threads' ) ) {
2069 $additional_connections[] = 'threads';
2070 }
2071
2072 return $additional_connections;
2073 }
2074
2075 /**
2076 * Check if we have a paid Jetpack Social plan.
2077 *
2078 * @param bool $refresh_from_wpcom Whether to force refresh the plan check.
2079 *
2080 * @return bool True if we have a paid plan, false otherwise.
2081 */
2082 public function has_paid_plan( $refresh_from_wpcom = false ) {
2083 static $has_paid_plan = null;
2084 if ( null === $has_paid_plan ) {
2085 $has_paid_plan = Current_Plan::supports( 'social-shares-1000', $refresh_from_wpcom );
2086 }
2087 return $has_paid_plan;
2088 }
2089
2090 /**
2091 * Check if we have paid features enabled.
2092 *
2093 * @return bool True if we have paid features, false otherwise.
2094 */
2095 public function has_paid_features() {
2096 return $this->has_enhanced_publishing_feature();
2097 }
2098
2099 /**
2100 * Get an array with all dismissed notices.
2101 *
2102 * @return array
2103 */
2104 public function get_dismissed_notices() {
2105 $dismissed_notices = get_option( self::OPTION_JETPACK_SOCIAL_DISMISSED_NOTICES );
2106
2107 if ( ! is_array( $dismissed_notices ) ) {
2108 return array();
2109 }
2110
2111 return $dismissed_notices;
2112 }
2113
2114 /**
2115 * Whether the current user can manage a connection.
2116 *
2117 * @param array $connection_data The connection data.
2118 *
2119 * @return bool
2120 */
2121 public static function can_manage_connection( $connection_data ) {
2122 return current_user_can( 'edit_others_posts' ) || get_current_user_id() === (int) $connection_data['user_id'];
2123 }
2124 }
2125
2126 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
2127
2128 /**
2129 * Get Calypso URL for Publicize connections.
2130 *
2131 * @return string
2132 */
2133 function publicize_calypso_url() {
2134 _deprecated_function( __METHOD__, '0.2.0', 'Publicize::publicize_connections_url' );
2135 return Redirect::get_url( 'calypso-marketing-connections', array( 'site' => ( new Status() )->get_site_suffix() ) );
2136 }
2137
2138 /**
2139 * Adds support for the post-thumbnails feature, regardless of underlying theme support.
2140 *
2141 * This ensures the featured image UI appears in the editor, allowing the user to
2142 * explicitly set an image for their social media post.
2143 */
2144 function add_theme_post_thumbnails_support() {
2145 add_theme_support( 'post-thumbnails', get_post_types_by_support( 'publicize' ) );
2146 }
2147