Completed
Push — try/gutenberg-separate-jetpack... ( e8dd3e...f0efb9 )
by Bernhard
39:26 queued 23:22
created

Jetpack_Display_Posts_Widget__Base::widget()   D

Complexity

Conditions 15
Paths 39

Size

Total Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 39
nop 2
dl 0
loc 107
rs 4.7333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * For back-compat, the final widget class must be named
5
 * Jetpack_Display_Posts_Widget.
6
 *
7
 * For convenience, it's nice to have a widget class constructor with no
8
 * arguments. Otherwise, we have to register the widget with an instance
9
 * instead of a class name. This makes unregistering annoying.
10
 *
11
 * Both WordPress.com and Jetpack implement the final widget class by
12
 * extending this __Base class and adding data fetching and storage.
13
 *
14
 * This would be a bit cleaner with dependency injection, but we already
15
 * use mocking to test, so it's not a big win.
16
 *
17
 * That this widget is currently implemented as these two classes
18
 * is an implementation detail and should not be depended on :)
19
 */
20
abstract class Jetpack_Display_Posts_Widget__Base extends WP_Widget {
21
	/**
22
	 * @var string Remote service API URL prefix.
23
	 */
24
	public $service_url = 'https://public-api.wordpress.com/rest/v1.1/';
25
26
	public function __construct() {
27
		parent::__construct(
28
		// internal id
29
			'jetpack_display_posts_widget',
30
			/** This filter is documented in modules/widgets/facebook-likebox.php */
31
			apply_filters( 'jetpack_widget_name', __( 'Display WordPress Posts', 'jetpack' ) ),
32
			array(
33
				'description' => __( 'Displays a list of recent posts from another WordPress.com or Jetpack-enabled blog.', 'jetpack' ),
34
				'customize_selective_refresh' => true,
35
			)
36
		);
37
38
		if ( is_customize_preview() ) {
39
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
40
		}
41
	}
42
43
	/**
44
	 * Enqueue CSS and JavaScript.
45
	 *
46
	 * @since 4.0.0
47
	 */
48
	public function enqueue_scripts() {
49
		wp_enqueue_style( 'jetpack_display_posts_widget', plugins_url( 'style.css', __FILE__ ) );
50
	}
51
52
53
	// DATA STORE: Must implement
54
55
	/**
56
	 * Gets blog data from the cache.
57
	 *
58
	 * @param string $site
59
	 *
60
	 * @return array|WP_Error
61
	 */
62
	abstract public function get_blog_data( $site );
63
64
	/**
65
	 * Update a widget instance.
66
	 *
67
	 * @param string $site The site to fetch the latest data for.
68
	 *
69
	 * @return array - the new data
70
	 */
71
	abstract public function update_instance( $site );
72
73
74
	// WIDGET API
75
76
	/**
77
	 * Set up the widget display on the front end.
78
	 *
79
	 * @param array $args
80
	 * @param array $instance
81
	 */
82
	public function widget( $args, $instance ) {
83
		/** This action is documented in modules/widgets/gravatar-profile.php */
84
		do_action( 'jetpack_stats_extra', 'widget_view', 'display_posts' );
85
86
		// Enqueue front end assets.
87
		$this->enqueue_scripts();
88
89
		$content = $args['before_widget'];
90
91
		if ( empty( $instance['url'] ) ) {
92
			if ( current_user_can( 'manage_options' ) ) {
93
				$content .= '<p>';
94
				/* Translators: the "Blog URL" field mentioned is the input field labeled as such in the widget form. */
95
				$content .= esc_html__( 'The Blog URL is not properly setup in the widget.', 'jetpack' );
96
				$content .= '</p>';
97
			}
98
			$content .= $args['after_widget'];
99
100
			echo $content;
101
			return;
102
		}
103
104
		$data = $this->get_blog_data( $instance['url'] );
105
		// check for errors
106
		if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) {
107
			$content .= '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>';
108
			$content .= $args['after_widget'];
109
110
			echo $content;
111
			return;
112
		}
113
114
		$site_info = $data['site_info']['data'];
115
116
		if ( ! empty( $instance['title'] ) ) {
117
			/** This filter is documented in core/src/wp-includes/default-widgets.php */
118
			$instance['title'] = apply_filters( 'widget_title', $instance['title'] );
119
			$content .= $args['before_title'] . esc_html( $instance['title'] . ': ' . $site_info->name ) . $args['after_title'];
120
		}
121
		else {
122
			$content .= $args['before_title'] . esc_html( $site_info->name ) . $args['after_title'];
123
		}
124
125
		$content .= '<div class="jetpack-display-remote-posts">';
126
127
		if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) {
128
			$content .= '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>';
129
			$content .= '</div><!-- .jetpack-display-remote-posts -->';
130
			$content .= $args['after_widget'];
131
132
			echo $content;
133
			return;
134
		}
