PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.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 All 502 releases
jetpack / modules / widgets / wordpress-post-widget / class.jetpack-display-posts-widget-base.php

class.jetpack-display-posts-widget-base.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php

851 lines 25.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3 use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit( 0 );
7 }
8
9 /**
10 * For back-compat, the final widget class must be named
11 * Jetpack_Display_Posts_Widget.
12 *
13 * For convenience, it's nice to have a widget class constructor with no
14 * arguments. Otherwise, we have to register the widget with an instance
15 * instead of a class name. This makes unregistering annoying.
16 *
17 * Both WordPress.com and Jetpack implement the final widget class by
18 * extending this __Base class and adding data fetching and storage.
19 *
20 * This would be a bit cleaner with dependency injection, but we already
21 * use mocking to test, so it's not a big win.
22 *
23 * That this widget is currently implemented as these two classes
24 * is an implementation detail and should not be depended on :)
25 *
26 * phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid
27 */
28 abstract class Jetpack_Display_Posts_Widget__Base extends WP_Widget {
29 // phpcs:enable PEAR.NamingConventions.ValidClassName.Invalid
30
31 /**
32 * Remote service API URL prefix.
33 *
34 * @var string
35 */
36 public $service_url = 'https://public-api.wordpress.com/rest/v1.1/';
37
38 /**
39 * Jetpack_Display_Posts_Widget__Base constructor.
40 */
41 public function __construct() {
42 parent::__construct(
43 // Internal id.
44 'jetpack_display_posts_widget',
45 /** This filter is documented in modules/widgets/facebook-likebox.php */
46 apply_filters( 'jetpack_widget_name', __( 'Display WordPress Posts', 'jetpack' ) ),
47 array(
48 'description' => __( 'Displays a list of recent posts from another WordPress.com or Jetpack-enabled blog.', 'jetpack' ),
49 'customize_selective_refresh' => true,
50 )
51 );
52
53 if ( is_customize_preview() ) {
54 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
55 }
56 }
57
58 /**
59 * Enqueue CSS and JavaScript.
60 *
61 * @since 4.0.0
62 */
63 public function enqueue_scripts() {
64 wp_enqueue_style(
65 'jetpack_display_posts_widget',
66 plugins_url( 'style.css', __FILE__ ),
67 array(),
68 JETPACK__VERSION
69 );
70 }
71
72 // DATA STORE: Must implement.
73
74 /**
75 * Gets blog data from the cache.
76 *
77 * @param string $site Site.
78 *
79 * @return array|WP_Error
80 */
81 abstract public function get_blog_data( $site );
82
83 /**
84 * Update a widget instance.
85 *
86 * @param string $site The site to fetch the latest data for.
87 *
88 * @return array - the new data
89 */
90 abstract public function update_instance( $site );
91
92 // WIDGET API.
93
94 /**
95 * Set up the widget display on the front end.
96 *
97 * @param array $args Widget args.
98 * @param array $instance Widget instance.
99 */
100 public function widget( $args, $instance ) {
101 /** This action is documented in modules/widgets/gravatar-profile.php */
102 do_action( 'jetpack_stats_extra', 'widget_view', 'display_posts' );
103
104 // Enqueue front end assets.
105 $this->enqueue_scripts();
106
107 $content = $args['before_widget'];
108
109 if ( empty( $instance['url'] ) ) {
110 if ( current_user_can( 'manage_options' ) ) {
111 $content .= '<p>';
112 /* Translators: the "Blog URL" field mentioned is the input field labeled as such in the widget form. */
113 $content .= esc_html__( 'The Blog URL is not properly set up in the widget.', 'jetpack' );
114 $content .= '</p>';
115 }
116 $content .= $args['after_widget'];
117
118 echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
119 return;
120 }
121
122 $data = $this->get_blog_data( $instance['url'] );
123 // Check for errors.
124 if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) {
125 $content .= '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>';
126 $content .= $args['after_widget'];
127
128 echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
129 return;
130 }
131
132 $site_info = $data['site_info']['data'];
133
134 if ( ! empty( $instance['title'] ) ) {
135 /** This filter is documented in core/src/wp-includes/default-widgets.php */
136 $instance['title'] = apply_filters( 'widget_title', $instance['title'] );
137 $content .= $args['before_title'] . $instance['title'] . ': ' . esc_html( $site_info->name ) . $args['after_title'];
138 } else {
139 $content .= $args['before_title'] . esc_html( $site_info->name ) . $args['after_title'];
140 }
141
142 $content .= '<div class="jetpack-display-remote-posts">';
143
144 if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) {
145 $content .= '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>';
146 $content .= '</div><!-- .jetpack-display-remote-posts -->';
147 $content .= $args['after_widget'];
148
149 echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
150 return;
151 }
152
153 $posts_list = $data['posts']['data'];
154
155 /**
156 * Show only as much posts as we need. If we have less than configured amount,
157 * we must show only that much posts.
158 */
159 $number_of_posts = min( $instance['number_of_posts'], is_countable( $posts_list ) ? count( $posts_list ) : 0 );
160
161 for ( $i = 0; $i < $number_of_posts; $i++ ) {
162 $single_post = $posts_list[ $i ];
163 $post_title = ( $single_post['title'] ) ? $single_post['title'] : '( No Title )';
164
165 $target = '';
166 if ( isset( $instance['open_in_new_window'] ) && true === $instance['open_in_new_window'] ) {
167 $target = ' target="_blank" rel="noopener"';
168 }
169 $content .= '<h4><a href="' . esc_url( $single_post['url'] ) . '"' . $target . '>' . esc_html( $post_title ) . '</a></h4>' . "\n";
170 if ( ( true === $instance['featured_image'] ) && ( ! empty( $single_post['featured_image'] ) ) ) {
171 $featured_image = $single_post['featured_image'];
172 /**
173 * Allows setting up custom Photon parameters to manipulate the image output in the Display Posts widget.
174 *
175 * @see https://developer.wordpress.com/docs/photon/
176 *
177 * @module widgets
178 *
179 * @since 3.6.0
180 *
181 * @param array $args Array of Photon Parameters.
182 */
183 $image_params = apply_filters( 'jetpack_display_posts_widget_image_params', array() );
184 $content .= '<a title="' . esc_attr( $post_title ) . '" href="' . esc_url( $single_post['url'] ) . '"' . $target . '><img src="' . esc_url( Image_CDN_Core::cdn_url( $featured_image, $image_params ) ) . '" alt="' . esc_attr( $post_title ) . '"/></a>';
185 }
186
187 if ( true === $instance['show_excerpts'] ) {
188 $content .= wp_kses_post( $single_post['excerpt'] );
189 }
190 }
191
192 $content .= '</div><!-- .jetpack-display-remote-posts -->';
193 $content .= $args['after_widget'];
194
195 /**
196 * Filter the WordPress Posts widget content.
197 *
198 * @module widgets
199 *
200 * @since 4.7.0
201 *
202 * @param string $content Widget content.
203 */
204 echo apply_filters( 'jetpack_display_posts_widget_content', $content ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
205 }
206
207 /**
208 * Display the widget administration form.
209 *
210 * @param array $instance Widget instance configuration.
211 *
212 * @return string|void
213 */
214 public function form( $instance ) {
215
216 /**
217 * Initialize widget configuration variables.
218 */
219 $title = $instance['title'] ?? __( 'Recent Posts', 'jetpack' );
220 $url = $instance['url'] ?? '';
221 $number_of_posts = $instance['number_of_posts'] ?? 5;
222 $open_in_new_window = $instance['open_in_new_window'] ?? false;
223 $featured_image = $instance['featured_image'] ?? false;
224 $show_excerpts = $instance['show_excerpts'] ?? false;
225
226 /**
227 * Check if the widget instance has errors available.
228 *
229 * Only do so if a URL is set.
230 */
231 $update_errors = array();
232
233 if ( ! empty( $url ) ) {
234 $data = $this->get_blog_data( $url );
235 $update_errors = $this->extract_errors_from_blog_data( $data );
236 }
237
238 ?>
239 <p>
240 <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label>
241 <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
242 </p>
243
244 <p>
245 <label for="<?php echo esc_attr( $this->get_field_id( 'url' ) ); ?>"><?php esc_html_e( 'Blog URL:', 'jetpack' ); ?></label>
246 <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'url' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'url' ) ); ?>" type="text" value="<?php echo esc_attr( $url ); ?>" />
247 <i>
248 <?php esc_html_e( 'Enter a WordPress.com or Jetpack WordPress site URL.', 'jetpack' ); ?>
249 </i>
250 <?php
251 /**
252 * Show an error if the URL field was left empty.
253 *
254 * The error is shown only when the widget was already saved.
255 */
256 if ( empty( $url ) && ! preg_match( '/__i__|%i%/', $this->id ) ) {
257 ?>
258 <br />
259 <i class="error-message"><?php esc_html_e( 'You must specify a valid blog URL!', 'jetpack' ); ?></i>
260 <?php
261 }
262 ?>
263 </p>
264 <p>
265 <label for="<?php echo esc_attr( $this->get_field_id( 'number_of_posts' ) ); ?>"><?php esc_html_e( 'Number of Posts to Display:', 'jetpack' ); ?></label>
266 <select name="<?php echo esc_attr( $this->get_field_name( 'number_of_posts' ) ); ?>">
267 <?php
268 for ( $i = 1; $i <= 10; $i++ ) {
269 echo '<option value="' . esc_attr( $i ) . '" ' . selected( $number_of_posts, $i ) . '>' . esc_html( $i ) . '</option>';
270 }
271 ?>
272 </select>
273 </p>
274 <p>
275 <label for="<?php echo esc_attr( $this->get_field_id( 'open_in_new_window' ) ); ?>"><?php esc_html_e( 'Open links in new window/tab:', 'jetpack' ); ?></label>
276 <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'open_in_new_window' ) ); ?>" <?php checked( $open_in_new_window, 1 ); ?> />
277 </p>
278 <p>
279 <label for="<?php echo esc_attr( $this->get_field_id( 'featured_image' ) ); ?>"><?php esc_html_e( 'Show Featured Image:', 'jetpack' ); ?></label>
280 <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'featured_image' ) ); ?>" <?php checked( $featured_image, 1 ); ?> />
281 </p>
282 <p>
283 <label for="<?php echo esc_attr( $this->get_field_id( 'show_excerpts' ) ); ?>"><?php esc_html_e( 'Show Excerpts:', 'jetpack' ); ?></label>
284 <input type="checkbox" name="<?php echo esc_attr( $this->get_field_name( 'show_excerpts' ) ); ?>" <?php checked( $show_excerpts, 1 ); ?> />
285 </p>
286
287 <?php
288
289 /**
290 * Show error messages.
291 */
292 if ( ! empty( $update_errors['message'] ) ) {
293
294 /**
295 * Prepare the error messages.
296 */
297
298 $where_message = '';
299 switch ( $update_errors['where'] ) {
300 case 'posts':
301 $where_message .= __( 'An error occurred while downloading blog posts list', 'jetpack' );
302 break;
303
304 /**
305 * If something else, beside `posts` and `site_info` broke,
306 * don't handle it and default to blog `information`,
307 * as it is generic enough.
308 */
309 case 'site_info':
310 default:
311 $where_message .= __( 'An error occurred while downloading blog information', 'jetpack' );
312 break;
313 }
314
315 ?>
316 <p class="error-message">
317 <?php echo esc_html( $where_message ); ?>:
318 <br />
319 <i>
320 <?php echo esc_html( $update_errors['message'] ); ?>
321 <?php
322 /**
323 * If there is any debug - show it here.
324 */
325 if ( ! empty( $update_errors['debug'] ) ) {
326 ?>
327 <br />
328 <br />
329 <?php esc_html_e( 'Detailed information', 'jetpack' ); ?>:
330 <br />
331 <?php echo esc_html( $update_errors['debug'] ); ?>
332 <?php
333 }
334 ?>
335 </i>
336 </p>
337
338 <?php
339 }
340 }
341
342 /**
343 * Widget update function.
344 *
345 * @param array $new_instance New instance widget settings.
346 * @param array $old_instance Old instance widget settings.
347 */
348 public function update( $new_instance, $old_instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
349
350 $instance = array();
351 $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? wp_strip_all_tags( $new_instance['title'] ) : '';
352 $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? wp_strip_all_tags( trim( $new_instance['url'] ) ) : '';
353 $instance['url'] = preg_replace( '!^https?://!is', '', $instance['url'] );
354 $instance['url'] = untrailingslashit( $instance['url'] );
355
356 /**
357 * Check if the URL should be with or without the www prefix before saving.
358 */
359 if ( ! empty( $instance['url'] ) ) {
360 $blog_data = $this->fetch_blog_data( $instance['url'], array(), true );
361
362 if ( is_wp_error( $blog_data['site_info']['error'] ) && str_starts_with( $instance['url'], 'www.' ) ) {
363 $blog_data = $this->fetch_blog_data( substr( $instance['url'], 4 ), array(), true );
364
365 if ( ! is_wp_error( $blog_data['site_info']['error'] ) ) {
366 $instance['url'] = substr( $instance['url'], 4 );
367 }
368 }
369 }
370
371 $instance['number_of_posts'] = ( ! empty( $new_instance['number_of_posts'] ) ) ? (int) $new_instance['number_of_posts'] : '';
372 $instance['open_in_new_window'] = ( ! empty( $new_instance['open_in_new_window'] ) ) ? true : '';
373 $instance['featured_image'] = ( ! empty( $new_instance['featured_image'] ) ) ? true : '';
374 $instance['show_excerpts'] = ( ! empty( $new_instance['show_excerpts'] ) ) ? true : '';
375
376 /**
377 * If there is no cache entry for the specified URL, run a forced update.
378 *
379 * @see get_blog_data Returns WP_Error if the cache is empty, which is what is needed here.
380 */
381 $cached_data = $this->get_blog_data( $instance['url'] );
382
383 if ( is_wp_error( $cached_data ) ) {
384 $this->update_instance( $instance['url'] );
385 }
386
387 return $instance;
388 }
389
390 // DATA PROCESSING.
391
392 /**
393 * Expiring transients have a name length maximum of 45 characters,
394 * so this function returns an abbreviated MD5 hash to use instead of
395 * the full URI.
396 *
397 * @param string $site Site to get the hash for.
398 *
399 * @return string
400 */
401 public function get_site_hash( $site ) {
402 return substr( md5( $site ), 0, 21 );
403 }
404
405 /**
406 * Fetch a remote service endpoint and parse it.
407 *
408 * Timeout is set to 15 seconds right now, because sometimes the WordPress API
409 * takes more than 5 seconds to fully respond.
410 *
411 * Caching is used here so we can avoid re-downloading the same endpoint
412 * in a single request.
413 *
414 * @param string $endpoint Parametrized endpoint to call.
415 *
416 * @param int $timeout How much time to wait for the API to respond before failing.
417 *
418 * @return array|WP_Error
419 */
420 public function fetch_service_endpoint( $endpoint, $timeout = 15 ) {
421
422 /**
423 * Holds endpoint request cache.
424 */
425 static $cache = array();
426
427 if ( ! isset( $cache[ $endpoint ] ) ) {
428 $raw_data = $this->wp_wp_remote_get( $this->service_url . ltrim( $endpoint, '/' ), array( 'timeout' => $timeout ) );
429 $cache[ $endpoint ] = $this->parse_service_response( $raw_data );
430 }
431
432 return $cache[ $endpoint ];
433 }
434
435 /**
436 * Parse data from service response.
437 * Do basic error handling for general service and data errors
438 *
439 * @param array $service_response Response from the service.
440 *
441 * @return array|WP_Error
442 */
443 public function parse_service_response( $service_response ) {
444 /**
445 * If there is an error, we add the error message to the parsed response
446 */
447 if ( is_wp_error( $service_response ) ) {
448 return new WP_Error(
449 'general_error',
450 __( 'An error occurred fetching the remote data.', 'jetpack' ),
451 $service_response->get_error_messages()
452 );
453 }
454
455 /**
456 * Validate HTTP response code.
457 */
458 if ( 200 !== wp_remote_retrieve_response_code( $service_response ) ) {
459 return new WP_Error(
460 'http_error',
461 __( 'An error occurred fetching the remote data.', 'jetpack' ),
462 wp_remote_retrieve_response_message( $service_response )
463 );
464 }
465
466 /**
467 * Extract service response body from the request.
468 */
469
470 $service_response_body = wp_remote_retrieve_body( $service_response );
471
472 /**
473 * No body has been set in the response. This should be pretty bad.
474 */
475 if ( ! $service_response_body ) {
476 return new WP_Error(
477 'no_body',
478 __( 'Invalid remote response.', 'jetpack' ),
479 'No body in response.'
480 );
481 }
482
483 /**
484 * Parse the JSON response from the API. Convert to associative array.
485 */
486 $parsed_data = json_decode( $service_response_body );
487
488 /**
489 * If there is a problem with parsing the posts return an empty array.
490 */
491 if ( $parsed_data === null ) {
492 return new WP_Error(
493 'no_body',
494 __( 'Invalid remote response.', 'jetpack' ),
495 'Invalid JSON from remote.'
496 );
497 }
498
499 /**
500 * Check for errors in the parsed body.
501 */
502 if ( isset( $parsed_data->error ) ) {
503 return new WP_Error(
504 'remote_error',
505 __( 'It looks like the WordPress site URL is incorrectly configured. Please check it in your widget settings.', 'jetpack' ),
506 $parsed_data->error
507 );
508 }
509
510 /**
511 * No errors found, return parsed data.
512 */
513 return $parsed_data;
514 }
515
516 /**
517 * Fetch site information from the WordPress public API
518 *
519 * @param string $site URL of the site to fetch the information for.
520 *
521 * @return array|WP_Error
522 */
523 public function fetch_site_info( $site ) {
524
525 $response = $this->fetch_service_endpoint( sprintf( '/sites/%s', rawurlencode( $site ) ) );
526
527 return $response;
528 }
529
530 /**
531 * Parse external API response from the site info call and handle errors if they occur.
532 *
533 * @param array|WP_Error $service_response The raw response to be parsed.
534 *
535 * @return array|WP_Error
536 */
537 public function parse_site_info_response( $service_response ) {
538
539 /**
540 * If the service returned an error, we pass it on.
541 */
542 if ( is_wp_error( $service_response ) ) {
543 return $service_response;
544 }
545
546 /**
547 * Check if the service returned proper site information.
548 */
549 if ( ! isset( $service_response->ID ) ) {
550 return new WP_Error(
551 'no_site_info',
552 __( 'Invalid site information returned from remote.', 'jetpack' ),
553 'No site ID present in the response.'
554 );
555 }
556
557 return $service_response;
558 }
559
560 /**
561 * Fetch list of posts from the WordPress public API.
562 *
563 * @param int $site_id The site to fetch the posts for.
564 *
565 * @return array|WP_Error
566 */
567 public function fetch_posts_for_site( $site_id ) {
568
569 $response = $this->fetch_service_endpoint(
570 sprintf(
571 '/sites/%1$d/posts/%2$s',
572 $site_id,
573 /**
574 * Filters the parameters used to fetch for posts in the Display Posts Widget.
575 *
576 * @see https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/
577 *
578 * @module widgets
579 *
580 * @since 3.6.0
581 *
582 * @param string $args Extra parameters to filter posts returned from the WordPress.com REST API.
583 */
584 apply_filters( 'jetpack_display_posts_widget_posts_params', '?fields=id,title,excerpt,URL,featured_image' )
585 )
586 );
587
588 return $response;
589 }
590
591 /**
592 * Parse external API response from the posts list request and handle errors if any occur.
593 *
594 * @param object|WP_Error $service_response The raw response to be parsed.
595 *
596 * @return array|WP_Error
597 */
598 public function parse_posts_response( $service_response ) {
599
600 /**
601 * If the service returned an error, we pass it on.
602 */
603 if ( is_wp_error( $service_response ) ) {
604 return $service_response;
605 }
606
607 /**
608 * Check if the service returned proper posts array.
609 */
610 if ( ! isset( $service_response->posts ) || ! is_array( $service_response->posts ) ) {
611 return new WP_Error(
612 'no_posts',
613 __( 'No posts data returned by remote.', 'jetpack' ),
614 'No posts information set in the returned data.'
615 );
616 }
617
618 /**
619 * Format the posts to preserve storage space.
620 */
621
622 return $this->format_posts_for_storage( $service_response );
623 }
624
625 /**
626 * Format the posts for better storage. Drop all the data that is not used.
627 *
628 * @param object $parsed_data Array of posts returned by the APIs.
629 *
630 * @return array Formatted posts or an empty array if no posts were found.
631 */
632 public function format_posts_for_storage( $parsed_data ) {
633
634 $formatted_posts = array();
635
636 /**
637 * Only go through the posts list if we have valid posts array.
638 */
639 if ( isset( $parsed_data->posts ) && is_array( $parsed_data->posts ) ) {
640
641 /**
642 * Loop through all the posts and format them appropriately.
643 */
644 foreach ( $parsed_data->posts as $single_post ) {
645
646 $prepared_post = array(
647 'title' => $single_post->title ? $single_post->title : '',
648 'excerpt' => $single_post->excerpt ? $single_post->excerpt : '',
649 'featured_image' => $single_post->featured_image ? $single_post->featured_image : '',
650 'url' => $single_post->URL, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
651 );
652
653 /**
654 * Append the formatted post to the results.
655 */
656 $formatted_posts[] = $prepared_post;
657 }
658 }
659
660 return $formatted_posts;
661 }
662
663 /**
664 * Fetch site information and posts list for a site.
665 *
666 * @param string $site Site to fetch the data for.
667 * @param array $original_data Optional original data to updated.
668 *
669 * @param bool $site_data_only Fetch only site information, skip posts list.
670 *
671 * @return array Updated or new data.
672 */
673 public function fetch_blog_data( $site, $original_data = array(), $site_data_only = false ) {
674
675 /**
676 * If no optional data is supplied, initialize a new structure
677 */
678 if ( ! empty( $original_data ) && is_array( $original_data ) ) {
679 $widget_data = $original_data;
680 } else {
681 $widget_data = array(
682 'site_info' => array(
683 'last_check' => null,
684 'last_update' => null,
685 'error' => null,
686 'data' => array(),
687 ),
688 'posts' => array(
689 'last_check' => null,
690 'last_update' => null,
691 'error' => null,
692 'data' => array(),
693 ),
694 );
695 }
696
697 /**
698 * Update check time and fetch site information.
699 */
700 $widget_data['site_info']['last_check'] = time();
701
702 $site_info_raw_data = $this->fetch_site_info( $site );
703 $site_info_parsed_data = $this->parse_site_info_response( $site_info_raw_data );
704
705 /**
706 * If there is an error with the fetched site info, save the error and update the checked time.
707 */
708 if ( is_wp_error( $site_info_parsed_data ) ) {
709 $widget_data['site_info']['error'] = $site_info_parsed_data;
710
711 return $widget_data;
712 } else {
713 /**
714 * If data is fetched successfully, update the data and set the proper time.
715 *
716 * Data is only updated if we have valid results. This is done this way so we can show
717 * something if external service is down.
718 */
719 $widget_data['site_info']['last_update'] = time();
720 $widget_data['site_info']['data'] = $site_info_parsed_data;
721 $widget_data['site_info']['error'] = null;
722 }
723
724 /**
725 * If only site data is needed, return it here, don't fetch posts data.
726 */
727 if ( true === $site_data_only ) {
728 return $widget_data;
729 }
730
731 /**
732 * Update check time and fetch posts list.
733 */
734 $widget_data['posts']['last_check'] = time();
735
736 $site_posts_raw_data = $this->fetch_posts_for_site( $site_info_parsed_data->ID );
737 $site_posts_parsed_data = $this->parse_posts_response( $site_posts_raw_data );
738
739 /**
740 * If there is an error with the fetched posts, save the error and update the checked time.
741 */
742 if ( is_wp_error( $site_posts_parsed_data ) ) {
743 $widget_data['posts']['error'] = $site_posts_parsed_data;
744
745 return $widget_data;
746 } else {
747 /**
748 * If data is fetched successfully, update the data and set the proper time.
749 *
750 * Data is only updated if we have valid results. This is done this way so we can show
751 * something if external service is down.
752 */
753 $widget_data['posts']['last_update'] = time();
754 $widget_data['posts']['data'] = $site_posts_parsed_data;
755 $widget_data['posts']['error'] = null;
756 }
757
758 return $widget_data;
759 }
760
761 /**
762 * Scan and extract first error from blog data array.
763 *
764 * @param array|WP_Error $blog_data Blog data to scan for errors.
765 *
766 * @return string First error message found
767 */
768 public function extract_errors_from_blog_data( $blog_data ) {
769
770 $errors = array(
771 'message' => '',
772 'debug' => '',
773 'where' => '',
774 );
775
776 /**
777 * When the cache result is an error. Usually when the cache is empty.
778 * This is not an error case for now.
779 */
780 if ( is_wp_error( $blog_data ) ) {
781 return $errors;
782 }
783
784 /**
785 * Loop through `site_info` and `posts` keys of $blog_data.
786 */
787 foreach ( array( 'site_info', 'posts' ) as $info_key ) {
788
789 /**
790 * Contains information on which stage the error ocurred.
791 */
792 $errors['where'] = $info_key;
793
794 /**
795 * If an error is set, we want to check it for usable messages.
796 */
797 if ( isset( $blog_data[ $info_key ]['error'] ) && ! empty( $blog_data[ $info_key ]['error'] ) ) {
798
799 /**
800 * Extract error message from the error, if possible.
801 */
802 if ( is_wp_error( $blog_data[ $info_key ]['error'] ) ) {
803 /**
804 * In the case of WP_Error we want to have the error message
805 * and the debug information available.
806 */
807 $error_messages = $blog_data[ $info_key ]['error']->get_error_messages();
808 $errors['message'] = reset( $error_messages );
809
810 $extra_data = $blog_data[ $info_key ]['error']->get_error_data();
811 if ( is_array( $extra_data ) ) {
812 $errors['debug'] = implode( '; ', $extra_data );
813 } else {
814 $errors['debug'] = $extra_data;
815 }
816
817 break;
818 } elseif ( is_array( $blog_data[ $info_key ]['error'] ) ) {
819 /**
820 * In this case we don't have debug information, because
821 * we have no way to know the format. The widget works with
822 * WP_Error objects only.
823 */
824 $errors['message'] = reset( $blog_data[ $info_key ]['error'] );
825 break;
826 }
827
828 /**
829 * We do nothing if no usable error is found.
830 */
831 }
832 }
833
834 return $errors;
835 }
836
837 /**
838 * This is just to make method mocks in the unit tests easier.
839 *
840 * @param string $url The URL to fetch.
841 * @param array $args Optional. Request arguments.
842 *
843 * @return array|WP_Error
844 *
845 * @codeCoverageIgnore
846 */
847 public function wp_wp_remote_get( $url, $args = array() ) {
848 return wp_remote_get( $url, $args );
849 }
850 }
851