PluginProbe
Social Media Auto Poster – Schedule & Publish to Buffer / 6.2.5
Social Media Auto Poster – Schedule & Publish to Buffer v6.2.5
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.2 6.1.1 6.1.0 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 All 126 releases
wp-to-buffer / lib / social / includes / class-publish.php

class-publish.php in Social Media Auto Poster – Schedule & Publish to Buffer 6.2.5, at lib/social/includes/class-publish.php

2,208 lines 75.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Publish class
4 *
5 * @package WPZinc\Social
6 * @author WP Zinc
7 */
8
9 namespace WPZinc\Social;
10
11 /**
12 * Handles publishing status(es) to the scheduling service
13 * based on the Post and Plugin settings, when a Post's
14 * status is transitioned.
15 *
16 * @package WPZinc\Social
17 * @author WP Zinc
18 * @version 3.0.0
19 */
20 class Publish {
21
22 /**
23 * Holds the base class object.
24 *
25 * @since 3.2.4
26 *
27 * @var object
28 */
29 public $base;
30
31 /**
32 * Holds all supported Tags and their Post data replacements.
33 *
34 * @since 3.7.8
35 *
36 * @var array|false
37 */
38 private $all_possible_searches_replacements = false;
39
40 /**
41 * Holds searches and replacements for status messages.
42 *
43 * @since 3.7.8
44 *
45 * @var array|false
46 */
47 private $searches_replacements = false;
48
49 /**
50 * Constructor
51 *
52 * @since 3.0.0
53 *
54 * @param object $base Base Plugin Class.
55 */
56 public function __construct( $base ) {
57
58 // Store base class.
59 $this->base = $base;
60
61 // Actions.
62 add_action( 'wp_loaded', array( $this, 'register_publish_hooks' ), 1 );
63 add_filter( $this->base->plugin->name, array( $this, 'publish' ), 1, 2 );
64
65 }
66
67 /**
68 * Registers publish hooks against all public Post Types,
69 *
70 * @since 3.0.0
71 */
72 public function register_publish_hooks() {
73
74 add_action( 'transition_post_status', array( $this, 'transition_post_status' ), 10, 3 );
75
76 }
77
78 /**
79 * Fired when a Post's status transitions. Called by WordPress when wp_insert_post() is called,
80 * and wp_insert_post() is called by WordPress and the REST API whenever creating or updating a Post.
81 *
82 * @since 3.1.6
83 *
84 * @param string $new_status New Status.
85 * @param string $old_status Old Status.
86 * @param \WP_Post $post Post.
87 */
88 public function transition_post_status( $new_status, $old_status, $post ) {
89
90 // Bail if the Post Type isn't public.
91 // This prevents the rest of this routine running on e.g. ACF Free, when saving Fields (which results in Field loss).
92 $post_types = array_keys( $this->base->get_class( 'common' )->get_post_types() );
93 if ( ! in_array( $post->post_type, $post_types, true ) ) {
94 return;
95 }
96
97 // New Post Screen loading.
98 // Draft saved.
99 if ( $new_status === 'auto-draft' || $new_status === 'draft' || $new_status === 'inherit' || $new_status === 'trash' ) {
100 return;
101 }
102
103 // Remove actions registered by this Plugin.
104 // This ensures that when Page Builders call publish or update events via AJAX, we don't run this multiple times.
105 remove_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 );
106 remove_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_publish' ), 10 );
107 remove_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 );
108 remove_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_update' ), 10 );
109
110 /**
111 * = REST API =
112 * If this is a REST API Request, we can't use the wp_insert_post action, because the metadata
113 * is *not* included in the call to wp_insert_post(). Instead, we must use a late REST API action
114 * that gives the REST API time to save metadata.
115 * Note that the meta being supplied in the REST API Request must be registered with WordPress using
116 * register_meta()
117 *
118 * = Gutenberg =
119 * If Gutenberg is being used on the given Post Type, two requests are sent:
120 * - a REST API request, comprising of Post Data and Metadata registered in Gutenberg,
121 * - a standard request, comprising of Post Metadata registered outside of Gutenberg (i.e. add_meta_box() data)
122 * The second request will be seen by transition_post_status() as an update.
123 * Therefore, we set a meta flag on the first Gutenberg REST API request to defer publishing the status until
124 * the second, standard request - at which point, all Post metadata will be available to the Plugin.
125 *
126 * = Classic Editor =
127 * Metadata is included in the call to wp_insert_post(), meaning that it's saved to the Post before we use it.
128 */
129
130 $this->base->get_class( 'log' )->add_to_debug_log( 'Post ID: #' . $post->ID );
131
132 // If transitioning from future to publish, this is a scheduled Post being published by WordPress Cron.
133 // We don't need to know whether it's a Gutenberg, Classic Editor or REST API request.
134 if ( $old_status === 'future' && $new_status === 'publish' ) {
135 $this->base->get_class( 'log' )->add_to_debug_log( 'Scheduled Post being published by WordPress' );
136
137 add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 );
138
139 // Don't need to do anything else, so exit.
140 return;
141 }
142
143 // Flag to determine if the current Post is a Gutenberg Post or Rest API Request.
144 $is_gutenberg_request = $this->is_gutenberg_request();
145 $is_rest_api_request = $this->is_rest_api_request();
146 $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg Post: ' . ( $is_gutenberg_request ? 'Yes' : 'No' ) );
147 $this->base->get_class( 'log' )->add_to_debug_log( 'REST API Request: ' . ( $is_rest_api_request ? 'Yes' : 'No' ) );
148
149 // If a previous request flagged that an 'update' request should be treated as a publish request (i.e.
150 // we're using Gutenberg and request to post.php was made after the REST API), do this now.
151 $needs_publishing = get_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_publishing', true );
152 if ( $needs_publishing ) {
153 $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Needs Publishing' );
154
155 // Run Publish Status Action now.
156 delete_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_publishing' );
157 add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 );
158
159 // Don't need to do anything else, so exit.
160 return;
161 }
162
163 // If a previous request flagged that an update request be deferred (i.e.
164 // we're using Gutenberg and request to post.php was made after the REST API), do this now.
165 $needs_updating = get_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_updating', true );
166 if ( $needs_updating ) {
167 $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Needs Updating' );
168
169 // Run Publish Status Action now.
170 delete_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_updating' );
171 add_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 );
172
173 // Don't need to do anything else, so exit.
174 return;
175 }
176
177 // Publish.
178 if ( $new_status === 'publish' && $new_status !== $old_status ) {
179 /**
180 * Gutenberg Editor REST API Request
181 * - Non-Gutenberg metaboxes are POSTed via a second, separate request to post.php, which appears
182 * as an 'update'. Define a meta key that we'll check on the separate request later.
183 */
184 if ( $is_gutenberg_request ) {
185 $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Defer Publish' );
186
187 update_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_publishing', 1 );
188
189 // Don't need to do anything else, so exit.
190 return;
191 }
192
193 /**
194 * REST API
195 */
196 if ( $is_rest_api_request ) {
197 $this->base->get_class( 'log' )->add_to_debug_log( 'REST API: Publish' );
198 add_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_publish' ), 10, 1 );
199
200 // Don't need to do anything else, so exit.
201 return;
202 }
203
204 /**
205 * Classic Editor
206 */
207 $this->base->get_class( 'log' )->add_to_debug_log( 'Classic Editor: Publish' );
208 add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 );
209
210 // Don't need to do anything else, so exit.
211 return;
212 }
213
214 // Update.
215 if ( $new_status === 'publish' && $old_status === 'publish' ) { // @phpstan-ignore-line
216 /**
217 * Gutenberg Editor REST API Request
218 * - Non-Gutenberg metaboxes are POSTed via a second, separate request to post.php, which appears
219 * as an 'update'. Define a meta key that we'll check on the separate request later.
220 */
221 if ( $is_gutenberg_request ) {
222 $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Defer Update' );
223
224 update_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_updating', 1 );
225
226 // Don't need to do anything else, so exit.
227 return;
228 }
229
230 /**
231 * REST API
232 */
233 if ( $is_rest_api_request ) {
234 $this->base->get_class( 'log' )->add_to_debug_log( 'REST API: Update' );
235 add_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_update' ), 10, 1 );
236
237 // Don't need to do anything else, so exit.
238 return;
239 }
240
241 /**
242 * Classic Editor
243 */
244 $this->base->get_class( 'log' )->add_to_debug_log( 'Classic Editor: Update' );
245 add_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 );
246
247 // Don't need to do anything else, so exit.
248 return;
249 }
250
251 }
252
253 /**
254 * Helper function to determine if the request is a Gutenberg REST API request.
255 *
256 * @since 3.9.1
257 *
258 * @return bool Is Gutenberg REST API Request
259 */
260 private function is_gutenberg_request() {
261
262 if ( ! defined( 'REST_REQUEST' ) ) {
263 return false;
264 }
265
266 if ( ! REST_REQUEST ) {
267 return false;
268 }
269
270 // Gutenberg requests are REST API requests, but include a _locale key.
271 // 'True' REST API requests do not include this key.
272 if ( ! filter_has_var( INPUT_POST, '_locale' ) && ! filter_has_var( INPUT_GET, '_locale' ) ) {
273 return false;
274 }
275
276 return true;
277
278 }
279
280 /**
281 * Helper function to determine if the request is a REST API request.
282 *
283 * @since 3.9.1
284 *
285 * @return bool Is REST API Request
286 */
287 private function is_rest_api_request() {
288
289 if ( ! defined( 'REST_REQUEST' ) ) {
290 return false;
291 }
292
293 if ( ! REST_REQUEST ) {
294 return false;
295 }
296
297 // Gutenberg requests are REST API requests, but include a _locale key.
298 // 'True' REST API requests do not include this key.
299 if ( filter_has_var( INPUT_POST, '_locale' ) || filter_has_var( INPUT_GET, '_locale' ) ) {
300 return false;
301 }
302
303 return true;
304
305 }
306
307 /**
308 * Helper function to determine if the Post contains Gutenberg Content.
309 *
310 * @since 3.9.1
311 *
312 * @param \WP_Post $post Post.
313 * @return bool Post Content contains Gutenberg Block Markup
314 */
315 private function is_gutenberg_post_content( $post ) {
316
317 if ( strpos( $post->post_content, '<!-- wp:' ) !== false ) {
318 return true;
319 }
320
321 return false;
322
323 }
324
325 /**
326 * Called when a Post has been Published via the REST API.
327 *
328 * @since 3.6.8
329 *
330 * @param \WP_Post $post Post.
331 */
332 public function rest_api_post_publish( $post ) {
333
334 $this->wp_insert_post_publish( $post->ID );
335
336 }
337
338 /**
339 * Called when a Post has been Published via the REST API
340 *
341 * @since 3.6.8
342 *
343 * @param \WP_Post $post Post.
344 */
345 public function rest_api_post_update( $post ) {
346
347 $this->wp_insert_post_update( $post->ID );
348
349 }
350
351 /**
352 * Called when a Post has been Published
353 *
354 * @since 3.6.2
355 *
356 * @param int $post_id Post ID.
357 */
358 public function wp_insert_post_publish( $post_id ) {
359
360 // Get Test Mode Flag.
361 $test_mode = $this->base->get_class( 'settings' )->get_option( 'test_mode', false );
362
363 // Call main function to publish status(es) to social media.
364 $results = $this->publish( $post_id, 'publish', $test_mode );
365
366 // If no result, bail.
367 if ( ! isset( $results ) ) {
368 return;
369 }
370
371 // If no errors, return.
372 if ( ! is_wp_error( $results ) ) {
373 return;
374 }
375
376 // If logging is disabled, return.
377 $log_enabled = $this->base->get_class( 'log' )->is_enabled();
378 if ( ! $log_enabled ) {
379 return;
380 }
381
382 // The result is a single warning caught before any statuses were sent to the API.
383 // Add the warning to the log so that the user can see why no statuses were sent to API.
384 $this->base->get_class( 'log' )->add(
385 $post_id,
386 array(
387 'action' => 'publish',
388 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
389 'result' => 'warning',
390 'result_message' => $results->get_error_message(),
391 )
392 );
393
394 }
395
396 /**
397 * Called when a Post has been Updated
398 *
399 * @since 3.6.2
400 *
401 * @param int $post_id Post ID.
402 */
403 public function wp_insert_post_update( $post_id ) {
404
405 // If a status was last sent within 5 seconds, don't send it again.
406 // Prevents Page Builders that trigger wp_update_post() multiple times on Publish or Update from
407 // causing statuses to send multiple times.
408 $last_sent = get_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_last_sent', true );
409 if ( ! empty( $last_sent ) ) {
410 $difference = ( time() - $last_sent );
411 if ( $difference < 5 ) {
412 return;
413 }
414 }
415
416 // Get Test Mode Flag.
417 $test_mode = $this->base->get_class( 'settings' )->get_option( 'test_mode', false );
418
419 // Call main function to publish status(es) to social media.
420 $results = $this->publish( $post_id, 'update', $test_mode );
421
422 // If no result, bail.
423 if ( ! isset( $results ) ) {
424 return;
425 }
426
427 // If no errors, return.
428 if ( ! is_wp_error( $results ) ) {
429 return;
430 }
431
432 // If logging is disabled, return.
433 $log_enabled = $this->base->get_class( 'log' )->is_enabled();
434 if ( ! $log_enabled ) {
435 return;
436 }
437
438 // The result is a single error caught before any statuses were sent to the API.
439 // Add the error to the log so that the user can see why no statuses were sent to API.
440 $this->base->get_class( 'log' )->add(
441 $post_id,
442 array(
443 'action' => 'update',
444 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
445 'result' => 'warning',
446 'result_message' => $results->get_error_message(),
447 )
448 );
449
450 }
451
452 /**
453 * Main function. Called when any Page, Post or CPT is published, updated, reposted
454 * or bulk published.
455 *
456 * @since 3.0.0
457 *
458 * @param int $post_id Post ID.
459 * @param string $action Action (publish|update|repost|bulk_publish).
460 * @param bool $test_mode Test Mode (won't send to API).
461 * @return mixed \WP_Error | API Results array
462 */
463 public function publish( $post_id, $action, $test_mode = false ) {
464
465 $this->base->get_class( 'log' )->add_to_debug_log( $this->base->plugin->displayName . ': publish(): Post ID: #' . $post_id );
466 $this->base->get_class( 'log' )->add_to_debug_log( $this->base->plugin->displayName . ': publish(): Action: ' . $action );
467 $this->base->get_class( 'log' )->add_to_debug_log( $this->base->plugin->displayName . ': publish(): Test Mode: ' . ( $test_mode ? 'Yes' : 'No' ) );
468
469 // Get settings, validating the Post and Action.
470 $settings = $this->validate( $post_id, $action );
471
472 // If an error occured, bail.
473 if ( is_wp_error( $settings ) ) {
474 $this->base->get_class( 'log' )->add_to_debug_log( $this->base->plugin->displayName . ': publish(): Settings Error: ' . $settings->get_error_message() );
475 return $settings;
476 }
477
478 // If settings are false, we're not sending this Post, so there's no need to schedule an event.
479 if ( ! $settings ) {
480 return false;
481 }
482
483 // Get post.
484 $post = get_post( $post_id );
485
486 // Clear any cached data that we have stored in this class.
487 $this->clear_search_replacements();
488
489 // Check at least one account is connected.
490 if ( ! $this->base->get_class( 'settings' )->account_connected() ) {
491 return new \WP_Error(
492 'no_access_token',
493 sprintf(
494 /* translators: %1$s: Social Media Service Name (Buffer, Hootsuite), %2$s: Plugin Name */
495 __( 'The Plugin has not been authorized with %1$s! Go to %2$s > Settings to setup the plugin.', 'wp-to-buffer' ),
496 $this->base->plugin->account,
497 $this->base->plugin->displayName
498 )
499 );
500 }
501
502 // Get Profiles.
503 $profiles = array();
504 foreach ( $this->base->get_class( 'settings' )->get_accounts() as $account_id => $account ) {
505 // Configure API for this account and fetch its profiles.
506 $this->base->get_class( 'api' )->set_tokens( $account['access_token'], $account['refresh_token'], $account['token_expires'] );
507 $account_profiles = $this->base->get_class( 'api' )->profiles( false, $account_id );
508
509 // Display an error.
510 if ( is_wp_error( $account_profiles ) ) {
511 $this->base->get_class( 'notices' )->add_error_notice( $account_profiles->get_error_message() );
512
513 // Log the connection error so it's visible in the Logs screen, not just debug.log.
514 if ( $this->base->get_class( 'log' )->is_enabled() ) {
515 $this->base->get_class( 'log' )->add(
516 $post_id,
517 array(
518 'action' => $action,
519 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
520 'result' => 'error',
521 'result_message' => $account_profiles->get_error_message(),
522 )
523 );
524 }
525
526 continue;
527 }
528
529 // Merge profiles with existing profiles from other accounts.
530 // array_merge() is not used here as it will re-index numeric keys.
531 foreach ( $account_profiles as $profile ) {
532 $profiles[ $profile['id'] ] = $profile;
533 }
534 }
535
536 // Array for storing statuses we'll send to the API.
537 $statuses = array();
538
539 // Iterate through each social media profile.
540 foreach ( $settings as $profile_id => $profile_settings ) {
541
542 // Skip some setting keys that aren't related to profiles.
543 if ( in_array( $profile_id, array( 'featured_image', 'additional_images', 'override' ), true ) ) {
544 continue;
545 }
546
547 // Skip if the Profile ID does not exist in the $profiles array, it's been removed from the API.
548 if ( $profile_id !== 'default' && ! isset( $profiles[ $profile_id ] ) ) {
549 continue;
550 }
551
552 // If the Profile's ID belongs to a Google Social Media Profile, skip it, as this is no longer supported
553 // as Google+ closed down.
554 if ( $profile_id !== 'default' && $profiles[ $profile_id ]['service'] === 'google' ) {
555 continue;
556 }
557
558 // Get detailed settings from Post or Plugin.
559 // Use Plugin Settings.
560 $profile_enabled = $this->base->get_class( 'settings' )->get_setting( $post->post_type, '[' . $profile_id . '][enabled]', 0 );
561 $profile_override = $this->base->get_class( 'settings' )->get_setting( $post->post_type, '[' . $profile_id . '][override]', 0 );
562
563 // Use Override Settings.
564 if ( $profile_override ) {
565 $action_enabled = $this->base->get_class( 'settings' )->get_setting( $post->post_type, '[' . $profile_id . '][' . $action . '][enabled]', 0 );
566 $status_settings = $this->base->get_class( 'settings' )->get_setting( $post->post_type, '[' . $profile_id . '][' . $action . '][status]', array() );
567 } else {
568 $action_enabled = $this->base->get_class( 'settings' )->get_setting( $post->post_type, '[default][' . $action . '][enabled]', 0 );
569 $status_settings = $this->base->get_class( 'settings' )->get_setting( $post->post_type, '[default][' . $action . '][status]', array() );
570 }
571
572 // Check if this profile is enabled.
573 if ( ! $profile_enabled ) {
574 continue;
575 }
576
577 // Check if this profile's action is enabled.
578 if ( ! $action_enabled ) {
579 continue;
580 }
581
582 // Determine which social media service this profile ID belongs to.
583 $service = false;
584 $account = false;
585 foreach ( $profiles as $profile ) {
586 if ( $profile['id'] == $profile_id ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
587 $service = $profile['service'];
588 break;
589 }
590 }
591
592 // Iterate through each Status.
593 foreach ( $status_settings as $index => $status ) {
594 // Add the status to our array for it to be sent to the API.
595 $status = $this->build_args( $post, $profile_id, $service, $status, $action, $account );
596
597 // If the status built is a WP_Error, something went wrong with e.g. the image.
598 // Include the error object and the profile ID, so the error is logged.
599 if ( is_wp_error( $status ) ) {
600 $status = array(
601 'profile_ids' => array( $profile_id ),
602 'error' => $status,
603 );
604 }
605
606 // Add status to array of statuses.
607 $statuses[] = $status;
608 }
609 }
610
611 $this->base->get_class( 'log' )->add_to_debug_log( $this->base->plugin->displayName . ': publish(): Statuses: ' . print_r( $statuses, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
612
613 // Check if any statuses exist.
614 // If not, exit.
615 if ( count( $statuses ) === 0 ) {
616 // Fetch Post Type object and Settings URL.
617 $post_type_object = get_post_type_object( $post->post_type );
618 $plugin_url = admin_url( 'admin.php?page=' . $this->base->plugin->name . '-settings&tab=post&type=' . $post->post_type );
619 $post_url = admin_url( 'post.php?post=' . $post_id . '&action=edit' );
620
621 // Return an error, depending on why no statuses were found.
622 $error = new \WP_Error(
623 $this->base->plugin->filter_name . '_no_statuses_enabled',
624 sprintf(
625 /* translators: %1$s: Post Type Name, Singular, %2$s: Social Media Service Name (Buffer, Hootsuite), %3$s: Action (Publish, Update, Repost, Bulk Publish), %4$s, %5$s, %6$s: Post Type Name, Singular, %7$s: Social Media Service Name (Buffer, Hootsuite), %8$s: Plugin URL, %9$s: Plugin Name, %10$s: Post Type Name, Singular, %11$s: Action (Publish, Update, Repost, Bulk Publish) */
626 __( 'No Plugin Settings are defined for sending %1$s to %2$s when you %3$s a %4$s. To send statuses to %5$s on %6$s, navigate to <a href="%7$s" target="_blank">%8$s > Settings > %9$s Tab > %10$s Action Tab</a>, tick "Enabled", and also enable at least one social media profile.', 'wp-to-buffer' ),
627 $post_type_object->labels->name,
628 $this->base->plugin->account,
629 ucwords( str_replace( '_', ' ', $action ) ),
630 $post_type_object->labels->singular_name,
631 $this->base->plugin->account,
632 ucwords( str_replace( '_', ' ', $action ) ),
633 $plugin_url,
634 $this->base->plugin->displayName,
635 $post_type_object->labels->name,
636 ucwords( str_replace( '_', ' ', $action ) )
637 )
638 );
639
640 $this->base->get_class( 'log' )->add_to_debug_log( $this->base->plugin->displayName . ': publish(): Statuses Error: ' . $error->get_error_message() );
641
642 return $error;
643 }
644
645 /**
646 * Determine the statuses to send, just before they're sent. Statuses can be added, edited
647 * and/or deleted as necessary here.
648 *
649 * @since 3.0.0
650 *
651 * @param array $statuses Statuses to be sent to social media.
652 * @param int $post_id Post ID.
653 * @param string $action Action (publish, update, repost).
654 */
655 $statuses = apply_filters( $this->base->plugin->filter_name . '_publish_statuses', $statuses, $post_id, $action );
656
657 // Debugging.
658 $this->base->get_class( 'log' )->add_to_debug_log( 'Statuses: ' . print_r( $statuses, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
659
660 // Send status messages to the API.
661 $results = $this->send( $statuses, $post_id, $action, $profiles, $test_mode );
662
663 // If no results, we're finished.
664 if ( empty( $results ) ) {
665 return false;
666 }
667
668 return $results;
669
670 }
671
672 /**
673 * Performs pre-publish and pre-schedule publish validation checks, including
674 * - if the action is supported
675 * - if the Post exists
676 * - if the Post Type's supported
677 * - whether the Post override disables sending statuses
678 *
679 * @since 4.3.3
680 *
681 * @param int $post_id Post ID.
682 * @param string $action Action (publish|update).
683 * @return mixed \WP_Error | API Results array
684 */
685 private function validate( $post_id, $action ) {
686
687 // Bail if the action isn't supported.
688 $supported_actions = array_keys( $this->base->get_class( 'common' )->get_post_actions() );
689 if ( ! in_array( $action, $supported_actions, true ) ) {
690 return new \WP_Error(
691 $this->base->plugin->filter_name . '_publish_invalid_action',
692 sprintf(
693 /* translators: Action */
694 __( 'The %s action is not supported.', 'wp-to-buffer' ),
695 $action
696 )
697 );
698 }
699
700 // Get Post.
701 $post = get_post( $post_id );
702 if ( ! $post ) {
703 return new \WP_Error(
704 'no_post',
705 sprintf(
706 /* translators: Post ID */
707 __( 'No WordPress Post could be found for Post ID %s', 'wp-to-buffer' ),
708 $post_id
709 )
710 );
711 }
712
713 // Bail if the Post Type isn't supported.
714 // This prevents non-public Post Types sending status(es) where Post Level Default = Post using Manual Settings
715 // and this non-public Post Type has been created by copying metadata from a public Post Type that specifies.
716 // Post-specific status settings.
717 $supported_post_types = array_keys( $this->base->get_class( 'common' )->get_post_types() );
718 if ( ! in_array( get_post_type( $post ), $supported_post_types, true ) ) {
719 return false;
720 }
721
722 return $this->base->get_class( 'settings' )->get_settings( get_post_type( $post ) );
723
724 }
725
726 /**
727 * Helper method to build arguments and create a status via the API
728 *
729 * @since 3.0.0
730 *
731 * @param \WP_Post $post Post.
732 * @param string $profile_id Profile ID.
733 * @param string $service Service.
734 * @param array $status Status Settings.
735 * @param string $action Action (publish|update|repost|bulk_publish).
736 * @param bool|array $account Account.
737 * @return array|\WP_Error
738 */
739 private function build_args( $post, $profile_id, $service, $status, $action, $account = false ) {
740
741 // For some services, the post_type may need to be changed to a supported post type.
742 // This might happen if e.g. only defaults are set, and per-profile settings are not defined.
743 switch ( $service ) {
744 /**
745 * Instagram:
746 * - If `image` or `story` is not specified, default to `image`.
747 */
748 case 'instagram':
749 if ( ! in_array( $status['post_type'], array( 'image', 'story' ), true ) ) {
750 $status['post_type'] = 'image';
751 }
752 break;
753
754 /**
755 * TikTok:
756 * - If `image` is not specified, default to `image`.
757 */
758 case 'tiktok':
759 if ( ! in_array( $status['post_type'], array( 'image' ), true ) ) {
760 $status['post_type'] = 'image';
761 }
762 break;
763
764 /**
765 * Pinterest: Change post type to `pin`.
766 */
767 case 'pinterest':
768 $status['post_type'] = 'pin';
769 break;
770
771 /**
772 * Google Business: Change post type to `googlebusiness`.
773 */
774 case 'googlebusiness':
775 $status['post_type'] = 'googlebusiness';
776 break;
777 }
778
779 // Build API compatible arguments.
780 $thumbnail = $this->get_post_image( $post, $service, $status['post_type'] );
781 $args = array(
782 'account' => $account,
783 'post_type' => $status['post_type'],
784 'profile_ids' => array( $profile_id ),
785 'text' => $this->parse_text( $post, $status['message'], ( $service === 'instagram' ? true : false ) ),
786 );
787
788 // Shorten URLs.
789 if ( $this->base->supports( 'url_shortening' ) ) {
790 $args['shorten'] = ( $this->base->get_class( 'settings' )->get_option( 'disable_url_shortening', false ) ? false : true );
791 }
792
793 // Drafts.
794 if ( $this->base->supports( 'drafts' ) ) {
795 $args['is_draft'] = $this->base->get_class( 'settings' )->get_option( 'is_draft', false );
796 }
797
798 // URL.
799 switch ( $status['post_type'] ) {
800 /**
801 * Link
802 */
803 case 'link':
804 case 'pin':
805 case 'googlebusiness':
806 // If no URL is specified in the status, use the Post's URL.
807 $status['url'] = empty( $status['url'] ) ? '{url}' : $status['url'];
808
809 // Get URL.
810 $url = $this->parse_text( $post, $status['url'] );
811
812 // If URL is empty, don't include it in the args.
813 if ( empty( $url ) ) {
814 break;
815 }
816
817 // Add URL to args.
818 $args['url'] = $url;
819 break;
820 }
821
822 // Image(s).
823 switch ( $status['post_type'] ) {
824 case 'pin':
825 case 'googlebusiness':
826 case 'story':
827 case 'image':
828 switch ( $status['image'] ) {
829 /**
830 * Featured, Additional or Content Image
831 * 1 and 2 are used for backward compatibility where settings are not updated.
832 */
833 case 'featured_image':
834 case '1':
835 case '2':
836 // Plugin's First (Featured) Image, Post's Featured Image or Post Content's First Image.
837 $image = $this->get_post_image( $post, $service, $status['post_type'] );
838
839 // If the image is a WP_Error object, log it and return.
840 if ( is_wp_error( $image ) ) {
841 $this->base->get_class( 'log' )->add_to_debug_log( 'Image Error: ' . $image->get_error_message() );
842 return $image;
843 }
844
845 // Add image to media_urls, if one was found.
846 if ( $image !== false ) {
847 $args['media_urls'] = array( $image );
848 }
849 break;
850
851 }
852 }
853
854 // Scheduling.
855 switch ( $status['schedule'] ) {
856 case 'queue_end':
857 case 'queue_start':
858 case 'immediate':
859 $args['schedule_type'] = $status['schedule'];
860 break;
861
862 default:
863 $args['schedule_type'] = 'queue_end';
864 break;
865 }
866
867 /**
868 * Determine the standardised arguments array to send via the API for a status message's settings.
869 *
870 * @since 3.0.0
871 *
872 * @param array $args API standardised arguments.
873 * @param \WP_Post $post WordPress Post.
874 * @param string $profile_id Social Media Profile ID.
875 * @param string $service Social Media Service.
876 * @param array $status Parsed Status Message Settings.
877 * @param string $action Action (publish|update|repost|bulk_publish).
878 */
879 $args = apply_filters( $this->base->plugin->filter_name . '_publish_build_args', $args, $post, $profile_id, $service, $status, $action );
880
881 // Return args.
882 return $args;
883
884 }
885
886 /**
887 * Attempts to fetch the primary Post's Image, in the following order:
888 * - Plugin's First (Featured) Image
889 * - Post's Featured Image
890 * - Post's Content's First Image
891 *
892 * @since 3.9.8
893 *
894 * @param \WP_Post $post Post ID.
895 * @param string $service Social Media Service.
896 * @param bool|string $format Status format (for example, 'story' or 'post' for Instagram).
897 * @return array|bool|\WP_Error
898 */
899 private function get_post_image( $post, $service, $format = false ) {
900
901 // Featured Image.
902 $image_id = get_post_thumbnail_id( $post->ID );
903 if ( $image_id > 0 ) {
904 return $this->base->get_class( 'image' )->get_image_sources( $image_id, 'featured_image', $service, $format );
905 }
906
907 // If here, no image was found in the Post.
908 return false;
909
910 }
911
912 /**
913 * Populates the status message by replacing tags with Post/Author data
914 *
915 * @since 3.0.0
916 *
917 * @param \WP_Post $post Post.
918 * @param string $message Status Message to parse.
919 * @param bool $strip_urls Whether to strip URLs from the status message.
920 * @return string Parsed Status Message
921 */
922 public function parse_text( $post, $message, $strip_urls = false ) {
923
924 // Get Author.
925 $author = get_user_by( 'id', $post->post_author );
926
927 // If we haven't yet populated the searches and replacements for this Post, do so now.
928 if ( ! $this->all_possible_searches_replacements ) {
929 $this->all_possible_searches_replacements = $this->register_all_possible_searches_replacements( $post, $author );
930 }
931
932 // If no searches and replacements are defined, we can't parse anything.
933 if ( ! $this->all_possible_searches_replacements ) {
934 return $message;
935 }
936
937 // Extract all of the tags in the message.
938 preg_match_all( '|{(.+?)}|', $message, $matches );
939
940 // If no tags exist in the message, there's nothing to parse.
941 if ( count( $matches[0] ) === 0 ) {
942 return $message;
943 }
944
945 // Define return text.
946 $text = $message;
947
948 // Iterate through matches, adding them to the search / replacement array.
949 foreach ( $matches[1] as $index => $tag ) {
950 // Clean up some vars.
951 unset( $tag_params, $transformation, $replacement );
952
953 // Define some default attributes for this tag.
954 $tag_params = $this->get_default_tag_params( $matches[0][ $index ], $tag );
955
956 // If we already have a replacement for this exact tag (i.e. from a previous status message),
957 // we don't need to define the replacement again.
958 if ( isset( $this->searches_replacements[ $tag_params['tag_with_braces'] ] ) ) {
959 continue;
960 }
961
962 // Backward compatibility for word, sentence and character limit tags
963 // Store them in the tag parameter's transformations array.
964 if ( preg_match( '/(.*?)\((.*?)_words\)/', $tag_params['tag'], $word_limit_matches ) ) {
965 $tag_params['tag'] = $word_limit_matches[1];
966 $transformation = array(
967 'transformation' => 'words',
968 'arguments' => array(
969 absint( $word_limit_matches[2] ),
970 ),
971 );
972 } elseif ( preg_match( '/(.*?)\((.*?)_sentences\)/', $tag_params['tag'], $sentence_limit_matches ) ) {
973 $tag_params['tag'] = $sentence_limit_matches[1];
974 $transformation = array(
975 'transformation' => 'sentences',
976 'arguments' => array(
977 absint( $sentence_limit_matches[2] ),
978 ),
979 );
980 } elseif ( preg_match( '/(.*?)\((.*?)\)/', $tag_params['tag'], $character_limit_matches ) ) {
981 $tag_params['tag'] = $character_limit_matches[1];
982 $transformation = array(
983 'transformation' => 'characters',
984 'arguments' => array(
985 absint( $character_limit_matches[2] ),
986 ),
987 );
988 }
989 if ( isset( $transformation ) ) {
990 if ( is_array( $tag_params['transformations'] ) ) {
991 $tag_params['transformations'][] = $transformation;
992 } else {
993 $tag_params['transformations'] = array( $transformation );
994 }
995 }
996
997 // If this Tag is a Taxonomy Tag, fetch some parameters that may be included in the tag.
998 if ( preg_match( '/^taxonomy_(.*?)$/', $tag_params['tag'], $taxonomy_matches ) ) {
999 // Taxonomy with Hashtag Format.
1000 $tag_params['taxonomy'] = str_replace( 'taxonomy_', '', $tag_params['tag'] );
1001 }
1002
1003 // Fetch possible tag replacement value.
1004 $replacement = ( isset( $this->all_possible_searches_replacements[ $tag_params['tag'] ] ) ? $this->all_possible_searches_replacements[ $tag_params['tag'] ] : '' );
1005
1006 // If this is a taxonomy replacement, replace according to the tag parameters.
1007 if ( $tag_params['taxonomy'] !== false ) {
1008 // Define a string to hold the list of terms.
1009 $term_names = '';
1010
1011 // Iterate through terms, building string.
1012 foreach ( $replacement as $term_index => $term ) {
1013 // If there's a term limit and this term exceeds it, exit the loop.
1014 if ( $tag_params['taxonomy_term_limit'] > 0 && $term_index + 1 > $tag_params['taxonomy_term_limit'] ) {
1015 break;
1016 }
1017
1018 // Lowercase and decode HTML.
1019 $term_name = strtolower( str_replace( ' ', '', html_entity_decode( $term->name ) ) );
1020
1021 // Remove anything that isn't alphanumeric or an underscore, to ensure the whole hashtag is linked
1022 // when posted to social media and not broken by e.g. a full stop.
1023 $term_name = '#' . preg_replace( '/[^\p{L}\p{N}\p{M}_]+/u', '', $term_name );
1024
1025 /**
1026 * Defines the Taxonomy Term Hashtag to replace the status template tag.
1027 *
1028 * @since 3.0.0
1029 *
1030 * @param string $term_name Term Name.
1031 * @param string $taxonomy_term_format Term Format.
1032 * @param WP_Term $term Term.
1033 * @param string $taxonomy Taxonomy.
1034 * @param string $text Status Text.
1035 */
1036 $term_name = apply_filters( $this->base->plugin->filter_name . '_publish_parse_text_term_hashtag', $term_name, $tag_params['taxonomy_term_format'], $term, $tag_params['taxonomy'], $text );
1037
1038 /**
1039 * Backward compat filter to define the Taxonomy Term Name to replace the status template tag.
1040 * _publish_parse_text_term_name and _publish_parse_text_term_hashtag should be used instead.
1041 *
1042 * @since 3.0.0
1043 *
1044 * @param string $term_name Term Name.
1045 * @param string $term->name Term Name.
1046 * @param string $taxonomy Taxonomy.
1047 * @param string $text Status Text.
1048 * @param string $taxonomy_term_format Term Format.
1049 */
1050 $term_name = apply_filters( $this->base->plugin->filter_name . '_term', $term_name, $term->name, $tag_params['taxonomy'], $text, $tag_params['taxonomy_term_format'] );
1051
1052 // Add term to term names string.
1053 $term_names .= $term_name . ' ';
1054 }
1055
1056 // Finally, replace the array of terms with the string of formatted terms.
1057 $replacement = trim( $term_names );
1058 }
1059
1060 // Trim replacement.
1061 $replacement = trim( $replacement );
1062
1063 // Apply Transformations.
1064 if ( $tag_params['transformations'] ) {
1065 foreach ( $tag_params['transformations'] as $transformation ) {
1066 $replacement = $this->apply_text_transformation(
1067 $tag_params['tag'],
1068 $transformation['transformation'],
1069 $replacement,
1070 $transformation['arguments']
1071 );
1072 }
1073 }
1074
1075 // Add the search and replacement to the array.
1076 $this->searches_replacements[ $tag_params['tag_with_braces'] ] = $replacement;
1077
1078 } // Close foreach tag match in text.
1079
1080 // Search and Replace.
1081 $text = str_replace( array_keys( $this->searches_replacements ), $this->searches_replacements, $text );
1082
1083 // Execute any shortcodes in the text now.
1084 $text = do_shortcode( $text );
1085
1086 // Convert to plain text.
1087 $text = $this->convert_to_plain_text( $text, true, $strip_urls );
1088
1089 /**
1090 * Filters the parsed status message text on a status.
1091 *
1092 * @since 3.0.0
1093 *
1094 * @param string $text Parsed Text, no Tags.
1095 * @param string $message Unparsed Text with Tags.
1096 * @param array $searches_replacements Specific Tag Search and Replacements for the given Text.
1097 * @param array $all_possible_searches_replacements All Registered Tag Search and Replacements.
1098 * @param \WP_Post $post WordPress Post.
1099 * @param \WP_User $author WordPress User (Author).
1100 */
1101 $text = apply_filters( $this->base->plugin->filter_name . '_publish_parse_text', $text, $message, $this->searches_replacements, $this->all_possible_searches_replacements, $post, $author );
1102
1103 return $text;
1104
1105 }
1106
1107 /**
1108 * Parses the status' Google Business configuration to return an array of compatible
1109 * arguments that can be used to send the status.
1110 *
1111 * @since 4.9.0
1112 *
1113 * @param \WP_Post $post Post.
1114 * @param array $status Status.
1115 * @return bool|array Google Business Profile status configuration
1116 */
1117 public function parse_google_business( $post, $status ) {
1118
1119 // Bail if no Google Business configuration exists in the status.
1120 if ( ! isset( $status['googlebusiness'] ) ) {
1121 return false;
1122 }
1123 if ( ! is_array( $status['googlebusiness'] ) ) {
1124 return false;
1125 }
1126 if ( ! isset( $status['googlebusiness']['post_type'] ) ) {
1127 return false;
1128 }
1129
1130 // Start building arguments.
1131 $google_business_args = array(
1132 'post_type' => $status['googlebusiness']['post_type'],
1133 );
1134
1135 // Depending on the Google Business Post Type, build arguments.
1136 switch ( $status['googlebusiness']['post_type'] ) {
1137 case 'offer':
1138 case 'event':
1139 // Title.
1140 $google_business_args['title'] = $this->parse_text( $post, $status['googlebusiness']['title'] );
1141
1142 // Code and Terms: Offers.
1143 if ( $status['googlebusiness']['post_type'] === 'offer' ) {
1144 $google_business_args = array_merge(
1145 $google_business_args,
1146 array(
1147 'code' => $this->parse_text( $post, $status['googlebusiness']['code'], true ),
1148 'terms' => $this->parse_text( $post, $status['googlebusiness']['terms'], true ),
1149 )
1150 );
1151 } else {
1152 // Event: Button.
1153 $google_business_args['cta'] = $status['googlebusiness']['cta'];
1154 }
1155
1156 // Start Date.
1157 switch ( $status['googlebusiness']['start_date_option'] ) {
1158 /**
1159 * Custom Post Meta
1160 */
1161 case 'custom':
1162 // If no custom field key is set, set the start date to now.
1163 if ( empty( $status['googlebusiness']['start_date'] ) ) {
1164 $date = gmdate( 'Y-m-d H:i:s' );
1165 } else {
1166 // Fetch the Post's Meta Value based on the given Custom Field Key.
1167 $date = get_post_meta( $post->ID, $status['googlebusiness']['start_date'], true );
1168
1169 // If the post date is numeric, it's most likely a timestamp
1170 // Convert it to a date and time.
1171 if ( is_numeric( $date ) ) {
1172 $date = gmdate( 'Y-m-d H:i:s', $date );
1173 }
1174 }
1175
1176 // Set start date.
1177 $google_business_args['start_date'] = strtotime( $date );
1178 $google_business_args['start_time'] = gmdate( 'H:i', strtotime( $date ) );
1179 break;
1180
1181 /**
1182 * None
1183 */
1184 case '':
1185 break;
1186
1187 /**
1188 * Third Party integrations
1189 */
1190 default:
1191 $date = false;
1192
1193 /**
1194 * Allows integrations to define the status' start date for a Google Business Profile Offer or Event.
1195 *
1196 * @since 4.9.0
1197 *
1198 * @param bool|string $date Date (yyyy-mm-dd hh:mm:ss format).
1199 * @param array $google_business_args Google Business specific arguments for status.
1200 * @param array $status Status.
1201 * @param \WP_Post $post WordPress Post.
1202 */
1203 $date = apply_filters( $this->base->plugin->filter_name . '_publish_parse_google_business_start_date_' . $status['googlebusiness']['start_date_option'], $date, $google_business_args, $status, $post );
1204
1205 // Ignore if no date defined.
1206 if ( ! $date ) {
1207 break;
1208 }
1209
1210 // Set start date.
1211 $google_business_args['start_date'] = strtotime( $date );
1212 $google_business_args['start_time'] = gmdate( 'H:i', strtotime( $date ) );
1213 break;
1214 }
1215
1216 // End Date.
1217 switch ( $status['googlebusiness']['end_date_option'] ) {
1218 /**
1219 * Custom Post Meta
1220 */
1221 case 'custom':
1222 // If no custom field key is set, set the end date to now.
1223 if ( empty( $status['googlebusiness']['end_date'] ) ) {
1224 $date = gmdate( 'Y-m-d H:i:s' );
1225 } else {
1226 // Fetch the Post's Meta Value based on the given Custom Field Key.
1227 $date = get_post_meta( $post->ID, $status['googlebusiness']['end_date'], true );
1228
1229 // If the post date is numeric, it's most likely a timestamp
1230 // Convert it to a date and time.
1231 if ( is_numeric( $date ) ) {
1232 $date = gmdate( 'Y-m-d H:i:s', $date );
1233 }
1234 }
1235
1236 // Set end date.
1237 $google_business_args['end_date'] = strtotime( $date );
1238 $google_business_args['end_time'] = gmdate( 'H:i', strtotime( $date ) );
1239 break;
1240
1241 /**
1242 * None
1243 */
1244 case '':
1245 break;
1246
1247 /**
1248 * Third Party integrations
1249 */
1250 default:
1251 $date = false;
1252
1253 /**
1254 * Allows integrations to define the status' end date for a Google Business Profile Offer or Event.
1255 *
1256 * @since 4.9.0
1257 *
1258 * @param bool|string $date Date (yyyy-mm-dd hh:mm:ss format).
1259 * @param array $google_business_args Google Business specific arguments for status.
1260 * @param array $status Status.
1261 * @param \WP_Post $post WordPress Post.
1262 */
1263 $date = apply_filters( $this->base->plugin->filter_name . '_publish_parse_google_business_end_date_' . $status['googlebusiness']['end_date_option'], $date, $google_business_args, $status, $post );
1264
1265 // Ignore if no date defined.
1266 if ( ! $date ) {
1267 break;
1268 }
1269
1270 // Set end date.
1271 $google_business_args['end_date'] = strtotime( $date );
1272 $google_business_args['end_time'] = gmdate( 'H:i', strtotime( $date ) );
1273 break;
1274 }
1275 break;
1276
1277 case 'whats_new':
1278 default:
1279 $google_business_args['cta'] = $status['googlebusiness']['cta'];
1280 break;
1281 }
1282
1283 return $google_business_args;
1284
1285 }
1286
1287 /**
1288 * Returns default tag parameters for the given tag e.g. {title:transformation(args)} or {title}.
1289 *
1290 * @since 4.5.9
1291 *
1292 * @param string $tag_with_braces Tag with Braces e.g. {title:transformation(args)} or {title}.
1293 * @param string $tag Tag without Braces e.g. title:transformation(args) or title.
1294 * @return array Tag Parameters
1295 * */
1296 private function get_default_tag_params( $tag_with_braces, $tag ) {
1297
1298 // Define array of tag parameters to be populated.
1299 $tag_params = array(
1300 'tag_with_braces' => $tag_with_braces, // Original tag with braces, including transformations.
1301 'tag' => $tag, // No braces, no transformations.
1302 'transformations' => false,
1303 'taxonomy' => false,
1304 'taxonomy_term_limit' => false,
1305 'taxonomy_term_format' => false,
1306 );
1307
1308 // If no transformations exist, return.
1309 if ( strpos( $tag, ':' ) === false ) {
1310 return $tag_params;
1311 }
1312
1313 // Extract transformations.
1314 $tag_params['transformations'] = explode( ':', substr( $tag_params['tag'], strpos( $tag_params['tag'], ':' ) + 1 ) );
1315
1316 // Remove transformations from tag.
1317 $tag_params['tag'] = substr( $tag_params['tag'], 0, strpos( $tag_params['tag'], ':' ) );
1318
1319 // Iterate through transformations to see if arguments are attached.
1320 foreach ( $tag_params['transformations'] as $index => $transformation ) {
1321 // If no arguments exist for this transformation, update the array structure and continue.
1322 if ( strpos( $transformation, '(' ) === false ) {
1323 $tag_params['transformations'][ $index ] = array(
1324 'transformation' => $transformation,
1325 'arguments' => false,
1326 );
1327 continue;
1328 }
1329
1330 // Extract arguments.
1331 $arguments = explode( '(', substr( $transformation, strpos( $transformation, '(' ) + 1 ) );
1332 foreach ( $arguments as $a_index => $argument ) {
1333 $arguments[ $a_index ] = str_replace( ')', '', $argument );
1334 }
1335
1336 // Remove arguments from transformation.
1337 $transformation = substr( $transformation, 0, strpos( $transformation, '(' ) );
1338
1339 // Update array structure.
1340 $tag_params['transformations'][ $index ] = array(
1341 'transformation' => $transformation,
1342 'arguments' => $arguments,
1343 );
1344 }
1345
1346 // Return.
1347 return $tag_params;
1348
1349 }
1350
1351 /**
1352 * Applies a transformation to the given value
1353 *
1354 * @since 4.5.8
1355 *
1356 * @param string $tag Tag e.g. title, date.
1357 * @param string $transformation Transformation.
1358 * @param string $value Value.
1359 * @param mixed $transformation_arguments false | array of arguments to apply to the transformation e.g. character limit, date format.
1360 * @return string Transformed Value
1361 */
1362 private function apply_text_transformation( $tag, $transformation, $value, $transformation_arguments = false ) {
1363
1364 switch ( $transformation ) {
1365 /**
1366 * Word Limit
1367 */
1368 case 'words':
1369 // Don't attempt to apply limit if the tag doesn't support it.
1370 if ( ! $this->can_apply_character_limit_to_tag( $tag ) ) {
1371 return $value;
1372 }
1373
1374 // Don't attempt to apply limit if no limit is given.
1375 if ( ! $transformation_arguments ) {
1376 return $value;
1377 }
1378
1379 return $this->apply_word_limit( $value, $transformation_arguments[0] );
1380
1381 /**
1382 * Sentence Limit
1383 */
1384 case 'sentences':
1385 // Don't attempt to apply limit if the tag doesn't support it.
1386 if ( ! $this->can_apply_character_limit_to_tag( $tag ) ) {
1387 return $value;
1388 }
1389
1390 // Don't attempt to apply limit if no limit is given.
1391 if ( ! $transformation_arguments ) {
1392 return $value;
1393 }
1394
1395 return $this->apply_sentence_limit( $value, $transformation_arguments[0] );
1396
1397 /**
1398 * Character Limit
1399 */
1400 case 'characters':
1401 // Don't attempt to apply limit if the tag doesn't support it.
1402 if ( ! $this->can_apply_character_limit_to_tag( $tag ) ) {
1403 return $value;
1404 }
1405
1406 // Don't attempt to apply limit if no limit is given.
1407 if ( ! $transformation_arguments ) {
1408 return $value;
1409 }
1410
1411 return $this->apply_character_limit( $value, $transformation_arguments[0] );
1412
1413 /**
1414 * Other Transformations
1415 */
1416 default:
1417 /**
1418 * Applies the given transformation to the given value
1419 *
1420 * @since 4.5.8
1421 *
1422 * @param string $value Value.
1423 * @param string $transformation Transformation.
1424 */
1425 $value = apply_filters( $this->base->plugin->filter_name . '_publish_apply_text_transformation', $value, $transformation );
1426
1427 return $value;
1428 }
1429
1430 }
1431
1432 /**
1433 * Returns an array comprising of all supported tags and their Post / Author / Taxonomy data replacements.
1434 *
1435 * @since 3.7.8
1436 *
1437 * @param \WP_Post $post WordPress Post.
1438 * @param \WP_User $author WordPress User (Author of the Post).
1439 * @return array Search / Replacement Key / Value pairs
1440 */
1441 private function register_all_possible_searches_replacements( $post, $author ) {
1442
1443 // Start with no searches or replacements.
1444 $searches_replacements = array();
1445
1446 // Register Post Tags and Replacements.
1447 $searches_replacements = $this->register_post_searches_replacements( $searches_replacements, $post );
1448
1449 // Register Taxonomy Tags and Replacements.
1450 // Add Taxonomies.
1451 $taxonomies = get_object_taxonomies( $post->post_type, 'names' );
1452 if ( count( $taxonomies ) > 0 ) {
1453 $searches_replacements = $this->register_taxonomy_searches_replacements( $searches_replacements, $post, $taxonomies );
1454 }
1455
1456 /**
1457 * Registers any additional status message tags, and their Post data replacements, that are supported.
1458 *
1459 * @since 3.7.8
1460 *
1461 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1462 * @param \WP_Post $post WordPress Post.
1463 * @param \WP_User $author WordPress User (Author of the Post).
1464 */
1465 $searches_replacements = apply_filters( $this->base->plugin->filter_name . '_publish_get_all_possible_searches_replacements', $searches_replacements, $post, $author );
1466
1467 // Return filtered results.
1468 return $searches_replacements;
1469
1470 }
1471
1472 /**
1473 * Registers status message tags and their data replacements for the given Post.
1474 *
1475 * @since 3.7.8
1476 *
1477 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1478 * @param \WP_Post $post WordPress Post.
1479 * @return array Registered Supported Tags and their Replacements
1480 */
1481 private function register_post_searches_replacements( $searches_replacements, $post ) {
1482
1483 // Check Plugin Settings to see if the excerpt should fallback to the content if no
1484 // Excerpt defined.
1485 $excerpt_fallback = ( $this->base->get_class( 'settings' )->get_option( 'disable_excerpt_fallback', false ) ? false : true );
1486
1487 $searches_replacements['sitename'] = get_bloginfo( 'name' );
1488 $searches_replacements['title'] = $this->get_title( $post );
1489 $searches_replacements['excerpt'] = $this->get_excerpt( $post, $excerpt_fallback );
1490 $searches_replacements['content'] = $this->get_content( $post );
1491 $searches_replacements['content_more_tag'] = $this->get_content( $post, true );
1492 $searches_replacements['date'] = $this->get_date( $post );
1493 $searches_replacements['url'] = $this->get_permalink( $post );
1494 $searches_replacements['url_short'] = $this->get_short_permalink( $post );
1495 $searches_replacements['id'] = absint( $post->ID );
1496
1497 /**
1498 * Registers any additional status message tags, and their Post data replacements, that are supported
1499 * for the given Post.
1500 *
1501 * @since 3.7.8
1502 *
1503 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1504 * @param \WP_Post $post WordPress Post.
1505 */
1506 $searches_replacements = apply_filters( $this->base->plugin->filter_name . '_publish_register_post_searches_replacements', $searches_replacements, $post );
1507
1508 // Return filtered results.
1509 return $searches_replacements;
1510
1511 }
1512
1513 /**
1514 * Registers status message tags and their data replacements for the given Post Taxonomies.
1515 *
1516 * @since 3.7.8
1517 *
1518 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1519 * @param \WP_Post $post WordPress Post.
1520 * @param array $taxonomies Post Taxonomies.
1521 * @return array $searches_replacements Registered Supported Tags and their Replacements.
1522 */
1523 private function register_taxonomy_searches_replacements( $searches_replacements, $post, $taxonomies ) {
1524
1525 foreach ( $taxonomies as $taxonomy ) {
1526 $searches_replacements[ 'taxonomy_' . $taxonomy ] = wp_get_post_terms( $post->ID, $taxonomy );
1527 }
1528
1529 /**
1530 * Registers any additional status message tags, and their Post data replacements, that are supported
1531 * for the given Post.
1532 *
1533 * @since 3.7.8
1534 *
1535 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1536 * @param \WP_Post $post WordPress Post.
1537 * @param array $taxonomies Post Taxonomies.
1538 */
1539 $searches_replacements = apply_filters( $this->base->plugin->filter_name . '_publish_register_post_searches_replacements', $searches_replacements, $post, $taxonomies );
1540
1541 // Return filtered results.
1542 return $searches_replacements;
1543
1544 }
1545
1546 /**
1547 * Safely generate a title, stripping tags and shortcodes, and applying filters so that
1548 * third party plugins (such as translation plugins) can determine the final output.
1549 *
1550 * @since 3.7.3
1551 *
1552 * @param \WP_Post $post WordPress Post.
1553 * @return string Title
1554 */
1555 private function get_title( $post ) {
1556
1557 // Define title.
1558 $title = $this->convert_to_plain_text( get_the_title( $post ), false, true );
1559
1560 /**
1561 * Filters the dynamic {title} replacement, when a Post's status is being built.
1562 *
1563 * @since 3.7.3
1564 *
1565 * @param string $title Post Title.
1566 * @param \WP_Post $post WordPress Post.
1567 */
1568 $title = apply_filters( $this->base->plugin->filter_name . '_publish_get_title', $title, $post );
1569
1570 // Return.
1571 return $title;
1572
1573 }
1574
1575 /**
1576 * Safely generate an excerpt, stripping tags, shortcodes, falling back
1577 * to the content if the Post Type doesn't have excerpt support, and applying filters so that
1578 * third party plugins (such as translation plugins) can determine the final output.
1579 *
1580 * @since 3.7.3
1581 *
1582 * @param \WP_Post $post WordPress Post.
1583 * @param bool $fallback Use Content if no Excerpt exists.
1584 * @return string Excerpt
1585 */
1586 private function get_excerpt( $post, $fallback = true ) {
1587
1588 // Fetch excerpt.
1589 if ( empty( $post->post_excerpt ) ) {
1590 if ( $fallback ) {
1591 $excerpt = $post->post_content;
1592 } else {
1593 $excerpt = $post->post_excerpt;
1594 }
1595 } else {
1596 // Remove some third party Plugin filters that wrongly output content that we don't want in a status.
1597 remove_filter( 'get_the_excerpt', 'powerpress_content' );
1598
1599 $excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
1600 }
1601
1602 // Convert to plain text.
1603 $excerpt = $this->convert_to_plain_text( $excerpt, false );
1604
1605 /**
1606 * Filters the dynamic {excerpt} replacement, when a Post's status is being built.
1607 *
1608 * @since 3.7.3
1609 *
1610 * @param string $excerpt Post Excerpt.
1611 * @param \WP_Post $post WordPress Post.
1612 */
1613 $excerpt = apply_filters( $this->base->plugin->filter_name . '_publish_get_excerpt', $excerpt, $post );
1614
1615 // Return.
1616 return $excerpt;
1617
1618 }
1619
1620 /**
1621 * Safely generate a title, stripping tags and shortcodes, and applying filters so that
1622 * third party plugins (such as translation plugins) can determine the final output.
1623 *
1624 * @since 3.7.3
1625 *
1626 * @param \WP_Post $post WordPress Post.
1627 * @param bool $to_more_tag Only return content up to the <!-- more --> tag.
1628 * @return string Content
1629 */
1630 private function get_content( $post, $to_more_tag = false ) {
1631
1632 // Fetch content.
1633 // get_the_content() only works for WordPress 5.2+, which added the $post param.
1634 if ( $to_more_tag ) {
1635 $extended = get_extended( $post->post_content );
1636
1637 if ( ! empty( $extended['main'] ) ) {
1638 $content = $extended['main'];
1639 } else {
1640 // Fallback to the Post Content.
1641 $content = $post->post_content;
1642 }
1643 } else {
1644 $content = $post->post_content;
1645 }
1646
1647 // Strip shortcodes.
1648 $content = strip_shortcodes( $content );
1649
1650 // Remove the wpautop filter, as this converts double newlines into <p> tags.
1651 // In turn, <p> tags are correctly discarded later on in this function, as social networks don't support HTML.
1652 // However, this results in separation between paragraphs going from two newlines to one newline.
1653 // Some social media services further drop a single newline, meaning paragraphs become one long block of text, which isn't
1654 // intended.
1655 remove_filter( 'the_content', 'wpautop' );
1656
1657 // Remove some third party Plugin filters that wrongly output content that we don't want in a status.
1658 remove_filter( 'the_content', 'powerpress_content' );
1659
1660 // Apply filters to get true output.
1661 $content = apply_filters( 'the_content', $content );
1662
1663 // Restore wpautop that we just removed.
1664 add_filter( 'the_content', 'wpautop' );
1665
1666 // If the content originates from Gutenberg, remove double newlines and convert breaklines
1667 // into newlines.
1668 $is_gutenberg_request_content = $this->is_gutenberg_post_content( $post );
1669 if ( $is_gutenberg_request_content ) {
1670 // Remove double newlines, which may occur due to using Gutenberg blocks.
1671 // (blocks are separated with HTML comments, stripped using apply_filters( 'the_content' ), which results in double, or even triple, breaklines).
1672 $content = preg_replace( '/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n\n", $content );
1673
1674 // Convert <br> and <br /> into newlines.
1675 $content = preg_replace( '/<br(\s+)?\/?>/i', "\n", $content );
1676 }
1677
1678 // Convert to plain text.
1679 $content = $this->convert_to_plain_text( $content );
1680
1681 /**
1682 * Filters the dynamic {content} replacement, when a Post's status is being built.
1683 *
1684 * @since 3.7.3
1685 *
1686 * @param string $content Post Content.
1687 * @param \WP_Post $post WordPress Post.
1688 * @param bool $is_gutenberg_request_content Is Gutenberg Post Content.
1689 */
1690 $content = apply_filters( $this->base->plugin->filter_name . '_publish_get_content', $content, $post, $is_gutenberg_request_content );
1691
1692 // Return.
1693 return $content;
1694
1695 }
1696
1697 /**
1698 * Returns the date in the locale specified in WordPress.
1699 *
1700 * @since 4.7.7
1701 *
1702 * @param \WP_Post $post WordPress Post.
1703 * @return string Date
1704 */
1705 private function get_date( $post ) {
1706
1707 $date = date_i18n( get_option( 'date_format' ), strtotime( $post->post_date ) );
1708
1709 /*
1710 * Filters the dynamic {date} replacement, when a Post's status is being built.
1711 *
1712 * @since 4.7.7
1713 *
1714 * @param string $date Date.
1715 * @param \WP_Post $post WordPress Post.
1716 */
1717 $date = apply_filters( $this->base->plugin->filter_name . '_publish_get_date', $date, $post );
1718
1719 // Return.
1720 return $date;
1721
1722 }
1723
1724 /**
1725 * Returns the Permalink, including or excluding a trailing slash, depending on the Plugin settings.
1726 *
1727 * @since 4.0.6
1728 *
1729 * @param \WP_Post $post WordPress Post.
1730 * @return string WordPress Post Permalink
1731 */
1732 private function get_permalink( $post ) {
1733
1734 $force_trailing_forwardslash = $this->base->get_class( 'settings' )->get_option( 'force_trailing_forwardslash', false );
1735
1736 // Define the URL, depending on whether it should end with a forwardslash or not.
1737 // This is by design; more users complain that they get 301 redirects from site.com/post/ to site.com/post
1738 // than from site.com/post to site.com/post/.
1739 // We can't control misconfigured WordPress installs, so this option gives them the choice.
1740 if ( $force_trailing_forwardslash ) {
1741 $url = get_permalink( $post->ID );
1742
1743 // If the Permalink doesn't have a forwardslash at the end of it, add it now.
1744 if ( substr( $url, -1 ) !== '/' ) {
1745 $url .= '/';
1746 }
1747 } else {
1748 $url = rtrim( get_permalink( $post->ID ), '/' );
1749 }
1750
1751 /**
1752 * Filters the Post's Permalink, including or excluding a trailing slash, depending on the Plugin settings
1753 *
1754 * @since 4.0.6
1755 *
1756 * @param string $url WordPress Post Permalink.
1757 * @param \WP_Post $post WordPress Post.
1758 * @param bool $force_trailing_forwardslash Force Trailing Forwardslash.
1759 */
1760 $url = apply_filters( $this->base->plugin->filter_name . '_publish_get_permalink', $url, $post, $force_trailing_forwardslash );
1761
1762 // Return.
1763 return $url;
1764
1765 }
1766
1767 /**
1768 * Returns the Short Permalink
1769 *
1770 * @since 4.2.7
1771 *
1772 * @param \WP_Post $post WordPress Post.
1773 * @return string WordPress Post Permalink
1774 */
1775 private function get_short_permalink( $post ) {
1776
1777 // Define short permalink e.g http://yoursite.com/?p=1.
1778 $url = rtrim( get_bloginfo( 'url' ), '/' ) . '/?p=' . $post->ID;
1779
1780 /**
1781 * Filters the Post's Permalink, including or excluding a trailing slash, depending on the Plugin settings
1782 *
1783 * @since 4.2.7
1784 *
1785 * @param string $url WordPress Post Permalink.
1786 * @param \WP_Post $post WordPress Post.
1787 */
1788 $url = apply_filters( $this->base->plugin->filter_name . '_publish_get_short_permalink', $url, $post );
1789
1790 // Return.
1791 return $url;
1792
1793 }
1794
1795 /**
1796 * Converts the given string (which is typically HTML from a WordPress Post or Post Meta Field)
1797 * to plain text, by performing several functions:
1798 * - stripping shortcodes (if shortcodes need processing, do so before calling this function)
1799 * - removing all inline style elements and their contents,
1800 * - stripping HTML tags, excluding <br>, <br />, <a>, <li>
1801 * - decoding HTML entities to avoid encoding issues on status output
1802 * - converting <br> and <br /> to newlines
1803 * - removing double spaces
1804 * - trimming the final result of any leading or trailing spaces
1805 *
1806 * @since 4.6.9
1807 *
1808 * @param string $text Text.
1809 * @param bool $convert_links_to_inline true: Convert e.g. `<a href="http://foo.com">text</a>` to `text (http://foo.com)`.
1810 * false: Convert e.g. `<a href="http://foo.com">text</a>` to `text`.
1811 * @param bool $strip_urls Whether to strip URLs from the text.
1812 * @return string Text
1813 */
1814 private function convert_to_plain_text( $text, $convert_links_to_inline = true, $strip_urls = false ) {
1815
1816 // Strip any shortcodes still remaining.
1817 // If shortcodes need to be processed, they should be processed before calling this function.
1818 $text = strip_shortcodes( $text );
1819
1820 // Wrap content in <html>, <head> and <body> tags with an UTF-8 Content-Type meta tag.
1821 // Forcibly tell DOMDocument that this HTML uses the UTF-8 charset.
1822 // <meta charset="utf-8"> isn't enough, as DOMDocument still interprets the HTML as ISO-8859, which breaks character encoding
1823 // Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method.
1824 // If we don't, special characters render incorrectly.
1825 $text = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>' . $text . '</body></html>';
1826
1827 // Load the HTML into a DOMDocument.
1828 libxml_use_internal_errors( true );
1829 $html = new \DOMDocument();
1830 $html->loadHTML( $text );
1831
1832 // Load DOMDocument into XPath.
1833 $xpath = new \DOMXPath( $html );
1834
1835 // Remove inline style tags and their contents.
1836 foreach ( $xpath->query( '//style' ) as $node ) {
1837 $node->parentNode->removeChild( $node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1838 }
1839
1840 // Fetch revised HTML.
1841 $text = $html->saveHTML();
1842
1843 // Remove HTML, except breaklines, links and unordered list items.
1844 $retain_tags = ( $strip_urls ? '<br><li>' : '<br><a><li>' );
1845 $text = strip_tags( $text, $retain_tags );
1846
1847 // Decode excerpt to avoid encoding issues on status output.
1848 $text = html_entity_decode( $text );
1849
1850 // Convert <br> and <br /> into newlines.
1851 $text = preg_replace( '/<br(\s+)?\/?>/i', "\n", $text );
1852
1853 // Convert <a> to text and inline link.
1854 if ( $convert_links_to_inline ) {
1855 // Extract the text from the link, and add the link in brackets after the text.
1856 $text = preg_replace( '/<a[^>]+href=\"(.*?)\"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $text );
1857 } else {
1858 // Just extract the text from the link and output it.
1859 $text = preg_replace( '/<a[^>]+href=\"(.*?)\"[^>]*>(.*?)<\/a>/i', '$2', $text );
1860 }
1861
1862 // If URLs are to be stripped, remove them.
1863 if ( $strip_urls ) {
1864 $text = preg_replace( '/https?:\/\/[^\s]+/', '', $text );
1865 }
1866
1867 // Convert <li> to hyphenated.
1868 $text = preg_replace( '/<li[^>]*>(.*?)<\/li>/i', '- $1', $text );
1869
1870 // Remove double spaces, but retain newlines and accented characters.
1871 $text = preg_replace( '/[ ]{2,}/', ' ', $text );
1872
1873 // Remove tabs.
1874 $text = str_replace( "\t", '', $text );
1875
1876 // Finally, trim the text.
1877 $text = trim( $text );
1878
1879 // Return.
1880 return $text;
1881
1882 }
1883
1884 /**
1885 * Returns a flag denoting whether a character limit can safely be applied
1886 * to the given tag.
1887 *
1888 * @since 3.7.8
1889 *
1890 * @param string $tag Tag.
1891 * @return bool Can apply character limit
1892 */
1893 private function can_apply_character_limit_to_tag( $tag ) {
1894
1895 // Get Tags.
1896 $tags = $this->base->get_class( 'common' )->get_tags_excluded_from_character_limit();
1897
1898 // If the tag is in the array of tags excluded from character limits, we
1899 // cannot apply a character limit to this tag.
1900 if ( in_array( $tag, $tags, true ) ) {
1901 return false;
1902 }
1903
1904 // Can apply character limit to tag.
1905 return true;
1906
1907 }
1908
1909 /**
1910 * Applies the given word limit to the given text
1911 *
1912 * @since 3.8.9
1913 *
1914 * @param string $text Text.
1915 * @param int $word_limit Word Limit.
1916 * @return string Text
1917 */
1918 private function apply_word_limit( $text, $word_limit = 0 ) {
1919
1920 // Store original text.
1921 $original_text = $text;
1922
1923 // Bail if the word limit is zero or false.
1924 if ( ! $word_limit ) {
1925 return $text;
1926 }
1927
1928 // Limit text.
1929 $text = wp_trim_words( $text, $word_limit, '' );
1930
1931 /**
1932 * Applies the given word limit to the given text.
1933 *
1934 * @since 3.8.9
1935 *
1936 * @param string $text Text, with word limit applied.
1937 * @param int $word_limit Sentence Limit.
1938 * @param string $original_text Original Text, with no limit applied.
1939 */
1940 $text = apply_filters( $this->base->plugin->filter_name . '_publish_apply_word_limit', $text, $word_limit, $original_text );
1941
1942 return $text;
1943
1944 }
1945
1946 /**
1947 * Applies the given sentence limit to the given text
1948 *
1949 * @since 4.3.1
1950 *
1951 * @param string $text Text.
1952 * @param int $sentence_limit Sentence Limit.
1953 * @param int $min_sentence_length Minimum Sentence Length.
1954 * @return string
1955 */
1956 public function apply_sentence_limit( $text, $sentence_limit = 0, $min_sentence_length = 5 ) {
1957
1958 // Store original text.
1959 $original_text = $text;
1960
1961 // Bail if the sentence limit is zero or false.
1962 if ( ! $sentence_limit ) {
1963 return $text;
1964 }
1965
1966 // Build array of sentences.
1967 $parts = preg_split( '/(?<=[.?!])\s+(?=[a-z])/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1968
1969 // Iterate through the array, adding sentences to the array until we hit the sentence limit.
1970 // Sentences do not count towards the limit if they are shorter than the minimum sentence length.
1971 // This ensures abbreviations do not count towards the limit.
1972 $sentences = array();
1973 $sentence_count = 0;
1974 foreach ( $parts as $index => $sentence ) {
1975 // If we've hit the sentence limit, stop.
1976 if ( $sentence_count >= $sentence_limit ) {
1977 break;
1978 }
1979
1980 // Trim the sentence, adding it to the array.
1981 $sentences[ $index ] = trim( $sentence );
1982
1983 // If the sentence is longer than the minimum sentence length, count this as a sentence.
1984 if ( mb_strlen( $sentences[ $index ] ) > $min_sentence_length ) {
1985 ++$sentence_count;
1986 }
1987 }
1988
1989 // Implode into text, with a space between each sentence, trimming the array results to avoid double spacing.
1990 $text = implode( ' ', array_map( 'trim', $sentences ) );
1991
1992 /**
1993 * Applies the given sentence limit to the given text.
1994 *
1995 * @since 4.3.1
1996 *
1997 * @param string $text Text, with word limit applied.
1998 * @param int $sentence_limit Sentence Limit.
1999 * @param string $original_text Original Text, with no limit applied.
2000 */
2001 $text = apply_filters( $this->base->plugin->filter_name . '_publish_apply_sentence_limit', $text, $sentence_limit, $original_text );
2002
2003 // Return.
2004 return $text;
2005
2006 }
2007
2008 /**
2009 * Applies the given character limit to the given text
2010 *
2011 * @since 3.7.3
2012 *
2013 * @param string $text Text.
2014 * @param int $character_limit Character Limit.
2015 * @return string Text
2016 */
2017 private function apply_character_limit( $text, $character_limit = 0 ) {
2018
2019 // Bail if the character limit is zero or false.
2020 if ( ! $character_limit ) {
2021 return $text;
2022 }
2023
2024 // Bail if the content isn't longer than the character limit.
2025 if ( strlen( $text ) <= $character_limit ) {
2026 return $text;
2027 }
2028
2029 // Limit text.
2030 // Use mb_substr so that emojis don't break, which would result in text not being saved
2031 // by the social network when the status is sent.
2032 $text = mb_substr( $text, 0, $character_limit );
2033
2034 /**
2035 * Filters the character limited text.
2036 *
2037 * @since 3.7.3
2038 *
2039 * @param string $text Text, with character limit applied.
2040 * @param int $character_limit Character Limit used.
2041 */
2042 $text = apply_filters( $this->base->plugin->filter_name . '_publish_apply_character_limit', $text, $character_limit );
2043
2044 // Return.
2045 return $text;
2046
2047 }
2048
2049 /**
2050 * Helper method to iterate through statuses, sending each via a separate API call
2051 * to the API.
2052 *
2053 * @since 3.0.0
2054 *
2055 * @param array $statuses Statuses.
2056 * @param int $post_id Post ID.
2057 * @param string $action Action.
2058 * @param array $profiles All Enabled Profiles.
2059 * @param bool $test_mode Test Mode (won't send to API).
2060 * @return array API Result for each status
2061 */
2062 public function send( $statuses, $post_id, $action, $profiles, $test_mode = false ) {
2063
2064 // Assume no errors.
2065 $errors = false;
2066
2067 // Setup logging.
2068 $logs = array();
2069 $log_error = array();
2070 $log_enabled = $this->base->get_class( 'log' )->is_enabled();
2071
2072 foreach ( $statuses as $index => $status ) {
2073
2074 // If the status is a WP_Error, something went wrong in building the status to be sent.
2075 // Log the error and continue to the next status.
2076 if ( isset( $status['error'] ) && is_wp_error( $status['error'] ) ) {
2077 // Error.
2078 $errors = true;
2079 $logs[] = array(
2080 'action' => $action,
2081 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2082 'profile_id' => $status['profile_ids'][0],
2083 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2084 'result' => 'error',
2085 'result_message' => sprintf(
2086 /* translators: %1$s: Plugin Error string, %2$s: Error message from Plugin */
2087 '%1$s: %2$s',
2088 __( 'Plugin Error', 'wp-to-buffer' ),
2089 $status['error']->get_error_message()
2090 ),
2091 'status_text' => false,
2092 );
2093 $log_error[] = ( $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'] . ': ' . $status['error']->get_error_message() );
2094 continue;
2095 }
2096
2097 // If this is a test, add to the log array only.
2098 if ( $test_mode ) {
2099 $logs[] = array(
2100 'action' => $action,
2101 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2102 'profile_id' => $status['profile_ids'][0],
2103 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2104 'result' => 'test',
2105 'result_message' => '',
2106 'status_text' => $status['text'],
2107 'status_created_at' => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ),
2108 'status_due_at' => ( isset( $status['scheduled_at'] ) ? $status['scheduled_at'] : '' ),
2109 );
2110
2111 continue;
2112 }
2113
2114 // Setup API.
2115 $this->base->get_class( 'api' )->set_tokens(
2116 $this->base->get_class( 'settings' )->get_access_token_by_profile_id( $status['profile_ids'][0] ),
2117 $this->base->get_class( 'settings' )->get_refresh_token_by_profile_id( $status['profile_ids'][0] ),
2118 $this->base->get_class( 'settings' )->get_token_expires_by_profile_id( $status['profile_ids'][0] )
2119 );
2120
2121 // Send request.
2122 $result = $this->base->get_class( 'api' )->updates_create( $status, $profiles[ $status['profile_ids'][0] ]['service'] );
2123
2124 // Store result in log array.
2125 if ( is_wp_error( $result ) ) {
2126 // Error.
2127 $errors = true;
2128 $logs[] = array(
2129 'action' => $action,
2130 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2131 'profile_id' => $status['profile_ids'][0],
2132 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2133 'result' => 'error',
2134 'result_message' => $result->get_error_message(),
2135 'status_text' => $status['text'],
2136 );
2137 $log_error[] = ( $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'] . ': ' . $result->get_error_message() );
2138 } else {
2139 // OK.
2140 $logs[] = array(
2141 'action' => $action,
2142 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2143 'profile_id' => $result['profile_id'],
2144 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2145 'result' => 'success',
2146 'result_message' => $result['message'],
2147 'status_text' => $result['status_text'],
2148 'status_created_at' => gmdate( 'Y-m-d H:i:s', $result['status_created_at'] ),
2149 'status_due_at' => ( $result['due_at'] !== '0000-00-00 00:00:00' ? gmdate( 'Y-m-d H:i:s', $result['due_at'] ) : '0000-00-00 00:00:00' ),
2150 );
2151 }
2152 }
2153
2154 // Set the last sent timestamp, which we may use to prevent duplicate statuses.
2155 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_last_sent', time() );
2156
2157 // If we're reposting, update the last reposted date against the Post.
2158 // We do this here to ensure the Post isn't reposting again where e.g. one profile status worked + one profile status failed,
2159 // which would be deemed a failure.
2160 if ( $action === 'repost' && ! $test_mode ) {
2161 $this->base->get_class( 'repost' )->update_last_reposted_date( $post_id );
2162 }
2163
2164 // If no errors were reported, set a meta key to show a success message.
2165 // This triggers admin_notices() to tell the user what happened.
2166 if ( ! $errors ) {
2167 // Only set a success message if test mode is disabled.
2168 if ( ! $test_mode ) {
2169 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_success', 1 );
2170 }
2171 delete_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_error' );
2172 delete_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_errors' );
2173
2174 // Request that the user review the plugin. Notification displayed later,
2175 // can be called multiple times and won't re-display the notification if dismissed.
2176 $this->base->dashboard->request_review();
2177 } else {
2178 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_success', 0 );
2179 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_error', 1 );
2180 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_errors', $log_error );
2181 }
2182
2183 // Save the log, if logging is enabled.
2184 if ( $log_enabled ) {
2185 foreach ( $logs as $log ) {
2186 $this->base->get_class( 'log' )->add( $post_id, $log );
2187 }
2188 }
2189
2190 // Return log results.
2191 return $logs;
2192
2193 }
2194
2195 /**
2196 * Clears any searches and replacements stored in this class.
2197 *
2198 * @since 3.8.0
2199 */
2200 private function clear_search_replacements() {
2201
2202 $this->all_possible_searches_replacements = array();
2203 $this->searches_replacements = array();
2204
2205 }
2206
2207 }
2208