PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / widgets / wordpress-post-widget / class.jetpack-display-posts-widget-base.php
jetpack / modules / widgets / wordpress-post-widget Last commit date
class.jetpack-display-posts-widget-base.php 2 weeks ago class.jetpack-display-posts-widget.php 7 months ago style.css 7 years ago
class.jetpack-display-posts-widget-base.php
851 lines
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'] . ': ' . $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="' . 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 .= $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