PluginProbe
Social Media Auto Poster – Schedule & Publish to Buffer / 6.2.7
Social Media Auto Poster – Schedule & Publish to Buffer v6.2.7
6.2.7 6.2.6 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 All 128 releases
wp-to-buffer / lib / social / includes / class-publish.php

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

2,207 lines 75.2 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 $args = array(
781 'account' => $account,
782 'post_type' => $status['post_type'],
783 'profile_ids' => array( $profile_id ),
784 'text' => $this->parse_text( $post, $status['message'], ( $service === 'instagram' ? true : false ) ),
785 );
786
787 // Shorten URLs.
788 if ( $this->base->supports( 'url_shortening' ) ) {
789 $args['shorten'] = ( $this->base->get_class( 'settings' )->get_option( 'disable_url_shortening', false ) ? false : true );
790 }
791
792 // Drafts.
793 if ( $this->base->supports( 'drafts' ) ) {
794 $args['is_draft'] = $this->base->get_class( 'settings' )->get_option( 'is_draft', false );
795 }
796
797 // URL.
798 switch ( $status['post_type'] ) {
799 /**
800 * Link
801 */
802 case 'link':
803 case 'pin':
804 case 'googlebusiness':
805 // If no URL is specified in the status, use the Post's URL.
806 $status['url'] = empty( $status['url'] ) ? '{url}' : $status['url'];
807
808 // Get URL.
809 $url = $this->parse_text( $post, $status['url'] );
810
811 // If URL is empty, don't include it in the args.
812 if ( empty( $url ) ) {
813 break;
814 }
815
816 // Add URL to args.
817 $args['url'] = $url;
818 break;
819 }
820
821 // Image(s).
822 switch ( $status['post_type'] ) {
823 case 'pin':
824 case 'googlebusiness':
825 case 'story':
826 case 'image':
827 switch ( $status['image'] ) {
828 /**
829 * Featured, Additional or Content Image
830 * 1 and 2 are used for backward compatibility where settings are not updated.
831 */
832 case 'featured_image':
833 case '1':
834 case '2':
835 // Plugin's First (Featured) Image, Post's Featured Image or Post Content's First Image.
836 $image = $this->get_post_image( $post, $service, $status['post_type'] );
837
838 // If the image is a WP_Error object, log it and return.
839 if ( is_wp_error( $image ) ) {
840 $this->base->get_class( 'log' )->add_to_debug_log( 'Image Error: ' . $image->get_error_message() );
841 return $image;
842 }
843
844 // Add image to media_urls, if one was found.
845 if ( $image !== false ) {
846 $args['media_urls'] = array( $image );
847 }
848 break;
849
850 }
851 }
852
853 // Scheduling.
854 switch ( $status['schedule'] ) {
855 case 'queue_end':
856 case 'queue_start':
857 case 'immediate':
858 $args['schedule_type'] = $status['schedule'];
859 break;
860
861 default:
862 $args['schedule_type'] = 'queue_end';
863 break;
864 }
865
866 /**
867 * Determine the standardised arguments array to send via the API for a status message's settings.
868 *
869 * @since 3.0.0
870 *
871 * @param array $args API standardised arguments.
872 * @param \WP_Post $post WordPress Post.
873 * @param string $profile_id Social Media Profile ID.
874 * @param string $service Social Media Service.
875 * @param array $status Parsed Status Message Settings.
876 * @param string $action Action (publish|update|repost|bulk_publish).
877 */
878 $args = apply_filters( $this->base->plugin->filter_name . '_publish_build_args', $args, $post, $profile_id, $service, $status, $action );
879
880 // Return args.
881 return $args;
882
883 }
884
885 /**
886 * Attempts to fetch the primary Post's Image, in the following order:
887 * - Plugin's First (Featured) Image
888 * - Post's Featured Image
889 * - Post's Content's First Image
890 *
891 * @since 3.9.8
892 *
893 * @param \WP_Post $post Post ID.
894 * @param string $service Social Media Service.
895 * @param bool|string $format Status format (for example, 'story' or 'post' for Instagram).
896 * @return array|bool|\WP_Error
897 */
898 private function get_post_image( $post, $service, $format = false ) {
899
900 // Featured Image.
901 $image_id = get_post_thumbnail_id( $post->ID );
902 if ( $image_id > 0 ) {
903 return $this->base->get_class( 'image' )->get_image_sources( $image_id, 'featured_image', $service, $format );
904 }
905
906 // If here, no image was found in the Post.
907 return false;
908
909 }
910
911 /**
912 * Populates the status message by replacing tags with Post/Author data
913 *
914 * @since 3.0.0
915 *
916 * @param \WP_Post $post Post.
917 * @param string $message Status Message to parse.
918 * @param bool $strip_urls Whether to strip URLs from the status message.
919 * @return string Parsed Status Message
920 */
921 public function parse_text( $post, $message, $strip_urls = false ) {
922
923 // Get Author.
924 $author = get_user_by( 'id', $post->post_author );
925
926 // If we haven't yet populated the searches and replacements for this Post, do so now.
927 if ( ! $this->all_possible_searches_replacements ) {
928 $this->all_possible_searches_replacements = $this->register_all_possible_searches_replacements( $post, $author );
929 }
930
931 // If no searches and replacements are defined, we can't parse anything.
932 if ( ! $this->all_possible_searches_replacements ) {
933 return $message;
934 }
935
936 // Extract all of the tags in the message.
937 preg_match_all( '|{(.+?)}|', $message, $matches );
938
939 // If no tags exist in the message, there's nothing to parse.
940 if ( count( $matches[0] ) === 0 ) {
941 return $message;
942 }
943
944 // Define return text.
945 $text = $message;
946
947 // Iterate through matches, adding them to the search / replacement array.
948 foreach ( $matches[1] as $index => $tag ) {
949 // Clean up some vars.
950 unset( $tag_params, $transformation, $replacement );
951
952 // Define some default attributes for this tag.
953 $tag_params = $this->get_default_tag_params( $matches[0][ $index ], $tag );
954
955 // If we already have a replacement for this exact tag (i.e. from a previous status message),
956 // we don't need to define the replacement again.
957 if ( isset( $this->searches_replacements[ $tag_params['tag_with_braces'] ] ) ) {
958 continue;
959 }
960
961 // Backward compatibility for word, sentence and character limit tags
962 // Store them in the tag parameter's transformations array.
963 if ( preg_match( '/(.*?)\((.*?)_words\)/', $tag_params['tag'], $word_limit_matches ) ) {
964 $tag_params['tag'] = $word_limit_matches[1];
965 $transformation = array(
966 'transformation' => 'words',
967 'arguments' => array(
968 absint( $word_limit_matches[2] ),
969 ),
970 );
971 } elseif ( preg_match( '/(.*?)\((.*?)_sentences\)/', $tag_params['tag'], $sentence_limit_matches ) ) {
972 $tag_params['tag'] = $sentence_limit_matches[1];
973 $transformation = array(
974 'transformation' => 'sentences',
975 'arguments' => array(
976 absint( $sentence_limit_matches[2] ),
977 ),
978 );
979 } elseif ( preg_match( '/(.*?)\((.*?)\)/', $tag_params['tag'], $character_limit_matches ) ) {
980 $tag_params['tag'] = $character_limit_matches[1];
981 $transformation = array(
982 'transformation' => 'characters',
983 'arguments' => array(
984 absint( $character_limit_matches[2] ),
985 ),
986 );
987 }
988 if ( isset( $transformation ) ) {
989 if ( is_array( $tag_params['transformations'] ) ) {
990 $tag_params['transformations'][] = $transformation;
991 } else {
992 $tag_params['transformations'] = array( $transformation );
993 }
994 }
995
996 // If this Tag is a Taxonomy Tag, fetch some parameters that may be included in the tag.
997 if ( preg_match( '/^taxonomy_(.*?)$/', $tag_params['tag'], $taxonomy_matches ) ) {
998 // Taxonomy with Hashtag Format.
999 $tag_params['taxonomy'] = str_replace( 'taxonomy_', '', $tag_params['tag'] );
1000 }
1001
1002 // Fetch possible tag replacement value.
1003 $replacement = ( isset( $this->all_possible_searches_replacements[ $tag_params['tag'] ] ) ? $this->all_possible_searches_replacements[ $tag_params['tag'] ] : '' );
1004
1005 // If this is a taxonomy replacement, replace according to the tag parameters.
1006 if ( $tag_params['taxonomy'] !== false ) {
1007 // Define a string to hold the list of terms.
1008 $term_names = '';
1009
1010 // Iterate through terms, building string.
1011 foreach ( $replacement as $term_index => $term ) {
1012 // If there's a term limit and this term exceeds it, exit the loop.
1013 if ( $tag_params['taxonomy_term_limit'] > 0 && $term_index + 1 > $tag_params['taxonomy_term_limit'] ) {
1014 break;
1015 }
1016
1017 // Lowercase and decode HTML.
1018 $term_name = strtolower( str_replace( ' ', '', html_entity_decode( $term->name ) ) );
1019
1020 // Remove anything that isn't alphanumeric or an underscore, to ensure the whole hashtag is linked
1021 // when posted to social media and not broken by e.g. a full stop.
1022 $term_name = '#' . preg_replace( '/[^\p{L}\p{N}\p{M}_]+/u', '', $term_name );
1023
1024 /**
1025 * Defines the Taxonomy Term Hashtag to replace the status template tag.
1026 *
1027 * @since 3.0.0
1028 *
1029 * @param string $term_name Term Name.
1030 * @param string $taxonomy_term_format Term Format.
1031 * @param WP_Term $term Term.
1032 * @param string $taxonomy Taxonomy.
1033 * @param string $text Status Text.
1034 */
1035 $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 );
1036
1037 /**
1038 * Backward compat filter to define the Taxonomy Term Name to replace the status template tag.
1039 * _publish_parse_text_term_name and _publish_parse_text_term_hashtag should be used instead.
1040 *
1041 * @since 3.0.0
1042 *
1043 * @param string $term_name Term Name.
1044 * @param string $term->name Term Name.
1045 * @param string $taxonomy Taxonomy.
1046 * @param string $text Status Text.
1047 * @param string $taxonomy_term_format Term Format.
1048 */
1049 $term_name = apply_filters( $this->base->plugin->filter_name . '_term', $term_name, $term->name, $tag_params['taxonomy'], $text, $tag_params['taxonomy_term_format'] );
1050
1051 // Add term to term names string.
1052 $term_names .= $term_name . ' ';
1053 }
1054
1055 // Finally, replace the array of terms with the string of formatted terms.
1056 $replacement = trim( $term_names );
1057 }
1058
1059 // Trim replacement.
1060 $replacement = trim( $replacement );
1061
1062 // Apply Transformations.
1063 if ( $tag_params['transformations'] ) {
1064 foreach ( $tag_params['transformations'] as $transformation ) {
1065 $replacement = $this->apply_text_transformation(
1066 $tag_params['tag'],
1067 $transformation['transformation'],
1068 $replacement,
1069 $transformation['arguments']
1070 );
1071 }
1072 }
1073
1074 // Add the search and replacement to the array.
1075 $this->searches_replacements[ $tag_params['tag_with_braces'] ] = $replacement;
1076
1077 } // Close foreach tag match in text.
1078
1079 // Search and Replace.
1080 $text = str_replace( array_keys( $this->searches_replacements ), $this->searches_replacements, $text );
1081
1082 // Execute any shortcodes in the text now.
1083 $text = do_shortcode( $text );
1084
1085 // Convert to plain text.
1086 $text = $this->convert_to_plain_text( $text, true, $strip_urls );
1087
1088 /**
1089 * Filters the parsed status message text on a status.
1090 *
1091 * @since 3.0.0
1092 *
1093 * @param string $text Parsed Text, no Tags.
1094 * @param string $message Unparsed Text with Tags.
1095 * @param array $searches_replacements Specific Tag Search and Replacements for the given Text.
1096 * @param array $all_possible_searches_replacements All Registered Tag Search and Replacements.
1097 * @param \WP_Post $post WordPress Post.
1098 * @param \WP_User $author WordPress User (Author).
1099 */
1100 $text = apply_filters( $this->base->plugin->filter_name . '_publish_parse_text', $text, $message, $this->searches_replacements, $this->all_possible_searches_replacements, $post, $author );
1101
1102 return $text;
1103
1104 }
1105
1106 /**
1107 * Parses the status' Google Business configuration to return an array of compatible
1108 * arguments that can be used to send the status.
1109 *
1110 * @since 4.9.0
1111 *
1112 * @param \WP_Post $post Post.
1113 * @param array $status Status.
1114 * @return bool|array Google Business Profile status configuration
1115 */
1116 public function parse_google_business( $post, $status ) {
1117
1118 // Bail if no Google Business configuration exists in the status.
1119 if ( ! isset( $status['googlebusiness'] ) ) {
1120 return false;
1121 }
1122 if ( ! is_array( $status['googlebusiness'] ) ) {
1123 return false;
1124 }
1125 if ( ! isset( $status['googlebusiness']['post_type'] ) ) {
1126 return false;
1127 }
1128
1129 // Start building arguments.
1130 $google_business_args = array(
1131 'post_type' => $status['googlebusiness']['post_type'],
1132 );
1133
1134 // Depending on the Google Business Post Type, build arguments.
1135 switch ( $status['googlebusiness']['post_type'] ) {
1136 case 'offer':
1137 case 'event':
1138 // Title.
1139 $google_business_args['title'] = $this->parse_text( $post, $status['googlebusiness']['title'] );
1140
1141 // Code and Terms: Offers.
1142 if ( $status['googlebusiness']['post_type'] === 'offer' ) {
1143 $google_business_args = array_merge(
1144 $google_business_args,
1145 array(
1146 'code' => $this->parse_text( $post, $status['googlebusiness']['code'], true ),
1147 'terms' => $this->parse_text( $post, $status['googlebusiness']['terms'], true ),
1148 )
1149 );
1150 } else {
1151 // Event: Button.
1152 $google_business_args['cta'] = $status['googlebusiness']['cta'];
1153 }
1154
1155 // Start Date.
1156 switch ( $status['googlebusiness']['start_date_option'] ) {
1157 /**
1158 * Custom Post Meta
1159 */
1160 case 'custom':
1161 // If no custom field key is set, set the start date to now.
1162 if ( empty( $status['googlebusiness']['start_date'] ) ) {
1163 $date = gmdate( 'Y-m-d H:i:s' );
1164 } else {
1165 // Fetch the Post's Meta Value based on the given Custom Field Key.
1166 $date = get_post_meta( $post->ID, $status['googlebusiness']['start_date'], true );
1167
1168 // If the post date is numeric, it's most likely a timestamp
1169 // Convert it to a date and time.
1170 if ( is_numeric( $date ) ) {
1171 $date = gmdate( 'Y-m-d H:i:s', $date );
1172 }
1173 }
1174
1175 // Set start date.
1176 $google_business_args['start_date'] = strtotime( $date );
1177 $google_business_args['start_time'] = gmdate( 'H:i', strtotime( $date ) );
1178 break;
1179
1180 /**
1181 * None
1182 */
1183 case '':
1184 break;
1185
1186 /**
1187 * Third Party integrations
1188 */
1189 default:
1190 $date = false;
1191
1192 /**
1193 * Allows integrations to define the status' start date for a Google Business Profile Offer or Event.
1194 *
1195 * @since 4.9.0
1196 *
1197 * @param bool|string $date Date (yyyy-mm-dd hh:mm:ss format).
1198 * @param array $google_business_args Google Business specific arguments for status.
1199 * @param array $status Status.
1200 * @param \WP_Post $post WordPress Post.
1201 */
1202 $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 );
1203
1204 // Ignore if no date defined.
1205 if ( ! $date ) {
1206 break;
1207 }
1208
1209 // Set start date.
1210 $google_business_args['start_date'] = strtotime( $date );
1211 $google_business_args['start_time'] = gmdate( 'H:i', strtotime( $date ) );
1212 break;
1213 }
1214
1215 // End Date.
1216 switch ( $status['googlebusiness']['end_date_option'] ) {
1217 /**
1218 * Custom Post Meta
1219 */
1220 case 'custom':
1221 // If no custom field key is set, set the end date to now.
1222 if ( empty( $status['googlebusiness']['end_date'] ) ) {
1223 $date = gmdate( 'Y-m-d H:i:s' );
1224 } else {
1225 // Fetch the Post's Meta Value based on the given Custom Field Key.
1226 $date = get_post_meta( $post->ID, $status['googlebusiness']['end_date'], true );
1227
1228 // If the post date is numeric, it's most likely a timestamp
1229 // Convert it to a date and time.
1230 if ( is_numeric( $date ) ) {
1231 $date = gmdate( 'Y-m-d H:i:s', $date );
1232 }
1233 }
1234
1235 // Set end date.
1236 $google_business_args['end_date'] = strtotime( $date );
1237 $google_business_args['end_time'] = gmdate( 'H:i', strtotime( $date ) );
1238 break;
1239
1240 /**
1241 * None
1242 */
1243 case '':
1244 break;
1245
1246 /**
1247 * Third Party integrations
1248 */
1249 default:
1250 $date = false;
1251
1252 /**
1253 * Allows integrations to define the status' end date for a Google Business Profile Offer or Event.
1254 *
1255 * @since 4.9.0
1256 *
1257 * @param bool|string $date Date (yyyy-mm-dd hh:mm:ss format).
1258 * @param array $google_business_args Google Business specific arguments for status.
1259 * @param array $status Status.
1260 * @param \WP_Post $post WordPress Post.
1261 */
1262 $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 );
1263
1264 // Ignore if no date defined.
1265 if ( ! $date ) {
1266 break;
1267 }
1268
1269 // Set end date.
1270 $google_business_args['end_date'] = strtotime( $date );
1271 $google_business_args['end_time'] = gmdate( 'H:i', strtotime( $date ) );
1272 break;
1273 }
1274 break;
1275
1276 case 'whats_new':
1277 default:
1278 $google_business_args['cta'] = $status['googlebusiness']['cta'];
1279 break;
1280 }
1281
1282 return $google_business_args;
1283
1284 }
1285
1286 /**
1287 * Returns default tag parameters for the given tag e.g. {title:transformation(args)} or {title}.
1288 *
1289 * @since 4.5.9
1290 *
1291 * @param string $tag_with_braces Tag with Braces e.g. {title:transformation(args)} or {title}.
1292 * @param string $tag Tag without Braces e.g. title:transformation(args) or title.
1293 * @return array Tag Parameters
1294 * */
1295 private function get_default_tag_params( $tag_with_braces, $tag ) {
1296
1297 // Define array of tag parameters to be populated.
1298 $tag_params = array(
1299 'tag_with_braces' => $tag_with_braces, // Original tag with braces, including transformations.
1300 'tag' => $tag, // No braces, no transformations.
1301 'transformations' => false,
1302 'taxonomy' => false,
1303 'taxonomy_term_limit' => false,
1304 'taxonomy_term_format' => false,
1305 );
1306
1307 // If no transformations exist, return.
1308 if ( strpos( $tag, ':' ) === false ) {
1309 return $tag_params;
1310 }
1311
1312 // Extract transformations.
1313 $tag_params['transformations'] = explode( ':', substr( $tag_params['tag'], strpos( $tag_params['tag'], ':' ) + 1 ) );
1314
1315 // Remove transformations from tag.
1316 $tag_params['tag'] = substr( $tag_params['tag'], 0, strpos( $tag_params['tag'], ':' ) );
1317
1318 // Iterate through transformations to see if arguments are attached.
1319 foreach ( $tag_params['transformations'] as $index => $transformation ) {
1320 // If no arguments exist for this transformation, update the array structure and continue.
1321 if ( strpos( $transformation, '(' ) === false ) {
1322 $tag_params['transformations'][ $index ] = array(
1323 'transformation' => $transformation,
1324 'arguments' => false,
1325 );
1326 continue;
1327 }
1328
1329 // Extract arguments.
1330 $arguments = explode( '(', substr( $transformation, strpos( $transformation, '(' ) + 1 ) );
1331 foreach ( $arguments as $a_index => $argument ) {
1332 $arguments[ $a_index ] = str_replace( ')', '', $argument );
1333 }
1334
1335 // Remove arguments from transformation.
1336 $transformation = substr( $transformation, 0, strpos( $transformation, '(' ) );
1337
1338 // Update array structure.
1339 $tag_params['transformations'][ $index ] = array(
1340 'transformation' => $transformation,
1341 'arguments' => $arguments,
1342 );
1343 }
1344
1345 // Return.
1346 return $tag_params;
1347
1348 }
1349
1350 /**
1351 * Applies a transformation to the given value
1352 *
1353 * @since 4.5.8
1354 *
1355 * @param string $tag Tag e.g. title, date.
1356 * @param string $transformation Transformation.
1357 * @param string $value Value.
1358 * @param mixed $transformation_arguments false | array of arguments to apply to the transformation e.g. character limit, date format.
1359 * @return string Transformed Value
1360 */
1361 private function apply_text_transformation( $tag, $transformation, $value, $transformation_arguments = false ) {
1362
1363 switch ( $transformation ) {
1364 /**
1365 * Word Limit
1366 */
1367 case 'words':
1368 // Don't attempt to apply limit if the tag doesn't support it.
1369 if ( ! $this->can_apply_character_limit_to_tag( $tag ) ) {
1370 return $value;
1371 }
1372
1373 // Don't attempt to apply limit if no limit is given.
1374 if ( ! $transformation_arguments ) {
1375 return $value;
1376 }
1377
1378 return $this->apply_word_limit( $value, $transformation_arguments[0] );
1379
1380 /**
1381 * Sentence Limit
1382 */
1383 case 'sentences':
1384 // Don't attempt to apply limit if the tag doesn't support it.
1385 if ( ! $this->can_apply_character_limit_to_tag( $tag ) ) {
1386 return $value;
1387 }
1388
1389 // Don't attempt to apply limit if no limit is given.
1390 if ( ! $transformation_arguments ) {
1391 return $value;
1392 }
1393
1394 return $this->apply_sentence_limit( $value, $transformation_arguments[0] );
1395
1396 /**
1397 * Character Limit
1398 */
1399 case 'characters':
1400 // Don't attempt to apply limit if the tag doesn't support it.
1401 if ( ! $this->can_apply_character_limit_to_tag( $tag ) ) {
1402 return $value;
1403 }
1404
1405 // Don't attempt to apply limit if no limit is given.
1406 if ( ! $transformation_arguments ) {
1407 return $value;
1408 }
1409
1410 return $this->apply_character_limit( $value, $transformation_arguments[0] );
1411
1412 /**
1413 * Other Transformations
1414 */
1415 default:
1416 /**
1417 * Applies the given transformation to the given value
1418 *
1419 * @since 4.5.8
1420 *
1421 * @param string $value Value.
1422 * @param string $transformation Transformation.
1423 */
1424 $value = apply_filters( $this->base->plugin->filter_name . '_publish_apply_text_transformation', $value, $transformation );
1425
1426 return $value;
1427 }
1428
1429 }
1430
1431 /**
1432 * Returns an array comprising of all supported tags and their Post / Author / Taxonomy data replacements.
1433 *
1434 * @since 3.7.8
1435 *
1436 * @param \WP_Post $post WordPress Post.
1437 * @param \WP_User $author WordPress User (Author of the Post).
1438 * @return array Search / Replacement Key / Value pairs
1439 */
1440 private function register_all_possible_searches_replacements( $post, $author ) {
1441
1442 // Start with no searches or replacements.
1443 $searches_replacements = array();
1444
1445 // Register Post Tags and Replacements.
1446 $searches_replacements = $this->register_post_searches_replacements( $searches_replacements, $post );
1447
1448 // Register Taxonomy Tags and Replacements.
1449 // Add Taxonomies.
1450 $taxonomies = get_object_taxonomies( $post->post_type, 'names' );
1451 if ( count( $taxonomies ) > 0 ) {
1452 $searches_replacements = $this->register_taxonomy_searches_replacements( $searches_replacements, $post, $taxonomies );
1453 }
1454
1455 /**
1456 * Registers any additional status message tags, and their Post data replacements, that are supported.
1457 *
1458 * @since 3.7.8
1459 *
1460 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1461 * @param \WP_Post $post WordPress Post.
1462 * @param \WP_User $author WordPress User (Author of the Post).
1463 */
1464 $searches_replacements = apply_filters( $this->base->plugin->filter_name . '_publish_get_all_possible_searches_replacements', $searches_replacements, $post, $author );
1465
1466 // Return filtered results.
1467 return $searches_replacements;
1468
1469 }
1470
1471 /**
1472 * Registers status message tags and their data replacements for the given Post.
1473 *
1474 * @since 3.7.8
1475 *
1476 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1477 * @param \WP_Post $post WordPress Post.
1478 * @return array Registered Supported Tags and their Replacements
1479 */
1480 private function register_post_searches_replacements( $searches_replacements, $post ) {
1481
1482 // Check Plugin Settings to see if the excerpt should fallback to the content if no
1483 // Excerpt defined.
1484 $excerpt_fallback = ( $this->base->get_class( 'settings' )->get_option( 'disable_excerpt_fallback', false ) ? false : true );
1485
1486 $searches_replacements['sitename'] = get_bloginfo( 'name' );
1487 $searches_replacements['title'] = $this->get_title( $post );
1488 $searches_replacements['excerpt'] = $this->get_excerpt( $post, $excerpt_fallback );
1489 $searches_replacements['content'] = $this->get_content( $post );
1490 $searches_replacements['content_more_tag'] = $this->get_content( $post, true );
1491 $searches_replacements['date'] = $this->get_date( $post );
1492 $searches_replacements['url'] = $this->get_permalink( $post );
1493 $searches_replacements['url_short'] = $this->get_short_permalink( $post );
1494 $searches_replacements['id'] = absint( $post->ID );
1495
1496 /**
1497 * Registers any additional status message tags, and their Post data replacements, that are supported
1498 * for the given Post.
1499 *
1500 * @since 3.7.8
1501 *
1502 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1503 * @param \WP_Post $post WordPress Post.
1504 */
1505 $searches_replacements = apply_filters( $this->base->plugin->filter_name . '_publish_register_post_searches_replacements', $searches_replacements, $post );
1506
1507 // Return filtered results.
1508 return $searches_replacements;
1509
1510 }
1511
1512 /**
1513 * Registers status message tags and their data replacements for the given Post Taxonomies.
1514 *
1515 * @since 3.7.8
1516 *
1517 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1518 * @param \WP_Post $post WordPress Post.
1519 * @param array $taxonomies Post Taxonomies.
1520 * @return array $searches_replacements Registered Supported Tags and their Replacements.
1521 */
1522 private function register_taxonomy_searches_replacements( $searches_replacements, $post, $taxonomies ) {
1523
1524 foreach ( $taxonomies as $taxonomy ) {
1525 $searches_replacements[ 'taxonomy_' . $taxonomy ] = wp_get_post_terms( $post->ID, $taxonomy );
1526 }
1527
1528 /**
1529 * Registers any additional status message tags, and their Post data replacements, that are supported
1530 * for the given Post.
1531 *
1532 * @since 3.7.8
1533 *
1534 * @param array $searches_replacements Registered Supported Tags and their Replacements.
1535 * @param \WP_Post $post WordPress Post.
1536 * @param array $taxonomies Post Taxonomies.
1537 */
1538 $searches_replacements = apply_filters( $this->base->plugin->filter_name . '_publish_register_post_searches_replacements', $searches_replacements, $post, $taxonomies );
1539
1540 // Return filtered results.
1541 return $searches_replacements;
1542
1543 }
1544
1545 /**
1546 * Safely generate a title, stripping tags and shortcodes, and applying filters so that
1547 * third party plugins (such as translation plugins) can determine the final output.
1548 *
1549 * @since 3.7.3
1550 *
1551 * @param \WP_Post $post WordPress Post.
1552 * @return string Title
1553 */
1554 private function get_title( $post ) {
1555
1556 // Define title.
1557 $title = $this->convert_to_plain_text( get_the_title( $post ), false, true );
1558
1559 /**
1560 * Filters the dynamic {title} replacement, when a Post's status is being built.
1561 *
1562 * @since 3.7.3
1563 *
1564 * @param string $title Post Title.
1565 * @param \WP_Post $post WordPress Post.
1566 */
1567 $title = apply_filters( $this->base->plugin->filter_name . '_publish_get_title', $title, $post );
1568
1569 // Return.
1570 return $title;
1571
1572 }
1573
1574 /**
1575 * Safely generate an excerpt, stripping tags, shortcodes, falling back
1576 * to the content if the Post Type doesn't have excerpt support, and applying filters so that
1577 * third party plugins (such as translation plugins) can determine the final output.
1578 *
1579 * @since 3.7.3
1580 *
1581 * @param \WP_Post $post WordPress Post.
1582 * @param bool $fallback Use Content if no Excerpt exists.
1583 * @return string Excerpt
1584 */
1585 private function get_excerpt( $post, $fallback = true ) {
1586
1587 // Fetch excerpt.
1588 if ( empty( $post->post_excerpt ) ) {
1589 if ( $fallback ) {
1590 $excerpt = $post->post_content;
1591 } else {
1592 $excerpt = $post->post_excerpt;
1593 }
1594 } else {
1595 // Remove some third party Plugin filters that wrongly output content that we don't want in a status.
1596 remove_filter( 'get_the_excerpt', 'powerpress_content' );
1597
1598 $excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
1599 }
1600
1601 // Convert to plain text.
1602 $excerpt = $this->convert_to_plain_text( $excerpt, false );
1603
1604 /**
1605 * Filters the dynamic {excerpt} replacement, when a Post's status is being built.
1606 *
1607 * @since 3.7.3
1608 *
1609 * @param string $excerpt Post Excerpt.
1610 * @param \WP_Post $post WordPress Post.
1611 */
1612 $excerpt = apply_filters( $this->base->plugin->filter_name . '_publish_get_excerpt', $excerpt, $post );
1613
1614 // Return.
1615 return $excerpt;
1616
1617 }
1618
1619 /**
1620 * Safely generate a title, stripping tags and shortcodes, and applying filters so that
1621 * third party plugins (such as translation plugins) can determine the final output.
1622 *
1623 * @since 3.7.3
1624 *
1625 * @param \WP_Post $post WordPress Post.
1626 * @param bool $to_more_tag Only return content up to the <!-- more --> tag.
1627 * @return string Content
1628 */
1629 private function get_content( $post, $to_more_tag = false ) {
1630
1631 // Fetch content.
1632 // get_the_content() only works for WordPress 5.2+, which added the $post param.
1633 if ( $to_more_tag ) {
1634 $extended = get_extended( $post->post_content );
1635
1636 if ( ! empty( $extended['main'] ) ) {
1637 $content = $extended['main'];
1638 } else {
1639 // Fallback to the Post Content.
1640 $content = $post->post_content;
1641 }
1642 } else {
1643 $content = $post->post_content;
1644 }
1645
1646 // Strip shortcodes.
1647 $content = strip_shortcodes( $content );
1648
1649 // Remove the wpautop filter, as this converts double newlines into <p> tags.
1650 // In turn, <p> tags are correctly discarded later on in this function, as social networks don't support HTML.
1651 // However, this results in separation between paragraphs going from two newlines to one newline.
1652 // Some social media services further drop a single newline, meaning paragraphs become one long block of text, which isn't
1653 // intended.
1654 remove_filter( 'the_content', 'wpautop' );
1655
1656 // Remove some third party Plugin filters that wrongly output content that we don't want in a status.
1657 remove_filter( 'the_content', 'powerpress_content' );
1658
1659 // Apply filters to get true output.
1660 $content = apply_filters( 'the_content', $content );
1661
1662 // Restore wpautop that we just removed.
1663 add_filter( 'the_content', 'wpautop' );
1664
1665 // If the content originates from Gutenberg, remove double newlines and convert breaklines
1666 // into newlines.
1667 $is_gutenberg_request_content = $this->is_gutenberg_post_content( $post );
1668 if ( $is_gutenberg_request_content ) {
1669 // Remove double newlines, which may occur due to using Gutenberg blocks.
1670 // (blocks are separated with HTML comments, stripped using apply_filters( 'the_content' ), which results in double, or even triple, breaklines).
1671 $content = preg_replace( '/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n\n", $content );
1672
1673 // Convert <br> and <br /> into newlines.
1674 $content = preg_replace( '/<br(\s+)?\/?>/i', "\n", $content );
1675 }
1676
1677 // Convert to plain text.
1678 $content = $this->convert_to_plain_text( $content );
1679
1680 /**
1681 * Filters the dynamic {content} replacement, when a Post's status is being built.
1682 *
1683 * @since 3.7.3
1684 *
1685 * @param string $content Post Content.
1686 * @param \WP_Post $post WordPress Post.
1687 * @param bool $is_gutenberg_request_content Is Gutenberg Post Content.
1688 */
1689 $content = apply_filters( $this->base->plugin->filter_name . '_publish_get_content', $content, $post, $is_gutenberg_request_content );
1690
1691 // Return.
1692 return $content;
1693
1694 }
1695
1696 /**
1697 * Returns the date in the locale specified in WordPress.
1698 *
1699 * @since 4.7.7
1700 *
1701 * @param \WP_Post $post WordPress Post.
1702 * @return string Date
1703 */
1704 private function get_date( $post ) {
1705
1706 $date = date_i18n( get_option( 'date_format' ), strtotime( $post->post_date ) );
1707
1708 /*
1709 * Filters the dynamic {date} replacement, when a Post's status is being built.
1710 *
1711 * @since 4.7.7
1712 *
1713 * @param string $date Date.
1714 * @param \WP_Post $post WordPress Post.
1715 */
1716 $date = apply_filters( $this->base->plugin->filter_name . '_publish_get_date', $date, $post );
1717
1718 // Return.
1719 return $date;
1720
1721 }
1722
1723 /**
1724 * Returns the Permalink, including or excluding a trailing slash, depending on the Plugin settings.
1725 *
1726 * @since 4.0.6
1727 *
1728 * @param \WP_Post $post WordPress Post.
1729 * @return string WordPress Post Permalink
1730 */
1731 private function get_permalink( $post ) {
1732
1733 $force_trailing_forwardslash = $this->base->get_class( 'settings' )->get_option( 'force_trailing_forwardslash', false );
1734
1735 // Define the URL, depending on whether it should end with a forwardslash or not.
1736 // This is by design; more users complain that they get 301 redirects from site.com/post/ to site.com/post
1737 // than from site.com/post to site.com/post/.
1738 // We can't control misconfigured WordPress installs, so this option gives them the choice.
1739 if ( $force_trailing_forwardslash ) {
1740 $url = get_permalink( $post->ID );
1741
1742 // If the Permalink doesn't have a forwardslash at the end of it, add it now.
1743 if ( substr( $url, -1 ) !== '/' ) {
1744 $url .= '/';
1745 }
1746 } else {
1747 $url = rtrim( get_permalink( $post->ID ), '/' );
1748 }
1749
1750 /**
1751 * Filters the Post's Permalink, including or excluding a trailing slash, depending on the Plugin settings
1752 *
1753 * @since 4.0.6
1754 *
1755 * @param string $url WordPress Post Permalink.
1756 * @param \WP_Post $post WordPress Post.
1757 * @param bool $force_trailing_forwardslash Force Trailing Forwardslash.
1758 */
1759 $url = apply_filters( $this->base->plugin->filter_name . '_publish_get_permalink', $url, $post, $force_trailing_forwardslash );
1760
1761 // Return.
1762 return $url;
1763
1764 }
1765
1766 /**
1767 * Returns the Short Permalink
1768 *
1769 * @since 4.2.7
1770 *
1771 * @param \WP_Post $post WordPress Post.
1772 * @return string WordPress Post Permalink
1773 */
1774 private function get_short_permalink( $post ) {
1775
1776 // Define short permalink e.g http://yoursite.com/?p=1.
1777 $url = rtrim( get_bloginfo( 'url' ), '/' ) . '/?p=' . $post->ID;
1778
1779 /**
1780 * Filters the Post's Permalink, including or excluding a trailing slash, depending on the Plugin settings
1781 *
1782 * @since 4.2.7
1783 *
1784 * @param string $url WordPress Post Permalink.
1785 * @param \WP_Post $post WordPress Post.
1786 */
1787 $url = apply_filters( $this->base->plugin->filter_name . '_publish_get_short_permalink', $url, $post );
1788
1789 // Return.
1790 return $url;
1791
1792 }
1793
1794 /**
1795 * Converts the given string (which is typically HTML from a WordPress Post or Post Meta Field)
1796 * to plain text, by performing several functions:
1797 * - stripping shortcodes (if shortcodes need processing, do so before calling this function)
1798 * - removing all inline style elements and their contents,
1799 * - stripping HTML tags, excluding <br>, <br />, <a>, <li>
1800 * - decoding HTML entities to avoid encoding issues on status output
1801 * - converting <br> and <br /> to newlines
1802 * - removing double spaces
1803 * - trimming the final result of any leading or trailing spaces
1804 *
1805 * @since 4.6.9
1806 *
1807 * @param string $text Text.
1808 * @param bool $convert_links_to_inline true: Convert e.g. `<a href="http://foo.com">text</a>` to `text (http://foo.com)`.
1809 * false: Convert e.g. `<a href="http://foo.com">text</a>` to `text`.
1810 * @param bool $strip_urls Whether to strip URLs from the text.
1811 * @return string Text
1812 */
1813 private function convert_to_plain_text( $text, $convert_links_to_inline = true, $strip_urls = false ) {
1814
1815 // Strip any shortcodes still remaining.
1816 // If shortcodes need to be processed, they should be processed before calling this function.
1817 $text = strip_shortcodes( $text );
1818
1819 // Wrap content in <html>, <head> and <body> tags with an UTF-8 Content-Type meta tag.
1820 // Forcibly tell DOMDocument that this HTML uses the UTF-8 charset.
1821 // <meta charset="utf-8"> isn't enough, as DOMDocument still interprets the HTML as ISO-8859, which breaks character encoding
1822 // Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method.
1823 // If we don't, special characters render incorrectly.
1824 $text = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>' . $text . '</body></html>';
1825
1826 // Load the HTML into a DOMDocument.
1827 libxml_use_internal_errors( true );
1828 $html = new \DOMDocument();
1829 $html->loadHTML( $text );
1830
1831 // Load DOMDocument into XPath.
1832 $xpath = new \DOMXPath( $html );
1833
1834 // Remove inline style tags and their contents.
1835 foreach ( $xpath->query( '//style' ) as $node ) {
1836 $node->parentNode->removeChild( $node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1837 }
1838
1839 // Fetch revised HTML.
1840 $text = $html->saveHTML();
1841
1842 // Remove HTML, except breaklines, links and unordered list items.
1843 $retain_tags = ( $strip_urls ? '<br><li>' : '<br><a><li>' );
1844 $text = strip_tags( $text, $retain_tags );
1845
1846 // Decode excerpt to avoid encoding issues on status output.
1847 $text = html_entity_decode( $text );
1848
1849 // Convert <br> and <br /> into newlines.
1850 $text = preg_replace( '/<br(\s+)?\/?>/i', "\n", $text );
1851
1852 // Convert <a> to text and inline link.
1853 if ( $convert_links_to_inline ) {
1854 // Extract the text from the link, and add the link in brackets after the text.
1855 $text = preg_replace( '/<a[^>]+href=\"(.*?)\"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $text );
1856 } else {
1857 // Just extract the text from the link and output it.
1858 $text = preg_replace( '/<a[^>]+href=\"(.*?)\"[^>]*>(.*?)<\/a>/i', '$2', $text );
1859 }
1860
1861 // If URLs are to be stripped, remove them.
1862 if ( $strip_urls ) {
1863 $text = preg_replace( '/https?:\/\/[^\s]+/', '', $text );
1864 }
1865
1866 // Convert <li> to hyphenated.
1867 $text = preg_replace( '/<li[^>]*>(.*?)<\/li>/i', '- $1', $text );
1868
1869 // Remove double spaces, but retain newlines and accented characters.
1870 $text = preg_replace( '/[ ]{2,}/', ' ', $text );
1871
1872 // Remove tabs.
1873 $text = str_replace( "\t", '', $text );
1874
1875 // Finally, trim the text.
1876 $text = trim( $text );
1877
1878 // Return.
1879 return $text;
1880
1881 }
1882
1883 /**
1884 * Returns a flag denoting whether a character limit can safely be applied
1885 * to the given tag.
1886 *
1887 * @since 3.7.8
1888 *
1889 * @param string $tag Tag.
1890 * @return bool Can apply character limit
1891 */
1892 private function can_apply_character_limit_to_tag( $tag ) {
1893
1894 // Get Tags.
1895 $tags = $this->base->get_class( 'common' )->get_tags_excluded_from_character_limit();
1896
1897 // If the tag is in the array of tags excluded from character limits, we
1898 // cannot apply a character limit to this tag.
1899 if ( in_array( $tag, $tags, true ) ) {
1900 return false;
1901 }
1902
1903 // Can apply character limit to tag.
1904 return true;
1905
1906 }
1907
1908 /**
1909 * Applies the given word limit to the given text
1910 *
1911 * @since 3.8.9
1912 *
1913 * @param string $text Text.
1914 * @param int $word_limit Word Limit.
1915 * @return string Text
1916 */
1917 private function apply_word_limit( $text, $word_limit = 0 ) {
1918
1919 // Store original text.
1920 $original_text = $text;
1921
1922 // Bail if the word limit is zero or false.
1923 if ( ! $word_limit ) {
1924 return $text;
1925 }
1926
1927 // Limit text.
1928 $text = wp_trim_words( $text, $word_limit, '' );
1929
1930 /**
1931 * Applies the given word limit to the given text.
1932 *
1933 * @since 3.8.9
1934 *
1935 * @param string $text Text, with word limit applied.
1936 * @param int $word_limit Sentence Limit.
1937 * @param string $original_text Original Text, with no limit applied.
1938 */
1939 $text = apply_filters( $this->base->plugin->filter_name . '_publish_apply_word_limit', $text, $word_limit, $original_text );
1940
1941 return $text;
1942
1943 }
1944
1945 /**
1946 * Applies the given sentence limit to the given text
1947 *
1948 * @since 4.3.1
1949 *
1950 * @param string $text Text.
1951 * @param int $sentence_limit Sentence Limit.
1952 * @param int $min_sentence_length Minimum Sentence Length.
1953 * @return string
1954 */
1955 public function apply_sentence_limit( $text, $sentence_limit = 0, $min_sentence_length = 5 ) {
1956
1957 // Store original text.
1958 $original_text = $text;
1959
1960 // Bail if the sentence limit is zero or false.
1961 if ( ! $sentence_limit ) {
1962 return $text;
1963 }
1964
1965 // Build array of sentences.
1966 $parts = preg_split( '/(?<=[.?!])\s+(?=[a-z])/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1967
1968 // Iterate through the array, adding sentences to the array until we hit the sentence limit.
1969 // Sentences do not count towards the limit if they are shorter than the minimum sentence length.
1970 // This ensures abbreviations do not count towards the limit.
1971 $sentences = array();
1972 $sentence_count = 0;
1973 foreach ( $parts as $index => $sentence ) {
1974 // If we've hit the sentence limit, stop.
1975 if ( $sentence_count >= $sentence_limit ) {
1976 break;
1977 }
1978
1979 // Trim the sentence, adding it to the array.
1980 $sentences[ $index ] = trim( $sentence );
1981
1982 // If the sentence is longer than the minimum sentence length, count this as a sentence.
1983 if ( mb_strlen( $sentences[ $index ] ) > $min_sentence_length ) {
1984 ++$sentence_count;
1985 }
1986 }
1987
1988 // Implode into text, with a space between each sentence, trimming the array results to avoid double spacing.
1989 $text = implode( ' ', array_map( 'trim', $sentences ) );
1990
1991 /**
1992 * Applies the given sentence limit to the given text.
1993 *
1994 * @since 4.3.1
1995 *
1996 * @param string $text Text, with word limit applied.
1997 * @param int $sentence_limit Sentence Limit.
1998 * @param string $original_text Original Text, with no limit applied.
1999 */
2000 $text = apply_filters( $this->base->plugin->filter_name . '_publish_apply_sentence_limit', $text, $sentence_limit, $original_text );
2001
2002 // Return.
2003 return $text;
2004
2005 }
2006
2007 /**
2008 * Applies the given character limit to the given text
2009 *
2010 * @since 3.7.3
2011 *
2012 * @param string $text Text.
2013 * @param int $character_limit Character Limit.
2014 * @return string Text
2015 */
2016 private function apply_character_limit( $text, $character_limit = 0 ) {
2017
2018 // Bail if the character limit is zero or false.
2019 if ( ! $character_limit ) {
2020 return $text;
2021 }
2022
2023 // Bail if the content isn't longer than the character limit.
2024 if ( strlen( $text ) <= $character_limit ) {
2025 return $text;
2026 }
2027
2028 // Limit text.
2029 // Use mb_substr so that emojis don't break, which would result in text not being saved
2030 // by the social network when the status is sent.
2031 $text = mb_substr( $text, 0, $character_limit );
2032
2033 /**
2034 * Filters the character limited text.
2035 *
2036 * @since 3.7.3
2037 *
2038 * @param string $text Text, with character limit applied.
2039 * @param int $character_limit Character Limit used.
2040 */
2041 $text = apply_filters( $this->base->plugin->filter_name . '_publish_apply_character_limit', $text, $character_limit );
2042
2043 // Return.
2044 return $text;
2045
2046 }
2047
2048 /**
2049 * Helper method to iterate through statuses, sending each via a separate API call
2050 * to the API.
2051 *
2052 * @since 3.0.0
2053 *
2054 * @param array $statuses Statuses.
2055 * @param int $post_id Post ID.
2056 * @param string $action Action.
2057 * @param array $profiles All Enabled Profiles.
2058 * @param bool $test_mode Test Mode (won't send to API).
2059 * @return array API Result for each status
2060 */
2061 public function send( $statuses, $post_id, $action, $profiles, $test_mode = false ) {
2062
2063 // Assume no errors.
2064 $errors = false;
2065
2066 // Setup logging.
2067 $logs = array();
2068 $log_error = array();
2069 $log_enabled = $this->base->get_class( 'log' )->is_enabled();
2070
2071 foreach ( $statuses as $index => $status ) {
2072
2073 // If the status is a WP_Error, something went wrong in building the status to be sent.
2074 // Log the error and continue to the next status.
2075 if ( isset( $status['error'] ) && is_wp_error( $status['error'] ) ) {
2076 // Error.
2077 $errors = true;
2078 $logs[] = array(
2079 'action' => $action,
2080 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2081 'profile_id' => $status['profile_ids'][0],
2082 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2083 'result' => 'error',
2084 'result_message' => sprintf(
2085 /* translators: %1$s: Plugin Error string, %2$s: Error message from Plugin */
2086 '%1$s: %2$s',
2087 __( 'Plugin Error', 'wp-to-buffer' ),
2088 $status['error']->get_error_message()
2089 ),
2090 'status_text' => false,
2091 );
2092 $log_error[] = ( $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'] . ': ' . $status['error']->get_error_message() );
2093 continue;
2094 }
2095
2096 // If this is a test, add to the log array only.
2097 if ( $test_mode ) {
2098 $logs[] = array(
2099 'action' => $action,
2100 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2101 'profile_id' => $status['profile_ids'][0],
2102 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2103 'result' => 'test',
2104 'result_message' => '',
2105 'status_text' => $status['text'],
2106 'status_created_at' => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ),
2107 'status_due_at' => ( isset( $status['scheduled_at'] ) ? $status['scheduled_at'] : '' ),
2108 );
2109
2110 continue;
2111 }
2112
2113 // Setup API.
2114 $this->base->get_class( 'api' )->set_tokens(
2115 $this->base->get_class( 'settings' )->get_access_token_by_profile_id( $status['profile_ids'][0] ),
2116 $this->base->get_class( 'settings' )->get_refresh_token_by_profile_id( $status['profile_ids'][0] ),
2117 $this->base->get_class( 'settings' )->get_token_expires_by_profile_id( $status['profile_ids'][0] )
2118 );
2119
2120 // Send request.
2121 $result = $this->base->get_class( 'api' )->updates_create( $status, $profiles[ $status['profile_ids'][0] ]['service'] );
2122
2123 // Store result in log array.
2124 if ( is_wp_error( $result ) ) {
2125 // Error.
2126 $errors = true;
2127 $logs[] = array(
2128 'action' => $action,
2129 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2130 'profile_id' => $status['profile_ids'][0],
2131 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2132 'result' => 'error',
2133 'result_message' => $result->get_error_message(),
2134 'status_text' => $status['text'],
2135 );
2136 $log_error[] = ( $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'] . ': ' . $result->get_error_message() );
2137 } else {
2138 // OK.
2139 $logs[] = array(
2140 'action' => $action,
2141 'request_sent' => gmdate( 'Y-m-d H:i:s' ),
2142 'profile_id' => $result['profile_id'],
2143 'profile_name' => $profiles[ $status['profile_ids'][0] ]['formatted_service'] . ': ' . $profiles[ $status['profile_ids'][0] ]['formatted_username'],
2144 'result' => 'success',
2145 'result_message' => $result['message'],
2146 'status_text' => $result['status_text'],
2147 'status_created_at' => gmdate( 'Y-m-d H:i:s', $result['status_created_at'] ),
2148 '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' ),
2149 );
2150 }
2151 }
2152
2153 // Set the last sent timestamp, which we may use to prevent duplicate statuses.
2154 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_last_sent', time() );
2155
2156 // If we're reposting, update the last reposted date against the Post.
2157 // We do this here to ensure the Post isn't reposting again where e.g. one profile status worked + one profile status failed,
2158 // which would be deemed a failure.
2159 if ( $action === 'repost' && ! $test_mode ) {
2160 $this->base->get_class( 'repost' )->update_last_reposted_date( $post_id );
2161 }
2162
2163 // If no errors were reported, set a meta key to show a success message.
2164 // This triggers admin_notices() to tell the user what happened.
2165 if ( ! $errors ) {
2166 // Only set a success message if test mode is disabled.
2167 if ( ! $test_mode ) {
2168 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_success', 1 );
2169 }
2170 delete_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_error' );
2171 delete_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_errors' );
2172
2173 // Request that the user review the plugin. Notification displayed later,
2174 // can be called multiple times and won't re-display the notification if dismissed.
2175 $this->base->dashboard->request_review();
2176 } else {
2177 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_success', 0 );
2178 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_error', 1 );
2179 update_post_meta( $post_id, '_' . $this->base->plugin->filter_name . '_errors', $log_error );
2180 }
2181
2182 // Save the log, if logging is enabled.
2183 if ( $log_enabled ) {
2184 foreach ( $logs as $log ) {
2185 $this->base->get_class( 'log' )->add( $post_id, $log );
2186 }
2187 }
2188
2189 // Return log results.
2190 return $logs;
2191
2192 }
2193
2194 /**
2195 * Clears any searches and replacements stored in this class.
2196 *
2197 * @since 3.8.0
2198 */
2199 private function clear_search_replacements() {
2200
2201 $this->all_possible_searches_replacements = array();
2202 $this->searches_replacements = array();
2203
2204 }
2205
2206 }
2207