diff options
Diffstat (limited to 'plugins/jetpack/modules')
49 files changed, 2915 insertions, 1123 deletions
diff --git a/plugins/jetpack/modules/after-the-deadline/config-options.php b/plugins/jetpack/modules/after-the-deadline/config-options.php index ef0ba8c9..0f81c2f0 100644 --- a/plugins/jetpack/modules/after-the-deadline/config-options.php +++ b/plugins/jetpack/modules/after-the-deadline/config-options.php @@ -80,7 +80,7 @@ function AtD_display_options_form() { echo '<br />'; AtD_print_option( 'Redundant Expression', __('Redundant Phrases', 'jetpack'), $options_show_types ); ?></p> - <p><?php printf( __( '<a href="%s" target="_blank">Learn more</a> about these options.', 'jetpack' ), 'http://support.wordpress.com/proofreading/' ); + <p><?php printf( __( '<a href="%s" rel="noopener noreferrer" target="_blank">Learn more</a> about these options.', 'jetpack' ), 'http://support.wordpress.com/proofreading/' ); ?></p> <p style="font-weight: bold"><?php _e( 'Language', 'jetpack' ); ?></p> diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel.php b/plugins/jetpack/modules/carousel/jetpack-carousel.php index 1fb4394b..77dc78e6 100644 --- a/plugins/jetpack/modules/carousel/jetpack-carousel.php +++ b/plugins/jetpack/modules/carousel/jetpack-carousel.php @@ -707,7 +707,7 @@ class Jetpack_Carousel { } function carousel_display_exif_callback() { - $this->settings_checkbox( 'carousel_display_exif', __( 'Show photo metadata (<a href="http://en.wikipedia.org/wiki/Exchangeable_image_file_format" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ); + $this->settings_checkbox( 'carousel_display_exif', __( 'Show photo metadata (<a href="http://en.wikipedia.org/wiki/Exchangeable_image_file_format" rel="noopener noreferrer" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ); } function carousel_display_exif_sanitize( $value ) { @@ -723,7 +723,7 @@ class Jetpack_Carousel { } function carousel_background_color_callback() { - $this->settings_select( 'carousel_background_color', array( 'black' => __( 'Black', 'jetpack' ), 'white' => __( 'White', 'jetpack', 'jetpack' ) ) ); + $this->settings_select( 'carousel_background_color', array( 'black' => __( 'Black', 'jetpack' ), 'white' => __( 'White', 'jetpack' ) ) ); } function carousel_background_color_sanitize( $value ) { diff --git a/plugins/jetpack/modules/comments/comments.php b/plugins/jetpack/modules/comments/comments.php index a60bfd5e..d5fe9251 100644 --- a/plugins/jetpack/modules/comments/comments.php +++ b/plugins/jetpack/modules/comments/comments.php @@ -270,6 +270,10 @@ class Jetpack_Comments extends Highlander_Comments_Base { if ( current_user_can( 'unfiltered_html' ) ) { $params['_wp_unfiltered_html_comment'] = wp_create_nonce( 'unfiltered-html-comment_' . get_the_ID() ); } + } else { + $commenter = wp_get_current_commenter(); + $params['show_cookie_consent'] = (int) has_action( 'set_comment_cookies', 'wp_set_comment_cookies' ); + $params['has_cookie_consent'] = (int) ! empty( $commenter['comment_author_email'] ); } $signature = Jetpack_Comments::sign_remote_comment_parameters( $params, Jetpack_Options::get_option( 'blog_token' ) ); diff --git a/plugins/jetpack/modules/custom-css/custom-css.php b/plugins/jetpack/modules/custom-css/custom-css.php index 3e29a410..6229014b 100644 --- a/plugins/jetpack/modules/custom-css/custom-css.php +++ b/plugins/jetpack/modules/custom-css/custom-css.php @@ -1004,8 +1004,8 @@ class Jetpack_Custom_CSS { * * @param string $str Intro text appearing above the Custom CSS editor. */ - echo apply_filters( 'safecss_intro_text', __( 'New to CSS? Start with a <a href="http://www.htmldog.com/guides/cssbeginner/" target="_blank">beginner tutorial</a>. Questions? - Ask in the <a href="https://wordpress.org/support/forum/themes-and-templates" target="_blank">Themes and Templates forum</a>.', 'jetpack' ) ); + echo apply_filters( 'safecss_intro_text', __( 'New to CSS? Start with a <a href="http://www.htmldog.com/guides/cssbeginner/" rel="noopener noreferrer" target="_blank">beginner tutorial</a>. Questions? + Ask in the <a href="https://wordpress.org/support/forum/themes-and-templates" rel="noopener noreferrer" target="_blank">Themes and Templates forum</a>.', 'jetpack' ) ); ?></p> <p class="css-support"><?php echo __( 'Note: Custom CSS will be reset when changing themes.', 'jetpack' ); ?></p> @@ -1053,7 +1053,7 @@ class Jetpack_Custom_CSS { <?php printf( /* translators: %1$s is replaced with an input field for numbers. */ - __( 'Limit width to %1$s pixels for full size images. (<a href="%2$s" target="_blank">More info</a>.)', 'jetpack' ), + __( 'Limit width to %1$s pixels for full size images. (<a href="%2$s" rel="noopener noreferrer" target="_blank">More info</a>.)', 'jetpack' ), '<input type="text" id="custom_content_width_visible" value="' . esc_attr( $custom_content_width ) . '" size="4" />', /** * Filter the Custom CSS limited width's support doc URL. diff --git a/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js index 4e98e155..7fef365f 100644 --- a/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js +++ b/plugins/jetpack/modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js @@ -41,6 +41,7 @@ $( '<a />', { id : 'help-link', target : '_blank', + rel: 'noopener noreferrer', href : window._jp_css_settings.cssHelpUrl, text : window._jp_css_settings.l10n.css_help_title }).prependTo( '#css-help-links' ); @@ -50,6 +51,7 @@ $( '<a />', { id : 'revisions-link', target : '_blank', + rel: 'noopener noreferrer', href : window._jp_css_settings.revisionsUrl, text : window._jp_css_settings.l10n.revisions }).prependTo( '#css-help-links' ); diff --git a/plugins/jetpack/modules/custom-post-types/nova.php b/plugins/jetpack/modules/custom-post-types/nova.php index 37404974..7301a314 100644 --- a/plugins/jetpack/modules/custom-post-types/nova.php +++ b/plugins/jetpack/modules/custom-post-types/nova.php @@ -307,11 +307,10 @@ class Nova_Restaurant { * Change ‘Enter Title Here’ text for the Menu Item. */ function change_default_title( $title ) { - $screen = get_current_screen(); - - if ( self::MENU_ITEM_POST_TYPE == $screen->post_type ) + if ( self::MENU_ITEM_POST_TYPE == get_post_type() ) { /* translators: this is about a food menu */ $title = esc_html__( "Enter the menu item's name here", 'jetpack' ); + } return $title; } diff --git a/plugins/jetpack/modules/custom-post-types/testimonial.php b/plugins/jetpack/modules/custom-post-types/testimonial.php index 3199d5a6..7972078f 100644 --- a/plugins/jetpack/modules/custom-post-types/testimonial.php +++ b/plugins/jetpack/modules/custom-post-types/testimonial.php @@ -364,10 +364,9 @@ class Jetpack_Testimonial { * Change ‘Enter Title Here’ text for the Testimonial. */ function change_default_title( $title ) { - $screen = get_current_screen(); - - if ( self::CUSTOM_POST_TYPE == $screen->post_type ) + if ( self::CUSTOM_POST_TYPE == get_post_type() ) { $title = esc_html__( "Enter the customer's name here", 'jetpack' ); + } return $title; } diff --git a/plugins/jetpack/modules/geo-location.php b/plugins/jetpack/modules/geo-location.php new file mode 100644 index 00000000..4d3e255c --- /dev/null +++ b/plugins/jetpack/modules/geo-location.php @@ -0,0 +1,81 @@ +<?php + +require_once dirname( __FILE__ ) . '/geo-location/class.jetpack-geo-location.php'; + +/** + * Geo-location shortcode for display of location data associated with a post. + * + * Usage with current global $post: + * [geo-location] + * + * Usage with specific post ID: + * [geo-location post=5] + */ +add_shortcode( 'geo-location', 'jetpack_geo_shortcode' ); + +function jetpack_geo_shortcode( $attributes ) { + $attributes = shortcode_atts( array( 'post' => null, 'id' => null ), $attributes ); + return jetpack_geo_get_location( $attributes['post'] ? $attributes['post'] : $attributes['id'] ); +} + +/** + * Get the geo-location data associated with the supplied post ID, if it's available + * and marked as being available for public display. The returned array will contain + * "latitude", "longitude" and "label" keys. + * + * If you do not supply a value for $post_id, the global $post will be used, if + * available. + * + * @param integer|null $post_id + * + * @return array|null + */ +function jetpack_geo_get_data( $post_id = null) { + $geo = Jetpack_Geo_Location::init(); + + if ( ! $post_id ) { + $post_id = $geo->get_post_id(); + } + + $meta_values = $geo->get_meta_values( $post_id ); + + if ( ! $meta_values['is_public'] || ! $meta_values['is_populated'] ) { + return null; + } + + return array( + 'latitude' => $meta_values['latitude'], + 'longitude' => $meta_values['longitude'], + 'label' => $meta_values['label'] + ); +} + +/** + * Display the label HTML for the geo-location information associated with the supplied + * post ID. + * + * If you do not supply a value for $post_id, the global $post will be used, if + * available. + * + * @param integer|null $post_id + * + * @return void + */ +function jetpack_geo_display_location( $post_id = null ) { + echo jetpack_geo_get_location( $post_id ); +} + +/** + * Return the label HTML for the geo-location information associated with the supplied + * post ID. + * + * If you do not supply a value for $post_id, the global $post will be used, if + * available. + * + * @param integer|null $post_id + * + * @return string + */ +function jetpack_geo_get_location( $post_id = null ) { + return Jetpack_Geo_Location::init()->get_location_label( $post_id ); +} diff --git a/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php b/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php new file mode 100644 index 00000000..1e0094f2 --- /dev/null +++ b/plugins/jetpack/modules/geo-location/class.jetpack-geo-location.php @@ -0,0 +1,411 @@ +<?php + +/** + * Adds support for geo-location features. + * + * All Jetpack sites can support geo-location features. Users can tag posts with geo-location data + * using the UI provided by Calypso. That information will be included in RSS feeds, meta tags during + * wp_head, and in the Geo microformat following post content. + * + * If your theme declares support for "geo-location", you'll also get a small icon and location label + * visible to users at the bottom of single posts and pages. + * + * To declare support in your theme, call `add_theme_support( 'jetpack-geo-location' )`. + * + * Once you've added theme support, you can rely on the standard HTML output generated in the + * the_content_location_display() method of this class. Or, you can use the "geo_location_display" + * filter to generate custom HTML for your particular theme. Your filter function will receive an + * the default HTML as its first argument and an array containing the geo-location information as + * its second argument in the following format: + * + * array( + * 'is_public' => boolean, + * 'latitude' => float, + * 'longitude' => float, + * 'label' => string, + * 'is_populated' => boolean + * ) + * + * Add your filter with: + * + * add_filter( 'jetpack_geo_location_display', 'your_filter_function_name', 10, 2); + */ +class Jetpack_Geo_Location { + private static $instance; + + public static function init() { + if ( is_null( self::$instance ) ) { + self::$instance = new Jetpack_Geo_Location(); + } + + return self::$instance; + } + + /** + * This is mostly just used for testing purposes. + */ + public static function reset_instance() { + self::$instance = null; + } + + public function __construct() { + add_action( 'init', array( $this, 'wordpress_init' ) ); + add_action( 'wp_head', array( $this, 'wp_head' ) ); + add_filter( 'the_content', array( $this, 'the_content_microformat' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + + $this->register_rss_hooks(); + } + + /** + * Register support for the geo-location feature on pages and posts. Register the meta + * fields managed by this plugin so that they are properly sanitized during save. + */ + public function wordpress_init() { + // Only render location label after post content, if the theme claims to support "geo-location". + if ( current_theme_supports( 'jetpack-geo-location' ) ) { + add_filter( 'the_content', array( $this, 'the_content_location_display' ), 15, 1 ); + } + + add_post_type_support( 'post', 'geo-location' ); + add_post_type_support( 'page', 'geo-location' ); + + register_meta( + 'post', + 'geo_public', + array( + 'sanitize_callback' => array( $this, 'sanitize_public' ), + 'type' => 'boolean', + 'single' => true, + ) + ); + + register_meta( + 'post', + 'geo_latitude', + array( + 'sanitize_callback' => array( $this, 'sanitize_coordinate' ), + 'type' => 'float', + 'single' => true, + ) + ); + + register_meta( + 'post', + 'geo_longitude', + array( + 'sanitize_callback' => array( $this, 'sanitize_coordinate' ), + 'type' => 'float', + 'single' => true, + ) + ); + + register_meta( + 'post', + 'geo_address', + array( + 'sanitize_callback' => 'sanitize_text_field', + 'type' => 'string', + 'single' => true, + ) + ); + } + + /** + * Filter "public" input to always be either 1 or 0. + * + * @param mixed $public + * + * @return int + */ + public function sanitize_public( $public ) { + return absint( $public ) ? 1 : 0; + } + + /** + * Filter geo coordinates and normalize them to floats with 7 digits of precision. + * + * @param mixed $coordinate + * + * @return float|null + */ + public function sanitize_coordinate( $coordinate ) { + if ( ! $coordinate ) { + return null; + } + + return round( (float) $coordinate, 7 ); + } + + /** + * Render geo.position and ICBM meta tags with public geo meta values when rendering + * a single post. + */ + public function wp_head() { + if ( ! is_single() ) { + return; + } + + $meta_values = $this->get_meta_values( $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return; + } + + echo "\n<!-- Jetpack Geo-location Tags -->\n"; + + if ( $meta_values['label'] ) { + printf( + '<meta name="geo.placename" content="%s" />', + esc_attr( $meta_values['label'] ) + ); + } + + printf( + '<meta name="geo.position" content="%s;%s" />' . PHP_EOL, + esc_attr( $meta_values['latitude'] ), + esc_attr( $meta_values['longitude'] ) + ); + + printf( + '<meta name="ICBM" content="%s, %s" />' . PHP_EOL, + esc_attr( $meta_values['latitude'] ), + esc_attr( $meta_values['longitude'] ) + ); + + echo "\n<!-- End Jetpack Geo-location Tags -->\n"; + } + + /** + * Append public meta values in the Geo microformat (https://en.wikipedia.org/wiki/Geo_(microformat) + * to the supplied content. + * + * Note that we cannot render the microformat in the context of an excerpt because tags are stripped + * in that context, making our microformat data visible. + * + * @param string $content + * + * @return string + */ + public function the_content_microformat( $content ) { + if ( is_feed() || $this->is_currently_excerpt_filter() ) { + return $content; + } + + $meta_values = $this->get_meta_values( $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return $content; + } + + $microformat = sprintf( + '<div id="geo-post-%d" class="geo geo-post" style="display: none">', + esc_attr( $this->get_post_id() ) + ); + + $microformat .= sprintf( + '<span class="latitude">%s</span>', + esc_html( $meta_values['latitude'] ) + ); + + $microformat .= sprintf( + '<span class="longitude">%s</span>', + esc_html( $meta_values['longitude'] ) + ); + + $microformat .= '</div>'; + + return $content . $microformat; + } + + /** + * Register a range of hooks for integrating geo data with various feeds. + */ + public function register_rss_hooks() { + add_action( 'rss2_ns', array( $this, 'rss_namespace' ) ); + add_action( 'atom_ns', array( $this, 'rss_namespace' ) ); + add_action( 'rdf_ns', array( $this, 'rss_namespace' ) ); + add_action( 'rss_item', array( $this, 'rss_item' ) ); + add_action( 'rss2_item', array( $this, 'rss_item' ) ); + add_action( 'atom_entry', array( $this, 'rss_item' ) ); + add_action( 'rdf_item', array( $this, 'rss_item' ) ); + } + + /** + * Add the georss namespace during RSS generation. + */ + public function rss_namespace() { + echo 'xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" '; + } + + /** + * Output georss data for RSS items, assuming we have data for the currently rendered post and + * that data as marked as public. + */ + public function rss_item() { + $meta_values = $this->get_meta_values( $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return; + } + + printf( + "\t<georss:point>%s %s</georss:point>\n", + ent2ncr( esc_html( $meta_values['latitude'] ) ), + ent2ncr( esc_html( $meta_values['longitude'] ) ) + ); + + printf( "\t\t<geo:lat>%s</geo:lat>\n", ent2ncr( esc_html( $meta_values['latitude'] ) ) ); + printf( "\t\t<geo:long>%s</geo:long>\n", ent2ncr( esc_html( $meta_values['longitude'] ) ) ); + } + + /** + * Enqueue CSS for rendering post flair with geo-location. + */ + public function enqueue_scripts() { + wp_enqueue_style( 'dashicons' ); + } + + /** + * If we're rendering a single post and public geo-location data is available for it, + * include the human-friendly location label in the output. + * + * @param string $content + * + * @return string + */ + public function the_content_location_display( $content ) { + if ( ! is_single() ) { + return $content; + } + + return $content . $this->get_location_label(); + } + + /** + * Get the HTML for displaying a label representing the location associated with the + * supplied post ID. If no post ID is given, we'll use the global $post variable, if + * it is available. + * + * @param integer|null $post_id + * + * @return string + */ + public function get_location_label( $post_id = null ) { + $meta_values = $this->get_meta_values( $post_id ? $post_id : $this->get_post_id() ); + + if ( ! $meta_values['is_public'] ) { + return ''; + } + + // If the location has not been labeled, do not show the location. + if ( ! $meta_values['label'] ) { + return ''; + } + + $html = '<div class="post-geo-location-label geo-chip">'; + $html .= '<span class="dashicons dashicons-location" style="vertical-align: text-top;"></span> '; + $html .= esc_html( $meta_values['label'] ); + $html .= '</div>'; + + /** + * Allow modification or replacement of the default geo-location display HTML. + * + * @module geo-location + * + * @param array $html The default HTML for displaying a geo-location label. + * @param array $geo_data An array containing "latitude", "longitude" and "label". + */ + $html = apply_filters( 'jetpack_geo_location_display', $html, $meta_values ); + + return $html; + } + + /** + * Get the ID of the current global post object, if available. Otherwise, return null. + * + * This isolates the access of the global scope to this single method, making it easier to + * safeguard against unexpected missing $post objects in other hook functions. + * + * @return int|null + */ + public function get_post_id() { + global $post; + + if ( ! isset( $post ) || ! $post || ! is_object( $post ) || ! isset( $post->ID ) ) { + return null; + } + + return $post->ID; + } + + /** + * This method always returns an array with the following structure: + * + * array(is_public => bool, latitude => float, longitude => float, label => string, is_populated => bool) + * + * So, regardless of whether your post actually has values in postmeta for the geo-location fields, + * you can be sure that you can reference those array keys in calling code without having to juggle + * isset(), array_key_exists(), etc. + * + * Mocking this method during testing can also be useful for testing output and logic in various + * hook functions. + * + * @param integer $post_id + * + * @return array A predictably structured array representing the meta values for the supplied post ID. + */ + public function get_meta_values( $post_id ) { + $meta_values = array( + 'is_public' => (bool) $this->sanitize_public( $this->get_meta_value( $post_id, 'public' ) ), + 'latitude' => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'latitude' ) ), + 'longitude' => $this->sanitize_coordinate( $this->get_meta_value( $post_id, 'longitude' ) ), + 'label' => trim( $this->get_meta_value( $post_id, 'address' ) ), + 'is_populated' => false, + ); + + if ( $meta_values['latitude'] && $meta_values['longitude'] && $meta_values['label'] ) { + $meta_values['is_populated'] = true; + } + + return $meta_values; + } + + /** + * This function wraps get_post_meta() to enable us to keep the "geo_" prefix isolated to a single + * location in the code and to assist in mocking during testing. + * + * @param integer $post_id + * @param string $meta_field_name + * + * @return mixed + */ + public function get_meta_value( $post_id, $meta_field_name ) { + if ( ! $post_id ) { + return null; + } + + return get_post_meta( $post_id, 'geo_' . $meta_field_name, true ); + } + + /** + * Check to see if the current filter is the get_the_excerpt filter. + * + * Just checking current_filter() here is not adequate because current_filter() only looks + * at the last element in the $wp_current_filter array. In the context of rendering an + * excerpt, however, both get_the_excerpt and the_content are present in that array. + * + * @return bool + */ + public function is_currently_excerpt_filter() { + if ( ! isset( $GLOBALS['wp_current_filter'] ) ) { + return false; + } + + $current_filters = (array) $GLOBALS['wp_current_filter']; + + return in_array( 'get_the_excerpt', $current_filters, true ); + } +} + +Jetpack_Geo_Location::init(); diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.php b/plugins/jetpack/modules/infinite-scroll/infinity.php index 896db249..4cd5a26d 100644 --- a/plugins/jetpack/modules/infinite-scroll/infinity.php +++ b/plugins/jetpack/modules/infinite-scroll/infinity.php @@ -1541,7 +1541,7 @@ class The_Neverending_Home_Page { */ private function default_footer() { $credits = sprintf( - '<a href="https://wordpress.org/" target="_blank" rel="generator">%1$s</a> ', + '<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ', __( 'Proudly powered by WordPress', 'jetpack' ) ); $credits .= sprintf( diff --git a/plugins/jetpack/modules/lazy-images.php b/plugins/jetpack/modules/lazy-images.php index 56a005e5..5031710a 100644 --- a/plugins/jetpack/modules/lazy-images.php +++ b/plugins/jetpack/modules/lazy-images.php @@ -3,6 +3,7 @@ /** * Module Name: Lazy Images * Module Description: Lazy load images + * Jumpstart Description: Lazy-loading images improve your site's speed and create a smoother viewing experience. Images will load as visitors scroll down the screen, instead of all at once. * Sort Order: 24 * Recommendation Order: 14 * First Introduced: 5.6.0 diff --git a/plugins/jetpack/modules/lazy-images/js/lazy-images.js b/plugins/jetpack/modules/lazy-images/js/lazy-images.js index 2c7137b9..7da313ce 100644 --- a/plugins/jetpack/modules/lazy-images/js/lazy-images.js +++ b/plugins/jetpack/modules/lazy-images/js/lazy-images.js @@ -17,6 +17,9 @@ var jetpackLazyImagesModule = function( $ ) { // Lazy load images that are brought in from Infinite Scroll $( 'body' ).bind( 'post-load', lazy_load_init ); + + // Add event to provide optional compatibility for other code. + $( 'body' ).bind( 'jetpack-lazy-images-load', lazy_load_init ); } ); function lazy_load_init() { diff --git a/plugins/jetpack/modules/lazy-images/lazy-images.php b/plugins/jetpack/modules/lazy-images/lazy-images.php index 25ca181b..59c64a1e 100644 --- a/plugins/jetpack/modules/lazy-images/lazy-images.php +++ b/plugins/jetpack/modules/lazy-images/lazy-images.php @@ -47,6 +47,7 @@ class Jetpack_Lazy_Images { add_action( 'admin_bar_menu', array( $this, 'remove_filters' ), 0 ); add_filter( 'wp_kses_allowed_html', array( $this, 'allow_lazy_attributes' ) ); + add_action( 'wp_head', array( $this, 'add_nojs_fallback' ) ); } public function setup_filters() { @@ -176,6 +177,9 @@ class Jetpack_Lazy_Images { return $matches[0]; } + // Ensure we add the jetpack-lazy-image class to this image. + $new_attributes['class'] = sprintf( '%s jetpack-lazy-image', empty( $new_attributes['class'] ) ? '' : $new_attributes['class'] ); + $new_attributes_str = self::build_attributes_string( $new_attributes ); return sprintf( '<img %1$s><noscript>%2$s</noscript>', $new_attributes_str, $matches[0] ); @@ -250,6 +254,30 @@ class Jetpack_Lazy_Images { return apply_filters( 'jetpack_lazy_images_new_attributes', $attributes ); } + /** + * Adds JavaScript to check if the current browser supports JavaScript as well as some styles to hide lazy + * images when the browser does not support JavaScript. + * + * @return void + */ + public function add_nojs_fallback() { + ?> + <style type="text/css"> + .jetpack-lazy-image { + display: none; + } + .jetpack-lazy-images-js .jetpack-lazy-image { + display: inline-block; + } + </style> + <script> + document.documentElement.classList.add( + 'jetpack-lazy-images-js' + ); + </script> + <?php + } + private static function get_placeholder_image() { /** * Allows plugins and themes to modify the placeholder image. diff --git a/plugins/jetpack/modules/markdown/easy-markdown.php b/plugins/jetpack/modules/markdown/easy-markdown.php index 65e4685e..dd6bba01 100644 --- a/plugins/jetpack/modules/markdown/easy-markdown.php +++ b/plugins/jetpack/modules/markdown/easy-markdown.php @@ -451,7 +451,7 @@ class WPCom_Markdown { ?> <script type="text/javascript"> jQuery( function() { - tinymce.on( 'AddEditor', function( event ) { + ( 'undefined' !== typeof tinymce ) && tinymce.on( 'AddEditor', function( event ) { event.editor.on( 'BeforeSetContent', function( event ) { var editor = event.target; Object.keys( editor.schema.elements ).forEach( function( key, index ) { diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php index c25fa83a..a55c1988 100644 --- a/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php +++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/footer.php @@ -56,7 +56,7 @@ do_action( 'minileven_credits' ); ?> - <a href="<?php echo esc_url( __( 'https://wordpress.org/', 'jetpack' ) ); ?>" target="_blank" title="<?php esc_attr_e( 'Semantic Personal Publishing Platform', 'jetpack' ); ?>" rel="generator"><?php printf( __( 'Proudly powered by %s', 'jetpack' ), 'WordPress' ); ?></a> + <a href="<?php echo esc_url( __( 'https://wordpress.org/', 'jetpack' ) ); ?>" rel="noopener noreferrer" target="_blank" title="<?php esc_attr_e( 'Semantic Personal Publishing Platform', 'jetpack' ); ?>" rel="generator"><?php printf( __( 'Proudly powered by %s', 'jetpack' ), 'WordPress' ); ?></a> </div> </footer><!-- #colophon --> diff --git a/plugins/jetpack/modules/module-extras.php b/plugins/jetpack/modules/module-extras.php index 368cf80b..01caab67 100644 --- a/plugins/jetpack/modules/module-extras.php +++ b/plugins/jetpack/modules/module-extras.php @@ -25,6 +25,7 @@ $tools = array( 'simple-payments/simple-payments.php', 'verification-tools/verification-tools-utils.php', 'woocommerce-analytics/wp-woocommerce-analytics.php', + 'geo-location.php' ); // Not every tool needs to be included if Jetpack is inactive and not in development mode diff --git a/plugins/jetpack/modules/module-headings.php b/plugins/jetpack/modules/module-headings.php index 6fb48631..d5215490 100644 --- a/plugins/jetpack/modules/module-headings.php +++ b/plugins/jetpack/modules/module-headings.php @@ -83,6 +83,7 @@ function jetpack_get_module_i18n( $key ) { 'lazy-images' => array(
'name' => _x( 'Lazy Images', 'Module Name', 'jetpack' ), 'description' => _x( 'Lazy load images', 'Module Description', 'jetpack' ), + 'recommended description' => _x( 'Lazy-loading images improve your site\'s speed and create a smoother viewing experience. Images will load as visitors scroll down the screen, instead of all at once.', 'Jumpstart Description', 'jetpack' ), ),
'likes' => array(
diff --git a/plugins/jetpack/modules/protect.php b/plugins/jetpack/modules/protect.php index 5a8b2e4f..48117d5e 100644 --- a/plugins/jetpack/modules/protect.php +++ b/plugins/jetpack/modules/protect.php @@ -50,7 +50,7 @@ class Jetpack_Protect_Module { add_action( 'jetpack_activate_module_protect', array ( $this, 'on_activation' ) ); add_action( 'jetpack_deactivate_module_protect', array ( $this, 'on_deactivation' ) ); add_action( 'jetpack_modules_loaded', array ( $this, 'modules_loaded' ) ); - add_action( 'login_init', array ( $this, 'check_use_math' ) ); + add_action( 'login_form', array ( $this, 'check_use_math' ), 0 ); add_filter( 'authenticate', array ( $this, 'check_preauth' ), 10, 3 ); add_action( 'wp_login', array ( $this, 'log_successful_login' ), 10, 2 ); add_action( 'wp_login_failed', array ( $this, 'log_failed_attempt' ) ); @@ -58,7 +58,7 @@ class Jetpack_Protect_Module { add_action( 'admin_init', array ( $this, 'maybe_display_security_warning' ) ); // This is a backup in case $pagenow fails for some reason - add_action( 'login_head', array ( $this, 'check_login_ability' ), 100, 3 ); + add_action( 'login_form', array ( $this, 'check_login_ability' ), 1 ); // Runs a script every day to clean up expired transients so they don't // clog up our users' databases diff --git a/plugins/jetpack/modules/protect/blocked-login-page.php b/plugins/jetpack/modules/protect/blocked-login-page.php index f26b5193..efba2b3e 100644 --- a/plugins/jetpack/modules/protect/blocked-login-page.php +++ b/plugins/jetpack/modules/protect/blocked-login-page.php @@ -600,7 +600,7 @@ class Jetpack_Protect_Blocked_Login_Page { <a href='javascript:history.back()'><?php printf( __( '%s Back' ), $back_button_icon ); ?></a> <?php } else { $help_icon = '<svg class="gridicon gridicons-help" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 16h-2v-2h2v2zm0-4.14V15h-2v-2c0-.552.448-1 1-1 1.103 0 2-.897 2-2s-.897-2-2-2-2 .897-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.862-1.278 3.413-3 3.86z"/></g></svg>';?> - <a href="<?php echo esc_url( self::HELP_URL ); ?>" target="_blank"><?php printf( __( '%s Get help unlocking your site' ), $help_icon );?></a> + <a href="<?php echo esc_url( self::HELP_URL ); ?>" rel="noopener noreferrer" target="_blank"><?php printf( __( '%s Get help unlocking your site' ), $help_icon );?></a> <?php } ?> </div> </body> diff --git a/plugins/jetpack/modules/publicize/ui.php b/plugins/jetpack/modules/publicize/ui.php index 162c3ee0..5e2e84e7 100644 --- a/plugins/jetpack/modules/publicize/ui.php +++ b/plugins/jetpack/modules/publicize/ui.php @@ -149,7 +149,7 @@ class Publicize_UI { } ?> - <p>→ <a href="<?php echo esc_url( $doc_link ); ?>" target="_blank"><?php esc_html_e( 'More information on using Publicize.', 'jetpack' ); ?></a></p> + <p>→ <a href="<?php echo esc_url( $doc_link ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'More information on using Publicize.', 'jetpack' ); ?></a></p> <div id="publicize-services-block"> <?php @@ -562,7 +562,7 @@ jQuery( function($) { <strong><?php echo esc_html( $item ); ?></strong> <?php endforeach; ?> </span> - <a href="#" id="publicize-form-edit"><?php esc_html_e( 'Edit', 'jetpack' ); ?></a> <a href="<?php echo esc_url( admin_url( 'options-general.php?page=sharing' ) ); ?>" target="_blank"><?php _e( 'Settings', 'jetpack' ); ?></a><br /> + <a href="#" id="publicize-form-edit"><?php esc_html_e( 'Edit', 'jetpack' ); ?></a> <a href="<?php echo esc_url( admin_url( 'options-general.php?page=sharing' ) ); ?>" rel="noopener noreferrer" target="_blank"><?php _e( 'Settings', 'jetpack' ); ?></a><br /> <?php else : ?> <?php $publicize_form = $this->get_metabox_form_disconnected( $available_services ); ?> <strong><?php echo __( 'Not Connected', 'jetpack' ); ?></strong> @@ -771,7 +771,7 @@ jQuery( function($) { <ul class="not-connected"> <?php foreach ( $available_services as $service_name => $service ) : ?> <li> - <a class="pub-service" data-service="<?php echo esc_attr( $service_name ); ?>" title="<?php echo esc_attr( sprintf( __( 'Connect and share your posts on %s', 'jetpack' ), $this->publicize->get_service_label( $service_name ) ) ); ?>" target="_blank" href="<?php echo esc_url( $this->publicize->connect_url( $service_name ) ); ?>"> + <a class="pub-service" data-service="<?php echo esc_attr( $service_name ); ?>" title="<?php echo esc_attr( sprintf( __( 'Connect and share your posts on %s', 'jetpack' ), $this->publicize->get_service_label( $service_name ) ) ); ?>" rel="noopener noreferrer" target="_blank" href="<?php echo esc_url( $this->publicize->connect_url( $service_name ) ); ?>"> <?php echo esc_html( $this->publicize->get_service_label( $service_name ) ); ?> </a> </li> diff --git a/plugins/jetpack/modules/related-posts/jetpack-related-posts.php b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php index 751a66dd..37790d6b 100644 --- a/plugins/jetpack/modules/related-posts/jetpack-related-posts.php +++ b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php @@ -384,7 +384,7 @@ EOT; $ui_settings = sprintf( $ui_settings_template, checked( $options['show_headline'], true, false ), - esc_html__( 'Show a "Related" header to more clearly separate the related section from posts', 'jetpack' ), + esc_html__( 'Highlight related content with a heading', 'jetpack' ), checked( $options['show_thumbnails'], true, false ), esc_html__( 'Show a thumbnail image where available', 'jetpack' ), checked( $options['show_date'], true, false ), diff --git a/plugins/jetpack/modules/sharedaddy/sharedaddy.php b/plugins/jetpack/modules/sharedaddy/sharedaddy.php index ec147d85..f8d0e92f 100644 --- a/plugins/jetpack/modules/sharedaddy/sharedaddy.php +++ b/plugins/jetpack/modules/sharedaddy/sharedaddy.php @@ -222,7 +222,7 @@ function sharing_plugin_settings( $links ) { function sharing_add_plugin_settings($links, $file) { if ( $file == basename( dirname( __FILE__ ) ).'/'.basename( __FILE__ ) ) { $links[] = '<a href="options-general.php?page=sharing.php">' . __( 'Settings', 'jetpack' ) . '</a>'; - $links[] = '<a href="http://support.wordpress.com/sharing/" target="_blank">' . __( 'Support', 'jetpack' ) . '</a>'; + $links[] = '<a href="http://support.wordpress.com/sharing/" rel="noopener noreferrer" target="_blank">' . __( 'Support', 'jetpack' ) . '</a>'; } return $links; diff --git a/plugins/jetpack/modules/sharedaddy/sharing-sources.php b/plugins/jetpack/modules/sharedaddy/sharing-sources.php index 6df9b210..a086e0f0 100644 --- a/plugins/jetpack/modules/sharedaddy/sharing-sources.php +++ b/plugins/jetpack/modules/sharedaddy/sharing-sources.php @@ -205,7 +205,7 @@ abstract class Sharing_Source { ( $id ? esc_attr( $id ) : '' ), implode( ' ', $klasses ), $url, - ( true == $this->open_link_in_new ) ? ' target="_blank"' : '', + ( true == $this->open_link_in_new ) ? ' rel="noopener noreferrer" target="_blank"' : '', $title, ( 'icon' == $this->button_style ) ? '></span><span class="sharing-screen-reader-text"' : '', $text diff --git a/plugins/jetpack/modules/sharedaddy/sharing.php b/plugins/jetpack/modules/sharedaddy/sharing.php index c6f57436..19b0e3be 100644 --- a/plugins/jetpack/modules/sharedaddy/sharing.php +++ b/plugins/jetpack/modules/sharedaddy/sharing.php @@ -183,7 +183,7 @@ class Sharing_Admin { if ( false == function_exists( 'mb_stripos' ) ) { echo '<div id="message" class="updated fade"><h3>' . __( 'Warning! Multibyte support missing!', 'jetpack' ) . '</h3>'; - echo '<p>' . sprintf( __( 'This plugin will work without it, but multibyte support is used <a href="%s" target="_blank">if available</a>. You may see minor problems with Tweets and other sharing services.', 'jetpack' ), 'http://www.php.net/manual/en/mbstring.installation.php' ) . '</p></div>'; + echo '<p>' . sprintf( __( 'This plugin will work without it, but multibyte support is used <a href="%s" rel="noopener noreferrer" target="_blank">if available</a>. You may see minor problems with Tweets and other sharing services.', 'jetpack' ), 'http://www.php.net/manual/en/mbstring.installation.php' ) . '</p></div>'; } if ( isset( $_GET['update'] ) && $_GET['update'] == 'saved' ) { diff --git a/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js b/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js index 5b070891..c166931a 100644 --- a/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js +++ b/plugins/jetpack/modules/simple-payments/paypal-express-checkout.js @@ -76,7 +76,7 @@ var PaypalExpressCheckout = { var cssClasses = PaypalExpressCheckout.messageCssClassName + ' show '; cssClasses += isError ? 'error' : 'success'; - // show message 1s after Paypal popup is closed + // show message 1s after PayPal popup is closed setTimeout( function() { domEl.innerHTML = message; domEl.setAttribute( 'class', cssClasses ); @@ -140,9 +140,9 @@ var PaypalExpressCheckout = { style: { label: 'pay', - fundingicons: true, shape: 'rect', - color: 'silver' + color: 'silver', + fundingicons: true, }, payment: function() { diff --git a/plugins/jetpack/modules/simple-payments/simple-payments.php b/plugins/jetpack/modules/simple-payments/simple-payments.php index a7e5371a..6ac3d8c7 100644 --- a/plugins/jetpack/modules/simple-payments/simple-payments.php +++ b/plugins/jetpack/modules/simple-payments/simple-payments.php @@ -27,7 +27,7 @@ class Jetpack_Simple_Payments { return self::$instance; } - private function register_scripts() { + private function register_scripts_and_styles() { /** * Paypal heavily discourages putting that script in your own server: * @see https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/add-paypal-button/ @@ -35,18 +35,26 @@ class Jetpack_Simple_Payments { wp_register_script( 'paypal-checkout-js', 'https://www.paypalobjects.com/api/checkout.js', array(), null, true ); wp_register_script( 'paypal-express-checkout', plugins_url( '/paypal-express-checkout.js', __FILE__ ), array( 'jquery', 'paypal-checkout-js' ), self::$version ); + wp_register_style( 'jetpack-simple-payments', plugins_url( '/simple-payments.css', __FILE__ ), array( 'dashicons' ) ); } + private function register_init_hook() { add_action( 'init', array( $this, 'init_hook_action' ) ); } + private function register_shortcode() { add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) ); } public function init_hook_action() { + if ( ! $this->is_enabled_jetpack_simple_payments() ) { + add_shortcode( self::$shortcode, array( $this, 'ignore_shortcode' ) ); + return; + } + add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) ); add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) ); - $this->register_scripts(); + $this->register_scripts_and_styles(); $this->register_shortcode(); $this->setup_cpts(); @@ -69,6 +77,33 @@ class Jetpack_Simple_Payments { return Jetpack_Options::get_option( 'id' ); } + /** + * Used to check whether Simple Payments are enabled for given site. + * + * @return bool True if Simple Payments are enabled, false otherwise. + */ + function is_enabled_jetpack_simple_payments() { + /** + * Can be used by plugin authors to disable the conflicting output of Simple Payments. + * + * @since 6.3.0 + * + * @param bool True if Simple Payments should be disabled, false otherwise. + */ + if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) { + return false; + } + + // For WPCOM sites + if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'has_blog_sticker' ) ) { + $site_id = $this->get_blog_id(); + return has_blog_sticker( 'premium-plan', $site_id ) || has_blog_sticker( 'business-plan', $site_id ); + } + + // For all Jetpack sites + return Jetpack::is_active() && Jetpack::active_plan_supports( 'simple-payments'); + } + function parse_shortcode( $attrs, $content = false ) { if ( empty( $attrs['id'] ) ) { return; @@ -100,12 +135,14 @@ class Jetpack_Simple_Payments { ); $data['id'] = $attrs['id']; + + if( ! wp_style_is( 'jetpack-simple-payments', 'enqueue' ) ) { + wp_enqueue_style( 'jetpack-simple-payments' ); + } + if ( ! wp_script_is( 'paypal-express-checkout', 'enqueued' ) ) { wp_enqueue_script( 'paypal-express-checkout' ); } - if ( ! wp_style_is( 'simple-payments', 'enqueued' ) ) { - wp_enqueue_style( 'simple-payments', plugins_url( 'simple-payments.css', __FILE__ ), array( 'dashicons' ) ); - } wp_add_inline_script( 'paypal-express-checkout', sprintf( "try{PaypalExpressCheckout.renderButton( '%d', '%d', '%s', '%d' );}catch(e){}", @@ -118,6 +155,8 @@ class Jetpack_Simple_Payments { return $this->output_shortcode( $data ); } + function ignore_shortcode() { return; } + function output_shortcode( $data ) { $items = ''; $css_prefix = self::$css_classname_prefix; @@ -213,7 +252,7 @@ class Jetpack_Simple_Payments { 'read_private_posts' => 'read_private_posts', ); $order_args = array( - 'label' => esc_html__( 'Order', 'jetpack' ), + 'label' => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack' ), 'description' => esc_html__( 'Simple Payments orders', 'jetpack' ), 'supports' => array( 'custom-fields', 'excerpt' ), 'hierarchical' => false, diff --git a/plugins/jetpack/modules/sitemaps/sitemap-builder.php b/plugins/jetpack/modules/sitemaps/sitemap-builder.php index 2af3b749..b83a7c49 100644 --- a/plugins/jetpack/modules/sitemaps/sitemap-builder.php +++ b/plugins/jetpack/modules/sitemaps/sitemap-builder.php @@ -475,13 +475,13 @@ class Jetpack_Sitemap_Builder { if ( 0 < $max[ JP_VIDEO_SITEMAP_TYPE ]['number'] ) { if ( 1 === $max[ JP_VIDEO_SITEMAP_TYPE ]['number'] ) { $video['filename'] = jp_sitemap_filename( JP_VIDEO_SITEMAP_TYPE, 1 ); - $video['last_modified'] = $max[ JP_VIDEO_SITEMAP_TYPE ]['lastmod']; + $video['last_modified'] = jp_sitemap_datetime( $max[ JP_VIDEO_SITEMAP_TYPE ]['lastmod'] ); } else { $video['filename'] = jp_sitemap_filename( JP_VIDEO_SITEMAP_INDEX_TYPE, $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['number'] ); - $video['last_modified'] = $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['lastmod']; + $video['last_modified'] = jp_sitemap_datetime( $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['lastmod'] ); } $buffer->append( diff --git a/plugins/jetpack/modules/sitemaps/sitemap-stylist.php b/plugins/jetpack/modules/sitemaps/sitemap-stylist.php index c5167d8b..2b4d2f0a 100644 --- a/plugins/jetpack/modules/sitemaps/sitemap-stylist.php +++ b/plugins/jetpack/modules/sitemaps/sitemap-stylist.php @@ -70,7 +70,7 @@ class Jetpack_Sitemap_Stylist { $description = self::sanitize_with_links( __( - 'This is an XML Sitemap generated by <a href="%1$s" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" target="_blank">Google</a> or <a href="%3$s" target="_blank">Bing</a>.', + 'This is an XML Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( @@ -82,7 +82,7 @@ class Jetpack_Sitemap_Stylist { $more_info = self::sanitize_with_links( __( - 'You can find more information on XML sitemaps at <a href="%1$s" target="_blank">sitemaps.org</a>', + 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( @@ -92,7 +92,7 @@ class Jetpack_Sitemap_Stylist { $generated_by = self::sanitize_with_links( __( - 'Generated by <a href="%s" target="_blank">Jetpack for WordPress</a>', + 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( @@ -182,7 +182,7 @@ XSL; $description = self::sanitize_with_links( __( - 'This is an XML Sitemap Index generated by <a href="%1$s" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" target="_blank">Google</a> or <a href="%3$s" target="_blank">Bing</a>.', + 'This is an XML Sitemap Index generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( @@ -194,7 +194,7 @@ XSL; $more_info = self::sanitize_with_links( __( - 'You can find more information on XML sitemaps at <a href="%1$s" target="_blank">sitemaps.org</a>', + 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( @@ -204,7 +204,7 @@ XSL; $generated_by = self::sanitize_with_links( __( - 'Generated by <a href="%s" target="_blank">Jetpack for WordPress</a>', + 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( @@ -297,7 +297,7 @@ XSL; $description = self::sanitize_with_links( __( - 'This is an XML Image Sitemap generated by <a href="%1$s" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" target="_blank">Google</a> or <a href="%3$s" target="_blank">Bing</a>.', + 'This is an XML Image Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( @@ -309,7 +309,7 @@ XSL; $more_info = self::sanitize_with_links( __( - 'You can find more information on XML sitemaps at <a href="%1$s" target="_blank">sitemaps.org</a>', + 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( @@ -319,7 +319,7 @@ XSL; $generated_by = self::sanitize_with_links( __( - 'Generated by <a href="%s" target="_blank">Jetpack for WordPress</a>', + 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( @@ -437,7 +437,7 @@ XSL; $description = self::sanitize_with_links( __( - 'This is an XML Video Sitemap generated by <a href="%1$s" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" target="_blank">Google</a> or <a href="%3$s" target="_blank">Bing</a>.', + 'This is an XML Video Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( @@ -449,7 +449,7 @@ XSL; $more_info = self::sanitize_with_links( __( - 'You can find more information on XML sitemaps at <a href="%1$s" target="_blank">sitemaps.org</a>', + 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( @@ -459,7 +459,7 @@ XSL; $generated_by = self::sanitize_with_links( __( - 'Generated by <a href="%s" target="_blank">Jetpack for WordPress</a>', + 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( @@ -577,7 +577,7 @@ XSL; $description = self::sanitize_with_links( __( - 'This is an XML News Sitemap generated by <a href="%1$s" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" target="_blank">Google</a> or <a href="%3$s" target="_blank">Bing</a>.', + 'This is an XML News Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( @@ -589,7 +589,7 @@ XSL; $more_info = self::sanitize_with_links( __( - 'You can find more information on XML sitemaps at <a href="%1$s" target="_blank">sitemaps.org</a>', + 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( @@ -599,7 +599,7 @@ XSL; $generated_by = self::sanitize_with_links( __( - 'Generated by <a href="%s" target="_blank">Jetpack for WordPress</a>', + 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( diff --git a/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php b/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php index f89de5ba..ca3c8baf 100644 --- a/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php +++ b/plugins/jetpack/modules/sso/class.jetpack-sso-notices.php @@ -20,7 +20,7 @@ class Jetpack_SSO_Notices { $error = sprintf( wp_kses( __( - 'Two-Step Authentication is required to access this site. Please visit your <a href="%1$s" target="_blank">Security Settings</a> to configure <a href="%2$s" target="_blank">Two-step Authentication</a> for your account.', + 'Two-Step Authentication is required to access this site. Please visit your <a href="%1$s" rel="noopener noreferrer" target="_blank">Security Settings</a> to configure <a href="%2$s" rel="noopener noreferrer" target="_blank">Two-step Authentication</a> for your account.', 'jetpack' ), array( 'a' => array( 'href' => array() ) ) diff --git a/plugins/jetpack/modules/stats.php b/plugins/jetpack/modules/stats.php index fd20f517..220b1268 100644 --- a/plugins/jetpack/modules/stats.php +++ b/plugins/jetpack/modules/stats.php @@ -500,7 +500,7 @@ function stats_reports_css() { } #jp-stats-wrap { - max-width: 720px; + max-width: 1040px; margin: 0 auto; overflow: hidden; } diff --git a/plugins/jetpack/modules/theme-tools.php b/plugins/jetpack/modules/theme-tools.php index c9d82c45..b421021d 100644 --- a/plugins/jetpack/modules/theme-tools.php +++ b/plugins/jetpack/modules/theme-tools.php @@ -35,9 +35,10 @@ function jetpack_load_theme_compat() { * @param array Associative array of theme compat files to load. */ $compat_files = apply_filters( 'jetpack_theme_compat_files', array( - 'twentyfourteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfourteen.php', - 'twentyfifteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfifteen.php', - 'twentysixteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentysixteen.php', + 'twentyfourteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfourteen.php', + 'twentyfifteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfifteen.php', + 'twentysixteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentysixteen.php', + 'twentyseventeen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyseventeen.php', ) ); _jetpack_require_compat_file( get_stylesheet(), $compat_files ); diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php index b48a971f..adaa42b7 100644 --- a/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php +++ b/plugins/jetpack/modules/theme-tools/compat/twentyfifteen.php @@ -9,6 +9,11 @@ function twentyfifteen_jetpack_setup() { * Add theme support for Responsive Videos. */ add_theme_support( 'jetpack-responsive-videos' ); + + /** + * Add theme support for geo-location. + */ + add_theme_support( 'jetpack-geo-location' ); } add_action( 'after_setup_theme', 'twentyfifteen_jetpack_setup' ); diff --git a/plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php b/plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php new file mode 100644 index 00000000..4a60e504 --- /dev/null +++ b/plugins/jetpack/modules/theme-tools/compat/twentyseventeen.php @@ -0,0 +1,13 @@ +<?php +/** + * Jetpack Compatibility File + * See: http://jetpack.com/ + */ + +function twentyseventeen_jetpack_setup() { + /** + * Add theme support for geo-location. + */ + add_theme_support( 'jetpack-geo-location' ); +} +add_action( 'after_setup_theme', 'twentyseventeen_jetpack_setup' ); diff --git a/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php b/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php index 7e106037..20fd7f5d 100644 --- a/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php +++ b/plugins/jetpack/modules/theme-tools/compat/twentysixteen.php @@ -9,6 +9,11 @@ function twentysixteen_jetpack_setup() { * Add theme support for Responsive Videos. */ add_theme_support( 'jetpack-responsive-videos' ); + + /** + * Add theme support for geo-location. + */ + add_theme_support( 'jetpack-geo-location' ); } add_action( 'after_setup_theme', 'twentysixteen_jetpack_setup' ); diff --git a/plugins/jetpack/modules/theme-tools/content-options/customizer.php b/plugins/jetpack/modules/theme-tools/content-options/customizer.php index 5123d98e..0fea7ae7 100644 --- a/plugins/jetpack/modules/theme-tools/content-options/customizer.php +++ b/plugins/jetpack/modules/theme-tools/content-options/customizer.php @@ -227,7 +227,7 @@ function jetpack_content_options_customize_register( $wp_customize ) { $wp_customize->add_control( new Jetpack_Customize_Control_Title( $wp_customize, 'jetpack_content_featured_images_title', array( 'section' => 'jetpack_content_options', - 'label' => esc_html__( 'Featured Images', 'jetpack' ) . sprintf( '<a href="https://en.support.wordpress.com/featured-images/" class="customize-help-toggle dashicons dashicons-editor-help" title="%1$s" target="_blank"><span class="screen-reader-text">%1$s</span></a>', esc_html__( 'Learn more about Featured Images', 'jetpack' ) ), + 'label' => esc_html__( 'Featured Images', 'jetpack' ) . sprintf( '<a href="https://en.support.wordpress.com/featured-images/" class="customize-help-toggle dashicons dashicons-editor-help" title="%1$s" rel="noopener noreferrer" target="_blank"><span class="screen-reader-text">%1$s</span></a>', esc_html__( 'Learn more about Featured Images', 'jetpack' ) ), 'type' => 'title', 'active_callback' => 'jetpack_post_thumbnail_supports', ) ) ); diff --git a/plugins/jetpack/modules/theme-tools/random-redirect.php b/plugins/jetpack/modules/theme-tools/random-redirect.php index 2e58bee6..78c8f349 100644 --- a/plugins/jetpack/modules/theme-tools/random-redirect.php +++ b/plugins/jetpack/modules/theme-tools/random-redirect.php @@ -45,7 +45,7 @@ function jetpack_matt_random_redirect() { // Persistent AppEngine abuse. ORDER BY RAND is expensive. if ( strstr( $_SERVER['HTTP_USER_AGENT'], 'AppEngine-Google' ) ) - wp_die( 'Please <a href="http://en.support.wordpress.com/contact/" target="_blank">contact support</a>' ); + wp_die( 'Please <a href="http://en.support.wordpress.com/contact/" rel="noopener noreferrer" target="_blank">contact support</a>' ); // Set the category ID if the parameter is set. if ( isset( $_GET['random_cat_id'] ) ) diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php index 694e7bf6..2399cd39 100644 --- a/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php +++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery/tiled-gallery-item.php @@ -19,11 +19,10 @@ abstract class Jetpack_Tiled_Gallery_Item { } $this->orig_file = wp_get_attachment_url( $this->image->ID ); - // If Photon is active, use it for original - if ( in_array( 'photon', Jetpack::get_active_modules() ) ) { - $this->orig_file = jetpack_photon_url( $this->orig_file ); - } - $this->link = $needs_attachment_link ? get_attachment_link( $this->image->ID ) : $this->orig_file; + $this->link = $needs_attachment_link + ? get_attachment_link( $this->image->ID ) + // The filter will photonize the URL if and only if Photon is active + : apply_filters( 'jetpack_photon_url', $this->orig_file ); $img_args = array( 'w' => $this->image->width, @@ -33,6 +32,8 @@ abstract class Jetpack_Tiled_Gallery_Item { if ( $this->image->height == $this->image->width ) { $img_args['crop'] = true; } + // The function will always photonoize the URL (even if Photon is + // not active). We need to photonize the URL to set the width/height. $this->img_src = jetpack_photon_url( $this->orig_file, $img_args ); } diff --git a/plugins/jetpack/modules/verification-tools/blog-verification-tools.php b/plugins/jetpack/modules/verification-tools/blog-verification-tools.php index f8b4b4a5..b96bd371 100644 --- a/plugins/jetpack/modules/verification-tools/blog-verification-tools.php +++ b/plugins/jetpack/modules/verification-tools/blog-verification-tools.php @@ -151,7 +151,7 @@ function jetpack_verification_tool_box() { $last = array_pop( $list ); if ( current_user_can( 'manage_options' ) ) { - echo '<div class="jp-verification-tools card"><h3 class="title">' . __( 'Website Verification Services' , 'jetpack' ) . ' <a href="http://support.wordpress.com/webmaster-tools/" target="_blank">(?)</a></h3>'; + echo '<div class="jp-verification-tools card"><h3 class="title">' . __( 'Website Verification Services' , 'jetpack' ) . ' <a href="http://support.wordpress.com/webmaster-tools/" rel="noopener noreferrer" target="_blank">(?)</a></h3>'; echo '<p>' . sprintf( esc_html( __( 'Enter your meta key "content" value to verify your blog with %s' , 'jetpack' ) ), implode( ', ', $list ) ) . ' ' . __( 'and' , 'jetpack' ) . ' ' . $last . '.</p>'; jetpack_verification_options_form(); echo '</div>'; diff --git a/plugins/jetpack/modules/videopress/class.videopress-player.php b/plugins/jetpack/modules/videopress/class.videopress-player.php index 11abc45c..e301802b 100644 --- a/plugins/jetpack/modules/videopress/class.videopress-player.php +++ b/plugins/jetpack/modules/videopress/class.videopress-player.php @@ -292,7 +292,7 @@ class VideoPress_Player { $html .= '<input type="submit" value="' . __( 'Submit', 'jetpack' ) . '" style="cursor:pointer;border-radius: 1em;border:1px solid #333;background-color:#333;background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0, #444), color-stop(1, #111) );background:-moz-linear-gradient(center top, #444 0%, #111 100%);font-size:13px;padding:4px 10px 5px;line-height:1em;vertical-align:top;color:white;text-decoration:none;margin:0" />'; $html .= '</fieldset>'; - $html .= '<p style="padding-top:20px;padding-bottom:60px;text-align:' . $text_align . ';"><a rel="nofollow" href="http://videopress.com/" target="_blank" style="color:rgb(128,128,128);text-decoration:underline;font-size:15px">' . __( 'More information', 'jetpack' ) . '</a></p>'; + $html .= '<p style="padding-top:20px;padding-bottom:60px;text-align:' . $text_align . ';"><a rel="nofollow" href="http://videopress.com/" rel="noopener noreferrer" target="_blank" style="color:rgb(128,128,128);text-decoration:underline;font-size:15px">' . __( 'More information', 'jetpack' ) . '</a></p>'; $html .= '</div>'; return $html; @@ -341,7 +341,7 @@ class VideoPress_Player { $html .= esc_attr( $this->video->title ); $html .= '" src="' . $thumbnail . '" width="' . $this->video->calculated_width . '" height="' . $this->video->calculated_height . '" /></div>'; if ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) - $html .= '<p class="robots-nocontent">' . sprintf( __( 'You do not have sufficient <a rel="nofollow" href="%s" target="_blank">freedom levels</a> to view this video. Support free software and upgrade.', 'jetpack' ), 'http://www.gnu.org/philosophy/free-sw.html' ) . '</p>'; + $html .= '<p class="robots-nocontent">' . sprintf( __( 'You do not have sufficient <a rel="nofollow" href="%s" rel="noopener noreferrer" target="_blank">freedom levels</a> to view this video. Support free software and upgrade.', 'jetpack' ), 'http://www.gnu.org/philosophy/free-sw.html' ) . '</p>'; elseif ( isset( $this->video->title ) ) $html .= '<p>' . esc_html( $this->video->title ) . '</p>'; $html .= '</video>'; @@ -795,7 +795,7 @@ class VideoPress_Player { foreach ( $this->get_flash_parameters() as $attribute => $value ) { $flash_params .= '<param name="' . esc_attr( $attribute ) . '" value="' . esc_attr( $value ) . '" />'; } - $flash_help = sprintf( __( 'This video requires <a rel="nofollow" href="%s" target="_blank">Adobe Flash</a> for playback.', 'jetpack' ), 'http://www.adobe.com/go/getflashplayer'); + $flash_help = sprintf( __( 'This video requires <a rel="nofollow" href="%s" rel="noopener noreferrer" target="_blank">Adobe Flash</a> for playback.', 'jetpack' ), 'http://www.adobe.com/go/getflashplayer'); $flash_player_url = esc_url( $this->video->players->swf->url, array( 'http', 'https' ) ); $description = ''; if ( isset( $this->video->title ) ) { diff --git a/plugins/jetpack/modules/widgets/customizer-utils.js b/plugins/jetpack/modules/widgets/customizer-utils.js index e14c5f16..da73225c 100644 --- a/plugins/jetpack/modules/widgets/customizer-utils.js +++ b/plugins/jetpack/modules/widgets/customizer-utils.js @@ -1,4 +1,4 @@ -/* global wp, gapi, FB, twttr */ +/* global wp, gapi, FB, twttr, PaypalExpressCheckout */ /** * Utilities to work with widgets in Customizer. @@ -64,6 +64,16 @@ wp.isJetpackWidgetPlaced = function( placement, widgetName ) { $( '.widget_eu_cookie_law_widget' ).removeClass( 'top' ); } placement.container.fadeIn(); + } else if ( wp.isJetpackWidgetPlaced( placement, 'jetpack_simple_payments_widget' ) ) { + // Refresh Simple Payments Widget + try { + var buttonId = $( '.jetpack-simple-payments-button', placement.container ).attr( 'id' ).replace( '_button', '' ); + PaypalExpressCheckout.renderButton( null, null, buttonId, null ); + } catch ( e ) { + // PaypalExpressCheckout may fail. + // For the same usage, see also: + // https://github.com/Automattic/jetpack/blob/6c1971e6bed7d3df793392a7a58ffe0afaeeb5fe/modules/simple-payments/simple-payments.php#L111 + } } } } ); @@ -74,6 +84,9 @@ wp.isJetpackWidgetPlaced = function( placement, widgetName ) { // Refresh Twitter timeline iframe, since it has to be re-built. if ( wp.isJetpackWidgetPlaced( placement, 'twitter_timeline' ) && placement.container.find( 'iframe.twitter-timeline:not([src]):first' ).length ) { placement.partial.refresh(); + } else if ( wp.isJetpackWidgetPlaced( placement, 'jetpack_simple_payments_widget' ) ) { + // Refresh Simple Payments Widget + placement.partial.refresh(); } } } ); diff --git a/plugins/jetpack/modules/widgets/simple-payments.php b/plugins/jetpack/modules/widgets/simple-payments.php new file mode 100644 index 00000000..f31af1b0 --- /dev/null +++ b/plugins/jetpack/modules/widgets/simple-payments.php @@ -0,0 +1,487 @@ +<?php +/** + * Disable direct access/execution to/of the widget code. + */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +if ( ! class_exists( 'Jetpack_Simple_Payments_Widget' ) ) { + /** + * Simple Payments Button + * + * Display a Simple Payment Button as a Widget. + */ + class Jetpack_Simple_Payments_Widget extends WP_Widget { + // https://developer.paypal.com/docs/integration/direct/rest/currency-codes/ + private static $supported_currency_list = array( + 'USD' => '$', + 'GBP' => '£', + 'JPY' => '¥', + 'BRL' => 'R$', + 'EUR' => '€', + 'NZD' => 'NZ$', + 'AUD' => 'A$', + 'CAD' => 'C$', + 'INR' => '₹', + 'ILS' => '₪', + 'RUB' => '₽', + 'MXN' => 'MX$', + 'SEK' => 'Skr', + 'HUF' => 'Ft', + 'CHF' => 'CHF', + 'CZK' => 'Kč', + 'DKK' => 'Dkr', + 'HKD' => 'HK$', + 'NOK' => 'Kr', + 'PHP' => '₱', + 'PLN' => 'PLN', + 'SGD' => 'S$', + 'TWD' => 'NT$', + 'THB' => '฿', + ); + + /** + * Constructor. + */ + function __construct() { + parent::__construct( + 'jetpack_simple_payments_widget', + /** This filter is documented in modules/widgets/facebook-likebox.php */ + apply_filters( 'jetpack_widget_name', __( 'Simple Payments', 'jetpack' ) ), + array( + 'classname' => 'jetpack-simple-payments', + 'description' => __( 'Add a Simple Payment Button as a Widget.', 'jetpack' ), + 'customize_selective_refresh' => true, + ) + ); + + if ( is_customize_preview() ) { + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_styles_and_scripts' ) ); + + add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) ); + add_action( 'wp_ajax_customize-jetpack-simple-payments-buttons-get', array( $this, 'ajax_get_payment_buttons' ) ); + add_action( 'wp_ajax_customize-jetpack-simple-payments-button-save', array( $this, 'ajax_save_payment_button' ) ); + add_action( 'wp_ajax_customize-jetpack-simple-payments-button-delete', array( $this, 'ajax_delete_payment_button' ) ); + } + + if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_style' ) ); + } + } + + /** + * Return an associative array of default values. + * + * These values are used in new widgets. + * + * @return array Default values for the widget options. + */ + private function defaults() { + $current_user = wp_get_current_user(); + $default_product_id = $this->get_first_product_id(); + + return array( + 'title' => '', + 'product_post_id' => $default_product_id, + 'form_action' => '', + 'form_product_id' => 0, + 'form_product_title' => '', + 'form_product_description' => '', + 'form_product_image_id' => 0, + 'form_product_image_src' => '', + 'form_product_currency' => '', + 'form_product_price' => '', + 'form_product_multiple' => '', + 'form_product_email' => $current_user->user_email, + ); + } + + /** + * Adds a nonce for customizing menus. + * + * @param array $nonces Array of nonces. + * @return array $nonces Modified array of nonces. + */ + function filter_nonces( $nonces ) { + $nonces['customize-jetpack-simple-payments'] = wp_create_nonce( 'customize-jetpack-simple-payments' ); + return $nonces; + } + + function enqueue_style() { + wp_enqueue_style( 'jetpack-simple-payments-widget-style', plugins_url( 'simple-payments/style.css', __FILE__ ), array(), '20180518' ); + } + + function admin_enqueue_styles_and_scripts(){ + wp_enqueue_style( 'jetpack-simple-payments-widget-customizer', plugins_url( 'simple-payments/customizer.css', __FILE__ ) ); + + wp_enqueue_media(); + wp_enqueue_script( 'jetpack-simple-payments-widget-customizer', plugins_url( '/simple-payments/customizer.js', __FILE__ ), array( 'jquery' ), false, true ); + wp_localize_script( 'jetpack-simple-payments-widget-customizer', 'jpSimplePaymentsStrings', array( + 'deleteConfirmation' => __( 'Are you sure you want to delete this item? It will be disabled and removed from all locations where it currently appears.', 'jetpack' ) + ) ); + } + + public function ajax_get_payment_buttons() { + if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) { + wp_send_json_error( 'bad_nonce', 400 ); + } + + if ( ! current_user_can( 'customize' ) ) { + wp_send_json_error( 'customize_not_allowed', 403 ); + } + + $post_type_object = get_post_type_object( Jetpack_Simple_Payments::$post_type_product ); + if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) { + wp_send_json_error( 'insufficient_post_permissions', 403 ); + } + + $product_posts = get_posts( array( + 'numberposts' => 100, + 'orderby' => 'date', + 'post_type' => Jetpack_Simple_Payments::$post_type_product, + 'post_status' => 'publish', + ) ); + + $formatted_products = array_map( array( $this, 'format_product_post_for_ajax_reponse' ), $product_posts ); + + wp_send_json_success( $formatted_products ); + } + + public function format_product_post_for_ajax_reponse( $product_post ) { + return array( + 'ID' => $product_post->ID, + 'post_title' => $product_post->post_title, + ); + } + + public function ajax_save_payment_button() { + if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) { + wp_send_json_error( 'bad_nonce', 400 ); + } + + if ( ! current_user_can( 'customize' ) ) { + wp_send_json_error( 'customize_not_allowed', 403 ); + } + + $post_type_object = get_post_type_object( Jetpack_Simple_Payments::$post_type_product ); + if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) { + wp_send_json_error( 'insufficient_post_permissions', 403 ); + } + + if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) { + wp_send_json_error( 'missing_params', 400 ); + } + + $params = wp_unslash( $_POST['params'] ); + $errors = $this->validate_ajax_params( $params ); + if ( ! empty( $errors->errors ) ) { + wp_send_json_error( $errors ); + } + + $product_post_id = isset( $params['product_post_id'] ) ? intval( $params['product_post_id'] ) : 0; + + $product_post = array( + 'ID' => $product_post_id, + 'post_type' => Jetpack_Simple_Payments::$post_type_product, + 'post_status' => 'publish', + 'post_title' => $params['post_title'], + 'post_content' => $params['post_content'], + '_thumbnail_id' => ! empty( $params['image_id'] ) ? $params['image_id'] : -1, + 'meta_input' => array( + 'spay_currency' => $params['currency'], + 'spay_price' => $params['price'], + 'spay_multiple' => isset( $params['multiple'] ) ? intval( $params['multiple'] ) : 0, + 'spay_email' => is_email( $params['email'] ), + ), + ); + + if ( empty( $product_post_id ) ) { + $product_post_id = wp_insert_post( $product_post ); + } else { + $product_post_id = wp_update_post( $product_post ); + } + + if ( ! $product_post_id || is_wp_error( $product_post_id ) ) { + wp_send_json_error( $product_post_id ); + } + + $tracks_properties = array( + 'id' => $product_post_id, + 'currency' => $params['currency'], + 'price' => $params['price'] + ); + if ( 0 === $product_post['ID'] ) { + $this->record_event( 'created', 'create', $tracks_properties ); + } else { + $this->record_event( 'updated', 'update', $tracks_properties ); + } + + wp_send_json_success( array( + 'product_post_id' => $product_post_id, + 'product_post_title' => $params['post_title'], + ) ); + } + + public function ajax_delete_payment_button() { + if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) { + wp_send_json_error( 'bad_nonce', 400 ); + } + + if ( ! current_user_can( 'customize' ) ) { + wp_send_json_error( 'customize_not_allowed', 403 ); + } + + if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) { + wp_send_json_error( 'missing_params', 400 ); + } + + $params = wp_unslash( $_POST['params'] ); + $illegal_params = array_diff( array_keys( $params ), array( 'product_post_id' ) ); + if ( ! empty( $illegal_params ) ) { + wp_send_json_error( 'illegal_params', 400 ); + } + + $product_id = ( int ) $params['product_post_id']; + $product_post = get_post( $product_id ); + + $return = array( 'status' => $product_post->post_status ); + + wp_delete_post( $product_id, true ); + $status = get_post_status( $product_id ); + if ( false === $status ) { + $return['status'] = 'deleted'; + } + + $this->record_event( 'deleted', 'delete', array( 'id' => $product_id ) ); + + wp_send_json_success( $return ); + } + + public function validate_ajax_params( $params ) { + $errors = new WP_Error(); + + $illegal_params = array_diff( array_keys( $params ), array( 'product_post_id', 'post_title', 'post_content', 'image_id', 'currency', 'price', 'multiple', 'email' ) ); + if ( ! empty( $illegal_params ) ) { + $errors.add( 'illegal_params' ); + } + + if ( empty( $params['post_title'] ) ) { + $errors->add( 'post_title', __( 'People need to know what they\'re paying for! Please add a brief title.' ) ); + } + + if ( empty( $params['price'] ) || floatval( $params['price'] ) <= 0 ) { + $errors->add( 'price', __( 'Everything comes with a price tag these days. Please add a your product price.' ) ); + } + + if ( empty( $params['email'] ) || ! is_email( $params['email'] ) ) { + $errors->add( 'email', __( 'We want to make sure payments reach you, so please add an email address.' ) ); + } + + return $errors; + } + + function get_first_product_id() { + $product_posts = get_posts( array( + 'numberposts' => 1, + 'orderby' => 'date', + 'post_type' => Jetpack_Simple_Payments::$post_type_product, + 'post_status' => 'publish', + ) ); + + return ! empty( $product_posts ) ? $product_posts[0]->ID : null; + } + + /** + * Front-end display of widget. + * + * @see WP_Widget::widget() + * + * @param array $args Widget arguments. + * @param array $instance Saved values from database. + */ + function widget( $args, $instance ) { + $instance = wp_parse_args( $instance, $this->defaults() ); + + echo $args['before_widget']; + + /** This filter is documented in core/src/wp-includes/default-widgets.php */ + $title = apply_filters( 'widget_title', $instance['title'] ); + if ( ! empty( $title ) ) { + echo $args['before_title'] . $title . $args['after_title']; + } + + echo '<div class="jetpack-simple-payments-content">'; + + if ( ! empty( $instance['form_action'] ) && in_array( $instance['form_action'], array( 'add', 'edit' ) ) && is_customize_preview() ) { + require( dirname( __FILE__ ) . '/simple-payments/widget.php' ); + } else { + $jsp = Jetpack_Simple_Payments::getInstance(); + $simple_payments_button = $jsp->parse_shortcode( array( + 'id' => $instance['product_post_id'], + ) ); + + if ( ! is_null( $simple_payments_button ) || is_customize_preview() ) { + echo $simple_payments_button; + } + } + + echo '</div><!--simple-payments-->'; + + echo $args['after_widget']; + + /** This action is already documented in modules/widgets/gravatar-profile.php */ + do_action( 'jetpack_stats_extra', 'widget_view', 'simple_payments' ); + } + + /** + * Gets the latests field value from either the old instance or the new instance. + * + * @param array $mixed Array of values for the new form instance. + * @param array $mixed Array of values for the old form instance. + * @return mixed $mixed Field value. + */ + private function get_latest_field_value( $new_instance, $old_instance, $field) { + return ! empty( $new_instance[ $field ] ) + ? sanitize_text_field( $new_instance[ $field ] ) + : $old_instance[ $field ]; + } + + /** + * Gets the product fields from the product post. If no post found + * it returns the default values. + * + * @param int Product Post ID. + * @return array $fields Product Fields from the Product Post. + */ + private function get_product_from_post( $product_post_id ) { + $product_post = get_post( $product_post_id ); + $form_product_id = $product_post_id; + if( ! empty( $product_post ) ) { + $form_product_image_id = get_post_thumbnail_id( $product_post_id ); + + return array( + 'form_product_id' => $form_product_id, + 'form_product_title' => get_the_title( $product_post ), + 'form_product_description' => $product_post->post_content, + 'form_product_image_id' => $form_product_image_id, + 'form_product_image_src' => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ), + 'form_product_currency' => get_post_meta( $product_post_id, 'spay_currency', true ), + 'form_product_price' => get_post_meta( $product_post_id, 'spay_price', true ), + 'form_product_multiple' => get_post_meta( $product_post_id, 'spay_multiple', true ) || '0', + 'form_product_email' => get_post_meta( $product_post_id, 'spay_email', true ), + ); + } + + return $this->defaults(); + } + + /** + * Record a Track event and bump a MC stat. + * + * @param string $stat_name + * @param string $event_action + * @param array $event_properties + */ + private function record_event( $stat_name, $event_action, $event_properties = array() ) { + $current_user = wp_get_current_user(); + + // `bumps_stats_extra` only exists on .com + if ( function_exists( 'bump_stats_extras' ) ) { + require_lib( 'tracks/client' ); + tracks_record_event( $current_user, 'simple_payments_button_' . $event_action, $event_properties ); + /** This action is documented in modules/widgets/social-media-icons.php */ + do_action( 'jetpack_bump_stats_extra', 'jetpack-simple_payments', $stat_name ); + return; + } + + jetpack_tracks_record_event( $current_user, 'jetpack_wpa_simple_payments_button_' . $event_action, $event_properties ); + $jetpack = Jetpack::init(); + // $jetpack->stat automatically prepends the stat group with 'jetpack-' + $jetpack->stat( 'simple_payments', $stat_name ) ; + $jetpack->do_stats( 'server_side' ); + } + + /** + * Sanitize widget form values as they are saved. + * + * @see WP_Widget::update() + * + * @param array $new_instance Values just sent to be saved. + * @param array $old_instance Previously saved values from database. + * + * @return array Updated safe values to be saved. + */ + function update( $new_instance, $old_instance ) { + $defaults = $this->defaults(); + //do not overrite `product_post_id` for `$new_instance` with the defaults + $new_instance = wp_parse_args( $new_instance, array_diff_key( $defaults, array( 'product_post_id' => 0 ) ) ); + $old_instance = wp_parse_args( $old_instance, $defaults ); + + $required_widget_props = array( + 'title' => $this->get_latest_field_value( $new_instance, $old_instance, 'title' ), + 'product_post_id' => $this->get_latest_field_value( $new_instance, $old_instance, 'product_post_id' ), + 'form_action' => $this->get_latest_field_value( $new_instance, $old_instance, 'form_action' ), + ); + + if ( strcmp( $new_instance['form_action'], $old_instance['form_action'] ) !== 0 ) { + if ( $new_instance['form_action'] == 'edit' ) { + return array_merge( $this->get_product_from_post( ( int ) $old_instance['product_post_id'] ), $required_widget_props ); + } + + if ( $new_instance['form_action'] == 'clear' ) { + return array_merge( $this->defaults(), $required_widget_props ); + } + } + + $form_product_image_id = (int) $new_instance['form_product_image_id']; + + $form_product_email = ! empty( $new_instance['form_product_email'] ) + ? sanitize_text_field( $new_instance['form_product_email'] ) + : $defaults['form_product_email']; + + return array_merge( $required_widget_props, array( + 'form_product_id' => ( int ) $new_instance['form_product_id'], + 'form_product_title' => sanitize_text_field( $new_instance['form_product_title'] ), + 'form_product_description' => sanitize_text_field( $new_instance['form_product_description'] ), + 'form_product_image_id' => $form_product_image_id, + 'form_product_image_src' => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ), + 'form_product_currency' => sanitize_text_field( $new_instance['form_product_currency'] ), + 'form_product_price' => sanitize_text_field( $new_instance['form_product_price'] ), + 'form_product_multiple' => sanitize_text_field( $new_instance['form_product_multiple'] ), + 'form_product_email' => $form_product_email, + ) ); + } + + /** + * Back-end widget form. + * + * @see WP_Widget::form() + * + * @param array $instance Previously saved values from database. + */ + function form( $instance ) { + $instance = wp_parse_args( $instance, $this->defaults() ); + + $product_posts = get_posts( array( + 'numberposts' => 100, + 'orderby' => 'date', + 'post_type' => Jetpack_Simple_Payments::$post_type_product, + 'post_status' => 'publish', + ) ); + + require( dirname( __FILE__ ) . '/simple-payments/form.php' ); + } + } + + // Register Jetpack_Simple_Payments_Widget widget. + function register_widget_jetpack_simple_payments() { + $jetpack_simple_payments = Jetpack_Simple_Payments::getInstance(); + if ( ! $jetpack_simple_payments->is_enabled_jetpack_simple_payments() ) { + return; + } + + register_widget( 'Jetpack_Simple_Payments_Widget' ); + } + add_action( 'widgets_init', 'register_widget_jetpack_simple_payments' ); +} diff --git a/plugins/jetpack/modules/widgets/simple-payments/customizer.css b/plugins/jetpack/modules/widgets/simple-payments/customizer.css new file mode 100644 index 00000000..48733d51 --- /dev/null +++ b/plugins/jetpack/modules/widgets/simple-payments/customizer.css @@ -0,0 +1,72 @@ +.widget-content .jetpack-simple-payments, +.widget-content .jetpack-simple-payments-form { + clear: both; +} + +.widget-content .jetpack-simple-payments-form .invalid { + border: 1px solid #dc3232; +} + +.widget-content .jetpack-simple-payments-form .cost label { + display: block; +} + +.widget-content .jetpack-simple-payments-image-fieldset { + position: relative; + width: 100%; +} + +.widget-content .jetpack-simple-payments-image-fieldset .placeholder { + border: 1px dashed #b4b9be; + box-sizing: border-box; + cursor: pointer; + line-height: 20px; + padding: 9px 0; + position: relative; + text-align: center; + width: 100%; + margin: 4px 0 1em; +} + +.widget-content .jetpack-simple-payments-image { + max-width: 100%; + margin-top: 4px; + position: relative; + text-align: center; +} + +.widget-content .jetpack-simple-payments-image img { + max-width: 100%; + box-sizing: border-box; + border: 1px dashed #b4b9be; + padding: 4px; + height: auto; + cursor: pointer; +} + +.widget-content .jetpack-simple-payments-image img:hover { + border-style: solid; +} + +.widget-content .jetpack-simple-payments-form .field-currency { + display: inline-block; + vertical-align: top; + width: 40%; +} + +.widget-content .jetpack-simple-payments-form .field-price { + display: inline-block; + line-height: 20px; + width: 58%; +} + +.widget-content .jetpack-simple-payments-form .alignleft button, +.widget-content .jetpack-simple-payments-form .alignright span { + display: inline-block; + margin-top: 5px; +} + +.widget-content .button-link:disabled, +.widget-content .button-link:hover[disabled] { + color: #a0a5aa; +} diff --git a/plugins/jetpack/modules/widgets/simple-payments/customizer.js b/plugins/jetpack/modules/widgets/simple-payments/customizer.js new file mode 100644 index 00000000..8930ebba --- /dev/null +++ b/plugins/jetpack/modules/widgets/simple-payments/customizer.js @@ -0,0 +1,373 @@ +/* global jQuery, jpSimplePaymentsStrings, confirm, _ */ +/* eslint no-var: 0, quote-props: 0 */ + +( function( api, wp, $ ) { + var $document = $( document ); + + $document.ready( function() { + $document.on( 'widget-added', function( event, widgetContainer ) { + if ( widgetContainer.is( '[id*="jetpack_simple_payments_widget"]' ) ) { + initWidget( widgetContainer ); + } + } ); + + $document.on( 'widget-synced widget-updated', function( event, widgetContainer ) { + //this fires for all widgets, this prevent errors for non SP widgets + if ( ! widgetContainer.is( '[id*="jetpack_simple_payments_widget"]' ) ) { + return; + } + + event.preventDefault(); + + syncProductLists(); + + var widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); + + enableFormActions( widgetForm ); + + updateProductImage( widgetForm ); + } ); + } ); + + function initWidget( widgetContainer ) { + var widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); + + //Add New Button + widgetForm.find( '.jetpack-simple-payments-add-product' ).on( 'click', showAddNewForm( widgetForm ) ); + //Edit Button + widgetForm.find( '.jetpack-simple-payments-edit-product' ).on( 'click', showEditForm( widgetForm ) ); + //Select an Image + widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder, .jetpack-simple-payments-image > img' ).on( 'click', selectImage( widgetForm ) ); + //Remove Image Button + widgetForm.find( '.jetpack-simple-payments-remove-image' ).on( 'click', removeImage( widgetForm ) ); + //Save Product button + widgetForm.find( '.jetpack-simple-payments-save-product' ).on( 'click', saveChanges( widgetForm ) ); + //Cancel Button + widgetForm.find( '.jetpack-simple-payments-cancel-form' ).on( 'click', clearForm( widgetForm ) ); + //Delete Selected Product + widgetForm.find( '.jetpack-simple-payments-delete-product' ).on( 'click', deleteProduct( widgetForm ) ); + //Input, Select and Checkbox change + widgetForm.find( 'select, input, textarea, checkbox' ).on( 'change input propertychange', _.debounce( function() { + disableFormActions( widgetForm ); + }, 250 ) ); + } + + function syncProductLists() { + var request = wp.ajax.post( 'customize-jetpack-simple-payments-buttons-get', { + 'customize-jetpack-simple-payments-nonce': api.settings.nonce[ 'customize-jetpack-simple-payments' ], + 'customize_changeset_uuid': api.settings.changeset.uuid + } ); + + request.done( function( data ) { + var selectedProduct = 0; + + $( document ).find( 'select.jetpack-simple-payments-products' ).each( function( index, select ) { + var $select = $( select ); + selectedProduct = $select.val(); + + $select.find( 'option' ).remove(); + $select.append( $.map( data, function( product ) { + return $( '<option>', { value: product.ID, text: product.post_title } ); + } ) ); + $select.val( selectedProduct ); + } ); + } ); + } + + function showForm( widgetForm ) { + //reset validations + widgetForm.find( '.invalid' ).removeClass( 'invalid' ); + //disable widget title and product selector + widgetForm.find( '.jetpack-simple-payments-widget-title' ) + .add( '.jetpack-simple-payments-products' ) + //disable add and edit buttons + .add( '.jetpack-simple-payments-add-product' ) + .add( '.jetpack-simple-payments-edit-product' ) + //disable save, delete and cancel until the widget update event is fired + .add( '.jetpack-simple-payments-save-product' ) + .add( '.jetpack-simple-payments-cancel-form' ) + .add( '.jetpack-simple-payments-delete-product' ) + .attr( 'disabled', 'disabled' ); + //show form + widgetForm.find( '.jetpack-simple-payments-form' ).show(); + } + + function hideForm( widgetForm ) { + //enable widget title and product selector + widgetForm.find( '.jetpack-simple-payments-widget-title' ) + .add( '.jetpack-simple-payments-products' ) + .removeAttr( 'disabled' ); + //hide the form + widgetForm.find( '.jetpack-simple-payments-form' ).hide(); + } + + function changeFormAction( widgetForm, action ) { + widgetForm.find( '.jetpack-simple-payments-form-action' ).val( action ).change(); + } + + function showAddNewForm( widgetForm ) { + return function( event ) { + event.preventDefault(); + + showForm( widgetForm ); + changeFormAction( widgetForm, 'add' ); + }; + } + + function showEditForm( widgetForm ) { + return function( event ) { + event.preventDefault(); + + showForm( widgetForm ); + changeFormAction( widgetForm, 'edit' ); + }; + } + + function clearForm( widgetForm ) { + return function( event ) { + event.preventDefault(); + + hideForm( widgetForm ); + widgetForm.find( '.jetpack-simple-payments-add-product, .jetpack-simple-payments-edit-product' ).attr( 'disabled', 'disabled' ); + changeFormAction( widgetForm, 'clear' ); + }; + } + + function enableFormActions( widgetForm ) { + var isFormVisible = widgetForm.find( '.jetpack-simple-payments-form' ).is( ':visible' ); + var isProductSelectVisible = widgetForm.find( '.jetpack-simple-payments-products' ).is( ':visible' ); //areProductsVisible ? + var isEdit = widgetForm.find( '.jetpack-simple-payments-form-action' ).val() === 'edit'; + + if ( isFormVisible ) { + widgetForm.find( '.jetpack-simple-payments-save-product' ) + .add( '.jetpack-simple-payments-cancel-form' ) + .removeAttr( 'disabled' ); + } else { + widgetForm.find( '.jetpack-simple-payments-add-product' ).removeAttr( 'disabled' ); + } + + if ( isFormVisible && isEdit ) { + widgetForm.find( '.jetpack-simple-payments-delete-product' ).removeAttr( 'disabled' ); + } + + if ( isProductSelectVisible && ! isFormVisible ) { + widgetForm.find( '.jetpack-simple-payments-edit-product' ).removeAttr( 'disabled' ); + } + } + + function disableFormActions( widgetForm ) { + widgetForm.find( '.jetpack-simple-payments-add-product' ) + .add( '.jetpack-simple-payments-edit-product' ) + .add( '.jetpack-simple-payments-save-product' ) + .add( '.jetpack-simple-payments-cancel-form' ) + .add( '.jetpack-simple-payments-delete-product' ) + .attr( 'disabled', 'disabled' ); + } + + function selectImage( widgetForm ) { + return function( event ) { + event.preventDefault(); + + var imageContainer = widgetForm.find( '.jetpack-simple-payments-image' ); + + var mediaFrame = new wp.media.view.MediaFrame.Select( { + title: 'Choose Product Image', + multiple: false, + library: { type: 'image' }, + button: { text: 'Choose Image' } + } ); + + mediaFrame.on( 'select', function() { + var selection = mediaFrame.state().get( 'selection' ).first().toJSON(); + //hide placeholder + widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ).hide(); + + //load image from media library + imageContainer.find( 'img' ) + .attr( 'src', selection.url ) + .show(); + + //show image and remove button + widgetForm.find( '.jetpack-simple-payments-image' ).show(); + + //set hidden field for the selective refresh + widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val( selection.id ).change(); + } ); + + mediaFrame.open(); + }; + } + + function removeImage( widgetForm ) { + return function( event ) { + event.preventDefault(); + + //show placeholder + widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ).show(); + + //hide image and remove button + widgetForm.find( '.jetpack-simple-payments-image' ).hide(); + + //set hidden field for the selective refresh + widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val( '' ).change(); + }; + } + + function updateProductImage( widgetForm ) { + var newImageId = parseInt( widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val(), 10 ); + var newImageSrc = widgetForm.find( '.jetpack-simple-payments-form-image-src' ).val(); + + var placeholder = widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ); + var image = widgetForm.find( '.jetpack-simple-payments-image > img' ); + var imageControls = widgetForm.find( '.jetpack-simple-payments-image' ); + + if ( newImageId && newImageSrc ) { + image.attr( 'src', newImageSrc ); + placeholder.hide(); + imageControls.show(); + } else { + placeholder.show(); + image.removeAttr( 'src' ); + imageControls.hide(); + } + } + + function isFormValid( widgetForm ) { + widgetForm.find( '.invalid' ).removeClass( 'invalid' ); + + var errors = false; + + var postTitle = widgetForm.find( '.jetpack-simple-payments-form-product-title' ).val(); + if ( ! postTitle ) { + widgetForm.find( '.jetpack-simple-payments-form-product-title' ).addClass( 'invalid' ); + errors = true; + } + + var productPrice = widgetForm.find( '.jetpack-simple-payments-form-product-price' ).val(); + if ( ! productPrice || isNaN( parseFloat( productPrice ) ) || parseFloat( productPrice ) <= 0 ) { + widgetForm.find( '.jetpack-simple-payments-form-product-price' ).addClass( 'invalid' ); + errors = true; + } + + var productEmail = widgetForm.find( '.jetpack-simple-payments-form-product-email' ).val(); + var isProductEmailValid = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test( productEmail ); + if ( ! productEmail || ! isProductEmailValid ) { + widgetForm.find( '.jetpack-simple-payments-form-product-email' ).addClass( 'invalid' ); + errors = true; + } + + return ! errors; + } + + function saveChanges( widgetForm ) { + return function( event ) { + event.preventDefault(); + var productPostId = widgetForm.find( '.jetpack-simple-payments-form-product-id' ).val(); + + if ( ! isFormValid( widgetForm ) ) { + return; + } + + disableFormActions( widgetForm ); + + widgetForm.find( '.spinner' ).show(); + + var request = wp.ajax.post( 'customize-jetpack-simple-payments-button-save', { + 'customize-jetpack-simple-payments-nonce': api.settings.nonce[ 'customize-jetpack-simple-payments' ], + 'customize_changeset_uuid': api.settings.changeset.uuid, + 'params': { + 'product_post_id': productPostId, + 'post_title': widgetForm.find( '.jetpack-simple-payments-form-product-title' ).val(), + 'post_content': widgetForm.find( '.jetpack-simple-payments-form-product-description' ).val(), + 'image_id': widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val(), + 'currency': widgetForm.find( '.jetpack-simple-payments-form-product-currency' ).val(), + 'price': widgetForm.find( '.jetpack-simple-payments-form-product-price' ).val(), + 'multiple': widgetForm.find( '.jetpack-simple-payments-form-product-multiple' ).is( ':checked' ) ? 1 : 0, + 'email': widgetForm.find( '.jetpack-simple-payments-form-product-email' ).val() + } + } ); + + request.done( function( data ) { + var select = widgetForm.find( 'select.jetpack-simple-payments-products' ); + var productOption = select.find( 'option[value="' + productPostId + '"]' ); + + if ( productOption.length > 0 ) { + productOption.text( data.product_post_title ); + } else { + select.append( + $( '<option>', { + value: data.product_post_id, + text: data.product_post_title + } ) + ); + select.val( data.product_post_id ).change(); + } + + widgetForm.find( '.jetpack-simple-payments-products-fieldset' ).show(); + widgetForm.find( '.jetpack-simple-payments-products-warning' ).hide(); + + changeFormAction( widgetForm, 'clear' ); + hideForm( widgetForm ); + } ); + + request.fail( function( data ) { + var validCodes = { + 'post_title': 'product-title', + 'price': 'product-price', + 'email': 'product-email' + }; + + data.forEach( function( item ) { + if ( validCodes.hasOwnProperty( item.code ) ) { + widgetForm.find( '.jetpack-simple-payments-form-' + validCodes[ item.code ] ).addClass( 'invalid' ); + } + } ); + + enableFormActions( widgetForm ); + } ); + }; + } + + function deleteProduct( widgetForm ) { + return function( event ) { + event.preventDefault(); + + if ( ! confirm( jpSimplePaymentsStrings.deleteConfirmation ) ) { + return; + } + + var formProductId = parseInt( widgetForm.find( '.jetpack-simple-payments-form-product-id' ).val(), 10 ); + if ( ! formProductId ) { + return; + } + + disableFormActions( widgetForm ); + + widgetForm.find( '.spinner' ).show(); + + var request = wp.ajax.post( 'customize-jetpack-simple-payments-button-delete', { + 'customize-jetpack-simple-payments-nonce': api.settings.nonce[ 'customize-jetpack-simple-payments' ], + 'customize_changeset_uuid': api.settings.changeset.uuid, + 'params': { + 'product_post_id': formProductId + } + } ); + + request.done( function() { + var productList = widgetForm.find( 'select.jetpack-simple-payments-products' )[ 0 ]; + productList.remove( productList.selectedIndex ); + productList.dispatchEvent( new Event( 'change' ) ); + + if ( $( productList ).has( 'option' ).length === 0 ) { + //hide products select and label + widgetForm.find( '.jetpack-simple-payments-products-fieldset' ).hide(); + //show empty products list warning + widgetForm.find( '.jetpack-simple-payments-products-warning' ).show(); + } + + changeFormAction( widgetForm, 'clear' ); + hideForm( widgetForm ); + } ); + }; + } +}( wp.customize, wp, jQuery ) ); diff --git a/plugins/jetpack/modules/widgets/simple-payments/form.php b/plugins/jetpack/modules/widgets/simple-payments/form.php new file mode 100644 index 00000000..483c4661 --- /dev/null +++ b/plugins/jetpack/modules/widgets/simple-payments/form.php @@ -0,0 +1,155 @@ +<p> + <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Widget Title', 'jetpack' ); ?></label> + <input + type="text" + class="widefat jetpack-simple-payments-widget-title" + id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" + value="<?php echo esc_attr( $instance['title'] ); ?>" /> +</p> +<p class="jetpack-simple-payments-products-fieldset" <?php if ( empty( $product_posts ) ) { echo 'style="display:none;"'; } ?>> + <label for="<?php echo $this->get_field_id('product_post_id'); ?>"><?php _e( 'Select a Simple Payment Button:', 'jetpack' ); ?></label> + <select + class="widefat jetpack-simple-payments-products" + id="<?php echo $this->get_field_id('product_post_id'); ?>" + name="<?php echo $this->get_field_name('product_post_id'); ?>"> + <?php foreach ( $product_posts as $product_post ) { ?> + <option value="<?php echo esc_attr( $product_post->ID ) ?>" <?php selected( (int) $instance['product_post_id'], $product_post->ID ); ?>> + <?php echo esc_attr( get_the_title( $product_post ) ) ?> + </option> + <?php } ?> + </select> +</p> +<?php if ( is_customize_preview() ) { ?> +<p class="jetpack-simple-payments-products-warning" <?php if ( ! empty( $product_posts ) ) { echo 'style="display:none;"'; } ?>> + <?php echo __( 'Looks like you don\'t have any products. You can create one using the Add New button below.' ) ?> +</p> +<p> + <div class="alignleft"> + <button class="button jetpack-simple-payments-edit-product" <?php disabled( empty( $product_posts ), true ); ?>> + <?php esc_html_e( 'Edit Selected' ); ?> + </button> + </div> + <div class="alignright"> + <button class="button jetpack-simple-payments-add-product"><?php esc_html_e( 'Add New' ); ?></button> + </div> + <br class="clear"> +</p> +<hr /> +<div class="jetpack-simple-payments-form" style="display: none;"> + <input + type="hidden" + id="<?php echo $this->get_field_id('form_action'); ?>" + name="<?php echo $this->get_field_name('form_action'); ?>" + value="<?php echo esc_attr( $instance['form_action'] ); ?>" + class="jetpack-simple-payments-form-action" /> + <input + type="hidden" + id="<?php echo $this->get_field_id('form_product_id'); ?>" + name="<?php echo $this->get_field_name('form_product_id'); ?>" + value="<?php echo esc_attr( $instance['form_product_id'] ); ?>" + class="jetpack-simple-payments-form-product-id" /> + <input + type="hidden" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_image_id' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_image_id' ) ); ?>" + value="<?php echo esc_attr( $instance['form_product_image_id'] ); ?>" + class="jetpack-simple-payments-form-image-id" /> + <input + type="hidden" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_image_src' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_image_src' ) ); ?>" + value="<?php echo esc_attr( $instance['form_product_image_src'] ); ?>" + class="jetpack-simple-payments-form-image-src" /> + <p> + <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_title' ) ); ?>"><?php esc_html_e( 'What is this payment for?' ); ?></label> + <input + type="text" + class="widefat field-title jetpack-simple-payments-form-product-title" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_title' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_title' ) ); ?>" + value="<?php echo esc_attr( $instance['form_product_title'] ); ?>" /> + <br /> + <small><?php _e( 'For example: event tickets, charitable donations, training courses, coaching fees, etc.' ); ?></small> + </p> + <div class="jetpack-simple-payments-image-fieldset"> + <label><?php esc_html_e( 'Product image' ); ?></label> + <div class="placeholder" <?php if ( ! empty( $instance['form_product_image_id'] ) ) echo 'style="display:none;"'; ?>><?php esc_html_e( 'Select an image' ); ?></div> + <div class="jetpack-simple-payments-image" <?php if ( empty( $instance['form_product_image_id'] ) ) echo 'style="display:none;"'; ?>> + <img src="<?php echo esc_url( $instance['form_product_image_src'] ); ?>" /> + <button class="button jetpack-simple-payments-remove-image"><?php esc_html_e( 'Remove image' ); ?></button> + </div> + </div> + <p> + <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_description' ) ); ?>"><?php esc_html_e( 'Description' ); ?></label> + <textarea + class="field-description widefat jetpack-simple-payments-form-product-description" + rows=5 + id="<?php echo esc_attr( $this->get_field_id( 'form_product_description' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_description' ) ); ?>"><?php esc_html_e( $instance['form_product_description'] ); ?></textarea> + </p> + <p class="cost"> + <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_price' ) ); ?>"><?php esc_html_e( 'Price' ); ?></label> + <select + class="field-currency widefat jetpack-simple-payments-form-product-currency" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_currency' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_currency' ) ); ?>"> + <?php foreach( Jetpack_Simple_Payments_Widget::$supported_currency_list as $code => $currency ) {?> + <option value="<?php echo esc_attr( $code ) ?>"<?php selected( $instance['form_product_currency'], $code ); ?>> + <?php esc_html_e( $code . ' ' . $currency ) ?> + </option> + <?php } ?> + </select> + <input + type="text" + class="field-price widefat jetpack-simple-payments-form-product-price" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_price' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_price' ) ); ?>" + value="<?php echo esc_attr( $instance['form_product_price'] ); ?>" + placeholder="1.00" /> + </p> + <p> + <input + class="field-multiple jetpack-simple-payments-form-product-multiple" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_multiple' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_multiple' ) ); ?>" + type="checkbox" + value="1" + <?php checked( $instance['form_product_multiple'], '1' ); ?> /> + <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_multiple' ) ); ?>"><?php esc_html_e( 'Allow people to buy more than one item at a time.' ); ?></label> + </p> + <p> + <label for="<?php echo esc_attr( $this->get_field_id( 'form_product_email' ) ); ?>"><?php esc_html_e( 'Email' ); ?></label> + <input + class="field-email widefat jetpack-simple-payments-form-product-email" + id="<?php echo esc_attr( $this->get_field_id( 'form_product_email' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'form_product_email' ) ); ?>" + type="email" + value="<?php echo esc_attr( $instance['form_product_email'] ); ?>" /> + <small><?php printf( esc_html__( 'This is where PayPal will send your money. To claim a payment, you\'ll need a %1$sPayPal account%2$s connected to a bank account.' ), '<a href="https://paypal.com" target="_blank">', '</a>' ) ?></small> + </p> + <p> + <div class="alignleft"> + <button type="button" class="button-link button-link-delete jetpack-simple-payments-delete-product"><?php _e( 'Delete Product' ); ?></button> + </div> + <div class="alignright"> + <button name="<?php echo $this->get_field_name('save'); ?>" class="button jetpack-simple-payments-save-product"><?php _e( 'Save' ); ?></button> + <span> | <button type="button" class="button-link jetpack-simple-payments-cancel-form"><?php _e( 'Cancel' ); ?></button></span> + </div> + <br class="clear"> + </p> + <hr /> +</div> +<?php } else { ?> +<p class="jetpack-simple-payments-products-warning"> + <?php + echo sprintf( + wp_kses( + __( 'This widget adds a payment button of your choice to your sidebar. To create or edit the payment buttons themselves, <a href="%s">use the Customizer</a>.' ), + array( 'a' => array( 'href' => array() ) ) + ), + esc_url( add_query_arg( array( 'autofocus[panel]' => 'widgets' ), admin_url( 'customize.php' ) ) ) + ); + ?> +</p> +<?php } ?> diff --git a/plugins/jetpack/modules/widgets/simple-payments/style.css b/plugins/jetpack/modules/widgets/simple-payments/style.css new file mode 100644 index 00000000..3a701e01 --- /dev/null +++ b/plugins/jetpack/modules/widgets/simple-payments/style.css @@ -0,0 +1,8 @@ +@media screen and (min-width: 400px) { + .widget.jetpack-simple-payments .jetpack-simple-payments-product { + flex-direction: column; + } + .widget.jetpack-simple-payments .jetpack-simple-payments-details { + padding-left: 0; + } +} diff --git a/plugins/jetpack/modules/widgets/simple-payments/widget.php b/plugins/jetpack/modules/widgets/simple-payments/widget.php new file mode 100644 index 00000000..740cbd74 --- /dev/null +++ b/plugins/jetpack/modules/widgets/simple-payments/widget.php @@ -0,0 +1,25 @@ +<div class='jetpack-simple-payments-wrapper'> + <div class='jetpack-simple-payments-product'> + <div class='jetpack-simple-payments-product-image' <?php if ( empty( $instance['form_product_image_id'] ) ) echo 'style="display:none;"'; ?>> + <div class='jetpack-simple-payments-image'> + <?php echo wp_get_attachment_image( $instance['form_product_image_id'], 'full' ) ?> + </div> + </div> + <div class='jetpack-simple-payments-details'> + <div class='jetpack-simple-payments-title'><p><?php esc_attr_e( $instance['form_product_title'] ); ?></p></div> + <div class='jetpack-simple-payments-description'><p><?php esc_html_e( $instance['form_product_description'] ); ?></p></div> + <div class='jetpack-simple-payments-price'><p><?php esc_attr_e( $instance['form_product_price'] ); ?> <?php esc_attr_e( $instance['form_product_currency'] ); ?></p></div> + <div class='jetpack-simple-payments-purchase-box'> + <?php if ( $instance['form_product_multiple'] ) { ?> + <div class='jetpack-simple-payments-items'> + <input + type='number' + class='jetpack-simple-payments-items-number' + value='1' + min='1' /> + </div> + <?php } ?> + </div> + </div> + </div> +</div> diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget.php b/plugins/jetpack/modules/widgets/wordpress-post-widget.php index 95856d1e..bb465285 100644 --- a/plugins/jetpack/modules/widgets/wordpress-post-widget.php +++ b/plugins/jetpack/modules/widgets/wordpress-post-widget.php @@ -15,6 +15,9 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } +require dirname( __FILE__ ) . '/wordpress-post-widget/class.jetpack-display-posts-widget-base.php'; +require dirname( __FILE__ ) . '/wordpress-post-widget/class.jetpack-display-posts-widget.php'; + add_action( 'widgets_init', 'jetpack_display_posts_widget' ); function jetpack_display_posts_widget() { register_widget( 'Jetpack_Display_Posts_Widget' ); @@ -111,1058 +114,3 @@ function jetpack_display_posts_widget_conditionally_activate_cron() { */ 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 - 'jetpack_display_posts_widget', - /** 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' ), - 'customize_selective_refresh' => true, - ) - ); - - if ( is_customize_preview() ) { - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); - } - } - - /** - * 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 ); - } - - /** - * 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', - __( 'It looks like the WordPress site URL is incorrectly configured. Please check it in your widget settings.', '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 ); - - $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 ); - } - - /** - * 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 ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) { - /** - * 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; - } - - /** - * 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; - } - - $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; - - } - - /** - * 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 action is documented in modules/widgets/gravatar-profile.php */ - do_action( 'jetpack_stats_extra', 'widget_view', 'display_posts' ); - - // Enqueue front end assets. - $this->enqueue_scripts(); - - $content = $args['before_widget']; - - if ( empty( $instance['url'] ) ) { - if ( current_user_can( 'manage_options' ) ) { - $content .= '<p>'; - /* Translators: the "Blog URL" field mentioned is the input field labeled as such in the widget form. */ - $content .= esc_html__( 'The Blog URL is not properly setup in the widget.', 'jetpack' ); - $content .= '</p>'; - } - $content .= $args['after_widget']; - - echo $content; - return; - } - - $data = $this->get_blog_data( $instance['url'] ); - // check for errors - if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) { - $content .= '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>'; - $content .= $args['after_widget']; - - echo $content; - return; - } - - $site_info = $data['site_info']['data']; - - if ( ! empty( $instance['title'] ) ) { - /** This filter is documented in core/src/wp-includes/default-widgets.php */ - $instance['title'] = apply_filters( 'widget_title', $instance['title'] ); - $content .= $args['before_title'] . esc_html( $instance['title'] . ': ' . $site_info->name ) . $args['after_title']; - } - else { - $content .= $args['before_title'] . esc_html( $site_info->name ) . $args['after_title']; - } - - $content .= '<div class="jetpack-display-remote-posts">'; - - if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) { - $content .= '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>'; - $content .= '</div><!-- .jetpack-display-remote-posts -->'; - $content .= $args['after_widget']; - - echo $content; - return; - } - - $posts_list = $data['posts']['data']; - - /** - * 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 ) ); - - 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" rel="noopener"'; - } - $content .= '<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() ); - $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>'; - } - - if ( $instance['show_excerpts'] == true ) { - $content .= $single_post['excerpt']; - } - } - - $content .= '</div><!-- .jetpack-display-remote-posts -->'; - $content .= $args['after_widget']; - - /** - * Filter the WordPress Posts widget content. - * - * @module widgets - * - * @since 4.7.0 - * - * @param string $content Widget content. - */ - echo apply_filters( 'jetpack_display_posts_widget_content', $content ); - } - - /** - * 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' => '', - ); - - - /** - * 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; - } - - /** - * 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. - */ - } - } - - return $errors; - } - - /** - * Enqueue CSS and JavaScript. - * - * @since 4.0.0 - */ - public function enqueue_scripts() { - wp_enqueue_style( 'jetpack_display_posts_widget', plugins_url( 'wordpress-post-widget/style.css', __FILE__ ) ); - } - - /** - * 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 ); - } - - ?> - <p> - <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'jetpack' ); ?></label> - <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 ); ?>" /> - </p> - - <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 ); ?>" /> - <i> - <?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?> - </i> - <?php - /** - * Show an error if the URL field was left empty. - * - * The error is shown only when the widget was already saved. - */ - if ( empty( $url ) && ! preg_match( '/__i__|%i%/', $this->id ) ) { - ?> - <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>'; - } - ?> - </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> - <p> - <label for="<?php echo $this->get_field_id( 'show_excerpts' ); ?>"><?php _e( 'Show Excerpts:', 'jetpack' ); ?></label> - <input type="checkbox" name="<?php echo $this->get_field_name( 'show_excerpts' ); ?>" <?php checked( $show_excerpts, 1 ); ?> /> - </p> - - <?php - - /** - * Show error messages. - */ - if ( ! empty( $update_errors['message'] ) ) { - - /** - * Prepare the error messages. - */ - - $where_message = ''; - switch ( $update_errors['where'] ) { - case 'posts': - $where_message .= __( 'An error occurred while downloading blog 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: - $where_message .= __( 'An error occurred while downloading blog information', 'jetpack' ); - break; - } - - ?> - <p class="error-message"> - <?php echo esc_html( $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 esc_html_e( '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['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : ''; - $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? strip_tags( trim( $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['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 ); - } -} diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php new file mode 100644 index 00000000..8a59545a --- /dev/null +++ b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget-base.php @@ -0,0 +1,843 @@ +<?php + +/* + * For back-compat, the final widget class must be named + * Jetpack_Display_Posts_Widget. + * + * For convenience, it's nice to have a widget class constructor with no + * arguments. Otherwise, we have to register the widget with an instance + * instead of a class name. This makes unregistering annoying. + * + * Both WordPress.com and Jetpack implement the final widget class by + * extending this __Base class and adding data fetching and storage. + * + * This would be a bit cleaner with dependency injection, but we already + * use mocking to test, so it's not a big win. + * + * That this widget is currently implemented as these two classes + * is an implementation detail and should not be depended on :) + */ +abstract class Jetpack_Display_Posts_Widget__Base extends WP_Widget { + /** + * @var string Remote service API URL prefix. + */ + public $service_url = 'https://public-api.wordpress.com/rest/v1.1/'; + + public function __construct() { + parent::__construct( + // internal id + 'jetpack_display_posts_widget', + /** 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' ), + 'customize_selective_refresh' => true, + ) + ); + + if ( is_customize_preview() ) { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + } + } + + /** + * Enqueue CSS and JavaScript. + * + * @since 4.0.0 + */ + public function enqueue_scripts() { + wp_enqueue_style( 'jetpack_display_posts_widget', plugins_url( 'style.css', __FILE__ ) ); + } + + + // DATA STORE: Must implement + + /** + * Gets blog data from the cache. + * + * @param string $site + * + * @return array|WP_Error + */ + abstract public function get_blog_data( $site ); + + /** + * Update a widget instance. + * + * @param string $site The site to fetch the latest data for. + * + * @return array - the new data + */ + abstract public function update_instance( $site ); + + + // WIDGET API + + /** + * Set up the widget display on the front end. + * + * @param array $args + * @param array $instance + */ + public function widget( $args, $instance ) { + /** This action is documented in modules/widgets/gravatar-profile.php */ + do_action( 'jetpack_stats_extra', 'widget_view', 'display_posts' ); + + // Enqueue front end assets. + $this->enqueue_scripts(); + + $content = $args['before_widget']; + + if ( empty( $instance['url'] ) ) { + if ( current_user_can( 'manage_options' ) ) { + $content .= '<p>'; + /* Translators: the "Blog URL" field mentioned is the input field labeled as such in the widget form. */ + $content .= esc_html__( 'The Blog URL is not properly setup in the widget.', 'jetpack' ); + $content .= '</p>'; + } + $content .= $args['after_widget']; + + echo $content; + return; + } + + $data = $this->get_blog_data( $instance['url'] ); + // check for errors + if ( is_wp_error( $data ) || empty( $data['site_info']['data'] ) ) { + $content .= '<p>' . __( 'Cannot load blog information at this time.', 'jetpack' ) . '</p>'; + $content .= $args['after_widget']; + + echo $content; + return; + } + + $site_info = $data['site_info']['data']; + + if ( ! empty( $instance['title'] ) ) { + /** This filter is documented in core/src/wp-includes/default-widgets.php */ + $instance['title'] = apply_filters( 'widget_title', $instance['title'] ); + $content .= $args['before_title'] . esc_html( $instance['title'] . ': ' . $site_info->name ) . $args['after_title']; + } + else { + $content .= $args['before_title'] . esc_html( $site_info->name ) . $args['after_title']; + } + + $content .= '<div class="jetpack-display-remote-posts">'; + + if ( is_wp_error( $data['posts']['data'] ) || empty( $data['posts']['data'] ) ) { + $content .= '<p>' . __( 'Cannot load blog posts at this time.', 'jetpack' ) . '</p>'; + $content .= '</div><!-- .jetpack-display-remote-posts -->'; + $content .= $args['after_widget']; + + echo $content; + return; + } + + $posts_list = $data['posts']['data']; + + /** + * 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 ) ); + + 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" rel="noopener"'; + } + $content .= '<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() ); + $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>'; + } + + if ( $instance['show_excerpts'] == true ) { + $content .= $single_post['excerpt']; + } + } + + $content .= '</div><!-- .jetpack-display-remote-posts -->'; + $content .= $args['after_widget']; + + /** + * Filter the WordPress Posts widget content. + * + * @module widgets + * + * @since 4.7.0 + * + * @param string $content Widget content. + */ + echo apply_filters( 'jetpack_display_posts_widget_content', $content ); + } + + /** + * 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 ); + } + + ?> + <p> + <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:', 'jetpack' ); ?></label> + <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 ); ?>" /> + </p> + + <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 ); ?>" /> + <i> + <?php _e( "Enter a WordPress.com or Jetpack WordPress site URL.", 'jetpack' ); ?> + </i> + <?php + /** + * Show an error if the URL field was left empty. + * + * The error is shown only when the widget was already saved. + */ + if ( empty( $url ) && ! preg_match( '/__i__|%i%/', $this->id ) ) { + ?> + <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>'; + } + ?> + </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> + <p> + <label for="<?php echo $this->get_field_id( 'show_excerpts' ); ?>"><?php _e( 'Show Excerpts:', 'jetpack' ); ?></label> + <input type="checkbox" name="<?php echo $this->get_field_name( 'show_excerpts' ); ?>" <?php checked( $show_excerpts, 1 ); ?> /> + </p> + + <?php + + /** + * Show error messages. + */ + if ( ! empty( $update_errors['message'] ) ) { + + /** + * Prepare the error messages. + */ + + $where_message = ''; + switch ( $update_errors['where'] ) { + case 'posts': + $where_message .= __( 'An error occurred while downloading blog 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: + $where_message .= __( 'An error occurred while downloading blog information', 'jetpack' ); + break; + } + + ?> + <p class="error-message"> + <?php echo esc_html( $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 esc_html_e( '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['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : ''; + $instance['url'] = ( ! empty( $new_instance['url'] ) ) ? strip_tags( trim( $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['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 : ''; + + /** + * 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; + } + + + // DATA PROCESSING + + /** + * 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 ); + } + + /** + * 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', + __( 'It looks like the WordPress site URL is incorrectly configured. Please check it in your widget settings.', '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; + } + + /** + * 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' => '', + ); + + + /** + * 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; + } + + /** + * 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. + */ + } + } + + return $errors; + } + + /** + * 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 ); + } +} diff --git a/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php new file mode 100644 index 00000000..265e2ebb --- /dev/null +++ b/plugins/jetpack/modules/widgets/wordpress-post-widget/class.jetpack-display-posts-widget.php @@ -0,0 +1,274 @@ +<?php + +/* + * Display a list of recent posts from a WordPress.com or Jetpack-enabled blog. + */ + +class Jetpack_Display_Posts_Widget extends Jetpack_Display_Posts_Widget__Base { + /** + * @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'; + + + // DATA STORE + + /** + * 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 ); + + $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; + + } + + /** + * Update a widget instance. + * + * @param string $site The site to fetch the latest data for. + * + * @return array - the new data + */ + 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 ); + } + + return $new_data; + } + + + // WIDGET API + + public function update( $new_instance, $old_instance ) { + $instance = parent::update( $new_instance, $old_instance ); + + /** + * Forcefully activate the update cron when saving widget instance. + * + * So we can be sure that it will be running later. + */ + $this->activate_cron(); + + return $instance; + } + + + // CRON + + /** + * 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 ); + } + + /** + * 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 ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) { + /** + * 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; + } + + /** + * 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; + } + + $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; + + } + + + // MOCKABLES + + /** + * 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 ); + } +} |