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 +2197 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,2197 @@
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 + // 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 );
277 +
278 + // The custom priority for this action ensures that any existing code that
279 + // removes post-thumbnails support during 'init' continues to work.
280 + add_action( 'init', __NAMESPACE__ . '\add_theme_post_thumbnails_support', 8 );
281 + }
282 +
283 + /**
284 + * Services: Facebook, Twitter, etc.
285 + */
286 +
287 + /**
288 + * Get services for the given blog and user.
289 + *
290 + * Can return all available services or just the ones with an active connection.
291 + *
292 + * @param string $filter Type of filter.
293 + * 'all' (default) - Get all services available for connecting.
294 + * 'connected' - Get all services currently connected.
295 + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
296 + * @param false|int $_user_id The user ID. Use false (default) for the current user.
297 + * @return array
298 + */
299 + abstract public function get_services( $filter = 'all', $_blog_id = false, $_user_id = false );
300 +
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 + /**
317 + * Does the given user have a connection to the service on the given blog?
318 + *
319 + * @param string $service_name 'facebook', 'twitter', etc.
320 + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
321 + * @param false|int $_user_id The user ID. Use false (default) for the current user.
322 + * @return bool
323 + */
324 + public function is_enabled( $service_name, $_blog_id = false, $_user_id = false ) {
325 + if ( ! $_blog_id ) {
326 + $_blog_id = $this->blog_id();
327 + }
328 +
329 + if ( ! $_user_id ) {
330 + $_user_id = $this->user_id();
331 + }
332 +
333 + $connections = $this->get_connections( $service_name, $_blog_id, $_user_id );
334 + return is_array( $connections ) && count( $connections ) > 0;
335 + }
336 +
337 + /**
338 + * Generates a connection URL.
339 + *
340 + * This is the URL, which, when visited by the user, starts the authentication
341 + * process required to forge a connection.
342 + *
343 + * @param string $service_name 'facebook', 'twitter', etc.
344 + * @return string
345 + */
346 + abstract public function connect_url( $service_name );
347 +
348 + /**
349 + * Generates a Connection refresh URL.
350 + *
351 + * This is the URL, which, when visited by the user, re-authenticates their
352 + * connection to the service.
353 + *
354 + * @param string $service_name 'facebook', 'twitter', etc.
355 + * @return string
356 + */
357 + abstract public function refresh_url( $service_name );
358 +
359 + /**
360 + * Generates a disconnection URL.
361 + *
362 + * This is the URL, which, when visited by the user, breaks their connection
363 + * with the service.
364 + *
365 + * @param string $service_name 'facebook', 'twitter', etc.
366 + * @param string $connection_id Connection ID.
367 + * @return string
368 + */
369 + abstract public function disconnect_url( $service_name, $connection_id );
370 +
371 + /**
372 + * Returns a display name for the Service
373 + *
374 + * @param string $service_name 'facebook', 'twitter', etc.
375 + * @return string
376 + */
377 + public static function get_service_label( $service_name ) {
378 + switch ( $service_name ) {
379 + case 'linkedin':
380 + return 'LinkedIn';
381 + case 'google_drive': // google-drive used to be called google_drive.
382 + case 'google-drive':
383 + return 'Google Drive';
384 + case 'instagram-business':
385 + return 'Instagram';
386 + case 'twitter':
387 + case 'facebook':
388 + case 'tumblr':
389 + default:
390 + return ucfirst( $service_name );
391 + }
392 + }
393 +
394 + /**
395 + * Connections: For each Service, there can be multiple connections
396 + * for a given user. For example, one user could be connected to Twitter
397 + * as both @jetpack and as @wordpressdotcom
398 + *
399 + * For historical reasons, Connections are represented as an object
400 + * on WordPress.com and as an array in Jetpack.
401 + */
402 +
403 + /**
404 + * Get the active Connections of a Service
405 + *
406 + * @param string $service_name 'facebook', 'twitter', etc.
407 + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
408 + * @param false|int $_user_id The user ID. Use false (default) for the current user.
409 + * @return false|object[]|array[] false if no connections exist
410 + */
411 + abstract public function get_connections( $service_name, $_blog_id = false, $_user_id = false );
412 +
413 + /**
414 + * Get a single Connection of a Service
415 + *
416 + * @param string $service_name 'facebook', 'twitter', etc.
417 + * @param string $connection_id Connection ID.
418 + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
419 + * @param false|int $_user_id The user ID. Use false (default) for the current user.
420 + * @return false|object[]|array[] false if no connections exist
421 + */
422 + abstract public function get_connection( $service_name, $connection_id, $_blog_id = false, $_user_id = false );
423 +
424 + /**
425 + * Get the Connection ID.
426 + *
427 + * Note that this is different than the Connection's uniqueid.
428 + *
429 + * Via a quirk of history, ID is globally unique and unique_id
430 + * is only unique per site.
431 + *
432 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
433 + * @return string
434 + */
435 + abstract public function get_connection_id( $connection );
436 +
437 + /**
438 + * Get the Connection unique_id
439 + *
440 + * Note that this is different than the Connections ID.
441 + *
442 + * Via a quirk of history, ID is globally unique and unique_id
443 + * is only unique per site.
444 + *
445 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
446 + * @return string
447 + */
448 + abstract public function get_connection_unique_id( $connection );
449 +
450 + /**
451 + * Get the Connection's Meta data
452 + *
453 + * @param object|array $connection Connection.
454 + * @return array Connection Meta
455 + */
456 + abstract public function get_connection_meta( $connection );
457 +
458 + /**
459 + * Disconnect a Connection
460 + *
461 + * @param string $service_name 'facebook', 'twitter', etc.
462 + * @param string $connection_id Connection ID.
463 + * @param false|int $_blog_id The blog ID. Use false (default) for the current blog.
464 + * @param false|int $_user_id The user ID. Use false (default) for the current user.
465 + * @param bool $force_delete Whether to skip permissions checks.
466 + * @return false|void False on failure. Void on success.
467 + */
468 + abstract public function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false );
469 +
470 + /**
471 + * Globalizes a Connection
472 + *
473 + * @param string $connection_id Connection ID.
474 + * @return bool Falsey on failure. Truthy on success.
475 + */
476 + abstract public function globalize_connection( $connection_id );
477 +
478 + /**
479 + * Unglobalizes a Connection
480 + *
481 + * @param string $connection_id Connection ID.
482 + * @return bool Falsey on failure. Truthy on success.
483 + */
484 + abstract public function unglobalize_connection( $connection_id );
485 +
486 + /**
487 + * Returns an external URL to the Connection's profile
488 + *
489 + * @param string $service_name 'facebook', 'twitter', etc.
490 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
491 + * @return false|string False on failure. URL on success.
492 + */
493 + public function get_profile_link( $service_name, $connection ) {
494 + $cmeta = $this->get_connection_meta( $connection );
495 +
496 + if ( isset( $cmeta['connection_data']['meta']['link'] ) ) {
497 + if ( 'facebook' === $service_name && str_starts_with( wp_parse_url( $cmeta['connection_data']['meta']['link'], PHP_URL_PATH ), '/app_scoped_user_id/' ) ) {
498 + // App-scoped Facebook user IDs are not usable profile links.
499 + return false;
500 + }
501 +
502 + return $cmeta['connection_data']['meta']['link'];
503 + }
504 +
505 + if ( 'facebook' === $service_name && isset( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
506 + return 'https://facebook.com/' . $cmeta['connection_data']['meta']['facebook_page'];
507 + }
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 +
538 + if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
539 + return 'https://' . $cmeta['connection_data']['meta']['tumblr_base_hostname'];
540 + }
541 +
542 + if ( 'twitter' === $service_name ) {
543 + return 'https://twitter.com/' . substr( $cmeta['external_display'], 1 ); // Has a leading '@'.
544 + }
545 +
546 + if ( 'bluesky' === $service_name ) {
547 + return 'https://bsky.app/profile/' . $cmeta['external_id'];
548 + }
549 +
550 + return false; // no fallback. we just won't link it.
551 + }
552 +
553 + /**
554 + * Returns a display name for the Connection
555 + *
556 + * @param string $service_name 'facebook', 'twitter', etc.
557 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
558 + * @return string
559 + */
560 + public function get_display_name( $service_name, $connection ) {
561 + $cmeta = $this->get_connection_meta( $connection );
562 +
563 + if ( 'mastodon' === $service_name && isset( $cmeta['external_display'] ) ) {
564 + return $cmeta['external_display'];
565 + }
566 +
567 + if ( isset( $cmeta['connection_data']['meta']['display_name'] ) ) {
568 + return $cmeta['connection_data']['meta']['display_name'];
569 + }
570 +
571 + if ( 'tumblr' === $service_name && isset( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
572 + return $cmeta['connection_data']['meta']['tumblr_base_hostname'];
573 + }
574 +
575 + if ( 'twitter' === $service_name ) {
576 + return $cmeta['external_display'];
577 + }
578 +
579 + $connection_display = $cmeta['external_display'];
580 +
581 + if ( empty( $connection_display ) ) {
582 + $connection_display = $cmeta['external_name'];
583 + }
584 +
585 + return $connection_display;
586 + }
587 +
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 + /**
628 + * Returns a profile picture for the Connection
629 + *
630 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
631 + * @return string
632 + */
633 + public function get_profile_picture( $connection ) {
634 + $cmeta = $this->get_connection_meta( $connection );
635 +
636 + if ( isset( $cmeta['profile_picture'] ) ) {
637 + return $cmeta['profile_picture'];
638 + }
639 +
640 + return '';
641 + }
642 +
643 + /**
644 + * Whether the user needs to select additional options after connecting
645 + *
646 + * @param string $service_name 'facebook', 'twitter', etc.
647 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
648 + * @return bool
649 + */
650 + public function show_options_popup( $service_name, $connection ) {
651 + $cmeta = $this->get_connection_meta( $connection );
652 +
653 + // Always show if no selection has been made for Facebook.
654 + if ( 'facebook' === $service_name && empty( $cmeta['connection_data']['meta']['facebook_profile'] ) && empty( $cmeta['connection_data']['meta']['facebook_page'] ) ) {
655 + return true;
656 + }
657 +
658 + // Always show if no selection has been made for Tumblr.
659 + if ( 'tumblr' === $service_name && empty( $cmeta['connection_data']['meta']['tumblr_base_hostname'] ) ) {
660 + return true;
661 + }
662 +
663 + // if we have the specific connection info..
664 + $id = ! empty( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
665 +
666 + if ( $id ) {
667 + if ( $cmeta['connection_data']['id'] === $id ) {
668 + return true;
669 + }
670 + } else {
671 + // Otherwise, just show if this is the completed step / first load.
672 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
673 + $is_completed = ! empty( $_GET['action'] ) && 'completed' === $_GET['action'];
674 + $service = ! empty( $_GET['service'] ) ? sanitize_text_field( wp_unslash( $_GET['service'] ) ) : false;
675 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
676 +
677 + if ( $is_completed && $service_name === $service && ! in_array( $service, array( 'facebook', 'tumblr' ), true ) ) {
678 + return true;
679 + }
680 + }
681 +
682 + return false;
683 + }
684 +
685 + /**
686 + * Check if a connection is global
687 + *
688 + * @param array $connection Connection data.
689 + * @return bool Whether the connection is global.
690 + */
691 + public function is_global_connection( $connection ) {
692 + return empty( $connection['connection_data']['user_id'] );
693 + }
694 +
695 + /**
696 + * Whether the Connection is "valid" wrt Facebook's requirements.
697 + *
698 + * Must be connected to a Page (not a Profile).
699 + * (Also returns true if we're in the middle of the connection process)
700 + *
701 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
702 + * @return bool
703 + */
704 + public function is_valid_facebook_connection( $connection ) {
705 + if ( $this->is_connecting_connection( $connection ) ) {
706 + return true;
707 + }
708 + $connection_meta = $this->get_connection_meta( $connection );
709 + $connection_data = $connection_meta['connection_data'];
710 + return isset( $connection_data['meta']['facebook_page'] );
711 + }
712 +
713 + /**
714 + * LinkedIn needs to be reauthenticated to use v2 of their API.
715 + * If it's using LinkedIn old API, it's an 'invalid' connection
716 + *
717 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
718 + * @return bool
719 + */
720 + public function is_invalid_linkedin_connection( $connection ) {
721 + // LinkedIn API v1 included the profile link in the connection data.
722 + $connection_meta = $this->get_connection_meta( $connection );
723 + return isset( $connection_meta['connection_data']['meta']['profile_url'] );
724 + }
725 +
726 + /**
727 + * Whether the Connection currently being connected
728 + *
729 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
730 + * @return bool
731 + */
732 + public function is_connecting_connection( $connection ) {
733 + $connection_meta = $this->get_connection_meta( $connection );
734 + $connection_data = $connection_meta['connection_data'];
735 + return isset( $connection_data['meta']['options_responses'] );
736 + }
737 +
738 + /**
739 + * Parse the error code returned by the XML-RPC API call.
740 + *
741 + * @param string $code Error code in numerical format.
742 + *
743 + * @return string Error code.
744 + */
745 + public function parse_connection_error_code( $code ) {
746 + if ( 1 === $code ) {
747 + return 'unsupported';
748 + }
749 +
750 + return 'broken';
751 + }
752 +
753 + /**
754 + * Run connection tests on all Connections
755 + *
756 + * @return array {
757 + * Array of connection test results.
758 + *
759 + * @type string 'connectionID' Connection identifier string that is unique for each connection
760 + * @type string 'serviceName' Slug of the connection's service (facebook, twitter, ...)
761 + * @type bool 'connectionTestPassed' Whether the connection test was successful
762 + * @type string 'connectionTestMessage' Test success or error message
763 + * @type bool 'userCanRefresh' Whether the user can re-authenticate their connection to the service
764 + * @type string 'refreshText' Message instructing user to re-authenticate their connection to the service
765 + * @type string 'refreshURL' URL, which, when visited by the user, re-authenticates their connection to the service.
766 + * @type string 'unique_id' ID string representing connection
767 + * }
768 + */
769 + public function get_publicize_conns_test_results() {
770 + $test_results = array();
771 +
772 + foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
773 + foreach ( $connections as $connection ) {
774 +
775 + $id = $this->get_connection_id( $connection );
776 +
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 = '';
783 +
784 + $connection_test_result = true;
785 + if ( method_exists( $this, 'test_connection' ) ) {
786 + $connection_test_result = $this->test_connection( $service_name, $connection );
787 + }
788 +
789 + if ( is_wp_error( $connection_test_result ) ) {
790 + $connection_test_passed = false;
791 + $connection_test_message = $connection_test_result->get_error_message();
792 + $error_data = $connection_test_result->get_error_data();
793 + $error_code = $connection_test_result->get_error_code();
794 +
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 );
801 + }
802 + // Mark Facebook profiles as deprecated.
803 + if ( 'facebook' === $service_name ) {
804 + if ( ! $this->is_valid_facebook_connection( $connection ) ) {
805 + $connection_test_passed = false;
806 + $user_can_refresh = false;
807 + $connection_test_message = __( 'Please select a Facebook Page to publish updates.', 'jetpack-publicize-pkg' );
808 + }
809 + }
810 +
811 + // LinkedIn needs reauthentication to be compatible with v2 of their API.
812 + if ( 'linkedin' === $service_name && $this->is_invalid_linkedin_connection( $connection ) ) {
813 + $connection_test_passed = 'must_reauth';
814 + $user_can_refresh = false;
815 + $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' );
816 + }
817 +
818 + $unique_id = null;
819 +
820 + if ( ! empty( $connection->unique_id ) ) {
821 + $unique_id = $connection->unique_id;
822 + } elseif ( ! empty( $connection['connection_data']['token_id'] ) ) {
823 + $unique_id = $connection['connection_data']['token_id'];
824 + }
825 +
826 + $test_results[] = array(
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,
836 + );
837 + }
838 + }
839 +
840 + return $test_results;
841 + }
842 +
843 + /**
844 + * Run the connection test for the Connection
845 + *
846 + * @param string $service_name $service_name 'facebook', 'twitter', etc.
847 + * @param object|array $connection The Connection object (WordPress.com) or array (Jetpack).
848 + * @return WP_Error|true WP_Error on failure. True on success
849 + */
850 + abstract public function test_connection( $service_name, $connection );
851 +
852 + /**
853 + * Retrieves current list of connections and applies filters.
854 + *
855 + * Retrieves current available connections and checks if the connections
856 + * have already been used to share current post. Finally, the checkbox
857 + * form UI fields are calculated. This function exposes connection form
858 + * data directly as array so it can be retrieved for static HTML generation
859 + * or JSON consumption.
860 + *
861 + * @since 0.1.0
862 + * @since-jetpack 6.7.0
863 + *
864 + * @param integer $selected_post_id Optional. Post ID to query connection status for.
865 + *
866 + * @return array {
867 + * Array of UI setup data for connection list form.
868 + *
869 + * @type string 'unique_id' ID string representing connection
870 + * @type string 'service_name' Slug of the connection's service (facebook, twitter, ...)
871 + * @type string 'service_label' Service Label (Facebook, Twitter, ...)
872 + * @type string 'display_name' Connection's human-readable Username: "@jetpack"
873 + * @type string 'profile_picture' Connection profile picture.
874 + * @type bool 'enabled' Default value for the connection (e.g., for a checkbox).
875 + * @type bool 'done' Has this connection already been publicized to?
876 + * @type bool 'toggleable' Is the user allowed to change the value for the connection?
877 + * @type bool 'global' Is this connection a global one?
878 + * @type string 'external_id' External ID for the connection.
879 + * }
880 + */
881 + public function get_filtered_connection_data( $selected_post_id = null ) {
882 + $connection_list = array();
883 +
884 + $post = get_post( $selected_post_id ); // Defaults to current post if $post_id is null.
885 + // Handle case where there is no current post.
886 + if ( ! empty( $post ) ) {
887 + $post_id = $post->ID;
888 + } else {
889 + $post_id = null;
890 + }
891 +
892 + // We don't allow Publicizing to the same external id twice, to prevent spam.
893 + $service_id_done = (array) get_post_meta( $post_id, $this->POST_SERVICE_DONE, true );
894 +
895 + $connections = Connections::get_all_for_user();
896 +
897 + if ( ! empty( $connections ) ) {
898 +
899 + foreach ( $connections as $connection ) {
900 + $service_name = $connection['service_name'];
901 + $unique_id = $connection['id'];
902 + $connection_id = $connection['connection_id'];
903 + // Was this connection (OR, old-format service) already Publicized to?
904 + $done = ! empty( $post ) && (
905 + // Flags based on token_id.
906 + 1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $unique_id, true )
907 + ||
908 + // Old flags.
909 + 1 === (int) get_post_meta( $post->ID, $this->POST_DONE . $service_name, true )
910 + );
911 +
912 + /**
913 + * Filter whether a post should be publicized to a given service.
914 + *
915 + * @since 0.1.0
916 + * @since-jetpack 2.0.0
917 + *
918 + * @param bool true Should the post be publicized to a given service? Default to true.
919 + * @param int $post_id Post ID.
920 + * @param string $service_name Service name.
921 + * @param array $connection The connection data.
922 + */
923 + /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */
924 + if ( ! apply_filters( 'wpas_submit_post?', true, $post_id, $service_name, $connection ) ) {
925 + continue;
926 + }
927 +
928 + // Should we be skipping this one?
929 + $skip = (
930 + (
931 + ! empty( $post )
932 + &&
933 + in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true )
934 + &&
935 + (
936 + // Flags based on token_id.
937 + get_post_meta( $post->ID, $this->POST_SKIP . $unique_id, true )
938 + ||
939 + // Flags based on the connection_id.
940 + get_post_meta( $post->ID, $this->POST_SKIP_PUBLICIZE . $connection_id, true )
941 + ||
942 + // Old flags.
943 + get_post_meta( $post->ID, $this->POST_SKIP . $service_name, false )
944 + )
945 + )
946 + ||
947 + (
948 + isset( $connection['external_id'] ) && ! empty( $service_id_done[ $service_name ][ $connection['external_id'] ] )
949 + )
950 + );
951 +
952 + // Determine the state of the checkbox (on/off) and allow filtering.
953 + $enabled = $done || ! $skip;
954 + /**
955 + * Filter the checkbox state of each Publicize connection appearing in the post editor.
956 + *
957 + * @since 0.1.0
958 + * @since-jetpack 2.0.1
959 + *
960 + * @param bool $enabled Should the Publicize checkbox be enabled for a given service.
961 + * @param int $post_id Post ID.
962 + * @param string $service_name Service name.
963 + * @param array $connection Array of connection details.
964 + */
965 + $enabled = apply_filters( 'publicize_checkbox_default', $enabled, $post_id, $service_name, $connection );
966 +
967 + /**
968 + * If this is a shared connection and this user doesn't have enough permissions to modify.
969 + */
970 + if ( ! $done && Connections::is_shared( $connection ) && ! current_user_can( $this->GLOBAL_CAP ) ) {
971 + /**
972 + * Filters the checkboxes for global connections with non-prilvedged users.
973 + *
974 + * @since 0.1.0
975 + * @since-jetpack 3.7.0
976 + *
977 + * @param bool $enabled Indicates if this connection should be enabled. Default true.
978 + * @param int $post_id ID of the current post
979 + * @param string $service_name Name of the connection (Facebook, Twitter, etc)
980 + * @param array $connection Array of data about the connection.
981 + */
982 + $enabled = apply_filters( 'publicize_checkbox_global_default', $enabled, $post_id, $service_name, $connection );
983 + }
984 +
985 + // Force the checkbox to be checked if the post was DONE, regardless of what the filter does.
986 + if ( $done ) {
987 + $enabled = true;
988 + }
989 +
990 + $connection_list[] = array_merge(
991 + $connection,
992 + array(
993 + 'enabled' => $enabled,
994 + 'done' => $done,
995 + )
996 + );
997 + }
998 + }
999 +
1000 + return $connection_list;
1001 + }
1002 +
1003 + /**
1004 + * Checks if post has already been shared by Publicize in the past.
1005 + *
1006 + * @since 0.1.0
1007 + * @since-jetpack 6.7.0
1008 + *
1009 + * @param integer $post_id Optional. Post ID to query connection status for: will use current post if missing.
1010 + *
1011 + * @return bool True if post has already been shared by Publicize, false otherwise.
1012 + */
1013 + abstract public function post_is_done_sharing( $post_id = null );
1014 +
1015 + /**
1016 + * Retrieve the available Publicize services and their connect URLs.
1017 + *
1018 + * @since 0.1.0
1019 + * @since-jetpack 6.7.0
1020 + * @deprecated 0.88.0 Services and their connect URLs come from the `publicize/services` REST endpoint.
1021 + *
1022 + * @return array Always empty.
1023 + */
1024 + public function get_available_service_data() {
1025 + _deprecated_function( __METHOD__, 'publicize-0.88.0' );
1026 +
1027 + return array();
1028 + }
1029 +
1030 + /**
1031 + * Site Data
1032 + */
1033 +
1034 + /**
1035 + * Get user ID.
1036 + *
1037 + * @return int The current user's ID, or 0 if no user is logged in.
1038 + */
1039 + public function user_id() {
1040 + return get_current_user_id();
1041 + }
1042 +
1043 + /**
1044 + * Get site ID.
1045 + *
1046 + * @return int Site ID.
1047 + */
1048 + public function blog_id() {
1049 + return get_current_blog_id();
1050 + }
1051 +
1052 + /**
1053 + * Posts
1054 + */
1055 +
1056 + /**
1057 + * Checks old and new status to see if the post should be flagged as
1058 + * ready to Publicize.
1059 + *
1060 + * Attached to the `transition_post_status` filter.
1061 + *
1062 + * @param string $new_status New status.
1063 + * @param string $old_status Old status.
1064 + * @param WP_Post $post Post object.
1065 + * @return void
1066 + */
1067 + abstract public function flag_post_for_publicize( $new_status, $old_status, $post );
1068 +
1069 + /**
1070 + * Ensures the Post internal post-type supports `publicize`
1071 + *
1072 + * This feature support flag is used by the REST API.
1073 + */
1074 + public function add_post_type_support() {
1075 + add_post_type_support( 'post', 'publicize' );
1076 + }
1077 +
1078 + /**
1079 + * Can the current user access Publicize Data.
1080 + *
1081 + * @param int $post_id 0 for general access. Post_ID for specific access.
1082 + * @return bool
1083 + */
1084 + public function current_user_can_access_publicize_data( $post_id = 0 ) {
1085 + /**
1086 + * 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.
1087 + *
1088 + * @since 0.1.0
1089 + * @since-jetpack 4.1.0
1090 + *
1091 + * @param string $capability User capability needed to use publicize
1092 + */
1093 + $capability = apply_filters( 'jetpack_publicize_capability', 'publish_posts' );
1094 +
1095 + if ( 'publish_posts' === $capability && $post_id ) {
1096 + return current_user_can( 'publish_post', $post_id );
1097 + }
1098 +
1099 + return current_user_can( $capability );
1100 + }
1101 +
1102 + /**
1103 + * Auth callback for the protected ->POST_MESS post_meta
1104 + *
1105 + * @param int $object_id Post ID.
1106 + * @return bool
1107 + */
1108 + public function message_meta_auth_callback( $object_id ) {
1109 + return $this->current_user_can_access_publicize_data( $object_id );
1110 + }
1111 +
1112 + /**
1113 + * Registers the post_meta for use in the REST API.
1114 + *
1115 + * Registers for each post type that with `publicize` feature support.
1116 + */
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 +
1125 + $message_args = array(
1126 + 'type' => 'string',
1127 + 'description' => __( 'The message to use instead of the title when sharing to Jetpack Social services', 'jetpack-publicize-pkg' ),
1128 + 'single' => true,
1129 + 'default' => $message_default,
1130 + 'show_in_rest' => array(
1131 + 'name' => 'jetpack_publicize_message',
1132 + ),
1133 + 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1134 + );
1135 +
1136 + $publicize_feature_enable_args = array(
1137 + 'type' => 'boolean',
1138 + 'description' => __( 'Whether or not the Share Post feature is enabled.', 'jetpack-publicize-pkg' ),
1139 + 'single' => true,
1140 + 'default' => true,
1141 + 'show_in_rest' => array(
1142 + 'name' => 'jetpack_publicize_feature_enabled',
1143 + ),
1144 + 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1145 + );
1146 +
1147 + $already_shared_flag_args = array(
1148 + 'type' => 'boolean',
1149 + 'description' => __( 'Whether or not the post has already been shared.', 'jetpack-publicize-pkg' ),
1150 + 'single' => true,
1151 + 'default' => false,
1152 + 'show_in_rest' => array(
1153 + 'name' => 'jetpack_social_post_already_shared',
1154 + ),
1155 + 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1156 + );
1157 +
1158 + $jetpack_social_options_args = array(
1159 + 'type' => 'object',
1160 + 'description' => __( 'Post options related to Jetpack Social.', 'jetpack-publicize-pkg' ),
1161 + 'single' => true,
1162 + 'default' => array(
1163 + 'image_generator_settings' => array(
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,
1168 + ),
1169 + 'version' => 2,
1170 + ),
1171 + 'show_in_rest' => array(
1172 + 'name' => 'jetpack_social_options',
1173 + 'schema' => array(
1174 + 'type' => 'object',
1175 + 'properties' => array(
1176 + 'version' => array(
1177 + 'type' => 'number',
1178 + ),
1179 + 'attached_media' => array(
1180 + 'type' => 'array',
1181 + 'items' => array(
1182 + 'type' => 'object',
1183 + 'properties' => array(
1184 + 'id' => array(
1185 + 'type' => 'number',
1186 + ),
1187 + 'url' => array(
1188 + 'type' => 'string',
1189 + ),
1190 + 'type' => array(
1191 + 'type' => 'string',
1192 + ),
1193 + ),
1194 + ),
1195 + ),
1196 + 'image_generator_settings' => array(
1197 + 'type' => 'object',
1198 + 'properties' => array(
1199 + 'enabled' => array(
1200 + 'type' => 'boolean',
1201 + ),
1202 + 'custom_text' => array(
1203 + 'type' => 'string',
1204 + ),
1205 + 'image_type' => array(
1206 + 'type' => 'string',
1207 + ),
1208 + 'image_id' => array(
1209 + 'type' => 'number',
1210 + ),
1211 + 'template' => array(
1212 + 'type' => 'string',
1213 + ),
1214 + 'font' => array(
1215 + 'type' => 'string',
1216 + ),
1217 + 'token' => array(
1218 + 'type' => 'string',
1219 + ),
1220 + 'default_image_id' => array(
1221 + 'type' => 'number',
1222 + ),
1223 + ),
1224 + ),
1225 + 'media_source' => array(
1226 + 'type' => 'string',
1227 + 'enum' => array( 'featured-image', 'sig', 'media-library', 'upload-video', 'none' ),
1228 + ),
1229 + ),
1230 + ),
1231 + ),
1232 + 'auth_callback' => array( $this, 'message_meta_auth_callback' ),
1233 + );
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 +
1287 + foreach ( get_post_types() as $post_type ) {
1288 + if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1289 + continue;
1290 + }
1291 +
1292 + $message_args['object_subtype'] = $post_type;
1293 + $publicize_feature_enable_args['object_subtype'] = $post_type;
1294 + $already_shared_flag_args['object_subtype'] = $post_type;
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;
1298 +
1299 + register_meta( 'post', $this->POST_MESS, $message_args );
1300 + register_meta( 'post', self::POST_PUBLICIZE_FEATURE_ENABLED, $publicize_feature_enable_args );
1301 + register_meta( 'post', $this->POST_DONE . 'all', $already_shared_flag_args );
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 );
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 );
1310 + }
1311 +
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 + /**
1408 + * Helper function to allow us to not publicize posts in certain contexts.
1409 + *
1410 + * @param WP_Post $post Post object.
1411 + */
1412 + public function should_submit_post_pre_checks( $post ) {
1413 + $submit_post = true;
1414 +
1415 + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
1416 + $submit_post = false;
1417 + }
1418 +
1419 + if (
1420 + defined( 'DOING_AUTOSAVE' )
1421 + &&
1422 + DOING_AUTOSAVE
1423 + ) {
1424 + $submit_post = false;
1425 + }
1426 +
1427 + // To stop quick edits from getting publicized.
1428 + if ( did_action( 'wp_ajax_inline-save' ) ) {
1429 + $submit_post = false;
1430 + }
1431 +
1432 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
1433 + if ( ! empty( $_GET['bulk_edit'] ) ) {
1434 + $submit_post = false;
1435 + }
1436 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
1437 +
1438 + // - API/XML-RPC Test Posts
1439 + if (
1440 + (
1441 + ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
1442 + ||
1443 + ( defined( 'APP_REQUEST' ) && constant( 'APP_REQUEST' ) )
1444 + )
1445 + &&
1446 + str_starts_with( $post->post_title, 'Temporary Post Used For Theme Detection' )
1447 + ) {
1448 + $submit_post = false;
1449 + }
1450 +
1451 + // Only work with certain statuses (avoids inherits, auto drafts etc).
1452 + if ( ! in_array( $post->post_status, array( 'publish', 'draft', 'future' ), true ) ) {
1453 + $submit_post = false;
1454 + }
1455 +
1456 + // Don't publish password protected posts.
1457 + if ( '' !== $post->post_password ) {
1458 + $submit_post = false;
1459 + }
1460 +
1461 + return $submit_post;
1462 + }
1463 +
1464 + /**
1465 + * Fires when a post is saved, checks conditions and saves state in postmeta so that it
1466 + * can be picked up later by @see ::publicize_post() on WordPress.com codebase.
1467 + *
1468 + * Attached to the `save_post` action.
1469 + *
1470 + * @param int $post_id Post ID.
1471 + * @param WP_Post $post Post object.
1472 + */
1473 + public function save_meta( $post_id, $post ) {
1474 + $cron_user = null;
1475 + $submit_post = true;
1476 +
1477 + if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
1478 + return;
1479 + }
1480 +
1481 + $submit_post = $this->should_submit_post_pre_checks( $post );
1482 +
1483 + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We're only checking if a value is set
1484 + $admin_page = $_POST[ $this->ADMIN_PAGE ] ?? null;
1485 +
1486 + // Did this request happen via wp-admin?
1487 + $from_web = isset( $_SERVER['REQUEST_METHOD'] )
1488 + &&
1489 + 'post' === strtolower( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) )
1490 + &&
1491 + ! empty( $admin_page );
1492 +
1493 + // phpcs:ignore WordPress.Security.NonceVerification.Missing
1494 + $title = isset( $_POST['wpas_title'] ) ? sanitize_textarea_field( wp_unslash( $_POST['wpas_title'] ) ) : null;
1495 +
1496 + if ( ( $from_web || defined( 'POST_BY_EMAIL' ) ) && ! empty( $title ) ) {
1497 + update_post_meta( $post_id, $this->POST_MESS, trim( stripslashes( $title ) ) );
1498 + }
1499 +
1500 + // Change current user to provide context for get_services() if we're running during cron.
1501 + if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1502 + $cron_user = (int) $GLOBALS['user_ID'];
1503 + wp_set_current_user( $post->post_author );
1504 + }
1505 +
1506 + /**
1507 + * In this phase, we mark connections that we want to SKIP. When Publicize is actually triggered,
1508 + * it will Publicize to everything *except* those marked for skipping.
1509 + */
1510 + foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1511 + foreach ( $connections as $connection ) {
1512 + $connection_data = '';
1513 + if ( is_object( $connection ) && method_exists( $connection, 'get_meta' ) ) {
1514 + $connection_data = $connection->get_meta( 'connection_data' );
1515 + } elseif ( ! empty( $connection['connection_data'] ) ) {
1516 + $connection_data = $connection['connection_data'];
1517 + }
1518 +
1519 + /** This action is documented in modules/publicize/ui.php */
1520 + /* phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores */
1521 + if ( false === apply_filters( 'wpas_submit_post?', $submit_post, $post_id, $service_name, $connection_data ) ) {
1522 + delete_post_meta( $post_id, $this->PENDING );
1523 + continue;
1524 + }
1525 +
1526 + // This was a wp-admin request, so we need to check the state of checkboxes.
1527 + if ( $from_web ) {
1528 + $connection_id = $this->get_connection_id( $connection );
1529 + // Delete stray service-based post meta.
1530 + delete_post_meta( $post_id, $this->POST_SKIP . $service_name );
1531 +
1532 + // We *unchecked* this stream from the admin page, or it's set to readonly, or it's a new addition.
1533 + if ( empty( $admin_page['submit'][ $connection_id ] ) ) {
1534 + // Also make sure that the service-specific input isn't there.
1535 + // If the user connected to a new service 'in-page' then a hidden field with the service
1536 + // name is added, so we just assume they wanted to Publicize to that service.
1537 + if ( empty( $admin_page['submit'][ $service_name ] ) ) {
1538 + // Nothing seems to be checked, so we're going to mark this one to be skipped.
1539 + update_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id, 1 );
1540 + continue;
1541 + } else {
1542 + // Clean up any stray post meta.
1543 + delete_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id );
1544 + }
1545 + } else {
1546 + // The checkbox for this connection is explicitly checked -- make sure we DON'T skip it.
1547 + delete_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id );
1548 + }
1549 + }
1550 +
1551 + /**
1552 + * Fires right before the post is processed for Publicize.
1553 + * Users may hook in here and do anything else they need to after meta is written,
1554 + * and before the post is processed for Publicize.
1555 + *
1556 + * @since 0.1.0
1557 + * @since-jetpack 2.1.2
1558 + *
1559 + * @param bool $submit_post Should the post be publicized.
1560 + * @param int $post->ID Post ID.
1561 + * @param string $service_name Service name.
1562 + * @param array $connection Array of connection details.
1563 + */
1564 + do_action( 'publicize_save_meta', $submit_post, $post_id, $service_name, $connection );
1565 + }
1566 + }
1567 +
1568 + if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
1569 + wp_set_current_user( $cron_user );
1570 + }
1571 +
1572 + // Next up will be ::publicize_post().
1573 + }
1574 +
1575 + /**
1576 + * Alters the "Post Published" message to include information about where the post
1577 + * was Publicized to.
1578 + *
1579 + * Attached to the `post_updated_messages` filter
1580 + *
1581 + * @param string[] $messages Array of messages.
1582 + * @return string[]
1583 + */
1584 + public function update_published_message( $messages ) {
1585 + global $post_type, $post_type_object, $post;
1586 + if ( ! $this->post_type_is_publicizeable( $post_type ) ) {
1587 + return $messages;
1588 + }
1589 +
1590 + // Bail early if the post is private.
1591 + if ( 'publish' !== $post->post_status ) {
1592 + return $messages;
1593 + }
1594 +
1595 + $view_post_link_html = '';
1596 + $viewable = is_post_type_viewable( $post_type_object );
1597 + if ( $viewable ) {
1598 + /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain, WordPress.Utils.I18nTextDomainFixer.MissingArgDomain */
1599 + $view_text = esc_html__( 'View post' ); // Intentionally omitted domain.
1600 +
1601 + if ( 'jetpack-portfolio' === $post_type ) {
1602 + $view_text = esc_html__( 'View project', 'jetpack-publicize-pkg' );
1603 + }
1604 +
1605 + $view_post_link_html = sprintf(
1606 + ' <a href="%1$s">%2$s</a>',
1607 + esc_url( get_permalink( $post ) ),
1608 + $view_text
1609 + );
1610 + }
1611 +
1612 + $services = $this->get_publicizing_services( $post->ID );
1613 + if ( empty( $services ) ) {
1614 + return $messages;
1615 + }
1616 +
1617 + $labels = array();
1618 + foreach ( $services as $service_name => $display_names ) {
1619 + $labels[] = sprintf(
1620 + /* translators: Service name is %1$s, and account name is %2$s. */
1621 + esc_html__( '%1$s (%2$s)', 'jetpack-publicize-pkg' ),
1622 + esc_html( $service_name ),
1623 + esc_html( is_array( $display_names ) ? implode( ', ', $display_names ) : $display_names )
1624 + );
1625 + }
1626 +
1627 + $messages['post'][6] = sprintf(
1628 + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1629 + esc_html__( 'Post published and sharing on %1$s.', 'jetpack-publicize-pkg' ),
1630 + implode( ', ', $labels )
1631 + ) . $view_post_link_html;
1632 +
1633 + if ( 'post' === $post_type && class_exists( 'Jetpack_Subscriptions' ) ) {
1634 + $subscription = \Jetpack_Subscriptions::init();
1635 + if ( $subscription->should_email_post_to_subscribers( $post ) ) {
1636 + $messages['post'][6] = sprintf(
1637 + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1638 + esc_html__( 'Post published, sending emails to subscribers and sharing post on %1$s.', 'jetpack-publicize-pkg' ),
1639 + implode( ', ', $labels )
1640 + ) . $view_post_link_html;
1641 + }
1642 + }
1643 +
1644 + $messages['jetpack-portfolio'][6] = sprintf(
1645 + /* translators: %1$s is a comma-separated list of services and accounts. Ex. Facebook (@jetpack) */
1646 + esc_html__( 'Project published and sharing project on %1$s.', 'jetpack-publicize-pkg' ),
1647 + implode( ', ', $labels )
1648 + ) . $view_post_link_html;
1649 +
1650 + return $messages;
1651 + }
1652 +
1653 + /**
1654 + * Get the Connections the Post was just Publicized to.
1655 + *
1656 + * Only reliable just after the Post was published.
1657 + *
1658 + * @param int $post_id Post ID.
1659 + * @return string[] Array of Service display name => Connection display name
1660 + */
1661 + public function get_publicizing_services( $post_id ) {
1662 + $services = array();
1663 +
1664 + foreach ( (array) $this->get_services( 'connected' ) as $service_name => $connections ) {
1665 + // services have multiple connections.
1666 + foreach ( $connections as $connection ) {
1667 + $connection_id = $this->get_connection_id( $connection );
1668 + // Did we skip this connection?
1669 + if ( get_post_meta( $post_id, $this->POST_SKIP_PUBLICIZE . $connection_id, true ) ) {
1670 + continue;
1671 + }
1672 + $services[ static::get_service_label( $service_name ) ][] = $this->get_display_name( $service_name, $connection );
1673 + }
1674 + }
1675 +
1676 + return $services;
1677 + }
1678 +
1679 + /**
1680 + * Is the post Publicize-able?
1681 + *
1682 + * Only valid prior to Publicizing a Post.
1683 + *
1684 + * @param WP_Post $post Post to check.
1685 + * @return bool
1686 + */
1687 + public function post_is_publicizeable( $post ) {
1688 + if ( ! $this->post_type_is_publicizeable( $post->post_type ) ) {
1689 + return false;
1690 + }
1691 +
1692 + // This is more a precaution. To only publicize posts that are published. (Mostly relevant for Jetpack sites).
1693 + if ( 'publish' !== $post->post_status ) {
1694 + return false;
1695 + }
1696 +
1697 + // If it's not flagged as ready, then abort. @see ::flag_post_for_publicize().
1698 + if ( ! get_post_meta( $post->ID, $this->PENDING, true ) ) {
1699 + return false;
1700 + }
1701 +
1702 + return true;
1703 + }
1704 +
1705 + /**
1706 + * Is a given post type Publicize-able?
1707 + *
1708 + * Not every CPT lends itself to Publicize-ation. Allow CPTs to register by adding their CPT via
1709 + * the publicize_post_types array filter.
1710 + *
1711 + * @param string $post_type The post type to check.
1712 + * @return bool True if the post type can be Publicized.
1713 + */
1714 + public function post_type_is_publicizeable( $post_type ) {
1715 + if ( 'post' === $post_type ) {
1716 + return true;
1717 + }
1718 +
1719 + return post_type_supports( $post_type, 'publicize' );
1720 + }
1721 +
1722 + /**
1723 + * Already-published posts should not be Publicized by default. This filter sets checked to
1724 + * false if a post has already been published.
1725 + *
1726 + * Attached to the `publicize_checkbox_default` filter
1727 + *
1728 + * @param bool $checked True if checkbox is checked, false otherwise.
1729 + * @param int $post_id Post ID to set checkbox for.
1730 + * @return bool
1731 + */
1732 + public function publicize_checkbox_default( $checked, $post_id ) {
1733 + if ( 'publish' === get_post_status( $post_id ) ) {
1734 + return false;
1735 + }
1736 +
1737 + return $checked;
1738 + }
1739 +
1740 + /**
1741 + * Get the attached image for a post.
1742 + *
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 + * }
1751 + */
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 +
1793 + if ( ! empty( $generated_image_url ) ) {
1794 + return array(
1795 + 'url' => $generated_image_url,
1796 + 'width' => 1200,
1797 + 'height' => 630,
1798 + );
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' );
1897 + return $tags;
1898 + }
1899 +
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 + /**
1960 + * Util
1961 + */
1962 +
1963 + /**
1964 + * Converts a Publicize message template string into a sprintf format string
1965 + *
1966 + * @param string[] $args Array of arguments.
1967 + * 0 - The Publicize message template: 'Check out my post: %title% @ %url'
1968 + * ... - The template tags 'title', 'url', etc.
1969 + * @return string
1970 + */
1971 + protected static function build_sprintf( $args ) {
1972 + $string = null;
1973 + $search = array();
1974 + $replace = array();
1975 + foreach ( $args as $k => $arg ) {
1976 + if ( 0 === $k ) {
1977 + $string = $arg;
1978 + continue;
1979 + }
1980 + $search[] = "%$arg%";
1981 + $replace[] = "%$k\$s";
1982 + }
1983 + return str_replace( $search, $replace, $string );
1984 + }
1985 +
1986 + /**
1987 + * Get the URL to the connections management page.
1988 + *
1989 + * @return string
1990 + */
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 ) );
1997 + }
1998 +
1999 + /**
2000 + * Get the Jetpack Social info from the API.
2001 + *
2002 + * @param int $blog_id The WPCOM blog_id for the current blog.
2003 + * @return array
2004 + */
2005 + public function get_api_data( $blog_id ) {
2006 + static $api_data_response = null;
2007 + $key = 'jetpack_social_api_data';
2008 +
2009 + if ( isset( $api_data_response ) ) {
2010 + return ! is_wp_error( $api_data_response ) ? $api_data_response : array();
2011 + }
2012 +
2013 + $response = Client::wpcom_json_api_request_as_blog(
2014 + sprintf( 'sites/%d/jetpack-social', absint( $blog_id ) ),
2015 + '2',
2016 + array(
2017 + 'headers' => array( 'content-type' => 'application/json' ),
2018 + 'method' => 'GET',
2019 + ),
2020 + null,
2021 + 'wpcom'
2022 + );
2023 + $api_data_response = Publicize_Utils::make_proper_response( $response );
2024 +
2025 + if ( ! is_wp_error( $api_data_response ) ) {
2026 + set_transient( $key, $api_data_response, DAY_IN_SECONDS );
2027 + return $api_data_response;
2028 + }
2029 +
2030 + $cached_response = get_transient( $key );
2031 + if ( ! empty( $cached_response ) ) {
2032 + return $cached_response;
2033 + }
2034 +
2035 + return array();
2036 + }
2037 +
2038 + /**
2039 + * Check if enhanced publishing is enabled.
2040 + *
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
2044 + */
2045 + public function is_enhanced_publishing_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2046 + return $this->has_enhanced_publishing_feature();
2047 + }
2048 +
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' );
2056 + }
2057 +
2058 + /**
2059 + * Check if the social image generator is enabled.
2060 + *
2061 + * @deprecated 0.24.2 use Automattic\Jetpack\Publicize\Publicize_Base\has_social_image_generator_feature instead.
2062 + * @param int $blog_id The blog ID for the current blog.
2063 + * @return bool
2064 + */
2065 + public function is_social_image_generator_enabled( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2066 + return $this->has_social_image_generator_feature();
2067 + }
2068 +
2069 + /**
2070 + * Check if the social image generator is enabled.
2071 + *
2072 + * @return bool
2073 + */
2074 + public function has_social_image_generator_feature() {
2075 + return Current_Plan::supports( 'social-image-generator' );
2076 + }
2077 +
2078 + /**
2079 + * Check if the auto-conversion feature is one of the active features.
2080 + *
2081 + * TODO: Remove this after certain releases of Jetpack v15.
2082 + *
2083 + * @param string $type Whether image or video.
2084 + *
2085 + * @return bool
2086 + */
2087 + public function has_social_auto_conversion_feature( $type ) {
2088 + return Current_Plan::supports( "social-$type-auto-convert" );
2089 + }
2090 +
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 + /**
2127 + * Check if we have a paid Jetpack Social plan.
2128 + *
2129 + * @param bool $refresh_from_wpcom Whether to force refresh the plan check.
2130 + *
2131 + * @return bool True if we have a paid plan, false otherwise.
2132 + */
2133 + public function has_paid_plan( $refresh_from_wpcom = false ) {
2134 + static $has_paid_plan = null;
2135 + if ( null === $has_paid_plan ) {
2136 + $has_paid_plan = Current_Plan::supports( 'social-shares-1000', $refresh_from_wpcom );
2137 + }
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'];
2174 + }
2175 +}
2176 +
2177 +// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
2178 +
2179 +/**
2180 + * Get Calypso URL for Publicize connections.
2181 + *
2182 + * @return string
2183 + */
2184 +function publicize_calypso_url() {
2185 + _deprecated_function( __METHOD__, '0.2.0', 'Publicize::publicize_connections_url' );
2186 + return Redirect::get_url( 'calypso-marketing-connections', array( 'site' => ( new Status() )->get_site_suffix() ) );
2187 +}
2188 +
2189 +/**
2190 + * Adds support for the post-thumbnails feature, regardless of underlying theme support.
2191 + *
2192 + * This ensures the featured image UI appears in the editor, allowing the user to
2193 + * explicitly set an image for their social media post.
2194 + */
2195 +function add_theme_post_thumbnails_support() {
2196 + add_theme_support( 'post-thumbnails', get_post_types_by_support( 'publicize' ) );
2197 +}