135
136
		$posts_list = $data['posts']['data'];
137
138
		/**
139
		 * Show only as much posts as we need. If we have less than configured amount,
140
		 * we must show only that much posts.
141
		 */
142
		$number_of_posts = min( $instance['number_of_posts'], count( $posts_list ) );
143
144
		for ( $i = 0; $i < $number_of_posts; $i ++ ) {
145
			$single_post = $posts_list[ $i ];
146
			$post_title  = ( $single_post['title'] ) ? $single_post['title'] : '( No Title )';
147
148
			$target = '';
149
			if ( isset( $instance['open_in_new_window'] ) && $instance['open_in_new_window'] == true ) {
150
				$target = ' target="_blank" rel="noopener"';
151
			}
152
			$content .= '<h4><a href="' . esc_url( $single_post['url'] ) . '"' . $target . '>' . esc_html( $post_title ) . '</a></h4>' . "\n";
153
			if ( ( $instance['featured_image'] == true ) && ( ! empty ( $single_post['featured_image'] ) ) ) {
154
				$featured_image = $single_post['featured_image'];
155
				/**
156
				 * Allows setting up custom Photon parameters to manipulate the image output in the Display Posts widget.
157
				 *
158
				 * @see    https://developer.wordpress.com/docs/photon/
159
				 *
160
				 * @module widgets
161
				 *
162
				 * @since  3.6.0
163
				 *
164
				 * @param array $args Array of Photon Parameters.
165
				 */
166
				$image_params = apply_filters( 'jetpack_display_posts_widget_image_params', array() );
167
				$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>';
168
			}
169
170
			if ( $instance['show_excerpts'] == true ) {
171
				$content .= $single_post['excerpt'];
172
			}
173
		}
174
175
		$content .= '</div><!-- .jetpack-display-remote-posts -->';
176
		$content .= $args['after_widget'];
177
178
		/**
179
		 * Filter the WordPress Posts widget content.
180
		 *
181
		 * @module widgets
182
		 *
183
		 * @since 4.7.0
184
		 *
185
		 * @param string $content Widget content.
186
		 */
187
		echo apply_filters( 'jetpack_display_posts_widget_content', $content );
188
	}
189
190
	/**
191
	 * Display the widget administration form.
192
	 *
193
	 * @param array $instance Widget instance configuration.
194
	 *
195
	 * @return string|void
196
	 */
197
	public function form( $instance ) {
198
199
		/**
200
		 * Initialize widget configuration variables.
201
		 */
202
		$title              = ( isset( $instance['title'] ) ) ? $instance['title'] : __( 'Recent Posts', 'jetpack' );
203
		$url                = ( isset( $instance['url'] ) ) ? $instance['url'] : '';
204
		$number_of_posts    = ( isset( $instance['number_of_posts'] ) ) ? $instance['number_of_posts'] : 5;
205
		$open_in_new_window = ( isset( $instance['open_in_new_window'] ) ) ? $instance['open_in_new_window'] : false;
206
		$featured_image     = ( isset( $instance['featured_image'] ) ) ? $instance['featured_image'] : false;
207
		$show_excerpts      = ( isset( $instance['show_excerpts'] ) ) ? $instance['show_excerpts'] : false;
208
209
210
		/**
211
		 * Check if the widget instance has errors available.
212
		 *
213
		 * Only do so if a URL is set.
214
		 */
215
		$update_errors = array();
216
217
		if ( ! empty( $url ) ) {
218
			$data          = $this->get_blog_data( $url );
219
			$update_errors = $this->extract_errors_from_blog_data( $data );
220
		}
221
222
		?>
223
		<p>
224
			<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'jetpack' ); ?></label>
