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