diff options
author | Anthony G. Basile <blueness@gentoo.org> | 2018-03-10 19:17:40 -0500 |
---|---|---|
committer | Anthony G. Basile <blueness@gentoo.org> | 2018-03-10 19:17:40 -0500 |
commit | 1ede1db458d07b50cfede5937958cb20752df616 (patch) | |
tree | b7484d24649fb07b8a591148ada617b14d8bbc6d /plugins/jetpack/modules | |
parent | Update akismet 4.0.3 (diff) | |
download | blogs-gentoo-1ede1db458d07b50cfede5937958cb20752df616.tar.gz blogs-gentoo-1ede1db458d07b50cfede5937958cb20752df616.tar.bz2 blogs-gentoo-1ede1db458d07b50cfede5937958cb20752df616.zip |
Update jetpack 5.9
Signed-off-by: Anthony G. Basile <blueness@gentoo.org>
Diffstat (limited to 'plugins/jetpack/modules')
84 files changed, 5334 insertions, 780 deletions
diff --git a/plugins/jetpack/modules/after-the-deadline.php b/plugins/jetpack/modules/after-the-deadline.php index 47af703f..a30b38be 100644 --- a/plugins/jetpack/modules/after-the-deadline.php +++ b/plugins/jetpack/modules/after-the-deadline.php @@ -196,11 +196,43 @@ function AtD_settings() { function AtD_load_javascripts() { if ( AtD_should_load_on_page() ) { - wp_enqueue_script( 'AtD_core', plugins_url( '/after-the-deadline/atd.core.js', __FILE__ ), array(), ATD_VERSION ); - wp_enqueue_script( 'AtD_quicktags', plugins_url( '/after-the-deadline/atd-nonvis-editor-plugin.js', __FILE__ ), array('quicktags'), ATD_VERSION ); - wp_enqueue_script( 'AtD_jquery', plugins_url( '/after-the-deadline/jquery.atd.js', __FILE__ ), array('jquery'), ATD_VERSION ); + wp_enqueue_script( + 'AtD_core', + Jetpack::get_file_url_for_environment( + '_inc/build/after-the-deadline/atd.core.min.js', + 'modules/after-the-deadline/atd.core.js' + ), + array(), + ATD_VERSION + ); + wp_enqueue_script( + 'AtD_quicktags', + Jetpack::get_file_url_for_environment( + '_inc/build/after-the-deadline/atd-nonvis-editor-plugin.min.js', + 'modules/after-the-deadline/atd-nonvis-editor-plugin.js' + ), + array('quicktags'), + ATD_VERSION + ); + wp_enqueue_script( + 'AtD_jquery', + Jetpack::get_file_url_for_environment( + '_inc/build/after-the-deadline/jquery.atd.min.js', + 'modules/after-the-deadline/jquery.atd.js' + ), + array('jquery'), + ATD_VERSION + ); wp_enqueue_script( 'AtD_settings', admin_url() . 'admin-ajax.php?action=atd_settings', array('AtD_jquery'), ATD_VERSION ); - wp_enqueue_script( 'AtD_autoproofread', plugins_url( '/after-the-deadline/atd-autoproofread.js', __FILE__ ), array('AtD_jquery'), ATD_VERSION ); + wp_enqueue_script( + 'AtD_autoproofread', + Jetpack::get_file_url_for_environment( + '_inc/build/after-the-deadline/atd-autoproofread.min.js', + 'modules/after-the-deadline/atd-autoproofread.js' + ), + array('AtD_jquery'), + ATD_VERSION + ); /* load localized strings for AtD */ wp_localize_script( 'AtD_core', 'AtD_l10n_r0ar', array ( diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel.php b/plugins/jetpack/modules/carousel/jetpack-carousel.php index e75b94cd..6b76fb0e 100644 --- a/plugins/jetpack/modules/carousel/jetpack-carousel.php +++ b/plugins/jetpack/modules/carousel/jetpack-carousel.php @@ -197,7 +197,16 @@ class Jetpack_Carousel { function enqueue_assets() { if ( $this->first_run ) { - wp_enqueue_script( 'jetpack-carousel', plugins_url( 'jetpack-carousel.js', __FILE__ ), array( 'jquery.spin' ), $this->asset_version( '20170209' ), true ); + wp_enqueue_script( + 'jetpack-carousel', + Jetpack::get_file_url_for_environment( + '_inc/build/carousel/jetpack-carousel.min.js', + 'modules/carousel/jetpack-carousel.js' + ), + array( 'jquery.spin' ), + $this->asset_version( '20170209' ), + true + ); // Note: using home_url() instead of admin_url() for ajaxurl to be sure to get same domain on wpcom when using mapped domains (also works on self-hosted) // Also: not hardcoding path since there is no guarantee site is running on site root in self-hosted context. @@ -365,7 +374,8 @@ class Jetpack_Carousel { $attachments = get_posts( array( 'include' => array_keys( $selected_images ), 'post_type' => 'any', - 'post_status' => 'any' + 'post_status' => 'any', + 'suppress_filters' => false, ) ); foreach ( $attachments as $attachment ) { @@ -432,7 +442,7 @@ class Jetpack_Carousel { unset( $img_meta['keywords'] ); } - $img_meta = json_encode( array_map( 'strval', $img_meta ) ); + $img_meta = json_encode( array_map( 'strval', array_filter( $img_meta, 'is_scalar' ) ) ); $attr['data-attachment-id'] = $attachment_id; $attr['data-permalink'] = esc_attr( get_permalink( $attachment->ID ) ); diff --git a/plugins/jetpack/modules/comment-likes.php b/plugins/jetpack/modules/comment-likes.php index c6e5ae8e..00c5a0bf 100644 --- a/plugins/jetpack/modules/comment-likes.php +++ b/plugins/jetpack/modules/comment-likes.php @@ -103,7 +103,15 @@ class Jetpack_Comment_Likes { function enqueue_admin_styles_scripts() { wp_enqueue_style( 'comment-like-count', plugins_url( 'comment-likes/admin-style.css', __FILE__ ), array(), JETPACK__VERSION ); - wp_enqueue_script( 'comment-like-count', plugins_url( 'comment-likes/comment-like-count.js', __FILE__ ), array( 'jquery' ), JETPACK__VERSION ); + wp_enqueue_script( + 'comment-like-count', + Jetpack::get_file_url_for_environment( + '_inc/build/comment-likes/comment-like-count.min.js', + 'modules/comment-likes/comment-like-count.js' + ), + array( 'jquery' ), + JETPACK__VERSION + ); } public function add_like_count_column( $columns ) { diff --git a/plugins/jetpack/modules/comments/comments.php b/plugins/jetpack/modules/comments/comments.php index 9160a164..2f317750 100644 --- a/plugins/jetpack/modules/comments/comments.php +++ b/plugins/jetpack/modules/comments/comments.php @@ -112,11 +112,10 @@ class Jetpack_Comments extends Highlander_Comments_Base { // Selfishly remove everything from the existing comment form remove_all_actions( 'comment_form_before' ); - remove_all_actions( 'comment_form_after' ); // Selfishly add only our actions back to the comment form add_action( 'comment_form_before', array( $this, 'comment_form_before' ) ); - add_action( 'comment_form_after', array( $this, 'comment_form_after' ) ); + add_action( 'comment_form_after', array( $this, 'comment_form_after' ), 1 ); // Set very early since we remove everything outputed before our action. // Before a comment is posted add_action( 'pre_comment_on_post', array( $this, 'pre_comment_on_post' ), 1 ); @@ -616,4 +615,4 @@ class Jetpack_Comments extends Highlander_Comments_Base { } } -Jetpack_Comments::init();
\ No newline at end of file +Jetpack_Comments::init(); diff --git a/plugins/jetpack/modules/contact-form/admin.php b/plugins/jetpack/modules/contact-form/admin.php index 24329bcc..e09730a3 100644 --- a/plugins/jetpack/modules/contact-form/admin.php +++ b/plugins/jetpack/modules/contact-form/admin.php @@ -799,7 +799,14 @@ function grunion_enable_spam_recheck() { } // Add the scripts that handle the spam check event. - wp_register_script( 'grunion-admin', plugin_dir_url( __FILE__ ) . 'js/grunion-admin.js', array( 'jquery' ) ); + wp_register_script( + 'grunion-admin', + Jetpack::get_file_url_for_environment( + '_inc/build/contact-form/js/grunion-admin.min.js', + 'modules/contact-form/js/grunion-admin.js' + ), + array( 'jquery' ) + ); wp_enqueue_script( 'grunion-admin' ); wp_enqueue_style( 'grunion.css' ); diff --git a/plugins/jetpack/modules/contact-form/grunion-contact-form.php b/plugins/jetpack/modules/contact-form/grunion-contact-form.php index 3323f5e8..7bb7c47a 100644 --- a/plugins/jetpack/modules/contact-form/grunion-contact-form.php +++ b/plugins/jetpack/modules/contact-form/grunion-contact-form.php @@ -2683,7 +2683,14 @@ class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode { $r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n"; $r .= "\t</div>\n"; - wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) ); + wp_enqueue_script( + 'grunion-frontend', + Jetpack::get_file_url_for_environment( + '_inc/build/contact-form/js/grunion-frontend.min.js', + 'modules/contact-form/js/grunion-frontend.js' + ), + array( 'jquery', 'jquery-ui-datepicker' ) + ); wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' ); // Using Core's built-in datepicker localization routine diff --git a/plugins/jetpack/modules/contact-form/grunion-editor-view.php b/plugins/jetpack/modules/contact-form/grunion-editor-view.php index ecdcf76e..e012894f 100644 --- a/plugins/jetpack/modules/contact-form/grunion-editor-view.php +++ b/plugins/jetpack/modules/contact-form/grunion-editor-view.php @@ -39,7 +39,10 @@ class Grunion_Editor_View { } public static function mce_external_plugins( $plugin_array ) { - $plugin_array['grunion_form'] = plugins_url( 'js/tinymce-plugin-form-button.js', __FILE__ ); + $plugin_array['grunion_form'] = Jetpack::get_file_url_for_environment( + '_inc/build/contact-form/js/tinymce-plugin-form-button.min.js', + 'modules/contact-form/js/tinymce-plugin-form-button.js' + ); return $plugin_array; } @@ -64,7 +67,16 @@ class Grunion_Editor_View { wp_enqueue_style( 'grunion-editor-ui', plugins_url( 'css/editor-ui.css', __FILE__ ) ); wp_style_add_data( 'grunion-editor-ui', 'rtl', 'replace' ); - wp_enqueue_script( 'grunion-editor-view', plugins_url( 'js/editor-view.js', __FILE__ ), array( 'wp-util', 'jquery', 'quicktags' ), false, true ); + wp_enqueue_script( + 'grunion-editor-view', + Jetpack::get_file_url_for_environment( + '_inc/build/contact-form/js/editor-view.min.js', + 'modules/contact-form/js/editor-view.js' + ), + array( 'wp-util', 'jquery', 'quicktags' ), + false, + true + ); wp_localize_script( 'grunion-editor-view', 'grunionEditorView', array( 'inline_editing_style' => plugins_url( 'css/editor-inline-editing-style.css', __FILE__ ), 'inline_editing_style_rtl' => plugins_url( 'css/editor-inline-editing-style-rtl.css', __FILE__ ), diff --git a/plugins/jetpack/modules/contact-form/grunion-form-view.php b/plugins/jetpack/modules/contact-form/grunion-form-view.php index e9c9a736..7417965e 100644 --- a/plugins/jetpack/modules/contact-form/grunion-form-view.php +++ b/plugins/jetpack/modules/contact-form/grunion-form-view.php @@ -14,7 +14,16 @@ */ $max_new_fields = apply_filters( 'grunion_max_new_fields', 5 ); -wp_register_script( 'grunion', GRUNION_PLUGIN_URL . 'js/grunion.js', array( 'jquery-ui-sortable', 'jquery-ui-draggable' ), JETPACK__VERSION ); +wp_register_script( + 'grunion', + Jetpack::get_file_url_for_environment( + '_inc/build/contact-form/js/grunion.min.js', + 'modules/contact-form/js/grunion.js' + ), + array( 'jquery-ui-sortable', 'jquery-ui-draggable' ), + JETPACK__VERSION +); + wp_localize_script( 'grunion', 'GrunionFB_i18n', array( 'nameLabel' => esc_attr( _x( 'Name', 'Label for HTML form "Name" field in contact form builder', 'jetpack' ) ), 'emailLabel' => esc_attr( _x( 'Email', 'Label for HTML form "Email" field in contact form builder', 'jetpack' ) ), diff --git a/plugins/jetpack/modules/custom-css/csstidy/data.inc.php b/plugins/jetpack/modules/custom-css/csstidy/data.inc.php index f65869a6..9ed2e1c4 100644 --- a/plugins/jetpack/modules/custom-css/csstidy/data.inc.php +++ b/plugins/jetpack/modules/custom-css/csstidy/data.inc.php @@ -424,8 +424,26 @@ $GLOBALS['csstidy']['all_properties']['font-stretch'] = 'CSS2.0,CSS3.0'; $GLOBALS['csstidy']['all_properties']['font-style'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0'; $GLOBALS['csstidy']['all_properties']['font-variant'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0'; $GLOBALS['csstidy']['all_properties']['font-weight'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-area'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-auto-columns'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-auto-flow'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-auto-rows'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-column'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['grid-columns'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-column-end'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-column-gap'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-column-start'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-gap'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-row'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['grid-rows'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-row-end'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-row-gap'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-row-start'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-template'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-template-areas'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-template-columns'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['grid-template-rows'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['hanging-punctuation'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['height'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0'; $GLOBALS['csstidy']['all_properties']['hyphenate-after'] = 'CSS3.0'; @@ -440,6 +458,8 @@ $GLOBALS['csstidy']['all_properties']['image-rendering'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['image-resolution'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['inline-box-align'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['justify-content'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['justify-items'] = 'CSS3.0'; +$GLOBALS['csstidy']['all_properties']['justify-self'] = 'CSS3.0'; $GLOBALS['csstidy']['all_properties']['left'] = 'CSS2.0,CSS2.1,CSS3.0'; $GLOBALS['csstidy']['all_properties']['letter-spacing'] = 'CSS1.0,CSS2.0,CSS2.1,CSS3.0'; $GLOBALS['csstidy']['all_properties']['line-break'] = 'CSS3.0'; @@ -670,4 +690,4 @@ $GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class= $GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span>'."\n"; // after comment $GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n"; -require dirname( __FILE__ ) . '/data-wp.inc.php';
\ No newline at end of file +require dirname( __FILE__ ) . '/data-wp.inc.php'; diff --git a/plugins/jetpack/modules/custom-css/custom-css-4.7.php b/plugins/jetpack/modules/custom-css/custom-css-4.7.php index 259bb79a..9e8fe4d8 100644 --- a/plugins/jetpack/modules/custom-css/custom-css-4.7.php +++ b/plugins/jetpack/modules/custom-css/custom-css-4.7.php @@ -67,15 +67,30 @@ class Jetpack_Custom_CSS_Enhancements { wp_register_style( 'jetpack-customizer-css', plugins_url( 'custom-css/css/customizer-control.css', __FILE__ ), $deps, '20140728' ); wp_register_script( 'jetpack-codemirror', plugins_url( 'custom-css/js/codemirror.min.js', __FILE__ ), array(), '3.16', true ); $deps = array( 'customize-controls', 'underscore' ); - $src = plugins_url( 'custom-css/js/core-customizer-css.core-4.9.js', __FILE__ ); + $src = Jetpack::get_file_url_for_environment( + '_inc/build/custom-css/custom-css/js/core-customizer-css.core-4.9.min.js', + 'modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js' + ); if ( ! function_exists( 'wp_enqueue_code_editor' ) ) { // If Core < 4.9 $deps[] = 'jetpack-codemirror'; - $src = plugins_url( 'custom-css/js/core-customizer-css.js', __FILE__ ); + $src = Jetpack::get_file_url_for_environment( + '_inc/build/custom-css/custom-css/js/core-customizer-css.min.js', + 'modules/custom-css/custom-css/js/core-customizer-css.js' + ); } wp_register_script( 'jetpack-customizer-css', $src, $deps, JETPACK__VERSION, true ); - wp_register_script( 'jetpack-customizer-css-preview', plugins_url( 'custom-css/js/core-customizer-css-preview.js', __FILE__ ), array( 'customize-selective-refresh' ), JETPACK__VERSION, true ); + wp_register_script( + 'jetpack-customizer-css-preview', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-css/custom-css/js/core-customizer-css-preview.min.js', + 'modules/custom-css/custom-css/js/core-customizer-css-preview.js' + ), + array( 'customize-selective-refresh' ), + JETPACK__VERSION, + true + ); remove_action( 'wp_head', 'wp_custom_css_cb', 11 ); // 4.7.0 had it at 11, 4.7.1 moved it to 101. remove_action( 'wp_head', 'wp_custom_css_cb', 101 ); diff --git a/plugins/jetpack/modules/custom-css/custom-css.php b/plugins/jetpack/modules/custom-css/custom-css.php index f5e91f09..77570e9b 100644 --- a/plugins/jetpack/modules/custom-css/custom-css.php +++ b/plugins/jetpack/modules/custom-css/custom-css.php @@ -930,7 +930,16 @@ class Jetpack_Custom_CSS { return; wp_enqueue_script( 'postbox' ); - wp_enqueue_script( 'custom-css-editor', plugins_url( 'custom-css/js/css-editor.js', __FILE__ ), 'jquery', '20130325', true ); + wp_enqueue_script( + 'custom-css-editor', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-css/custom-css/js/css-editor.min.js', + 'modules/custom-css/custom-css/js/css-editor.js' + ), + 'jquery', + '20130325', + true + ); wp_enqueue_style( 'custom-css-editor', plugins_url( 'custom-css/css/css-editor.css', __FILE__ ) ); if ( defined( 'SAFECSS_USE_ACE' ) && SAFECSS_USE_ACE ) { @@ -938,7 +947,16 @@ class Jetpack_Custom_CSS { wp_enqueue_style( 'jetpack-css-use-codemirror', plugins_url( 'custom-css/css/use-codemirror.css', __FILE__ ), array( 'jetpack-css-codemirror' ), '20120905' ); wp_register_script( 'jetpack-css-codemirror', plugins_url( 'custom-css/js/codemirror.min.js', __FILE__ ), array(), '3.16', true ); - wp_enqueue_script( 'jetpack-css-use-codemirror', plugins_url( 'custom-css/js/use-codemirror.js', __FILE__ ), array( 'jquery', 'underscore', 'jetpack-css-codemirror' ), '20131009', true ); + wp_enqueue_script( + 'jetpack-css-use-codemirror', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-css/custom-css/js/use-codemirror.min.js', + 'modules/custom-css/custom-css/js/use-codemirror.js' + ), + array( 'jquery', 'underscore', 'jetpack-css-codemirror' ), + '20131009', + true + ); } } diff --git a/plugins/jetpack/modules/custom-post-types/comics.php b/plugins/jetpack/modules/custom-post-types/comics.php index a1d8bf78..6ff4fbad 100644 --- a/plugins/jetpack/modules/custom-post-types/comics.php +++ b/plugins/jetpack/modules/custom-post-types/comics.php @@ -176,7 +176,14 @@ class Jetpack_Comic { wp_enqueue_style( 'jetpack-comics-style', plugins_url( 'comics/comics.css', __FILE__ ) ); wp_style_add_data( 'jetpack-comics-style', 'rtl', 'replace' ); - wp_enqueue_script( 'jetpack-comics', plugins_url( 'comics/comics.js', __FILE__ ), array( 'jquery', 'jquery.spin' ) ); + wp_enqueue_script( + 'jetpack-comics', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-post-types/comics/comics.min.js', + 'modules/custom-post-types/comics/comics.js' + ), + array( 'jquery', 'jquery.spin' ) + ); $options = array( 'nonce' => wp_create_nonce( 'jetpack_comic_upload_nonce' ), diff --git a/plugins/jetpack/modules/custom-post-types/nova.php b/plugins/jetpack/modules/custom-post-types/nova.php index 33d5bce9..8024825c 100644 --- a/plugins/jetpack/modules/custom-post-types/nova.php +++ b/plugins/jetpack/modules/custom-post-types/nova.php @@ -442,7 +442,16 @@ class Nova_Restaurant { $this->setup_menu_item_columns(); - wp_register_script( 'nova-menu-checkboxes', plugins_url( 'js/menu-checkboxes.js', __FILE__ ), array( 'jquery' ), $this->version, true ); + wp_register_script( + 'nova-menu-checkboxes', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-post-types/js/menu-checkboxes.min.js', + 'modules/custom-post-types/js/menu-checkboxes.js' + ), + array( 'jquery' ), + $this->version, + true + ); } @@ -610,7 +619,17 @@ class Nova_Restaurant { $this->maybe_reorder_menu_items(); - wp_enqueue_script( 'nova-drag-drop', plugins_url( 'js/nova-drag-drop.js', __FILE__ ), array( 'jquery-ui-sortable' ), $this->version, true ); + wp_enqueue_script( + 'nova-drag-drop', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-post-types/js/nova-drag-drop.min.js', + 'modules/custom-post-types/js/nova-drag-drop.js' + ), + array( 'jquery-ui-sortable' ), + $this->version, + true + ); + wp_localize_script( 'nova-drag-drop', '_novaDragDrop', array( 'nonce' => wp_create_nonce( 'drag-drop-reorder' ), 'nonceName' => 'drag-drop-reorder', @@ -847,7 +866,16 @@ class Nova_Restaurant { } function enqueue_many_items_scripts() { - wp_enqueue_script( 'nova-many-items', plugins_url( 'js/many-items.js', __FILE__ ), array( 'jquery' ), $this->version, true ); + wp_enqueue_script( + 'nova-many-items', + Jetpack::get_file_url_for_environment( + '_inc/build/custom-post-types/js/many-items.min.js', + 'modules/custom-post-types/js/many-items.js' + ), + array( 'jquery' ), + $this->version, + true + ); } function process_form_request() { diff --git a/plugins/jetpack/modules/custom-post-types/portfolios.php b/plugins/jetpack/modules/custom-post-types/portfolios.php index 7d40510f..416aea81 100644 --- a/plugins/jetpack/modules/custom-post-types/portfolios.php +++ b/plugins/jetpack/modules/custom-post-types/portfolios.php @@ -252,6 +252,8 @@ class Jetpack_Portfolio { 'comments', 'publicize', 'wpcom-markdown', + 'revisions', + 'excerpt', ), 'rewrite' => array( 'slug' => 'portfolio', diff --git a/plugins/jetpack/modules/custom-post-types/testimonial.php b/plugins/jetpack/modules/custom-post-types/testimonial.php index c2006731..6a7426cc 100644 --- a/plugins/jetpack/modules/custom-post-types/testimonial.php +++ b/plugins/jetpack/modules/custom-post-types/testimonial.php @@ -319,6 +319,7 @@ class Jetpack_Testimonial { 'thumbnail', 'page-attributes', 'revisions', + 'excerpt', ), 'rewrite' => array( 'slug' => 'testimonial', diff --git a/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php index 58311ed4..1b85567e 100644 --- a/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php +++ b/plugins/jetpack/modules/google-analytics/classes/wp-google-analytics-legacy.php @@ -102,22 +102,19 @@ class Jetpack_Google_Analytics_Legacy { $custom_vars = apply_filters( 'jetpack_wga_classic_custom_vars', $custom_vars ); // Ref: https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingEcommerce#Example - $async_code = "<!-- Jetpack Google Analytics --> + printf( + "<!-- Jetpack Google Analytics --> <script type='text/javascript'> var _gaq = _gaq || []; - %custom_vars% - + %s (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + ga.src = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); - </script>"; - - $custom_vars_string = implode( "\r\n", $custom_vars ); - $async_code = str_replace( '%custom_vars%', $custom_vars_string, $async_code ); - - echo "$async_code\r\n"; + </script>\r\n", + implode( "\r\n", $custom_vars ) + ); } /** diff --git a/plugins/jetpack/modules/holiday-snow/snowstorm.js b/plugins/jetpack/modules/holiday-snow/snowstorm.js index 96fd609d..2ff6320e 100644 --- a/plugins/jetpack/modules/holiday-snow/snowstorm.js +++ b/plugins/jetpack/modules/holiday-snow/snowstorm.js @@ -257,6 +257,8 @@ var snowStorm = (function(window, document) { this.active = 1; this.fontSize = (10+(this.type/5)*10); this.o = document.createElement('div'); + this.o.setAttribute('aria-hidden', 'true'); + this.o.setAttribute('role', 'presentation'); this.o.innerHTML = storm.snowCharacter; this.o.style.color = storm.snowColor; this.o.style.position = (fixedForEverything?'fixed':'absolute'); @@ -536,4 +538,4 @@ var snowStorm = (function(window, document) { return this; -}(window, document));
\ No newline at end of file +}(window, document)); diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.php b/plugins/jetpack/modules/infinite-scroll/infinity.php index 06223d15..4d3f05f6 100644 --- a/plugins/jetpack/modules/infinite-scroll/infinity.php +++ b/plugins/jetpack/modules/infinite-scroll/infinity.php @@ -406,7 +406,16 @@ class The_Neverending_Home_Page { return; // Add our scripts. - wp_register_script( 'the-neverending-homepage', plugins_url( 'infinity.js', __FILE__ ), array( 'jquery' ), '4.0.0', true ); + wp_register_script( + 'the-neverending-homepage', + Jetpack::get_file_url_for_environment( + '_inc/build/infinite-scroll/infinity.min.js', + 'modules/infinite-scroll/infinity.js' + ), + array( 'jquery' ), + '4.0.0', + true + ); // Add our default styles. wp_register_style( 'the-neverending-homepage', plugins_url( 'infinity.css', __FILE__ ), array(), '20140422' ); diff --git a/plugins/jetpack/modules/lazy-images.php b/plugins/jetpack/modules/lazy-images.php index d0dd0a38..a0c498cc 100644 --- a/plugins/jetpack/modules/lazy-images.php +++ b/plugins/jetpack/modules/lazy-images.php @@ -2,7 +2,7 @@ /** * Module Name: Lazy Images - * Module Description: Improve performance by loading images just before they scroll into view + * Module Description: Lazy load images * Sort Order: 24 * Recommendation Order: 14 * First Introduced: 5.6.0 @@ -10,7 +10,7 @@ * Auto Activate: No * Module Tags: Appearance, Recommended * Feature: Appearance - * Additional Search Queries: mobile, theme, performance + * Additional Search Queries: mobile, theme, performance, image */ /** @@ -24,4 +24,16 @@ */ require_once( JETPACK__PLUGIN_DIR . 'modules/lazy-images/lazy-images.php' ); -Jetpack_Lazy_Images::instance(); + +/* + * Initialize lazy images on the wp action so that conditional + * tags are safe to use. + * + * As an example, this is important if a theme wants to disable lazy images except + * on single posts, pages, or attachments by short-circuiting lazy images when + * is_singular() returns false. + * + * See: https://github.com/Automattic/jetpack/issues/8888 + */ + +add_action( 'wp', array( 'Jetpack_Lazy_Images', 'instance' ) ); diff --git a/plugins/jetpack/modules/lazy-images/js/lazy-images.js b/plugins/jetpack/modules/lazy-images/js/lazy-images.js index bc117e9b..08ff18ce 100644 --- a/plugins/jetpack/modules/lazy-images/js/lazy-images.js +++ b/plugins/jetpack/modules/lazy-images/js/lazy-images.js @@ -121,36 +121,736 @@ var jetpackLazyImagesModule = function( $ ) { }; /** - * The following is an Intersection observer polyfill which is licensed under MIT - * and can be found at https://github.com/que-etc/intersection-observer-polyfill + * The following is an Intersection observer polyfill which is licensed under + * the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE and can be found at: + * https://github.com/w3c/IntersectionObserver/tree/master/polyfill */ -/* - * The MIT License (MIT) - * - * Copyright (c) 2016 Denis Rul +/* jshint ignore:start */ +/** + * Copyright 2016 Google Inc. All Rights Reserved. * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: + * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE. * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. */ -/* jshint ignore:start */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.IntersectionObserver=e():t.IntersectionObserver=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var o=r[n]={exports:{},id:n,loaded:!1};return t[n].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=r(1),i=n(o),s=void 0;s="function"==typeof window.IntersectionObserver?window.IntersectionObserver:i["default"],e["default"]=s,t.exports=e["default"]},function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=r(2),u=r(3),c=n(u),l=r(5),h=n(l),f=new c["default"],p=new a.WeakMap,d=function(){function t(e,r){if(o(this,t),!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=new h["default"](e,r,f,this);Object.defineProperties(this,{root:{value:n.root},thresholds:{value:n.thresholds},rootMargin:{value:n.rootMargin}}),p.set(this,n)}return s(t,null,[{key:"idleTimeout",get:function(){return f.idleTimeout},set:function(t){if("number"!=typeof t)throw new TypeError('type of "idleTimeout" value must be a number.');if(("undefined"==typeof t?"undefined":i(t))<0)throw new TypeError('"idleTimeout" value must be greater than 0.');f.idleTimeout=t}},{key:"trackHovers",get:function(){return f.isHoverEnabled()},set:function(t){if("boolean"!=typeof t)throw new TypeError('type of "trackHovers" value must be a boolean.');t?f.enableHover():f.disableHover()}}]),t}();["observe","unobserve","disconnect","takeRecords"].forEach(function(t){d.prototype[t]=function(){var e;return(e=p.get(this))[t].apply(e,arguments)}}),e["default"]=d,t.exports=e["default"]},function(t,e){"use strict";function r(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),s="function"==typeof window.WeakMap&&"function"==typeof window.Map,a=function(){function t(t,e){var r=-1;return t.some(function(t,n){var o=t[0]===e;return o&&(r=n),o}),r}return s?window.WeakMap:function(){function e(){o(this,e),this.__entries__=[]}return e.prototype.get=function(e){var r=t(this.__entries__,e);return this.__entries__[r][1]},e.prototype.set=function(e,r){var n=t(this.__entries__,e);~n?this.__entries__[n][1]=r:this.__entries__.push([e,r])},e.prototype["delete"]=function(e){var r=this.__entries__,n=t(r,e);~n&&r.splice(n,1)},e.prototype.has=function(e){return!!~t(this.__entries__,e)},e}()}(),u=function(){return s?window.Map:function(t){function e(){return o(this,e),r(this,t.apply(this,arguments))}return n(e,t),e.prototype.clear=function(){this.__entries__.splice(0,this.__entries__.length)},e.prototype.entries=function(){return this.__entries__.slice()},e.prototype.keys=function(){return this.__entries__.map(function(t){return t[0]})},e.prototype.values=function(){return this.__entries__.map(function(t){return t[1]})},e.prototype.forEach=function(t){for(var e=arguments.length<=1||void 0===arguments[1]?null:arguments[1],r=this.__entries__,n=Array.isArray(r),o=0,r=n?r:r[Symbol.iterator]();;){var i;if(n){if(o>=r.length)break;i=r[o++]}else{if(o=r.next(),o.done)break;i=o.value}var s=i;t.call(e,s[1],s[0])}},i(e,[{key:"size",get:function(){return this.__entries__.length}}]),e}(a)}();e.Map=u,e.WeakMap=a},function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t){var e=arguments.length<=1||void 0===arguments[1]?0:arguments[1],r=!1;return function(){for(var n=this,o=arguments.length,i=Array(o),s=0;s<o;s++)i[s]=arguments[s];r!==!1&&clearTimeout(r),r=setTimeout(function(){r=!1,t.apply(n,i)},e)}}Object.defineProperty(e,"__esModule",{value:!0});var s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=r(4),u=n(a),c="function"==typeof window.MutationObserver,l=function(){return window.requestAnimationFrame?window.requestAnimationFrame:function(t){return setTimeout(function(){return t((0,u["default"])())},1e3/60)}}(),h=function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?50:arguments[0],r=!(arguments.length<=1||void 0===arguments[1])&&arguments[1];o(this,t),this._idleTimeout=e,this._trackHovers=r,this._cycleStartTime=-1,this._isUpdateScheduled=!1,this._repeatCycle=!1,this._hoverInitiated=!1,this._mutationsObserver=null,this._isListening=!1,this._observers=[],this.startUpdateCycle=this.startUpdateCycle.bind(this),this.scheduleUpdate=this.scheduleUpdate.bind(this),this._onMutation=this._onMutation.bind(this),this._repeatHandler=i(this.scheduleUpdate,200),this._onMouseOver=i(this.startUpdateCycle,200)}return t.prototype.connect=function(t){this.isConnected(t)||this._observers.push(t),this._isListening||this._initListeners()},t.prototype.disconnect=function(t){var e=this._observers,r=e.indexOf(t);~r&&e.splice(r,1),!e.length&&this._isListening&&this._removeListeners()},t.prototype.isConnected=function(t){return!!~this._observers.indexOf(t)},t.prototype._updateObservers=function(){for(var t=!1,e=this._observers,r=Array.isArray(e),n=0,e=r?e:e[Symbol.iterator]();;){var o;if(r){if(n>=e.length)break;o=e[n++]}else{if(n=e.next(),n.done)break;o=n.value}var i=o;i.updateObservations()&&(t=!0),i.hasEntries()&&i.notifySubscriber()}return t},t.prototype.startUpdateCycle=function(){this._cycleStartTime=(0,u["default"])(),this.scheduleUpdate()},t.prototype.scheduleUpdate=function(t){var e="number"==typeof t;if(e){var r=this._updateObservers();if(this._isUpdateScheduled=!1,!this._wasCycleStarted())return;r?this.startUpdateCycle():this._hasIdleTimeEnded()?this._onCycleEnded():this.scheduleUpdate()}else this._isUpdateScheduled||(l(this.scheduleUpdate),this._isUpdateScheduled=!0)},t.prototype._hasIdleTimeEnded=function(){return(0,u["default"])()-this._cycleStartTime>this._idleTimeout},t.prototype._wasCycleStarted=function(){return this._cycleStartTime!==-1},t.prototype._onCycleEnded=function(){this._cycleStartTime=-1,this._repeatCycle&&(this._cycleStartTime=0,this._repeatHandler())},t.prototype._initListeners=function(){this._isListening||(this._isListening=!0,window.addEventListener("resize",this.startUpdateCycle,!0),window.addEventListener("scroll",this.scheduleUpdate,!0),this._trackHovers&&this._addHoverListener(),c?(this._mutationsObserver=new MutationObserver(this._onMutation),this._mutationsObserver.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(this._repeatCycle=!0,window.addEventListener("click",this.startUpdateCycle,!0),this.startUpdateCycle()))},t.prototype._removeListeners=function(){this._isListening&&(window.removeEventListener("resize",this.startUpdateCycle,!0),window.removeEventListener("scroll",this.scheduleUpdate,!0),this._removeHoverListener(),c?this._mutationsObserver&&(this._mutationsObserver.disconnect(),this._mutationsObserver=null):(this._repeatCycle=!1,window.removeEventListener("click",this.startUpdateCycle,!0)),this._isListening=!1)},t.prototype.enableHover=function(){this._trackHovers=!0,this._isListening&&this._addHoverListener()},t.prototype.disableHover=function(){this._trackHovers=!1,this._removeHoverListener()},t.prototype.isHoverEnabled=function(){return this._trackHovers},t.prototype._addHoverListener=function(){this._hoverInitiated||(window.addEventListener("mouseover",this._onMouseOver,!0),this._hoverInitiated=!0)},t.prototype._removeHoverListener=function(){this._hoverInitiated&&(window.removeEventListener("mouseover",this._onMouseOver,!0),this._hoverInitiated=!1)},t.prototype._onMutation=function(t){var e=t.every(function(t){return"attributes"!==t.type});e?this.scheduleUpdate():this.startUpdateCycle()},s(t,[{key:"idleTimeout",get:function(){return this._idleTimeout},set:function(t){this._idleTimeout=t}}]),t}();e["default"]=h,t.exports=e["default"]},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=function(){return window.performance&&window.performance.now?function(){return window.performance.now()}:function(){return Date.now()}}(),t.exports=e["default"]},function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){var t=arguments.length<=0||void 0===arguments[0]?0:arguments[0],e=t;return Array.isArray(t)?t.length||(e=[0]):e=[t],e.map(function(t){if(t=Number(t),!window.isFinite(t))throw new TypeError("The provided double value is non-finite.");if(t<0||t>1)throw new RangeError("Threshold values must be between 0 and 1.");return t}).sort()}function s(){var t=arguments.length<=0||void 0===arguments[0]?"0px":arguments[0];if(t=(t+"").split(/\s+/),t.length>4)throw new Error("Extra text found at the end of rootMargin.");t[0]=t[0]||"0px",t[1]=t[1]||t[0],t[2]=t[2]||t[0],t[3]=t[3]||t[1];var e=t.join(" "),r=t.map(function(t){var e=/^(-?\d*\.?\d+)(px|%)$/.exec(t)||[],r=e[1],n=e[2],o="px"===n;if(r=parseFloat(r),!window.isFinite(r))throw new Error("rootMargin must be specified in pixels or percent.");return o||(r/=100),{value:r,pixels:o}});return{rawData:e,parsedData:r}}function a(t,e){e=e.map(function(e,r){var n=e.value;return e.pixels||(n*=r%2?t.width:t.height),n});var r={top:t.top-e[0],right:t.right+e[1],bottom:t.bottom+e[2],left:t.left-e[3]};return r.width=r.right-r.left,r.height=r.bottom-r.top,r}Object.defineProperty(e,"__esModule",{value:!0});var u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},c=r(2),l=r(6),h=r(7),f=n(h),p=function(){function t(e){var r=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=arguments[2],a=arguments[3];if(o(this,t),"function"!=typeof e)throw new TypeError("The callback provided as parameter 1 is not a function.");if("object"!==("undefined"==typeof r?"undefined":u(r)))throw new TypeError("parameter 2 is not an object.");if("root"in r&&!(r.root instanceof Element))throw new TypeError("member root is not of type Element.");var l=i(r.threshold),h=s(r.rootMargin);this.root=r.root||null,this.rootMargin=h.rawData,this.thresholds=Object.freeze(l),this._root=r.root||document.documentElement,this._callback=e,this._rootMargin=h.parsedData,this._targets=new c.Map,this._quedEntries=[],this._publicObserver=a||this,this.controller=n}return t.prototype.observe=function(t){if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");if(!(t instanceof Element))throw new TypeError('parameter 1 is not of type "Element".');var e=this._targets;e.has(t)||(e.set(t,new f["default"](t,this)),this.controller.isConnected(this)||this.controller.connect(this),this.controller.startUpdateCycle())},t.prototype.unobserve=function(t){if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");if(!(t instanceof Element))throw new TypeError('parameter 1 is not of type "Element".');var e=this._targets;e.has(t)&&e["delete"](t),e.size||this.disconnect()},t.prototype.disconnect=function(){this._targets.clear(),this.controller.disconnect(this)},t.prototype.takeRecords=function(){return this._quedEntries.splice(0)},t.prototype.notifySubscriber=function(){var t=this.takeRecords(),e=this._publicObserver;t.length&&this._callback.call(e,t,e)},t.prototype.queueEntry=function(t){this._quedEntries.push(t)},t.prototype.hasEntries=function(){return!!this._quedEntries.length},t.prototype.updateObservations=function(){var t=this._root,e=this.getRootRect(),r=!1;return this._targets.forEach(function(n){var o=n.updateIntersection(t,e);(o.ratioChanged||o.targetRectChanged)&&(r=!0)}),r},t.prototype.getThresholdGreaterThan=function(t){for(var e=this.thresholds,r=e.length,n=0;n<r&&e[n]<=t;)++n;return n},t.prototype.getRootRect=function(){var t=(0,l.getRectangle)(this._root);return a(t,this._rootMargin)},t}();e["default"]=p,t.exports=e["default"]},function(t,e){"use strict";function r(t){for(var e={},r=Object.keys(t),n=Array.isArray(r),o=0,r=n?r:r[Symbol.iterator]();;){var i;if(n){if(o>=r.length)break;i=r[o++]}else{if(o=r.next(),o.done)break;i=o.value}var s=i;e[s]={value:t[s]}}return Object.defineProperties({},e)}function n(){var t=arguments.length<=0||void 0===arguments[0]?0:arguments[0],e=arguments.length<=1||void 0===arguments[1]?0:arguments[1],r=arguments.length<=2||void 0===arguments[2]?0:arguments[2],n=arguments.length<=3||void 0===arguments[3]?0:arguments[3];return{left:t,top:e,width:r,height:n,bottom:e+n,right:t+r}}function o(t){return t===document.documentElement?n(0,0,t.clientWidth,t.clientHeight):t.getBoundingClientRect()}function i(t){return t.width*t.height}function s(t){return 0===t.height&&0===t.width}function a(t,e){return t.top===e.top&&t.left===e.left&&t.right===e.right&&t.bottom===e.bottom}Object.defineProperty(e,"__esModule",{value:!0}),e.mapToClientRect=r,e.createRectangle=n,e.getRectangle=o,e.getArea=i,e.isEmpty=s,e.isEqual=a},function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){var r=document.documentElement;return t!==r&&!r.contains(t)||!t.contains(e)}function s(t,e){var r=Math.max(e.left,t.left),n=Math.min(e.right,t.right),o=Math.max(e.top,t.top),i=Math.min(e.bottom,t.bottom),s=n-r,a=i-o;return(0,l.createRectangle)(r,o,s,a)}function a(t,e,r,n){for(var o=n,i=e.parentNode,a=!1;!a;){var u=null;i===t||1!==i.nodeType?(a=!0,u=r):"visible"!==window.getComputedStyle(i).overflow&&(u=(0,l.getRectangle)(i)),u&&(o=s(o,u)),i=i.parentNode}return o}Object.defineProperty(e,"__esModule",{value:!0});var u=r(4),c=n(u),l=r(6),h=r(8),f=n(h),p=(0,l.createRectangle)(),d=function(){function t(e,r){o(this,t),this.target=e,this.observer=r,this.prevTargetRect=p,this.prevThreshold=0,this.prevRatio=0}return t.prototype.updateIntersection=function(t,e){var r=(0,l.getRectangle)(this.target),n=this.getIntersectionData(t,e,r),o=+n.exists,i=n.ratio!==this.prevRatio,s=!(0,l.isEqual)(r,this.prevTargetRect),a=void 0;if(n.exists&&!(0,l.isEmpty)(r)&&(o=this.observer.getThresholdGreaterThan(n.ratio)),a=o!==this.prevThreshold,this.prevTargetRect=r,this.prevThreshold=o,this.prevRatio=n.ratio,n.exists||(n.ratio=0,n.rect=p),a){var u=new f["default"](this.target,r,n.rect,n.ratio,e,(0,c["default"])());this.observer.queueEntry(u)}return{ratioChanged:i,thresholdChanged:a,targetRectChanged:s}},t.prototype.getIntersectionData=function(t,e,r){var n=this.target;r||(r=(0,l.getRectangle)(this.target)),e||(e=(0,l.getRectangle)(t));var o=i(t,n),s=o?p:a(t,n,e,r),u=!o&&s.width>=0&&s.height>=0,c=(0,l.getArea)(s)/(0,l.getArea)(r)||0;return{rect:s,ratio:c,exists:u}},t}();e["default"]=d,t.exports=e["default"]},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var o=r(6),i=function s(t,e,r,i,a,u){n(this,s),Object.defineProperties(this,{boundingClientRect:{value:e},intersectionRatio:{value:i},intersectionRect:{value:(0,o.mapToClientRect)(r)},rootBounds:{value:(0,o.mapToClientRect)(a)},target:{value:t},time:{value:u}})};e["default"]=i,t.exports=e["default"]}])}); +(function(window, document) { + 'use strict'; + + + // Exits early if all IntersectionObserver and IntersectionObserverEntry + // features are natively supported. + if ('IntersectionObserver' in window && + 'IntersectionObserverEntry' in window && + 'intersectionRatio' in window.IntersectionObserverEntry.prototype) { + + // Minimal polyfill for Edge 15's lack of `isIntersecting` + // See: https://github.com/w3c/IntersectionObserver/issues/211 + if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) { + Object.defineProperty(window.IntersectionObserverEntry.prototype, + 'isIntersecting', { + get: function () { + return this.intersectionRatio > 0; + } + }); + } + return; + } + + + /** + * An IntersectionObserver registry. This registry exists to hold a strong + * reference to IntersectionObserver instances currently observering a target + * element. Without this registry, instances without another reference may be + * garbage collected. + */ + var registry = []; + + + /** + * Creates the global IntersectionObserverEntry constructor. + * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry + * @param {Object} entry A dictionary of instance properties. + * @constructor + */ + function IntersectionObserverEntry(entry) { + this.time = entry.time; + this.target = entry.target; + this.rootBounds = entry.rootBounds; + this.boundingClientRect = entry.boundingClientRect; + this.intersectionRect = entry.intersectionRect || getEmptyRect(); + this.isIntersecting = !!entry.intersectionRect; + + // Calculates the intersection ratio. + var targetRect = this.boundingClientRect; + var targetArea = targetRect.width * targetRect.height; + var intersectionRect = this.intersectionRect; + var intersectionArea = intersectionRect.width * intersectionRect.height; + + // Sets intersection ratio. + if (targetArea) { + this.intersectionRatio = intersectionArea / targetArea; + } else { + // If area is zero and is intersecting, sets to 1, otherwise to 0 + this.intersectionRatio = this.isIntersecting ? 1 : 0; + } + } + + + /** + * Creates the global IntersectionObserver constructor. + * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface + * @param {Function} callback The function to be invoked after intersection + * changes have queued. The function is not invoked if the queue has + * been emptied by calling the `takeRecords` method. + * @param {Object=} opt_options Optional configuration options. + * @constructor + */ + function IntersectionObserver(callback, opt_options) { + + var options = opt_options || {}; + + if (typeof callback != 'function') { + throw new Error('callback must be a function'); + } + + if (options.root && options.root.nodeType != 1) { + throw new Error('root must be an Element'); + } + + // Binds and throttles `this._checkForIntersections`. + this._checkForIntersections = throttle( + this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT); + + // Private properties. + this._callback = callback; + this._observationTargets = []; + this._queuedEntries = []; + this._rootMarginValues = this._parseRootMargin(options.rootMargin); + + // Public properties. + this.thresholds = this._initThresholds(options.threshold); + this.root = options.root || null; + this.rootMargin = this._rootMarginValues.map(function(margin) { + return margin.value + margin.unit; + }).join(' '); + } + + + /** + * The minimum interval within which the document will be checked for + * intersection changes. + */ + IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100; + + + /** + * The frequency in which the polyfill polls for intersection changes. + * this can be updated on a per instance basis and must be set prior to + * calling `observe` on the first target. + */ + IntersectionObserver.prototype.POLL_INTERVAL = null; + + /** + * Use a mutation observer on the root element + * to detect intersection changes. + */ + IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true; + + + /** + * Starts observing a target element for intersection changes based on + * the thresholds values. + * @param {Element} target The DOM element to observe. + */ + IntersectionObserver.prototype.observe = function(target) { + var isTargetAlreadyObserved = this._observationTargets.some(function(item) { + return item.element == target; + }); + + if (isTargetAlreadyObserved) { + return; + } + + if (!(target && target.nodeType == 1)) { + throw new Error('target must be an Element'); + } + + this._registerInstance(); + this._observationTargets.push({element: target, entry: null}); + this._monitorIntersections(); + this._checkForIntersections(); + }; + + + /** + * Stops observing a target element for intersection changes. + * @param {Element} target The DOM element to observe. + */ + IntersectionObserver.prototype.unobserve = function(target) { + this._observationTargets = + this._observationTargets.filter(function(item) { + + return item.element != target; + }); + if (!this._observationTargets.length) { + this._unmonitorIntersections(); + this._unregisterInstance(); + } + }; + + + /** + * Stops observing all target elements for intersection changes. + */ + IntersectionObserver.prototype.disconnect = function() { + this._observationTargets = []; + this._unmonitorIntersections(); + this._unregisterInstance(); + }; + + + /** + * Returns any queue entries that have not yet been reported to the + * callback and clears the queue. This can be used in conjunction with the + * callback to obtain the absolute most up-to-date intersection information. + * @return {Array} The currently queued entries. + */ + IntersectionObserver.prototype.takeRecords = function() { + var records = this._queuedEntries.slice(); + this._queuedEntries = []; + return records; + }; + + + /** + * Accepts the threshold value from the user configuration object and + * returns a sorted array of unique threshold values. If a value is not + * between 0 and 1 and error is thrown. + * @private + * @param {Array|number=} opt_threshold An optional threshold value or + * a list of threshold values, defaulting to [0]. + * @return {Array} A sorted list of unique and valid threshold values. + */ + IntersectionObserver.prototype._initThresholds = function(opt_threshold) { + var threshold = opt_threshold || [0]; + if (!Array.isArray(threshold)) threshold = [threshold]; + + return threshold.sort().filter(function(t, i, a) { + if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) { + throw new Error('threshold must be a number between 0 and 1 inclusively'); + } + return t !== a[i - 1]; + }); + }; + + + /** + * Accepts the rootMargin value from the user configuration object + * and returns an array of the four margin values as an object containing + * the value and unit properties. If any of the values are not properly + * formatted or use a unit other than px or %, and error is thrown. + * @private + * @param {string=} opt_rootMargin An optional rootMargin value, + * defaulting to '0px'. + * @return {Array<Object>} An array of margin objects with the keys + * value and unit. + */ + IntersectionObserver.prototype._parseRootMargin = function(opt_rootMargin) { + var marginString = opt_rootMargin || '0px'; + var margins = marginString.split(/\s+/).map(function(margin) { + var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin); + if (!parts) { + throw new Error('rootMargin must be specified in pixels or percent'); + } + return {value: parseFloat(parts[1]), unit: parts[2]}; + }); + + // Handles shorthand. + margins[1] = margins[1] || margins[0]; + margins[2] = margins[2] || margins[0]; + margins[3] = margins[3] || margins[1]; + + return margins; + }; + + + /** + * Starts polling for intersection changes if the polling is not already + * happening, and if the page's visibilty state is visible. + * @private + */ + IntersectionObserver.prototype._monitorIntersections = function() { + if (!this._monitoringIntersections) { + this._monitoringIntersections = true; + + // If a poll interval is set, use polling instead of listening to + // resize and scroll events or DOM mutations. + if (this.POLL_INTERVAL) { + this._monitoringInterval = setInterval( + this._checkForIntersections, this.POLL_INTERVAL); + } + else { + addEvent(window, 'resize', this._checkForIntersections, true); + addEvent(document, 'scroll', this._checkForIntersections, true); + + if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) { + this._domObserver = new MutationObserver(this._checkForIntersections); + this._domObserver.observe(document, { + attributes: true, + childList: true, + characterData: true, + subtree: true + }); + } + } + } + }; + + + /** + * Stops polling for intersection changes. + * @private + */ + IntersectionObserver.prototype._unmonitorIntersections = function() { + if (this._monitoringIntersections) { + this._monitoringIntersections = false; + + clearInterval(this._monitoringInterval); + this._monitoringInterval = null; + + removeEvent(window, 'resize', this._checkForIntersections, true); + removeEvent(document, 'scroll', this._checkForIntersections, true); + + if (this._domObserver) { + this._domObserver.disconnect(); + this._domObserver = null; + } + } + }; + + + /** + * Scans each observation target for intersection changes and adds them + * to the internal entries queue. If new entries are found, it + * schedules the callback to be invoked. + * @private + */ + IntersectionObserver.prototype._checkForIntersections = function() { + var rootIsInDom = this._rootIsInDom(); + var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect(); + + this._observationTargets.forEach(function(item) { + var target = item.element; + var targetRect = getBoundingClientRect(target); + var rootContainsTarget = this._rootContainsTarget(target); + var oldEntry = item.entry; + var intersectionRect = rootIsInDom && rootContainsTarget && + this._computeTargetAndRootIntersection(target, rootRect); + + var newEntry = item.entry = new IntersectionObserverEntry({ + time: now(), + target: target, + boundingClientRect: targetRect, + rootBounds: rootRect, + intersectionRect: intersectionRect + }); + + if (!oldEntry) { + this._queuedEntries.push(newEntry); + } else if (rootIsInDom && rootContainsTarget) { + // If the new entry intersection ratio has crossed any of the + // thresholds, add a new entry. + if (this._hasCrossedThreshold(oldEntry, newEntry)) { + this._queuedEntries.push(newEntry); + } + } else { + // If the root is not in the DOM or target is not contained within + // root but the previous entry for this target had an intersection, + // add a new record indicating removal. + if (oldEntry && oldEntry.isIntersecting) { + this._queuedEntries.push(newEntry); + } + } + }, this); + + if (this._queuedEntries.length) { + this._callback(this.takeRecords(), this); + } + }; + + + /** + * Accepts a target and root rect computes the intersection between then + * following the algorithm in the spec. + * TODO(philipwalton): at this time clip-path is not considered. + * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo + * @param {Element} target The target DOM element + * @param {Object} rootRect The bounding rect of the root after being + * expanded by the rootMargin value. + * @return {?Object} The final intersection rect object or undefined if no + * intersection is found. + * @private + */ + IntersectionObserver.prototype._computeTargetAndRootIntersection = + function(target, rootRect) { + + // If the element isn't displayed, an intersection can't happen. + if (window.getComputedStyle(target).display == 'none') return; + + var targetRect = getBoundingClientRect(target); + var intersectionRect = targetRect; + var parent = getParentNode(target); + var atRoot = false; + + while (!atRoot) { + var parentRect = null; + var parentComputedStyle = parent.nodeType == 1 ? + window.getComputedStyle(parent) : {}; + + // If the parent isn't displayed, an intersection can't happen. + if (parentComputedStyle.display == 'none') return; + + if (parent == this.root || parent == document) { + atRoot = true; + parentRect = rootRect; + } else { + // If the element has a non-visible overflow, and it's not the <body> + // or <html> element, update the intersection rect. + // Note: <body> and <html> cannot be clipped to a rect that's not also + // the document rect, so no need to compute a new intersection. + if (parent != document.body && + parent != document.documentElement && + parentComputedStyle.overflow != 'visible') { + parentRect = getBoundingClientRect(parent); + } + } + + // If either of the above conditionals set a new parentRect, + // calculate new intersection data. + if (parentRect) { + intersectionRect = computeRectIntersection(parentRect, intersectionRect); + + if (!intersectionRect) break; + } + parent = getParentNode(parent); + } + return intersectionRect; + }; + + + /** + * Returns the root rect after being expanded by the rootMargin value. + * @return {Object} The expanded root rect. + * @private + */ + IntersectionObserver.prototype._getRootRect = function() { + var rootRect; + if (this.root) { + rootRect = getBoundingClientRect(this.root); + } else { + // Use <html>/<body> instead of window since scroll bars affect size. + var html = document.documentElement; + var body = document.body; + rootRect = { + top: 0, + left: 0, + right: html.clientWidth || body.clientWidth, + width: html.clientWidth || body.clientWidth, + bottom: html.clientHeight || body.clientHeight, + height: html.clientHeight || body.clientHeight + }; + } + return this._expandRectByRootMargin(rootRect); + }; + + + /** + * Accepts a rect and expands it by the rootMargin value. + * @param {Object} rect The rect object to expand. + * @return {Object} The expanded rect. + * @private + */ + IntersectionObserver.prototype._expandRectByRootMargin = function(rect) { + var margins = this._rootMarginValues.map(function(margin, i) { + return margin.unit == 'px' ? margin.value : + margin.value * (i % 2 ? rect.width : rect.height) / 100; + }); + var newRect = { + top: rect.top - margins[0], + right: rect.right + margins[1], + bottom: rect.bottom + margins[2], + left: rect.left - margins[3] + }; + newRect.width = newRect.right - newRect.left; + newRect.height = newRect.bottom - newRect.top; + + return newRect; + }; + + + /** + * Accepts an old and new entry and returns true if at least one of the + * threshold values has been crossed. + * @param {?IntersectionObserverEntry} oldEntry The previous entry for a + * particular target element or null if no previous entry exists. + * @param {IntersectionObserverEntry} newEntry The current entry for a + * particular target element. + * @return {boolean} Returns true if a any threshold has been crossed. + * @private + */ + IntersectionObserver.prototype._hasCrossedThreshold = + function(oldEntry, newEntry) { + + // To make comparing easier, an entry that has a ratio of 0 + // but does not actually intersect is given a value of -1 + var oldRatio = oldEntry && oldEntry.isIntersecting ? + oldEntry.intersectionRatio || 0 : -1; + var newRatio = newEntry.isIntersecting ? + newEntry.intersectionRatio || 0 : -1; + + // Ignore unchanged ratios + if (oldRatio === newRatio) return; + + for (var i = 0; i < this.thresholds.length; i++) { + var threshold = this.thresholds[i]; + + // Return true if an entry matches a threshold or if the new ratio + // and the old ratio are on the opposite sides of a threshold. + if (threshold == oldRatio || threshold == newRatio || + threshold < oldRatio !== threshold < newRatio) { + return true; + } + } + }; + + + /** + * Returns whether or not the root element is an element and is in the DOM. + * @return {boolean} True if the root element is an element and is in the DOM. + * @private + */ + IntersectionObserver.prototype._rootIsInDom = function() { + return !this.root || containsDeep(document, this.root); + }; + + + /** + * Returns whether or not the target element is a child of root. + * @param {Element} target The target element to check. + * @return {boolean} True if the target element is a child of root. + * @private + */ + IntersectionObserver.prototype._rootContainsTarget = function(target) { + return containsDeep(this.root || document, target); + }; + + + /** + * Adds the instance to the global IntersectionObserver registry if it isn't + * already present. + * @private + */ + IntersectionObserver.prototype._registerInstance = function() { + if (registry.indexOf(this) < 0) { + registry.push(this); + } + }; + + + /** + * Removes the instance from the global IntersectionObserver registry. + * @private + */ + IntersectionObserver.prototype._unregisterInstance = function() { + var index = registry.indexOf(this); + if (index != -1) registry.splice(index, 1); + }; + + + /** + * Returns the result of the performance.now() method or null in browsers + * that don't support the API. + * @return {number} The elapsed time since the page was requested. + */ + function now() { + return window.performance && performance.now && performance.now(); + } + + + /** + * Throttles a function and delays its executiong, so it's only called at most + * once within a given time period. + * @param {Function} fn The function to throttle. + * @param {number} timeout The amount of time that must pass before the + * function can be called again. + * @return {Function} The throttled function. + */ + function throttle(fn, timeout) { + var timer = null; + return function () { + if (!timer) { + timer = setTimeout(function() { + fn(); + timer = null; + }, timeout); + } + }; + } + + + /** + * Adds an event handler to a DOM node ensuring cross-browser compatibility. + * @param {Node} node The DOM node to add the event handler to. + * @param {string} event The event name. + * @param {Function} fn The event handler to add. + * @param {boolean} opt_useCapture Optionally adds the even to the capture + * phase. Note: this only works in modern browsers. + */ + function addEvent(node, event, fn, opt_useCapture) { + if (typeof node.addEventListener == 'function') { + node.addEventListener(event, fn, opt_useCapture || false); + } + else if (typeof node.attachEvent == 'function') { + node.attachEvent('on' + event, fn); + } + } + + + /** + * Removes a previously added event handler from a DOM node. + * @param {Node} node The DOM node to remove the event handler from. + * @param {string} event The event name. + * @param {Function} fn The event handler to remove. + * @param {boolean} opt_useCapture If the event handler was added with this + * flag set to true, it should be set to true here in order to remove it. + */ + function removeEvent(node, event, fn, opt_useCapture) { + if (typeof node.removeEventListener == 'function') { + node.removeEventListener(event, fn, opt_useCapture || false); + } + else if (typeof node.detatchEvent == 'function') { + node.detatchEvent('on' + event, fn); + } + } + + + /** + * Returns the intersection between two rect objects. + * @param {Object} rect1 The first rect. + * @param {Object} rect2 The second rect. + * @return {?Object} The intersection rect or undefined if no intersection + * is found. + */ + function computeRectIntersection(rect1, rect2) { + var top = Math.max(rect1.top, rect2.top); + var bottom = Math.min(rect1.bottom, rect2.bottom); + var left = Math.max(rect1.left, rect2.left); + var right = Math.min(rect1.right, rect2.right); + var width = right - left; + var height = bottom - top; + + return (width >= 0 && height >= 0) && { + top: top, + bottom: bottom, + left: left, + right: right, + width: width, + height: height + }; + } + + + /** + * Shims the native getBoundingClientRect for compatibility with older IE. + * @param {Element} el The element whose bounding rect to get. + * @return {Object} The (possibly shimmed) rect of the element. + */ + function getBoundingClientRect(el) { + var rect; + + try { + rect = el.getBoundingClientRect(); + } catch (err) { + // Ignore Windows 7 IE11 "Unspecified error" + // https://github.com/w3c/IntersectionObserver/pull/205 + } + + if (!rect) return getEmptyRect(); + + // Older IE + if (!(rect.width && rect.height)) { + rect = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.right - rect.left, + height: rect.bottom - rect.top + }; + } + return rect; + } + + + /** + * Returns an empty rect object. An empty rect is returned when an element + * is not in the DOM. + * @return {Object} The empty rect. + */ + function getEmptyRect() { + return { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 0, + height: 0 + }; + } + + /** + * Checks to see if a parent element contains a child elemnt (including inside + * shadow DOM). + * @param {Node} parent The parent element. + * @param {Node} child The child element. + * @return {boolean} True if the parent node contains the child node. + */ + function containsDeep(parent, child) { + var node = child; + while (node) { + if (node == parent) return true; + + node = getParentNode(node); + } + return false; + } + + + /** + * Gets the parent node of an element or its host element if the parent node + * is a shadow root. + * @param {Node} node The node whose parent to get. + * @return {Node|null} The parent node or null if no parent exists. + */ + function getParentNode(node) { + var parent = node.parentNode; + + if (parent && parent.nodeType == 11 && parent.host) { + // If the parent is a shadow root, return the host element. + return parent.host; + } + return parent; + } + + + // Exposes the constructors globally. + window.IntersectionObserver = IntersectionObserver; + window.IntersectionObserverEntry = IntersectionObserverEntry; + + }(window, document)); /* jshint ignore:end */ // Let's kick things off now diff --git a/plugins/jetpack/modules/lazy-images/lazy-images.php b/plugins/jetpack/modules/lazy-images/lazy-images.php index 38610bab..fc800159 100644 --- a/plugins/jetpack/modules/lazy-images/lazy-images.php +++ b/plugins/jetpack/modules/lazy-images/lazy-images.php @@ -45,18 +45,49 @@ class Jetpack_Lazy_Images { // Do not lazy load avatar in admin bar add_action( 'admin_bar_menu', array( $this, 'remove_filters' ), 0 ); + + add_filter( 'wp_kses_allowed_html', array( $this, 'allow_lazy_attributes' ) ); } public function setup_filters() { - add_filter( 'the_content', array( $this, 'add_image_placeholders' ), 99 ); // run this later, so other content filters have run, including image_add_wh on WP.com - add_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), 11 ); - add_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), 11 ); + add_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); // run this later, so other content filters have run, including image_add_wh on WP.com + add_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + add_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + add_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + add_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX); + add_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX ); } public function remove_filters() { - remove_filter( 'the_content', array( $this, 'add_image_placeholders' ), 99 ); - remove_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), 11 ); - remove_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), 11 ); + remove_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + remove_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + remove_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + remove_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); + remove_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX); + remove_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX ); + } + + /** + * Ensure that our lazy image attributes are not filtered out of image tags. + * + * @param array $allowed_tags The allowed tags and their attributes. + * @return array + */ + public function allow_lazy_attributes( $allowed_tags ) { + if ( ! isset( $allowed_tags['img'] ) ) { + return $allowed_tags; + } + + // But, if images are allowed, ensure that our attributes are allowed! + $img_attributes = array_merge( $allowed_tags['img'], array( + 'data-lazy-src' => 1, + 'data-lazy-srcset' => 1, + 'data-lazy-sizes' => 1, + ) ); + + $allowed_tags['img'] = $img_attributes; + + return $allowed_tags; } public function add_image_placeholders( $content ) { @@ -81,6 +112,54 @@ class Jetpack_Lazy_Images { return $content; } + /** + * Returns true when a given string of classes contains a class signifying lazy images + * should not process the image. + * + * @since 5.9.0 + * + * @param string $classes A string of space-separated classes. + * @return bool + */ + public static function should_skip_image_with_blacklisted_class( $classes ) { + $blacklisted_classes = array( + 'skip-lazy', + 'gazette-featured-content-thumbnail', + ); + + /** + * Allow plugins and themes to tell lazy images to skip an image with a given class. + * + * @module lazy-images + * + * @since 5.9.0 + * + * @param array An array of strings where each string is a class. + */ + $blacklisted_classes = apply_filters( 'jetpack_lazy_images_blacklisted_classes', $blacklisted_classes ); + + if ( ! is_array( $blacklisted_classes ) || empty( $blacklisted_classes ) ) { + return false; + } + + foreach ( $blacklisted_classes as $class ) { + if ( false !== strpos( $classes, $class ) ) { + return true; + } + } + + return false; + } + + /** + * Processes images in content by acting as the preg_replace_callback + * + * @since 5.6.0 + * + * @param array $matches + * + * @return string The image with updated lazy attributes + */ static function process_image( $matches ) { $old_attributes_str = $matches[2]; $old_attributes_kses_hair = wp_kses_hair( $old_attributes_str, wp_allowed_protocols() ); @@ -90,22 +169,67 @@ class Jetpack_Lazy_Images { } $old_attributes = self::flatten_kses_hair_data( $old_attributes_kses_hair ); - $new_attributes = $old_attributes; + $new_attributes = self::process_image_attributes( $old_attributes ); + + // If we didn't add lazy attributes, just return the original image source. + if ( empty( $new_attributes['data-lazy-src'] ) ) { + return $matches[0]; + } + + $new_attributes_str = self::build_attributes_string( $new_attributes ); + + return sprintf( '<img %1$s><noscript>%2$s</noscript>', $new_attributes_str, $matches[0] ); + } + + /** + * Given an array of image attributes, updates the `src`, `srcset`, and `sizes` attributes so + * that they load lazily. + * + * @since 5.7.0 + * + * @param array $attributes + * + * @return array The updated image attributes array with lazy load attributes + */ + static function process_image_attributes( $attributes ) { + if ( empty( $attributes['src'] ) ) { + return $attributes; + } + + if ( ! empty( $attributes['class'] ) && self::should_skip_image_with_blacklisted_class( $attributes['class'] ) ) { + return $attributes; + } + + /** + * Allow plugins and themes to conditionally skip processing an image via its attributes. + * + * @module-lazy-images + * + * @since 5.9.0 + * + * @param bool Default to not skip processing the current image. + * @param array An array of attributes via wp_kses_hair() for the current image. + */ + if ( apply_filters( 'jetpack_lazy_images_skip_image_with_atttributes', false, $attributes ) ) { + return $attributes; + } + + $old_attributes = $attributes; // Set placeholder and lazy-src - $new_attributes['src'] = self::get_placeholder_image(); - $new_attributes['data-lazy-src'] = $old_attributes['src']; + $attributes['src'] = self::get_placeholder_image(); + $attributes['data-lazy-src'] = $old_attributes['src']; // Handle `srcset` - if ( ! empty( $new_attributes['srcset'] ) ) { - $new_attributes['data-lazy-srcset'] = $old_attributes['srcset']; - unset( $new_attributes['srcset'] ); + if ( ! empty( $attributes['srcset'] ) ) { + $attributes['data-lazy-srcset'] = $old_attributes['srcset']; + unset( $attributes['srcset'] ); } // Handle `sizes` - if ( ! empty( $new_attributes['sizes'] ) ) { - $new_attributes['data-lazy-sizes'] = $old_attributes['sizes']; - unset( $new_attributes['sizes'] ); + if ( ! empty( $attributes['sizes'] ) ) { + $attributes['data-lazy-sizes'] = $old_attributes['sizes']; + unset( $attributes['sizes'] ); } /** @@ -113,7 +237,7 @@ class Jetpack_Lazy_Images { * * One potential use of this filter is for themes that set `height:auto` on the `img` tag. * With this filter, the theme could get the width and height attributes from the - * $new_attributes array and then add a style tag that sets those values as well, which could + * $attributes array and then add a style tag that sets those values as well, which could * minimize reflow as images load. * * @module lazy-images @@ -123,10 +247,7 @@ class Jetpack_Lazy_Images { * @param array An array containing the attributes for the image, where the key is the attribute name * and the value is the attribute value. */ - $new_attributes = apply_filters( 'jetpack_lazy_images_new_attributes', $new_attributes ); - $new_attributes_str = self::build_attributes_string( $new_attributes ); - - return sprintf( '<img %1$s><noscript>%2$s</noscript>', $new_attributes_str, $matches[0] ); + return apply_filters( 'jetpack_lazy_images_new_attributes', $attributes ); } private static function get_placeholder_image() { @@ -171,7 +292,10 @@ class Jetpack_Lazy_Images { public function enqueue_assets() { wp_enqueue_script( 'jetpack-lazy-images', - plugins_url( 'modules/lazy-images/js/lazy-images.js', JETPACK__PLUGIN_FILE ), + Jetpack::get_file_url_for_environment( + '_inc/build/lazy-images/js/lazy-images.min.js', + 'modules/lazy-images/js/lazy-images.js' + ), array( 'jquery' ), JETPACK__VERSION, true diff --git a/plugins/jetpack/modules/likes.php b/plugins/jetpack/modules/likes.php index 84204cc3..deaaa676 100644 --- a/plugins/jetpack/modules/likes.php +++ b/plugins/jetpack/modules/likes.php @@ -296,7 +296,16 @@ class Jetpack_Likes { JETPACK__VERSION, false ); - wp_register_script( 'jetpack_likes_queuehandler', plugins_url( 'likes/queuehandler.js' , __FILE__ ), array( 'jquery', 'postmessage', 'jetpack_resize' ), JETPACK__VERSION, true ); + wp_register_script( + 'jetpack_likes_queuehandler', + Jetpack::get_file_url_for_environment( + '_inc/build/likes/queuehandler.min.js', + 'modules/likes/queuehandler.js' + ), + array( 'jquery', 'postmessage', 'jetpack_resize' ), + JETPACK__VERSION, + true + ); } /** @@ -348,8 +357,24 @@ class Jetpack_Likes { function enqueue_admin_scripts() { if ( empty( $_GET['post_type'] ) || 'post' == $_GET['post_type'] || 'page' == $_GET['post_type'] ) { if ( $this->in_jetpack ) { - wp_enqueue_script( 'likes-post-count', plugins_url( 'modules/likes/post-count.js', dirname( __FILE__ ) ), array( 'jquery' ), JETPACK__VERSION ); - wp_enqueue_script( 'likes-post-count-jetpack', plugins_url( 'modules/likes/post-count-jetpack.js', dirname( __FILE__ ) ), array( 'likes-post-count' ), JETPACK__VERSION ); + wp_enqueue_script( + 'likes-post-count', + Jetpack::get_file_url_for_environment( + '_inc/build/likes/post-count.min.js', + 'modules/likes/post-count.js' + ), + array( 'jquery' ), + JETPACK__VERSION + ); + wp_enqueue_script( + 'likes-post-count-jetpack', + Jetpack::get_file_url_for_environment( + '_inc/build/likes/post-count-jetpack.min.js', + 'modules/likes/post-count-jetpack.js' + ), + array( 'likes-post-count' ), + JETPACK__VERSION + ); } else { wp_enqueue_script( 'jquery.wpcom-proxy-request', "/wp-content/js/jquery/jquery.wpcom-proxy-request.js", array('jquery'), NULL, true ); wp_enqueue_script( 'likes-post-count', plugins_url( 'likes/post-count.js', dirname( __FILE__ ) ), array( 'jquery' ), JETPACK__VERSION ); diff --git a/plugins/jetpack/modules/manage/confirm-admin.php b/plugins/jetpack/modules/manage/confirm-admin.php index c8f105f9..d0507053 100644 --- a/plugins/jetpack/modules/manage/confirm-admin.php +++ b/plugins/jetpack/modules/manage/confirm-admin.php @@ -18,7 +18,7 @@ $description = __( 'Well that was easy. You can now manage all of your sites in switch( $section ) { case 'plugins': - $link = 'https://wordpress.com/plugins/' . $normalized_site_url; + $link = 'https://wordpress.com/plugins/manage/' . $normalized_site_url; $link_title = __( 'Manage Your Plugins', 'jetpack' ); break; @@ -28,7 +28,7 @@ switch( $section ) { break; case 'themes': - $link = 'https://wordpress.com/design/' . $normalized_site_url; + $link = 'https://wordpress.com/themes/' . $normalized_site_url; $link_title = __( 'Manage Your Themes', 'jetpack' ); break; diff --git a/plugins/jetpack/modules/masterbar/masterbar.php b/plugins/jetpack/modules/masterbar/masterbar.php index c634c9f8..d38b9e67 100644 --- a/plugins/jetpack/modules/masterbar/masterbar.php +++ b/plugins/jetpack/modules/masterbar/masterbar.php @@ -40,10 +40,13 @@ class A8C_WPCOM_Masterbar { '//2.gravatar.com', ) ); - // Atomic only - override user setting that hides masterbar from site's front. - // https://github.com/Automattic/jetpack/issues/7667 + // Atomic only if ( jetpack_is_atomic_site() ) { + // override user setting that hides masterbar from site's front. + // https://github.com/Automattic/jetpack/issues/7667 add_filter( 'show_admin_bar', '__return_true' ); + // Always sign out from .com from the masterbar + add_filter( 'jetpack_masterbar_should_logout_from_wpcom', '__return_true' ); } $this->user_data = Jetpack::get_connected_user_data( $this->user_id ); @@ -84,7 +87,19 @@ class A8C_WPCOM_Masterbar { } public function maybe_logout_user_from_wpcom() { - if ( isset( $_GET['context'] ) && 'masterbar' === $_GET['context'] ) { + /** + * Whether we should sign out from wpcom too when signing out from the masterbar. + * + * @since 5.9.0 + * + * @param bool $masterbar_should_logout_from_wpcom False by default. + */ + $masterbar_should_logout_from_wpcom = apply_filters( 'jetpack_masterbar_should_logout_from_wpcom', false ); + if ( + isset( $_GET['context'] ) && + 'masterbar' === $_GET['context'] && + $masterbar_should_logout_from_wpcom + ) { do_action( 'wp_masterbar_logout' ); } } @@ -138,7 +153,15 @@ class A8C_WPCOM_Masterbar { array(), JETPACK__VERSION ); - wp_enqueue_script( 'a8c_wpcom_masterbar_tracks_events', plugins_url( 'tracks-events.js', __FILE__ ), array( 'jquery' ), JETPACK__VERSION ); + wp_enqueue_script( + 'a8c_wpcom_masterbar_tracks_events', + Jetpack::get_file_url_for_environment( + '_inc/build/masterbar/tracks-events.min.js', + 'modules/masterbar/tracks-events.js' + ), + array( 'jquery' ), + JETPACK__VERSION + ); wp_enqueue_script( 'a8c_wpcom_masterbar_overrides', $this->wpcom_static_url( '/wp-content/mu-plugins/admin-bar/masterbar-overrides/masterbar.js' ), array( 'jquery' ), JETPACK__VERSION ); } @@ -852,7 +875,7 @@ class A8C_WPCOM_Masterbar { $theme_title = $this->create_menu_item_pair( array( - 'url' => 'https://wordpress.com/design/' . esc_attr( $this->primary_site_slug ), + 'url' => 'https://wordpress.com/themes/' . esc_attr( $this->primary_site_slug ), 'id' => 'wp-admin-bar-themes', 'label' => esc_html__( 'Themes', 'jetpack' ), ), diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php index 5e0119e7..fadd678c 100644 --- a/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php +++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/functions.php @@ -79,7 +79,16 @@ function minileven_scripts() { wp_enqueue_style( 'style', get_stylesheet_uri() ); - wp_enqueue_script( 'small-menu', get_template_directory_uri() . '/js/small-menu.js', array( 'jquery' ), '20120206', true ); + wp_enqueue_script( + 'small-menu', + Jetpack::get_file_url_for_environment( + '_inc/build/minileven/theme/pub/minileven/js/small-menu.min.js', + 'modules/minileven/theme/pub/minileven/js/small-menu.js' + ), + array( 'jquery' ), + '20120206', + true + ); if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) { wp_enqueue_script( 'comment-reply' ); @@ -223,13 +232,14 @@ function minileven_get_gallery_images() { if ( ! $images ) { $images = get_posts( array( - 'fields' => 'ids', - 'numberposts' => 999, - 'order' => 'ASC', - 'orderby' => 'menu_order', - 'post_mime_type' => 'image', - 'post_parent' => get_the_ID(), - 'post_type' => 'attachment', + 'fields' => 'ids', + 'numberposts' => 999, + 'order' => 'ASC', + 'orderby' => 'menu_order', + 'post_mime_type' => 'image', + 'post_parent' => get_the_ID(), + 'post_type' => 'attachment', + 'suppress_filters' => false, ) ); } diff --git a/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php b/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php index b63cd70f..02032c2c 100644 --- a/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php +++ b/plugins/jetpack/modules/minileven/theme/pub/minileven/image.php @@ -26,7 +26,7 @@ get_header(); ?> * Grab the IDs of all the image attachments in a gallery so we can get the URL of the next adjacent image in a gallery, * or the first image (if we're looking at the last image in a gallery), or, in a gallery of one, just the link to that image file */ - $attachments = array_values( get_children( array( 'post_parent' => $post->post_parent, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => 'ASC', 'orderby' => 'menu_order ID' ) ) ); + $attachments = array_values( get_children( array( 'post_parent' => $post->post_parent, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => 'ASC', 'orderby' => 'menu_order ID', 'suppress_filters' => false ) ) ); foreach ( $attachments as $k => $attachment ) { if ( $attachment->ID == $post->ID ) break; diff --git a/plugins/jetpack/modules/module-extras.php b/plugins/jetpack/modules/module-extras.php index cbc3dc02..b6bacd6f 100644 --- a/plugins/jetpack/modules/module-extras.php +++ b/plugins/jetpack/modules/module-extras.php @@ -25,6 +25,7 @@ $tools = array( 'seo-tools/jetpack-seo-posts.php', 'simple-payments/simple-payments.php', 'verification-tools/verification-tools-utils.php', + 'woocommerce-analytics/wp-woocommerce-analytics.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 5612ccbb..477094ba 100644 --- a/plugins/jetpack/modules/module-headings.php +++ b/plugins/jetpack/modules/module-headings.php @@ -82,7 +82,7 @@ function jetpack_get_module_i18n( $key ) { 'lazy-images' => array(
'name' => _x( 'Lazy Images', 'Module Name', 'jetpack' ), - 'description' => _x( 'Improve performance by loading images just before they scroll into view', 'Module Description', 'jetpack' ), + 'description' => _x( 'Lazy load images', 'Module Description', 'jetpack' ), ),
'likes' => array(
@@ -125,7 +125,7 @@ function jetpack_get_module_i18n( $key ) { 'photon' => array(
'name' => _x( 'Photon', 'Module Name', 'jetpack' ), - 'description' => _x( 'Speed up images and photos', 'Module Description', 'jetpack' ), + 'description' => _x( 'Serve images from our servers', 'Module Description', 'jetpack' ), 'recommended description' => _x( 'Mirrors and serves your images from our free and fast image CDN, improving your site’s performance with no additional load on your servers.', 'Jumpstart Description', 'jetpack' ), ),
@@ -163,7 +163,6 @@ function jetpack_get_module_i18n( $key ) { 'seo-tools' => array(
'name' => _x( 'SEO Tools', 'Module Name', 'jetpack' ), 'description' => _x( 'Better results on search engines and social media.', 'Module Description', 'jetpack' ), - 'recommended description' => _x( 'Better results on search engines and social media.', 'Jumpstart Description', 'jetpack' ), ),
'sharedaddy' => array(
diff --git a/plugins/jetpack/modules/module-info.php b/plugins/jetpack/modules/module-info.php index 620b5e16..ece1e050 100644 --- a/plugins/jetpack/modules/module-info.php +++ b/plugins/jetpack/modules/module-info.php @@ -404,13 +404,30 @@ add_action( 'jetpack_learn_more_button_photon', 'jetpack_photon_more_link' ); function jetpack_photon_more_info() { esc_html_e( - "Your images are automatically optimized for different display resolutions to serve the best - possible image quality. We also cache and serve them from our fast, global network (CDN)." + 'Jetpack will optimize your images and serve them from the server location nearest + to your visitors. Using our global content delivery network will boost the loading speed of your site.' , 'jetpack' ); } add_action( 'jetpack_module_more_info_photon', 'jetpack_photon_more_info' ); /** + * Lazy Images + */ +function jetpack_lazy_images_more_link() { + echo 'https://jetpack.com/support/lazy-images/'; +} +add_action( 'jetpack_learn_more_button_lazy-images', 'jetpack_lazy_images_more_link' ); + +function jetpack_lazy_images_more_info() { + esc_html_e( + 'Improve your site\'s speed by only loading images visible on the screen. + New images will load just before they scroll into view. This prevents viewers + from having to download all the images on a page all at once, even ones they can\'t see.' + , 'jetpack' ); +} +add_action( 'jetpack_module_more_info_lazy-images', 'jetpack_lazy_images_more_info' ); + +/** * Tiled Galleries */ function jetpack_tiled_gallery_more_link() { @@ -650,3 +667,18 @@ function jetpack_google_analytics_more_info() { , 'jetpack' ); } add_action( 'jetpack_module_more_info_google-analytics', 'jetpack_google_analytics_more_info' ); + +/** + * WooCommerce Analytics + */ +function jetpack_woocommerce_analytics_more_link() { + echo 'https://jetpack.com/support/'; +} +add_action( 'jetpack_learn_more_button_woocommerce-analytics', 'jetpack_woocommerce_analytics_more_link' ); + +function jetpack_woocommerce_analytics_more_info() { + esc_html_e( + 'Enhanced analytics for WooCommerce and Jetpack users.' + , 'jetpack' ); +} +add_action( 'jetpack_module_more_info_woocommerce-analytics', 'jetpack_woocommerce_analytics_more_info' ); diff --git a/plugins/jetpack/modules/photon.php b/plugins/jetpack/modules/photon.php index c2eb97a0..628f57c5 100644 --- a/plugins/jetpack/modules/photon.php +++ b/plugins/jetpack/modules/photon.php @@ -1,7 +1,7 @@ <?php /** * Module Name: Photon - * Module Description: Speed up images and photos + * Module Description: Serve images from our servers * Jumpstart Description: Mirrors and serves your images from our free and fast image CDN, improving your site’s performance with no additional load on your servers. * Sort Order: 25 * Recommendation Order: 1 diff --git a/plugins/jetpack/modules/protect.php b/plugins/jetpack/modules/protect.php index a47a1af9..6865179e 100644 --- a/plugins/jetpack/modules/protect.php +++ b/plugins/jetpack/modules/protect.php @@ -268,7 +268,8 @@ class Jetpack_Protect_Module { * * @return void */ - function log_failed_attempt() { + function log_failed_attempt( $login_user = null ) { + /** * Fires before every failed login attempt. * @@ -276,9 +277,12 @@ class Jetpack_Protect_Module { * * @since 3.4.0 * - * @param string jetpack_protect_get_ip IP stored by Protect. + * @param array Information about failed login attempt + * [ + * 'login' => (string) Username or email used in failed login attempt + * ] */ - do_action( 'jpp_log_failed_attempt', jetpack_protect_get_ip() ); + do_action( 'jpp_log_failed_attempt', array( 'login' => $login_user ) ); if ( isset( $_COOKIE['jpp_math_pass'] ) ) { @@ -431,7 +435,7 @@ class Jetpack_Protect_Module { /** * JETPACK_ALWAYS_PROTECT_LOGIN will always disable the login page, and use a page provided by Jetpack. */ - if ( defined( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) && JETPACK_ALWAYS_PROTECT_LOGIN ) { + if ( Jetpack_Constants::is_true( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) ) { $this->kill_login(); } diff --git a/plugins/jetpack/modules/protect/blocked-login-page.php b/plugins/jetpack/modules/protect/blocked-login-page.php index ec1e8835..f26b5193 100644 --- a/plugins/jetpack/modules/protect/blocked-login-page.php +++ b/plugins/jetpack/modules/protect/blocked-login-page.php @@ -17,7 +17,6 @@ class Jetpack_Protect_Blocked_Login_Page { public $can_send_recovery_emails; public $ip_address; public $valid_blocked_user_id; - public $page_title; public $email_address; const HELP_URL = 'https://jetpack.com/support/security-features/#unblock'; const HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429; @@ -183,8 +182,6 @@ class Jetpack_Protect_Blocked_Login_Page { } public function render_and_die() { - $this->page_title = __( 'Login Blocked by Jetpack', 'jetpack' ); - if ( ! $this->can_send_recovery_emails ) { $this->render_blocked_login_message(); @@ -192,7 +189,8 @@ class Jetpack_Protect_Blocked_Login_Page { } if ( isset( $_GET['validate_jetpack_protect_recovery'] ) && $_GET['user_id'] ) { - $this->protect_die( __( 'Could not validate recovery token.', 'jetpack' ) ); + $error = new WP_Error( 'invalid_token', __( "Oops, we couldn't validate the recovery token.", 'jetpack' ) ); + $this->protect_die( $error ); return; } @@ -220,9 +218,12 @@ class Jetpack_Protect_Blocked_Login_Page { function process_recovery_email() { $sent = $this->send_recovery_email(); - + $show_recovery_form = true; if ( is_wp_error( $sent ) ) { - $this->protect_die( $sent, true ); + if ( 'email_already_sent' === $sent->get_error_code() ) { + $show_recovery_form = false; + } + $this->protect_die( $sent,null,true, $show_recovery_form ); } else { $this->render_recovery_success(); } @@ -236,7 +237,7 @@ class Jetpack_Protect_Blocked_Login_Page { $user = get_user_by( 'email', trim( $email ) ); if ( ! $user ) { - return new WP_Error( 'invalid_user', __( 'Oops, could not find a user with that email address.', 'jetpack' ) ); + return new WP_Error( 'invalid_user', __( "Oops, we couldn't find a user with that email. Please try again!", 'jetpack' ) ); } $this->email_address = $email; $path = sprintf( '/sites/%d/protect/recovery/request', Jetpack::get_option( 'id' ) ); @@ -258,72 +259,62 @@ class Jetpack_Protect_Blocked_Login_Page { $result = json_decode( wp_remote_retrieve_body( $response ) ); if ( self::HTTP_STATUS_CODE_TOO_MANY_REQUESTS === $code ) { - return new WP_Error( 'email_already_sent', __( 'An email was already sent to this address.', 'jetpack' ) ); + return new WP_Error( 'email_already_sent', sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack' ), $this->email_address ) ); } else if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) { - return new WP_Error( 'email_send_error', __( 'There was an error sending your email.', 'jetpack' ) ); + return new WP_Error( 'email_send_error', __( 'Oops, we were unable to send a recovery email. Try again.', 'jetpack' ) ); } return true; } - function protect_die( $content, $back_link = false ) { - $image = sprintf( - '<img src="%s" width="180" style="display: block; margin: 0 auto;" />', - plugins_url( 'modules/protect/jetpack-security.png', JETPACK__PLUGIN_FILE ) - ); - + function protect_die( $content, $title = null, $back_link = false, $recovery_form = false ) { + if ( empty( $title ) ) { + $title = __( 'Jetpack has locked your site\'s login page.', 'jetpack' ); + } if ( is_wp_error( $content ) ) { - $content = $content->get_error_message(); + $svg = '<svg class="gridicon gridicons-notice" 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 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>'; + $content = '<span class="error"> '. $svg . $content->get_error_message() . '</span>'; } - // hack to get around default wp_die_handler. https://core.trac.wordpress.org/browser/tags/4.8.1/src/wp-includes/functions.php#L2698 - $content = $image . '</p> ' . $content . '<p>'; + $content = '<p>'. $content .'</p>'; + // If for some reason the login pop up box show up in the wp-admin. if ( isset( $_GET['interim-login'] ) ) { - $content = "<style>html{ background-color: #fff; } #error-page { margin:0 auto; padding: 1em; box-shadow: none; } </style>" . $content; + $content = "<style>html{ background-color: #fff; } #error-message { margin:0 auto; padding: 1em; box-shadow: none; } </style>" . $content; } + $this->display_page( $title, $content, $back_link, $recovery_form ); - wp_die( $content, $this->page_title, array( 'back_link' => $back_link, 'response' => 200 ) ); } function render_recovery_form() { - $content = $this->get_html_blocked_login_message() . $this->get_html_recovery_form(); - $this->protect_die( $content ); + $content = $this->get_html_blocked_login_message(); + $this->protect_die( $content, null, null, true ); } function render_recovery_success() { - $this->protect_die( sprintf( __( 'An email with recovery instructions was sent to %s.', 'jetpack' ), $this->email_address ) ); + $this->protect_die( sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack' ), $this->email_address ) ); } function get_html_blocked_login_message() { $icon = '<svg class="gridicon gridicons-spam" style="fill:#d94f4f" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M17 2H7L2 7v10l5 5h10l5-5V7l-5-5zm-4 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>'; $ip = str_replace( 'http://', '', esc_url( 'http://' . $this->ip_address ) ); - ob_start(); ?> - <h3><?php printf( __( 'Jetpack Protect has locked your site\'s login page.', 'jetpack' ) ); ?></h3> - <?php printf( - __( '<p><span style="float:left; display:block; margin-right:10px;">%1$s</span>Your IP (%2$s) has been flagged for potential security violations. <a href="%3$s">Learn More</a></p>', 'jetpack' ), + return sprintf( + __( '<p>Your IP address <code>%2$s</code> has been flagged for potential security violations. You can unlock your login by sending yourself a special link via email. <a href="%3$s">Learn More</a></p>', 'jetpack' ), $icon, $ip, esc_url( self::HELP_URL ) ); - - $contents = ob_get_contents(); - ob_end_clean(); - - return $contents; } function get_html_recovery_form() { ob_start(); ?> - <div style="margin-top:100px;"> - <p><?php _e( 'Email yourself a special link to regain access the login form.', 'jetpack' ); ?></p> + <div> <form method="post" action="?jetpack-protect-recovery=true"> <?php echo wp_nonce_field( 'bypass-protect' ); ?> - <p><label for="email" style="font-size:12px;">Email Address<br/></label> - <input type="email" name="email" style="font-size:24px; padding:3px; margin: 2px 6px 16px 0; width:100%; border: 1px solid #ddd; - box-shadow: inset 0 1px 2px rgba(0,0,0,.07);"/> - <input type="submit" class="button button-primary button-large" - value="<?php echo esc_attr( __( 'Send', 'jetpack' ) ); ?>"/> + <p><label for="email"><?php esc_html_e( 'Your email', 'jetpack' ); ?><br/></label> + <input type="email" name="email" class="text-input"/> + <input type="submit" class="button" + value="<?php esc_attr_e( 'Send email', 'jetpack' ); ?>"/> </p> </form> </div> @@ -334,4 +325,287 @@ class Jetpack_Protect_Blocked_Login_Page { return $contents; } + + function display_page( $title, $message, $back_button = false, $recovery_form = false ) { + + if ( ! headers_sent() ) { + nocache_headers(); + header( 'Content-Type: text/html; charset=utf-8' ); + } + + $text_direction = 'ltr'; + if ( is_rtl() ) { + $text_direction = 'rtl'; + } + ?> + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" <?php if ( function_exists( 'language_attributes' ) && function_exists( 'is_rtl' ) ) { + language_attributes(); + } else { + echo "dir='$text_direction'"; + } ?>> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width"> + <?php + if ( function_exists( 'wp_no_robots' ) ) { + wp_no_robots(); + } + ?> + <title><?php echo $title ?></title> + <style type="text/css"> + html { + background: #f3f6f8; + } + + body { + color: #2e4453; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + margin: 2em auto; + padding: 1em 2em; + max-width: 460px; + text-align: left; + } + body.is-rtl { + text-align: right; + } + h1 { + clear: both; + color: #3d596d; + font-size: 24px; + margin:0 0 24px 0; + padding: 0; + font-weight: 400; + } + + #error-message { + box-sizing: border-box; + background: white; + box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3; + padding: 24px; + } + + #error-message img { + margin: 0 auto; + display: block; + } + + #error-page { + margin-top: 50px; + } + + #error-page p { + font-size: 14px; + line-height: 1.5; + margin: 24px 0 0; + } + + #error-page code { + font-family: Consolas, Monaco, monospace; + } + + ul li { + margin-bottom: 10px; + font-size: 14px; + } + + a { + color: #00aadc; + } + + label { + font-weight: bold; + font-size:16px; + } + + a:hover, + a:active { + color: #0085be; + } + + a:focus { + color: #124964; + -webkit-box-shadow: 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); + box-shadow: 0 0 0 1px #5b9dd9, + 0 0 2px 1px rgba(30, 140, 190, .8); + outline: none; + } + + .button { + background: #00aadc; + color: white; + border-color: #008ab3; + border-style: solid; + border-width: 1px 1px 2px; + cursor: pointer; + display: inline-block; + margin: 0; + margin-right: 0px; + outline: 0; + overflow: hidden; + font-weight: 500; + text-overflow: ellipsis; + text-decoration: none; + vertical-align: top; + box-sizing: border-box; + font-size: 14px; + line-height: 21px; + border-radius: 4px; + padding: 7px 14px 9px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + font-size: 14px; + width: 100%; + } + + .button:hover, + .button:focus { + border-color: #005082; + outline: none; + } + + .button:focus { + border-color: #005082; + -webkit-box-shadow: 0 0 3px rgba(0, 115, 170, .8); + box-shadow: 0 0 3px rgba(0, 115, 170, .8); + outline: none; + } + .button::-moz-focus-inner { + border: 0; + } + + .button:active { + border-width: 2px 1px 1px; + } + .gridicon { + fill: currentColor; + vertical-align: middle; + } + #error-footer { + padding: 16px; + } + #error-footer a { + text-decoration: none; + line-height:20px; + font-size: 14px; + color: #4f748e; + } + #error-footer a:hover { + color: #2e4453; + } + #error-footer .gridicon{ + width: 16px; + } + #error-footer .gridicons-help { + width: 24px; + margin-right:8px; + } + + .is-rtl #error-footer .gridicons-help { + margin-left:8px; + } + + .error { + background: #d94f4f; + color:#FFF; + display: block; + border-radius: 3px; + line-height: 1.5; + padding: 16px; + padding-left: 42px; + } + .is-rtl .error { + padding-right: 42px; + } + .error .gridicon { + float: left; + margin-left: -32px; + } + + .is-rtl .error .gridicon { + float: right; + margin-right: -32px; + } + + .text-input { + margin: 0; + padding: 7px 14px; + width: 100%; + color: #2e4453; + font-size: 16px; + line-height: 1.5; + border: 1px solid #c8d7e1; + background-color: white; + transition: all .15s ease-in-out; + box-sizing: border-box; + margin: 8px 0 16px; + } + #image { + display: block; + width: 200px; + margin: 0 auto; + } + <?php + $rtl_class = ''; + if ( 'rtl' == $text_direction ) { + $rtl_class = 'class="is-rtl"'; + echo 'body { font-family: Tahoma, Arial; }'; + } + ?> + </style> + </head> + <body id="error-page" <?php echo $rtl_class; ?>> + <h1 id="error-title"><?php echo esc_html( $title ); ?></h1> + <div id="error-message"> + <svg id="image" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 250 134"> + <path fill="#E9EFF4" d="M205.2,129.8c3.7-0.7,7.4-0.9,11.1-1.1l5.5-0.1l5.5,0c3.7,0,7.4,0.1,11.1,0.2c3.7,0.1,7.4,0.3,11.1,0.8 c0.3,0,0.5,0.3,0.5,0.6c0,0.2-0.2,0.4-0.5,0.5c-3.7,0.5-7.4,0.6-11.1,0.8c-3.7,0.1-7.4,0.2-11.1,0.2l-5.5,0l-5.5-0.1 c-3.7-0.1-7.4-0.4-11.1-1.1c-0.1,0-0.2-0.2-0.2-0.3C205,129.9,205.1,129.8,205.2,129.8"/> + <path fill="#E9EFF4" d="M0.2,130.9c3-0.7,5.9-0.9,8.9-1.1l4.4-0.1l4.4,0c3,0,5.9,0.1,8.9,0.2c3,0.1,5.9,0.3,8.9,0.8 c0.3,0,0.5,0.3,0.4,0.6c0,0.2-0.2,0.4-0.4,0.4c-3,0.5-5.9,0.6-8.9,0.8c-3,0.1-5.9,0.2-8.9,0.2l-4.4,0l-4.4-0.1 c-3-0.1-5.9-0.4-8.9-1.1c-0.1,0-0.2-0.2-0.2-0.3C0,131,0.1,130.9,0.2,130.9"/> + <path fill="#C8D7E2" d="M101.6,130.1H70.1V52.5c0-8.5,6.9-15.3,15.3-15.3h16.1V130.1z"/> + <path fill="#0DA9DD" d="M191.5,130.1h-73.8v-5.4c0-8.9,7.2-16.1,16.1-16.1h57.7V130.1z"/> + <path fill="#C7E9F5" d="M55.2,25.6l-0.1,9.8L55,57l-0.1,21.6c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4L56.6,57l0.8-21.6 c0.1-3.3,0.2-6.5,0.3-9.8H55.2z"/> + <path fill="#C7E9F5" d="M203.1,25.6l0.1,18.1c0.2,28.8,0.4,57.6,1.2,86.3c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.8-0.3,0.8-0.8 c0.8-28.8,1-57.6,1.2-86.3l0.1-18.1H203.1z"/> + <path fill="#7FD3F2" d="M55.3,25.6v-8.2v-6.8c0-5.9,4-10.7,9-10.7h134c5,0,9,4.8,9,10.7v14.9H55.3z"/> + <path fill="#005083" d="M210.7,25.6c-13.3,1.1-26.7,1-40,1l-40,0.2l-40-0.2c-13.3-0.1-26.7,0-40-1V25c13.3-1.1,26.7-1,40-1l40-0.2 l40,0.2c13.3,0.1,26.7,0,40,1V25.6z"/> + <polygon fill="#C7E9F5" points="168.7,95.6 117.7,95.6 117.7,44.6 "/> + <path fill="#C8D7E2" d="M191.5,56.5c0,11-8.9,19.9-19.9,19.9c-11,0-19.9-8.9-19.9-19.9c0-11,8.9-19.9,19.9-19.9 C182.6,36.6,191.5,45.5,191.5,56.5"/> + <path fill="#FFFFFF" d="M213.2,95.5c-3.3-5.1-3.2-16.7-3.2-28.4h-32.3c0,0-5.2,25.5,4.6,33c7.5-0.1,29.9-0.6,29.9-0.6"/> + <path fill="#C8D7E2" d="M213.5,95.3l-0.1-0.1l-0.3-0.5c-0.2-0.4-0.3-0.7-0.5-1.1c-0.3-0.8-0.5-1.6-0.7-2.4c-0.1-0.5-0.2-1.1-0.3-1.6 c-0.4,0-0.8,0-1.2,0c0.5,2.1,1.1,4.3,2.4,6.1l0.2,0.2c0.2,0,0.4-0.1,0.5-0.3C213.6,95.5,213.6,95.4,213.5,95.3L213.5,95.3z"/> + <path fill="#C8D7E2" d="M212.5,98.6c-0.1,0-0.2,0-0.3,0l-0.1,0H212l-0.3,0l-0.6,0l-1.3,0l-2.5,0l-5,0l-19.5,0.2 c-1.9-1.7-3.1-4.1-3.8-6.5c-0.8-2.6-1.1-5.4-1.2-8.2c-0.2-5.2,0.3-10.4,1.1-15.6l5.7-0.1c0-0.9,0-1.8,0-2.6l-4.4,0l-2.5,0 c-0.4,0-0.8,0.2-1,0.5c-0.1,0.2-0.2,0.3-0.3,0.5l-0.1,0.3l-0.2,1.2c-0.3,1.7-0.5,3.3-0.7,5c-0.3,3.3-0.5,6.7-0.4,10.1 c0.1,3.4,0.5,6.7,1.5,10c0.5,1.6,1.2,3.2,2.2,4.7c0.5,0.7,1,1.4,1.7,2c0.3,0.3,0.6,0.6,1,0.9l0.1,0.1c0.1,0,0.2,0.1,0.3,0.2 c0.2,0.1,0.5,0.1,0.6,0.1l0.6,0l20-0.6l5-0.2l2.5-0.1l1.2,0l0.3,0l0.2,0c0,0,0.3,0,0.4-0.1c0.3-0.2,0.5-0.5,0.5-0.9 C213.1,99.1,212.9,98.7,212.5,98.6z"/> + <path fill="#FFFFFF" d="M223.1,84.8c-3.3-5.1-4.8-16.7-4.8-28.4h-32.3c0,0-3.5,25.5,6.3,33c7.5-0.1,29.9-0.6,29.9-0.6"/> + <path fill="#C8D7E2" d="M222.9,84.9c-1.3-2.1-2.2-4.4-2.8-6.7c-0.6-2.4-1.1-4.8-1.5-7.2c-0.7-4.8-1-9.1-1-13.9l0,0l-31,0.1l0,0 c-0.4,2.8-0.5,5.1-0.5,7.9c-0.1,2.9,0,5.7,0.3,8.6c0.3,2.8,0.8,5.7,1.7,8.3c0.9,2.6,2.3,5.2,4.5,6.9l-0.4-0.1l14.9-0.2 c5-0.1,10-0.1,14.9-0.1c0.1,0,0.3,0.1,0.3,0.3c0,0.1-0.1,0.3-0.2,0.3c-5,0.2-10,0.4-14.9,0.5l-14.9,0.4c-0.1,0-0.3,0-0.4-0.1l0,0 c-2.5-1.9-3.9-4.7-5-7.4c-1-2.8-1.5-5.7-1.9-8.6c-0.3-2.9-0.4-5.8-0.4-8.8c0.1-2.9,0.2-5.8,0.6-8.8c0-0.4,0.4-0.6,0.7-0.6h0 l32.3,0.1h0c0.3,0,0.6,0.3,0.6,0.6v0c0,4.8,0.2,9.6,0.7,14.4c0.3,2.4,0.6,4.8,1.2,7.1c0.5,2.3,1.2,4.7,2.4,6.8c0,0.1,0,0.1,0,0.2 C223.1,85,223,85,222.9,84.9"/> + <path fill="#C8D7E2" d="M192.1,67.1c1.6-0.9,3.4-1.2,5.1-1.3c1.7-0.2,3.5-0.2,5.2-0.2c3.5,0.1,6.9,0.2,10.3,1c0.1,0,0.2,0.2,0.2,0.3 c0,0.1-0.1,0.2-0.2,0.2c-3.4,0.2-6.9,0-10.3,0c-1.7,0-3.4,0-5.1,0c-1.7,0-3.4,0.1-5.1,0.3l0,0c-0.1,0-0.1,0-0.1-0.1 C192,67.2,192.1,67.1,192.1,67.1"/> + <path fill="#C8D7E2" d="M194.1,74c1.4,0,2.7,0,4.1,0c1.4,0,2.7,0,4.1,0c2.7,0,5.4-0.1,8.2-0.2c0.1,0,0.3,0.1,0.3,0.3 c0,0.1-0.1,0.2-0.2,0.3c-1.3,0.5-2.7,0.7-4.1,0.9c-1.4,0.2-2.8,0.2-4.2,0.3c-1.4,0-2.8,0-4.2-0.2c-1.4-0.2-2.8-0.4-4.1-1.1 c-0.1,0-0.1-0.1,0-0.2C193.9,74.1,194,74,194.1,74L194.1,74z"/> + <path fill="#86A6BD" d="M40.2,88.6c-0.5,0-0.8-0.4-0.9-0.9l-0.1-8.2c0-0.7,0-1.4,0-2.1c0.1-0.7,0.2-1.5,0.4-2.2c0.4-1.4,1-2.8,1.9-4 c1.7-2.5,4.3-4.3,7.1-5.1c0.7-0.2,1.5-0.3,2.2-0.5c0.7-0.1,1.5-0.1,2.2-0.1c1.3,0,2.9,0,4.4,0.4c2.9,0.7,5.6,2.5,7.4,4.9 c0.9,1.2,1.6,2.6,2.1,4c0.5,1.4,0.6,3,0.6,4.4l0,16.4c0,0.7-0.6,1.3-1.3,1.3l-6.7,0c-0.7,0-1.3-0.6-1.3-1.3v0l0-10.8l0-5.4 c0-1.4-0.7-2.8-1.8-3.5c-0.6-0.4-1.3-0.6-2-0.7c-0.7,0-1.9,0-2.5,0c-1.4,0.1-2.7,1-3.3,2.3c-0.3,0.7-0.4,1.3-0.4,2.1l0,2.7 l-0.1,5.4l0,0c0,0.5-0.4,0.9-1,0.9"/> + <path fill="#FFFFFF" d="M41.1,86.9l0.1-7.3c-0.1-2.6,0.7-5,2.1-7.1c1.4-2,3.6-3.5,5.9-4.1c0.6-0.2,1.2-0.3,1.8-0.3 c0.6,0,1.2-0.1,1.9,0c1.4,0,2.5,0,3.7,0.4c2.4,0.6,4.5,2,5.9,4c0.7,1,1.3,2.1,1.6,3.2c0.4,1.2,0.5,2.3,0.5,3.7l0,15.1l0,0l-4.2,0 l0-9.5l0-5.4c0-2.2-1.2-4.4-3-5.5c-0.9-0.6-2-0.9-3.1-1c-1.1,0-1.7,0-2.9,0c-2.2,0.2-4.2,1.7-5.1,3.6c-0.5,0.9-0.7,2.1-0.6,3.1 l0,2.7l0.1,4.4l0,0L41.1,86.9L41.1,86.9"/> + <path fill="#86A6BD" d="M36.3,133c-1.9,0-3.8-1.1-4.8-2.8c-0.5-0.8-0.7-1.8-0.7-2.8l0-2.4l0-9.6l-0.1-9.6l0-4.8c0-0.7,0-1.8,0.3-2.8 c0.3-1,0.9-1.8,1.7-2.5c0.8-0.6,1.7-1.1,2.7-1.3c1.1-0.2,1.8-0.1,2.6-0.1l4.8,0l9.6-0.1l19.2,0c2.1,0,4.1,1.2,5.1,3 c0.5,0.9,0.8,2,0.8,3l0,2.4l0,9.6l-0.1,9.6l0,4.8c0,0.7,0,1.8-0.4,2.8c-0.3,0.9-1,1.8-1.7,2.4c-0.8,0.6-1.7,1.1-2.7,1.2 c-1.1,0.1-1.8,0-2.6,0.1l-4.8,0l-9.6-0.1L36.3,133z"/> + <path fill="#FFFFFF" d="M74.8,112.3l-0.1-9.6l0-2.4c0-0.6-0.1-1.1-0.4-1.6c-0.6-1-1.7-1.6-2.8-1.6l-19.2,0L42.7,97l-4.8,0 c-0.8,0-1.7,0-2.2,0c-0.6,0.1-1.1,0.3-1.6,0.7c-0.5,0.4-0.8,0.9-1,1.4c-0.2,0.6-0.2,1.1-0.2,2l0,4.8l-0.1,9.6l0,9.6l0,2.4 c0,0.6,0.2,1.3,0.5,1.8c0.6,1.1,1.9,1.8,3.1,1.8l19.2-0.1l9.6-0.1l4.8,0c0.8,0,1.7,0,2.2-0.1c0.6-0.1,1.2-0.4,1.6-0.8 c0.5-0.4,0.8-0.9,1-1.5c0.2-0.6,0.2-1.1,0.2-2l0-4.8L74.8,112.3z"/> + <path fill="#86A6BD" d="M48.1,121.4l2.9-6.2c0.3-0.6,0.2-1.3-0.3-1.8c-1-1-1.5-2.5-1.2-4c0.3-1.7,1.7-3.1,3.4-3.4 c2.9-0.6,5.4,1.6,5.4,4.4c0,1.2-0.5,2.3-1.3,3.1c-0.5,0.5-0.6,1.2-0.3,1.8l2.9,6.2c0.1,0.2-0.1,0.5-0.3,0.5H48.4 C48.1,121.9,48,121.6,48.1,121.4"/> + </svg> + + <?php echo $message; ?> + <?php if ( $recovery_form ) { + echo $this->get_html_recovery_form(); + } ?> + </div> + <div id="error-footer"> + <?php if ( $back_button && ! $recovery_form ) { + if ( 'rtl' == $text_direction ) { + $back_button_icon = '<svg class="gridicon gridicons-arrow-right" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></g></svg>'; + } else { + $back_button_icon = '<svg class="gridicon gridicons-arrow-left" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></g></svg>'; + } + ?> + <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> + <?php } ?> + </div> + </body> + </html> + <?php + die(); + } } diff --git a/plugins/jetpack/modules/protect/jetpack-security.png b/plugins/jetpack/modules/protect/jetpack-security.png Binary files differdeleted file mode 100644 index 189ae6b3..00000000 --- a/plugins/jetpack/modules/protect/jetpack-security.png +++ /dev/null diff --git a/plugins/jetpack/modules/protect/math-fallback.php b/plugins/jetpack/modules/protect/math-fallback.php index 69d283e7..94b5d942 100644 --- a/plugins/jetpack/modules/protect/math-fallback.php +++ b/plugins/jetpack/modules/protect/math-fallback.php @@ -37,7 +37,9 @@ if ( ! class_exists( 'Jetpack_Protect_Math_Authenticate' ) ) { $correct_ans = isset( $_POST[ 'jetpack_protect_answer' ] ) ? $_POST[ 'jetpack_protect_answer' ] : '' ; if( isset( $_COOKIE[ 'jpp_math_pass' ] ) ) { - $transient = Jetpack_Protect_Module::get_transient( 'jpp_math_pass_' . $_COOKIE[ 'jpp_math_pass' ] ); + $jetpack_protect = Jetpack_Protect_Module::instance(); + $transient = $jetpack_protect->get_transient( 'jpp_math_pass_' . $_COOKIE[ 'jpp_math_pass' ] ); + if( !$transient || $transient < 1 ) { Jetpack_Protect_Math_Authenticate::generate_math_page(); } @@ -100,7 +102,9 @@ if ( ! class_exists( 'Jetpack_Protect_Math_Authenticate' ) ) { Jetpack_Protect_Math_Authenticate::generate_math_page(true); } else { $temp_pass = substr( sha1( rand( 1, 100000000 ) . get_site_option( 'jetpack_protect_key' ) ), 5, 25 ); - Jetpack_Protect_Module::set_transient( 'jpp_math_pass_' . $temp_pass, 3, DAY_IN_SECONDS ); + + $jetpack_protect = Jetpack_Protect_Module::instance(); + $jetpack_protect->set_transient( 'jpp_math_pass_' . $temp_pass, 3, DAY_IN_SECONDS ); setcookie('jpp_math_pass', $temp_pass, time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false); return true; } diff --git a/plugins/jetpack/modules/protect/protect.png b/plugins/jetpack/modules/protect/protect.png Binary files differnew file mode 100644 index 00000000..67bcdbad --- /dev/null +++ b/plugins/jetpack/modules/protect/protect.png diff --git a/plugins/jetpack/modules/publicize/publicize.php b/plugins/jetpack/modules/publicize/publicize.php index 75c9333e..8f051f75 100644 --- a/plugins/jetpack/modules/publicize/publicize.php +++ b/plugins/jetpack/modules/publicize/publicize.php @@ -260,7 +260,7 @@ abstract class Publicize_Base { /** * Fires when a post is saved, checks conditions and saves state in postmeta so that it - * can be picked up later by @see ::publicize_post() + * can be picked up later by @see ::publicize_post() on WordPress.com codebase. */ function save_meta( $post_id, $post ) { $cron_user = null; diff --git a/plugins/jetpack/modules/publicize/ui.php b/plugins/jetpack/modules/publicize/ui.php index 7d5e3f36..9d347e35 100644 --- a/plugins/jetpack/modules/publicize/ui.php +++ b/plugins/jetpack/modules/publicize/ui.php @@ -78,7 +78,10 @@ class Publicize_UI { function load_assets() { wp_enqueue_script( 'publicize', - plugins_url( 'assets/publicize.js', __FILE__ ), + Jetpack::get_file_url_for_environment( + '_inc/build/publicize/assets/publicize.min.js', + 'modules/publicize/assets/publicize.js' + ), array( 'jquery', 'thickbox' ), '20121019' ); diff --git a/plugins/jetpack/modules/related-posts/class.related-posts-customize.php b/plugins/jetpack/modules/related-posts/class.related-posts-customize.php index 4b937396..ae1d1d78 100644 --- a/plugins/jetpack/modules/related-posts/class.related-posts-customize.php +++ b/plugins/jetpack/modules/related-posts/class.related-posts-customize.php @@ -156,6 +156,15 @@ class Jetpack_Related_Posts_Customize { restore_previous_locale(); } + /** + * The filter allows you to change the options used to display Related Posts in the Customizer. + * + * @module related-posts + * + * @since 4.4.0 + * + * @param array $options Array of options used to display Related Posts in the Customizer. + */ return apply_filters( 'jetpack_related_posts_customize_options', array( 'enabled' => array( @@ -235,7 +244,15 @@ class Jetpack_Related_Posts_Customize { * @since 4.4.0 */ function customize_controls_enqueue_scripts() { - wp_enqueue_script( 'jetpack_related-posts-customizer', plugins_url( 'related-posts-customizer.js', __FILE__ ), array( 'customize-controls' ), JETPACK__VERSION); + wp_enqueue_script( + 'jetpack_related-posts-customizer', + Jetpack::get_file_url_for_environment( + '_inc/build/related-posts/related-posts-customizer.min.js', + 'modules/related-posts/related-posts-customizer.js' + ), + array( 'customize-controls' ), + JETPACK__VERSION + ); } } // class end diff --git a/plugins/jetpack/modules/related-posts/jetpack-related-posts.php b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php index c9255afd..131d6a93 100644 --- a/plugins/jetpack/modules/related-posts/jetpack-related-posts.php +++ b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php @@ -1449,7 +1449,15 @@ EOT; protected function _enqueue_assets( $script, $style ) { $dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' ); if ( $script ) { - wp_enqueue_script( 'jetpack_related-posts', plugins_url( 'related-posts.js', __FILE__ ), $dependencies, self::VERSION ); + wp_enqueue_script( + 'jetpack_related-posts', + Jetpack::get_file_url_for_environment( + '_inc/build/related-posts/related-posts.min.js', + 'modules/related-posts/related-posts.js' + ), + $dependencies, + self::VERSION + ); $related_posts_js_options = array( /** * Filter each Related Post Heading structure. diff --git a/plugins/jetpack/modules/search.php b/plugins/jetpack/modules/search.php index 82a71c4c..585fe0b6 100644 --- a/plugins/jetpack/modules/search.php +++ b/plugins/jetpack/modules/search.php @@ -4,6 +4,7 @@ * Module Name: Search * Module Description: Enhanced search, powered by Elasticsearch * First Introduced: 5.0 + * Sort Order: 34 * Free: false * Requires Connection: Yes * Auto Activate: No diff --git a/plugins/jetpack/modules/search/class.jetpack-search-helpers.php b/plugins/jetpack/modules/search/class.jetpack-search-helpers.php new file mode 100644 index 00000000..e235ba9a --- /dev/null +++ b/plugins/jetpack/modules/search/class.jetpack-search-helpers.php @@ -0,0 +1,699 @@ +<?php +/** + * Jetpack Search: Jetpack_Search_Helpers class + * + * @package Jetpack + * @subpackage Jetpack Search + * @since 5.8.0 + */ + +/** + * Various helper functions for reuse throughout the Jetpack Search code. + * + * @since 5.8.0 + */ +class Jetpack_Search_Helpers { + + /** + * The search widget's base ID. + * + * @since 5.8.0 + * @var string + */ + const FILTER_WIDGET_BASE = 'jetpack-search-filters'; + + /** + * Create a URL for the current search that doesn't include the "paged" parameter. + * + * @since 5.8.0 + * + * @return string The search URL. + */ + static function get_search_url() { + $query_args = $_GET; + + // Handle the case where a permastruct is being used, such as /search/{$query} + if ( ! isset( $query_args['s'] ) ) { + $query_args['s'] = get_search_query(); + } + + if ( isset( $query_args['paged'] ) ) { + unset( $query_args['paged'] ); + } + + $query = http_build_query( $query_args ); + + return home_url( "?{$query}" ); + } + + /** + * Wraps add_query_arg() with the URL defaulting to the current search URL. + * + * @see add_query_arg() + * + * @since 5.8.0 + * + * @param string|array $key Either a query variable key, or an associative array of query variables. + * @param string $value Optional. A query variable value. + * @param bool|string $url Optional. A URL to act upon. Defaults to the current search URL. + * + * @return string New URL query string (unescaped). + */ + static function add_query_arg( $key, $value = false, $url = false ) { + $url = empty( $url ) ? self::get_search_url() : $url; + if ( is_array( $key ) ) { + return add_query_arg( $key, $url ); + } + + return add_query_arg( $key, $value, $url ); + } + + /** + * Wraps remove_query_arg() with the URL defaulting to the current search URL. + * + * @see remove_query_arg() + * + * @since 5.8.0 + * + * @param string|array $key Query key or keys to remove. + * @param bool|string $query Optional. A URL to act upon. Defaults to the current search URL. + * + * @return string New URL query string (unescaped). + */ + static function remove_query_arg( $key, $url = false ) { + $url = empty( $url ) ? self::get_search_url() : $url; + + return remove_query_arg( $key, $url ); + } + + /** + * Returns the name of the search widget's option. + * + * @since 5.8.0 + * + * @return string The search widget option name. + */ + static function get_widget_option_name() { + return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE ); + } + + /** + * Returns the search widget instances from the widget's option. + * + * @since 5.8.0 + * + * @return array The widget options. + */ + static function get_widgets_from_option() { + $widget_options = get_option( self::get_widget_option_name(), array() ); + + // We don't need this + if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) { + unset( $widget_options['_multiwidget'] ); + } + + return $widget_options; + } + + /** + * Returns the widget ID (widget base plus the numeric ID). + * + * @param int $number The widget's numeric ID. + * + * @return string The widget's numeric ID prefixed with the search widget base. + */ + static function build_widget_id( $number ) { + return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number ); + } + + /** + * Wrapper for is_active_widget() with the other parameters automatically supplied. + * + * @see is_active_widget() + * + * @since 5.8.0 + * + * @param int $widget_id Widget ID. + * + * @return bool Whether the widget is active or not. + */ + static function is_active_widget( $widget_id ) { + return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE, true ); + } + + /** + * Returns an array of the filters from all active search widgets. + * + * @since 5.8.0 + * + * @return array Active filters. + */ + static function get_filters_from_widgets() { + $filters = array(); + + $widget_options = self::get_widgets_from_option(); + if ( empty( $widget_options ) ) { + return $filters; + } + + foreach ( (array) $widget_options as $number => $settings ) { + $widget_id = self::build_widget_id( $number ); + if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) { + continue; + } + + foreach ( (array) $settings['filters'] as $widget_filter ) { + $widget_filter['widget_id'] = $widget_id; + + if ( empty( $widget_filter['name'] ) ) { + $widget_filter['name'] = self::generate_widget_filter_name( $widget_filter ); + } + + $key = sprintf( '%s_%d', $widget_filter['type'], count( $filters ) ); + + $filters[ $key ] = $widget_filter; + } + } + + return $filters; + } + + /** + * Get the localized default label for a date filter. + * + * @since 5.8.0 + * + * @param string $type Date type, either year or month. + * @param bool $is_updated Whether the filter was updated or not (adds "Updated" to the end). + * + * @return string The filter label. + */ + static function get_date_filter_type_name( $type, $is_updated = false ) { + switch ( $type ) { + case 'year': + $string = ( $is_updated ) + ? esc_html_x( 'Year Updated', 'label for filtering posts', 'jetpack' ) + : esc_html_x( 'Year', 'label for filtering posts', 'jetpack' ); + break; + case 'month': + default: + $string = ( $is_updated ) + ? esc_html_x( 'Month Updated', 'label for filtering posts', 'jetpack' ) + : esc_html_x( 'Month', 'label for filtering posts', 'jetpack' ); + break; + } + + return $string; + } + + /** + * Creates a default name for a filter. Used when the filter label is blank. + * + * @since 5.8.0 + * + * @param array $widget_filter The filter to generate the title for. + * + * @return string The suggested filter name. + */ + static function generate_widget_filter_name( $widget_filter ) { + $name = ''; + + switch ( $widget_filter['type'] ) { + case 'post_type': + $name = _x( 'Post Types', 'label for filtering posts', 'jetpack' ); + break; + + case 'date_histogram': + $modified_fields = array( + 'post_modified', + 'post_modified_gmt', + ); + switch ( $widget_filter['interval'] ) { + case 'year': + $name = self::get_date_filter_type_name( + 'year', + in_array( $widget_filter['field'], $modified_fields ) + ); + break; + case 'month': + default: + $name = self::get_date_filter_type_name( + 'month', + in_array( $widget_filter['field'], $modified_fields ) + ); + break; + } + break; + + case 'taxonomy': + $tax = get_taxonomy( $widget_filter['taxonomy'] ); + if ( ! $tax ) { + break; + } + + if ( isset( $tax->label ) ) { + $name = $tax->label; + } elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) { + $name = $tax->labels->name; + } + break; + } + + return $name; + } + + /** + * Whether we should rerun a search in the customizer preview or not. + * + * @since 5.8.0 + * + * @return bool + */ + static function should_rerun_search_in_customizer_preview() { + // Only update when in a customizer preview and data is being posted. + // Check for $_POST removes an extra update when the customizer loads. + // + // Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests. + if ( ! isset( $GLOBALS['wp_customize'] ) || ! $GLOBALS['wp_customize']->is_preview() || empty( $_POST ) ) { + return false; + } + + return true; + } + + /** + * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays, + * if there are less values in array 1, it's possible to get an empty diff where one might be expected. + * + * @since 5.8.0 + * + * @param array $array_1 + * @param array $array_2 + * + * @return array + */ + static function array_diff( $array_1, $array_2 ) { + // If the array counts are the same, then the order doesn't matter. If the count of + // $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher, + // we need to swap the array order though. + if ( count( $array_1 ) !== count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) { + $temp = $array_1; + $array_1 = $array_2; + $array_2 = $temp; + } + + // Disregard keys + return array_values( array_diff( $array_1, $array_2 ) ); + } + + /** + * Given the widget instance, will return true when selected post types differ from searchable post types. + * + * @since 5.8.0 + * + * @param array $post_types An array of post types. + * + * @return bool + */ + static function post_types_differ_searchable( $post_types ) { + if ( empty( $post_types ) ) { + return false; + } + + $searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) ); + $diff_of_searchable = self::array_diff( $searchable_post_types, (array) $post_types ); + + return ! empty( $diff_of_searchable ); + } + + /** + * Given the array of post types, will return true when these differ from the current search query. + * + * @since 5.8.0 + * + * @param array $post_types An array of post types. + * + * @return bool + */ + static function post_types_differ_query( $post_types ) { + if ( empty( $post_types ) ) { + return false; + } + + if ( empty( $_GET['post_type'] ) ) { + $post_types_from_query = array(); + } elseif ( is_array( $_GET['post_type'] ) ) { + $post_types_from_query = $_GET['post_type']; + } else { + $post_types_from_query = (array) explode( ',', $_GET['post_type'] ); + } + + $post_types_from_query = array_map( 'trim', $post_types_from_query ); + + $diff_query = self::array_diff( (array) $post_types, $post_types_from_query ); + + return ! empty( $diff_query ); + } + + /** + * Determine what Tracks value should be used when updating a widget. + * + * @since 5.8.0 + * + * @param mixed $old_value The old option value. + * @param mixed $new_value The new option value. + * + * @return array|false False if the widget wasn't updated, otherwise an array of the Tracks action and widget properties. + */ + static function get_widget_tracks_value( $old_value, $new_value ) { + $old_value = (array) $old_value; + if ( isset( $old_value['_multiwidget'] ) ) { + unset( $old_value['_multiwidget'] ); + } + + $new_value = (array) $new_value; + if ( isset( $new_value['_multiwidget'] ) ) { + unset( $new_value['_multiwidget'] ); + } + + $old_keys = array_keys( $old_value ); + $new_keys = array_keys( $new_value ); + + if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added + $diff = self::array_diff( $new_keys, $old_keys ); + $action = 'widget_added'; + $widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] ) + ? false + : $new_value[ $diff[0] ]; + } elseif ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted + $diff = self::array_diff( $old_keys, $new_keys ); + $action = 'widget_deleted'; + $widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] ) + ? false + : $old_value[ $diff[0] ]; + } else { + $action = 'widget_updated'; + $widget = false; + + // This is a bit crazy. Since there can be multiple widgets stored in a single option, + // we need to diff the old and new values to figure out which widget was updated. + foreach ( $new_value as $key => $new_instance ) { + if ( ! isset( $old_value[ $key ] ) ) { + continue; + } + $old_instance = $old_value[ $key ]; + + // First, let's test the keys of each instance + $diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) ); + if ( ! empty( $diff ) ) { + $widget = $new_instance; + break; + } + + // Next, lets's loop over each value and compare it + foreach ( $new_instance as $k => $v ) { + if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) { + $widget = $new_instance; + break; + } + + if ( 'filters' == $k ) { + if ( count( $new_instance['filters'] ) != count( $old_instance['filters'] ) ) { + $widget = $new_instance; + break; + } + + foreach ( $v as $filter_key => $new_filter_value ) { + $diff = self::array_diff( $new_filter_value, $old_instance['filters'][ $filter_key ] ); + if ( ! empty( $diff ) ) { + $widget = $new_instance; + break; + } + } + } + } + } + } + + if ( empty( $action ) || empty( $widget ) ) { + return false; + } + + return array( + 'action' => $action, + 'widget' => self::get_widget_properties_for_tracks( $widget ), + ); + } + + /** + * Creates the widget properties for sending to Tracks. + * + * @since 5.8.0 + * + * @param array $widget The widget instance. + * + * @return array The widget properties. + */ + static function get_widget_properties_for_tracks( $widget ) { + $sanitized = array(); + + foreach ( (array) $widget as $key => $value ) { + if ( '_multiwidget' == $key ) { + continue; + } + + if ( is_scalar( $value ) ) { + $key = str_replace( '-', '_', sanitize_key( $key ) ); + $key = "widget_{$key}"; + $sanitized[ $key ] = $value; + } + } + + $filters_properties = ! empty( $widget['filters'] ) + ? self::get_filter_properties_for_tracks( $widget['filters'] ) + : array(); + + return array_merge( $sanitized, $filters_properties ); + } + + /** + * Creates the filter properties for sending to Tracks. + * + * @since 5.8.0 + * + * @param array $filters An array of filters. + * + * @return array The filter properties. + */ + static function get_filter_properties_for_tracks( $filters ) { + if ( empty( $filters ) ) { + return $filters; + } + + $filters_properties = array( + 'widget_filter_count' => count( $filters ), + ); + + foreach ( $filters as $filter ) { + if ( empty( $filter['type'] ) ) { + continue; + } + + $key = sprintf( 'widget_filter_type_%s', $filter['type'] ); + if ( isset( $filters_properties[ $key ] ) ) { + $filters_properties[ $key ] ++; + } else { + $filters_properties[ $key ] = 1; + } + } + + return $filters_properties; + } + + /** + * Gets the active post types given a set of filters. + * + * @since 5.8.0 + * + * @param array $filters The active filters for the current query. + * + * @return array The active post types. + */ + public static function get_active_post_types( $filters ) { + $active_post_types = array(); + + foreach ( $filters as $item ) { + if ( ( 'post_type' == $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) { + $active_post_types[] = $item['query_vars']['post_type']; + } + } + + return $active_post_types; + } + + /** + * Sets active to false on all post type buckets. + * + * @since 5.8.0 + * + * @param array $filters The available filters for the current query. + * + * @return array The filters for the current query with modified active field. + */ + public static function remove_active_from_post_type_buckets( $filters ) { + $modified = $filters; + foreach ( $filters as $key => $filter ) { + if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) { + foreach ( $filter['buckets'] as $k => $bucket ) { + $bucket['active'] = false; + $modified[ $key ]['buckets'][ $k ] = $bucket; + } + } + } + + return $modified; + } + + /** + * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args. + * + * @since 5.8.0 + * + * @param string $url The URL to add post types to. + * @param array $post_types An array of post types that should be added to the URL. + * + * @return string The URL with added post types. + */ + public static function add_post_types_to_url( $url, $post_types ) { + $url = Jetpack_Search_Helpers::remove_query_arg( 'post_type', $url ); + if ( empty( $post_types ) ) { + return $url; + } + + $url = Jetpack_Search_Helpers::add_query_arg( + 'post_type', + implode( ',', $post_types ), + $url + ); + + return $url; + } + + /** + * Since we provide support for the widget restricting post types by adding the selected post types as + * active filters, if removing a post type filter would result in there no longer be post_type args in the URL, + * we need to be sure to add them back. + * + * @since 5.8.0 + * + * @param array $filters An array of possible filters for the current query. + * @param array $post_types The post types to ensure are on the link. + * + * @return array The updated array of filters with post typed added to the remove URLs. + */ + public static function ensure_post_types_on_remove_url( $filters, $post_types ) { + $modified = $filters; + + foreach ( (array) $filters as $filter_key => $filter ) { + if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) { + $modified[ $filter_key ] = $filter; + continue; + } + + foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) { + if ( empty( $bucket['remove_url'] ) ) { + continue; + } + + $parsed = wp_parse_url( $bucket['remove_url'] ); + if ( ! $parsed ) { + continue; + } + + $query = array(); + if ( ! empty( $parsed['query'] ) ) { + wp_parse_str( $parsed['query'], $query ); + } + + if ( empty( $query['post_type'] ) ) { + $modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url( + $bucket['remove_url'], + $post_types + ); + } + } + } + + return $modified; + } + + /** + * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows + * developers to disable filters supplied by the search widget. Useful if filters are + * being defined at the code level. + * + * @since 5.8.0 + * + * @return bool + */ + public static function are_filters_by_widget_disabled() { + /** + * Allows developers to disable filters being set by widget, in favor of manually + * setting filters via `Jetpack_Search::set_filters()`. + * + * @module search + * + * @since 5.7.0 + * + * @param bool false + */ + return apply_filters( 'jetpack_search_disable_widget_filters', false ); + } + + /** + * Returns a boolean for whether the current site has a VIP index. + * + * @since 5.8.0 + * + * @return bool + */ + public static function site_has_vip_index() { + $has_vip_index = ( + Jetpack_Constants::is_defined( 'JETPACK_SEARCH_VIP_INDEX' ) && + Jetpack_Constants::get_constant( 'JETPACK_SEARCH_VIP_INDEX' ) + ); + + /** + * Allows developers to filter whether the current site has a VIP index. + * + * @module search + * + * @since 5.8.0 + * + * @param bool $has_vip_index Whether the current site has a VIP index. + */ + return apply_filters( 'jetpack_search_has_vip_index', $has_vip_index ); + } + + /** + * Returns the maximum posts per page for a search query. + * + * @since 5.8.0 + * + * @return int + */ + public static function get_max_posts_per_page() { + return self::site_has_vip_index() ? 1000 : 100; + } + + /** + * Returns the maximum offset for a search query. + * + * @since 5.8.0 + * + * @return int + */ + public static function get_max_offset() { + return self::site_has_vip_index() ? 9000 : 1000; + } +} diff --git a/plugins/jetpack/modules/search/class.jetpack-search-template-tags.php b/plugins/jetpack/modules/search/class.jetpack-search-template-tags.php new file mode 100644 index 00000000..303e6caa --- /dev/null +++ b/plugins/jetpack/modules/search/class.jetpack-search-template-tags.php @@ -0,0 +1,225 @@ +<?php +/** + * Jetpack Search: Jetpack_Search_Template_Tags class + * + * @package Jetpack + * @subpackage Jetpack Search + * @since 5.8.0 + */ + +/** + * Class that has various methods for outputting functionality into a theme that doesn't support widgets. + * Additionally the widget itself makes use of these class. + * + * @since 5.8.0 + */ +class Jetpack_Search_Template_Tags { + + /** + * Renders all available filters that can be used to filter down search results on the frontend. + * + * @since 5.8.0 + * + * @param array $filters The available filters for the current query. + * @param array $post_types An array of post types to make filterable + */ + public static function render_available_filters( $filters = null, $post_types = null ) { + if ( is_null( $filters ) ) { + $filters = Jetpack_Search::instance()->get_filters(); + } + + if ( is_null( $post_types ) ) { + $post_types = get_post_types( array( 'exclude_from_search' => false ) ); + } + + /** + * If the post types specified by the widget differ from the default set of searchable post types, + * then we need to track their state. + */ + $active_post_types = array(); + if ( Jetpack_Search_Helpers::post_types_differ_searchable( $post_types ) ) { + // get the active filter buckets from the query + $active_buckets = Jetpack_Search::instance()->get_active_filter_buckets(); + $post_types_differ_query = Jetpack_Search_Helpers::post_types_differ_query( $post_types ); + + // remove any post_type filters from display if the current query + // already specifies to match all post types + if ( ! $post_types_differ_query ) { + $active_buckets = array_filter( $active_buckets, array( __CLASS__, 'is_not_post_type_filter' ) ); + } + + $active_post_types = Jetpack_Search_Helpers::get_active_post_types( $active_buckets ); + if ( empty( $active_post_types ) ) { + $active_post_types = $post_types; + } + + if ( $post_types_differ_query ) { + $filters = Jetpack_Search_Helpers::ensure_post_types_on_remove_url( $filters, $post_types ); + } else { + $filters = Jetpack_Search_Helpers::remove_active_from_post_type_buckets( $filters ); + } + } else { + $post_types = array(); + } + + foreach ( (array) $filters as $filter ) { + if ( 'post_type' == $filter['type'] ) { + self::render_filter( $filter, $post_types ); + } else { + self::render_filter( $filter, $active_post_types ); + } + } + } + + /** + * Renders a single filter that can be applied to the current search. + * + * @since 5.8.0 + * + * @param array $filter The filter to render. + * @param array $default_post_types The default post types for this filter. + */ + public static function render_filter( $filter, $default_post_types ) { + if ( empty( $filter ) || empty( $filter['buckets'] ) ) { + return; + } + + $query_vars = null; + foreach ( $filter['buckets'] as $item ) { + if ( $item['active'] ) { + $query_vars = array_keys( $item['query_vars'] ); + break; + } + } + $clear_url = null; + if ( ! empty( $query_vars ) ) { + $clear_url = Jetpack_Search_Helpers::remove_query_arg( $query_vars ); + if ( ! empty( $default_post_types ) ) { + $clear_url = Jetpack_Search_Helpers::add_post_types_to_url( $clear_url, $default_post_types ); + } + } + + ?> + <h4 class="jetpack-search-filters-widget__sub-heading"> + <?php echo esc_html( $filter['name'] ); ?> + </h4> + <?php if ( $clear_url ) : ?> + <div class="jetpack-search-filters-widget__clear"> + <a href="<?php echo esc_url( $clear_url ); ?>"> + <?php esc_html_e( '< Clear Filters', 'jetpack' ); ?> + </a> + </div> + <?php endif; ?> + <ul class="jetpack-search-filters-widget__filter-list"> + <?php + foreach ( $filter['buckets'] as $item ) : + $url = ( empty( $item['active'] ) ) ? $item['url'] : $item['remove_url']; + ?> + <li> + <label> + <input type="checkbox"<?php checked( ! empty( $item['active'] ) ); ?> disabled="disabled" /> + <a href="<?php echo esc_url( $url ); ?>"> + <?php + echo esc_html( $item['name'] ); + echo ' '; + echo esc_html( sprintf( + '(%s)', + number_format_i18n( absint( $item['count'] ) ) + ) ); + ?> + </a> + </label> + </li> + <?php endforeach; ?> + </ul> + <?php + } + + /** + * Outputs the search widget's title. + * + * @since 5.8.0 + * + * @param string $title The widget's title + * @param string $before_title The HTML tag to display before the title + * @param string $after_title The HTML tag to display after the title + */ + public static function render_widget_title( $title, $before_title, $after_title ) { + echo $before_title . esc_html( $title ) . $after_title; + } + + /** + * Responsible for rendering the search box within our widget on the frontend. + * + * @since 5.8.0 + * + * @param array $post_types Array of post types to limit search results to. + * @param string $orderby How to order the search results. + * @param string $order In what direction to order the search results. + */ + public static function render_widget_search_form( $post_types, $orderby, $order ) { + $form = get_search_form( false ); + + $fields_to_inject = array( + 'orderby' => $orderby, + 'order' => $order + ); + + // If the widget has specified post types to search within and IF the post types differ + // from the default post types that would have been searched, set the selected post + // types via hidden inputs. + if ( Jetpack_Search_Helpers::post_types_differ_searchable( $post_types ) ) { + $fields_to_inject['post_type'] = implode( ',', $post_types ); + } + + $form = self::inject_hidden_form_fields( $form, $fields_to_inject ); + + echo '<div class="jetpack-search-form">'; + echo $form; + echo '</div>'; + } + + /** + * Modifies an HTML form to add some additional hidden fields. + * + * @since 5.8.0 + * + * @param string $form The form HTML to modify. + * @param array $fields Array of hidden fields to add. Key is field name and value is the field value. + * + * @return string The modified form HTML. + */ + private static function inject_hidden_form_fields( $form, $fields ) { + $form_injection = ''; + + foreach ( $fields as $field_name => $field_value ) { + $form_injection .= sprintf( + '<input type="hidden" name="%s" value="%s" />', + esc_attr( $field_name ), + esc_attr( $field_value ) + ); + } + + // This shouldn't need to be escaped since we've escaped above as we built $form_injection + $form = str_replace( + '</form>', + $form_injection . '</form>', + $form + ); + + return $form; + } + + /** + * Internal method for filtering out non-post_type filters. + * + * @since 5.8.0 + * + * @param array $filter + * + * @return bool + */ + private static function is_not_post_type_filter( $filter ) { + return 'post_type' !== $filter['type']; + } +}
\ No newline at end of file diff --git a/plugins/jetpack/modules/search/class.jetpack-search-widget-filters.php b/plugins/jetpack/modules/search/class.jetpack-search-widget-filters.php deleted file mode 100644 index 7de39e10..00000000 --- a/plugins/jetpack/modules/search/class.jetpack-search-widget-filters.php +++ /dev/null @@ -1,126 +0,0 @@ -<?php - - -/** - * Provides a widget to show available/selected filters on searches - */ -class Jetpack_Search_Widget_Filters extends WP_Widget { - - function __construct() { - if ( ! class_exists( 'Jetpack_Search' ) ) { - return; - } - - parent::__construct( - 'jetpack-search-filters', - /** This filter is documented in modules/widgets/facebook-likebox.php */ - apply_filters( 'jetpack_widget_name', esc_html__( 'Search Filters', 'jetpack' ) ), - array( - 'classname' => 'jetpack-filters', - 'description' => __( 'Displays search result filters when viewing search results.', 'jetpack' ), - ) - ); - } - - function widget( $args, $instance ) { - if ( ! class_exists( 'Jetpack_Search' ) || ! is_search() ) { - return; - } - - $search = Jetpack_Search::instance(); - - $filters = $search->get_filters(); - - $active_buckets = $search->get_active_filter_buckets(); - - if ( empty( $filters ) && empty( $active_buckets ) ) { - return; - } - - $buckets_found = false; - - foreach ( $filters as $filter ) { - if ( isset( $filter['buckets'] ) && count( $filter['buckets'] ) > 1 ) { - $buckets_found = true; - - break; - } - } - - if ( ! $buckets_found && empty( $active_buckets ) ) { - return; - } - - $title = $instance['title']; - - if ( empty( $title ) ) { - $title = __( 'Filter By', 'jetpack' ); - } - - /** This filter is documented in core/src/wp-includes/default-widgets.php */ - $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); - - echo $args['before_widget']; - - echo $args['before_title'] . esc_html( $title ) . $args['after_title']; - - if ( ! empty( $active_buckets ) ) { - echo '<h3>' . esc_html__( 'Current Filters', 'jetpack' ) . '</h3>'; - - echo '<ul>'; - - foreach ( $active_buckets as $item ) { - echo '<li><a href="' . esc_url( $item['remove_url'] ) . '">' . sprintf( _x( '(X) %1$s: %2$s', 'aggregation widget: active filter type and name', 'jetpack' ), esc_html( $item['type_label'] ), esc_html( $item['name'] ) ) . '</a></li>'; - } - - if ( count( $active_buckets ) > 1 ) { - echo '<li><a href="' . esc_url( add_query_arg( 's', get_query_var( 's' ), home_url() ) ) . '">' . esc_html__( 'Remove All Filters', 'jetpack' ) . '</a></li>'; - } - - echo '</ul>'; - } - - foreach ( $filters as $label => $filter ) { - if ( count( $filter['buckets'] ) < 2 ) { - continue; - } - - echo '<h3>' . esc_html( $label ) . '</h3>'; - - echo '<ul>'; - - foreach ( $filter['buckets'] as $item ) { - if ( $item['active'] ) { - continue; - } - - echo '<li><a href="' . esc_url( $item['url'] ) . '">' . esc_html( $item['name'] ) . '</a> (' . number_format_i18n( absint( $item['count'] ) ) . ')</li>'; - } - - echo '</ul>'; - } - - echo $args['after_widget']; - } - - function update( $new_instance, $old_instance ) { - $instance = array(); - - $instance['title'] = sanitize_text_field( $new_instance['title'] ); - - return $instance; - } - - function form( $instance ) { - $instance = wp_parse_args( (array) $instance, array( - 'title' => '', - ) ); - - $title = strip_tags( $instance['title'] ); - - ?> - <p><label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'jetpack' ); ?></label> - <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" /></p> - <?php - } -} diff --git a/plugins/jetpack/modules/search/class.jetpack-search.php b/plugins/jetpack/modules/search/class.jetpack-search.php index 9733d2a2..19b55e3a 100644 --- a/plugins/jetpack/modules/search/class.jetpack-search.php +++ b/plugins/jetpack/modules/search/class.jetpack-search.php @@ -1,54 +1,136 @@ <?php - +/** + * Jetpack Search: Main Jetpack_Search class + * + * @package Jetpack + * @subpackage Jetpack Search + * @since 5.0.0 + */ + +/** + * The main class for the Jetpack Search module. + * + * @since 5.0.0 + */ class Jetpack_Search { + /** + * The number of found posts. + * + * @since 5.0.0 + * + * @var int + */ protected $found_posts = 0; /** - * The maximum offset ('from' param), since deep pages get exponentially slower. + * The search result, as returned by the WordPress.com REST API. * - * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html + * @since 5.0.0 + * + * @var array */ - protected $max_offset = 200; - protected $search_result; - protected $original_blog_id; + /** + * This site's blog ID on WordPress.com. + * + * @since 5.0.0 + * + * @var int + */ protected $jetpack_blog_id; + /** + * The Elasticsearch aggregations (filters). + * + * @since 5.0.0 + * + * @var array + */ protected $aggregations = array(); + + /** + * The maximum number of aggregations allowed. + * + * @since 5.0.0 + * + * @var int + */ protected $max_aggregations_count = 100; - // used to output query meta into page - protected $last_query_info; - protected $last_query_failure_info; + /** + * Statistics about the last Elasticsearch query. + * + * @since 5.6.0 + * + * @var array + */ + protected $last_query_info = array(); + /** + * Statistics about the last Elasticsearch query failure. + * + * @since 5.6.0 + * + * @var array + */ + protected $last_query_failure_info = array(); + + /** + * The singleton instance of this class. + * + * @since 5.0.0 + * + * @var Jetpack_Search + */ protected static $instance; - //Languages with custom analyzers, other languages are supported, - // but are analyzed with the default analyzer. + /** + * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer. + * + * @since 5.0.0 + * + * @var array + */ public static $analyzed_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' ); + /** + * Jetpack_Search constructor. + * + * @since 5.0.0 + * + * Doesn't do anything. This class needs to be initialized via the instance() method instead. + */ protected function __construct() { - /* Don't do anything, needs to be initialized via instance() method */ } + /** + * Prevent __clone()'ing of this class. + * + * @since 5.0.0 + */ public function __clone() { wp_die( "Please don't __clone Jetpack_Search" ); } + /** + * Prevent __wakeup()'ing of this class. + * + * @since 5.0.0 + */ public function __wakeup() { wp_die( "Please don't __wakeup Jetpack_Search" ); } /** - * Get singleton instance of Jetpack_Search + * Get singleton instance of Jetpack_Search. * - * Instantiates and sets up a new instance if needed, or returns the singleton + * Instantiates and sets up a new instance if needed, or returns the singleton. * - * @module search + * @since 5.0.0 * - * @return Jetpack_Search The Jetpack_Search singleton + * @return Jetpack_Search The Jetpack_Search singleton. */ public static function instance() { if ( ! isset( self::$instance ) ) { @@ -61,11 +143,11 @@ class Jetpack_Search { } /** - * Perform various setup tasks for the class + * Perform various setup tasks for the class. * - * Checks various pre-requisites and adds hooks + * Checks various pre-requisites and adds hooks. * - * @module search + * @since 5.0.0 */ public function setup() { if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) { @@ -78,61 +160,170 @@ class Jetpack_Search { return; } + require_once( dirname( __FILE__ ) . '/class.jetpack-search-helpers.php' ); + require_once( dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php' ); + $this->init_hooks(); } /** - * Setup the various hooks needed for the plugin to take over Search duties + * Setup the various hooks needed for the plugin to take over search duties. * - * @module search + * @since 5.0.0 */ public function init_hooks() { - add_action( 'widgets_init', array( $this, 'action__widgets_init' ) ); - if ( ! is_admin() ) { add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 ); - add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 ); + add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 ); - add_action( 'did_jetpack_search_query', array( $this, 'store_query_success' ) ); + add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) ); add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) ); + + add_action( 'init', array( $this, 'set_filters_from_widgets' ) ); + + add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) ); + } else { + add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 ); } + + add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) ); } /** - * Print query info as a HTML comment in the footer + * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer. + * + * @since 5.6.0 + * + * @param array $meta Information about the failure. */ - public function store_query_failure( $meta ) { $this->last_query_failure_info = $meta; add_action( 'wp_footer', array( $this, 'print_query_failure' ) ); } + /** + * Outputs information about the last Elasticsearch failure. + * + * @since 5.6.0 + */ public function print_query_failure() { if ( $this->last_query_failure_info ) { - echo '<!-- Jetpack Search failed with code ' . $this->last_query_failure_info['response_code'] . ': ' . $this->last_query_failure_info['json']['error'] . ' - ' . $this->last_query_failure_info['json']['message'] . ' -->'; + printf( + '<!-- Jetpack Search failed with code %s: %s - %s -->', + esc_html( $this->last_query_failure_info['response_code'] ), + esc_html( $this->last_query_failure_info['json']['error'] ), + esc_html( $this->last_query_failure_info['json']['message'] ) + ); } } - public function store_query_success( $meta ) { + /** + * Stores information about the last Elasticsearch query and enqueues some debug information in the footer. + * + * @since 5.6.0 + * + * @param array $meta Information about the query. + */ + public function store_last_query_info( $meta ) { $this->last_query_info = $meta; add_action( 'wp_footer', array( $this, 'print_query_success' ) ); } + /** + * Outputs information about the last Elasticsearch search. + * + * @since 5.6.0 + */ public function print_query_success() { if ( $this->last_query_info ) { - echo '<!-- Jetpack Search took ' . intval( $this->last_query_info['elapsed_time'] ) . ' ms, ES time ' . $this->last_query_info['es_time'] . ' ms -->'; + printf( + '<!-- Jetpack Search took %s ms, ES time %s ms -->', + intval( $this->last_query_info['elapsed_time'] ), + esc_html( $this->last_query_info['es_time'] ) + ); + } + } + + /** + * Returns the last query information, or false if no information was stored. + * + * @since 5.8.0 + * + * @return bool|array + */ + public function get_last_query_info() { + return empty( $this->last_query_info ) ? false : $this->last_query_info; + } + + /** + * Returns the last query failure information, or false if no failure information was stored. + * + * @since 5.8.0 + * + * @return bool|array + */ + public function get_last_query_failure_info() { + return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info; + } + + /** + * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows + * developers to disable filters supplied by the search widget. Useful if filters are + * being defined at the code level. + * + * @since 5.7.0 + * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly. + * + * @return bool + */ + public function are_filters_by_widget_disabled() { + return Jetpack_Search_Helpers::are_filters_by_widget_disabled(); + } + + /** + * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget, + * and applies those filters to this Jetpack_Search object. + * + * @since 5.7.0 + */ + public function set_filters_from_widgets() { + if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) { + return; + } + + $filters = Jetpack_Search_Helpers::get_filters_from_widgets(); + + if ( ! empty( $filters ) ) { + $this->set_filters( $filters ); + } + } + + /** + * Restricts search results to certain post types via a GET argument. + * + * @since 5.8.0 + * + * @param WP_Query $query A WP_Query instance. + */ + public function maybe_add_post_type_as_var( WP_Query $query ) { + if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) { + $post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) ) + ? $post_type = explode( ',', $_GET['post_type'] ) + : (array) $_GET['post_type']; + $post_types = array_map( 'sanitize_key', $post_types ); + $query->set( 'post_type', $post_types ); } } /* - * Run a search on the WP.com public API. + * Run a search on the WordPress.com public API. * - * @module search + * @since 5.0.0 * - * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint + * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint. * - * @return object|WP_Error The response from the public api, or a WP_Error + * @return object|WP_Error The response from the public API, or a WP_Error. */ public function search( array $es_args ) { $endpoint = sprintf( '/sites/%s/search', $this->jetpack_blog_id ); @@ -141,22 +332,22 @@ class Jetpack_Search { $do_authenticated_request = false; if ( class_exists( 'Jetpack_Client' ) && - isset( $es_args['authenticated_request'] ) && - true === $es_args['authenticated_request'] ) { + isset( $es_args['authenticated_request'] ) && + true === $es_args['authenticated_request'] ) { $do_authenticated_request = true; } unset( $es_args['authenticated_request'] ); $request_args = array( - 'headers' => array( + 'headers' => array( 'Content-Type' => 'application/json', ), 'timeout' => 10, 'user-agent' => 'jetpack_search', ); - $request_body = json_encode( $es_args ); + $request_body = wp_json_encode( $es_args ); $start_time = microtime( true ); @@ -179,35 +370,23 @@ class Jetpack_Search { } $response_code = wp_remote_retrieve_response_code( $request ); - $response = json_decode( wp_remote_retrieve_body( $request ), true ); + $response = json_decode( wp_remote_retrieve_body( $request ), true ); - if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) { - /** - * Fires after a search query request has failed - * - * @module search - * - * @since 5.6.0 - * - * @param array Array containing the response code and response from the failed search query - */ - do_action( 'failed_jetpack_search_query', array( 'response_code' => $response_code, 'json' => $response ) ); - return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code ); - } - - $took = is_array( $response ) && $response['took'] ? $response['took'] : null; + $took = is_array( $response ) && ! empty( $response['took'] ) + ? $response['took'] + : null; $query = array( 'args' => $es_args, 'response' => $response, 'response_code' => $response_code, - 'elapsed_time' => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms + 'elapsed_time' => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms. 'es_time' => $took, 'url' => $service_url, ); /** - * Fires after a search request has been performed + * Fires after a search request has been performed. * * Includes the following info in the $query parameter: * @@ -220,36 +399,56 @@ class Jetpack_Search { * * @module search * - * @since 5.0.0 + * @since 5.0.0 + * @since 5.8.0 This action now fires on all queries instead of just successful queries. * * @param array $query Array of information about the query performed */ do_action( 'did_jetpack_search_query', $query ); + if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) { + /** + * Fires after a search query request has failed + * + * @module search + * + * @since 5.6.0 + * + * @param array Array containing the response code and response from the failed search query + */ + do_action( 'failed_jetpack_search_query', array( + 'response_code' => $response_code, + 'json' => $response, + ) ); + + return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code ); + } + return $response; } /** - * Bypass the normal Search query and offload it to Jetpack servers + * Bypass the normal Search query and offload it to Jetpack servers. * - * This is the main hook of the plugin and is responsible for returning the posts that match the search query + * This is the main hook of the plugin and is responsible for returning the posts that match the search query. * - * @module search + * @since 5.0.0 * - * @param array $posts Current array of posts (still pre-query) - * @param WP_Query $query The WP_Query being filtered + * @param array $posts Current array of posts (still pre-query). + * @param WP_Query $query The WP_Query being filtered. * - * @return array Array of matching posts + * @return array Array of matching posts. */ public function filter__posts_pre_query( $posts, $query ) { /** - * Determine whether a given WP_Query should be handled by ElasticSearch + * Determine whether a given WP_Query should be handled by ElasticSearch. * * @module search * - * @since 5.6.0 - * @param bool $should_handle Should be handled by Jetpack Search - * @param WP_Query $query The wp_query object + * @since 5.6.0 + * + * @param bool $should_handle Should be handled by Jetpack Search. + * @param WP_Query $query The WP_Query object. */ if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) { return $posts; @@ -274,15 +473,17 @@ class Jetpack_Search { // Query all posts now $args = array( - 'post__in' => $post_ids, - 'perm' => 'readable', - 'post_type' => 'any', + 'post__in' => $post_ids, + 'orderby' => 'post__in', + 'perm' => 'readable', + 'post_type' => 'any', + 'ignore_sticky_posts' => true, + 'suppress_filters' => true, ); $posts_query = new WP_Query( $args ); - // WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to - // do these manually + // WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually. $query->found_posts = $this->found_posts; $query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) ); @@ -290,22 +491,26 @@ class Jetpack_Search { } /** - * Build up the search, then run it against the Jetpack servers + * Build up the search, then run it against the Jetpack servers. + * + * @since 5.0.0 * - * @param WP_Query $query The original WP_Query to use for the parameters of our search + * @param WP_Query $query The original WP_Query to use for the parameters of our search. */ public function do_search( WP_Query $query ) { $page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1; - $posts_per_page = $query->get( 'posts_per_page' ); + // Get maximum allowed offset and posts per page values for the API. + $max_offset = Jetpack_Search_Helpers::get_max_offset(); + $max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page(); - // ES API does not allow more than 15 results at a time - if ( $posts_per_page > 15 ) { - $posts_per_page = 15; + $posts_per_page = $query->get( 'posts_per_page' ); + if ( $posts_per_page > $max_posts_per_page ) { + $posts_per_page = $max_posts_per_page; } - // Start building the WP-style search query args - // They'll be translated to ES format args later + // Start building the WP-style search query args. + // They'll be translated to ES format args later. $es_wp_query_args = array( 'query' => $query->get( 's' ), 'posts_per_page' => $posts_per_page, @@ -324,10 +529,8 @@ class Jetpack_Search { } $es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query ); - $es_wp_query_args['terms'] = $this->get_es_wp_query_terms_for_query( $query ); - /** * Modify the search query parameters, such as controlling the post_type. * @@ -335,30 +538,30 @@ class Jetpack_Search { * * @module search * - * @since 5.0.0 + * @since 5.0.0 * - * @param array $es_wp_query_args The current query args, in WP_Query format - * @param WP_Query $query The original query object + * @param array $es_wp_query_args The current query args, in WP_Query format. + * @param WP_Query $query The original WP_Query object. */ $es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query ); // If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is - // capped at $this->max_offset, so a high page would always return the last page of results otherwise - if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $this->max_offset ) { + // capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise. + if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) { $query->set_404(); return; } // If there were no post types returned, then 404 to avoid querying against non-public post types, which could - // happen if we don't add the post type restriction to the ES query + // happen if we don't add the post type restriction to the ES query. if ( empty( $es_wp_query_args['post_type'] ) ) { $query->set_404(); return; } - // Convert the WP-style args into ES args + // Convert the WP-style args into ES args. $es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args ); //Only trust ES to give us IDs, not the content since it is a mirror @@ -373,10 +576,10 @@ class Jetpack_Search { * * @module search * - * @since 5.0.0 + * @since 5.0.0 * - * @param array $es_query_args The raw ES query args - * @param WP_Query $query The original query object + * @param array $es_query_args The raw Elasticsearch query args. + * @param WP_Query $query The original WP_Query object. */ $es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query ); @@ -389,27 +592,48 @@ class Jetpack_Search { return; } - // If we have aggregations, fix the ordering to match the input order (ES doesn't - // guarantee the return order) + // If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order). if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) { $this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations ); } - // Total number of results for paging purposes. Capped at $this->>max_offset + $posts_per_page, as deep paging - // gets quite expensive - $this->found_posts = min( $this->search_result['results']['total'], $this->max_offset + $posts_per_page ); + // Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive. + $this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page ); + } + + /** + * If the query has already been run before filters have been updated, then we need to re-run the query + * to get the latest aggregations. + * + * This is especially useful for supporting widget management in the customizer. + * + * @since 5.8.0 + * + * @return bool Whether the query was successful or not. + */ + public function update_search_results_aggregations() { + if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) { + return false; + } - return; + $es_args = $this->last_query_info['args']; + $builder = new Jetpack_WPES_Query_Builder(); + $this->add_aggregations_to_es_query_builder( $this->aggregations, $builder ); + $es_args['aggregations'] = $builder->build_aggregation(); + + $this->search_result = $this->search( $es_args ); + + return ! is_wp_error( $this->search_result ); } /** - * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style ES term arguments for the search + * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search. * - * @module search + * @since 5.0.0 * - * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query + * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query. * - * @return array The new WP-style ES arguments (that will be converted into 'real' ES arguments) + * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments). */ public function get_es_wp_query_terms_for_query( WP_Query $query ) { $args = array(); @@ -446,20 +670,20 @@ class Jetpack_Search { } /** - * Parse out the post type from a WP_Query + * Parse out the post type from a WP_Query. * - * Only allows post types that are not marked as 'exclude_from_search' + * Only allows post types that are not marked as 'exclude_from_search'. * - * @module search + * @since 5.0.0 * - * @param WP_Query $query Original WP_Query object + * @param WP_Query $query Original WP_Query object. * - * @return array Array of searchable post types corresponding to the original query + * @return array Array of searchable post types corresponding to the original query. */ public function get_es_wp_query_post_type_for_query( WP_Query $query ) { $post_types = $query->get( 'post_type' ); - // If we're searching 'any', we want to only pass searchable post types to ES + // If we're searching 'any', we want to only pass searchable post types to Elasticsearch. if ( 'any' === $post_types ) { $post_types = array_values( get_post_types( array( 'exclude_from_search' => false, @@ -474,7 +698,7 @@ class Jetpack_Search { $sanitized_post_types = array(); - // Make sure the post types are queryable + // Make sure the post types are queryable. foreach ( $post_types as $post_type ) { if ( ! $post_type ) { continue; @@ -491,25 +715,15 @@ class Jetpack_Search { return $sanitized_post_types; } - /** - * Initialze widgets for the Search module - * - * @module search - */ - public function action__widgets_init() { - require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' ); - - register_widget( 'Jetpack_Search_Widget_Filters' ); - } /** - * Get the Elasticsearch result + * Get the Elasticsearch result. * - * @module search + * @since 5.0.0 * - * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response + * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response. * - * @return array|bool The search results, or false if there was a failure + * @return array|bool The search results, or false if there was a failure. */ public function get_search_result( $raw = false ) { if ( $raw ) { @@ -520,12 +734,14 @@ class Jetpack_Search { } /** - * Add the date portion of a WP_Query onto the query args + * Add the date portion of a WP_Query onto the query args. * - * @param array $es_wp_query_args - * @param WP_Query $query The original WP_Query + * @since 5.0.0 * - * @return array The es wp query args, with date filters added (as needed) + * @param array $es_wp_query_args The Elasticsearch query arguments in WordPress form. + * @param WP_Query $query The original WP_Query. + * + * @return array The es wp query args, with date filters added (as needed). */ public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) { if ( $query->get( 'year' ) ) { @@ -561,41 +777,31 @@ class Jetpack_Search { } /** - * Converts WP_Query style args to ES args + * Converts WP_Query style args to Elasticsearch args. * - * @module search + * @since 5.0.0 * - * @param array $args Array of WP_Query style arguments + * @param array $args Array of WP_Query style arguments. * - * @return array Array of ES style query arguments + * @return array Array of ES style query arguments. */ - function convert_wp_es_to_es_args( array $args ) { - jetpack_require_lib( 'jetpack-wpes-query-builder' ); - - $builder = new Jetpack_WPES_Query_Builder(); + public function convert_wp_es_to_es_args( array $args ) { + jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' ); $defaults = array( 'blog_id' => get_current_blog_id(), - 'query' => null, // Search phrase - 'query_fields' => array( 'title', 'content', 'author', 'tag', 'category' ), - - 'post_type' => null, // string or an array + 'query_fields' => array(), //list of fields to search + 'post_type' => null, // string or an array 'terms' => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) ) - 'author' => null, // id or an array of ids 'author_name' => array(), // string or an array - 'date_range' => null, // array( 'field' => 'date', 'gt' => 'YYYY-MM-dd', 'lte' => 'YYYY-MM-dd' ); date formats: 'YYYY-MM-dd' or 'YYYY-MM-dd HH:MM:SS' - 'orderby' => null, // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders. 'order' => 'DESC', - 'posts_per_page' => 10, - 'offset' => null, 'paged' => null, - /** * Aggregations. Examples: * array( @@ -603,11 +809,137 @@ class Jetpack_Search { * 'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ), * ); */ - 'aggregations' => null, + 'aggregations' => null, ); $args = wp_parse_args( $args, $defaults ); + $parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) ); + + if ( empty( $args['query_fields'] ) ) { + if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) { + // VIP indices do not have per language fields + $match_fields = array( + 'title^0.1', + 'content^0.1', + 'excerpt^0.1', + 'tag.name^0.1', + 'category.name^0.1', + 'author_login^0.1', + 'author^0.1', + ); + $boost_fields = array( + 'title^2', + 'tag.name', + 'category.name', + 'author_login', + 'author', + ); + $boost_phrase_fields = array( + 'title', + 'content', + 'excerpt', + 'tag.name', + 'category.name', + 'author', + ); + } else { + $match_fields = $parser->merge_ml_fields( + array( + 'title' => 0.1, + 'content' => 0.1, + 'excerpt' => 0.1, + 'tag.name' => 0.1, + 'category.name' => 0.1, + ), + array( + 'author_login^0.1', + 'author^0.1', + ) + ); + + $boost_fields = $parser->merge_ml_fields( + array( + 'title' => 2, + 'tag.name' => 1, + 'category.name' => 1, + ), + array( + 'author_login', + 'author', + ) + ); + + $boost_phrase_fields = $parser->merge_ml_fields( + array( + 'title' => 1, + 'content' => 1, + 'excerpt' => 1, + 'tag.name' => 1, + 'category.name' => 1, + ), + array( + 'author', + ) + ); + } + } else { + // If code is overriding the fields, then use that. Important for backwards compatibility. + $match_fields = $args['query_fields']; + $boost_phrase_fields = $match_fields; + $boost_fields = null; + } + + $parser->phrase_filter( array( + 'must_query_fields' => $match_fields, + 'boost_query_fields' => null, + ) ); + $parser->remaining_query( array( + 'must_query_fields' => $match_fields, + 'boost_query_fields' => $boost_fields, + ) ); + + // Boost on phrase matches + $parser->remaining_query( array( + 'boost_query_fields' => $boost_phrase_fields, + 'boost_query_type' => 'phrase', + ) ); + + /** + * Modify the recency decay parameters for the search query. + * + * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments: + * - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today. + * - offset: Number of days/months/years (eg 30d). All posts within this time range of the origin (before and after) will have no decay applied. Default is no offset. + * - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d + * - decay: The amount of decay applied at offset+scale. Default 0.9. + * + * The curve applied is a Gaussian. More details available at {@see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay} + * + * @module search + * + * @since 5.8.0 + * + * @param array $decay_params The decay parameters. + * @param array $args The WP query parameters. + */ + $decay_params = apply_filters( + 'jetpack_search_recency_score_decay', + array( + 'origin' => date( 'Y-m-d' ), + 'scale' => '360d', + 'decay' => 0.9, + ), + $args + ); + + if ( ! empty( $decay_params ) ) { + // Newer content gets weighted slightly higher + $parser->add_decay( 'gauss', array( + 'date_gmt' => $decay_params + ) ); + } + $es_query_args = array( 'blog_id' => absint( $args['blog_id'] ), 'size' => absint( $args['posts_per_page'] ), @@ -620,9 +952,7 @@ class Jetpack_Search { $es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] ); } - // Limit the offset to $this->max_offset posts, as deep pages get exponentially slower - // See https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html - $es_query_args['from'] = min( $es_query_args['from'], $this->max_offset ); + $es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() ); if ( ! is_array( $args['author_name'] ) ) { $args['author_name'] = array( $args['author_name'] ); @@ -648,26 +978,25 @@ class Jetpack_Search { // Filters rock because they are cached from one query to the next // but they are cached as individual filters, rather than all combined together. // May get performance boost by also caching the top level boolean filter too. - $filters = array(); if ( $args['post_type'] ) { if ( ! is_array( $args['post_type'] ) ) { $args['post_type'] = array( $args['post_type'] ); } - $filters[] = array( + $parser->add_filter( array( 'terms' => array( 'post_type' => $args['post_type'], ), - ); + ) ); } if ( $args['author_name'] ) { - $filters[] = array( + $parser->add_filter( array( 'terms' => array( 'author_login' => $args['author_name'], ), - ); + ) ); } if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) { @@ -675,11 +1004,11 @@ class Jetpack_Search { unset( $args['date_range']['field'] ); - $filters[] = array( + $parser->add_filter( array( 'range' => array( $field => $args['date_range'], ), - ); + ) ); } if ( is_array( $args['terms'] ) ) { @@ -705,35 +1034,20 @@ class Jetpack_Search { } foreach ( $terms as $term ) { - $filters[] = array( + $parser->add_filter( array( 'term' => array( $tax_fld => $term, ), - ); + ) ); } } } } - if ( $args['query'] ) { - $query = array( - 'multi_match' => array( - 'query' => $args['query'], - 'fields' => $args['query_fields'], - 'operator' => 'and', - 'type' => 'cross_fields', - ), - ); - - $builder->add_query( $query ); - - Jetpack_Search::score_query_by_recency( $builder ); - - if ( ! $args['orderby'] ) { + if ( ! $args['orderby'] ) { + if ( $args['query'] ) { $args['orderby'] = array( 'relevance' ); - } - } else { - if ( ! $args['orderby'] ) { + } else { $args['orderby'] = array( 'date' ); } } @@ -798,34 +1112,24 @@ class Jetpack_Search { unset( $es_query_args['sort'] ); } - if ( ! empty( $filters ) && is_array( $filters ) ) { - foreach ( $filters as $filter ) { - $builder->add_filter( $filter ); - } - - $es_query_args['filter'] = $builder->build_filter(); - } - - $es_query_args['query'] = $builder->build_query(); - - // Aggregations if ( ! empty( $args['aggregations'] ) ) { - $this->add_aggregations_to_es_query_builder( $args['aggregations'], $builder ); - - $es_query_args['aggregations'] = $builder->build_aggregation(); + $this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser ); } + $es_query_args['filter'] = $parser->build_filter(); + $es_query_args['query'] = $parser->build_query(); + $es_query_args['aggregations'] = $parser->build_aggregation(); + return $es_query_args; } /** - * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in ES - * - * @module search + * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch. * - * @param array $aggregations Array of Aggregations (filters) to add to the Jetpack_WPES_Query_Builder + * @since 5.0.0 * - * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query + * @param array $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder. + * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query. */ public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) { foreach ( $aggregations as $label => $aggregation ) { @@ -849,13 +1153,13 @@ class Jetpack_Search { } /** - * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES + * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch. * - * @module search + * @since 5.0.0 * - * @param array $aggregation The aggregation to add to the query builder - * @param string $label The 'label' (unique id) for this aggregation - * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query + * @param array $aggregation The aggregation to add to the query builder. + * @param string $label The 'label' (unique id) for this aggregation. + * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query. */ public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) { $field = null; @@ -877,37 +1181,37 @@ class Jetpack_Search { $builder->add_aggs( $label, array( 'terms' => array( 'field' => $field . '.slug', - 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ), + 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ), ), - )); + ) ); } /** - * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES + * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch. * - * @module search + * @since 5.0.0 * - * @param array $aggregation The aggregation to add to the query builder - * @param string $label The 'label' (unique id) for this aggregation - * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query + * @param array $aggregation The aggregation to add to the query builder. + * @param string $label The 'label' (unique id) for this aggregation. + * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query. */ public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) { $builder->add_aggs( $label, array( 'terms' => array( 'field' => 'post_type', - 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ), + 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ), ), - )); + ) ); } /** - * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES + * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch. * - * @module search + * @since 5.0.0 * - * @param array $aggregation The aggregation to add to the query builder - * @param string $label The 'label' (unique id) for this aggregation - * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query + * @param array $aggregation The aggregation to add to the query builder. + * @param string $label The 'label' (unique id) for this aggregation. + * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query. */ public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) { $args = array( @@ -923,7 +1227,7 @@ class Jetpack_Search { $builder->add_aggs( $label, array( 'date_histogram' => $args, - )); + ) ); } /** @@ -931,12 +1235,12 @@ class Jetpack_Search { * * Attempts to optimize the filters somewhat. * - * @module search + * @since 5.0.0 * - * @param array $curr_filter The existing filters to build upon - * @param array $filters The new filters to add + * @param array $curr_filter The existing filters to build upon. + * @param array $filters The new filters to add. * - * @return array The resulting merged filters + * @return array The resulting merged filters. */ public static function and_es_filters( array $curr_filter, array $filters ) { if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) { @@ -955,61 +1259,38 @@ class Jetpack_Search { } /** - * Add a recency score to a given Jetpack_WPES_Query_Builder object, for emphasizing newer posts in results - * - * Internally uses a gauss decay function - * - * @module search - * - * @param Jetpack_WPES_Query_Builder $builder The Jetpack_WPES_Query_Builder to add the recency score to - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay - */ - public static function score_query_by_recency( Jetpack_WPES_Query_Builder &$builder ) { - //Newer content gets weighted slightly higher - $date_scale = '360d'; - $date_decay = 0.9; - $date_origin = date( 'Y-m-d' ); - - $builder->add_decay( 'gauss', array( - 'date_gmt' => array( - 'origin' => $date_origin, - 'scale' => $date_scale, - 'decay' => $date_decay, - ), - )); - } - - /** - * Set the available filters for the search + * Set the available filters for the search. * - * These get rendered via the Jetpack_Search_Widget_Filters() widget + * These get rendered via the Jetpack_Search_Widget() widget. * * Behind the scenes, these are implemented using Elasticsearch Aggregations. * * If you do not require counts of how many documents match each filter, please consider using regular WP Query * arguments instead, such as via the jetpack_search_es_wp_query_args filter * - * @module search + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html + * @since 5.0.0 * * @param array $aggregations Array of filters (aggregations) to apply to the search */ public function set_filters( array $aggregations ) { + foreach ( (array) $aggregations as $key => $agg ) { + if ( empty( $agg['name'] ) ) { + $aggregations[ $key ]['name'] = $key; + } + } $this->aggregations = $aggregations; } /** - * Set the search's facets (deprecated) + * Set the search's facets (deprecated). * - * @module search + * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead. * - * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead + * @see Jetpack_Search::set_filters() * - * @see Jetpack_Search::set_filters() - * - * @param array $facets Array of facets to apply to the search + * @param array $facets Array of facets to apply to the search. */ public function set_facets( array $facets ) { _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' ); @@ -1018,13 +1299,13 @@ class Jetpack_Search { } /** - * Get the raw Aggregation results from the ES response + * Get the raw Aggregation results from the Elasticsearch response. * - * @module search + * @since 5.0.0 * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html * - * @return array Array of Aggregations performed on the search + * @return array Array of Aggregations performed on the search. */ public function get_search_aggregations_results() { $aggregations = array(); @@ -1039,15 +1320,13 @@ class Jetpack_Search { } /** - * Get the raw Facet results from the ES response - * - * @module search + * Get the raw Facet results from the Elasticsearch response. * - * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead + * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead. * - * @see Jetpack_Search::get_search_aggregations_results() + * @see Jetpack_Search::get_search_aggregations_results() * - * @return array Array of Facets performed on the search + * @return array Array of Facets performed on the search. */ public function get_search_facets() { _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' ); @@ -1056,19 +1335,19 @@ class Jetpack_Search { } /** - * Get the results of the Filters performed, including the number of matching documents + * Get the results of the Filters performed, including the number of matching documents. * * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting * matching buckets, the url for applying/removing each bucket, etc. * * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class - * member if you need to access the raw filters set in Jetpack_Search::set_filters() + * member if you need to access the raw filters set in Jetpack_Search::set_filters(). * - * @module search + * @since 5.0.0 * - * @param WP_Query $query The optional original WP_Query to use for determining which filters are active. Defaults to the main query + * @param WP_Query $query The optional original WP_Query to use for determining which filters are active. Defaults to the main query. * - * @return array Array of Filters applied and info about them + * @return array Array of filters applied and info about them. */ public function get_filters( WP_Query $query = null ) { if ( ! $query instanceof WP_Query ) { @@ -1105,10 +1384,10 @@ class Jetpack_Search { // Figure out which terms are active in the query, for this taxonomy if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) { - $tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] ); + $tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] ); if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) { - foreach( $query->tax_query->queries as $tax_query ) { + foreach ( $query->tax_query->queries as $tax_query ) { if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] && 'slug' === $tax_query['field'] && is_array( $tax_query['terms'] ) ) { @@ -1118,14 +1397,18 @@ class Jetpack_Search { } } - // Now take the resulting found aggregation items and generate the additional info about them, such as - // activation/deactivation url, name, count, etc + // Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc. $buckets = array(); if ( ! empty( $aggregation['buckets'] ) ) { $buckets = (array) $aggregation['buckets']; } + if ( 'date_histogram' == $type ) { + //re-order newest to oldest + $buckets = array_reverse( $buckets ); + } + // Some aggregation types like date_histogram don't support the max results parameter if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) { $buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] ); @@ -1162,9 +1445,12 @@ class Jetpack_Search { $slug_count = count( $existing_term_slugs ); if ( $slug_count > 1 ) { - $remove_url = add_query_arg( $tax_query_var, urlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) ) ); + $remove_url = Jetpack_Search_Helpers::add_query_arg( + $tax_query_var, + rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) ) + ); } else { - $remove_url = remove_query_arg( $tax_query_var ); + $remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var ); } } @@ -1197,9 +1483,12 @@ class Jetpack_Search { // For the right 'remove filter' url, we need to remove the post type from the array, or remove the param entirely if it's the only one if ( $post_type_count > 1 ) { - $remove_url = add_query_arg( 'post_type', urlencode_deep( array_diff( $post_types, array( $item['key'] ) ) ) ); + $remove_url = Jetpack_Search_Helpers::add_query_arg( + 'post_type', + rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) ) + ); } else { - $remove_url = remove_query_arg( 'post_type' ); + $remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' ); } } @@ -1228,7 +1517,7 @@ class Jetpack_Search { if ( ! empty( $current_year ) && (int) $current_year === $year ) { $active = true; - $remove_url = remove_query_arg( array( 'year', 'monthnum', 'day' ) ); + $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) ); } break; @@ -1250,7 +1539,7 @@ class Jetpack_Search { ! empty( $current_month ) && (int) $current_month === $month ) { $active = true; - $remove_url = remove_query_arg( array( 'monthnum', 'day' ) ); + $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) ); } break; @@ -1274,7 +1563,7 @@ class Jetpack_Search { ! empty( $current_day ) && (int) $current_day === $day ) { $active = true; - $remove_url = remove_query_arg( array( 'day' ) ); + $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) ); } break; @@ -1293,14 +1582,15 @@ class Jetpack_Search { $url_params = urlencode_deep( $query_vars ); $aggregation_data[ $label ]['buckets'][] = array( - 'url' => add_query_arg( $url_params ), + 'url' => Jetpack_Search_Helpers::add_query_arg( $url_params ), 'query_vars' => $query_vars, 'name' => $name, 'count' => $item['doc_count'], 'active' => $active, 'remove_url' => $remove_url, 'type' => $type, - 'type_label' => $label, + 'type_label' => $aggregation_data[ $label ]['name'], + 'widget_id' => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0 ); } // End foreach(). } // End foreach(). @@ -1309,15 +1599,13 @@ class Jetpack_Search { } /** - * Get the results of the Facets performed + * Get the results of the facets performed. * - * @module search + * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead. * - * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead + * @see Jetpack_Search::get_filters() * - * @see Jetpack_Search::get_filters() - * - * @return array $facets Array of Facets applied and info about them + * @return array $facets Array of facets applied and info about them. */ public function get_search_facet_data() { _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' ); @@ -1326,11 +1614,11 @@ class Jetpack_Search { } /** - * Get the Filters that are currently applied to this search + * Get the filters that are currently applied to this search. * - * @module search + * @since 5.0.0 * - * @return array Array if Filters that were applied + * @return array Array of filters that were applied. */ public function get_active_filter_buckets() { $active_buckets = array(); @@ -1341,9 +1629,9 @@ class Jetpack_Search { return $active_buckets; } - foreach( $filters as $filter ) { + foreach ( $filters as $filter ) { if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) { - foreach( $filter['buckets'] as $item ) { + foreach ( $filter['buckets'] as $item ) { if ( isset( $item['active'] ) && $item['active'] ) { $active_buckets[] = $item; } @@ -1355,11 +1643,13 @@ class Jetpack_Search { } /** - * Get the Filters that are currently applied to this search + * Get the filters that are currently applied to this search. + * + * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead. * - * @module search + * @see Jetpack_Search::get_active_filter_buckets() * - * @return array Array if Filters that were applied + * @return array Array of filters that were applied. */ public function get_current_filters() { _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' ); @@ -1368,15 +1658,15 @@ class Jetpack_Search { } /** - * Calculate the right query var to use for a given taxonomy + * Calculate the right query var to use for a given taxonomy. * - * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter + * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter. * - * @module search + * @since 5.0.0 * - * @param string $taxonomy_name The name of the taxonomy for which to get the query var + * @param string $taxonomy_name The name of the taxonomy for which to get the query var. * - * @return bool|string The query var to use for this taxonomy, or false if none found + * @return bool|string The query var to use for this taxonomy, or false if none found. */ public function get_taxonomy_query_var( $taxonomy_name ) { $taxonomy = get_taxonomy( $taxonomy_name ); @@ -1390,9 +1680,9 @@ class Jetpack_Search { * * @module search * - * @since 5.0.0 + * @since 5.0.0 * - * @param string $query_var The current query_var for the taxonomy + * @param string $query_var The current query_var for the taxonomy * @param string $taxonomy_name The taxonomy name */ return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name ); @@ -1400,17 +1690,17 @@ class Jetpack_Search { /** * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired - * which is the input order + * which is the input order. * - * Necessary because ES does not always return Aggs in the same order that you pass them in, and it should be possible - * to control the display order easily + * Necessary because ES does not always return aggregations in the same order that you pass them in, + * and it should be possible to control the display order easily. * - * @module search + * @since 5.0.0 * - * @param array $aggregations Agg results to be reordered - * @param array $desired Array with keys representing the desired ordering + * @param array $aggregations Aggregation results to be reordered. + * @param array $desired Array with keys representing the desired ordering. * - * @return array A new array with reordered keys, matching those in $desired + * @return array A new array with reordered keys, matching those in $desired. */ public function fix_aggregation_ordering( array $aggregations, array $desired ) { if ( empty( $aggregations ) || empty( $desired ) ) { @@ -1419,7 +1709,7 @@ class Jetpack_Search { $reordered = array(); - foreach( array_keys( $desired ) as $agg_name ) { + foreach ( array_keys( $desired ) as $agg_name ) { if ( isset( $aggregations[ $agg_name ] ) ) { $reordered[ $agg_name ] = $aggregations[ $agg_name ]; } @@ -1427,4 +1717,72 @@ class Jetpack_Search { return $reordered; } + + /** + * Sends events to Tracks when a search filters widget is updated. + * + * @since 5.8.0 + * + * @param string $option The option name. Only "widget_jetpack-search-filters" is cared about. + * @param array $old_value The old option value. + * @param array $new_value The new option value. + */ + public function track_widget_updates( $option, $old_value, $new_value ) { + if ( 'widget_jetpack-search-filters' !== $option ) { + return; + } + + $event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value ); + if ( ! $event ) { + return; + } + + jetpack_tracks_record_event( + wp_get_current_user(), + sprintf( 'jetpack_search_widget_%s', $event['action'] ), + $event['widget'] + ); + } + + /** + * Moves any active search widgets to the inactive category. + * + * @since 5.9.0 + * + * @param string $module Unused. The Jetpack module being disabled. + */ + public function move_search_widgets_to_inactive( $module ) { + if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) { + return; + } + + $sidebars_widgets = wp_get_sidebars_widgets(); + + if ( ! is_array( $sidebars_widgets ) ) { + return; + } + + $changed = false; + + foreach ( $sidebars_widgets as $sidebar => $widgets ) { + if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) { + continue; + } + + if ( is_array( $widgets ) ) { + foreach ( $widgets as $key => $widget ) { + if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) { + $changed = true; + + array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget ); + unset( $sidebars_widgets[ $sidebar ][ $key ] ); + } + } + } + } + + if ( $changed ) { + wp_set_sidebars_widgets( $sidebars_widgets ); + } + } } diff --git a/plugins/jetpack/modules/seo-tools.php b/plugins/jetpack/modules/seo-tools.php index 18b3ea84..db573a4f 100644 --- a/plugins/jetpack/modules/seo-tools.php +++ b/plugins/jetpack/modules/seo-tools.php @@ -2,14 +2,13 @@ /** * Module Name: SEO Tools * Module Description: Better results on search engines and social media. - * Jumpstart Description: Better results on search engines and social media. * Sort Order: 35 * Recommendation Order: 15 * First Introduced: 4.4 * Requires Connection: Yes * Auto Activate: No * Module Tags: Social, Appearance - * Feature: Traffic, Jumpstart + * Feature: Traffic * Additional Search Queries: search engine optimization, social preview, meta description, custom title format */ diff --git a/plugins/jetpack/modules/sharedaddy/sharedaddy.php b/plugins/jetpack/modules/sharedaddy/sharedaddy.php index 2bc170e7..c9de29b1 100644 --- a/plugins/jetpack/modules/sharedaddy/sharedaddy.php +++ b/plugins/jetpack/modules/sharedaddy/sharedaddy.php @@ -51,7 +51,18 @@ function sharing_email_send_post( $data ) { // Make sure to pass the title through the normal sharing filters. $title = $data['sharing_source']->get_share_title( $data['post']->ID ); - wp_mail( $data['target'], '[' . __( 'Shared Post', 'jetpack' ) . '] ' . $title, $content, $headers ); + /** + * Filter the Sharing Email Send Post Subject. + * + * @module sharedaddy + * + * @since 5.8.0 + * + * @param string $var Sharing Email Send Post Subject. Default is "Shared Post". + */ + $subject = apply_filters( 'wp_sharing_email_send_post_subject', '[' . __( 'Shared Post', 'jetpack' ) . '] ' . $title ); + + wp_mail( $data['target'], $subject, $content, $headers ); } diff --git a/plugins/jetpack/modules/sharedaddy/sharing-service.php b/plugins/jetpack/modules/sharedaddy/sharing-service.php index 21e42836..2a056991 100644 --- a/plugins/jetpack/modules/sharedaddy/sharing-service.php +++ b/plugins/jetpack/modules/sharedaddy/sharing-service.php @@ -791,7 +791,15 @@ function sharing_display( $text = '', $echo = false ) { } else { $ver = '20141212'; } - wp_register_script( 'sharing-js', plugin_dir_url( __FILE__ ).'sharing.js', array( 'jquery' ), $ver ); + wp_register_script( + 'sharing-js', + Jetpack::get_file_url_for_environment( + '_inc/build/sharedaddy/sharing.min.js', + 'modules/sharedaddy/sharing.js' + ), + array( 'jquery' ), + $ver + ); // Enqueue scripts for the footer add_action( 'wp_footer', 'sharing_add_footer' ); diff --git a/plugins/jetpack/modules/sharedaddy/sharing-sources.php b/plugins/jetpack/modules/sharedaddy/sharing-sources.php index c4427a1a..679449e1 100644 --- a/plugins/jetpack/modules/sharedaddy/sharing-sources.php +++ b/plugins/jetpack/modules/sharedaddy/sharing-sources.php @@ -1699,7 +1699,7 @@ class Jetpack_Share_WhatsApp extends Sharing_Source { } public function get_display( $post ) { - return $this->get_link( 'whatsapp://send?text=' . rawurlencode( $this->get_share_title( $post->ID ) ) . ' ' . rawurlencode( $this->get_share_url( $post->ID ) ), _x( 'WhatsApp', 'share to', 'jetpack' ), __( 'Click to share on WhatsApp', 'jetpack' ) ); + return $this->get_link( 'https://api.whatsapp.com/send?text=' . rawurlencode( $this->get_share_title( $post->ID ) ) . ' ' . rawurlencode( $this->get_share_url( $post->ID ) ), _x( 'WhatsApp', 'share to', 'jetpack' ), __( 'Click to share on WhatsApp', 'jetpack' ) ); } } diff --git a/plugins/jetpack/modules/sharedaddy/sharing.js b/plugins/jetpack/modules/sharedaddy/sharing.js index 6c31c6c7..fd7d47b9 100644 --- a/plugins/jetpack/modules/sharedaddy/sharing.js +++ b/plugins/jetpack/modules/sharedaddy/sharing.js @@ -19,12 +19,7 @@ if ( sharing_js_options && sharing_js_options.counts ) { } requests = { - // LinkedIn actually gets the share count for both the http and https version automatically -- so we don't need to do extra magic - linkedin: [ - 'https://www.linkedin.com/countserv/count/share?format=jsonp&callback=updateLinkedInCount&url=' + - encodeURIComponent( url ) - ], - // Pinterest, like LinkedIn, handles share counts for both http and https + // Pinterest handles share counts for both http and https pinterest: [ window.location.protocol + '//api.pinterest.com/v1/urls/count.json?callback=WPCOMSharing.update_pinterest_count&url=' + @@ -85,11 +80,6 @@ if ( sharing_js_options && sharing_js_options.counts ) { WPCOMSharing.inject_share_count( 'sharing-facebook-' + WPCOM_sharing_counts[ permalink ], data[ url ].share.share_count ); } }, - update_linkedin_count : function( data ) { - if ( 'undefined' !== typeof data.count && ( data.count * 1 ) > 0 ) { - WPCOMSharing.inject_share_count( 'sharing-linkedin-' + WPCOM_sharing_counts[ data.url ], data.count ); - } - }, update_pinterest_count : function( data ) { if ( 'undefined' !== typeof data.count && ( data.count * 1 ) > 0 ) { WPCOMSharing.inject_share_count( 'sharing-pinterest-' + WPCOM_sharing_counts[ data.url ], data.count ); @@ -115,10 +105,6 @@ if ( sharing_js_options && sharing_js_options.counts ) { }; } -var updateLinkedInCount = function( data ) { - WPCOMSharing.update_linkedin_count( data ); -}; - (function($){ var $body, $sharing_email; diff --git a/plugins/jetpack/modules/sharedaddy/sharing.php b/plugins/jetpack/modules/sharedaddy/sharing.php index b147b81a..f6a6bebd 100644 --- a/plugins/jetpack/modules/sharedaddy/sharing.php +++ b/plugins/jetpack/modules/sharedaddy/sharing.php @@ -23,7 +23,15 @@ class Sharing_Admin { } public function sharing_head() { - wp_enqueue_script( 'sharing-js', WP_SHARING_PLUGIN_URL . 'admin-sharing.js', array( 'jquery-ui-draggable', 'jquery-ui-droppable', 'jquery-ui-sortable', 'jquery-form' ), 2 ); + wp_enqueue_script( + 'sharing-js', + Jetpack::get_file_url_for_environment( + '_inc/build/sharedaddy/admin-sharing.min.js', + 'modules/sharedaddy/admin-sharing.js' + ), + array( 'jquery-ui-draggable', 'jquery-ui-droppable', 'jquery-ui-sortable', 'jquery-form' ), + 2 + ); $postfix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; if ( is_rtl() ) { wp_enqueue_style( 'sharing-admin', WP_SHARING_PLUGIN_URL . 'admin-sharing-rtl' . $postfix . '.css', false, JETPACK__VERSION ); @@ -376,10 +384,10 @@ class Sharing_Admin { $label = $post_type_object->labels->name; } ?> - <?php - if ( $br ) { + <?php + if ( $br ) { echo '<br />'; - } + } ?> <label><input type="checkbox"<?php checked( in_array( $show, $global['show'] ) ); ?> name="show[]" value="<?php echo esc_attr( $show ); ?>" /> <?php echo esc_html( $label ); ?></label> <?php diff --git a/plugins/jetpack/modules/shortcodes/brightcove.php b/plugins/jetpack/modules/shortcodes/brightcove.php index 8dbc9fd5..c774c915 100644 --- a/plugins/jetpack/modules/shortcodes/brightcove.php +++ b/plugins/jetpack/modules/shortcodes/brightcove.php @@ -56,7 +56,7 @@ class Jetpack_Brightcove_Shortcode { * @return array */ static public function normalize_attributes( $atts ) { - if ( 1 == count( $atts ) ) { // this is the case we need to take care of. + if ( is_array( $atts ) && 1 == count( $atts ) ) { // this is the case we need to take care of. $parsed_atts = array(); $params = shortcode_new_to_old_params( $atts ); $params = apply_filters( 'brightcove_dimensions', $params ); diff --git a/plugins/jetpack/modules/shortcodes/getty.php b/plugins/jetpack/modules/shortcodes/getty.php index 3fa4956a..cdca6859 100644 --- a/plugins/jetpack/modules/shortcodes/getty.php +++ b/plugins/jetpack/modules/shortcodes/getty.php @@ -9,26 +9,20 @@ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { add_action( 'init', 'jetpack_getty_enable_embeds' ); } else { - jetpack_getty_enable_embeds( 'jetpack' ); + jetpack_getty_enable_embeds(); } /** * Register Getty as oembed provider. Add filter to reverse iframes to shortcode. Register [getty] shortcode. * * @since 4.5.0 - * - * @param string $site Can be 'wpcom' or 'jetpack' and determines if we're in wpcom or in a Jetpack site. + * @since 5.8.0 removed string parameter. */ -function jetpack_getty_enable_embeds( $site = 'wpcom' ) { - - // Set the caller argument to pass to Getty's oembed provider. - $caller = 'jetpack' === $site - ? parse_url( get_home_url(), PHP_URL_HOST ) - : 'wordpress.com'; +function jetpack_getty_enable_embeds() { // Support their oEmbed Endpoint - wp_oembed_add_provider( '#https?://www\.gettyimages\.com/detail/.*#i', "https://embed.gettyimages.com/oembed/?caller=$caller", true ); - wp_oembed_add_provider( '#https?://(www\.)?gty\.im/.*#i', "https://embed.gettyimages.com/oembed/?caller=$caller", true ); + wp_oembed_add_provider( '#https?://www\.gettyimages\.com/detail/.*#i', "https://embed.gettyimages.com/oembed/", true ); + wp_oembed_add_provider( '#https?://(www\.)?gty\.im/.*#i', "https://embed.gettyimages.com/oembed/", true ); // Allow iframes to be filtered to short code (so direct copy+paste can be done) add_filter( 'pre_kses', 'wpcom_shortcodereverse_getty' ); @@ -38,6 +32,47 @@ function jetpack_getty_enable_embeds( $site = 'wpcom' ) { } /** + * Filters the oEmbed provider URL for Getty URLs to include site URL host as + * caller if available, falling back to "wordpress.com". Must be applied at + * time of embed in case that `init` is too early (WP.com REST API). + * + * @module shortcodes + * + * @since 5.8.0 + * + * @see WP_oEmbed::fetch + * + * @return string oEmbed provider URL + */ +add_filter( 'oembed_fetch_url', 'getty_add_oembed_endpoint_caller' ); + +function getty_add_oembed_endpoint_caller( $provider ) { + // By time filter is called, original provider URL has had url, maxwidth, + // maxheight query parameters added. + if ( 0 !== strpos( $provider, 'https://embed.gettyimages.com/oembed/' ) ) { + return $provider; + } + + // Set the caller argument to pass to Getty's oembed provider. + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + + // Only include caller for non-private sites + if ( ! function_exists( 'is_private_blog' ) || ! is_private_blog() ) { + $host = parse_url( get_bloginfo( 'url' ), PHP_URL_HOST ); + } + + // Fall back to WordPress.com + if ( empty( $host ) ) { + $host = 'wordpress.com'; + } + } else { + $host = parse_url( get_home_url(), PHP_URL_HOST ); + } + + return add_query_arg( 'caller', $host, $provider ); +} + +/** * Compose shortcode based on Getty iframes. * * @since 4.5.0 @@ -47,39 +82,68 @@ function jetpack_getty_enable_embeds( $site = 'wpcom' ) { * @return mixed */ function wpcom_shortcodereverse_getty( $content ) { - if ( ! is_string( $content ) || false === stripos( $content, 'embed.gettyimages.com/embed' ) ) { + if ( ! is_string( $content ) || false === stripos( $content, '.gettyimages.com/' ) ) { return $content; } - $regexp = '!<iframe\s+src=[\'"](https?:)?//embed\.gettyimages\.com/embed(/|/?\?assets=)(\d+(,\d+)*)[^\'"]*?[\'"]((?:\s+\w+=[\'"][^\'"]*[\'"])*)((?:[\s\w]*))></iframe>!i'; + $regexp = '!<iframe\s+src=[\'"](https?:)?//embed\.gettyimages\.com/embed(/|/?\?assets=)([a-z0-9_-]+(,[a-z0-9_-]+)*)[^\'"]*?[\'"]((?:\s+\w+=[\'"][^\'"]*[\'"])*)((?:[\s\w]*))></iframe>!i'; $regexp_ent = str_replace( '&#0*58;', '&#0*58;|�*58;', htmlspecialchars( $regexp, ENT_NOQUOTES ) ); - foreach ( array( 'regexp', 'regexp_ent' ) as $reg ) { + + // Markup pattern for 2017 embed syntax with significant differences from + // the prior pattern: + $regexp_2017 = '!<a.+?class=\'gie-(single|slideshow)\'.+?gie\.widgets\.load\({([^}]+)}\).+?embed-cdn\.gettyimages\.com/widgets\.js.+?</script>!'; + $regexp_2017_ent = str_replace( '&#0*58;', '&#0*58;|�*58;', htmlspecialchars( $regexp_2017, ENT_NOQUOTES ) ); + + foreach ( array( 'regexp_2017', 'regexp_2017_ent', 'regexp', 'regexp_ent' ) as $reg ) { if ( ! preg_match_all( $$reg, $content, $matches, PREG_SET_ORDER ) ) { continue; } foreach ( $matches as $match ) { - $ids = esc_html( $match[3] ); - - $params = $match[5]; - - if ( 'regexp_ent' == $reg ) { - $params = html_entity_decode( $params ); + if ( 'regexp_2017' === $reg || 'regexp_2017_ent' === $reg ) { + // Extract individual keys from the matched JavaScript object + $params = $match[2]; + if ( ! preg_match_all( '!(?P<key>\w+)\s*:\s*([\'"](?P<value>[^\'"]*?)(px)?[\'"])!', $params, $key_matches, PREG_SET_ORDER ) ) { + continue; + } + + foreach ( $key_matches as $key_match ) { + switch ( $key_match['key'] ) { + case 'items': $ids = $key_match['value']; break; + case 'w': $width = (int) $key_match['value']; break; + case 'h': $height = (int) $key_match['value']; break; + case 'tld': $tld = $key_match['value']; break; + } + } + } else { + $params = $match[5]; + if ( 'regexp_ent' === $reg ) { + $params = html_entity_decode( $params ); + } + $params = wp_kses_hair( $params, array( 'http' ) ); + + $ids = esc_html( $match[3] ); + $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0; + $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0; } - $params = wp_kses_hair( $params, array( 'http' ) ); - - $width = isset( $params['width'] ) ? (int) $params['width']['value'] : 0; - $height = isset( $params['height'] ) ? (int) $params['height']['value'] : 0; + if ( empty( $ids ) ) { + continue; + } $shortcode = '[getty src="' . esc_attr( $ids ) . '"'; - if ( $width ) { + if ( ! empty( $width ) ) { $shortcode .= ' width="' . esc_attr( $width ) . '"'; } - if ( $height ) { + if ( ! empty( $height ) ) { $shortcode .= ' height="' . esc_attr( $height ) . '"'; } + // While it does not appear to have any practical impact, Getty has + // requested that we include TLD in the embed request + if ( ! empty( $tld ) ) { + $shortcode .= ' tld="' . esc_attr( $tld ). '"'; + } $shortcode .= ']'; $content = str_replace( $match[0], $shortcode, $content ); @@ -128,11 +192,16 @@ function jetpack_getty_shortcode( $atts, $content = '' ) { return '<!-- Missing Getty Source ID -->'; } - $src = preg_replace( '/^(\d+(,\d+)*).*$/', '$1', $src ); + $src = preg_replace( '/^([\da-z-]+(,[\da-z-]+)*).*$/', '$1', $src ); - $args = array(); - $args['width'] = isset( $atts['width'] ) ? (int) $atts['width'] : '462'; - $args['height'] = isset( $atts['height'] ) ? (int) $atts['height'] : '370'; + $params = array( + 'width' => isset( $atts['width'] ) ? (int) $atts['width'] : null, + 'height' => isset( $atts['height'] ) ? (int) $atts['height'] : null + ); + + if ( ! empty( $atts['tld'] ) ) { + $params['tld'] = $atts['tld']; + } - return wp_oembed_get( 'https://gty.im/' . $src, $args ); + return wp_oembed_get( 'https://gty.im/' . $src, array_filter( $params ) ); } diff --git a/plugins/jetpack/modules/shortcodes/mailchimp.php b/plugins/jetpack/modules/shortcodes/mailchimp.php index 8bba9978..2cbbefac 100644 --- a/plugins/jetpack/modules/shortcodes/mailchimp.php +++ b/plugins/jetpack/modules/shortcodes/mailchimp.php @@ -199,6 +199,6 @@ class MailChimp_Subscriber_Popup { $displayed_once = true; - return "\n\n" . '<script type="text/javascript" data-dojo-config="' . esc_attr( implode( ', ', $config_vars ) ) . '">jQuery.getScript( "//downloads.mailchimp.com/js/signup-forms/popup/embed.js", function( data, textStatus, jqxhr ) { require(["mojo/signup-forms/Loader"], function(L) { L.start(' . wp_json_encode( $js_vars ) . ') }); } );</script>' . "\n\n"; + return "\n\n" . '<script type="text/javascript" data-dojo-config="' . esc_attr( implode( ', ', $config_vars ) ) . '">jQuery.getScript( "//downloads.mailchimp.com/js/signup-forms/popup/embed.js", function( data, textStatus, jqxhr ) { require(["mojo/signup-forms/Loader"], function(L) { L.start(' . wp_json_encode( $js_vars ) . ') }); window.define.amd = undefined; } );</script>' . "\n\n"; } } diff --git a/plugins/jetpack/modules/shortcodes/slideshow.php b/plugins/jetpack/modules/shortcodes/slideshow.php index e84ab0d1..e9046b6c 100644 --- a/plugins/jetpack/modules/shortcodes/slideshow.php +++ b/plugins/jetpack/modules/shortcodes/slideshow.php @@ -140,15 +140,16 @@ class Jetpack_Slideshow_Shortcode { $attachments = get_posts( array( - 'post_status' => 'inherit', - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'posts_per_page' => - 1, - 'post_parent' => $post_parent, - 'order' => $attr['order'], - 'orderby' => $attr['orderby'], - 'include' => $attr['include'], - 'exclude' => $attr['exclude'], + 'post_status' => 'inherit', + 'post_type' => 'attachment', + 'post_mime_type' => 'image', + 'posts_per_page' => - 1, + 'post_parent' => $post_parent, + 'order' => $attr['order'], + 'orderby' => $attr['orderby'], + 'include' => $attr['include'], + 'exclude' => $attr['exclude'], + 'suppress_filters' => false, ) ); diff --git a/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php b/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php index fed7601b..1bf2617c 100644 --- a/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php +++ b/plugins/jetpack/modules/sso/class.jetpack-sso-helpers.php @@ -58,7 +58,7 @@ class Jetpack_SSO_Helpers { $new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? WPCC_NEW_USER_OVERRIDE : false; /** - * Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations. + * Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations. * If you return a string that corresponds to a user role, the user will be given that role. * * @module sso @@ -203,7 +203,6 @@ class Jetpack_SSO_Helpers { static function generate_user( $user_data ) { $username = $user_data->login; - /** * Determines how many times the SSO module can attempt to randomly generate a user. * @@ -224,10 +223,10 @@ class Jetpack_SSO_Helpers { return false; } - $password = wp_generate_password( 20 ); - $user_id = wp_create_user( $username, $password, $user_data->email ); - $user = get_userdata( $user_id ); - + $user = (object) array(); + $user->user_pass = wp_generate_password( 20 ); + $user->user_login = wp_slash( $username ); + $user->user_email = wp_slash( $user_data->email ); $user->display_name = $user_data->display_name; $user->first_name = $user_data->first_name; $user->last_name = $user_data->last_name; @@ -238,11 +237,10 @@ class Jetpack_SSO_Helpers { $user->role = $user_data->role; } - wp_update_user( $user ); + $created_user_id = wp_insert_user( $user ); - update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID ); - - return $user; + update_user_meta( $created_user_id, 'wpcom_user_id', $user_data->ID ); + return get_userdata( $created_user_id ); } static function extend_auth_cookie_expiration_for_sso() { diff --git a/plugins/jetpack/modules/stats.php b/plugins/jetpack/modules/stats.php index a0a1f7d4..841c603e 100644 --- a/plugins/jetpack/modules/stats.php +++ b/plugins/jetpack/modules/stats.php @@ -164,7 +164,7 @@ function stats_template_redirect() { $data_stats_array = stats_array( $data ); $stats_footer = <<<END -<script type='text/javascript' src='{$script}' async defer></script> +<script type='text/javascript' src='{$script}' async='async' defer='defer'></script> <script type='text/javascript'> _stq = window._stq || []; _stq.push([ 'view', {{$data_stats_array}} ]); @@ -702,6 +702,7 @@ function stats_convert_post_titles( $html ) { 'post_type' => 'any', 'post_status' => 'any', 'numberposts' => -1, + 'suppress_filters' => false, ) ); foreach ( $posts as $post ) { diff --git a/plugins/jetpack/modules/theme-tools/featured-content.php b/plugins/jetpack/modules/theme-tools/featured-content.php index 7fdfa70c..07ad833b 100644 --- a/plugins/jetpack/modules/theme-tools/featured-content.php +++ b/plugins/jetpack/modules/theme-tools/featured-content.php @@ -172,9 +172,10 @@ class Featured_Content { } $featured_posts = get_posts( array( - 'include' => $post_ids, - 'posts_per_page' => count( $post_ids ), - 'post_type' => self::$post_types, + 'include' => $post_ids, + 'posts_per_page' => count( $post_ids ), + 'post_type' => self::$post_types, + 'suppress_filters' => false, ) ); return $featured_posts; @@ -230,6 +231,7 @@ class Featured_Content { $featured = get_posts( array( 'numberposts' => $quantity, 'post_type' => self::$post_types, + 'suppress_filters' => false, 'tax_query' => array( array( 'field' => 'term_id', @@ -459,7 +461,7 @@ class Featured_Content { */ public static function customize_register( $wp_customize ) { $wp_customize->add_section( 'featured_content', array( - 'title' => __( 'Featured Content', 'jetpack' ), + 'title' => esc_html__( 'Featured Content', 'jetpack' ), 'description' => sprintf( __( 'Easily feature all posts with the <a href="%1$s">"featured" tag</a> or a tag of your choice. Your theme supports up to %2$s posts in its featured content area.', 'jetpack' ), admin_url( '/edit.php?tag=featured' ), absint( self::$max_posts ) ), 'priority' => 130, 'theme_supports' => 'featured-content', @@ -487,20 +489,20 @@ class Featured_Content { // Add Featured Content controls. $wp_customize->add_control( 'featured-content[tag-name]', array( - 'label' => __( 'Tag name', 'jetpack' ), + 'label' => esc_html__( 'Tag name', 'jetpack' ), 'section' => 'featured_content', 'theme_supports' => 'featured-content', 'priority' => 20, ) ); $wp_customize->add_control( 'featured-content[hide-tag]', array( - 'label' => __( 'Do not display tag in post details and tag clouds.', 'jetpack' ), + 'label' => esc_html__( 'Do not display tag in post details and tag clouds.', 'jetpack' ), 'section' => 'featured_content', 'theme_supports' => 'featured-content', 'type' => 'checkbox', 'priority' => 30, ) ); $wp_customize->add_control( 'featured-content[show-all]', array( - 'label' => __( 'Also display tagged posts outside the Featured Content area.', 'jetpack' ), + 'label' => esc_html__( 'Also display tagged posts outside the Featured Content area.', 'jetpack' ), 'section' => 'featured_content', 'theme_supports' => 'featured-content', 'type' => 'checkbox', diff --git a/plugins/jetpack/modules/theme-tools/social-links.php b/plugins/jetpack/modules/theme-tools/social-links.php index 5f477cd8..8b38814a 100644 --- a/plugins/jetpack/modules/theme-tools/social-links.php +++ b/plugins/jetpack/modules/theme-tools/social-links.php @@ -118,7 +118,7 @@ class Social_Links { */ public function customize_register( $wp_customize ) { $wp_customize->add_section( 'jetpack_social_links', array( - 'title' => __( 'Connect', 'jetpack' ), + 'title' => esc_html__( 'Connect', 'jetpack' ), 'priority' => 35, ) ); diff --git a/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php b/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php index 9955b0f1..ac4d294b 100644 --- a/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php +++ b/plugins/jetpack/modules/tiled-gallery/tiled-gallery.php @@ -70,7 +70,7 @@ class Jetpack_Tiled_Gallery { if ( !empty( $include ) ) { $include = preg_replace( '/[^0-9,]+/', '', $include ); - $_attachments = get_posts( array('include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby) ); + $_attachments = get_posts( array('include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby, 'suppress_filters' => false) ); $attachments = array(); foreach ( $_attachments as $key => $val ) { @@ -83,15 +83,22 @@ class Jetpack_Tiled_Gallery { $attachments = array(); } elseif ( !empty( $exclude ) ) { $exclude = preg_replace( '/[^0-9,]+/', '', $exclude ); - $attachments = get_children( array('post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby) ); + $attachments = get_children( array('post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby, 'suppress_filters' => false) ); } else { - $attachments = get_children( array('post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) ); + $attachments = get_children( array('post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby, 'suppress_filters' => false ) ); } return $attachments; } public static function default_scripts_and_styles() { - wp_enqueue_script( 'tiled-gallery', plugins_url( 'tiled-gallery/tiled-gallery.js', __FILE__ ), array( 'jquery' ) ); + wp_enqueue_script( + 'tiled-gallery', + Jetpack::get_file_url_for_environment( + '_inc/build/tiled-gallery/tiled-gallery/tiled-gallery.min.js', + 'modules/tiled-gallery/tiled-gallery/tiled-gallery.js' + ), + array( 'jquery' ) + ); wp_enqueue_style( 'tiled-gallery', plugins_url( 'tiled-gallery/tiled-gallery.css', __FILE__ ), array(), '2012-09-21' ); wp_style_add_data( 'tiled-gallery', 'rtl', 'replace' ); } diff --git a/plugins/jetpack/modules/videopress/class.jetpack-videopress.php b/plugins/jetpack/modules/videopress/class.jetpack-videopress.php index 8cbd3bd7..f69fa34d 100644 --- a/plugins/jetpack/modules/videopress/class.jetpack-videopress.php +++ b/plugins/jetpack/modules/videopress/class.jetpack-videopress.php @@ -130,7 +130,10 @@ class Jetpack_VideoPress { if ( $this->should_override_media_uploader() ) { wp_enqueue_script( 'videopress-plupload', - plugins_url( 'js/videopress-plupload.js', __FILE__ ), + Jetpack::get_file_url_for_environment( + '_inc/build/videopress/js/videopress-plupload.min.js', + 'modules/videopress/js/videopress-plupload.js' + ), array( 'jquery', 'wp-plupload' @@ -140,7 +143,10 @@ class Jetpack_VideoPress { wp_enqueue_script( 'videopress-uploader', - plugins_url( 'js/videopress-uploader.js', __FILE__ ), + Jetpack::get_file_url_for_environment( + '_inc/build/videopress/js/videopress-uploader.min.js', + 'modules/videopress/js/videopress-uploader.js' + ), array( 'videopress-plupload' ), @@ -149,7 +155,10 @@ class Jetpack_VideoPress { wp_enqueue_script( 'media-video-widget-extensions', - plugins_url( 'js/media-video-widget-extensions.js', __FILE__ ), + Jetpack::get_file_url_for_environment( + '_inc/build/videopress/js/media-video-widget-extensions.min.js', + 'modules/videopress/js/media-video-widget-extensions.js' + ), array(), $this->version, true diff --git a/plugins/jetpack/modules/videopress/editor-media-view.php b/plugins/jetpack/modules/videopress/editor-media-view.php index 427c3552..559eed3a 100644 --- a/plugins/jetpack/modules/videopress/editor-media-view.php +++ b/plugins/jetpack/modules/videopress/editor-media-view.php @@ -13,7 +13,16 @@ function videopress_handle_editor_view_js() { add_action( 'admin_print_footer_scripts', 'videopress_editor_view_js_templates' ); wp_enqueue_style( 'videopress-editor-ui', plugins_url( 'css/editor.css', __FILE__ ) ); - wp_enqueue_script( 'videopress-editor-view', plugins_url( 'js/editor-view.js', __FILE__ ), array( 'wp-util', 'jquery' ), false, true ); + wp_enqueue_script( + 'videopress-editor-view', + Jetpack::get_file_url_for_environment( + '_inc/build/videopress/js/editor-view.min.js', + 'modules/videopress/js/editor-view.js' + ), + array( 'wp-util', 'jquery' ), + false, + true + ); wp_localize_script( 'videopress-editor-view', 'vpEditorView', array( 'home_url_host' => parse_url( home_url(), PHP_URL_HOST ), 'min_content_width' => VIDEOPRESS_MIN_WIDTH, diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions.php b/plugins/jetpack/modules/widget-visibility/widget-conditions.php index e85b25ae..0360909a 100644 --- a/plugins/jetpack/modules/widget-visibility/widget-conditions.php +++ b/plugins/jetpack/modules/widget-visibility/widget-conditions.php @@ -23,7 +23,16 @@ class Jetpack_Widget_Conditions { public static function widget_admin_setup() { wp_enqueue_style( 'widget-conditions', plugins_url( 'widget-conditions/widget-conditions.css', __FILE__ ) ); wp_style_add_data( 'widget-conditions', 'rtl', 'replace' ); - wp_enqueue_script( 'widget-conditions', plugins_url( 'widget-conditions/widget-conditions.js', __FILE__ ), array( 'jquery', 'jquery-ui-core' ), 20140721, true ); + wp_enqueue_script( + 'widget-conditions', + Jetpack::get_file_url_for_environment( + '_inc/build/widget-visibility/widget-conditions/widget-conditions.min.js', + 'modules/widget-visibility/widget-conditions/widget-conditions.js' + ), + array( 'jquery', 'jquery-ui-core' ), + 20171227, + true + ); // Set up a single copy of all of the data that Widget Visibility needs. // This allows all widget conditions to reuse the same data, keeping page size down diff --git a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js index 609c6c7f..4a9eac59 100644 --- a/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js +++ b/plugins/jetpack/modules/widget-visibility/widget-conditions/widget-conditions.js @@ -224,12 +224,12 @@ jQuery( function( $ ) { subkey = majorData[i][1][j][0]; subval = majorData[i][1][j][1]; - optgroup.append( $( '<option/>' ).val( subkey ).text( subval.replace( / /g, '\xA0' ) ) ); + optgroup.append( $( '<option/>' ).val( subkey ).text( decodeEntities( subval.replace( / /g, '\xA0' ) ) ) ); } select.append( optgroup ); } else { - select.append( $( '<option/>' ).val( key ).text( val.replace( / /g, '\xA0' ) ) ); + select.append( $( '<option/>' ).val( key ).text( decodeEntities( val.replace( / /g, '\xA0' ) ) ) ); } } @@ -256,4 +256,10 @@ jQuery( function( $ ) { index++; } ); } + + function decodeEntities( encodedString ) { + var textarea = document.createElement( 'textarea' ); + textarea.innerHTML = encodedString; + return textarea.value; + } } ); diff --git a/plugins/jetpack/modules/widgets/contact-info.php b/plugins/jetpack/modules/widgets/contact-info.php index d994f20b..3705e975 100644 --- a/plugins/jetpack/modules/widgets/contact-info.php +++ b/plugins/jetpack/modules/widgets/contact-info.php @@ -246,7 +246,15 @@ if ( ! class_exists( 'Jetpack_Contact_Info_Widget' ) ) { */ function form( $instance ) { $instance = wp_parse_args( $instance, $this->defaults() ); - wp_enqueue_script( 'contact-info-admin', plugins_url( 'contact-info/contact-info-admin.js', __FILE__ ), array( 'jquery' ), 20160727 ); + wp_enqueue_script( + 'contact-info-admin', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/contact-info/contact-info-admin.min.js', + 'modules/widgets/contact-info/contact-info-admin.js' + ), + array( 'jquery' ), + 20160727 + ); ?> <p> diff --git a/plugins/jetpack/modules/widgets/eu-cookie-law.php b/plugins/jetpack/modules/widgets/eu-cookie-law.php index 14bb9460..a5e37dba 100644 --- a/plugins/jetpack/modules/widgets/eu-cookie-law.php +++ b/plugins/jetpack/modules/widgets/eu-cookie-law.php @@ -94,7 +94,16 @@ if ( ! class_exists( 'Jetpack_EU_Cookie_Law_Widget' ) ) { */ function enqueue_frontend_scripts() { wp_enqueue_style( 'eu-cookie-law-style', plugins_url( 'eu-cookie-law/style.css', __FILE__ ), array(), '20170403' ); - wp_enqueue_script( 'eu-cookie-law-script', plugins_url( 'eu-cookie-law/eu-cookie-law.js', __FILE__ ), array( 'jquery' ), '20170404', true ); + wp_enqueue_script( + 'eu-cookie-law-script', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/eu-cookie-law/eu-cookie-law.min.js', + 'modules/widgets/eu-cookie-law/eu-cookie-law.js' + ), + array( 'jquery' ), + '20170404', + true + ); } /** diff --git a/plugins/jetpack/modules/widgets/gallery.php b/plugins/jetpack/modules/widgets/gallery.php index ca68dd03..83b73845 100644 --- a/plugins/jetpack/modules/widgets/gallery.php +++ b/plugins/jetpack/modules/widgets/gallery.php @@ -34,8 +34,7 @@ class Jetpack_Gallery_Widget extends WP_Widget { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); if ( class_exists( 'Jetpack_Tiled_Gallery' ) ) { - $tiled_gallery = new Jetpack_Tiled_Gallery(); - add_action( 'wp_enqueue_scripts', array( $tiled_gallery, 'default_scripts_and_styles' ) ); + add_action( 'wp_enqueue_scripts', array( 'Jetpack_Tiled_Gallery', 'default_scripts_and_styles' ) ); } if ( class_exists( 'Jetpack_Slideshow_Shortcode' ) ) { @@ -179,8 +178,7 @@ class Jetpack_Gallery_Widget extends WP_Widget { return; } - $widget_tiled_gallery = new Jetpack_Tiled_Gallery(); - $widget_tiled_gallery->default_scripts_and_styles(); + Jetpack_Tiled_Gallery::default_scripts_and_styles(); $layout = new Jetpack_Tiled_Gallery_Layout_Rectangular( $instance['attachments'], $instance['link'], false, 3 ); return $layout->HTML(); @@ -199,8 +197,7 @@ class Jetpack_Gallery_Widget extends WP_Widget { return; } - $widget_tiled_gallery = new Jetpack_Tiled_Gallery(); - $widget_tiled_gallery->default_scripts_and_styles(); + Jetpack_Tiled_Gallery::default_scripts_and_styles(); $layout = new Jetpack_Tiled_Gallery_Layout_Square( $instance['attachments'], $instance['link'], false, 3 ); return $layout->HTML(); @@ -219,8 +216,7 @@ class Jetpack_Gallery_Widget extends WP_Widget { return; } - $widget_tiled_gallery = new Jetpack_Tiled_Gallery(); - $widget_tiled_gallery->default_scripts_and_styles(); + Jetpack_Tiled_Gallery::default_scripts_and_styles(); $layout = new Jetpack_Tiled_Gallery_Layout_Circle( $instance['attachments'], $instance['link'], false, 3 ); return $layout->HTML(); @@ -394,7 +390,13 @@ class Jetpack_Gallery_Widget extends WP_Widget { } public function enqueue_frontend_scripts() { - wp_register_script( 'gallery-widget', plugins_url( '/gallery/js/gallery.js', __FILE__ ) ); + wp_register_script( + 'gallery-widget', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/gallery/js/gallery.min.js', + 'modules/widgets/gallery/js/gallery.js' + ) + ); wp_enqueue_script( 'gallery-widget' ); } @@ -405,7 +407,13 @@ class Jetpack_Gallery_Widget extends WP_Widget { if ( 'widgets.php' == $pagenow || 'customize.php' == $pagenow ) { wp_enqueue_media(); - wp_enqueue_script( 'gallery-widget-admin', plugins_url( '/gallery/js/admin.js', __FILE__ ), array( + wp_enqueue_script( + 'gallery-widget-admin', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/gallery/js/admin.min.js', + 'modules/widgets/gallery/js/admin.js' + ), + array( 'media-models', 'media-views' ), diff --git a/plugins/jetpack/modules/widgets/goodreads.php b/plugins/jetpack/modules/widgets/goodreads.php index 2b823c23..e8b04136 100644 --- a/plugins/jetpack/modules/widgets/goodreads.php +++ b/plugins/jetpack/modules/widgets/goodreads.php @@ -59,7 +59,7 @@ class WPCOM_Widget_Goodreads extends WP_Widget { echo '<p>' . sprintf( __( 'You need to enter your numeric user ID for the <a href="%1$s">Goodreads Widget</a> to work correctly. <a href="%2$s" target="_blank">Full instructions</a>.', 'jetpack' ), esc_url( admin_url( 'widgets.php' ) ), - 'http://support.wordpress.com/widgets/goodreads-widget/#goodreads-user-id' + 'https://support.wordpress.com/widgets/goodreads-widget/#goodreads-user-id' ) . '</p>'; echo $args['after_widget']; } @@ -88,12 +88,13 @@ class WPCOM_Widget_Goodreads extends WP_Widget { } function goodreads_user_id_exists( $user_id ) { - $url = "http://www.goodreads.com/user/show/$user_id/"; - $response = wp_remote_head( $url, array( 'httpversion'=>'1.1', 'timeout'=>3, 'redirection'=> 2 ) ); - if ( wp_remote_retrieve_response_code( $response ) === 200 ) + $url = "https://www.goodreads.com/user/show/$user_id/"; + $response = wp_remote_head( $url, array( 'httpversion' => '1.1', 'timeout' => 3, 'redirection' => 2 ) ); + if ( 200 === wp_remote_retrieve_response_code( $response ) ) { return true; - else + } else { return false; + } } function update( $new_instance, $old_instance ) { diff --git a/plugins/jetpack/modules/widgets/google-translate.php b/plugins/jetpack/modules/widgets/google-translate.php index a5787112..6bb8c7e6 100644 --- a/plugins/jetpack/modules/widgets/google-translate.php +++ b/plugins/jetpack/modules/widgets/google-translate.php @@ -44,7 +44,13 @@ class Jetpack_Google_Translate_Widget extends WP_Widget { * Enqueue frontend JS scripts. */ public function enqueue_scripts() { - wp_register_script( 'google-translate-init', plugins_url( 'google-translate/google-translate.js', __FILE__ ) ); + wp_register_script( + 'google-translate-init', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/google-translate/google-translate.min.js', + 'modules/widgets/google-translate/google-translate.js' + ) + ); wp_register_script( 'google-translate', '//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit', array( 'google-translate-init' ) ); // Admin bar is also displayed on top of the site which causes google translate bar to hide beneath. // This is a hack to show google translate bar a bit lower. diff --git a/plugins/jetpack/modules/widgets/googleplus-badge.php b/plugins/jetpack/modules/widgets/googleplus-badge.php index 1a0ff1af..7f1f7693 100644 --- a/plugins/jetpack/modules/widgets/googleplus-badge.php +++ b/plugins/jetpack/modules/widgets/googleplus-badge.php @@ -71,7 +71,14 @@ class WPCOM_Widget_GooglePlus_Badge extends WP_Widget { global $pagenow; if ( 'widgets.php' == $pagenow || 'customize.php' == $pagenow ) { - wp_enqueue_script( 'googleplus-widget-admin', plugins_url( '/google-plus/js/admin.js', __FILE__ ), array( 'jquery' ) ); + wp_enqueue_script( + 'googleplus-widget-admin', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/google-plus/js/admin.min.js', + 'modules/widgets/google-plus/js/admin.js' + ), + array( 'jquery' ) + ); } } diff --git a/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php b/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php index e2a12ca8..70d2d12b 100644 --- a/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php +++ b/plugins/jetpack/modules/widgets/migrate-to-core/image-widget.php @@ -130,6 +130,7 @@ function jetpack_migrate_image_widget() { ), 'post_status' => 'inherit', 'post_type' => 'attachment', + 'suppress_filters' => false, ) ); foreach ( $attachment_ids as $attachment_id ) { diff --git a/plugins/jetpack/modules/widgets/milestone/milestone.php b/plugins/jetpack/modules/widgets/milestone/milestone.php index 2364d7fa..2dd38361 100644 --- a/plugins/jetpack/modules/widgets/milestone/milestone.php +++ b/plugins/jetpack/modules/widgets/milestone/milestone.php @@ -60,12 +60,30 @@ class Milestone_Widget extends WP_Widget { public static function enqueue_admin( $hook_suffix ) { if ( 'widgets.php' == $hook_suffix ) { wp_enqueue_style( 'milestone-admin', self::$url . 'style-admin.css', array(), '20161215' ); - wp_enqueue_script( 'milestone-admin-js', self::$url . 'admin.js', array( 'jquery' ), '20170915', true ); + wp_enqueue_script( + 'milestone-admin-js', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/milestone/admin.min.js', + 'modules/widgets/milestone/admin.js' + ), + array( 'jquery' ), + '20170915', + true + ); } } public static function enqueue_template() { - wp_enqueue_script( 'milestone', self::$url . 'milestone.js', array( 'jquery' ), '20160520', true ); + wp_enqueue_script( + 'milestone', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/milestone/milestone.min.js', + 'modules/widgets/milestone/milestone.js' + ), + array( 'jquery' ), + '20160520', + true + ); } public static function styles_template() { diff --git a/plugins/jetpack/modules/widgets/search.php b/plugins/jetpack/modules/widgets/search.php new file mode 100644 index 00000000..38752ffd --- /dev/null +++ b/plugins/jetpack/modules/widgets/search.php @@ -0,0 +1,760 @@ +<?php +/** + * Jetpack Search: Jetpack_Search_Widget class + * + * @package Jetpack + * @subpackage Jetpack Search + * @since 5.0.0 + */ + +require_once JETPACK__PLUGIN_DIR . 'modules/search/class.jetpack-search-helpers.php'; + +add_action( 'widgets_init', 'jetpack_search_widget_init' ); + +function jetpack_search_widget_init() { + if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) { + return; + } + + register_widget( 'Jetpack_Search_Widget' ); +} + +/** + * Provides a widget to show available/selected filters on searches. + * + * @since 5.0.0 + * + * @see WP_Widget + */ +class Jetpack_Search_Widget extends WP_Widget { + + /** + * The Jetpack_Search instance. + * + * @since 5.7.0 + * @var Jetpack_Search + */ + protected $jetpack_search; + + /** + * Number of aggregations (filters) to show by default. + * + * @since 5.8.0 + * @var int + */ + const DEFAULT_FILTER_COUNT = 5; + + /** + * Default sort order for search results. + * + * @since 5.8.0 + * @var string + */ + const DEFAULT_SORT = 'relevance_desc'; + + /** + * Jetpack_Search_Widget constructor. + * + * @since 5.0.0 + */ + public function __construct() { + parent::__construct( + Jetpack_Search_Helpers::FILTER_WIDGET_BASE, + /** This filter is documented in modules/widgets/facebook-likebox.php */ + apply_filters( 'jetpack_widget_name', esc_html__( 'Search', 'jetpack' ) ), + array( + 'classname' => 'jetpack-filters widget_search', + 'description' => __( 'Replaces the default search with an Elasticsearch-powered search interface and filters.', 'jetpack' ), + ) + ); + + if ( + Jetpack_Search_Helpers::is_active_widget( $this->id ) && + ! Jetpack::is_module_active( 'search' ) + ) { + Jetpack::activate_module( 'search', false, false ); + } + + if ( is_admin() ) { + add_action( 'sidebar_admin_setup', array( $this, 'widget_admin_setup' ) ); + } else { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); + } + + add_action( 'jetpack_search_render_filters_widget_title', array( 'Jetpack_Search_Template_Tags', 'render_widget_title' ), 10, 3 ); + add_action( 'jetpack_search_render_filters', array( 'Jetpack_Search_Template_Tags', 'render_available_filters' ), 10, 2 ); + } + + /** + * Enqueues the scripts and styles needed for the customizer. + * + * @since 5.7.0 + */ + public function widget_admin_setup() { + wp_enqueue_style( 'widget-jetpack-search-filters', plugins_url( 'search/css/search-widget-admin-ui.css', __FILE__ ) ); + + // Required for Tracks + wp_register_script( + 'jp-tracks', + '//stats.wp.com/w.js', + array(), + gmdate( 'YW' ), + true + ); + + wp_register_script( + 'jp-tracks-functions', + plugins_url( '_inc/lib/tracks/tracks-callables.js', JETPACK__PLUGIN_FILE ), + array(), + JETPACK__VERSION, + false + ); + + wp_register_script( + 'jetpack-search-widget-admin', + plugins_url( 'search/js/search-widget-admin.js', __FILE__ ), + array( 'jquery', 'jquery-ui-sortable', 'jp-tracks', 'jp-tracks-functions' ), + JETPACK__VERSION + ); + + wp_localize_script( 'jetpack-search-widget-admin', 'jetpack_search_filter_admin', array( + 'defaultFilterCount' => self::DEFAULT_FILTER_COUNT, + 'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(), + 'tracksEventData' => array( + 'is_customizer' => ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) ? 1 : 0, + ), + 'i18n' => array( + 'month' => Jetpack_Search_Helpers::get_date_filter_type_name( 'month', false ), + 'year' => Jetpack_Search_Helpers::get_date_filter_type_name( 'year', false ), + 'monthUpdated' => Jetpack_Search_Helpers::get_date_filter_type_name( 'month', true ), + 'yearUpdated' => Jetpack_Search_Helpers::get_date_filter_type_name( 'year', true ), + ), + ) ); + + wp_enqueue_script( 'jetpack-search-widget-admin' ); + } + + /** + * Enqueue scripts and styles for the frontend. + * + * @since 5.8.0 + */ + public function enqueue_frontend_scripts() { + if ( ! is_active_widget( false, false, $this->id_base, true ) ) { + return; + } + + wp_enqueue_script( + 'jetpack-search-widget', + plugins_url( 'search/js/search-widget.js', __FILE__ ), + array( 'jquery' ), + JETPACK__VERSION, + true + ); + + wp_enqueue_style( 'jetpack-search-widget', plugins_url( 'search/css/search-widget-frontend.css', __FILE__ ) ); + } + + /** + * Get the list of valid sort types/orders. + * + * @since 5.8.0 + * + * @return array The sort orders. + */ + private function get_sort_types() { + return array( + 'relevance|DESC' => is_admin() ? esc_html__( 'Relevance (recommended)', 'jetpack' ) : esc_html__( 'Relevance', 'jetpack' ), + 'date|DESC' => esc_html__( 'Newest first', 'jetpack' ), + 'date|ASC' => esc_html__( 'Oldest first', 'jetpack' ) + ); + } + + /** + * Callback for an array_filter() call in order to only get filters for the current widget. + * + * @see Jetpack_Search_Widget::widget() + * + * @since 5.7.0 + * + * @param array $item Filter item. + * + * @return bool Whether the current filter item is for the current widget. + */ + function is_for_current_widget( $item ) { + return isset( $item['widget_id'] ) && $this->id == $item['widget_id']; + } + + /** + * This method returns a boolean for whether the widget should show site-wide filters for the site. + * + * This is meant to provide backwards-compatibility for VIP, and other professional plan users, that manually + * configured filters via `Jetpack_Search::set_filters()`. + * + * @since 5.7.0 + * + * @return bool Whether the widget should display site-wide filters or not. + */ + public function should_display_sitewide_filters() { + $filter_widgets = get_option( 'widget_jetpack-search-filters' ); + + // This shouldn't be empty, but just for sanity + if ( empty( $filter_widgets ) ) { + return false; + } + + // If any widget has any filters, return false + foreach ( $filter_widgets as $number => $widget ) { + $widget_id = sprintf( '%s-%d', $this->id_base, $number ); + if ( ! empty( $widget['filters'] ) && is_active_widget( false, $widget_id, $this->id_base ) ) { + return false; + } + } + + return true; + } + + /** + * Responsible for rendering the widget on the frontend. + * + * @since 5.0.0 + * + * @param array $args Widgets args supplied by the theme. + * @param array $instance The current widget instance. + */ + public function widget( $args, $instance ) { + $display_filters = false; + + if ( is_search() ) { + if ( Jetpack_Search_Helpers::should_rerun_search_in_customizer_preview() ) { + Jetpack_Search::instance()->update_search_results_aggregations(); + } + + $filters = Jetpack_Search::instance()->get_filters(); + + if ( ! Jetpack_Search_Helpers::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) { + $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) ); + } + + if ( ! empty( $filters ) ) { + $display_filters = true; + } + } + + if ( ! $display_filters && empty( $instance['search_box_enabled'] ) && empty( $instance['user_sort_enabled'] ) ) { + return; + } + + $title = isset( $instance['title'] ) ? $instance['title'] : ''; + + if ( empty( $title ) ) { + $title = ''; + } + + /** This filter is documented in core/src/wp-includes/default-widgets.php */ + $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); + + echo $args['before_widget']; + ?><div id="<?php echo esc_attr( $this->id ); ?>-wrapper"><?php + + if ( ! empty( $title ) ) { + /** + * Responsible for displaying the title of the Jetpack Search filters widget. + * + * @module search + * + * @since 5.7.0 + * + * @param string $title The widget's title + * @param string $args['before_title'] The HTML tag to display before the title + * @param string $args['after_title'] The HTML tag to display after the title + */ + do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] ); + } + + $default_sort = isset( $instance['sort'] ) ? $instance['sort'] : self::DEFAULT_SORT; + list( $orderby, $order ) = $this->sorting_to_wp_query_param( $default_sort ); + $current_sort = "{$orderby}|{$order}"; + + // we need to dynamically inject the sort field into the search box when the search box is enabled, and display + // it separately when it's not. + if ( ! empty( $instance['search_box_enabled'] ) ) { + Jetpack_Search_Template_Tags::render_widget_search_form( $instance['post_types'], $orderby, $order ); + } + + if ( ! empty( $instance['search_box_enabled'] ) && ! empty( $instance['user_sort_enabled'] ) ): ?> + <div class="jetpack-search-sort-wrapper"> + <label> + <?php esc_html_e( 'Sort by', 'jetpack' ); ?> + <select class="jetpack-search-sort"> + <?php foreach ( $this->get_sort_types() as $sort => $label ) { ?> + <option value="<?php echo esc_attr( $sort ); ?>" <?php selected( $current_sort, $sort ); ?>> + <?php echo esc_html( $label ); ?> + </option> + <?php } ?> + </select> + </label> + </div> + <?php endif; + + if ( $display_filters ) { + /** + * Responsible for rendering filters to narrow down search results. + * + * @module search + * + * @since 5.8.0 + * + * @param array $filters The possible filters for the current query. + * @param array $post_types An array of post types to limit filtering to. + */ + do_action( + 'jetpack_search_render_filters', + $filters, + isset( $instance['post_types'] ) ? $instance['post_types'] : null + ); + } + + $this->maybe_render_sort_javascript( $instance, $order, $orderby ); + + echo "</div>"; + echo $args['after_widget']; + } + + /** + * Renders JavaScript for the sorting controls on the frontend. + * + * This JS is a bit complicated, but here's what it's trying to do: + * - find the search form + * - find the orderby/order fields and set default values + * - detect changes to the sort field, if it exists, and use it to set the order field values + * + * @since 5.8.0 + * + * @param array $instance The current widget instance. + * @param string $order The order to initialize the select with. + * @param string $orderby The orderby to initialize the select with. + */ + private function maybe_render_sort_javascript( $instance, $order, $orderby ) { + if ( ! empty( $instance['user_sort_enabled'] ) ) : + ?> + <script type="text/javascript"> + jQuery( document ).ready( function( $ ) { + var orderByDefault = <?php echo wp_json_encode( $orderby ); ?>, + orderDefault = <?php echo wp_json_encode( $order ); ?>, + widgetId = <?php echo wp_json_encode( $this->id ); ?>, + searchQuery = <?php echo wp_json_encode( get_query_var( 's', '' ) ); ?>, + isSearch = <?php echo wp_json_encode( is_search() ); ?>; + + var container = $( '#' + widgetId + '-wrapper' ), + form = container.find('.jetpack-search-form form'), + orderBy = form.find( 'input[name=orderby]'), + order = form.find( 'input[name=order]'), + searchInput = form.find( 'input[name="s"]' ); + + orderBy.val( orderByDefault ); + order.val( orderDefault ); + + // Some themes don't set the search query, which results in the query being lost + // when doing a sort selection. So, if the query isn't set, let's set it now. This approach + // is chosen over running a regex over HTML for every search query performed. + if ( isSearch && ! searchInput.val() ) { + searchInput.val( searchQuery ); + } + + searchInput.addClass( 'show-placeholder' ); + + container.find( '.jetpack-search-sort' ).change( function( event ) { + var values = event.target.value.split( '|' ); + orderBy.val( values[0] ); + order.val( values[1] ); + + form.submit(); + }); + } ); + </script> + <?php + endif; + } + + /** + * Convert a sort string into the separate order by and order parts. + * + * @since 5.8.0 + * + * @param string $sort A sort string. + * + * @return array Order by and order. + */ + private function sorting_to_wp_query_param( $sort ) { + $parts = explode( '|', $sort ); + $orderby = isset( $_GET['orderby'] ) + ? $_GET['orderby'] + : $parts[0]; + + $order = isset( $_GET['order'] ) + ? strtoupper( $_GET['order'] ) + : ( ( isset( $parts[1] ) && 'ASC' === strtoupper( $parts[1] ) ) ? 'ASC' : 'DESC' ); + + return array( $orderby, $order ); + } + + /** + * Updates a particular instance of the widget. Validates and sanitizes the options. + * + * @since 5.0.0 + * + * @param array $new_instance New settings for this instance as input by the user via Jetpack_Search_Widget::form(). + * @param array $old_instance Old settings for this instance. + * + * @return array Settings to save. + */ + public function update( $new_instance, $old_instance ) { + $instance = array(); + + $instance['title'] = sanitize_text_field( $new_instance['title'] ); + $instance['search_box_enabled'] = empty( $new_instance['search_box_enabled'] ) ? '0' : '1'; + $instance['user_sort_enabled'] = empty( $new_instance['user_sort_enabled'] ) ? '0' : '1'; + $instance['sort'] = $new_instance['sort']; + $instance['post_types'] = empty( $new_instance['post_types'] ) || empty( $instance['search_box_enabled'] ) + ? array() + : array_map( 'sanitize_key', $new_instance['post_types'] ); + + $filters = array(); + if ( isset( $new_instance['filter_type'] ) ) { + foreach ( (array) $new_instance['filter_type'] as $index => $type ) { + $count = intval( $new_instance['num_filters'][ $index ] ); + $count = min( 50, $count ); // Set max boundary at 20. + $count = max( 1, $count ); // Set min boundary at 1. + + switch ( $type ) { + case 'taxonomy': + $filters[] = array( + 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ), + 'type' => 'taxonomy', + 'taxonomy' => sanitize_key( $new_instance['taxonomy_type'][ $index ] ), + 'count' => $count, + ); + break; + case 'post_type': + $filters[] = array( + 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ), + 'type' => 'post_type', + 'count' => $count, + ); + break; + case 'date_histogram': + $filters[] = array( + 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ), + 'type' => 'date_histogram', + 'count' => $count, + 'field' => sanitize_key( $new_instance['date_histogram_field'][ $index ] ), + 'interval' => sanitize_key( $new_instance['date_histogram_interval'][ $index ] ), + ); + break; + } + } + } + + if ( ! empty( $filters ) ) { + $instance['filters'] = $filters; + } + + return $instance; + } + + /** + * Outputs the settings update form. + * + * @since 5.0.0 + * + * @param array $instance Current settings. + */ + public function form( $instance ) { + $instance = wp_parse_args( (array) $instance, array( + 'title' => '', + 'filters' => array( array() ) + ) ); + + $title = strip_tags( $instance['title'] ); + + $hide_filters = Jetpack_Search_Helpers::are_filters_by_widget_disabled(); + $search_box_enabled = ! isset( $instance['search_box_enabled'] ) || ! empty( $instance['search_box_enabled'] ); + $user_sort_enabled = ! empty( $instance['user_sort_enabled'] ); + $sort = isset( $instance['sort'] ) ? $instance['sort'] : self::DEFAULT_SORT; + $classes = sprintf( + 'jetpack-search-filters-widget %s %s %s', + $hide_filters ? 'hide-filters' : '', + $search_box_enabled ? '' : 'hide-post-types', + $this->id + ); + ?> + <div class="<?php echo esc_attr( $classes ); ?>"> + <p> + <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"> + <?php esc_html_e( 'Title (optional):', 'jetpack' ); ?> + </label> + <input + class="widefat" + id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" + type="text" + value="<?php echo esc_attr( $title ); ?>" + /> + </p> + + <p> + <label> + <input + type="checkbox" + class="jetpack-search-filters-widget__search-box-enabled" + name="<?php echo esc_attr( $this->get_field_name( 'search_box_enabled' ) ); ?>" + <?php checked( $search_box_enabled ); ?> + /> + <?php esc_html_e( 'Show search box', 'jetpack' ); ?> + </label> + </p> + <p> + <label> + <input + type="checkbox" + class="jetpack-search-filters-widget__sort-controls-enabled" + name="<?php echo esc_attr( $this->get_field_name( 'user_sort_enabled' ) ); ?>" + <?php checked( $user_sort_enabled ); ?> + <?php disabled( ! $search_box_enabled ); ?> + /> + <?php esc_html_e( 'Show sort selection dropdown', 'jetpack' ); ?> + </label> + </p> + + <p class="jetpack-search-filters-widget__post-types-select"> + <label><?php esc_html_e( 'Post types to search (minimum of 1):', 'jetpack' ); ?></label> + <?php foreach ( get_post_types( array( 'exclude_from_search' => false ), 'objects' ) as $post_type ) : ?> + <label> + <input + type="checkbox" + value="<?php echo esc_attr( $post_type->name ); ?>" + name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]" + <?php checked( empty( $instance['post_types'] ) || in_array( $post_type->name, $instance['post_types'] ) ); ?> + /> + <?php echo esc_html( $post_type->label ); ?> + </label> + <?php endforeach; ?> + </p> + + <p> + <label> + <?php esc_html_e( 'Default sort order:', 'jetpack' ); ?> + <select + name="<?php echo esc_attr( $this->get_field_name( 'sort' ) ); ?>" + class="widefat jetpack-search-filters-widget__sort-order"> + <?php foreach ( $this->get_sort_types() as $sort_type => $label ) { ?> + <option value="<?php echo esc_attr( $sort_type ); ?>" <?php selected( $sort, $sort_type ); ?>> + <?php echo esc_html( $label ); ?> + </option> + <?php } ?> + </select> + </label> + </p> + + <?php if ( ! $hide_filters ): ?> + <script class="jetpack-search-filters-widget__filter-template" type="text/template"> + <?php echo $this->render_widget_edit_filter( array(), true ); ?> + </script> + <div class="jetpack-search-filters-widget__filters"> + <?php foreach ( (array) $instance['filters'] as $filter ) : ?> + <?php $this->render_widget_edit_filter( $filter ); ?> + <?php endforeach; ?> + </div> + <p class="jetpack-search-filters-widget__add-filter-wrapper"> + <a class="button jetpack-search-filters-widget__add-filter" href="#"> + <?php esc_html_e( 'Add a filter', 'jetpack' ); ?> + </a> + </p> + <noscript> + <p class="jetpack-search-filters-help"> + <?php echo esc_html_e( 'Adding filters requires JavaScript!', 'jetpack' ); ?> + </p> + </noscript> + <?php if ( is_customize_preview() ) : ?> + <p class="jetpack-search-filters-help"> + <a href="https://jetpack.com/support/search/#filters-not-showing-up" target="_blank"> + <?php esc_html_e( "Why aren't my filters appearing?", 'jetpack' ); ?> + </a> + </p> + <?php endif; ?> + <?php endif; ?> + </div> + <?php + } + + /** + * We need to render HTML in two formats: an Underscore template (client-size) + * and native PHP (server-side). This helper function allows for easy rendering + * of attributes in both formats. + * + * @since 5.8.0 + * + * @param string $name Attribute name. + * @param string $value Attribute value. + * @param bool $is_template Whether this is for an Underscore template or not. + */ + private function render_widget_attr( $name, $value, $is_template ) { + echo $is_template ? "<%= $name %>" : esc_attr( $value ); + } + + /** + * We need to render HTML in two formats: an Underscore template (client-size) + * and native PHP (server-side). This helper function allows for easy rendering + * of the "selected" attribute in both formats. + * + * @since 5.8.0 + * + * @param string $name Attribute name. + * @param string $value Attribute value. + * @param string $compare Value to compare to the attribute value to decide if it should be selected. + * @param bool $is_template Whether this is for an Underscore template or not. + */ + private function render_widget_option_selected( $name, $value, $compare, $is_template ) { + $compare_json = wp_json_encode( $compare ); + echo $is_template ? "<%= $compare_json === $name ? 'selected=\"selected\"' : '' %>" : selected( $value, $compare ); + } + + /** + * Responsible for rendering a single filter in the customizer or the widget administration screen in wp-admin. + * + * We use this method for two purposes - rendering the fields server-side, and also rendering a script template for Underscore. + * + * @since 5.7.0 + * + * @param array $filter The filter to render. + * @param bool $is_template Whether this is for an Underscore template or not. + */ + public function render_widget_edit_filter( $filter, $is_template = false ) { + $args = wp_parse_args( $filter, array( + 'name' => '', + 'type' => 'taxonomy', + 'taxonomy' => '', + 'post_type' => '', + 'field' => '', + 'interval' => '', + 'count' => self::DEFAULT_FILTER_COUNT, + ) ); + + $args['name_placeholder'] = Jetpack_Search_Helpers::generate_widget_filter_name( $args ); + + ?> + <div class="jetpack-search-filters-widget__filter is-<?php $this->render_widget_attr( 'type', $args['type'], $is_template ); ?>"> + <p class="jetpack-search-filters-widget__type-select"> + <label> + <?php esc_html_e( 'Filter Type:', 'jetpack' ); ?> + <select name="<?php echo esc_attr( $this->get_field_name( 'filter_type' ) ); ?>[]" class="widefat filter-select"> + <option value="taxonomy" <?php $this->render_widget_option_selected( 'type', $args['type'], 'taxonomy', $is_template ); ?>> + <?php esc_html_e( 'Taxonomy', 'jetpack' ); ?> + </option> + <option value="post_type" <?php $this->render_widget_option_selected( 'type', $args['type'], 'post_type', $is_template ); ?>> + <?php esc_html_e( 'Post Type', 'jetpack' ); ?> + </option> + <option value="date_histogram" <?php $this->render_widget_option_selected( 'type', $args['type'], 'date_histogram', $is_template ); ?>> + <?php esc_html_e( 'Date', 'jetpack' ); ?> + </option> + </select> + </label> + </p> + + <p class="jetpack-search-filters-widget__taxonomy-select"> + <label> + <?php + esc_html_e( 'Choose a taxonomy:', 'jetpack' ); + $seen_taxonomy_labels = array(); + ?> + <select name="<?php echo esc_attr( $this->get_field_name( 'taxonomy_type' ) ); ?>[]" class="widefat taxonomy-select"> + <?php foreach ( get_taxonomies( array( 'public' => true ), 'objects' ) as $taxonomy ) : ?> + <option value="<?php echo esc_attr( $taxonomy->name ); ?>" <?php $this->render_widget_option_selected( 'taxonomy', $args['taxonomy'], $taxonomy->name, $is_template ); ?>> + <?php + $label = in_array( $taxonomy->label, $seen_taxonomy_labels ) + ? sprintf( + /* translators: %1$s is the taxonomy name, %2s is the name of its type to help distinguish between several taxonomies with the same name, e.g. category and tag. */ + _x( '%1$s (%2$s)', 'A label for a taxonomy selector option', 'jetpack' ), + $taxonomy->label, + $taxonomy->name + ) + : $taxonomy->label; + echo esc_html( $label ); + $seen_taxonomy_labels[] = $taxonomy->label; + ?> + </option> + <?php endforeach; ?> + </select> + </label> + </p> + + <p class="jetpack-search-filters-widget__date-histogram-select"> + <label> + <?php esc_html_e( 'Choose a field:', 'jetpack' ); ?> + <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_field' ) ); ?>[]" class="widefat date-field-select"> + <option value="post_date" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date', $is_template ); ?>> + <?php esc_html_e( 'Date', 'jetpack' ); ?> + </option> + <option value="post_date_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date_gmt', $is_template ); ?>> + <?php esc_html_e( 'Date GMT', 'jetpack' ); ?> + </option> + <option value="post_modified" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified', $is_template ); ?>> + <?php esc_html_e( 'Modified', 'jetpack' ); ?> + </option> + <option value="post_modified_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified_gmt', $is_template ); ?>> + <?php esc_html_e( 'Modified GMT', 'jetpack' ); ?> + </option> + </select> + </label> + </p> + + <p class="jetpack-search-filters-widget__date-histogram-select"> + <label> + <?php esc_html_e( 'Choose an interval:' ); ?> + <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_interval' ) ); ?>[]" class="widefat date-interval-select"> + <option value="month" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'month', $is_template ); ?>> + <?php esc_html_e( 'Month', 'jetpack' ); ?> + </option> + <option value="year" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'year', $is_template ); ?>> + <?php esc_html_e( 'Year', 'jetpack' ); ?> + </option> + </select> + </label> + </p> + + <p class="jetpack-search-filters-widget__title"> + <label> + <?php esc_html_e( 'Title:', 'jetpack' ); ?> + <input + class="widefat" + type="text" + name="<?php echo esc_attr( $this->get_field_name( 'filter_name' ) ); ?>[]" + value="<?php $this->render_widget_attr( 'name', $args['name'], $is_template ); ?>" + placeholder="<?php $this->render_widget_attr( 'name_placeholder', $args['name_placeholder'], $is_template ); ?>" + /> + </label> + </p> + + <p> + <label> + <?php esc_html_e( 'Maximum number of filters (1-50):', 'jetpack' ); ?> + <input + class="widefat filter-count" + name="<?php echo esc_attr( $this->get_field_name( 'num_filters' ) ); ?>[]" + type="number" + value="<?php $this->render_widget_attr( 'count', $args['count'], $is_template ); ?>" + min="1" + max="50" + step="1" + required + /> + </label> + </p> + + <p class="jetpack-search-filters-widget__controls"> + <a href="#" class="delete"><?php esc_html_e( 'Remove', 'jetpack' ); ?></a> + </p> + </div> + <?php } +} diff --git a/plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css b/plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css new file mode 100644 index 00000000..a3313f05 --- /dev/null +++ b/plugins/jetpack/modules/widgets/search/css/search-widget-admin-ui.css @@ -0,0 +1,87 @@ +.jetpack-search-filters-widget__filter { + background: #f9f9f9; + border: 1px solid #dfdfdf; + padding: 0 12px; + margin-bottom: 12px; + cursor: move; +} + +.jetpack-search-filters-widget__controls { + text-align: right; +} + +.jetpack-search-filters-widget .jetpack-search-filters-widget__sort-controls-enabled { + margin-left: 24px; +} + +.jetpack-search-filters-widget__controls .delete { + color: #a00; +} + +.jetpack-search-filters-widget.hide-filters .jetpack-search-filters-widget__filter { + display: none; +} + +.button.jetpack-search-filters-widget__add-filter { + margin-bottom: 10px; +} + +/* Assume that taxonomy select is the default selected. Other controls should be hidden here. */ +.jetpack-search-filters-widget__post-type-select { + display: none; +} + +.jetpack-search-filters-widget__date-histogram-select { + display: none; +} + +.jetpack-search-filters-widget__filter-placeholder { + border: 1px #555 dashed; + background-color: #eee; + height: 286px; + margin-bottom: 12px; +} + +/* When post type is selected, remove the other controls */ +.jetpack-search-filters-widget__filter.is-post_type .jetpack-search-filters-widget__taxonomy-select { + display: none; +} + +/* When date is selected, remove the other controls */ +.jetpack-search-filters-widget__filter.is-date_histogram .jetpack-search-filters-widget__date-histogram-select { + display: inline; +} + +.jetpack-search-filters-widget__filter.is-date_histogram .jetpack-search-filters-widget__taxonomy-select { + display: none; +} + +.jetpack-search-filters-widget.hide-post-types .jetpack-search-filters-widget__post-types-select { + display: none; +} + +.jetpack-search-filters-help:before { + display: inline-block; + position: relative; + font-family: dashicons; + font-size: 20px; + top: 5px; + line-height: 1px; + content:"\f223"; +} +.jetpack-search-filters-help { + padding: 5px 5px 15px 0; +} + +.jetpack-search-filters-widget__post-types-select label { + display: block; + margin-bottom: 4px; +} + +.jetpack-search-filters-widget__post-types-select input[type="checkbox"] { + margin-left: 24px; +} + +body.no-js .jetpack-search-filters-widget__add-filter-wrapper { + display: none; +} diff --git a/plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css b/plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css new file mode 100644 index 00000000..58c7cf3e --- /dev/null +++ b/plugins/jetpack/modules/widgets/search/css/search-widget-frontend.css @@ -0,0 +1,66 @@ +.jetpack-search-filters-widget__sub-heading { + font-size: inherit; + font-weight: bold; + margin: 0 0 .5em; + padding: 0; +} + +/* The first heading after the form */ +.jetpack-search-form + .jetpack-search-filters-widget__sub-heading { + margin-top: 1.5em; + margin-bottom: 0.5em !important; +} + +.jetpack-search-filters-widget__clear { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.jetpack-search-sort-wrapper { + margin-top: 1em; + margin-bottom: 1.5em; +} + +.jetpack-search-sort-wrapper label { + display: inherit; +} + +.widget_search .jetpack-search-filters-widget__filter-list input[type="checkbox"] { + width: auto; + height: auto; +} + +ul.jetpack-search-filters-widget__filter-list li { + border: none; + padding: 0; + list-style: none; +} + +ul.jetpack-search-filters-widget__filter-list li a { + text-decoration: none; +} + +ul.jetpack-search-filters-widget__filter-list li a:hover { + box-shadow: none; +} + +ul.jetpack-search-filters-widget__filter-list li label { + font-weight: inherit; + display: inherit; +} + +.jetpack-search-filters-widget__filter-list { + list-style: none; +} + +ul.jetpack-search-filters-widget__filter-list { + margin-bottom: 1.5em; +} + +body.search .jetpack-search-form input[name="s"]::placeholder { + color: transparent; +} + +body.search .jetpack-search-form input[name="s"].show-placeholder::placeholder { + color: inherit; +} diff --git a/plugins/jetpack/modules/widgets/search/js/search-widget-admin.js b/plugins/jetpack/modules/widgets/search/js/search-widget-admin.js new file mode 100644 index 00000000..5840a408 --- /dev/null +++ b/plugins/jetpack/modules/widgets/search/js/search-widget-admin.js @@ -0,0 +1,360 @@ +/* globals jetpack_search_filter_admin, jQuery, analytics */ + +( function( $, args ) { + var defaultFilterCount = ( 'undefined' !== typeof args && args.defaultFilterCount ) ? + args.defaultFilterCount : + 5; // Just in case we couldn't find the defaultFiltercount arg + + $( document ).ready( function() { + setListeners(); + + window.JetpackSearch = window.JetpackSearch || {}; + window.JetpackSearch.addFilter = addFilter; + + // Initialize Tracks + if ( 'undefined' !== typeof analytics && args.tracksUserData ) { + analytics.initialize( args.tracksUserData.userid, args.tracksUserData.username ); + } + } ); + + function generateFilterTitlePlaceholder( container ) { + var placeholder = null, + isModified = null, + isMonth = null, + type = container.find( '.filter-select' ).val(); + + if ( 'taxonomy' === type ) { + placeholder = container.find('.taxonomy-select option:selected').text().trim(); + } else if ( 'date_histogram' === type && args && args.i18n ) { + isModified = ( -1 !== container.find( '.date-field-select' ).val().indexOf( 'modified' ) ); + isMonth = ( 'month' === container.find( '.date-interval-select' ).val() ); + + if ( isMonth ) { + placeholder = isModified ? + args.i18n.monthUpdated : + args.i18n.month; + } else { + placeholder = isModified ? + args.i18n.yearUpdated : + args.i18n.year; + } + } else { + placeholder = container.find('.filter-select option:selected').text().trim(); + } + + $( container ).find('.jetpack-search-filters-widget__title input').prop( 'placeholder', placeholder ); + } + + var addFilter = function( filtersContainer, args ) { + var template = _.template( + filtersContainer + .closest( '.jetpack-search-filters-widget' ) + .find( '.jetpack-search-filters-widget__filter-template' ) + .html() + ); + generateFilterTitlePlaceholder( filtersContainer.append( template( args ) ) ); + }; + + var setListeners = function( widget ) { + widget = ( 'undefined' === typeof widget ) ? + $( '.jetpack-search-filters-widget' ): + widget; + + var getContainer = function( el ) { + return $( el ).closest('.jetpack-search-filters-widget__filter'); + }; + + widget.on( 'change', '.filter-select', function() { + var select = $( this ), + selectVal = select.val(), + eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.type = selectVal; + + select + .closest( '.jetpack-search-filters-widget__filter' ) + .attr( 'class', 'jetpack-search-filters-widget__filter' ) + .addClass( 'is-' + selectVal ); + + generateFilterTitlePlaceholder( getContainer( this ) ); + + trackAndBumpMCStats( 'changed_filter_type', eventArgs ); + } ); + + // enable showing sort controls only if showing search box is enabled + widget.on( 'change', '.jetpack-search-filters-widget__search-box-enabled', function() { + var checkbox = $( this ), + checkboxVal = checkbox.is(':checked'), + filterParent = checkbox.closest( '.jetpack-search-filters-widget' ), + sortControl = filterParent.find( '.jetpack-search-filters-widget__sort-controls-enabled' ); + + filterParent.toggleClass( 'hide-post-types' ); + + if ( checkboxVal ) { + sortControl.removeAttr( 'disabled' ); + trackAndBumpMCStats( 'enabled_search_box', args.tracksEventData ); + } else { + sortControl.prop( 'checked', false ); + sortControl.prop( 'disabled', true ); + trackAndBumpMCStats( 'disabled_search_box', args.tracksEventData ); + } + } ); + + widget.on( 'change', '.jetpack-search-filters-widget__sort-controls-enabled', function() { + if ( $( this ).is( ':checked' ) ) { + trackAndBumpMCStats( 'enabled_sort_controls', args.tracksEventData ); + } else { + trackAndBumpMCStats( 'disabled_sort_controls', args.tracksEventData ); + } + } ); + + widget.on( 'click', '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]', function( e ) { + var t = $( this ); + var siblingsChecked = t.closest( '.jetpack-search-filters-widget' ) + .find( '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]:checked' ); + + if ( 0 === siblingsChecked.length ) { + e.preventDefault(); + e.stopPropagation(); + + trackAndBumpMCStats( 'attempted_no_post_types', args.tracksEventData ); + } + } ); + + widget.on( 'change', '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]', function() { + var t = $( this ); + var eventArgs = { + is_customizer: args.tracksEventData.is_customizer, + post_type: t.val() + }; + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + if ( t.is( ':checked' ) ) { + trackAndBumpMCStats( 'added_post_type', eventArgs ); + } else { + trackAndBumpMCStats( 'removed_post_type', eventArgs ); + } + } ); + + widget.on( 'change', '.jetpack-search-filters-widget__sort-order', function() { + var eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.order = $( this ).val(); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + trackAndBumpMCStats( 'changed_sort_order', eventArgs ); + } ); + + widget.on( 'change', '.jetpack-search-filters-widget__taxonomy-select select', function() { + var eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.taxonomy = $( this ).val(); + + generateFilterTitlePlaceholder( getContainer( this ) ); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + trackAndBumpMCStats( 'changed_taxonomy', eventArgs ); + } ); + + widget.on( 'change', 'select.date-field-select', function() { + var eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.field = $( this ).val(); + + generateFilterTitlePlaceholder( getContainer( this ) ); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + trackAndBumpMCStats( 'changed_date_field', eventArgs ); + } ); + + widget.on( 'change', 'select.date-interval-select', function() { + var eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.interval = $( this ).val(); + + generateFilterTitlePlaceholder( getContainer( this ) ); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + trackAndBumpMCStats( 'changed_date_interval', eventArgs ); + } ); + + widget.on( 'change', 'input.filter-count', function() { + var eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.count = $( this ).val(); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + trackAndBumpMCStats( 'changed_filter_count', eventArgs ); + } ); + + // add filter button + widget.on( 'click', '.jetpack-search-filters-widget__add-filter', function( e ) { + e.preventDefault(); + + var filtersContainer = $( this ) + .closest( '.jetpack-search-filters-widget' ) + .find( '.jetpack-search-filters-widget__filters' ); + + addFilter( filtersContainer, { + type: 'taxonomy', + taxonomy: '', + post_type: '', + field: '', + interval: '', + count: defaultFilterCount, + name_placeholder: '', + name: '' + } ); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + + // Trigger change event to let legacy widget admin know the widget state is "dirty" + filtersContainer + .find( '.jetpack-search-filters-widget__filter' ) + .find( 'input, textarea, select' ) + .change(); + + trackAndBumpMCStats( 'added_filter', args.tracksEventData ); + } ); + + widget.on( 'click', '.jetpack-search-filters-widget__controls .delete', function( e ) { + e.preventDefault(); + var filter = $( this ).closest( '.jetpack-search-filters-widget__filter' ), + eventArgs = { + is_customizer: args.tracksEventData.is_customizer + }; + + eventArgs.type = filter.find( '.filter-select' ).val(); + + switch ( eventArgs.type ) { + case 'taxonomy': + eventArgs.taxonomy = filter.find( '.jetpack-search-filters-widget__taxonomy-select select' ).val(); + break; + case 'date_histogram': + eventArgs.dateField = filter.find( '.jetpack-search-filters-widget__date-histogram-select:first select' ).val(); + eventArgs.dateInterval = filter.find( '.jetpack-search-filters-widget__date-histogram-select:nth-child( 2 ) select' ).val(); + break; + } + + eventArgs.filterCount = filter.find( '.filter-count' ).val(); + + trackAndBumpMCStats( 'deleted_filter', eventArgs ); + + filter.find( 'input, textarea, select' ).change(); + filter.remove(); + + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + } ); + + // make the filters sortable + $( '.jetpack-search-filters-widget__filters' ).sortable( { + placeholder: 'jetpack-search-filters-widget__filter-placeholder', + axis: 'y', + revert: true, + cancel: 'input,textarea,button,select,option,.jetpack-search-filters-widget__controls a', + change: function() { + if ( wp && wp.customize ) { + wp.customize.state( 'saved' ).set( false ); + } + }, + update: function( e, ui ) { + $( ui.item ).find( 'input, textarea, select' ).change(); + } + } ) + .disableSelection(); + }; + + // When widgets are updated, remove and re-add listeners + $( document ).on( 'widget-updated widget-added', function( e, widget ) { + widget = $( widget ); + + var id = widget.attr( 'id' ), + isJetpackSearch = ( id && ( -1 !== id.indexOf( 'jetpack-search-filters' ) ) ); + + if ( ! isJetpackSearch ) { + return; + } + + // Intentionally not tracking widget additions and updates here as these events + // seem noisy in the customizer. We'll track those via PHP. + + widget.off( 'change', '.filter-select' ); + widget.off( 'click', '.jetpack-search-filters-widget__controls .delete' ); + widget.off( 'change', '.jetpack-search-filters-widget__use-filters' ); + widget.off( 'change', '.jetpack-search-filters-widget__search-box-enabled' ); + widget.off( 'change', '.jetpack-search-filters-widget__sort-controls-enabled' ); + widget.off( 'change', '.jetpack-search-filters-widget__sort-controls-enabled' ); + widget.off( 'change', '.jetpack-search-filters-widget__post-type-selector' ); + widget.off( 'change', '.jetpack-search-filters-widget__sort-order' ); + widget.off( 'change', '.jetpack-search-filters-widget__taxonomy-select' ); + widget.off( 'change', '.jetpack-search-filters-widget__date-histogram-select:first select' ); + widget.off( 'change', '.jetpack-search-filters-widget__date-histogram-select:eq(1) select' ); + widget.off( 'click', '.jetpack-search-filters-widget__post-types-select input[type="checkbox"]' ); + widget.off( 'click', '.jetpack-search-filters-widget__add-filter'); + + setListeners( widget ); + } ); + + /** + * This function will fire both a Tracks and MC stat. + * + * Tracks: Will be prefixed by 'jetpack_widget_search_' and use underscores. + * MC: Will not be prefixed, and will use dashes. + * + * Logic borrowed from `idc-notice.js`. + * + * @param eventName string + * @param extraProps object + */ + function trackAndBumpMCStats( eventName, extraProps ) { + if ( 'undefined' === typeof extraProps || 'object' !== typeof extraProps ) { + extraProps = {}; + } + + if ( eventName && eventName.length && 'undefined' !== typeof analytics && analytics.tracks && analytics.mc ) { + // Format for Tracks + eventName = eventName.replace( /-/g, '_' ); + eventName = eventName.indexOf( 'jetpack_widget_search_' ) !== 0 ? 'jetpack_widget_search_' + eventName : eventName; + analytics.tracks.recordEvent( eventName, extraProps ); + + // Now format for MC stats + eventName = eventName.replace( 'jetpack_widget_search_', '' ); + eventName = eventName.replace( /_/g, '-' ); + analytics.mc.bumpStat( 'jetpack-search-widget', eventName ); + } + } +} )( jQuery, jetpack_search_filter_admin ); diff --git a/plugins/jetpack/modules/widgets/search/js/search-widget.js b/plugins/jetpack/modules/widgets/search/js/search-widget.js new file mode 100644 index 00000000..8d1c4775 --- /dev/null +++ b/plugins/jetpack/modules/widgets/search/js/search-widget.js @@ -0,0 +1,15 @@ +jQuery( document ).ready( function() { + var filter_list = jQuery( '.jetpack-search-filters-widget__filter-list' ); + + filter_list.on( 'click', 'a', function() { + var checkbox = jQuery( this ).siblings( 'input[type="checkbox"]' ); + checkbox.prop( 'checked', ! checkbox.prop( 'checked' ) ); + } ); + + filter_list.find( 'input[type="checkbox"]' ).prop( 'disabled', false ).css( 'cursor', 'inherit' ).on( 'click', function() { + var anchor = jQuery( this ).siblings( 'a' ); + if ( anchor.length ) { + window.location.href = anchor.prop( 'href' ); + } + } ); +} ); diff --git a/plugins/jetpack/modules/widgets/top-posts.php b/plugins/jetpack/modules/widgets/top-posts.php index 0178d111..a4b6b157 100644 --- a/plugins/jetpack/modules/widgets/top-posts.php +++ b/plugins/jetpack/modules/widgets/top-posts.php @@ -336,6 +336,8 @@ class Jetpack_Top_Posts_Widget extends WP_Widget { $post['post_id'], array( 'fallback_to_avatars' => true, + 'width' => (int) $width, + 'height' => (int) $height, 'avatar_size' => (int) $get_image_options['avatar_size'], ) ); diff --git a/plugins/jetpack/modules/widgets/twitter-timeline.php b/plugins/jetpack/modules/widgets/twitter-timeline.php index 9b4283b0..72e9242f 100644 --- a/plugins/jetpack/modules/widgets/twitter-timeline.php +++ b/plugins/jetpack/modules/widgets/twitter-timeline.php @@ -63,7 +63,13 @@ class Jetpack_Twitter_Timeline_Widget extends WP_Widget { public function admin_scripts( $hook ) { // This is still 'widgets.php' when managing widgets via the Customizer. if ( 'widgets.php' === $hook ) { - wp_enqueue_script( 'twitter-timeline-admin', plugins_url( 'twitter-timeline-admin.js', __FILE__ ) ); + wp_enqueue_script( + 'twitter-timeline-admin', + Jetpack::get_file_url_for_environment( + '_inc/build/widgets/twitter-timeline-admin.min.js', + 'modules/widgets/twitter-timeline-admin.js' + ) + ); } } diff --git a/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php b/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php new file mode 100644 index 00000000..9745b91c --- /dev/null +++ b/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-universal.php @@ -0,0 +1,328 @@ +<?php +/** + * Jetpack_WooCommerce_Analytics_Universal + * + * @package Jetpack + * @author Automattic + */ + +/** + * Bail if accessed directly + */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Class Jetpack_WooCommerce_Analytics_Universal + * Filters and Actions added to Store pages to perform analytics + */ +class Jetpack_WooCommerce_Analytics_Universal { + /** + * Jetpack_WooCommerce_Analytics_Universal constructor. + */ + public function __construct() { + // loading _wca + add_action( 'wp_head', array( $this, 'wp_head_top' ), 1 ); + + // loading s.js + add_action( 'wp_head', array( $this, 'wp_head_bottom' ), 999999 ); + + // single product page view + add_action( 'woocommerce_after_single_product', array( $this, 'product_detail' ) ); + + // add to cart on single product page + add_action( 'woocommerce_after_add_to_cart_button', array( $this, 'add_to_cart' ) ); + + // add to carts from list views (search, store etc.) + add_action( 'wp_footer', array( $this, 'loop_add_to_cart' ) ); + + + add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) ); + add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) ); + add_action( 'wcct_before_cart_widget', array( $this, 'remove_from_cart' ) ); + add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 ); + + // cart checkout + add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) ); + + // order confirmed + add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 ); + add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 ); + } + + /** + * Make _wca available to queue events + */ + public function wp_head_top() { + if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) { + $prevent_referrer_code = "<script>window._wca_prevent_referrer = true;</script>"; + echo "$prevent_referrer_code\r\n"; + } + $wca_code = "<script>window._wca = window._wca || [];</script>"; + echo "$wca_code\r\n"; + } + + + /** + * Place script to call s.js, Store Analytics + */ + public function wp_head_bottom() { + $filename = 's-' . gmdate( 'YW' ) . '.js'; + $async_code = "<script async src='https://stats.wp.com/" . $filename . "'></script>"; + echo "$async_code\r\n"; + } + + /** + * On a product page, add a click event listener to "Add to Cart" button click + */ + public function add_to_cart() { + + if ( ! is_single() ) { + return; + } + + $blogid = Jetpack::get_option( 'id' ); + global $product; + + wc_enqueue_js( + "jQuery( '" . esc_js( '.single_add_to_cart_button' ) . "' ).click( function() { + _wca.push( { + '_en': 'woocommerceanalytics_add_to_cart', + 'blog_id': " . esc_js( $blogid ) . ", + 'pi': '" . esc_js( $product->get_id() ) . "', + 'pn' : '" . esc_js( $product->get_title() ) . "', + 'pq': jQuery( 'input.qty' ).val() ? jQuery( 'input.qty' ).val() : '1', + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } ); + } );" + ); + } + + /** + * On product lists or other non-product pages, add an event listener to "Add to Cart" button click + */ + public function loop_add_to_cart() { + $blogid = Jetpack::get_option( 'id' ); + $selector = '.add_to_cart_button:not(.product_type_variable, .product_type_grouped)'; + + wc_enqueue_js( + "jQuery( '" . esc_js( $selector ) . "' ).click( function() { + var productID = jQuery( this ).data( 'product_id' ); + var productDetails = { + 'id': productID, + 'quantity': jQuery( this ).data( 'quantity' ), + }; + _wca.push( { + '_en': 'woocommerceanalytics_product_view', + 'blog_id': '" . esc_js( $blogid ) . "', + 'pi': productDetails.id, + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } ); + _wca.push( { + '_en': 'woocommerceanalytics_add_to_cart', + 'blog_id': " . esc_js( $blogid ) . ", + 'pi': productDetails.id, + 'pq': productDetails.quantity, + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } ); + } );" + ); + } + + /** + * On the cart page, add an event listener for removal of product click + */ + public function remove_from_cart() { + + // We listen at div.woocommerce because the cart 'form' contents get forcibly + // updated and subsequent removals from cart would then not have this click + // handler attached. + $blogid = Jetpack::get_option( 'id' ); + wc_enqueue_js( + "jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() { + var productID = jQuery( this ).data( 'product_id' ); + var quantity = jQuery( this ).parent().parent().find( '.qty' ).val() + var productDetails = { + 'id': productID, + 'quantity': quantity ? quantity : '1', + }; + _wca.push( { + '_en': 'woocommerceanalytics_remove_from_cart', + 'blog_id': '" . esc_js( $blogid ) . "', + 'pi': productDetails.id, + 'pq': productDetails.quantity, + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } ); + } );" + ); + } + + /** + * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present + * + * @param string $url url. + * @param string $key key. + * @return mixed. + */ + public function remove_from_cart_attributes( $url, $key ) { + if ( false !== strpos( $url, 'data-product_id' ) ) { + return $url; + } + + $item = WC()->cart->get_cart_item( $key ); + $product = $item['data']; + + $new_attributes = sprintf( + 'href="%s" data-product_id="%s" data-product_sku="%s"', + esc_attr( $url ), + esc_attr( $product->get_id() ), + esc_attr( $product->get_sku() ) + ); + $url = str_replace( 'href=', $new_attributes ); + return $url; + } + + /** + * Gather relevant product information + * + * @param array $product product + * @return array + */ + public function get_item_details( $product ) { + return array( + 'id' => $product->get_id(), + 'name' => $product->get_title(), + 'category' => Jetpack_WooCommerce_Analytics_Utils::get_product_categories_concatenated( $product ), + 'price' => $product->get_price(), + ); + } + + /** + * Track a product page view + */ + public function product_detail() { + + global $product; + $blogid = Jetpack::get_option( 'id' ); + + $item_details = $this->get_item_details( $product ); + + wc_enqueue_js( + "_wca.push( { + '_en': 'woocommerceanalytics_product_view', + 'blog_id': '" . esc_js( $blogid ) . "', + 'pi': '" . esc_js( $item_details['id'] ) . "', + 'pn': '" . esc_js( $item_details['name'] ) . "', + 'pc': '" . esc_js( $item_details['category'] ) . "', + 'pp': '" . esc_js( $item_details['price'] ) . "', + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } );" + ); + } + + /** + * On the Checkout page, trigger an event for each product in the cart + */ + public function checkout_process() { + + $universal_commands = array(); + $cart = WC()->cart->get_cart(); + $blogid = Jetpack::get_option( 'id' ); + + foreach ( $cart as $cart_item_key => $cart_item ) { + /** + * This filter is already documented in woocommerce/templates/cart/cart.php + */ + $product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + + $item_details = $this->get_item_details( $product ); + + $universal_commands[] = "_wca.push( { + '_en': 'woocommerceanalytics_product_checkout', + 'blog_id': '" . esc_js( $blogid ) . "', + 'pi': '" . esc_js( $item_details['id'] ) . "', + 'pn': '" . esc_js( $item_details['name'] ) . "', + 'pc': '" . esc_js( $item_details['category'] ) . "', + 'pp': '" . esc_js( $item_details['price'] ) . "', + 'pq': '" . esc_js( $cart_item['quantity'] ) . "', + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } );"; + } + + wc_enqueue_js( implode( "\r\n", $universal_commands ) ); + } + + /** + * After the checkout process, fire an event for each item in the order + * + * @param string $order_id Order Id. + */ + public function order_process( $order_id ) { + $order = wc_get_order( $order_id ); + $universal_commands = array(); + $blogid = Jetpack::get_option( 'id' ); + + // loop through products in the order and queue a purchase event. + foreach ( $order->get_items() as $order_item_id => $order_item ) { + $product = $order->get_product_from_item( $order_item ); + + $item_details = $this->get_item_details( $product ); + + $universal_commands[] = "_wca.push( { + '_en': 'woocommerceanalytics_product_purchase', + 'blog_id': '" . esc_js( $blogid ) . "', + 'pi': '" . esc_js( $item_details['id'] ) . "', + 'pn': '" . esc_js( $item_details['name'] ) . "', + 'pc': '" . esc_js( $item_details['category'] ) . "', + 'pp': '" . esc_js( $item_details['price'] ) . "', + 'pq': '" . esc_js( $order_item->get_quantity() ) . "', + 'oi': '" . esc_js( $order->get_order_number() ) . "', + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } );"; + } + + wc_enqueue_js( implode( "\r\n", $universal_commands ) ); + } + + /** + * Listen for clicks on the "Update Cart" button to know if an item has been removed by + * updating its quantity to zero + */ + public function remove_from_cart_via_quantity() { + $blogid = Jetpack::get_option( 'id' ); + + wc_enqueue_js( " + jQuery( 'button[name=update_cart]' ).on( 'click', function() { + var cartItems = jQuery( '.cart_item' ); + cartItems.each( function( item ) { + var qty = jQuery( this ).find( 'input.qty' ); + if ( qty && qty.val() === '0' ) { + var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' ); + _wca.push( { + '_en': 'woocommerceanalytics_remove_from_cart', + 'blog_id': '" . esc_js( $blogid ) . "', + 'pi': productID, + 'ui': '" . esc_js( $this->get_user_id() ) . "', + } ); + } + } ); + } ); + " ); + } + + /** + * Get the current user id + * + * @return int + */ + public function get_user_id() { + if ( is_user_logged_in() ) { + $blogid = Jetpack::get_option( 'id' ); + $userid = get_current_user_id(); + return $blogid . ":" . $userid; + } + return 'null'; + } + +} diff --git a/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-utils.php b/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-utils.php new file mode 100644 index 00000000..b27dc043 --- /dev/null +++ b/plugins/jetpack/modules/woocommerce-analytics/classes/wp-woocommerce-analytics-utils.php @@ -0,0 +1,51 @@ +<?php +/** + * Jetpack_WooCommerce_Analytics_Options provides a single interface to module options + * + * @package Jetpack + * @author Automattic + */ + +/** + * Bail if accessed directly + */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Class Jetpack_WooCommerce_Analytics_Utils + * Utility function for WooCommerce Analytics + */ +class Jetpack_WooCommerce_Analytics_Utils { + /** + * Gets product categories or varation attributes as a formatted concatenated string + * + * @param array $product WC_Product. + * @return string + */ + public static function get_product_categories_concatenated( $product ) { + if ( ! class_exists( 'WooCommerce' ) ) { + return ''; + } + + if ( ! $product ) { + return ''; + } + + $variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : ''; + if ( is_array( $variation_data ) && ! empty( $variation_data ) ) { + $line = wc_get_formatted_variation( $variation_data, true ); + } else { + $out = array(); + $categories = get_the_terms( $product->get_id(), 'product_cat' ); + if ( $categories ) { + foreach ( $categories as $category ) { + $out[] = $category->name; + } + } + $line = join( '/', $out ); + } + return $line; + } +} diff --git a/plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php b/plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php new file mode 100644 index 00000000..cc1c5ad0 --- /dev/null +++ b/plugins/jetpack/modules/woocommerce-analytics/wp-woocommerce-analytics.php @@ -0,0 +1,96 @@ +<?php +/** + * Jetpack_WooCommerce_Analytics is ported from the Jetpack_Google_Analytics code. + * + * @package Jetpack + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +require_once plugin_basename( 'classes/wp-woocommerce-analytics-utils.php' ); +require_once plugin_basename( 'classes/wp-woocommerce-analytics-universal.php' ); + +/** + * Class Jetpack_WooCommerce_Analytics + * Instantiate WooCommerce Analytics + */ +class Jetpack_WooCommerce_Analytics { + + /** + * Instance of this class + * + * @var Jetpack_WooCommerce_Analytics - Static property to hold our singleton instance + */ + private static $instance = false; + + /** + * Instance of the Universal functions + * + * @var Static property to hold concrete analytics impl that does the work (universal or legacy) + */ + private static $analytics = false; + + /** + * WooCommerce Analytics is only available to Jetpack connected WooCommerce stores with both plugins set to active + * and WooCommerce version 3.0 or higher + * + * @return bool + */ + public static function shouldTrackStore() { + // Tracking only Site pages + if ( is_admin() ) { + return false; + } + // Don't track site admins + if ( is_user_logged_in() && in_array( 'administrator', wp_get_current_user()->roles ) ) { + return false; + } + // Make sure Jetpack is installed and active + if ( ! Jetpack::is_active() ) { + return false; + } + /** + * Make sure WooCommerce is installed and active + * + * This action is documented in https://docs.woocommerce.com/document/create-a-plugin + */ + if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) { + return false; + } + + // Ensure the WooCommerce class exists and is a valid version + $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' ); + if ( ! $minimum_woocommerce_active ) { + return false; + } + return true; + } + + /** + * This is our constructor, which is private to force the use of get_instance() + * + * @return void + */ + private function __construct() { + $analytics = new Jetpack_WooCommerce_Analytics_Universal(); + } + + /** + * Function to instantiate our class and make it a singleton + */ + public static function get_instance() { + if ( ! Jetpack_WooCommerce_Analytics::shouldTrackStore() ) { + return; + } + if ( ! self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } +} + +global $jetpack_woocommerce_analytics; +$jetpack_woocommerce_analytics = Jetpack_WooCommerce_Analytics::get_instance(); diff --git a/plugins/jetpack/modules/wordads/php/params.php b/plugins/jetpack/modules/wordads/php/params.php index 9ec48911..dbce7145 100644 --- a/plugins/jetpack/modules/wordads/php/params.php +++ b/plugins/jetpack/modules/wordads/php/params.php @@ -13,7 +13,7 @@ class WordAds_Params { 'wordads_approved' => false, 'wordads_active' => false, 'wordads_house' => true, - 'enable_header_ad' => false, + 'enable_header_ad' => true, 'wordads_second_belowpost' => true, 'wordads_display_front_page' => true, 'wordads_display_post' => true, diff --git a/plugins/jetpack/modules/wordads/php/widgets.php b/plugins/jetpack/modules/wordads/php/widgets.php index c7beccac..04fed064 100644 --- a/plugins/jetpack/modules/wordads/php/widgets.php +++ b/plugins/jetpack/modules/wordads/php/widgets.php @@ -8,6 +8,7 @@ class WordAds_Sidebar_Widget extends WP_Widget { private static $allowed_tags = array( 'mrec', 'wideskyscraper' ); + private static $num_widgets = 0; function __construct() { parent::__construct( @@ -31,9 +32,14 @@ class WordAds_Sidebar_Widget extends WP_Widget { $instance['unit'] = 'mrec'; } + self::$num_widgets++; $about = __( 'Advertisements', 'jetpack' ); $width = WordAds::$ad_tag_ids[$instance['unit']]['width']; $height = WordAds::$ad_tag_ids[$instance['unit']]['height']; + $unit_id = 1 == self::$num_widgets ? 3 : self::$num_widgets + 3; // 2nd belowpost is '4' + $section_id = 0 === $wordads->params->blog_id ? + WORDADS_API_TEST_ID : + $wordads->params->blog_id . $unit_id; $snippet = ''; if ( $wordads->option( 'wordads_house', true ) ) { @@ -46,13 +52,7 @@ class WordAds_Sidebar_Widget extends WP_Widget { $snippet = $wordads->get_house_ad( $unit ); } else { - $section_id = 0 === $wordads->params->blog_id ? WORDADS_API_TEST_ID : $wordads->params->blog_id . '3'; - $data_tags = ( $wordads->params->cloudflare ) ? ' data-cfasync="false"' : ''; - $snippet = <<<HTML - <script$data_tags type='text/javascript'> - (function(g){g.__ATA.initAd({sectionId:$section_id, width:$width, height:$height});})(window); - </script> -HTML; + $snippet = $wordads->get_ad_snippet( $section_id, $height, $width ); } echo <<< HTML @@ -108,10 +108,9 @@ HTML; } } -add_action( - 'widgets_init', - create_function( - '', - 'return register_widget( "WordAds_Sidebar_Widget" );' - ) -); +function jetpack_wordads_widgets_init_callback() { + return register_widget( 'WordAds_Sidebar_Widget' ); +} + +add_action( 'widgets_init', 'jetpack_wordads_widgets_init_callback' ); + diff --git a/plugins/jetpack/modules/wordads/wordads.php b/plugins/jetpack/modules/wordads/wordads.php index 1f63d02f..dbad9102 100644 --- a/plugins/jetpack/modules/wordads/wordads.php +++ b/plugins/jetpack/modules/wordads/wordads.php @@ -113,10 +113,38 @@ class WordAds { add_action( 'wp_head', array( $this, 'insert_head_meta' ), 20 ); add_action( 'wp_head', array( $this, 'insert_head_iponweb' ), 30 ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); - add_filter( 'the_content', array( $this, 'insert_ad' ) ); - add_filter( 'the_excerpt', array( $this, 'insert_ad' ) ); - if ( $this->option( 'enable_header_ad' ) ) { + /** + * Filters enabling ads in `the_content` filter + * + * @see https://jetpack.com/support/ads/ + * + * @module wordads + * + * @since 5.8.0 + * + * @param bool True to disable ads in `the_content` + */ + if ( ! apply_filters( 'wordads_content_disable', false ) ) { + add_filter( 'the_content', array( $this, 'insert_ad' ) ); + } + + /** + * Filters enabling ads in `the_excerpt` filter + * + * @see https://jetpack.com/support/ads/ + * + * @module wordads + * + * @since 5.8.0 + * + * @param bool True to disable ads in `the_excerpt` + */ + if ( ! apply_filters( 'wordads_excerpt_disable', false ) ) { + add_filter( 'the_excerpt', array( $this, 'insert_ad' ) ); + } + + if ( $this->option( 'enable_header_ad', true ) ) { switch ( get_stylesheet() ) { case 'twentyseventeen': case 'twentyfifteen': @@ -188,8 +216,7 @@ HTML; <link rel='dns-prefetch' href='//cdn.switchadhub.com' /> <link rel='dns-prefetch' href='//delivery.g.switchadhub.com' /> <link rel='dns-prefetch' href='//delivery.swid.switchadhub.com' /> - <script$data_tags type="text/javascript" src="//s.pubmine.com/head.js"></script> - <script$data_tags type="text/javascript" src="//static.criteo.net/js/ld/publishertag.js"></script> + <script$data_tags async type="text/javascript" src="//s.pubmine.com/head.js"></script> HTML; } @@ -298,32 +325,25 @@ HTML; $width = 300; $height = 250; $second_belowpost = ''; + $snippet = ''; if ( 'top' == $spot ) { // mrec for mobile, leaderboard for desktop $section_id = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID : $this->params->blog_id . '2'; $width = $this->params->mobile_device ? 300 : 728; $height = $this->params->mobile_device ? 250 : 90; $blocker_unit = $this->params->mobile_device ? 'top_mrec' : 'top'; + $snippet = $this->get_ad_snippet( $section_id, $height, $width, $blocker_unit ); } else if ( 'belowpost' == $spot ) { $section_id = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID : $this->params->blog_id . '1'; $width = 300; $height = 250; + + $snippet = $this->get_ad_snippet( $section_id, $height, $width, 'mrec', 'float:left;margin-right:5px;margin-top:0px;' ); if ( $this->option( 'wordads_second_belowpost', true ) ) { $section_id2 = 0 === $this->params->blog_id ? WORDADS_API_TEST_ID2 : $this->params->blog_id . '4'; - $second_belowpost = - "g.__ATA.initAd({collapseEmpty:'after', sectionId:$section_id2, width:$width, height:$height});"; + $snippet .= $this->get_ad_snippet( $section_id2, $height, $width, 'mrec2', 'float:left;margin-top:0px;' ); } } - - $data_tags = ( $this->params->cloudflare ) ? ' data-cfasync="false"' : ''; - $snippet = <<<HTML - <script$data_tags id='s$section_id' type='text/javascript'> - (function(g){if('undefined'!=typeof g.__ATA){ - g.__ATA.initAd({collapseEmpty:'after', sectionId:$section_id, width:$width, height:$height}); - $second_belowpost - }})(window); - </script> -HTML; } else if ( 'house' == $type ) { $leaderboard = 'top' == $spot && ! $this->params->mobile_device; $snippet = $this->get_house_ad( $leaderboard ? 'leaderboard' : 'mrec' ); @@ -332,35 +352,50 @@ HTML; } } - $ad_blocker_ad = 'iponweb' == $type ? $this->get_adblocker_ad( $blocker_unit ) : ''; - $second_belowpost_css = ''; - $double_mrec = ''; - if ( 'belowpost' == $spot && $this->option( 'wordads_second_belowpost', true ) ) { - if ( 'iponweb' == $type ) { - $ad_blocker_ad .= $this->get_adblocker_ad( 'mrec2' ); - } - - $double_mrec = 'wpmrec2x'; - $second_belowpost_css = <<<HTML - <style type="text/css"> - div.wpmrec2x{max-width:610px;} - div.wpmrec2x div.u > div{float:left;margin-right:10px;} - div.wpmrec2x div.u > div:nth-child(3n){margin-right:0px;} - </style> -HTML; - } - $header = 'top' == $spot ? 'wpcnt-header' : ''; $about = __( 'Advertisements', 'jetpack' ); return <<<HTML - $second_belowpost_css - <div class="wpcnt $header $double_mrec"> + <div class="wpcnt $header"> <div class="wpa"> <span class="wpa-about">$about</span> <div class="u $spot"> $snippet </div> - $ad_blocker_ad + </div> + </div> +HTML; + } + + + /** + * Returns the snippet to be inserted into the ad unit + * @param int $section_id + * @param int $height + * @param int $width + * @param string $css + * @return string + * + * @since 5.7 + */ + function get_ad_snippet( $section_id, $height, $width, $adblock_unit = 'mrec', $css = '' ) { + $this->ads[] = array( 'id' => $section_id, 'width' => $width, 'height' => $height ); + $data_tags = $this->params->cloudflare ? ' data-cfasync="false"' : ''; + $adblock_ad = $this->get_adblocker_ad( $adblock_unit ); + + return <<<HTML + <div style="padding-bottom:15px;width:{$width}px;height:{$height}px;$css"> + <div id="atatags-{$section_id}"> + <script$data_tags type="text/javascript"> + __ATA.cmd.push(function() { + __ATA.initSlot('atatags-{$section_id}', { + collapseEmpty: 'before', + sectionId: '{$section_id}', + width: {$width}, + height: {$height} + }); + }); + </script> + $adblock_ad </div> </div> HTML; @@ -373,6 +408,7 @@ HTML; * @since 5.3 */ public function get_adblocker_ad( $unit = 'mrec' ) { + $data_tags = $this->params->cloudflare ? ' data-cfasync="false"' : ''; $criteo_id = mt_rand(); $height = 250; $width = 300; @@ -392,18 +428,9 @@ HTML; } return <<<HTML - <div id="crt-$criteo_id" style="width:{$width}px;height:{$height}px;"></div> - <script type="text/javascript"> - var o = document.getElementById('crt-$criteo_id'); - if ("undefined"!=typeof Criteo) { - var p = o.parentNode; - p.style.setProperty('display', 'inline-block', 'important'); - o.style.setProperty('display', 'block', 'important'); - Criteo.DisplayAcceptableAdIfAdblocked({zoneid:$zone_id,containerid:"crt-$criteo_id",collapseContainerIfNotAdblocked:true,"callifnotadblocked": function () {var o = document.getElementById('crt-$criteo_id'); o.style.setProperty('display','none','important');o.style.setProperty('visbility','hidden','important'); } }); - } else { - o.style.setProperty('display', 'none', 'important'); - o.style.setProperty('visibility', 'hidden', 'important'); - } + <div id="crt-$criteo_id" style="width:{$width}px;height:{$height}px;display:none !important;"></div> + <script$data_tags type="text/javascript"> + (function(){var c=function(){var a=document.getElementById("crt-{$criteo_id}");window.Criteo?(a.parentNode.style.setProperty("display","inline-block","important"),a.style.setProperty("display","block","important"),window.Criteo.DisplayAcceptableAdIfAdblocked({zoneid:{$zone_id},containerid:"crt-{$criteo_id}",collapseContainerIfNotAdblocked:!0,callifnotadblocked:function(){a.style.setProperty("display","none","important");a.style.setProperty("visbility","hidden","important")}})):(a.style.setProperty("display","none","important"),a.style.setProperty("visibility","hidden","important"))};if(window.Criteo)c();else{if(!__ATA.criteo.script){var b=document.createElement("script");b.src="//static.criteo.net/js/ld/publishertag.js";b.onload=function(){for(var a=0;a<__ATA.criteo.cmd.length;a++){var b=__ATA.criteo.cmd[a];"function"===typeof b&&b()}};(document.head||document.getElementsByTagName("head")[0]).appendChild(b);__ATA.criteo.script=b}__ATA.criteo.cmd.push(c)}})(); </script> HTML; } |