225
			<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 ); ?>" />
226
		</p>
227
228
		<p>
229
			<label for="<?php echo $this->get_field_id( 'url' ); ?>"><?php _e( 'Blog URL:', 'jetpack' ); ?></label>
230
			<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 ); ?>" />
231
			<i>
232
				<?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?>
233
			</i>
234
			<?php
235
			/**
236
			 * Show an error if the URL field was left empty.
237
			 *
238
			 * The error is shown only when the widget was already saved.
239
			 */
240 View Code Duplication
			if ( empty( $url ) && ! preg_match( '/__i__|%i%/', $this->id ) ) {
241
				?>
242
				<br />
243
				<i class="error-message"><?php echo __( 'You must specify a valid blog URL!', 'jetpack' ); ?></i>
244
				<?php
245
			}
246
			?>
247
		</p>
248
		<p>
249
			<label for="<?php echo $this->get_field_id( 'number_of_posts' ); ?>"><?php _e( 'Number of Posts to Display:', 'jetpack' ); ?></label>
250
			<select name="<?php echo $this->get_field_name( 'number_of_posts' ); ?>">
251
				<?php
252
				for ( $i = 1; $i <= 10; $i ++ ) {
253
					echo '<option value="' . $i . '" ' . selected( $number_of_posts, $i ) . '>' . $i . '</option>';
254
				}
255
				?>
256
			</select>
257
		</p>
258
		<p>
259
			<label for="<?php echo $this->get_field_id( 'open_in_new_window' ); ?>"><?php _e( 'Open links in new window/tab:', 'jetpack' ); ?></label>
260
			<input type="checkbox" name="<?php echo $this->get_field_name( 'open_in_new_window' ); ?>" <?php checked( $open_in_new_window, 1 ); ?> />
261
		</p>
262
		<p>
263
			<label for="<?php echo $this->get_field_id( 'featured_image' ); ?>"><?php _e( 'Show Featured Image:', 'jetpack' ); ?></label>
264
			<input type="checkbox" name="<?php echo $this->get_field_name( 'featured_image' ); ?>" <?php checked( $featured_image, 1 ); ?> />
265
		</p>
266
		<p>
267
			<label for="<?php echo $this->get_field_id( 'show_excerpts' ); ?>"><?php _e( 'Show Excerpts:', 'jetpack' ); ?></label>
268
			<input type="checkbox" name="<?php echo $this->get_field_name( 'show_excerpts' ); ?>" <?php checked( $show_excerpts, 1 ); ?> />
269
		</p>
270
271
		<?php
272
273
		/**
274
		 * Show error messages.
275
		 */
276
		if ( ! empty( $update_errors['message'] ) ) {
277
278
			/**
279
			 * Prepare the error messages.
280
			 */
281
282
			$where_message = '';
283
			switch ( $update_errors['where'] ) {
284
				case 'posts':
285
					$where_message .= __( 'An error occurred while downloading blog posts list', 'jetpack' );
286
					break;
287
288
				/**
289
				 * If something else, beside `posts` and `site_info` broke,
290
				 * don't handle it and default to blog `information`,
291
				 * as it is generic enough.
292
				 */
293
				case 'site_info':
294
				default:
295
					$where_message .= __( 'An error occurred while downloading blog information', 'jetpack' );
296
					break;
297
			}
298
299
			?>
300
			<p class="error-message">
301
				<?php echo esc_html( $where_message ); ?>:
302
				<br />
303
				<i>
304
					<?php echo esc_html( $update_errors['message'] ); ?>
305
					<?php
306
					/**
307
					 * If there is any debug - show it here.
308
					 */
309
					if ( ! empty( $update_errors['debug'] ) ) {
310
						?>
311
						<br />
312
						<br />
313
						<?php esc_html_e( 'Detailed information', 'jetpack' ); ?>:
314
						<br />
315
						<?php echo esc_html( $update_errors['debug'] ); ?>
316
						<?php
317
					}
318
					?>
319
				</i>
320
			</p>
321
322
			<?php
323
		}
324
	}
