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