PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 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 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-publicize/src/class-publicize-base.php +738 -225 12.0.3 → 16.3 View file →
@@ -10,10 +10,13 @@
10 10 namespace Automattic\Jetpack\Publicize;
11 11
12 12 use Automattic\Jetpack\Connection\Client;
13 13 use Automattic\Jetpack\Current_Plan;
14 +use Automattic\Jetpack\Paths;
14 15 use Automattic\Jetpack\Redirect;
15 16 use Automattic\Jetpack\Status;
17 +use WP_Error;
18 +use WP_Post;
16 19
17 20 /**
18 21 * Base class for Publicize.
19 22 */
@@ -49,37 +52,65 @@
49 52 */
50 53 public $POST_MESS = '_wpas_mess';
51 54
52 55 /**
53 - * Post meta key for flagging when the post is a tweetstorm.
56 + * Post meta key for the flagging when the post share feature is disabled.
54 57 *
55 58 * @var string
56 59 */
57 - public $POST_TWEETSTORM = '_wpas_is_tweetstorm';
60 + const POST_PUBLICIZE_FEATURE_ENABLED = '_wpas_feature_enabled';
58 61
59 62 /**
60 - * Post meta key for the flagging when the post share feature is disabled.
63 + * Post meta key for Jetpack Social options.
61 64 *
62 65 * @var string
63 66 */
64 - const POST_PUBLICIZE_FEATURE_ENABLED = '_wpas_feature_enabled';
67 + const POST_JETPACK_SOCIAL_OPTIONS = '_wpas_options';
65 68
66 69 /**
67 - * Post meta key for Jetpack Social options.
70 + * Post meta key for per-connection customization overrides.
68 71 *
69 72 * @var string
70 73 */
71 - const POST_JETPACK_SOCIAL_OPTIONS = '_wpas_options';
74 + const POST_CONNECTION_OVERRIDES = '_wpas_connection_overrides';
72 75
73 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 + /**
74 104 * Connection ID appended to indicate that a connection should NOT be publicized to.
75 105 *
76 106 * @var string
77 107 */
78 - public $POST_SKIP = '_wpas_skip_';
108 + public $POST_SKIP_PUBLICIZE = '_wpas_skip_publicize_';
79 109
110 + // Done meta keys.
80 111 /**
81 - * Connection ID appended to indicate a connection has already been publicized to.
112 + * Token ID appended to indicate a connection has already been publicized to.
82 113 *
83 114 * @var string
84 115 */
85 116 public $POST_DONE = '_wpas_done_';
@@ -112,8 +143,22 @@
112 143 */
113 144 public $POST_SERVICE_DONE = '_publicize_done_external';
114 145
115 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 + /**
116 161 * Default pieces of the message used in constructing the
117 162 * content pushed out to other social networks.
118 163 */
119 164
@@ -216,19 +261,20 @@
216 261 add_action( 'save_post', array( $this, 'save_meta' ), 20, 2 );
217 262
218 263 // Default checkbox state for each Connection.
219 264 add_filter( 'publicize_checkbox_default', array( $this, 'publicize_checkbox_default' ), 10, 2 );
220 - add_filter( 'jetpack_open_graph_tags', array( $this, 'get_sig_image_for_post' ), 10, 1 );
221 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 +
222 269 // Alter the "Post Publish" admin notice to mention the Connections we Publicized to.
223 270 add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 20, 1 );
224 271
225 - // Connection test callback.
226 - add_action( 'wp_ajax_test_publicize_conns', array( $this, 'test_publicize_conns' ) );
227 -
228 272 // Custom priority to ensure post type support is added prior to thumbnail support being added to the theme.
229 273 add_action( 'init', array( $this, 'add_post_type_support' ), 8 );
230 274 add_action( 'init', array( $this, 'register_post_meta' ), 20 );
275 + // Last, so a veto from another filter is respected before any row changes.
276 + add_filter( 'update_post_metadata', array( $this, 'collapse_duplicate_post_meta' ), PHP_INT_MAX, 5 );
231 277
232 278 // The custom priority for this action ensures that any existing code that
233 279 // removes post-thumbnails support during 'init' continues to work.
234 280 add_action( 'init', __NAMESPACE__ . '\add_theme_post_thumbnails_support', 8 );
@@ -252,8 +298,23 @@
252 298 */
253 299 abstract public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false );
254 300
255 301 /**
302 + * Whether the site has the feature flag enabled.
303 + *
304 + * @deprecated 0.69.1 Use Current_Plan::supports() directly instead.
305 + *
306 + * @todo Remove this method After March 2026.
307 + *
308 + * @param string $flag_name The feature flag to check. Will be prefixed with 'jetpack_social_has_' for the option.
309 + * @param string $feature_name The feature name to check for for the Current_Plan check, without the social- prefix.
310 + * @return bool
311 + */
312 + public function has_feature_flag( $flag_name, $feature_name ): bool {
313 + return Publicize_Script_Data::has_feature_flag( $feature_name );
314 + }
315 +
316 + /**
256 317 * Does the given user have a connection to the service on the given blog?
257 318 *
258 319 * @param string $service_name 'facebook', 'twitter', etc.
259 320 * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
@@ -269,9 +330,9 @@
269 330 $_user_id = $this->user_id();
270 331 }
271 332
272 333 $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
273 - return ( is_array( $connections ) && count( $connections ) > 0 ? true : false );
334 + return is_array( $connections ) && count( $connections ) > 0;
274 335 }
275 336
276 337 /**
277 338 * Generates a connection URL.
@@ -319,8 +380,10 @@
319 380 return 'LinkedIn';
320 381 case 'google_drive': // google-drive used to be called google_drive.
321 382 case 'google-drive':
322 383 return 'Google Drive';
384 + case 'instagram-business':
385 + return 'Instagram';
323 386 case 'twitter':
324 387 case 'facebook':
325 388 case 'tumblr':
326 389 default:
@@ -430,9 +493,9 @@
430 493 public function get_profile_link( $service_name, $connection ) {
431 494 $cmeta = $this->get_connection_meta( $connection );
432 495
433 496 if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
434 - if ( 'facebook' === $service_name && 0 === strpos( wp_parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
497 + if ( 'facebook' === $service_name && str_starts_with( wp_parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
435 498 // App-scoped Facebook user IDs are not usable profile links.
436 499 return false;
437 500 }
438 501
@@ -442,8 +505,37 @@
442 505 if ( 'facebook' === $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
443 506 return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
444 507 }
445 508
509 + if ( 'instagram-business' === $service_name && isset( $cmeta['connection_data']['meta']['username'] ) ) {
510 + return 'https://instagram.com/' . $cmeta['connection_data']['meta']['username'];
511 + }
512 +
513 + if ( 'linkedin' === $service_name ) {
514 +
515 + $entity_type = $cmeta['connection_data']['meta']['entity_type'] ?? 'person';
516 +
517 + if ( 'organization' === $entity_type && ! empty( $cmeta['connection_data']['meta']['external_name'] ) ) {
518 + return 'https://www.linkedin.com/company/' . $cmeta['connection_data']['meta']['external_name'];
519 + }
520 +
521 + if ( 'person' === $entity_type && ! empty( $cmeta['external_name'] ) ) {
522 + return 'https://www.linkedin.com/in/' . $cmeta['external_name'];
523 + }
524 + }
525 +
526 + if ( 'threads' === $service_name && isset( $cmeta['external_name'] ) ) {
527 + return 'https://www.threads.net/@' . $cmeta['external_name'];
528 + }
529 +
530 + if ( 'mastodon' === $service_name && isset( $cmeta['external_name'] ) ) {
531 + return 'https://mastodon.social/@' . $cmeta['external_name'];
532 + }
533 +
534 + if ( 'nextdoor' === $service_name && isset( $cmeta['external_id'] ) ) {
535 + return 'https://nextdoor.com/profile/' . $cmeta['external_id'];
536 + }
537 +
446 538 if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
447 539 return 'https://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
448 540 }
449 541
@@ -450,28 +542,10 @@
450 542 if ( 'twitter' === $service_name ) {
451 543 return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'.
452 544 }
453 545
454 - if ( 'linkedin' === $service_name ) {
455 - if ( ! isset( $cmeta['connection_data']['meta']['profile_url'] ) ) {
456 - return false;
457 - }
458 -
459 - $profile_url_query = wp_parse_url( $cmeta['connection_data']['meta']['profile_url'], PHP_URL_QUERY );
460 - $profile_url_query_args = null;
461 - wp_parse_str( $profile_url_query, $profile_url_query_args );
462 -
463 - $id = null;
464 -
465 - if ( isset( $profile_url_query_args['key'] ) ) {
466 - $id = $profile_url_query_args['key'];
467 - } elseif ( isset( $profile_url_query_args['id'] ) ) {
468 - $id = $profile_url_query_args['id'];
469 - } else {
470 - return false;
471 - }
472 -
473 - return esc_url_raw( add_query_arg( 'id', rawurlencode( $id ), 'https://www.linkedin.com/profile/view' ) );
546 + if ( 'bluesky' === $service_name ) {
547 + return 'https://bsky.app/profile/' . $cmeta['external_id'];
474 548 }
475 549
476 550 return false; // no fallback. we just won't link it.
477 551 }
@@ -485,8 +559,12 @@
485 559 */
486 560 public function get_display_name( $service_name, $connection ) {
487 561 $cmeta = $this->get_connection_meta( $connection );
488 562
563 + if ( 'mastodon' === $service_name && isset( $cmeta['external_display'] ) ) {
564 + return $cmeta['external_display'];
565 + }
566 +
489 567 if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
490 568 return $cmeta['connection_data']['meta']['display_name'];
491 569 }
492 570
@@ -507,14 +585,53 @@
507 585 return $connection_display;
508 586 }
509 587
510 588 /**
589 + * Returns the account name for the Connection. For services like Instagram, we need both the username and the account's name.
590 + *
591 + * @param string $service_name 'facebook', 'linkedin', etc.
592 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
593 + * @return string
594 + */
595 + public function get_username( $service_name, $connection ) {
596 + $handle = $this->get_external_handle( $service_name, $connection );
597 +
598 + return $handle ?? $this->get_display_name( $service_name, $connection );
599 + }
600 +
601 + /**
602 + * Returns the external handle for the Connection.
603 + *
604 + * @param string $service_name 'facebook', 'linkedin', etc.
605 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
606 + * @return string|null
607 + */
608 + public function get_external_handle( $service_name, $connection ) {
609 + $cmeta = $this->get_connection_meta( $connection );
610 +
611 + switch ( $service_name ) {
612 + case 'mastodon':
613 + return $cmeta['external_display'] ?? null;
614 +
615 + case 'bluesky':
616 + case 'threads':
617 + return $cmeta['external_name'] ?? null;
618 +
619 + case 'instagram-business':
620 + return $cmeta['connection_data']['meta']['username'] ?? null;
621 +
622 + default:
623 + return null;
624 + }
625 + }
626 +
627 + /**
511 628 * Returns a profile picture for the Connection
512 629 *
513 630 * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
514 631 * @return string
515 632 */
516 - private function get_profile_picture( $connection ) {
633 + public function get_profile_picture( $connection ) {
517 634 $cmeta = $this->get_connection_meta( $connection );
518 635
519 636 if ( isset( $cmeta['profile_picture'] ) ) {
520 637 return $cmeta['profile_picture'];
@@ -618,14 +735,20 @@
618 735 return isset( $connection_data['meta']['options_responses'] );
619 736 }
620 737
621 738 /**
622 - * AJAX Handler to run connection tests on all Connections
739 + * Parse the error code returned by the XML-RPC API call.
623 740 *
624 - * @return void
741 + * @param string $code Error code in numerical format.
742 + *
743 + * @return string Error code.
625 744 */
626 - public function test_publicize_conns() {
627 - wp_send_json_success( $this->get_publicize_conns_test_results() );
745 + public function parse_connection_error_code( $code ) {
746 + if ( 1 === $code ) {
747 + return 'unsupported';
748 + }
749 +
750 + return 'broken';
628 751 }
629 752
630 753 /**
631 754 * Run connection tests on all Connections
@@ -650,13 +773,14 @@
650 773 foreach ( $connections as $connection ) {
651 774
652 775 $id = $this->get_connection_id( $connection );
653 776
654 - $connection_test_passed = true;
655 - $connection_test_message = __( 'This connection is working correctly.', 'jetpack-publicize-pkg' );
656 - $user_can_refresh = false;
657 - $refresh_text = '';
658 - $refresh_url = '';
777 + $connection_test_error_code = '';
778 + $connection_test_passed = true;
779 + $connection_test_message = __( 'This connection is working correctly.', 'jetpack-publicize-pkg' );
780 + $user_can_refresh = false;
781 + $refresh_text = '';
782 + $refresh_url = '';
659 783
660 784 $connection_test_result = true;
661 785 if ( method_exists( $this, 'test_connection' ) ) {
662 786 $connection_test_result = $this->test_connection( $service_name, $connection );
@@ -665,12 +789,16 @@
665 789 if ( is_wp_error( $connection_test_result ) ) {
666 790 $connection_test_passed = false;
667 791 $connection_test_message = $connection_test_result->get_error_message();
668 792 $error_data = $connection_test_result->get_error_data();
793 + $error_code = $connection_test_result->get_error_code();
669 794
670 - $user_can_refresh = $error_data['user_can_refresh'];
671 - $refresh_text = $error_data['refresh_text'];
672 - $refresh_url = $error_data['refresh_url'];
795 + if ( ! empty( $error_data ) ) {
796 + $user_can_refresh = $error_data['user_can_refresh'];
797 + $refresh_text = $error_data['refresh_text'];
798 + $refresh_url = $error_data['refresh_url'];
799 + }
800 + $connection_test_error_code = $connection_test_passed ? '' : $this->parse_connection_error_code( $error_code );
673 801 }
674 802 // Mark Facebook profiles as deprecated.
675 803 if ( 'facebook' === $service_name ) {
676 804 if ( ! $this->is_valid_facebook_connection( $connection ) ) {
@@ -695,16 +823,17 @@
695 823 $unique_id = $connection['connection_data']['token_id'];
696 824 }
697 825
698 826 $test_results[] = array(
699 - 'connectionID' => $id,
700 - 'serviceName' => $service_name,
701 - 'connectionTestPassed' => $connection_test_passed,
702 - 'connectionTestMessage' => esc_attr( $connection_test_message ),
703 - 'userCanRefresh' => $user_can_refresh,
704 - 'refreshText' => esc_attr( $refresh_text ),
705 - 'refreshURL' => $refresh_url,
706 - 'unique_id' => $unique_id,
827 + 'connectionID' => $id,
828 + 'serviceName' => $service_name,
829 + 'connectionTestPassed' => $connection_test_passed,
830 + 'connectionTestErrorCode' => $connection_test_error_code,
831 + 'connectionTestMessage' => esc_attr( $connection_test_message ),
832 + 'userCanRefresh' => $user_can_refresh,
833 + 'refreshText' => esc_attr( $refresh_text ),
834 + 'refreshURL' => $refresh_url,
835 + 'unique_id' => $unique_id,
707 836 );
708 837 }
709 838 }
710 839
@@ -745,8 +874,9 @@
745 874 * @type bool 'enabled' Default value for the connection (e.g., for a checkbox).
746 875 * @type bool 'done' Has this connection already been publicized to?
747 876 * @type bool 'toggleable' Is the user allowed to change the value for the connection?
748 877 * @type bool 'global' Is this connection a global one?
878 + * @type string 'external_id' External ID for the connection.
749 879 * }
750 880 */
751 881 public function get_filtered_connection_data( $selected_post_id = null ) {
752 882 $connection_list = array();
@@ -758,24 +888,22 @@
758 888 } else {
759 889 $post_id = null;
760 890 }
761 891
762 - $services = $this->get_services( 'connected' );
763 - $all_done = $this->post_is_done_sharing( $post_id );
764 -
765 892 // We don't allow Publicizing to the same external id twice, to prevent spam.
766 893 $service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true );
767 894
768 - foreach ( $services as $service_name => $connections ) {
769 - foreach ( $connections as $connection ) {
770 - $connection_meta = $this->get_connection_meta( $connection );
771 - $connection_data = $connection_meta['connection_data'];
895 + $connections = Connections::get_all_for_user();
772 896
773 - $unique_id = $this->get_connection_unique_id( $connection );
897 + if ( ! empty( $connections ) ) {
774 898
899 + foreach ( $connections as $connection ) {
900 + $service_name = $connection['service_name'];
901 + $unique_id = $connection['id'];
902 + $connection_id = $connection['connection_id'];
775 903 // Was this connection (OR, old-format service) already Publicized to?
776 904 $done = ! empty( $post ) && (
777 - // New flags.
905 + // Flags based on token_id.
778 906 1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true )
779 907 ||
780 908 // Old flags.
781 909 1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $service_name, true )
@@ -789,12 +917,12 @@
789 917 *
790 918 * @param bool true Should the post be publicized to a given service? Default to true.
791 919 * @param int $post_id Post ID.
792 920 * @param string $service_name Service name.
793 - * @param array $connection_data Array of information about all Publicize details for the site.
921 + * @param array $connection The connection data.
794 922 */
795 923 /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */
796 - if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection_data ) ) {
924 + if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection ) ) {
797 925 continue;
798 926 }
799 927
800 928 // Should we be skipping this one?
@@ -804,26 +932,24 @@
804 932 &&
805 933 in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true )
806 934 &&
807 935 (
808 - // New flags.
936 + // Flags based on token_id.
809 937 get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true )
810 938 ||
939 + // Flags based on the connection_id.
940 + get_post_meta( $post->ID, $this->POST_SKIP_PUBLICIZE . $connection_id, true )
941 + ||
811 942 // Old flags.
812 - get_post_meta( $post->ID, $this->POST_SKIP . $service_name )
943 + get_post_meta( $post->ID, $this->POST_SKIP . $service_name, false )
813 944 )
814 945 )
815 946 ||
816 947 (
817 - is_array( $connection )
818 - &&
819 - isset( $connection_meta['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection_meta['external_id'] ] )
948 + isset( $connection['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection['external_id'] ] )
820 949 )
821 950 );
822 951
823 - // If this one has already been publicized to, don't let it happen again.
824 - $toggleable = ! $done && ! $all_done;
825 -
826 952 // Determine the state of the checkbox (on/off) and allow filtering.
827 953 $enabled = $done || ! $skip;
828 954 /**
829 955 * Filter the checkbox state of each Publicize connection appearing in the post editor.
@@ -838,14 +964,11 @@
838 964 */
839 965 $enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection );
840 966
841 967 /**
842 - * If this is a global connection and this user doesn't have enough permissions to modify
843 - * those connections, don't let them change it.
968 + * If this is a shared connection and this user doesn't have enough permissions to modify.
844 969 */
845 - if ( ! $done && $this->is_global_connection( $connection_meta ) && ! current_user_can( $this->GLOBAL_CAP ) ) {
846 - $toggleable = false;
847 -
970 + if ( ! $done && Connections::is_shared( $connection ) && ! current_user_can( $this->GLOBAL_CAP ) ) {
848 971 /**
849 972 * Filters the checkboxes for global connections with non-prilvedged users.
850 973 *
851 974 * @since 0.1.0
@@ -863,18 +986,14 @@
863 986 if ( $done ) {
864 987 $enabled = true;
865 988 }
866 989
867 - $connection_list[] = array(
868 - 'unique_id' => $unique_id,
869 - 'service_name' => $service_name,
870 - 'service_label' => $this->get_service_label( $service_name ),
871 - 'display_name' => $this->get_display_name( $service_name, $connection ),
872 - 'profile_picture' => $this->get_profile_picture( $connection ),
873 - 'enabled' => $enabled,
874 - 'done' => $done,
875 - 'toggleable' => $toggleable,
876 - 'global' => 0 == $connection_data['user_id'], // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual,WordPress.PHP.StrictComparisons.LooseComparison -- Other types can be used at times.
990 + $connection_list[] = array_merge(
991 + $connection,
992 + array(
993 + 'enabled' => $enabled,
994 + 'done' => $done,
995 + )
877 996 );
878 997 }
879 998 }
880 999
@@ -893,37 +1012,20 @@
893 1012 */
894 1013 abstract public function post_is_done_sharing( $post_id = null );
895 1014
896 1015 /**
897 - * Retrieves full list of available Publicize connection services.
1016 + * Retrieve the available Publicize services and their connect URLs.
898 1017 *
899 - * Retrieves current available publicize service connections
900 - * with associated labels and URLs.
901 - *
902 1018 * @since 0.1.0
903 1019 * @since-jetpack 6.7.0
1020 + * @deprecated 0.88.0 Services and their connect URLs come from the `publicize/services` REST endpoint.
904 1021 *
905 - * @return array {
906 - * Array of UI service connection data for all services
907 - *
908 - * @type string 'name' Name of service.
909 - * @type string 'label' Display label for service.
910 - * @type string 'url' URL for adding connection to service.
911 - * }
1022 + * @return array Always empty.
912 1023 */
913 1024 public function get_available_service_data() {
914 - $available_services = $this->get_services( 'all' );
915 - $available_service_data = array();
1025 + _deprecated_function( __METHOD__, 'publicize-0.88.0' );
916 1026
917 - foreach ( $available_services as $service_name => $service ) {
918 - $available_service_data[] = array(
919 - 'name' => $service_name,
920 - 'label' => $this->get_service_label( $service_name ),
921 - 'url' => $this->connect_url( $service_name ),
922 - );
923 - }
924 -
925 - return $available_service_data;
1027 + return array();
926 1028 }
927 1029
928 1030 /**
929 1031 * Site Data
@@ -1012,13 +1114,20 @@
1012 1114 *
1013 1115 * Registers for each post type that with `publicize` feature support.
1014 1116 */
1015 1117 public function register_post_meta() {
1118 + /*
1119 + * Default the share-message meta to the saved global template
1120 + */
1121 + $message_default = $this->has_paid_features()
1122 + ? ( new Jetpack_Social_Settings\Settings() )->get_message_template()
1123 + : '';
1124 +
1016 1125 $message_args = array(
1017 1126 'type' => 'string',
1018 1127 'description' => __( 'The message to use instead of the title when sharing to Jetpack Social services', 'jetpack-publicize-pkg' ),
1019 1128 'single' => true,
1020 - 'default' => '',
1129 + 'default' => $message_default,
1021 1130 'show_in_rest' => array(
1022 1131 'name' => 'jetpack_publicize_message',
1023 1132 ),
1024 1133 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
@@ -1023,19 +1132,8 @@
1023 1132 ),
1024 1133 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1025 1134 );
1026 1135
1027 - $tweetstorm_args = array(
1028 - 'type' => 'boolean',
1029 - 'description' => __( 'Whether or not the post should be treated as a Twitter thread.', 'jetpack-publicize-pkg' ),
1030 - 'single' => true,
1031 - 'default' => false,
1032 - 'show_in_rest' => array(
1033 - 'name' => 'jetpack_is_tweetstorm',
1034 - ),
1035 - 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1036 - );
1037 -
1038 1136 $publicize_feature_enable_args = array(
1039 1137 'type' => 'boolean',
1040 1138 'description' => __( 'Whether or not the Share Post feature is enabled.', 'jetpack-publicize-pkg' ),
1041 1139 'single' => true,
@@ -1062,11 +1160,14 @@
1062 1160 'description' => __( 'Post options related to Jetpack Social.', 'jetpack-publicize-pkg' ),
1063 1161 'single' => true,
1064 1162 'default' => array(
1065 1163 'image_generator_settings' => array(
1066 - 'template' => ( new Social_Image_Generator\Settings() )->get_default_template(),
1067 - 'enabled' => false,
1164 + 'template' => ( new Jetpack_Social_Settings\Settings() )->sig_get_default_template(),
1165 + 'default_image_id' => ( new Jetpack_Social_Settings\Settings() )->sig_get_default_image_id(),
1166 + 'font' => ( new Jetpack_Social_Settings\Settings() )->sig_get_default_font(),
1167 + 'enabled' => false,
1068 1168 ),
1169 + 'version' => 2,
1069 1170 ),
1070 1171 'show_in_rest' => array(
1071 1172 'name' => 'jetpack_social_options',
1072 1173 'schema' => array(
@@ -1071,19 +1172,25 @@
1071 1172 'name' => 'jetpack_social_options',
1072 1173 'schema' => array(
1073 1174 'type' => 'object',
1074 1175 'properties' => array(
1176 + 'version' => array(
1177 + 'type' => 'number',
1178 + ),
1075 1179 'attached_media' => array(
1076 1180 'type' => 'array',
1077 1181 'items' => array(
1078 1182 'type' => 'object',
1079 1183 'properties' => array(
1080 - 'id' => array(
1184 + 'id' => array(
1081 1185 'type' => 'number',
1082 1186 ),
1083 - 'url' => array(
1187 + 'url' => array(
1084 1188 'type' => 'string',
1085 1189 ),
1190 + 'type' => array(
1191 + 'type' => 'string',
1192 + ),
1086 1193 ),
1087 1194 ),
1088 1195 ),
1089 1196 'image_generator_settings' => array(
@@ -1088,28 +1195,38 @@
1088 1195 ),
1089 1196 'image_generator_settings' => array(
1090 1197 'type' => 'object',
1091 1198 'properties' => array(
1092 - 'enabled' => array(
1199 + 'enabled' => array(
1093 1200 'type' => 'boolean',
1094 1201 ),
1095 - 'custom_text' => array(
1202 + 'custom_text' => array(
1096 1203 'type' => 'string',
1097 1204 ),
1098 - 'image_type' => array(
1205 + 'image_type' => array(
1099 1206 'type' => 'string',
1100 1207 ),
1101 - 'image_id' => array(
1208 + 'image_id' => array(
1102 1209 'type' => 'number',
1103 1210 ),
1104 - 'template' => array(
1211 + 'template' => array(
1105 1212 'type' => 'string',
1106 1213 ),
1107 - 'token' => array(
1214 + 'font' => array(
1108 1215 'type' => 'string',
1109 1216 ),
1217 + 'token' => array(
1218 + 'type' => 'string',
1219 + ),
1220 + 'default_image_id' => array(
1221 + 'type' => 'number',
1222 + ),
1110 1223 ),
1111 1224 ),
1225 + 'media_source' => array(
1226 + 'type' => 'string',
1227 + 'enum' => array( 'featured-image', 'sig', 'media-library', 'upload-video', 'none' ),
1228 + ),
1112 1229 ),
1113 1230 ),
1114 1231 ),
1115 1232 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
@@ -1114,8 +1231,60 @@
1114 1231 ),
1115 1232 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1116 1233 );
1117 1234
1235 + $image_focal_point_args = array(
1236 + 'type' => 'object',
1237 + 'description' => __( 'The focal point of the image, used to crop social share variants.', 'jetpack-publicize-pkg' ),
1238 + 'single' => true,
1239 + 'default' => array(
1240 + 'x' => 0.5,
1241 + 'y' => 0.5,
1242 + ),
1243 + 'show_in_rest' => array(
1244 + 'schema' => array(
1245 + 'type' => 'object',
1246 + 'required' => array( 'x', 'y' ),
1247 + 'properties' => array(
1248 + 'x' => array(
1249 + 'type' => 'number',
1250 + 'minimum' => 0,
1251 + 'maximum' => 1,
1252 + ),
1253 + 'y' => array(
1254 + 'type' => 'number',
1255 + 'minimum' => 0,
1256 + 'maximum' => 1,
1257 + ),
1258 + ),
1259 + 'additionalProperties' => false,
1260 + ),
1261 + ),
1262 + 'auth_callback' => array( $this, 'image_focal_point_auth_callback' ),
1263 + );
1264 +
1265 + $connection_overrides_args = array(
1266 + 'type' => 'object',
1267 + 'description' => __( 'Per-connection customizations for message and media.', 'jetpack-publicize-pkg' ),
1268 + 'single' => true,
1269 + 'default' => array(),
1270 + 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1271 + );
1272 +
1273 + $customize_per_network_default = (
1274 + $this->has_paid_features()
1275 + && $this->any_connection_has_custom_template()
1276 + );
1277 +
1278 + $customize_per_network_args = array(
1279 + 'type' => 'boolean',
1280 + 'description' => __( 'Whether to enable per-network customization.', 'jetpack-publicize-pkg' ),
1281 + 'single' => true,
1282 + 'default' => $customize_per_network_default,
1283 + 'show_in_rest' => $this->has_paid_features(),
1284 + 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1285 + );
1286 +
1118 1287 foreach ( get_post_types() as $post_type ) {
1119 1288 if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1120 1289 continue;
1121 1290 }
@@ -1120,22 +1289,123 @@
1120 1289 continue;
1121 1290 }
1122 1291
1123 1292 $message_args['object_subtype'] = $post_type;
1124 - $tweetstorm_args['object_subtype'] = $post_type;
1125 1293 $publicize_feature_enable_args['object_subtype'] = $post_type;
1126 1294 $already_shared_flag_args['object_subtype'] = $post_type;
1127 1295 $jetpack_social_options_args['object_subtype'] = $post_type;
1296 + $connection_overrides_args['object_subtype'] = $post_type;
1297 + $customize_per_network_args['object_subtype'] = $post_type;
1128 1298
1129 1299 register_meta( 'post', $this->POST_MESS, $message_args );
1130 - register_meta( 'post', $this->POST_TWEETSTORM, $tweetstorm_args );
1131 1300 register_meta( 'post', self::POST_PUBLICIZE_FEATURE_ENABLED, $publicize_feature_enable_args );
1132 1301 register_meta( 'post', $this->POST_DONE . 'all', $already_shared_flag_args );
1133 1302 register_meta( 'post', self::POST_JETPACK_SOCIAL_OPTIONS, $jetpack_social_options_args );
1303 + register_meta( 'post', self::POST_CONNECTION_OVERRIDES, $connection_overrides_args );
1304 + register_meta( 'post', self::POST_CUSTOMIZE_PER_NETWORK, $customize_per_network_args );
1134 1305 }
1306 +
1307 + // The focal point lives on the image (attachment), not the post, so it is shared
1308 + // by every post that uses the image. Registered once, not per publicizeable type.
1309 + register_post_meta( 'attachment', self::ATTACHMENT_IMAGE_FOCAL_POINT, $image_focal_point_args );
1135 1310 }
1136 1311
1137 1312 /**
1313 + * Collapse duplicate rows of a single-value Publicize post meta key into one on update.
1314 + *
1315 + * With several rows core skips its "unchanged" check, so an UPDATE that matches no changed row fails.
1316 + *
1317 + * @param null|bool $check Whether to short-circuit the update.
1318 + * @param int $object_id Post ID.
1319 + * @param string $meta_key Meta key.
1320 + * @param mixed $meta_value Unslashed meta value.
1321 + * @param mixed $prev_value Previous value to match, if any.
1322 + * @return null|bool Null to let core update the meta, otherwise whether the update succeeded.
1323 + */
1324 + public function collapse_duplicate_post_meta( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
1325 + // Cheap bail first: this runs on every post meta update site-wide.
1326 + if ( null !== $check || ! empty( $prev_value ) || 0 !== strncmp( $meta_key, '_wpas_', 6 ) ) {
1327 + return $check;
1328 + }
1329 +
1330 + $single_keys = array(
1331 + $this->POST_MESS,
1332 + self::POST_PUBLICIZE_FEATURE_ENABLED,
1333 + $this->POST_DONE . 'all',
1334 + self::POST_JETPACK_SOCIAL_OPTIONS,
1335 + self::POST_CONNECTION_OVERRIDES,
1336 + self::POST_CUSTOMIZE_PER_NETWORK,
1337 + );
1338 + if ( ! in_array( $meta_key, $single_keys, true ) ) {
1339 + return $check;
1340 + }
1341 +
1342 + $stored = get_metadata_raw( 'post', $object_id, $meta_key, false );
1343 + if ( ! is_array( $stored ) || count( $stored ) < 2 ) {
1344 + return $check;
1345 + }
1346 +
1347 + global $wpdb;
1348 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Row IDs aren't exposed by the meta API.
1349 + $rows = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s ORDER BY meta_id ASC", $object_id, $meta_key ) );
1350 + if ( ! is_array( $rows ) || count( $rows ) < 2 ) {
1351 + return $check;
1352 + }
1353 +
1354 + // Bail before deleting, so a failed write leaves every row intact.
1355 + $keep = array_shift( $rows );
1356 + // Cast like `$wpdb` does, e.g. `true` is stored as "1".
1357 + $wanted = (string) maybe_serialize( $meta_value );
1358 + if ( $wanted !== $keep->meta_value && ! update_metadata_by_mid( 'post', (int) $keep->meta_id, $meta_value ) ) {
1359 + // A 0-row UPDATE (e.g. a concurrent collapse) looks like a failure, so confirm the row.
1360 + $current = get_metadata_by_mid( 'post', (int) $keep->meta_id );
1361 + if ( ! $current || $wanted !== (string) maybe_serialize( $current->meta_value ) ) {
1362 + return false;
1363 + }
1364 + }
1365 +
1366 + // The kept row holds the value, so a redundant row that is already gone is not a failure.
1367 + foreach ( $rows as $row ) {
1368 + delete_metadata_by_mid( 'post', (int) $row->meta_id );
1369 + }
1370 +
1371 + // The 0-row and already-deleted paths skip core's cache flush.
1372 + wp_cache_delete( $object_id, 'post_meta' );
1373 +
1374 + // Unlike core, report an unchanged value as success, which is what lets the REST save complete.
1375 + return true;
1376 + }
1377 +
1378 + /**
1379 + * Auth callback for the image focal point attachment meta.
1380 + *
1381 + * Writing the focal point edits the image, so it requires edit rights on the attachment.
1382 + *
1383 + * @param bool $allowed Whether the user can edit the meta. Unused; recomputed here.
1384 + * @param string $meta_key The meta key. Unused.
1385 + * @param int $object_id The attachment ID.
1386 + * @return bool
1387 + */
1388 + public function image_focal_point_auth_callback( $allowed, $meta_key, $object_id ) {
1389 + return current_user_can( 'edit_post', $object_id );
1390 + }
1391 +
1392 + /**
1393 + * Whether any connection available to the current user has a custom message template.
1394 + *
1395 + * @return bool
1396 + */
1397 + protected function any_connection_has_custom_template() {
1398 + foreach ( Connections::get_all_for_user() as $connection ) {
1399 + if ( '' !== trim( (string) ( $connection['template'] ?? '' ) ) ) {
1400 + return true;
1401 + }
1402 + }
1403 +
1404 + return false;
1405 + }
1406 +
1407 + /**
1138 1408 * Helper function to allow us to not publicize posts in certain contexts.
1139 1409 *
1140 1410 * @param WP_Post $post Post object.
1141 1411 */
@@ -1167,18 +1437,14 @@
1167 1437
1168 1438 // - API/XML-RPC Test Posts
1169 1439 if (
1170 1440 (
1171 - defined( 'XMLRPC_REQUEST' )
1172 - &&
1173 - XMLRPC_REQUEST
1441 + ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
1174 1442 ||
1175 - defined( 'APP_REQUEST' )
1176 - &&
1177 - APP_REQUEST
1443 + ( defined( 'APP_REQUEST' ) && constant( 'APP_REQUEST' ) )
1178 1444 )
1179 1445 &&
1180 - 0 === strpos( $post->post_title, 'Temporary Post Used For Theme Detection' )
1446 + str_starts_with( $post->post_title, 'Temporary Post Used For Theme Detection' )
1181 1447 ) {
1182 1448 $submit_post = false;
1183 1449 }
1184 1450
@@ -1214,9 +1480,9 @@
1214 1480
1215 1481 $submit_post = $this->should_submit_post_pre_checks( $post );
1216 1482
1217 1483 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We're only checking if a value is set
1218 - $admin_page = isset( $_POST[ $this->ADMIN_PAGE ] ) ? $_POST[ $this->ADMIN_PAGE ] : null;
1484 + $admin_page = $_POST[ $this->ADMIN_PAGE ] ?? null;
1219 1485
1220 1486 // Did this request happen via wp-admin?
1221 1487 $from_web = isset( $_SERVER['REQUEST_METHOD'] )
1222 1488 &&
@@ -1226,14 +1492,10 @@
1226 1492
1227 1493 // phpcs:ignore WordPress.Security.NonceVerification.Missing
1228 1494 $title = isset( $_POST['wpas_title'] ) ? sanitize_textarea_field( wp_unslash( $_POST['wpas_title'] ) ) : null;
1229 1495
1230 - if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && $title ) {
1231 - if ( empty( $title ) ) {
1232 - delete_post_meta( $post_id, $this->POST_MESS );
1233 - } else {
1234 - update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $title ) ) );
1235 - }
1496 + if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && ! empty( $title ) ) {
1497 + update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $title ) ) );
1236 1498 }
1237 1499
1238 1500 // Change current user to provide context for get_services() if we're running during cron.
1239 1501 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
@@ -1260,35 +1522,30 @@
1260 1522 delete_post_meta( $post_id, $this->PENDING );
1261 1523 continue;
1262 1524 }
1263 1525
1264 - if ( ! empty( $connection->unique_id ) ) {
1265 - $unique_id = $connection->unique_id;
1266 - } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) {
1267 - $unique_id = $connection['connection_data']['token_id'];
1268 - }
1269 -
1270 1526 // This was a wp-admin request, so we need to check the state of checkboxes.
1271 1527 if ( $from_web ) {
1528 + $connection_id = $this->get_connection_id( $connection );
1272 1529 // Delete stray service-based post meta.
1273 1530 delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
1274 1531
1275 1532 // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition.
1276 - if ( empty( $admin_page['submit'][ $unique_id ] ) ) {
1533 + if ( empty( $admin_page['submit'][ $connection_id ] ) ) {
1277 1534 // Also make sure that the service-specific input isn't there.
1278 1535 // If the user connected to a new service 'in-page' then a hidden field with the service
1279 1536 // name is added, so we just assume they wanted to Publicize to that service.
1280 1537 if ( empty( $admin_page['submit'][ $service_name ] ) ) {
1281 1538 // Nothing seems to be checked, so we're going to mark this one to be skipped.
1282 - update_post_meta( $post_id, $this->POST_SKIP . $unique_id, 1 );
1539 + update_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id, 1 );
1283 1540 continue;
1284 1541 } else {
1285 1542 // Clean up any stray post meta.
1286 - delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
1543 + delete_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id );
1287 1544 }
1288 1545 } else {
1289 1546 // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it.
1290 - delete_post_meta( $post_id, $this->POST_SKIP . $unique_id );
1547 + delete_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id );
1291 1548 }
1292 1549 }
1293 1550
1294 1551 /**
@@ -1367,9 +1624,9 @@
1367 1624 );
1368 1625 }
1369 1626
1370 1627 $messages['post'][6] = sprintf(
1371 - /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1628 + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1372 1629 esc_html__( 'Post published and sharing on %1$s.', 'jetpack-publicize-pkg' ),
1373 1630 implode( ', ', $labels )
1374 1631 ) . $view_post_link_html;
1375 1632
@@ -1376,9 +1633,9 @@
1376 1633 if ( 'post' === $post_type && class_exists( 'Jetpack_Subscriptions' ) ) {
1377 1634 $subscription = \Jetpack_Subscriptions::init();
1378 1635 if ( $subscription->should_email_post_to_subscribers( $post ) ) {
1379 1636 $messages['post'][6] = sprintf(
1380 - /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1637 + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1381 1638 esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack-publicize-pkg' ),
1382 1639 implode( ', ', $labels )
1383 1640 ) . $view_post_link_html;
1384 1641 }
@@ -1384,9 +1641,9 @@
1384 1641 }
1385 1642 }
1386 1643
1387 1644 $messages['jetpack-portfolio'][6] = sprintf(
1388 - /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack), Twitter (@jetpack) */
1645 + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1389 1646 esc_html__( 'Project published and sharing project on %1$s.', 'jetpack-publicize-pkg' ),
1390 1647 implode( ', ', $labels )
1391 1648 ) . $view_post_link_html;
1392 1649
@@ -1406,20 +1663,14 @@
1406 1663
1407 1664 foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1408 1665 // services have multiple connections.
1409 1666 foreach ( $connections as $connection ) {
1410 - $unique_id = '';
1411 - if ( ! empty( $connection->unique_id ) ) {
1412 - $unique_id = $connection->unique_id;
1413 - } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) {
1414 - $unique_id = $connection['connection_data']['token_id'];
1415 - }
1416 -
1667 + $connection_id = $this->get_connection_id( $connection );
1417 1668 // Did we skip this connection?
1418 - if ( get_post_meta( $post_id, $this->POST_SKIP . $unique_id, true ) ) {
1669 + if ( get_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id, true ) ) {
1419 1670 continue;
1420 1671 }
1421 - $services[ $this->get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
1672 + $services[ static::get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
1422 1673 }
1423 1674 }
1424 1675
1425 1676 return $services;
@@ -1486,28 +1737,227 @@
1486 1737 return $checked;
1487 1738 }
1488 1739
1489 1740 /**
1490 - * Adds the sig image to the meta tags array.
1741 + * Get the attached image for a post.
1491 1742 *
1492 - * @param array $tags Current tags.
1743 + * @param int $post_id ID of the post to get attached media for.
1744 + * @return array {
1745 + * Array of image data, or empty array if no image is available.
1746 + *
1747 + * @type string $url Image source URL.
1748 + * @type int $width Image width in pixels.
1749 + * @type int $height Image height in pixels.
1750 + * }
1493 1751 */
1494 - public function get_sig_image_for_post( $tags ) {
1495 - $generated_image_url = Social_Image_Generator\get_image_url( get_the_ID() );
1752 + public function get_attached_media_image( $post_id ) {
1753 + $options = get_post_meta( $post_id, self::POST_JETPACK_SOCIAL_OPTIONS, true );
1754 +
1755 + if ( ! is_array( $options ) || empty( $options['attached_media'] ) || empty( $options['attached_media'][0]['id'] ) ) {
1756 + return array();
1757 + }
1758 +
1759 + $media_id = $options['attached_media'][0]['id'];
1760 +
1761 + if ( ! wp_attachment_is_image( $media_id ) ) {
1762 + return array();
1763 + }
1764 +
1765 + $image = wp_get_attachment_image_src( $media_id, array( 1200, 1200 ) );
1766 +
1767 + if ( ! $image ) {
1768 + return array();
1769 + }
1770 +
1771 + return array(
1772 + 'url' => $image[0],
1773 + 'width' => $image[1],
1774 + 'height' => $image[2],
1775 + );
1776 + }
1777 +
1778 + /**
1779 + * Get the OpenGraph image for a post.
1780 + *
1781 + * @param int $post_id ID of the post to get OpenGraph image for.
1782 + * @return array {
1783 + * Array of image data, or empty array if no image is available.
1784 + *
1785 + * @type string $url Image source URL.
1786 + * @type int $width Image width in pixels.
1787 + * @type int $height Image height in pixels.
1788 + * }
1789 + */
1790 + public function get_social_opengraph_image( $post_id ) {
1791 + $generated_image_url = Social_Image_Generator\get_image_url( $post_id );
1792 +
1496 1793 if ( ! empty( $generated_image_url ) ) {
1497 - $tags = array_merge(
1498 - $tags,
1499 - array(
1500 - 'og:image' => $generated_image_url,
1501 - 'og:image:width' => 1200,
1502 - 'og:image:height' => 630,
1503 - )
1794 + return array(
1795 + 'url' => $generated_image_url,
1796 + 'width' => 1200,
1797 + 'height' => 630,
1504 1798 );
1505 1799 }
1800 +
1801 + $attached_media = $this->get_attached_media_image( $post_id );
1802 +
1803 + if ( $attached_media ) {
1804 + return $attached_media;
1805 + }
1806 +
1807 + $featured_image_id = get_post_thumbnail_id( $post_id );
1808 +
1809 + if ( $featured_image_id && Current_Plan::supports( 'social-image-focal-point' ) ) {
1810 + $featured_image = Focal_Point::get_cropped_image( $featured_image_id );
1811 +
1812 + if ( $featured_image ) {
1813 + return $featured_image;
1814 + }
1815 + }
1816 +
1817 + return array();
1818 + }
1819 +
1820 + /**
1821 + * Returns the image size in bytes of a remote image.
1822 + *
1823 + * @deprecated 0.66.3
1824 + *
1825 + * @param string $image_url Image URL.
1826 + * @return integer|null $bytes Image size in bytes, or null if request failed.
1827 + */
1828 + public function get_remote_filesize( $image_url ) { // 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 + * Returns the resized Photon URL for a given image.
1836 + *
1837 + * @deprecated 0.66.3
1838 + *
1839 + * @param string $image_url Image URL.
1840 + * @param int $width Image width.
1841 + * @param int $height Image height.
1842 + * @return string
1843 + */
1844 + public function get_resized_image_url( $image_url, $width, $height ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1845 + _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1846 +
1847 + return $image_url;
1848 + }
1849 +
1850 + /**
1851 + * This function runs the image through Site Accelerator to compress it, and also scales it down if needed.
1852 + *
1853 + * @deprecated 0.66.3
1854 + *
1855 + * @param string $url Image URL.
1856 + * @param int $width Image width.
1857 + * @param int $height Image height.
1858 + * @return array The compressed image data.
1859 + */
1860 + public function compress_and_scale_og_image( $url, $width, $height ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1861 + _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1862 +
1863 + return array();
1864 + }
1865 +
1866 + /**
1867 + * Reduce the filesize of an image by reducing the dimensions. Uses Photon.
1868 + * Returns null if the image cannot be reduced enough.
1869 + *
1870 + * @deprecated 0.66.3
1871 + *
1872 + * @param string $url Image URL.
1873 + * @param int $width Image width.
1874 + * @param int $height Image height.
1875 + * @param int $filesize Image filesize.
1876 + * @param int $tries Number of times to try reducing the image size. Default is 5.
1877 + * @return array|null
1878 + */
1879 + public function reduce_file_size( $url, $width, $height, $filesize, $tries = 5 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1880 + _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1881 +
1882 + return null;
1883 + }
1884 +
1885 + /**
1886 + * Hooks into jetpack_open_graph_tags to add the Jetpack Social images to the OpenGraph tags,
1887 + * or to make the Jetpack open graph images pass restrictions.
1888 + *
1889 + * @deprecated 0.66.3 use add_jetpack_social_og_images instead.
1890 + *
1891 + * @param array $tags Current tags.
1892 + *
1893 + * @return array The modified tags.
1894 + */
1895 + public function jetpack_social_open_graph_filter( $tags ) {
1896 + _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1506 1897 return $tags;
1507 1898 }
1508 1899
1509 1900 /**
1901 + * Add the Jetpack Social images (attached media, SIG image) to the OpenGraph tags.
1902 + *
1903 + * @deprecated 0.66.3 use add_jetpack_social_og_images instead.
1904 + *
1905 + * @param array $tags Current tags.
1906 + * @param array $opengraph_image The Jetpack Social image data.
1907 + *
1908 + * @return array The modified tags.
1909 + */
1910 + public function add_jetpack_social_og_image( $tags, $opengraph_image ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1911 + _deprecated_function( __METHOD__, '0.66.3', 'add_jetpack_social_og_images' );
1912 +
1913 + return $tags;
1914 + }
1915 +
1916 + /**
1917 + * Add the Jetpack Social images (attached media, SIG image) to the OpenGraph tags.
1918 + *
1919 + * @param array $tags Current tags.
1920 + */
1921 + public function add_jetpack_social_og_images( $tags ) {
1922 + if ( ! is_singular() ) {
1923 + return $tags;
1924 + }
1925 +
1926 + $post_id = get_the_ID();
1927 + if ( ! $post_id ) {
1928 + return $tags;
1929 + }
1930 +
1931 + if ( ! post_type_supports( get_post_type( $post_id ), 'publicize' ) ) {
1932 + return $tags;
1933 + }
1934 +
1935 + $opengraph_image = $this->get_social_opengraph_image( $post_id );
1936 + if ( empty( $opengraph_image ) ) {
1937 + return $tags;
1938 + }
1939 +
1940 + // If this code is running in Jetpack, we need to add Twitter cards.
1941 + // Some active plugins disable Jetpack's Twitter Cards, so we need
1942 + // to check if the class was instantiated before adding the cards.
1943 + $needs_twitter_cards = class_exists( \Automattic\Jetpack\Post_Media\Twitter_Cards::class );
1944 +
1945 + return array_merge(
1946 + $tags,
1947 + array(
1948 + 'og:image' => $opengraph_image['url'],
1949 + 'og:image:width' => $opengraph_image['width'],
1950 + 'og:image:height' => $opengraph_image['height'],
1951 + ),
1952 + $needs_twitter_cards ? array(
1953 + 'twitter:image' => $opengraph_image['url'],
1954 + 'twitter:card' => 'summary_large_image',
1955 + ) : array()
1956 + );
1957 + }
1958 +
1959 + /**
1510 1960 * Util
1511 1961 */
1512 1962
1513 1963 /**
@@ -1518,8 +1968,9 @@
1518 1968 * ... - The template tags 'title', 'url', etc.
1519 1969 * @return string
1520 1970 */
1521 1971 protected static function build_sprintf( $args ) {
1972 + $string = null;
1522 1973 $search = array();
1523 1974 $replace = array();
1524 1975 foreach ( $args as $k => $arg ) {
1525 1976 if ( 0 === $k ) {
@@ -1532,17 +1983,18 @@
1532 1983 return str_replace( $search, $replace, $string );
1533 1984 }
1534 1985
1535 1986 /**
1536 - * Get Calypso URL for Publicize connections.
1987 + * Get the URL to the connections management page.
1537 1988 *
1538 - * @param string $source The idenfitier of the place the function is called from.
1539 1989 * @return string
1540 1990 */
1541 - public function publicize_connections_url( $source = 'calypso-marketing-connections' ) {
1542 - $allowed_sources = array( 'jetpack-social-connections-admin-page', 'jetpack-social-connections-classic-editor', 'calypso-marketing-connections' );
1543 - $source = in_array( $source, $allowed_sources, true ) ? $source : 'calypso-marketing-connections';
1544 - return Redirect::get_url( $source, array( 'site' => ( new Status() )->get_site_suffix() ) );
1991 + public function publicize_connections_url() {
1992 + $has_social_admin_page = defined( 'JETPACK_SOCIAL_PLUGIN_DIR' );
1993 +
1994 + $page = $has_social_admin_page ? 'jetpack-social' : 'jetpack#/sharing';
1995 +
1996 + return ( new Paths() )->admin_url( array( 'page' => $page ) );
1545 1997 }
1546 1998
1547 1999 /**
1548 2000 * Get the Jetpack Social info from the API.
@@ -1557,9 +2009,8 @@
1557 2009 if ( isset( $api_data_response ) ) {
1558 2010 return ! is_wp_error( $api_data_response ) ? $api_data_response : array();
1559 2011 }
1560 2012
1561 - $rest_controller = new REST_Controller();
1562 2013 $response = Client::wpcom_json_api_request_as_blog(
1563 2014 sprintf( 'sites/%d/jetpack-social', absint( $blog_id ) ),
1564 2015 '2',
1565 2016 array(
@@ -1568,9 +2019,9 @@
1568 2019 ),
1569 2020 null,
1570 2021 'wpcom'
1571 2022 );
1572 - $api_data_response = $rest_controller->make_proper_response( $response );
2023 + $api_data_response = Publicize_Utils::make_proper_response( $response );
1573 2024
1574 2025 if ( ! is_wp_error( $api_data_response ) ) {
1575 2026 set_transient( $key, $api_data_response, DAY_IN_SECONDS );
1576 2027 return $api_data_response;
@@ -1584,37 +2035,36 @@
1584 2035 return array();
1585 2036 }
1586 2037
1587 2038 /**
1588 - * Get the Publicize shares info.
2039 + * Check if enhanced publishing is enabled.
1589 2040 *
1590 - * @param string $blog_id The WPCOM blog_id for the current blog.
1591 - * @return ?array
2041 + * @deprecated $$next-version use Automattic\Jetpack\Publicize\Publicize_Base\has_enhanced_publishing_feature instead.
2042 + * @param int $blog_id The blog ID for the current blog.
2043 + * @return bool
1592 2044 */
1593 - public function get_publicize_shares_info( $blog_id ) {
1594 - $data = $this->get_api_data( $blog_id );
2045 + public function is_enhanced_publishing_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2046 + return $this->has_enhanced_publishing_feature();
2047 + }
1595 2048
1596 - if ( empty( $data ) ) {
1597 - return null;
1598 - }
1599 -
1600 - return $data;
2049 + /**
2050 + * Check if the enhanced publishing feature is enabled.
2051 + *
2052 + * @return bool
2053 + */
2054 + public function has_enhanced_publishing_feature() {
2055 + return Current_Plan::supports( 'social-enhanced-publishing' );
1601 2056 }
1602 2057
1603 2058 /**
1604 - * Check if enhanced publishing is enabled.
2059 + * Check if the social image generator is enabled.
1605 2060 *
2061 + * @deprecated 0.24.2 use Automattic\Jetpack\Publicize\Publicize_Base\has_social_image_generator_feature instead.
1606 2062 * @param int $blog_id The blog ID for the current blog.
1607 2063 * @return bool
1608 2064 */
1609 - public function is_enhanced_publishing_enabled( $blog_id ) {
1610 - $data = $this->get_api_data( $blog_id );
1611 -
1612 - if ( empty( $data ) ) {
1613 - return false;
1614 - }
1615 -
1616 - return ! empty( $data['is_enhanced_publishing_enabled'] );
2065 + public function is_social_image_generator_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2066 + return $this->has_social_image_generator_feature();
1617 2067 }
1618 2068
1619 2069 /**
1620 2070 * Check if the social image generator is enabled.
@@ -1625,28 +2075,56 @@
1625 2075 return Current_Plan::supports( 'social-image-generator' );
1626 2076 }
1627 2077
1628 2078 /**
1629 - * Call the WPCOM REST API to calculate the scheduled shares.
2079 + * Check if the auto-conversion feature is one of the active features.
1630 2080 *
1631 - * @param string $blog_id The blog_id.
2081 + * TODO: Remove this after certain releases of Jetpack v15.
2082 + *
2083 + * @param string $type Whether image or video.
2084 + *
2085 + * @return bool
1632 2086 */
1633 - public function calculate_scheduled_shares( $blog_id ) {
1634 - $response = Client::wpcom_json_api_request_as_blog(
1635 - sprintf( 'sites/%d/jetpack-social/count-scheduled-shares', absint( $blog_id ) ),
1636 - '2',
1637 - array(
1638 - 'headers' => array( 'content-type' => 'application/json' ),
1639 - 'method' => 'GET',
1640 - ),
1641 - null,
1642 - 'wpcom'
1643 - );
1644 - $rest_controller = new REST_Controller();
1645 - return $rest_controller->make_proper_response( $response );
2087 + public function has_social_auto_conversion_feature( $type ) {
2088 + return Current_Plan::supports( "social-$type-auto-convert" );
1646 2089 }
1647 2090
1648 2091 /**
2092 + * Check if a connection is enabled.
2093 + *
2094 + * @param string $connection The connection name like 'instagram', 'mastodon', 'nextdoor' etc.
2095 + *
2096 + * @return bool
2097 + */
2098 + public function has_connection_feature( $connection ) {
2099 + return Current_Plan::supports( "social-$connection-connection" );
2100 + }
2101 +
2102 + /**
2103 + * Check if the new connections management is enabled is enabled.
2104 + *
2105 + * @return bool
2106 + */
2107 + public function has_connections_management_feature() {
2108 + return Current_Plan::supports( 'social-connections-management' );
2109 + }
2110 +
2111 + /**
2112 + * Get a list of additional connections that are supported by the current plan.
2113 + *
2114 + * @return array
2115 + */
2116 + public function get_supported_additional_connections() {
2117 + $additional_connections = array();
2118 +
2119 + if ( $this->has_connection_feature( 'threads' ) ) {
2120 + $additional_connections[] = 'threads';
2121 + }
2122 +
2123 + return $additional_connections;
2124 + }
2125 +
2126 + /**
1649 2127 * Check if we have a paid Jetpack Social plan.
1650 2128 *
1651 2129 * @param bool $refresh_from_wpcom Whether to force refresh the plan check.
1652 2130 *
@@ -1653,12 +2131,47 @@
1653 2131 * @return bool True if we have a paid plan, false otherwise.
1654 2132 */
1655 2133 public function has_paid_plan( $refresh_from_wpcom = false ) {
1656 2134 static $has_paid_plan = null;
1657 - if ( ! $has_paid_plan ) {
2135 + if ( null === $has_paid_plan ) {
1658 2136 $has_paid_plan = Current_Plan::supports( 'social-shares-1000', $refresh_from_wpcom );
1659 2137 }
1660 2138 return $has_paid_plan;
2139 + }
2140 +
2141 + /**
2142 + * Check if we have paid features enabled.
2143 + *
2144 + * @return bool True if we have paid features, false otherwise.
2145 + */
2146 + public function has_paid_features() {
2147 + return $this->has_enhanced_publishing_feature();
2148 + }
2149 +
2150 + /**
2151 + * Get an array with all dismissed notices.
2152 + *
2153 + * @return array
2154 + */
2155 + public function get_dismissed_notices() {
2156 + $dismissed_notices = get_option( self::OPTION_JETPACK_SOCIAL_DISMISSED_NOTICES );
2157 +
2158 + if ( ! is_array( $dismissed_notices ) ) {
2159 + return array();
2160 + }
2161 +
2162 + return $dismissed_notices;
2163 + }
2164 +
2165 + /**
2166 + * Whether the current user can manage a connection.
2167 + *
2168 + * @param array $connection_data The connection data.
2169 + *
2170 + * @return bool
2171 + */
2172 + public static function can_manage_connection( $connection_data ) {
2173 + return current_user_can( 'edit_others_posts' ) || get_current_user_id() === (int) $connection_data['user_id'];
1661 2174 }
1662 2175 }
1663 2176
1664 2177 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.