325
326
	public function update( $new_instance, $old_instance ) {
327
328
		$instance          = array();
329
		$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
330
		$instance['url']   = ( ! empty( $new_instance['url'] ) ) ? strip_tags( trim( $new_instance['url'] ) ) : '';
331
		$instance['url']   = preg_replace( "!^https?://!is", "", $instance['url'] );
332
		$instance['url']   = untrailingslashit( $instance['url'] );
333
334
335
		/**
336
		 * Check if the URL should be with or without the www prefix before saving.
337
		 */
338
		if ( ! empty( $instance['url'] ) ) {
339
			$blog_data = $this->fetch_blog_data( $instance['url'], array(), true );
340
341
			if ( is_wp_error( $blog_data['site_info']['error'] ) && 'www.' === substr( $instance['url'], 0, 4 ) ) {
342
				$blog_data = $this->fetch_blog_data( substr( $instance['url'], 4 ), array(), true );
343
344
				if ( ! is_wp_error( $blog_data['site_info']['error'] ) ) {
345
					$instance['url'] = substr( $instance['url'], 4 );
346
				}
347
			}
348
		}
349
350
		$instance['number_of_posts']    = ( ! empty( $new_instance['number_of_posts'] ) ) ? intval( $new_instance['number_of_posts'] ) : '';
351
		$instance['open_in_new_window'] = ( ! empty( $new_instance['open_in_new_window'] ) ) ? true : '';
352
		$instance['featured_image']     = ( ! empty( $new_instance['featured_image'] ) ) ? true : '';
353
		$instance['show_excerpts']      = ( ! empty( $new_instance['show_excerpts'] ) ) ? true : '';
354
355
		/**
356
		 * If there is no cache entry for the specified URL, run a forced update.
357
		 *
358
		 * @see get_blog_data Returns WP_Error if the cache is empty, which is what is needed here.
359
		 */
360
		$cached_data = $this->get_blog_data( $instance['url'] );
361
362
		if ( is_wp_error( $cached_data ) ) {
363
			$this->update_instance( $instance['url'] );
364
		}
365
366
		return $instance;
367
	}
368
369
370
	// DATA PROCESSING
371
372
	/**
373
	 * Expiring transients have a name length maximum of 45 characters,
374
	 * so this function returns an abbreviated MD5 hash to use instead of
375
	 * the full URI.
376
	 *
377
	 * @param string $site Site to get the hash for.
378
	 *
379
	 * @return string
380
	 */
381
	public function get_site_hash( $site ) {
382
		return substr( md5( $site ), 0, 21 );
383
	}
384
385
	/**
386
	 * Fetch a remote service endpoint and parse it.
387
	 *
388
	 * Timeout is set to 15 seconds right now, because sometimes the WordPress API
389
	 * takes more than 5 seconds to fully respond.
390
	 *
391
	 * Caching is used here so we can avoid re-downloading the same endpoint
392
	 * in a single request.
393
	 *
394
	 * @param string $endpoint Parametrized endpoint to call.
395
	 *
396
	 * @param int    $timeout  How much time to wait for the API to respond before failing.
397
	 *
398
	 * @return array|WP_Error
399
	 */
400
	public function fetch_service_endpoint( $endpoint, $timeout = 15 ) {
401
402
		/**
403
		 * Holds endpoint request cache.
404
		 */
405
		static $cache = array();
406
407
		if ( ! isset( $cache[ $endpoint ] ) ) {
408
			$raw_data           = $this->wp_wp_remote_get( $this->service_url . ltrim( $endpoint, '/' ), array( 'timeout' => $timeout ) );
409
			$cache[ $endpoint ] = $this->parse_service_response( $raw_data );
410
		}
411
412
		return $cache[ $endpoint ];
413
	}
414
415
	/**
416
	 * Parse data from service response.
417
	 * Do basic error handling for general service and data errors
418
	 *
419
	 * @param array $service_response Response from the service.
420
	 *
421
	 * @return array|WP_Error
422
	 */
