class.jetpack-display-posts-widget-base.php 24 KB

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