PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 6.2.2
Jetpack – WP Security, Backup, Speed, & Growth v6.2.2
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / widgets / wordpress-post-widget.php
wordpress-post-widget.php
1,169 lines 32.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Display Recent WordPress Posts Widget
4 * Description: Displays recent posts from a WordPress.com or Jetpack-enabled self-hosted WordPress site.
5 * Version: 1.0
6 * Author: Brad Angelcyk, Kathryn Presner, Justin Shreve, Carolyn Sonnek
7 * Author URI: http://automattic.com
8 * License: GPL2
9 */
10
11 /**
12 * Disable direct access/execution to/of the widget code.
13 */
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 add_action( 'widgets_init', 'jetpack_display_posts_widget' );
19 function jetpack_display_posts_widget() {
20 register_widget( 'Jetpack_Display_Posts_Widget' );
21 }
22
23
24 /**
25 * Cron tasks
26 */
27
28 add_filter( 'cron_schedules', 'jetpack_display_posts_widget_cron_intervals' );
29
30 /**
31 * Adds 10 minute running interval to the cron schedules.
32 *
33 * @param array $current_schedules Currently defined schedules list.
34 *
35 * @return array
36 */
37 function jetpack_display_posts_widget_cron_intervals( $current_schedules ) {
38
39 /**
40 * Only add the 10 minute interval if it wasn't already set.
41 */
42 if ( ! isset( $current_schedules['minutes_10'] ) ) {
43 $current_schedules['minutes_10'] = array(
44 'interval' => 10 * MINUTE_IN_SECONDS,
45 'display' => 'Every 10 minutes'
46 );
47 }
48
49 return $current_schedules;
50 }
51
52 /**
53 * Execute the cron task
54 */
55 add_action( 'jetpack_display_posts_widget_cron_update', 'jetpack_display_posts_update_cron_action' );
56 function jetpack_display_posts_update_cron_action() {
57 $widget = new Jetpack_Display_Posts_Widget();
58 $widget->cron_task();
59 }
60
61 /**
62 * Handle activation procedures for the cron.
63 *
64 * `updating_jetpack_version` - Handle cron activation when Jetpack gets updated. It's here
65 * to cover the first cron activation after the update.
66 *
67 * `jetpack_activate_module_widgets` - Activate the cron when the Extra Sidebar widgets are activated.
68 *
69 * `activated_plugin` - Activate the cron when Jetpack gets activated.
70 *
71 */
72 add_action( 'updating_jetpack_version', 'jetpack_display_posts_widget_conditionally_activate_cron' );
73 add_action( 'jetpack_activate_module_widgets', 'Jetpack_Display_Posts_Widget::activate_cron' );
74 add_action( 'activated_plugin', 'jetpack_conditionally_activate_cron_on_plugin_activation' );
75
76 /**
77 * Executed when Jetpack gets activated. Tries to activate the cron if it is needed.
78 *
79 * @param string $plugin_file_name The plugin file that was activated.
80 */
81 function jetpack_conditionally_activate_cron_on_plugin_activation( $plugin_file_name ) {
82 if ( plugin_basename( JETPACK__PLUGIN_FILE ) === $plugin_file_name ) {
83 jetpack_display_posts_widget_conditionally_activate_cron();
84 }
85 }
86
87 /**
88 * Activates the cron only when needed.
89 * @see Jetpack_Display_Posts_Widget::should_cron_be_running
90 */
91 function jetpack_display_posts_widget_conditionally_activate_cron() {
92 $widget = new Jetpack_Display_Posts_Widget();
93 if ( $widget->should_cron_be_running() ) {
94 $widget->activate_cron();
95 }
96
97 unset( $widget );
98 }
99
100 /**
101 * End of cron activation handling.
102 */
103
104
105 /**
106 * Handle deactivation procedures where they are needed.
107 *
108 * If Extra Sidebar Widgets module is deactivated, the cron is not needed.
109 *
110 * If Jetpack is deactivated, the cron is not needed.
111 */
112 add_action( 'jetpack_deactivate_module_widgets', 'Jetpack_Display_Posts_Widget::deactivate_cron_static' );
113 register_deactivation_hook( plugin_basename( JETPACK__PLUGIN_FILE ), 'Jetpack_Display_Posts_Widget::deactivate_cron_static' );
114
115 /**
116 * End of Cron tasks
117 */
118 /*
119 * Display a list of recent posts from a WordPress.com or Jetpack-enabled blog.
120 */
121
122 class Jetpack_Display_Posts_Widget extends WP_Widget {
123
124 /**
125 * @var string Remote service API URL prefix.
126 */
127 public $service_url = 'https://public-api.wordpress.com/rest/v1.1/';
128
129 /**
130 * @var string Widget options key prefix.
131 */
132 public $widget_options_key_prefix = 'display_posts_site_data_';
133
134 /**
135 * @var string The name of the cron that will update widget data.
136 */
137 public static $cron_name = 'jetpack_display_posts_widget_cron_update';
138
139
140 public function __construct() {
141 parent::__construct(
142 // internal id
143 'jetpack_display_posts_widget',
144 /** This filter is documented in modules/widgets/facebook-likebox.php */
145 apply_filters( 'jetpack_widget_name', __( 'Display WordPress Posts', 'jetpack' ) ),
146 array(
147 'description' => __( 'Displays a list of recent posts from another WordPress.com or Jetpack-enabled blog.', 'jetpack' ),
148 'customize_selective_refresh' => true,
149 )
150 );
151
152 if ( is_customize_preview() ) {
153 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
154 }
155 }
156
157 /**
158 * Expiring transients have a name length maximum of 45 characters,
159 * so this function returns an abbreviated MD5 hash to use instead of
160 * the full URI.
161 *
162 * @param string $site Site to get the hash for.
163 *
164 * @return string
165 */
166 public function get_site_hash( $site ) {
167 return substr( md5( $site ), 0, 21 );
168 }
169
170 /**
171 * Fetch a remote service endpoint and parse it.
172 *
173 * Timeout is set to 15 seconds right now, because sometimes the WordPress API
174 * takes more than 5 seconds to fully respond.
175 *
176 * Caching is used here so we can avoid re-downloading the same endpoint
177 * in a single request.
178 *
179 * @param string $endpoint Parametrized endpoint to call.
180 *
181 * @param int $timeout How much time to wait for the API to respond before failing.
182 *
183 * @return array|WP_Error
184 */
185 public function fetch_service_endpoint( $endpoint, $timeout = 15 ) {
186
187 /**
188 * Holds endpoint request cache.
189 */
190 static $cache = array();
191
192 if ( ! isset( $cache[ $endpoint ] ) ) {
193 $raw_data = $this->wp_wp_remote_get( $this->service_url . ltrim( $endpoint, '/' ), array( 'timeout' => $timeout ) );
194 $cache[ $endpoint ] = $this->parse_service_response( $raw_data );
195 }
196
197 return $cache[ $endpoint ];
198 }
199
200 /**
201 * Parse data from service response.
202 * Do basic error handling for general service and data errors
203 *
204 * @param array $service_response Response from the service.
205 *
206 * @return array|WP_Error
207 */
208 public function parse_service_response( $service_response ) {
209 /**
210 * If there is an error, we add the error message to the parsed response
211 */
212 if ( is_wp_error( $service_response ) ) {
213 return new WP_Error(
214 'general_error',
215 __( 'An error occurred fetching the remote data.', 'jetpack' ),
216 $service_response->get_error_messages()
217 );
218 }
219
220 /**
221 * Validate HTTP response code.
222 */
223 if ( 200 !== wp_remote_retrieve_response_code( $service_response ) ) {
224 return new WP_Error(
225 'http_error',
226 __( 'An error occurred fetching the remote data.', 'jetpack' ),
227 wp_remote_retrieve_response_message( $service_response )
228 );
229 }
230
231
232 /**
233 * Extract service response body from the request.
234 */
235
236 $service_response_body = wp_remote_retrieve_body( $service_response );
237
238
239 /**
240 * No body has been set in the response. This should be pretty bad.
241 */
242 if ( ! $service_response_body ) {
243 return new WP_Error(
244 'no_body',
245 __( 'Invalid remote response.', 'jetpack' ),
246 'No body in response.'
247 );
248 }
249
250 /**
251 * Parse the JSON response from the API. Convert to associative array.
252 */
253 $parsed_data = json_decode( $service_response_body );
254
255 /**
256 * If there is a problem with parsing the posts return an empty array.
257 */
258 if ( is_null( $parsed_data ) ) {
259 return new WP_Error(
260 'no_body',
261 __( 'Invalid remote response.', 'jetpack' ),
262 'Invalid JSON from remote.'
263 );
264 }
265
266 /**
267 * Check for errors in the parsed body.
268 */
269 if ( isset( $parsed_data->error ) ) {
270 return new WP_Error(
271 'remote_error',
272 __( 'It looks like the WordPress site URL is incorrectly configured. Please check it in your widget settings.', 'jetpack' ),
273 $parsed_data->error
274 );
275 }
276
277
278 /**
279 * No errors found, return parsed data.
280 */
281 return $parsed_data;
282 }
283
284 /**
285 * Fetch site information from the WordPress public API
286 *
287 * @param string $site URL of the site to fetch the information for.
288 *
289 * @return array|WP_Error
290 */
291 public function fetch_site_info( $site ) {
292
293 $response = $this->fetch_service_endpoint( sprintf( '/sites/%s', urlencode( $site ) ) );
294
295 return $response;
296 }
297
298 /**
299 * Parse external API response from the site info call and handle errors if they occur.
300 *
301 * @param array|WP_Error $service_response The raw response to be parsed.
302 *
303 * @return array|WP_Error
304 */
305 public function parse_site_info_response( $service_response ) {
306
307 /**
308 * If the service returned an error, we pass it on.
309 */
310 if ( is_wp_error( $service_response ) ) {
311 return $service_response;
312 }
313
314 /**
315 * Check if the service returned proper site information.
316 */
317 if ( ! isset( $service_response->ID ) ) {
318 return new WP_Error(
319 'no_site_info',
320 __( 'Invalid site information returned from remote.', 'jetpack' ),
321 'No site ID present in the response.'
322 );
323 }
324
325 return $service_response;
326 }
327
328 /**
329 * Fetch list of posts from the WordPress public API.
330 *
331 * @param int $site_id The site to fetch the posts for.
332 *
333 * @return array|WP_Error
334 */
335 public function fetch_posts_for_site( $site_id ) {
336
337 $response = $this->fetch_service_endpoint(
338 sprintf(
339 '/sites/%1$d/posts/%2$s',
340 $site_id,
341 /**
342 * Filters the parameters used to fetch for posts in the Display Posts Widget.
343 *
344 * @see https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/
345 *
346 * @module widgets
347 *
348 * @since 3.6.0
349 *
350 * @param string $args Extra parameters to filter posts returned from the WordPress.com REST API.
351 */
352 apply_filters( 'jetpack_display_posts_widget_posts_params', '' )
353 )
354 );
355
356 return $response;
357 }
358
359 /**
360 * Parse external API response from the posts list request and handle errors if any occur.
361 *
362 * @param object|WP_Error $service_response The raw response to be parsed.
363 *
364 * @return array|WP_Error
365 */
366 public function parse_posts_response( $service_response ) {
367
368 /**
369 * If the service returned an error, we pass it on.
370 */
371 if ( is_wp_error( $service_response ) ) {
372 return $service_response;
373 }
374
375 /**
376 * Check if the service returned proper posts array.
377 */
378 if ( ! isset( $service_response->posts ) || ! is_array( $service_response->posts ) ) {
379 return new WP_Error(
380 'no_posts',
381 __( 'No posts data returned by remote.', 'jetpack' ),
382 'No posts information set in the returned data.'
383 );
384 }
385
386 /**
387 * Format the posts to preserve storage space.
388 */
389
390 return $this->format_posts_for_storage( $service_response );
391 }
392
393 /**
394 * Format the posts for better storage. Drop all the data that is not used.
395 *
396 * @param object $parsed_data Array of posts returned by the APIs.
397 *
398 * @return array Formatted posts or an empty array if no posts were found.
399 */
400 public function format_posts_for_storage( $parsed_data ) {
401
402 $formatted_posts = array();
403
404 /**
405 * Only go through the posts list if we have valid posts array.
406 */
407 if ( isset( $parsed_data->posts ) && is_array( $parsed_data->posts ) ) {
408
409 /**
410 * Loop through all the posts and format them appropriately.
411 */
412 foreach ( $parsed_data->posts as $single_post ) {
413
414 $prepared_post = array(
415 'title' => $single_post->title ? $single_post->title : '',
416 'excerpt' => $single_post->excerpt ? $single_post->excerpt : '',
417 'featured_image' => $single_post->featured_image ? $single_post->featured_image : '',
418 'url' => $single_post->URL,
419 );
420
421 /**
422 * Append the formatted post to the results.
423 */
424 $formatted_posts[] = $prepared_post;
425 }
426 }
427
428 return $formatted_posts;
429 }
430
431 /**
432 * Fetch site information and posts list for a site.
433 *
434 * @param string $site Site to fetch the data for.
435 * @param array $original_data Optional original data to updated.
436 *
437 * @param bool $site_data_only Fetch only site information, skip posts list.
438 *
439 * @return array Updated or new data.
440 */
441 public function fetch_blog_data( $site, $original_data = array(), $site_data_only = false ) {
442
443 /**
444 * If no optional data is supplied, initialize a new structure
445 */
446 if ( ! empty( $original_data ) ) {
447 $widget_data = $original_data;
448 }
449 else {
450 $widget_data = array(
451 'site_info' => array(
452 'last_check' => null,
453 'last_update' => null,
454 'error' => null,
455 'data' => array(),
456 ),
457 'posts' => array(
458 'last_check' => null,
459 'last_update' => null,
460 'error' => null,
461 'data' => array(),
462 )
463 );
464 }
465
466 /**
467 * Update check time and fetch site information.
468 */
469 $widget_data['site_info']['last_check'] = time();
470
471 $site_info_raw_data = $this->fetch_site_info( $site );
472 $site_info_parsed_data = $this->parse_site_info_response( $site_info_raw_data );
473
474
475 /**
476 * If there is an error with the fetched site info, save the error and update the checked time.
477 */
478 if ( is_wp_error( $site_info_parsed_data ) ) {
479 $widget_data['site_info']['error'] = $site_info_parsed_data;
480
481 return $widget_data;
482 }
483 /**
484 * If data is fetched successfully, update the data and set the proper time.
485 *
486 * Data is only updated if we have valid results. This is done this way so we can show
487 * something if external service is down.
488 *
489 */
490 else {
491 $widget_data['site_info']['last_update'] = time();
492 $widget_data['site_info']['data'] = $site_info_parsed_data;
493 $widget_data['site_info']['error'] = null;
494 }
495
496
497 /**
498 * If only site data is needed, return it here, don't fetch posts data.
499 */
500 if ( true === $site_data_only ) {
501 return $widget_data;
502 }
503
504 /**
505 * Update check time and fetch posts list.
506 */
507 $widget_data['posts']['last_check'] = time();
508
509 $site_posts_raw_data = $this->fetch_posts_for_site( $site_info_parsed_data->ID );
510 $site_posts_parsed_data = $this->parse_posts_response( $site_posts_raw_data );
511
512
513 /**
514 * If there is an error with the fetched posts, save the error and update the checked time.
515 */
516 if ( is_wp_error( $site_posts_parsed_data ) ) {
517 $widget_data['posts']['error'] = $site_posts_parsed_data;
518
519 return $widget_data;
520 }
521 /**
522 * If data is fetched successfully, update the data and set the proper time.
523 *
524 * Data is only updated if we have valid results. This is done this way so we can show
525 * something if external service is down.
526 *
527 */
528 else {
529 $widget_data['posts']['last_update'] = time();
530 $widget_data['posts']['data'] = $site_posts_parsed_data;
531 $widget_data['posts']['error'] = null;
532 }
533
534 return $widget_data;
535 }
536
537 /**
538 * Gets blog data from the cache.
539 *
540 * @param string $site
541 *
542 * @return array|WP_Error
543 */
544 public function get_blog_data( $site ) {
545 // load from cache, if nothing return an error
546 $site_hash = $this->get_site_hash( $site );
547
548 $cached_data = $this->wp_get_option( $this->widget_options_key_prefix . $site_hash );
549
550 /**
551 * If the cache is empty, return an empty_cache error.
552 */
553 if ( false === $cached_data ) {
554 return new WP_Error(
555 'empty_cache',
556 __( 'Information about this blog is currently being retrieved.', 'jetpack' )
557 );
558 }
559
560 return $cached_data;
561
562 }
563
564 /**
565 * Activates widget update cron task.
566 */
567 public static function activate_cron() {
568 if ( ! wp_next_scheduled( self::$cron_name ) ) {
569 wp_schedule_event( time(), 'minutes_10', self::$cron_name );
570 }
571 }
572
573 /**
574 * Deactivates widget update cron task.
575 *
576 * This is a wrapper over the static method as it provides some syntactic sugar.
577 */
578 public function deactivate_cron() {
579 self::deactivate_cron_static();
580 }
581
582 /**
583 * Deactivates widget update cron task.
584 */
585 public static function deactivate_cron_static() {
586 $next_scheduled_time = wp_next_scheduled( self::$cron_name );
587 wp_unschedule_event( $next_scheduled_time, self::$cron_name );
588 }
589
590 /**
591 * Checks if the update cron should be running and returns appropriate result.
592 *
593 * @return bool If the cron should be running or not.
594 */
595 public function should_cron_be_running() {
596 /**
597 * The cron doesn't need to run empty loops.
598 */
599 $widget_instances = $this->get_instances_sites();
600
601 if ( empty( $widget_instances ) || ! is_array( $widget_instances ) ) {
602 return false;
603 }
604
605 if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
606 /**
607 * If Jetpack is not active or in development mode, we don't want to update widget data.
608 */
609 if ( ! Jetpack::is_active() && ! Jetpack::is_development_mode() ) {
610 return false;
611 }
612
613 /**
614 * If Extra Sidebar Widgets module is not active, we don't need to update widget data.
615 */
616 if ( ! Jetpack::is_module_active( 'widgets' ) ) {
617 return false;
618 }
619 }
620
621 /**
622 * If none of the above checks failed, then we definitely want to update widget data.
623 */
624 return true;
625 }
626
627 /**
628 * Main cron code. Updates all instances of the widget.
629 *
630 * @return bool
631 */
632 public function cron_task() {
633
634 /**
635 * If the cron should not be running, disable it.
636 */
637 if ( false === $this->should_cron_be_running() ) {
638 return true;
639 }
640
641 $instances_to_update = $this->get_instances_sites();
642
643 /**
644 * If no instances are found to be updated - stop.
645 */
646 if ( empty( $instances_to_update ) || ! is_array( $instances_to_update ) ) {
647 return true;
648 }
649
650 foreach ( $instances_to_update as $site_url ) {
651 $this->update_instance( $site_url );
652 }
653
654 return true;
655 }
656
657 /**
658 * Get a list of unique sites from all instances of the widget.
659 *
660 * @return array|bool
661 */
662 public function get_instances_sites() {
663
664 $widget_settings = $this->wp_get_option( 'widget_jetpack_display_posts_widget' );
665
666 /**
667 * If the widget still hasn't been added anywhere, the config will not be present.
668 *
669 * In such case we don't want to continue execution.
670 */
671 if ( false === $widget_settings || ! is_array( $widget_settings ) ) {
672 return false;
673 }
674
675 $urls = array();
676
677 foreach ( $widget_settings as $widget_instance_data ) {
678 if ( isset( $widget_instance_data['url'] ) && ! empty( $widget_instance_data['url'] ) ) {
679 $urls[] = $widget_instance_data['url'];
680 }
681 }
682
683 /**
684 * Make sure only unique URLs are returned.
685 */
686 $urls = array_unique( $urls );
687
688 return $urls;
689
690 }
691
692 /**
693 * Update a widget instance.
694 *
695 * @param string $site The site to fetch the latest data for.
696 */
697 public function update_instance( $site ) {
698
699 /**
700 * Fetch current information for a site.
701 */
702 $site_hash = $this->get_site_hash( $site );
703
704 $option_key = $this->widget_options_key_prefix . $site_hash;
705
706 $instance_data = $this->wp_get_option( $option_key );
707
708 /**
709 * Fetch blog data and save it in $instance_data.
710 */
711 $new_data = $this->fetch_blog_data( $site, $instance_data );
712
713 /**
714 * If the option doesn't exist yet - create a new option
715 */
716 if ( false === $instance_data ) {
717 $this->wp_add_option( $option_key, $new_data );
718 }
719 else {
720 $this->wp_update_option( $option_key, $new_data );
721 }
722 }
723
724 /**
725 * Set up the widget display on the front end.
726 *
727 * @param array $args
728 * @param array $instance
729 */
730 public function widget( $args, $instance ) {
731 /** This action is documented in modules/widgets/gravatar-profile.php */
732 do_action( 'jetpack_stats_extra', 'widget_view', 'display_posts' );
733
734 // Enqueue front end assets.
735 $this->enqueue_scripts();
736
737 $content = $args['before_widget'];
738
739 if ( empty( $instance['url'] ) ) {
740 if ( current_user_can( 'manage_options' ) ) {
741 $content .= '<p>';
742 /* Translators: the "Blog URL" field mentioned is the input field labeled as such in the widget form. */
743 $content .= esc_html__( 'The Blog URL is not properly setup in the widget.', 'jetpack' );
744 $content .= '</p>';
745 }
746 $content .= $args['after_widget'];
747
748 echo $content;
749 return;
750 }
751
752 $data = $this->get_blog_data( $instance['url'] );
753 // check for errors
754 if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) {
755 $content .= '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>';
756 $content .= $args['after_widget'];
757
758 echo $content;
759 return;
760 }
761
762 $site_info = $data['site_info']['data'];
763
764 if ( ! empty( $instance['title'] ) ) {
765 /** This filter is documented in core/src/wp-includes/default-widgets.php */
766 $instance['title'] = apply_filters( 'widget_title', $instance['title'] );
767 $content .= $args['before_title'] . esc_html( $instance['title'] . ': ' . $site_info->name ) . $args['after_title'];
768 }
769 else {
770 $content .= $args['before_title'] . esc_html( $site_info->name ) . $args['after_title'];
771 }
772
773 $content .= '<div class="jetpack-display-remote-posts">';
774
775 if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) {
776 $content .= '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>';
777 $content .= '</div><!-- .jetpack-display-remote-posts -->';
778 $content .= $args['after_widget'];
779
780 echo $content;
781 return;
782 }
783
784 $posts_list = $data['posts']['data'];
785
786 /**
787 * Show only as much posts as we need. If we have less than configured amount,
788 * we must show only that much posts.
789 */
790 $number_of_posts = min( $instance['number_of_posts'], count( $posts_list ) );
791
792 for ( $i = 0; $i < $number_of_posts; $i ++ ) {
793 $single_post = $posts_list[ $i ];
794 $post_title = ( $single_post['title'] ) ? $single_post['title'] : '( No Title )';
795
796 $target = '';
797 if ( isset( $instance['open_in_new_window'] ) && $instance['open_in_new_window'] == true ) {
798 $target = ' target="_blank" rel="noopener"';
799 }
800 $content .= '<h4><a href="' . esc_url( $single_post['url'] ) . '"' . $target . '>' . esc_html( $post_title ) . '</a></h4>' . "\n";
801 if ( ( $instance['featured_image'] == true ) && ( ! empty ( $single_post['featured_image'] ) ) ) {
802 $featured_image = $single_post['featured_image'];
803 /**
804 * Allows setting up custom Photon parameters to manipulate the image output in the Display Posts widget.
805 *
806 * @see https://developer.wordpress.com/docs/photon/
807 *
808 * @module widgets
809 *
810 * @since 3.6.0
811 *
812 * @param array $args Array of Photon Parameters.
813 */
814 $image_params = apply_filters( 'jetpack_display_posts_widget_image_params', array() );
815 $content .= '<a title="' . esc_attr( $post_title ) . '" href="' . esc_url( $single_post['url'] ) . '"' . $target . '><img src="' . jetpack_photon_url( $featured_image, $image_params ) . '" alt="' . esc_attr( $post_title ) . '"/></a>';
816 }
817
818 if ( $instance['show_excerpts'] == true ) {
819 $content .= $single_post['excerpt'];
820 }
821 }
822
823 $content .= '</div><!-- .jetpack-display-remote-posts -->';
824 $content .= $args['after_widget'];
825
826 /**
827 * Filter the WordPress Posts widget content.
828 *
829 * @module widgets
830 *
831 * @since 4.7.0
832 *
833 * @param string $content Widget content.
834 */
835 echo apply_filters( 'jetpack_display_posts_widget_content', $content );
836 }
837
838 /**
839 * Scan and extract first error from blog data array.
840 *
841 * @param array|WP_Error $blog_data Blog data to scan for errors.
842 *
843 * @return string First error message found
844 */
845 public function extract_errors_from_blog_data( $blog_data ) {
846
847 $errors = array(
848 'message' => '',
849 'debug' => '',
850 'where' => '',
851 );
852
853
854 /**
855 * When the cache result is an error. Usually when the cache is empty.
856 * This is not an error case for now.
857 */
858 if ( is_wp_error( $blog_data ) ) {
859 return $errors;
860 }
861
862 /**
863 * Loop through `site_info` and `posts` keys of $blog_data.
864 */
865 foreach ( array( 'site_info', 'posts' ) as $info_key ) {
866
867 /**
868 * Contains information on which stage the error ocurred.
869 */
870 $errors['where'] = $info_key;
871
872 /**
873 * If an error is set, we want to check it for usable messages.
874 */
875 if ( isset( $blog_data[ $info_key ]['error'] ) && ! empty( $blog_data[ $info_key ]['error'] ) ) {
876
877 /**
878 * Extract error message from the error, if possible.
879 */
880 if ( is_wp_error( $blog_data[ $info_key ]['error'] ) ) {
881 /**
882 * In the case of WP_Error we want to have the error message
883 * and the debug information available.
884 */
885 $error_messages = $blog_data[ $info_key ]['error']->get_error_messages();
886 $errors['message'] = reset( $error_messages );
887
888 $extra_data = $blog_data[ $info_key ]['error']->get_error_data();
889 if ( is_array( $extra_data ) ) {
890 $errors['debug'] = implode( '; ', $extra_data );
891 }
892 else {
893 $errors['debug'] = $extra_data;
894 }
895
896 break;
897 }
898 elseif ( is_array( $blog_data[ $info_key ]['error'] ) ) {
899 /**
900 * In this case we don't have debug information, because
901 * we have no way to know the format. The widget works with
902 * WP_Error objects only.
903 */
904 $errors['message'] = reset( $blog_data[ $info_key ]['error'] );
905 break;
906 }
907
908 /**
909 * We do nothing if no usable error is found.
910 */
911 }
912 }
913
914 return $errors;
915 }
916
917 /**
918 * Enqueue CSS and JavaScript.
919 *
920 * @since 4.0.0
921 */
922 public function enqueue_scripts() {
923 wp_enqueue_style( 'jetpack_display_posts_widget', plugins_url( 'wordpress-post-widget/style.css', __FILE__ ) );
924 }
925
926 /**
927 * Display the widget administration form.
928 *
929 * @param array $instance Widget instance configuration.
930 *
931 * @return string|void
932 */
933 public function form( $instance ) {
934
935 /**
936 * Initialize widget configuration variables.
937 */
938 $title = ( isset( $instance['title'] ) ) ? $instance['title'] : __( 'Recent Posts', 'jetpack' );
939 $url = ( isset( $instance['url'] ) ) ? $instance['url'] : '';
940 $number_of_posts = ( isset( $instance['number_of_posts'] ) ) ? $instance['number_of_posts'] : 5;
941 $open_in_new_window = ( isset( $instance['open_in_new_window'] ) ) ? $instance['open_in_new_window'] : false;
942 $featured_image = ( isset( $instance['featured_image'] ) ) ? $instance['featured_image'] : false;
943 $show_excerpts = ( isset( $instance['show_excerpts'] ) ) ? $instance['show_excerpts'] : false;
944
945
946 /**
947 * Check if the widget instance has errors available.
948 *
949 * Only do so if a URL is set.
950 */
951 $update_errors = array();
952
953 if ( ! empty( $url ) ) {
954 $data = $this->get_blog_data( $url );
955 $update_errors = $this->extract_errors_from_blog_data( $data );
956 }
957
958 ?>
959 <p>
960 <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'jetpack' ); ?></label>
961 <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
962 </p>
963
964 <p>
965 <label for="<?php echo $this->get_field_id( 'url' ); ?>"><?php _e( 'Blog URL:', 'jetpack' ); ?></label>
966 <input class="widefat" id="<?php echo $this->get_field_id( 'url' ); ?>" name="<?php echo $this->get_field_name( 'url' ); ?>" type="text" value="<?php echo esc_attr( $url ); ?>" />
967 <i>
968 <?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?>
969 </i>
970 <?php
971 /**
972 * Show an error if the URL field was left empty.
973 *
974 * The error is shown only when the widget was already saved.
975 */
976 if ( empty( $url ) && ! preg_match( '/__i__|%i%/', $this->id ) ) {
977 ?>
978 <br />
979 <i class="error-message"><?php echo __( 'You must specify a valid blog URL!', 'jetpack' ); ?></i>
980 <?php
981 }
982 ?>
983 </p>
984 <p>
985 <label for="<?php echo $this->get_field_id( 'number_of_posts' ); ?>"><?php _e( 'Number of Posts to Display:', 'jetpack' ); ?></label>
986 <select name="<?php echo $this->get_field_name( 'number_of_posts' ); ?>">
987 <?php
988 for ( $i = 1; $i <= 10; $i ++ ) {
989 echo '<option value="' . $i . '" ' . selected( $number_of_posts, $i ) . '>' . $i . '</option>';
990 }
991 ?>
992 </select>
993 </p>
994 <p>
995 <label for="<?php echo $this->get_field_id( 'open_in_new_window' ); ?>"><?php _e( 'Open links in new window/tab:', 'jetpack' ); ?></label>
996 <input type="checkbox" name="<?php echo $this->get_field_name( 'open_in_new_window' ); ?>" <?php checked( $open_in_new_window, 1 ); ?> />
997 </p>
998 <p>
999 <label for="<?php echo $this->get_field_id( 'featured_image' ); ?>"><?php _e( 'Show Featured Image:', 'jetpack' ); ?></label>
1000 <input type="checkbox" name="<?php echo $this->get_field_name( 'featured_image' ); ?>" <?php checked( $featured_image, 1 ); ?> />
1001 </p>
1002 <p>
1003 <label for="<?php echo $this->get_field_id( 'show_excerpts' ); ?>"><?php _e( 'Show Excerpts:', 'jetpack' ); ?></label>
1004 <input type="checkbox" name="<?php echo $this->get_field_name( 'show_excerpts' ); ?>" <?php checked( $show_excerpts, 1 ); ?> />
1005 </p>
1006
1007 <?php
1008
1009 /**
1010 * Show error messages.
1011 */
1012 if ( ! empty( $update_errors['message'] ) ) {
1013
1014 /**
1015 * Prepare the error messages.
1016 */
1017
1018 $where_message = '';
1019 switch ( $update_errors['where'] ) {
1020 case 'posts':
1021 $where_message .= __( 'An error occurred while downloading blog posts list', 'jetpack' );
1022 break;
1023
1024 /**
1025 * If something else, beside `posts` and `site_info` broke,
1026 * don't handle it and default to blog `information`,
1027 * as it is generic enough.
1028 */
1029 case 'site_info':
1030 default:
1031 $where_message .= __( 'An error occurred while downloading blog information', 'jetpack' );
1032 break;
1033 }
1034
1035 ?>
1036 <p class="error-message">
1037 <?php echo esc_html( $where_message ); ?>:
1038 <br />
1039 <i>
1040 <?php echo esc_html( $update_errors['message'] ); ?>
1041 <?php
1042 /**
1043 * If there is any debug - show it here.
1044 */
1045 if ( ! empty( $update_errors['debug'] ) ) {
1046 ?>
1047 <br />
1048 <br />
1049 <?php esc_html_e( 'Detailed information', 'jetpack' ); ?>:
1050 <br />
1051 <?php echo esc_html( $update_errors['debug'] ); ?>
1052 <?php
1053 }
1054 ?>
1055 </i>
1056 </p>
1057
1058 <?php
1059 }
1060 }
1061
1062 public function update( $new_instance, $old_instance ) {
1063
1064 $instance = array();
1065 $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
1066 $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? strip_tags( trim( $new_instance['url'] ) ) : '';
1067 $instance['url'] = preg_replace( "!^https?://!is", "", $instance['url'] );
1068 $instance['url'] = untrailingslashit( $instance['url'] );
1069
1070
1071 /**
1072 * Check if the URL should be with or without the www prefix before saving.
1073 */
1074 if ( ! empty( $instance['url'] ) ) {
1075 $blog_data = $this->fetch_blog_data( $instance['url'], array(), true );
1076
1077 if ( is_wp_error( $blog_data['site_info']['error'] ) && 'www.' === substr( $instance['url'], 0, 4 ) ) {
1078 $blog_data = $this->fetch_blog_data( substr( $instance['url'], 4 ), array(), true );
1079
1080 if ( ! is_wp_error( $blog_data['site_info']['error'] ) ) {
1081 $instance['url'] = substr( $instance['url'], 4 );
1082 }
1083 }
1084 }
1085
1086 $instance['number_of_posts'] = ( ! empty( $new_instance['number_of_posts'] ) ) ? intval( $new_instance['number_of_posts'] ) : '';
1087 $instance['open_in_new_window'] = ( ! empty( $new_instance['open_in_new_window'] ) ) ? true : '';
1088 $instance['featured_image'] = ( ! empty( $new_instance['featured_image'] ) ) ? true : '';
1089 $instance['show_excerpts'] = ( ! empty( $new_instance['show_excerpts'] ) ) ? true : '';
1090
1091 /**
1092 * Forcefully activate the update cron when saving widget instance.
1093 *
1094 * So we can be sure that it will be running later.
1095 */
1096 $this->activate_cron();
1097
1098
1099 /**
1100 * If there is no cache entry for the specified URL, run a forced update.
1101 *
1102 * @see get_blog_data Returns WP_Error if the cache is empty, which is what is needed here.
1103 */
1104 $cached_data = $this->get_blog_data( $instance['url'] );
1105
1106 if ( is_wp_error( $cached_data ) ) {
1107 $this->update_instance( $instance['url'] );
1108 }
1109
1110 return $instance;
1111 }
1112
1113 /**
1114 * This is just to make method mocks in the unit tests easier.
1115 *
1116 * @param string $param Option key to get
1117 *
1118 * @return mixed
1119 *
1120 * @codeCoverageIgnore
1121 */
1122 public function wp_get_option( $param ) {
1123 return get_option( $param );
1124 }
1125
1126 /**
1127 * This is just to make method mocks in the unit tests easier.
1128 *
1129 * @param string $option_name Option name to be added
1130 * @param mixed $option_value Option value
1131 *
1132 * @return mixed
1133 *
1134 * @codeCoverageIgnore
1135 */
1136 public function wp_add_option( $option_name, $option_value ) {
1137 return add_option( $option_name, $option_value );
1138 }
1139
1140 /**
1141 * This is just to make method mocks in the unit tests easier.
1142 *
1143 * @param string $option_name Option name to be updated
1144 * @param mixed $option_value Option value
1145 *
1146 * @return mixed
1147 *
1148 * @codeCoverageIgnore
1149 */
1150 public function wp_update_option( $option_name, $option_value ) {
1151 return update_option( $option_name, $option_value );
1152 }
1153
1154
1155 /**
1156 * This is just to make method mocks in the unit tests easier.
1157 *
1158 * @param string $url The URL to fetch
1159 * @param array $args Optional. Request arguments.
1160 *
1161 * @return array|WP_Error
1162 *
1163 * @codeCoverageIgnore
1164 */
1165 public function wp_wp_remote_get( $url, $args = array() ) {
1166 return wp_remote_get( $url, $args );
1167 }
1168 }
1169