423
	public function parse_service_response( $service_response ) {
424
		/**
425
		 * If there is an error, we add the error message to the parsed response
426
		 */
427
		if ( is_wp_error( $service_response ) ) {
428
			return new WP_Error(
429
				'general_error',
430
				__( 'An error occurred fetching the remote data.', 'jetpack' ),
431
				$service_response->get_error_messages()
0 ignored issues
show
Bug introduced by
The method get_error_messages cannot be called on $service_response (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
432
			);
433
		}
434
435
		/**
436
		 * Validate HTTP response code.
437
		 */
438
		if ( 200 !== wp_remote_retrieve_response_code( $service_response ) ) {
439
			return new WP_Error(
440
				'http_error',
441
				__( 'An error occurred fetching the remote data.', 'jetpack' ),
442
				wp_remote_retrieve_response_message( $service_response )
443
			);
444
		}
445
446
447
		/**
448
		 * Extract service response body from the request.
449
		 */
450
451
		$service_response_body = wp_remote_retrieve_body( $service_response );
452
453
454
		/**
455
		 * No body has been set in the response. This should be pretty bad.
456
		 */
457
		if ( ! $service_response_body ) {
458
			return new WP_Error(
459
				'no_body',
460
				__( 'Invalid remote response.', 'jetpack' ),
461
				'No body in response.'
462
			);
463
		}
464
465
		/**
466
		 * Parse the JSON response from the API. Convert to associative array.
467
		 */
468
		$parsed_data = json_decode( $service_response_body );
469
470
		/**
471
		 * If there is a problem with parsing the posts return an empty array.
472
		 */
473
		if ( is_null( $parsed_data ) ) {
474
			return new WP_Error(
475
				'no_body',
476
				__( 'Invalid remote response.', 'jetpack' ),
477
				'Invalid JSON from remote.'
478
			);
479
		}
480
481
		/**
482
		 * Check for errors in the parsed body.
483
		 */
484
		if ( isset( $parsed_data->error ) ) {
485
			return new WP_Error(
486
				'remote_error',
487
				__( 'It looks like the WordPress site URL is incorrectly configured. Please check it in your widget settings.', 'jetpack' ),
488
				$parsed_data->error
489
			);
490
		}
491
492
		/**
493
		 * No errors found, return parsed data.
494
		 */
495
		return $parsed_data;
496
	}
497
498
	/**
499
	 * Fetch site information from the WordPress public API
500
	 *
501
	 * @param string $site URL of the site to fetch the information for.
502
	 *
503
	 * @return array|WP_Error
504
	 */
505
	public function fetch_site_info( $site ) {
506
507
		$response = $this->fetch_service_endpoint( sprintf( '/sites/%s', urlencode( $site ) ) );
508
509
		return $response;
510
	}
511
512
	/**
513
	 * Parse external API response from the site info call and handle errors if they occur.
514
	 *
515
	 * @param array|WP_Error $service_response The raw response to be parsed.
516
	 *
517
	 * @return array|WP_Error
518
	 */
519 View Code Duplication
	public function parse_site_info_response( $service_response ) {
520
521
		/**
522
		 * If the service returned an error, we pass it on.
523
		 */
524
		if ( is_wp_error( $service_response ) ) {
525
			return $service_response;
526
		}
527
528
		/**
529
		 * Check if the service returned proper site information.
530
		 */
531
		if ( ! isset( $service_response->ID ) ) {
532
			return new WP_Error(
533
				'no_site_info',
534
				__( 'Invalid site information returned from remote.', 'jetpack' ),
535
				'No site ID present in the response.'
536
			);
537
		}
538
539
		return $service_response;
540
	}
541
542
	/**
543
	 * Fetch list of posts from the WordPress public API.
544
	 *
545
	 * @param int $site_id The site to fetch the posts for.
546
	 *
547
	 * @return array|WP_Error
548
	 */
549
	public function fetch_posts_for_site( $site_id ) {
550
551
		$response = $this->fetch_service_endpoint(
552
			sprintf(
553
				'/sites/%1$d/posts/%2$s',
554
				$site_id,
555
				/**
556
				 * Filters the parameters used to fetch for posts in the Display Posts Widget.
557
				 *
558
				 * @see    https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/
559
				 *
560
				 * @module widgets
561
				 *
562
				 * @since  3.6.0
563
				 *
564
				 * @param string $args Extra parameters to filter posts returned from the WordPress.com REST API.
565
				 */
566
				apply_filters( 'jetpack_display_posts_widget_posts_params', '' )
567
			)
568
		);
569
570
		return $response;
571
	}
572
573
	/**
574
	 * Parse external API response from the posts list request and handle errors if any occur.
575
	 *
576
	 * @param object|WP_Error $service_response The raw response to be parsed.
577
	 *
578
	 * @return array|WP_Error
579
	 */
580 View Code Duplication
	public function parse_posts_response( $service_response ) {
581
582
		/**
583
		 * If the service returned an error, we pass it on.
584
		 */
585
		if ( is_wp_error( $service_response ) ) {
586
			return $service_response;
587
		}
588
589
		/**
590
		 * Check if the service returned proper posts array.
591
		 */
592
		if ( ! isset( $service_response->posts ) || ! is_array( $service_response->posts ) ) {
593
			return new WP_Error(
594
				'no_posts',
595
				__( 'No posts data returned by remote.', 'jetpack' ),
596
				'No posts information set in the returned data.'
597
			);
598
		}
599
600
		/**
601
		 * Format the posts to preserve storage space.
602
		 */
603
604
		return $this->format_posts_for_storage( $service_response );
605
	}
606
607
	/**
608
	 * Format the posts for better storage. Drop all the data that is not used.
609
	 *
610
	 * @param object $parsed_data Array of posts returned by the APIs.
611
	 *
612
	 * @return array Formatted posts or an empty array if no posts were found.
613
	 */
614
	public function format_posts_for_storage( $parsed_data ) {
615
616
		$formatted_posts = array();
617
618
		/**
619
		 * Only go through the posts list if we have valid posts array.
620
		 */
621
		if ( isset( $parsed_data->posts ) && is_array( $parsed_data->posts ) ) {
622
623
			/**
624
			 * Loop through all the posts and format them appropriately.
625
			 */
626
			foreach ( $parsed_data->posts as $single_post ) {
627
628
				$prepared_post = array(
629
					'title'          => $single_post->title ? $single_post->title : '',
630
					'excerpt'        => $single_post->excerpt ? $single_post->excerpt : '',
631
					'featured_image' => $single_post->featured_image ? $single_post->featured_image : '',
632
					'url'            => $single_post->URL,
633
				);
634
635
				/**
636
				 * Append the formatted post to the results.
637
				 */
638
				$formatted_posts[] = $prepared_post;
639
			}
640
		}
641
642
		return $formatted_posts;
643
	}
644
645
	/**
646
	 * Fetch site information and posts list for a site.
647
	 *
648
	 * @param string $site           Site to fetch the data for.
649
	 * @param array  $original_data  Optional original data to updated.
650
	 *
651
	 * @param bool   $site_data_only Fetch only site information, skip posts list.
652
	 *
653
	 * @return array Updated or new data.
654
	 */
655
	public function fetch_blog_data( $site, $original_data = array(), $site_data_only = false ) {
656
657
		/**
658
		 * If no optional data is supplied, initialize a new structure
659
		 */
660
		if ( ! empty( $original_data ) ) {
661
			$widget_data = $original_data;
662
		}
663
		else {
664
			$widget_data = array(
665
				'site_info' => array(
666
					'last_check'  => null,
667
					'last_update' => null,
668
					'error'       => null,
669
					'data'        => array(),
670
				),
671
				'posts'     => array(
672
					'last_check'  => null,
673
					'last_update' => null,
674
					'error'       => null,
675
					'data'        => array(),
676
				)
677
			);
678
		}
679
680
		/**
681
		 * Update check time and fetch site information.
682
		 */
683
		$widget_data['site_info']['last_check'] = time();
684
685
		$site_info_raw_data    = $this->fetch_site_info( $site );
686
		$site_info_parsed_data = $this->parse_site_info_response( $site_info_raw_data );
687
688
689
		/**
690
		 * If there is an error with the fetched site info, save the error and update the checked time.
691
		 */
692 View Code Duplication
		if ( is_wp_error( $site_info_parsed_data ) ) {
693
			$widget_data['site_info']['error'] = $site_info_parsed_data;
694
695
			return $widget_data;
696
		}
697
		/**
698
		 * If data is fetched successfully, update the data and set the proper time.
699
		 *
700
		 * Data is only updated if we have valid results. This is done this way so we can show
701
		 * something if external service is down.
702
		 *
703
		 */
704
		else {
705
			$widget_data['site_info']['last_update'] = time();
706
			$widget_data['site_info']['data']        = $site_info_parsed_data;
707
			$widget_data['site_info']['error']       = null;
708
		}
709
710
711
		/**
712
		 * If only site data is needed, return it here, don't fetch posts data.
713
		 */
714
		if ( true === $site_data_only ) {
715
			return $widget_data;
716
		}
717
718
		/**
719
		 * Update check time and fetch posts list.
720
		 */
721
		$widget_data['posts']['last_check'] = time();
722
723
		$site_posts_raw_data    = $this->fetch_posts_for_site( $site_info_parsed_data->ID );
724
		$site_posts_parsed_data = $this->parse_posts_response( $site_posts_raw_data );
725
726
727
		/**
728
		 * If there is an error with the fetched posts, save the error and update the checked time.
729
		 */
730 View Code Duplication
		if ( is_wp_error( $site_posts_parsed_data ) ) {
731
			$widget_data['posts']['error'] = $site_posts_parsed_data;
732
733
			return $widget_data;
734
		}
735
		/**
736
		 * If data is fetched successfully, update the data and set the proper time.
737
		 *
738
		 * Data is only updated if we have valid results. This is done this way so we can show
739
		 * something if external service is down.
740
		 *
741
		 */
742
		else {
743
			$widget_data['posts']['last_update'] = time();
744
			$widget_data['posts']['data']        = $site_posts_parsed_data;
745
			$widget_data['posts']['error']       = null;
746
		}
747
748
		return $widget_data;
749
	}
750
751
	/**
752
	 * Scan and extract first error from blog data array.
753
	 *
754
	 * @param array|WP_Error $blog_data Blog data to scan for errors.
755
	 *
756
	 * @return string First error message found
757
	 */
758
	public function extract_errors_from_blog_data( $blog_data ) {
759
760
		$errors = array(
761
			'message' => '',
762
			'debug'   => '',
763
			'where'   => '',
764
		);
765
766
767
		/**
768
		 * When the cache result is an error. Usually when the cache is empty.
769
		 * This is not an error case for now.
770
		 */
771
		if ( is_wp_error( $blog_data ) ) {
772
			return $errors;
773
		}
774
775
		/**
776
		 * Loop through `site_info` and `posts` keys of $blog_data.
777
		 */
778
		foreach ( array( 'site_info', 'posts' ) as $info_key ) {
779
780
			/**
781
			 * Contains information on which stage the error ocurred.
782
			 */
783
			$errors['where'] = $info_key;
784
785
			/**
786
			 * If an error is set, we want to check it for usable messages.
787
			 */
788
			if ( isset( $blog_data[ $info_key ]['error'] ) && ! empty( $blog_data[ $info_key ]['error'] ) ) {
789
790
				/**
791
				 * Extract error message from the error, if possible.
792
				 */
793
				if ( is_wp_error( $blog_data[ $info_key ]['error'] ) ) {
794
					/**
795
					 * In the case of WP_Error we want to have the error message
796
					 * and the debug information available.
797
					 */
798
					$error_messages    = $blog_data[ $info_key ]['error']->get_error_messages();
799
					$errors['message'] = reset( $error_messages );
800
801
					$extra_data = $blog_data[ $info_key ]['error']->get_error_data();
802
					if ( is_array( $extra_data ) ) {
803
						$errors['debug'] = implode( '; ', $extra_data );
804
					}
805
					else {
806
						$errors['debug'] = $extra_data;
807
					}
808
809
					break;
810
				}
811
				elseif ( is_array( $blog_data[ $info_key ]['error'] ) ) {
812
					/**
813
					 * In this case we don't have debug information, because
814
					 * we have no way to know the format. The widget works with
815
					 * WP_Error objects only.
816
					 */
817
					$errors['message'] = reset( $blog_data[ $info_key ]['error'] );
818
					break;
819
				}
820
821
				/**
822
				 * We do nothing if no usable error is found.
823
				 */
824
			}
825
		}
826
827
		return $errors;
828
	}
829
830
	/**
831
	 * This is just to make method mocks in the unit tests easier.
832
	 *
833
	 * @param string $url  The URL to fetch
834
	 * @param array  $args Optional. Request arguments.
835
	 *
836
	 * @return array|WP_Error
837
	 *
838
	 * @codeCoverageIgnore
839
	 */
840
	public function wp_wp_remote_get( $url, $args = array() ) {
841
		return wp_remote_get( $url, $args );
842
	}
843
}
844