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

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