PluginProbe
Social Media Auto Poster – Schedule & Publish to Buffer / 6.2.2
Social Media Auto Poster – Schedule & Publish to Buffer v6.2.2
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.2, at lib/social/includes/class-publish.php

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