summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/modules/widgets/wordpress-post-widget.php')
-rw-r--r--plugins/jetpack/modules/widgets/wordpress-post-widget.php1093
1 files changed, 999 insertions, 94 deletions
diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget.php b/plugins/jetpack/modules/widgets/wordpress-post-widget.php
index a3c724e1..198effc4 100644
--- a/plugins/jetpack/modules/widgets/wordpress-post-widget.php
+++ b/plugins/jetpack/modules/widgets/wordpress-post-widget.php
@@ -7,21 +7,141 @@
* Author URI: http://automattic.com
* License: GPL2
*/
+
+/**
+ * Disable direct access/execution to/of the widget code.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
add_action( 'widgets_init', 'jetpack_display_posts_widget' );
function jetpack_display_posts_widget() {
- register_widget( 'Jetpack_Display_Posts_Widget' );
+ register_widget( 'Jetpack_Display_Posts_Widget' );
+}
+
+
+/**
+ * Cron tasks
+ */
+
+add_filter( 'cron_schedules', 'jetpack_display_posts_widget_cron_intervals' );
+
+/**
+ * Adds 10 minute running interval to the cron schedules.
+ *
+ * @param array $current_schedules Currently defined schedules list.
+ *
+ * @return array
+ */
+function jetpack_display_posts_widget_cron_intervals( $current_schedules ) {
+
+ /**
+ * Only add the 10 minute interval if it wasn't already set.
+ */
+ if ( ! isset( $current_schedules['minutes_10'] ) ) {
+ $current_schedules['minutes_10'] = array(
+ 'interval' => 10 * MINUTE_IN_SECONDS,
+ 'display' => 'Every 10 minutes'
+ );
+ }
+
+ return $current_schedules;
}
+/**
+ * Execute the cron task
+ */
+add_action( 'jetpack_display_posts_widget_cron_update', 'jetpack_display_posts_update_cron_action' );
+function jetpack_display_posts_update_cron_action() {
+ $widget = new Jetpack_Display_Posts_Widget();
+ $widget->cron_task();
+}
+
+/**
+ * Handle activation procedures for the cron.
+ *
+ * `updating_jetpack_version` - Handle cron activation when Jetpack gets updated. It's here
+ * to cover the first cron activation after the update.
+ *
+ * `jetpack_activate_module_widgets` - Activate the cron when the Extra Sidebar widgets are activated.
+ *
+ * `activated_plugin` - Activate the cron when Jetpack gets activated.
+ *
+ */
+add_action( 'updating_jetpack_version', 'jetpack_display_posts_widget_conditionally_activate_cron' );
+add_action( 'jetpack_activate_module_widgets', 'Jetpack_Display_Posts_Widget::activate_cron' );
+add_action( 'activated_plugin', 'jetpack_conditionally_activate_cron_on_plugin_activation' );
+
+/**
+ * Executed when Jetpack gets activated. Tries to activate the cron if it is needed.
+ *
+ * @param string $plugin_file_name The plugin file that was activated.
+ */
+function jetpack_conditionally_activate_cron_on_plugin_activation( $plugin_file_name ) {
+ if ( plugin_basename( JETPACK__PLUGIN_FILE ) === $plugin_file_name ) {
+ jetpack_display_posts_widget_conditionally_activate_cron();
+ }
+}
+
+/**
+ * Activates the cron only when needed.
+ * @see Jetpack_Display_Posts_Widget::should_cron_be_running
+ */
+function jetpack_display_posts_widget_conditionally_activate_cron() {
+ $widget = new Jetpack_Display_Posts_Widget();
+ if ( $widget->should_cron_be_running() ) {
+ $widget->activate_cron();
+ }
+
+ unset( $widget );
+}
+
+/**
+ * End of cron activation handling.
+ */
+
+
+/**
+ * Handle deactivation procedures where they are needed.
+ *
+ * If Extra Sidebar Widgets module is deactivated, the cron is not needed.
+ *
+ * If Jetpack is deactivated, the cron is not needed.
+ */
+add_action( 'jetpack_deactivate_module_widgets', 'Jetpack_Display_Posts_Widget::deactivate_cron_static' );
+register_deactivation_hook( plugin_basename( JETPACK__PLUGIN_FILE ), 'Jetpack_Display_Posts_Widget::deactivate_cron_static' );
+
+/**
+ * End of Cron tasks
+ */
/*
* Display a list of recent posts from a WordPress.com or Jetpack-enabled blog.
*/
+
class Jetpack_Display_Posts_Widget extends WP_Widget {
+ /**
+ * @var string Remote service API URL prefix.
+ */
+ public $service_url = 'https://public-api.wordpress.com/rest/v1.1/';
+
+ /**
+ * @var string Widget options key prefix.
+ */
+ public $widget_options_key_prefix = 'display_posts_site_data_';
+
+ /**
+ * @var string The name of the cron that will update widget data.
+ */
+ public static $cron_name = 'jetpack_display_posts_widget_cron_update';
+
+
public function __construct() {
parent::__construct(
- // internal id
+ // internal id
'jetpack_display_posts_widget',
- // wp-admin title
+ /** This filter is documented in modules/widgets/facebook-likebox.php */
apply_filters( 'jetpack_widget_name', __( 'Display WordPress Posts', 'jetpack' ) ),
array(
'description' => __( 'Displays a list of recent posts from another WordPress.com or Jetpack-enabled blog.', 'jetpack' ),
@@ -33,98 +153,648 @@ class Jetpack_Display_Posts_Widget extends WP_Widget {
* Expiring transients have a name length maximum of 45 characters,
* so this function returns an abbreviated MD5 hash to use instead of
* the full URI.
+ *
+ * @param string $site Site to get the hash for.
+ *
+ * @return string
*/
public function get_site_hash( $site ) {
return substr( md5( $site ), 0, 21 );
}
- public function get_site_info( $site ) {
+ /**
+ * Fetch a remote service endpoint and parse it.
+ *
+ * Timeout is set to 15 seconds right now, because sometimes the WordPress API
+ * takes more than 5 seconds to fully respond.
+ *
+ * Caching is used here so we can avoid re-downloading the same endpoint
+ * in a single request.
+ *
+ * @param string $endpoint Parametrized endpoint to call.
+ *
+ * @param int $timeout How much time to wait for the API to respond before failing.
+ *
+ * @return array|WP_Error
+ */
+ public function fetch_service_endpoint( $endpoint, $timeout = 15 ) {
+
+ /**
+ * Holds endpoint request cache.
+ */
+ static $cache = array();
+
+ if ( ! isset( $cache[ $endpoint ] ) ) {
+ $raw_data = $this->wp_wp_remote_get( $this->service_url . ltrim( $endpoint, '/' ), array( 'timeout' => $timeout ) );
+ $cache[ $endpoint ] = $this->parse_service_response( $raw_data );
+ }
+
+ return $cache[ $endpoint ];
+ }
+
+ /**
+ * Parse data from service response.
+ * Do basic error handling for general service and data errors
+ *
+ * @param array $service_response Response from the service.
+ *
+ * @return array|WP_Error
+ */
+ public function parse_service_response( $service_response ) {
+ /**
+ * If there is an error, we add the error message to the parsed response
+ */
+ if ( is_wp_error( $service_response ) ) {
+ return new WP_Error(
+ 'general_error',
+ __( 'An error occurred fetching the remote data.', 'jetpack' ),
+ $service_response->get_error_messages()
+ );
+ }
+
+ /**
+ * Validate HTTP response code.
+ */
+ if ( 200 !== wp_remote_retrieve_response_code( $service_response ) ) {
+ return new WP_Error(
+ 'http_error',
+ __( 'An error occurred fetching the remote data.', 'jetpack' ),
+ wp_remote_retrieve_response_message( $service_response )
+ );
+ }
+
+
+ /**
+ * Extract service response body from the request.
+ */
+
+ $service_response_body = wp_remote_retrieve_body( $service_response );
+
+
+ /**
+ * No body has been set in the response. This should be pretty bad.
+ */
+ if ( ! $service_response_body ) {
+ return new WP_Error(
+ 'no_body',
+ __( 'Invalid remote response.', 'jetpack' ),
+ 'No body in response.'
+ );
+ }
+
+ /**
+ * Parse the JSON response from the API. Convert to associative array.
+ */
+ $parsed_data = json_decode( $service_response_body );
+
+ /**
+ * If there is a problem with parsing the posts return an empty array.
+ */
+ if ( is_null( $parsed_data ) ) {
+ return new WP_Error(
+ 'no_body',
+ __( 'Invalid remote response.', 'jetpack' ),
+ 'Invalid JSON from remote.'
+ );
+ }
+
+ /**
+ * Check for errors in the parsed body.
+ */
+ if ( isset( $parsed_data->error ) ) {
+ return new WP_Error(
+ 'remote_error',
+ __( 'We cannot display information for this blog.', 'jetpack' ),
+ $parsed_data->error
+ );
+ }
+
+
+ /**
+ * No errors found, return parsed data.
+ */
+ return $parsed_data;
+ }
+
+ /**
+ * Fetch site information from the WordPress public API
+ *
+ * @param string $site URL of the site to fetch the information for.
+ *
+ * @return array|WP_Error
+ */
+ public function fetch_site_info( $site ) {
+
+ $response = $this->fetch_service_endpoint( sprintf( '/sites/%s', urlencode( $site ) ) );
+
+ return $response;
+ }
+
+ /**
+ * Parse external API response from the site info call and handle errors if they occur.
+ *
+ * @param array|WP_Error $service_response The raw response to be parsed.
+ *
+ * @return array|WP_Error
+ */
+ public function parse_site_info_response( $service_response ) {
+
+ /**
+ * If the service returned an error, we pass it on.
+ */
+ if ( is_wp_error( $service_response ) ) {
+ return $service_response;
+ }
+
+ /**
+ * Check if the service returned proper site information.
+ */
+ if ( ! isset( $service_response->ID ) ) {
+ return new WP_Error(
+ 'no_site_info',
+ __( 'Invalid site information returned from remote.', 'jetpack' ),
+ 'No site ID present in the response.'
+ );
+ }
+
+ return $service_response;
+ }
+
+ /**
+ * Fetch list of posts from the WordPress public API.
+ *
+ * @param int $site_id The site to fetch the posts for.
+ *
+ * @return array|WP_Error
+ */
+ public function fetch_posts_for_site( $site_id ) {
+
+ $response = $this->fetch_service_endpoint(
+ sprintf(
+ '/sites/%1$d/posts/%2$s',
+ $site_id,
+ /**
+ * Filters the parameters used to fetch for posts in the Display Posts Widget.
+ *
+ * @see https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param string $args Extra parameters to filter posts returned from the WordPress.com REST API.
+ */
+ apply_filters( 'jetpack_display_posts_widget_posts_params', '' )
+ )
+ );
+
+ return $response;
+ }
+
+ /**
+ * Parse external API response from the posts list request and handle errors if any occur.
+ *
+ * @param object|WP_Error $service_response The raw response to be parsed.
+ *
+ * @return array|WP_Error
+ */
+ public function parse_posts_response( $service_response ) {
+
+ /**
+ * If the service returned an error, we pass it on.
+ */
+ if ( is_wp_error( $service_response ) ) {
+ return $service_response;
+ }
+
+ /**
+ * Check if the service returned proper posts array.
+ */
+ if ( ! isset( $service_response->posts ) || ! is_array( $service_response->posts ) ) {
+ return new WP_Error(
+ 'no_posts',
+ __( 'No posts data returned by remote.', 'jetpack' ),
+ 'No posts information set in the returned data.'
+ );
+ }
+
+ /**
+ * Format the posts to preserve storage space.
+ */
+
+ return $this->format_posts_for_storage( $service_response );
+ }
+
+ /**
+ * Format the posts for better storage. Drop all the data that is not used.
+ *
+ * @param object $parsed_data Array of posts returned by the APIs.
+ *
+ * @return array Formatted posts or an empty array if no posts were found.
+ */
+ public function format_posts_for_storage( $parsed_data ) {
+
+ $formatted_posts = array();
+
+ /**
+ * Only go through the posts list if we have valid posts array.
+ */
+ if ( isset( $parsed_data->posts ) && is_array( $parsed_data->posts ) ) {
+
+ /**
+ * Loop through all the posts and format them appropriately.
+ */
+ foreach ( $parsed_data->posts as $single_post ) {
+
+ $prepared_post = array(
+ 'title' => $single_post->title ? $single_post->title : '',
+ 'excerpt' => $single_post->excerpt ? $single_post->excerpt : '',
+ 'featured_image' => $single_post->featured_image ? $single_post->featured_image : '',
+ 'url' => $single_post->URL,
+ );
+
+ /**
+ * Append the formatted post to the results.
+ */
+ $formatted_posts[] = $prepared_post;
+ }
+ }
+
+ return $formatted_posts;
+ }
+
+ /**
+ * Fetch site information and posts list for a site.
+ *
+ * @param string $site Site to fetch the data for.
+ * @param array $original_data Optional original data to updated.
+ *
+ * @param bool $site_data_only Fetch only site information, skip posts list.
+ *
+ * @return array Updated or new data.
+ */
+ public function fetch_blog_data( $site, $original_data = array(), $site_data_only = false ) {
+
+ /**
+ * If no optional data is supplied, initialize a new structure
+ */
+ if ( ! empty( $original_data ) ) {
+ $widget_data = $original_data;
+ }
+ else {
+ $widget_data = array(
+ 'site_info' => array(
+ 'last_check' => null,
+ 'last_update' => null,
+ 'error' => null,
+ 'data' => array(),
+ ),
+ 'posts' => array(
+ 'last_check' => null,
+ 'last_update' => null,
+ 'error' => null,
+ 'data' => array(),
+ )
+ );
+ }
+
+ /**
+ * Update check time and fetch site information.
+ */
+ $widget_data['site_info']['last_check'] = time();
+
+ $site_info_raw_data = $this->fetch_site_info( $site );
+ $site_info_parsed_data = $this->parse_site_info_response( $site_info_raw_data );
+
+
+ /**
+ * If there is an error with the fetched site info, save the error and update the checked time.
+ */
+ if ( is_wp_error( $site_info_parsed_data ) ) {
+ $widget_data['site_info']['error'] = $site_info_parsed_data;
+
+ return $widget_data;
+ }
+ /**
+ * If data is fetched successfully, update the data and set the proper time.
+ *
+ * Data is only updated if we have valid results. This is done this way so we can show
+ * something if external service is down.
+ *
+ */
+ else {
+ $widget_data['site_info']['last_update'] = time();
+ $widget_data['site_info']['data'] = $site_info_parsed_data;
+ $widget_data['site_info']['error'] = null;
+ }
+
+
+ /**
+ * If only site data is needed, return it here, don't fetch posts data.
+ */
+ if ( true === $site_data_only ) {
+ return $widget_data;
+ }
+
+ /**
+ * Update check time and fetch posts list.
+ */
+ $widget_data['posts']['last_check'] = time();
+
+ $site_posts_raw_data = $this->fetch_posts_for_site( $site_info_parsed_data->ID );
+ $site_posts_parsed_data = $this->parse_posts_response( $site_posts_raw_data );
+
+
+ /**
+ * If there is an error with the fetched posts, save the error and update the checked time.
+ */
+ if ( is_wp_error( $site_posts_parsed_data ) ) {
+ $widget_data['posts']['error'] = $site_posts_parsed_data;
+
+ return $widget_data;
+ }
+ /**
+ * If data is fetched successfully, update the data and set the proper time.
+ *
+ * Data is only updated if we have valid results. This is done this way so we can show
+ * something if external service is down.
+ *
+ */
+ else {
+ $widget_data['posts']['last_update'] = time();
+ $widget_data['posts']['data'] = $site_posts_parsed_data;
+ $widget_data['posts']['error'] = null;
+ }
+
+ return $widget_data;
+ }
+
+ /**
+ * Gets blog data from the cache.
+ *
+ * @param string $site
+ *
+ * @return array|WP_Error
+ */
+ public function get_blog_data( $site ) {
+ // load from cache, if nothing return an error
$site_hash = $this->get_site_hash( $site );
- $data_from_cache = get_transient( 'display_posts_site_info_' . $site_hash );
- if ( false === $data_from_cache ) {
- $response = wp_remote_get( sprintf( 'https://public-api.wordpress.com/rest/v1/sites/%s', urlencode( $site ) ) );
- set_transient( 'display_posts_site_info_' . $site_hash, $response, 10 * MINUTE_IN_SECONDS );
- } else {
- $response = $data_from_cache;
+
+ $cached_data = $this->wp_get_option( $this->widget_options_key_prefix . $site_hash );
+
+ /**
+ * If the cache is empty, return an empty_cache error.
+ */
+ if ( false === $cached_data ) {
+ return new WP_Error(
+ 'empty_cache',
+ __( 'Information about this blog is currently being retrieved.', 'jetpack' )
+ );
+ }
+
+ return $cached_data;
+
+ }
+
+ /**
+ * Activates widget update cron task.
+ */
+ public static function activate_cron() {
+ if ( ! wp_next_scheduled( self::$cron_name ) ) {
+ wp_schedule_event( time(), 'minutes_10', self::$cron_name );
}
+ }
+
+ /**
+ * Deactivates widget update cron task.
+ *
+ * This is a wrapper over the static method as it provides some syntactic sugar.
+ */
+ public function deactivate_cron() {
+ self::deactivate_cron_static();
+ }
+
+ /**
+ * Deactivates widget update cron task.
+ */
+ public static function deactivate_cron_static() {
+ $next_scheduled_time = wp_next_scheduled( self::$cron_name );
+ wp_unschedule_event( $next_scheduled_time, self::$cron_name );
+ }
- if ( is_wp_error( $response ) ) {
+ /**
+ * Checks if the update cron should be running and returns appropriate result.
+ *
+ * @return bool If the cron should be running or not.
+ */
+ public function should_cron_be_running() {
+ /**
+ * The cron doesn't need to run empty loops.
+ */
+ $widget_instances = $this->get_instances_sites();
+
+ if ( empty( $widget_instances ) || ! is_array( $widget_instances ) ) {
+ return false;
+ }
+
+ /**
+ * If Jetpack is not active or in development mode, we don't want to update widget data.
+ */
+ if ( ! Jetpack::is_active() && ! Jetpack::is_development_mode() ) {
return false;
}
- $site_info = json_decode( $response ['body'] );
- if ( ! isset( $site_info->ID ) ) {
+ /**
+ * If Extra Sidebar Widgets module is not active, we don't need to update widget data.
+ */
+ if ( ! Jetpack::is_module_active( 'widgets' ) ) {
+ return false;
+ }
+
+
+ /**
+ * If none of the above checks failed, then we definitely want to update widget data.
+ */
+ return true;
+ }
+
+ /**
+ * Main cron code. Updates all instances of the widget.
+ *
+ * @return bool
+ */
+ public function cron_task() {
+
+ /**
+ * If the cron should not be running, disable it.
+ */
+ if ( false === $this->should_cron_be_running() ) {
+ return true;
+ }
+
+ $instances_to_update = $this->get_instances_sites();
+
+ /**
+ * If no instances are found to be updated - stop.
+ */
+ if ( empty( $instances_to_update ) || ! is_array( $instances_to_update ) ) {
+ return true;
+ }
+
+ foreach ( $instances_to_update as $site_url ) {
+ $this->update_instance( $site_url );
+ }
+
+ return true;
+ }
+
+ /**
+ * Get a list of unique sites from all instances of the widget.
+ *
+ * @return array|bool
+ */
+ public function get_instances_sites() {
+
+ $widget_settings = $this->wp_get_option( 'widget_jetpack_display_posts_widget' );
+
+ /**
+ * If the widget still hasn't been added anywhere, the config will not be present.
+ *
+ * In such case we don't want to continue execution.
+ */
+ if ( false === $widget_settings || ! is_array( $widget_settings ) ) {
return false;
}
- return $site_info;
+ $urls = array();
+
+ foreach ( $widget_settings as $widget_instance_data ) {
+ if ( isset( $widget_instance_data['url'] ) && ! empty( $widget_instance_data['url'] ) ) {
+ $urls[] = $widget_instance_data['url'];
+ }
+ }
+
+ /**
+ * Make sure only unique URLs are returned.
+ */
+ $urls = array_unique( $urls );
+
+ return $urls;
+
}
- /*
- * Set up the widget display on the front end
+ /**
+ * Update a widget instance.
+ *
+ * @param string $site The site to fetch the latest data for.
+ */
+ public function update_instance( $site ) {
+
+ /**
+ * Fetch current information for a site.
+ */
+ $site_hash = $this->get_site_hash( $site );
+
+ $option_key = $this->widget_options_key_prefix . $site_hash;
+
+ $instance_data = $this->wp_get_option( $option_key );
+
+ /**
+ * Fetch blog data and save it in $instance_data.
+ */
+ $new_data = $this->fetch_blog_data( $site, $instance_data );
+
+ /**
+ * If the option doesn't exist yet - create a new option
+ */
+ if ( false === $instance_data ) {
+ $this->wp_add_option( $option_key, $new_data );
+ }
+ else {
+ $this->wp_update_option( $option_key, $new_data );
+ }
+ }
+
+ /**
+ * Set up the widget display on the front end.
+ *
+ * @param array $args
+ * @param array $instance
*/
public function widget( $args, $instance ) {
+
+ /** This filter is documented in core/src/wp-includes/default-widgets.php */
$title = apply_filters( 'widget_title', $instance['title'] );
wp_enqueue_style( 'jetpack_display_posts_widget', plugins_url( 'wordpress-post-widget/style.css', __FILE__ ) );
- $site_info = $this->get_site_info( $instance['url'] );
-
echo $args['before_widget'];
- if ( false === $site_info ) {
- echo '<p>' . __( 'We cannot load blog data at this time.', 'jetpack' ) . '</p>';
+ $data = $this->get_blog_data( $instance['url'] );
+
+ // check for errors
+ if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) {
+ echo '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>';
echo $args['after_widget'];
+
return;
}
+ $site_info = $data['site_info']['data'];
+
if ( ! empty( $title ) ) {
echo $args['before_title'] . esc_html( $title . ': ' . $site_info->name ) . $args['after_title'];
- } else {
- echo $args['before_title'] . esc_html( $site_info->name ) . $args['after_title'];
}
-
- $site_hash = $this->get_site_hash( $instance['url'] );
- $data_from_cache = get_transient( 'display_posts_post_info_' . $site_hash );
- if ( false === $data_from_cache ) {
- $response = wp_remote_get( sprintf( 'https://public-api.wordpress.com/rest/v1/sites/%d/posts/', $site_info->ID ) );
- set_transient( 'display_posts_post_info_' . $site_hash, $response, 10 * MINUTE_IN_SECONDS );
- } else {
- $response = $data_from_cache;
- }
-
- if ( is_wp_error( $response ) ) {
- echo '<p>' . __( 'We cannot load blog data at this time.', 'jetpack' ) . '</p>';
- echo $args['after_widget'];
- return;
+ else {
+ echo $args['before_title'] . esc_html( $site_info->name ) . $args['after_title'];
}
- $posts_info = json_decode( $response['body'] );
-
echo '<div class="jetpack-display-remote-posts">';
- if ( isset( $posts_info->error ) && 'jetpack_error' == $posts_info->error ) {
- echo '<p>' . __( 'We cannot display posts for this blog.', 'jetpack' ) . '</p>';
+ if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) {
+ echo '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>';
echo '</div><!-- .jetpack-display-remote-posts -->';
echo $args['after_widget'];
+
return;
}
- $number_of_posts = min( $instance['number_of_posts'], count( $posts_info->posts ) );
+ $posts_list = $data['posts']['data'];
- for ( $i = 0; $i < $number_of_posts; $i++ ) {
- $single_post = $posts_info->posts[$i];
- $post_title = ( $single_post->title ) ? $single_post->title : '( No Title )';
+ /**
+ * Show only as much posts as we need. If we have less than configured amount,
+ * we must show only that much posts.
+ */
+ $number_of_posts = min( $instance['number_of_posts'], count( $posts_list ) );
- echo '<h4><a href="' . esc_url( $single_post->URL ) . '">' . esc_html( $post_title ) . '</a></h4>' . "\n";
- if ( ( $instance['featured_image'] == true ) && ( ! empty ( $single_post->featured_image) ) ) {
- $featured_image = ( $single_post->featured_image ) ? $single_post->featured_image : '';
- echo '<a title="' . esc_attr( $post_title ) . '" href="' . esc_url( $single_post->URL ) . '"><img src="' . $featured_image . '" alt="' . esc_attr( $post_title ) . '"/></a>';
+ for ( $i = 0; $i < $number_of_posts; $i ++ ) {
+ $single_post = $posts_list[ $i ];
+ $post_title = ( $single_post['title'] ) ? $single_post['title'] : '( No Title )';
+
+ $target = '';
+ if ( isset( $instance['open_in_new_window'] ) && $instance['open_in_new_window'] == true ) {
+ $target = ' target="_blank"';
+ }
+ echo '<h4><a href="' . esc_url( $single_post['url'] ) . '"' . $target . '>' . esc_html( $post_title ) . '</a></h4>' . "\n";
+ if ( ( $instance['featured_image'] == true ) && ( ! empty ( $single_post['featured_image'] ) ) ) {
+ $featured_image = $single_post['featured_image'];
+ /**
+ * Allows setting up custom Photon parameters to manipulate the image output in the Display Posts widget.
+ *
+ * @see https://developer.wordpress.com/docs/photon/
+ *
+ * @module widgets
+ *
+ * @since 3.6.0
+ *
+ * @param array $args Array of Photon Parameters.
+ */
+ $image_params = apply_filters( 'jetpack_display_posts_widget_image_params', array() );
+ echo '<a title="' . esc_attr( $post_title ) . '" href="' . esc_url( $single_post['url'] ) . '"><img src="' . jetpack_photon_url( $featured_image, $image_params ) . '" alt="' . esc_attr( $post_title ) . '"/></a>';
}
if ( $instance['show_excerpts'] == true ) {
- $post_excerpt = ( $single_post->excerpt ) ? $single_post->excerpt : '';
- echo $post_excerpt;
+ echo $single_post['excerpt'];
}
}
@@ -132,35 +802,115 @@ class Jetpack_Display_Posts_Widget extends WP_Widget {
echo $args['after_widget'];
}
- public function form( $instance ) {
- if ( isset( $instance[ 'title' ] ) ) {
- $title = $instance[ 'title' ];
- } else {
- $title = __( 'Recent Posts', 'jetpack' );
- }
+ /**
+ * Scan and extract first error from blog data array.
+ *
+ * @param array|WP_Error $blog_data Blog data to scan for errors.
+ *
+ * @return string First error message found
+ */
+ public function extract_errors_from_blog_data( $blog_data ) {
+
+ $errors = array(
+ 'message' => '',
+ 'debug' => '',
+ 'where' => '',
+ );
- if ( isset( $instance[ 'url' ] ) ) {
- $url = $instance[ 'url' ];
- } else {
- $url = '';
- }
- if ( isset( $instance[ 'number_of_posts' ] ) ) {
- $number_of_posts = $instance[ 'number_of_posts' ];
- } else {
- $number_of_posts = 5;
+ /**
+ * When the cache result is an error. Usually when the cache is empty.
+ * This is not an error case for now.
+ */
+ if ( is_wp_error( $blog_data ) ) {
+ return $errors;
}
- if ( isset( $instance[ 'featured_image'] ) ) {
- $featured_image = $instance[ 'featured_image'];
- } else {
- $featured_image = false;
+ /**
+ * Loop through `site_info` and `posts` keys of $blog_data.
+ */
+ foreach ( array( 'site_info', 'posts' ) as $info_key ) {
+
+ /**
+ * Contains information on which stage the error ocurred.
+ */
+ $errors['where'] = $info_key;
+
+ /**
+ * If an error is set, we want to check it for usable messages.
+ */
+ if ( isset( $blog_data[ $info_key ]['error'] ) && ! empty( $blog_data[ $info_key ]['error'] ) ) {
+
+ /**
+ * Extract error message from the error, if possible.
+ */
+ if ( is_wp_error( $blog_data[ $info_key ]['error'] ) ) {
+ /**
+ * In the case of WP_Error we want to have the error message
+ * and the debug information available.
+ */
+ $error_messages = $blog_data[ $info_key ]['error']->get_error_messages();
+ $errors['message'] = reset( $error_messages );
+
+ $extra_data = $blog_data[ $info_key ]['error']->get_error_data();
+ if ( is_array( $extra_data ) ) {
+ $errors['debug'] = implode( '; ', $extra_data );
+ }
+ else {
+ $errors['debug'] = $extra_data;
+ }
+
+ break;
+ }
+ elseif ( is_array( $blog_data[ $info_key ]['error'] ) ) {
+ /**
+ * In this case we don't have debug information, because
+ * we have no way to know the format. The widget works with
+ * WP_Error objects only.
+ */
+ $errors['message'] = reset( $blog_data[ $info_key ]['error'] );
+ break;
+ }
+
+ /**
+ * We do nothing if no usable error is found.
+ */
+ }
}
- if ( isset( $instance[ 'show_excerpts'] ) ) {
- $show_excerpts = $instance[ 'show_excerpts'];
- } else {
- $show_excerpts = false;
+ return $errors;
+ }
+
+ /**
+ * Display the widget administration form.
+ *
+ * @param array $instance Widget instance configuration.
+ *
+ * @return string|void
+ */
+ public function form( $instance ) {
+
+ /**
+ * Initialize widget configuration variables.
+ */
+ $title = ( isset( $instance['title'] ) ) ? $instance['title'] : __( 'Recent Posts', 'jetpack' );
+ $url = ( isset( $instance['url'] ) ) ? $instance['url'] : '';
+ $number_of_posts = ( isset( $instance['number_of_posts'] ) ) ? $instance['number_of_posts'] : 5;
+ $open_in_new_window = ( isset( $instance['open_in_new_window'] ) ) ? $instance['open_in_new_window'] : false;
+ $featured_image = ( isset( $instance['featured_image'] ) ) ? $instance['featured_image'] : false;
+ $show_excerpts = ( isset( $instance['show_excerpts'] ) ) ? $instance['show_excerpts'] : false;
+
+
+ /**
+ * Check if the widget instance has errors available.
+ *
+ * Only do so if a URL is set.
+ */
+ $update_errors = array();
+
+ if ( ! empty( $url ) ) {
+ $data = $this->get_blog_data( $url );
+ $update_errors = $this->extract_errors_from_blog_data( $data );
}
?>
@@ -172,21 +922,33 @@ class Jetpack_Display_Posts_Widget extends WP_Widget {
<p>
<label for="<?php echo $this->get_field_id( 'url' ); ?>"><?php _e( 'Blog URL:', 'jetpack' ); ?></label>
<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 ); ?>" />
- <p>
- <?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?>
- </p>
+ <i>
+ <?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?>
+ </i>
+ <?php
+ if ( empty( $url ) ) {
+ ?>
+ <br />
+ <i class="error-message"><?php echo __( 'You must specify a valid blog URL!', 'jetpack' ); ?></i>
+ <?php
+ }
+ ?>
</p>
<p>
<label for="<?php echo $this->get_field_id( 'number_of_posts' ); ?>"><?php _e( 'Number of Posts to Display:', 'jetpack' ); ?></label>
<select name="<?php echo $this->get_field_name( 'number_of_posts' ); ?>">
<?php
- for ($i = 1; $i <= 10; $i++) {
- echo '<option value="' . $i . '" '.selected( $number_of_posts, $i ).'>' . $i . '</option>';
- }
+ for ( $i = 1; $i <= 10; $i ++ ) {
+ echo '<option value="' . $i . '" ' . selected( $number_of_posts, $i ) . '>' . $i . '</option>';
+ }
?>
</select>
</p>
<p>
+ <label for="<?php echo $this->get_field_id( 'open_in_new_window' ); ?>"><?php _e( 'Open links in new window/tab:', 'jetpack' ); ?></label>
+ <input type="checkbox" name="<?php echo $this->get_field_name( 'open_in_new_window' ); ?>" <?php checked( $open_in_new_window, 1 ); ?> />
+ </p>
+ <p>
<label for="<?php echo $this->get_field_id( 'featured_image' ); ?>"><?php _e( 'Show Featured Image:', 'jetpack' ); ?></label>
<input type="checkbox" name="<?php echo $this->get_field_name( 'featured_image' ); ?>" <?php checked( $featured_image, 1 ); ?> />
</p>
@@ -196,27 +958,170 @@ class Jetpack_Display_Posts_Widget extends WP_Widget {
</p>
<?php
+
+ /**
+ * Show error messages.
+ */
+ if ( ! empty( $update_errors['message'] ) ) {
+
+ /**
+ * Prepare the error messages.
+ */
+
+ $what_broke_down = '';
+ switch ( $update_errors['where'] ) {
+ case 'posts':
+ $what_broke_down .= __( 'posts list', 'jetpack' );
+ break;
+
+ /**
+ * If something else, beside `posts` and `site_info` broke,
+ * don't handle it and default to blog `information`,
+ * as it is generic enough.
+ */
+ case 'site_info':
+ default:
+ $what_broke_down .= __( 'information', 'jetpack' );
+ break;
+ }
+
+ $where_message = sprintf(
+ __( 'An error occurred while downloading blog %s', 'jetpack' ),
+ $what_broke_down
+ );
+
+
+ ?>
+ <p class="error-message">
+ <?php echo $where_message; ?>:
+ <br />
+ <i>
+ <?php echo esc_html( $update_errors['message'] ); ?>
+ <?php
+ /**
+ * If there is any debug - show it here.
+ */
+ if ( ! empty( $update_errors['debug'] ) ) {
+ ?>
+ <br />
+ <br />
+ <?php echo __( 'Detailed information', 'jetpack' ); ?>:
+ <br />
+ <?php echo esc_html( $update_errors['debug'] ); ?>
+ <?php
+ }
+ ?>
+ </i>
+ </p>
+
+ <?php
+ }
}
public function update( $new_instance, $old_instance ) {
- $instance = array();
+
+ $instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
- $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? strip_tags( $new_instance['url'] ) : '';
- $instance['url'] = str_replace( "http://", "", $instance['url'] );
- $instance['url'] = untrailingslashit( $instance['url'] );
-
- // Normalize www.
- $site_info = $this->get_site_info( $instance['url'] );
- if ( ! $site_info && 'www.' === substr( $instance['url'], 0, 4 ) ) {
- $site_info = $this->get_site_info( substr( $instance['url'], 4 ) );
- if ( $site_info ) {
- $instance['url'] = substr( $instance['url'], 4 );
+ $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? strip_tags( $new_instance['url'] ) : '';
+ $instance['url'] = preg_replace( "!^https?://!is", "", $instance['url'] );
+ $instance['url'] = untrailingslashit( $instance['url'] );
+
+
+ /**
+ * Check if the URL should be with or without the www prefix before saving.
+ */
+ if ( ! empty( $instance['url'] ) ) {
+ $blog_data = $this->fetch_blog_data( $instance['url'], array(), true );
+
+ if ( is_wp_error( $blog_data['site_info']['error'] ) && 'www.' === substr( $instance['url'], 0, 4 ) ) {
+ $blog_data = $this->fetch_blog_data( substr( $instance['url'], 4 ), array(), true );
+
+ if ( ! is_wp_error( $blog_data['site_info']['error'] ) ) {
+ $instance['url'] = substr( $instance['url'], 4 );
+ }
}
}
- $instance['number_of_posts'] = ( ! empty( $new_instance['number_of_posts'] ) ) ? intval( $new_instance['number_of_posts'] ) : '';
- $instance['featured_image'] = ( ! empty( $new_instance['featured_image'] ) ) ? true : '';
- $instance['show_excerpts'] = ( ! empty( $new_instance['show_excerpts'] ) ) ? true : '';
+ $instance['number_of_posts'] = ( ! empty( $new_instance['number_of_posts'] ) ) ? intval( $new_instance['number_of_posts'] ) : '';
+ $instance['open_in_new_window'] = ( ! empty( $new_instance['open_in_new_window'] ) ) ? true : '';
+ $instance['featured_image'] = ( ! empty( $new_instance['featured_image'] ) ) ? true : '';
+ $instance['show_excerpts'] = ( ! empty( $new_instance['show_excerpts'] ) ) ? true : '';
+
+ /**
+ * Forcefully activate the update cron when saving widget instance.
+ *
+ * So we can be sure that it will be running later.
+ */
+ $this->activate_cron();
+
+
+ /**
+ * If there is no cache entry for the specified URL, run a forced update.
+ *
+ * @see get_blog_data Returns WP_Error if the cache is empty, which is what is needed here.
+ */
+ $cached_data = $this->get_blog_data( $instance['url'] );
+
+ if ( is_wp_error( $cached_data ) ) {
+ $this->update_instance( $instance['url'] );
+ }
+
return $instance;
}
-}
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $param Option key to get
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_get_option( $param ) {
+ return get_option( $param );
+ }
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $option_name Option name to be added
+ * @param mixed $option_value Option value
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_add_option( $option_name, $option_value ) {
+ return add_option( $option_name, $option_value );
+ }
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $option_name Option name to be updated
+ * @param mixed $option_value Option value
+ *
+ * @return mixed
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_update_option( $option_name, $option_value ) {
+ return update_option( $option_name, $option_value );
+ }
+
+
+ /**
+ * This is just to make method mocks in the unit tests easier.
+ *
+ * @param string $url The URL to fetch
+ * @param array $args Optional. Request arguments.
+ *
+ * @return array|WP_Error
+ *
+ * @codeCoverageIgnore
+ */
+ public function wp_wp_remote_get( $url, $args = array() ) {
+ return wp_remote_get( $url, $args );
+ }
+} \ No newline at end of file