diff --git a/.github/workflows/update-phan-stubs.yml b/.github/workflows/update-phan-stubs.yml index 46c2230c4ce2b..944b9e14f9523 100644 --- a/.github/workflows/update-phan-stubs.yml +++ b/.github/workflows/update-phan-stubs.yml @@ -63,7 +63,7 @@ jobs: git checkout -b update/phan-custom-stubs git commit -am 'phan: Update custom stubs' git push origin HEAD - gh pr create --title 'phan: Update custom stubs' --body 'This is an automatic update generated by a GitHub Action. If closed it will be recreated the next time the action runs.' --label '[Pri] Normal' --label '[Type] Janitorial' --label '[Status] Needs Review' + gh pr create --title 'phan: Update custom stubs' --body 'This is an automatic update generated by a GitHub Action. If closed it will be recreated the next time the action runs.' --label '[Pri] Normal' --label '[Type] Janitorial' --label '[Status] Needs Review' --reviewer Automattic/jetpack-garage - name: Update existing branch if: steps.changes.outputs.needed == 'true' && steps.changes.outputs.has-branch == 'true' diff --git a/.phan/stubs/akismet-stubs.php b/.phan/stubs/akismet-stubs.php index 74b8062fe05be..0ce1519bcf851 100644 --- a/.phan/stubs/akismet-stubs.php +++ b/.phan/stubs/akismet-stubs.php @@ -1,6 +1,6 @@ a { + position: relative; + top: -3px; + margin-left: 0; + vertical-align: baseline; + } +} diff --git a/projects/packages/external-media/src/features/admin/external-media-import.js b/projects/packages/external-media/src/features/admin/external-media-import.js index d6feba02f7716..f1abfe45f3722 100644 --- a/projects/packages/external-media/src/features/admin/external-media-import.js +++ b/projects/packages/external-media/src/features/admin/external-media-import.js @@ -1,3 +1,4 @@ +import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; import { sprintf, __ } from '@wordpress/i18n'; import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; @@ -22,6 +23,7 @@ const Notice = ( { message, onDismiss } ) => ( const JetpackExternalMediaImport = () => { const [ selectedSource, setSelectedSource ] = useState( null ); const [ noticeMessage, setNoticeMessage ] = useState( '' ); + const { tracks } = useAnalytics(); const ExternalLibrary = getExternalLibrary( selectedSource ); const selectButtonText = ( selectedImages, isCopying ) => { @@ -77,6 +79,9 @@ const JetpackExternalMediaImport = () => { const slug = event.target.dataset.slug; if ( slug ) { setSelectedSource( slug ); + tracks.recordEvent( 'jetpack_external_media_import_media_page_import_click', { + media_source: slug, + } ); } }; @@ -89,7 +94,7 @@ const JetpackExternalMediaImport = () => { element.removeEventListener( 'click', handleClick ); } }; - }, [] ); + }, [ tracks ] ); return ( <> diff --git a/projects/packages/external-media/src/features/admin/external-media-import.php b/projects/packages/external-media/src/features/admin/external-media-import.php index c5a43769b7f09..972f8b7b9c764 100644 --- a/projects/packages/external-media/src/features/admin/external-media-import.php +++ b/projects/packages/external-media/src/features/admin/external-media-import.php @@ -47,10 +47,39 @@ function add_jetpack_external_media_import_page() { __NAMESPACE__ . '\render_jetpack_external_media_import_page' ); + add_action( 'load-upload.php', __NAMESPACE__ . '\enqueue_jetpack_external_media_import_button' ); add_action( "load-$external_media_import_page_hook", __NAMESPACE__ . '\enqueue_jetpack_external_media_import_page' ); } add_action( 'admin_menu', __NAMESPACE__ . '\add_jetpack_external_media_import_page' ); +/** + * Enqueue the assets of the Jetpack external media import button. + */ +function enqueue_jetpack_external_media_import_button() { + $assets_base_path = 'build/'; + $asset_name = 'jetpack-external-media-import-button'; + + Assets::register_script( + $asset_name, + $assets_base_path . "$asset_name/$asset_name.js", + External_Media::BASE_FILE, + array( + 'in_footer' => true, + 'textdomain' => 'jetpack-external-media', + 'css_path' => $assets_base_path . "$asset_name/$asset_name.css", + ) + ); + + Assets::enqueue_script( $asset_name ); + wp_localize_script( + $asset_name, + 'JETPACK_EXTERNAL_MEDIA_IMPORT_BUTTON', + array( + 'href' => admin_url( 'upload.php?page=jetpack_external_media_import_page&untangling-media=true' ), + ) + ); +} + /** * Enqueue the assets of the Jetpack external media page. */ diff --git a/projects/packages/external-media/src/shared/media-browser/index.js b/projects/packages/external-media/src/shared/media-browser/index.js index e9bd6bd4551aa..7ba3619044401 100644 --- a/projects/packages/external-media/src/shared/media-browser/index.js +++ b/projects/packages/external-media/src/shared/media-browser/index.js @@ -1,9 +1,12 @@ -import { Button, Spinner, Composite } from '@wordpress/components'; +import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; +import { Spinner, Composite } from '@wordpress/components'; import { useCallback, useState, useRef, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import React from 'react'; +import MediaBrowserSelectButton from './media-browser-select-button'; import MediaItem from './media-item'; +import usePageSource from './use-page-source'; import './style.scss'; const MAX_SELECTED = 10; @@ -13,6 +16,7 @@ const MAX_SELECTED = 10; * * @param {object} props - The component props * @param {object[]} props.media - The list of media + * @param {string} props.mediaSource - The source of media * @param {boolean} props.isCopying - Whether the media browser is copying the media * @param {boolean} props.isLoading - Whether the media browser is loading * @param {boolean} props.imageOnly - Whether to skip non-media items @@ -22,12 +26,13 @@ const MAX_SELECTED = 10; * @param {Function} props.setPath - To set the path for the folder item * @param {Function} props.nextPage - To get the next path * @param {Function} props.onCopy - To handle the copy - * @param {string} props.selectButtonText - The text of the selection button + * @param {Function} props.selectButtonText - To get the select button text * @param {boolean} props.shouldProxyImg - Whether to use the proxy for the media URL * @return {React.ReactElement} - JSX element */ function MediaBrowser( { media, + mediaSource, isCopying, isLoading, imageOnly, @@ -42,6 +47,8 @@ function MediaBrowser( { } ) { const [ selected, setSelected ] = useState( [] ); const gridEl = useRef( null ); + const { tracks } = useAnalytics(); + const pageSource = usePageSource(); const select = useCallback( newlySelected => { @@ -65,45 +72,29 @@ function MediaBrowser( { ); const onCopyAndInsert = useCallback( () => { + tracks.recordEvent( 'jetpack_external_media_modal_submit', { + page: pageSource, + media_source: mediaSource, + media_count: selected.length, + multiple: !! multiple, + } ); + onCopy( selected ); - }, [ selected, onCopy ] ); + }, [ tracks, pageSource, mediaSource, selected, multiple, onCopy ] ); const hasMediaItems = media.filter( item => item.type !== 'folder' ).length > 0; - const classes = clsx( { - 'jetpack-external-media-browser__media': true, - 'jetpack-external-media-browser__media__loading': isLoading, - } ); - const wrapper = clsx( { - 'jetpack-external-media-browser': true, - [ className ]: true, - } ); - - // Using _event to avoid eslint errors. Can change to event if it's in use again. - const handleMediaItemClick = ( _event, { item } ) => { - select( item ); - }; - const SelectButton = selectProps => { - const disabled = selected.length === 0 || isCopying; + const getSelectButtonLabel = () => { const defaultLabel = isCopying ? __( 'Inserting…', 'jetpack-external-media' ) : __( 'Select', 'jetpack-external-media', /* dummy arg to avoid bad minification */ 0 ); - const label = selectProps?.labelText - ? selectProps?.labelText( selected.length, isCopying ) - : defaultLabel; - - return ( -
- -
- ); + + return selectButtonText ? selectButtonText( selected.length, isCopying ) : defaultLabel; + }; + + // Using _event to avoid eslint errors. Can change to event if it's in use again. + const handleMediaItemClick = ( _event, { item } ) => { + select( item ); }; // Infinite scroll @@ -126,11 +117,19 @@ function MediaBrowser( { }, [ pageHandle, isLoading, gridEl ] ); // eslint-disable-line react-hooks/exhaustive-deps return ( -
+
} > @@ -161,7 +160,14 @@ function MediaBrowser( {
) } - { hasMediaItems && } + { hasMediaItems && ( + + ) }
); } diff --git a/projects/packages/external-media/src/shared/media-browser/media-browser-select-button.js b/projects/packages/external-media/src/shared/media-browser/media-browser-select-button.js new file mode 100644 index 0000000000000..a612a7daef41a --- /dev/null +++ b/projects/packages/external-media/src/shared/media-browser/media-browser-select-button.js @@ -0,0 +1,24 @@ +import { Button } from '@wordpress/components'; +import React from 'react'; + +/** + * MediaBrowserSelectButton component + * + * @param {object} props - The component props + * @param {string} props.label - The label of the button + * @param {boolean} props.isLoading - Whether the button is loading + * @param {boolean} props.disabled - Whether the button is disabled + * @param {Function} props.onClick - To handle the click + * @return {React.ReactElement} - JSX element + */ +const MediaBrowserSelectButton = ( { label, isLoading, disabled, onClick } ) => { + return ( +
+ +
+ ); +}; + +export default MediaBrowserSelectButton; diff --git a/projects/packages/external-media/src/shared/media-browser/use-page-source.js b/projects/packages/external-media/src/shared/media-browser/use-page-source.js new file mode 100644 index 0000000000000..70cfa13b6e7e6 --- /dev/null +++ b/projects/packages/external-media/src/shared/media-browser/use-page-source.js @@ -0,0 +1,12 @@ +import { useSelect } from '@wordpress/data'; + +const usePageSource = () => { + const isEditor = useSelect( select => !! select( 'core/editor' ), [] ); + + if ( isEditor ) { + return 'editor'; + } + return 'media-library'; +}; + +export default usePageSource; diff --git a/projects/packages/external-media/src/shared/sources/google-photos/google-photos-media.js b/projects/packages/external-media/src/shared/sources/google-photos/google-photos-media.js index e334c03a156e7..5fcb598812966 100644 --- a/projects/packages/external-media/src/shared/sources/google-photos/google-photos-media.js +++ b/projects/packages/external-media/src/shared/sources/google-photos/google-photos-media.js @@ -11,6 +11,7 @@ import { DATE_RANGE_ANY, } from '../../constants'; import MediaBrowser from '../../media-browser'; +import { MediaSource } from '../../media-service/types'; import { getExternalMediaApiUrl } from '../api'; import Breadcrumbs from './breadcrumbs'; import GoogleFilterOption from './filter-option'; @@ -183,6 +184,7 @@ function GooglePhotosMedia( props ) { className="jetpack-external-media-browser__google" key={ listUrl } media={ media } + mediaSource={ MediaSource.GooglePhotos } imageOnly={ imageOnly } isCopying={ isCopying } isLoading={ isLoading } diff --git a/projects/packages/external-media/src/shared/sources/jetpack-app-media/index.js b/projects/packages/external-media/src/shared/sources/jetpack-app-media/index.js index a4a2cd0643639..640b38de3781b 100644 --- a/projects/packages/external-media/src/shared/sources/jetpack-app-media/index.js +++ b/projects/packages/external-media/src/shared/sources/jetpack-app-media/index.js @@ -136,6 +136,7 @@ function JetpackAppMedia( props ) { key={ 'jetpack-app-media' } className="jetpack-external-media-browser__jetpack_app_media_browser" media={ media } + mediaSource={ MediaSource.JetpackAppMedia } isCopying={ isCopying } isLoading={ false } nextPage={ getNextPage } diff --git a/projects/packages/external-media/src/shared/sources/openverse/index.js b/projects/packages/external-media/src/shared/sources/openverse/index.js index 331c648b817e3..86675f7cc59b0 100644 --- a/projects/packages/external-media/src/shared/sources/openverse/index.js +++ b/projects/packages/external-media/src/shared/sources/openverse/index.js @@ -67,6 +67,7 @@ function OpenverseMedia( props ) { getNextPage( searchQuery ) } diff --git a/projects/packages/external-media/src/shared/sources/pexels/index.js b/projects/packages/external-media/src/shared/sources/pexels/index.js index 83b142ed04fcf..23c162874afe5 100644 --- a/projects/packages/external-media/src/shared/sources/pexels/index.js +++ b/projects/packages/external-media/src/shared/sources/pexels/index.js @@ -68,6 +68,7 @@ function PexelsMedia( props ) { getNextPage( searchQuery ) } diff --git a/projects/packages/external-media/webpack.config.js b/projects/packages/external-media/webpack.config.js index abfb18bdd1654..992136af6494e 100644 --- a/projects/packages/external-media/webpack.config.js +++ b/projects/packages/external-media/webpack.config.js @@ -5,6 +5,10 @@ module.exports = [ { entry: { 'jetpack-external-media-editor': './src/features/editor/index.js', + 'jetpack-external-media-import-button': [ + './src/features/admin/external-media-import-button.js', + './src/features/admin/external-media-import-button.scss', + ], 'jetpack-external-media-import-page': './src/features/admin/external-media-import.js', }, mode: jetpackWebpackConfig.mode, diff --git a/projects/packages/forms/changelog/fix-25025-form-seperator b/projects/packages/forms/changelog/fix-25025-form-seperator new file mode 100644 index 0000000000000..6c872c241915f --- /dev/null +++ b/projects/packages/forms/changelog/fix-25025-form-seperator @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Improves the styling options of the separator block when placed inside the form block diff --git a/projects/packages/forms/changelog/fix-date-validation-bug b/projects/packages/forms/changelog/fix-date-validation-bug new file mode 100644 index 0000000000000..b812fbbf2ae5f --- /dev/null +++ b/projects/packages/forms/changelog/fix-date-validation-bug @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Forms: fixes the date format input if multiple date pickers are used with different date formats. diff --git a/projects/packages/forms/changelog/fix-form-submit-miulti-page b/projects/packages/forms/changelog/fix-form-submit-miulti-page new file mode 100644 index 0000000000000..bf6cbe84766ef --- /dev/null +++ b/projects/packages/forms/changelog/fix-form-submit-miulti-page @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Forms: Add support for having multiple forms accross paginated pages diff --git a/projects/packages/forms/changelog/fix-invalid-field-ids b/projects/packages/forms/changelog/fix-invalid-field-ids new file mode 100644 index 0000000000000..ced045f8f6a66 --- /dev/null +++ b/projects/packages/forms/changelog/fix-invalid-field-ids @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Forms: Fix invalid html IDs. diff --git a/projects/packages/forms/src/blocks/contact-form/editor.scss b/projects/packages/forms/src/blocks/contact-form/editor.scss index d506b5bc8dc98..15b4e982be665 100644 --- a/projects/packages/forms/src/blocks/contact-form/editor.scss +++ b/projects/packages/forms/src/blocks/contact-form/editor.scss @@ -93,6 +93,17 @@ &[data-type='jetpack/field-consent'] { align-self: center; } + + &:where( .wp-block-jetpack-contact-form .wp-block-separator ){ + max-width: var( --wp--preset--spacing--80, 100px ); + margin-left: auto; + margin-right: auto; + } + &:where( .wp-block-jetpack-contact-form .wp-block-separator.is-style-wide ), + &:where( .wp-block-jetpack-contact-form .wp-block-separator.is-style-dots ) { + max-width: inherit; + } + } } diff --git a/projects/packages/forms/src/class-wpcom-rest-api-v2-endpoint-forms.php b/projects/packages/forms/src/class-wpcom-rest-api-v2-endpoint-forms.php index ad29686fb12f2..0e9b8295363dd 100644 --- a/projects/packages/forms/src/class-wpcom-rest-api-v2-endpoint-forms.php +++ b/projects/packages/forms/src/class-wpcom-rest-api-v2-endpoint-forms.php @@ -159,13 +159,7 @@ public function get_responses( $request ) { array_diff_key( $filter_args, array( 'post_parent' => '' ) ) ); - $base_fields = array( - 'email_marketing_consent' => '', - 'entry_title' => '', - 'entry_permalink' => '', - 'feedback_id' => '', - ); - + $base_fields = Contact_Form_Plugin::NON_PRINTABLE_FIELDS; $data_defaults = array( '_feedback_author' => '', '_feedback_author_email' => '', diff --git a/projects/packages/forms/src/contact-form/class-admin.php b/projects/packages/forms/src/contact-form/class-admin.php index 0f2ec1379352c..77bb034a45c59 100644 --- a/projects/packages/forms/src/contact-form/class-admin.php +++ b/projects/packages/forms/src/contact-form/class-admin.php @@ -702,12 +702,6 @@ public function grunion_manage_post_column_from( $post ) { * @return void */ public function grunion_manage_post_column_response( $post ) { - $non_printable_keys = array( - 'email_marketing_consent', - 'entry_title', - 'entry_permalink', - 'feedback_id', - ); $post_content = get_post_field( 'post_content', $post->ID ); $content = explode( '', $post_content ); @@ -750,7 +744,12 @@ public function grunion_manage_post_column_response( $post ) { } } - $response_fields = array_diff_key( $response_fields, array_flip( $non_printable_keys ) ); + $url = get_permalink( $post->post_parent ); + if ( isset( $response_fields['entry_page'] ) ) { + $url = add_query_arg( 'page', $response_fields['entry_page'], $url ); + } + + $response_fields = array_diff_key( $response_fields, array_flip( array_keys( Contact_Form_Plugin::NON_PRINTABLE_FIELDS ) ) ); echo ''; echo ''; } diff --git a/projects/packages/forms/src/contact-form/class-contact-form-field.php b/projects/packages/forms/src/contact-form/class-contact-form-field.php index 52e5f6385e6e6..4cd4819420d45 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-field.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-field.php @@ -655,7 +655,7 @@ public function render_textarea_field( $id, $label, $value, $class, $required, $ /** * Return the HTML for the radio field. * - * @param int $id - the ID. + * @param string $id - the ID (starts with 'g' - see constructor). * @param string $label - the label. * @param string $value - the value of the field. * @param string $class - the field class. @@ -670,11 +670,20 @@ public function render_radio_field( $id, $label, $value, $class, $required, $req $field_style = 'style="' . $this->option_styles . '"'; + $used_html_ids = array(); + foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) { $option = Contact_Form_Plugin::strip_tags( $option ); if ( is_string( $option ) && $option !== '' ) { $radio_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ); - $radio_id = "$id-$radio_value"; + $radio_id = $id . '-' . sanitize_html_class( $radio_value ); + + // If exact id was already used in this radio group, append option index. + // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc. + if ( isset( $used_html_ids[ $radio_id ] ) ) { + $radio_id .= '-' . $option_index; + } + $used_html_ids[ $radio_id ] = true; $field .= "

"; $field .= "option_styles . '"'; + $used_html_ids = array(); + foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) { $option = Contact_Form_Plugin::strip_tags( $option ); if ( is_string( $option ) && $option !== '' ) { $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ); - $checkbox_id = "$id-$checkbox_value"; + $checkbox_id = $id . '-' . sanitize_html_class( $checkbox_value ); + + // If exact id was already used in this checkbox group, append option index. + // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc. + if ( isset( $used_html_ids[ $checkbox_id ] ) ) { + $checkbox_id .= '-' . $option_index; + } + $used_html_ids[ $checkbox_id ] = true; $field .= "

"; $field .= " '', + 'email_marketing_consent' => '', + 'entry_permalink' => '', + 'entry_page' => '', + 'feedback_id' => '', + ); + /** * Initializing function. */ @@ -712,8 +726,16 @@ public function process_form_submission() { // Process the content to populate Contact_Form::$last if ( $post ) { + if ( str_contains( $post->post_content, '' ) ) { + $postdata = generate_postdata( $post ); + $page = isset( $_POST['page'] ) ? absint( wp_unslash( $_POST['page'] ) ) : null; // phpcs:Ignore WordPress.Security.NonceVerification.Missing + $paged = isset( $page ) ? $page : 1; + $content = isset( $postdata['pages'][ $paged - 1 ] ) ? $postdata['pages'][ $paged - 1 ] : $post->post_content; + } else { + $content = $post->post_content; + } /** This filter is already documented in core. wp-includes/post-template.php */ - apply_filters( 'the_content', $post->post_content ); + apply_filters( 'the_content', $content ); } } @@ -1148,7 +1170,7 @@ public function get_post_meta_for_csv_export( $post_id, $has_json_data = false ) $content_fields = self::parse_fields_from_content( $post_id ); $all_fields = isset( $content_fields['_feedback_all_fields'] ) ? $content_fields['_feedback_all_fields'] : array(); $md = $has_json_data - ? array_diff_key( $all_fields, array_flip( array( 'entry_title', 'email_marketing_consent', 'entry_permalink', 'feedback_id' ) ) ) + ? array_diff_key( $all_fields, array_flip( array_keys( self::NON_PRINTABLE_FIELDS ) ) ) : (array) get_post_meta( $post_id, '_feedback_extra_fields', true ); $md['-3_response_date'] = get_the_date( 'Y-m-d H:i:s', $post_id ); diff --git a/projects/packages/forms/src/contact-form/class-contact-form.php b/projects/packages/forms/src/contact-form/class-contact-form.php index 96bea7549ca73..764c7d52240de 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form.php +++ b/projects/packages/forms/src/contact-form/class-contact-form.php @@ -82,7 +82,7 @@ class Contact_Form extends Contact_Form_Shortcode { * @param string $content - the content. */ public function __construct( $attributes, $content = null ) { - global $post; + global $post, $page; // Set up the default subject and recipient for this form. $default_to = ''; @@ -123,7 +123,7 @@ public function __construct( $attributes, $content = null ) { if ( ! isset( $attributes['id'] ) ) { $attributes['id'] = ''; } - $attributes['id'] = $attributes['id'] . '-' . ( count( self::$forms ) + 1 ); + $attributes['id'] = $attributes['id'] . '-' . ( count( self::$forms ) + 1 ) . '-' . $page; } $this->hash = sha1( wp_json_encode( $attributes ) ); @@ -249,8 +249,7 @@ public static function style_on() { * @return string HTML for the concat form. */ public static function parse( $attributes, $content ) { - global $post; - + global $post, $page; // $page is used in the contact-form submission redirect if ( Settings::is_syncing() ) { return ''; } @@ -347,6 +346,9 @@ public static function parse( $attributes, $content ) { } else { // Submit form to the post permalink $url = get_permalink(); + if ( $page ) { + $url = add_query_arg( 'page', $page, $url ); + } } // For SSL/TLS page. See RFC 3986 Section 4.2 @@ -364,7 +366,7 @@ public static function parse( $attributes, $content ) { * @param $post $GLOBALS['post'] Post global variable. * @param int $id Contact Form ID. */ - $url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id ); + $url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id, $page ); $has_submit_button_block = str_contains( $content, 'wp-block-jetpack-button' ); $form_classes = 'contact-form commentsblock'; $post_title = $post->post_title ?? ''; @@ -434,6 +436,10 @@ public static function parse( $attributes, $content ) { $r .= "\t\t\n"; $r .= "\t\t\n"; + if ( $page && $page > 1 ) { + $r .= "\t\t\n"; + } + if ( ! $has_submit_button_block ) { $r .= "\t

\n"; } @@ -1323,10 +1329,14 @@ public function process_submission() { $entry_values = array( 'entry_title' => the_title_attribute( 'echo=0' ), - 'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ), + 'entry_permalink' => esc_url( self::get_permalink( get_the_ID() ) ), 'feedback_id' => $feedback_id, ); + if ( isset( $_POST['page'] ) ) { // phpcs:Ignore WordPress.Security.NonceVerification.Missing + $entry_values['entry_page'] = absint( wp_unslash( $_POST['page'] ) ); // phpcs:Ignore WordPress.Security.NonceVerification.Missing + } + $all_values = array_merge( $all_values, $entry_values ); /** This filter is already documented in \Automattic\Jetpack\Forms\ContactForm\Admin */ @@ -1338,7 +1348,7 @@ public function process_submission() { if ( $block_template || $block_template_part || $widget ) { $url = home_url( '/' ); } else { - $url = get_permalink( $post->ID ); + $url = self::get_permalink( $post->ID ); } // translators: the time of the form submission. @@ -1645,6 +1655,21 @@ public function process_submission() { wp_redirect( $redirect ); exit( 0 ); } + /** + * Get the permalink for the post ID that include the page query parameter if it was set. + * + * @param int $post_id The post ID. + * + * return string The permalink for the post ID. + */ + public static function get_permalink( $post_id ) { + $url = get_permalink( $post_id ); + $page = isset( $_POST['page'] ) ? absint( wp_unslash( $_POST['page'] ) ) : null; // phpcs:Ignore WordPress.Security.NonceVerification.Missing + if ( $page ) { + return add_query_arg( 'page', $page, $url ); + } + return $url; + } /** * Wrapper for wp_mail() that enables HTML messages with text alternatives diff --git a/projects/packages/forms/src/contact-form/css/grunion.css b/projects/packages/forms/src/contact-form/css/grunion.css index d189111812244..30f83ecd6565c 100644 --- a/projects/packages/forms/src/contact-form/css/grunion.css +++ b/projects/packages/forms/src/contact-form/css/grunion.css @@ -235,6 +235,16 @@ box-sizing: border-box; } +:where( .wp-block-jetpack-contact-form .wp-block-separator ) { + max-width: var( --wp--preset--spacing--80, 100px ); + margin-top: 0; + margin-bottom: 0; +} +:where( .wp-block-jetpack-contact-form .wp-block-separator.is-style-wide ), +:where( .wp-block-jetpack-contact-form .wp-block-separator.is-style-dots ) { + max-width: inherit; +} + /* Added circa Nov 2022: container class assigned to topmost block div */ .wp-block-jetpack-contact-form-container.alignfull .wp-block-jetpack-contact-form { padding-right: 0; diff --git a/projects/packages/forms/src/contact-form/js/grunion-frontend.js b/projects/packages/forms/src/contact-form/js/grunion-frontend.js index c10e153fc19e6..b615836bb8a18 100644 --- a/projects/packages/forms/src/contact-form/js/grunion-frontend.js +++ b/projects/packages/forms/src/contact-form/js/grunion-frontend.js @@ -1,13 +1,15 @@ jQuery( function ( $ ) { const $input = $( '.contact-form input.jp-contact-form-date' ); - const dateFormat = $input.attr( 'data-format' ) || 'yy-mm-dd'; - - $input.datepicker( { - dateFormat, - constrainInput: false, - showOptions: { direction: 'down' }, - onSelect: function () { - $( this ).focus(); - }, + $input.each( function () { + const el = $( this ); + const dateFormat = el.attr( 'data-format' ) || 'yy-mm-dd'; + el.datepicker( { + dateFormat, + constrainInput: false, + showOptions: { direction: 'down' }, + onSelect: function () { + $( this ).focus(); + }, + } ); } ); } ); diff --git a/projects/packages/jetpack-mu-wpcom/changelog/media-tracks b/projects/packages/jetpack-mu-wpcom/changelog/media-tracks new file mode 100644 index 0000000000000..48205a668b670 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/media-tracks @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Media Library: add track events for upload from URL feature diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload-form/index.jsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload-form/index.jsx index ab965659d96e3..bc6d37c441b79 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload-form/index.jsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload-form/index.jsx @@ -1,10 +1,11 @@ import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import { useState } from 'react'; +import { wpcomTrackEvent } from '../../../common/tracks'; import './style.scss'; -const WpcomMediaUrlUploadForm = ( { ajaxUrl, action, nonce, isEditor } ) => { +const WpcomMediaUrlUploadForm = ( { ajaxUrl, action, nonce, page } ) => { const [ url, setUrl ] = useState( '' ); const [ show, setShow ] = useState( false ); @@ -25,6 +26,10 @@ const WpcomMediaUrlUploadForm = ( { ajaxUrl, action, nonce, isEditor } ) => { } e.preventDefault(); + wpcomTrackEvent( 'wpcom_media_upload_from_url_submit', { + page, + } ); + const formData = new FormData(); formData.append( 'action', action ); formData.append( 'url', url ); @@ -48,7 +53,7 @@ const WpcomMediaUrlUploadForm = ( { ajaxUrl, action, nonce, isEditor } ) => { .collection.add( attachmentToAdd ); }; - if ( isEditor ) { + if ( page === 'editor' ) { const mediaLibraryTab = window.wp.media.frame.state( 'library' ); mediaLibraryTab.trigger( 'open' ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload.php index 82afc12ae90a3..930151c080d2e 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-media/wpcom-media-url-upload.php @@ -23,10 +23,10 @@ function wpcom_media_url_upload() { $data = wp_json_encode( array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'action' => 'wpcom_media_url_upload', - 'nonce' => wp_create_nonce( 'wpcom_media_url_upload' ), - 'isEditor' => $pagenow !== 'upload.php', + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'action' => 'wpcom_media_url_upload', + 'nonce' => wp_create_nonce( 'wpcom_media_url_upload' ), + 'page' => $pagenow === 'upload.php' ? 'media-library' : 'editor', ) ); diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx index 9b1d94839270e..9b12175cf32c1 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/auto-firewall-status.tsx @@ -19,10 +19,11 @@ export const AutoFirewallStatus = () => { const { protect: { wafConfig: wafData }, } = getMyJetpackWindowInitialState(); - const { jetpack_waf_automatic_rules: isAutoFirewallEnabled } = wafData || {}; + const { jetpack_waf_automatic_rules: isAutoFirewallEnabled, waf_enabled: isWafEnabled } = + wafData || {}; if ( isPluginActive && isSiteConnected ) { - if ( isAutoFirewallEnabled ) { + if ( isAutoFirewallEnabled && isWafEnabled ) { return ; } diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts index 45f7f150190f9..662b37aff79ec 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts @@ -50,6 +50,7 @@ export function useProtectTooltipCopy(): TooltipContent { blocked_logins: blockedLoginsCount, brute_force_protection: hasBruteForceProtection, waf_supported: wafSupported, + waf_enabled: isWafEnabled, } = wafData || {}; const pluginsCount = fromScanPlugins.length || Object.keys( plugins ).length; @@ -247,7 +248,7 @@ export function useProtectTooltipCopy(): TooltipContent { ), }, autoFirewallTooltip: - ( hasProtectPaidPlan && ! isAutoFirewallEnabled ) || ! wafSupported + ( hasProtectPaidPlan && ( ! isAutoFirewallEnabled || ! isWafEnabled ) ) || ! wafSupported ? { title: __( 'Auto-Firewall: Inactive', 'jetpack-my-jetpack' ), text: wafSupported diff --git a/projects/packages/my-jetpack/changelog/fix-my-jetpack-protect-card-shield-inconsistency b/projects/packages/my-jetpack/changelog/fix-my-jetpack-protect-card-shield-inconsistency new file mode 100644 index 0000000000000..48b9490da9170 --- /dev/null +++ b/projects/packages/my-jetpack/changelog/fix-my-jetpack-protect-card-shield-inconsistency @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fix bug where firewall was displayed as active if automatic rules were enabled but firewall was off diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index 117857b70ec97..eb8fc175024ce 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -288,6 +288,7 @@ interface Window { jetpack_waf_share_debug_data: boolean; standalone_mode: boolean; waf_supported: boolean; + waf_enabled: boolean; }; }; videopress: { diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index ec2a40c7af88b..b8ee063b144f4 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -237,8 +237,9 @@ public static function enqueue_scripts() { $scan_data = Products\Protect::get_protect_data(); self::update_historically_active_jetpack_modules(); - $waf_config = array(); - $waf_supported = false; + $waf_config = array(); + $waf_supported = false; + $is_waf_enabled = false; $sandboxed_domain = ''; $is_dev_version = false; @@ -248,8 +249,9 @@ public static function enqueue_scripts() { } if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) { - $waf_config = Waf_Runner::get_config(); - $waf_supported = Waf_Runner::is_supported_environment(); + $waf_config = Waf_Runner::get_config(); + $is_waf_enabled = Waf_Runner::is_enabled(); + $waf_supported = Waf_Runner::is_supported_environment(); } wp_localize_script( @@ -313,6 +315,7 @@ public static function enqueue_scripts() { $waf_config, array( 'waf_supported' => $waf_supported, + 'waf_enabled' => $is_waf_enabled, ), array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) ) ), diff --git a/projects/packages/sync/changelog/update-dont-sync-set-object-terms-action-for-blacklisted-taxonomies b/projects/packages/sync/changelog/update-dont-sync-set-object-terms-action-for-blacklisted-taxonomies new file mode 100644 index 0000000000000..2b074cde382a9 --- /dev/null +++ b/projects/packages/sync/changelog/update-dont-sync-set-object-terms-action-for-blacklisted-taxonomies @@ -0,0 +1,4 @@ +Significance: minor +Type: deprecated + +Sync: Full sync for posts not sending term relationships diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index 54c07996855ba..6d20fafe732fa 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -228,7 +228,7 @@ public function init_before_send() { // Full sync. $sync_module = Modules::get_module( 'full-sync' ); if ( $sync_module instanceof Full_Sync_Immediately ) { - add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'add_term_relationships' ) ); + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'build_full_sync_action_array' ) ); } else { add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_posts_with_metadata_and_terms' ) ); } @@ -779,6 +779,25 @@ public function send_published( $post_ID, $post ) { } } + /** + * Build the full sync action object for Posts. + * + * @access public + * + * @param array $args An array with the posts and the previous end. + * + * @return array An array with the posts, postmeta and the previous end. + */ + public function build_full_sync_action_array( $args ) { + list( $filtered_posts, $previous_end ) = $args; + return array( + $filtered_posts['objects'], + $filtered_posts['meta'], + array(), // WPCOM does not process term relationships in full sync posts actions for a while now, let's skip them. + $previous_end, + ); + } + /** * Add term relationships to post objects within a hook before they are serialized and sent to the server. * This is used in Full Sync Immediately @@ -787,8 +806,10 @@ public function send_published( $post_ID, $post ) { * * @param array $args The hook parameters. * @return array $args The expanded hook parameters. + * @deprecated since $$next-version$$ */ public function add_term_relationships( $args ) { + _deprecated_function( __METHOD__, '$$next-version$$' ); list( $filtered_posts, $previous_interval_end ) = $args; return array( diff --git a/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php b/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php index 13a892dea24c2..2bc63511b98fa 100644 --- a/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php +++ b/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php @@ -231,7 +231,7 @@ public function get_objects_by_id( $object_type, $ids ) { * @deprecated since $$next-version$$ */ public function expand_order_objects( $args ) { - _deprecated_function( __METHOD__, 'next-version' ); + _deprecated_function( __METHOD__, '$$next-version$$' ); list( $order_ids, $previous_end ) = $args; return array( 'orders' => $this->get_objects_by_id( 'order', $order_ids ), diff --git a/projects/plugins/debug-helper/.phan/baseline.php b/projects/plugins/debug-helper/.phan/baseline.php index b7c093aa21e3b..724eaf018ac4c 100644 --- a/projects/plugins/debug-helper/.phan/baseline.php +++ b/projects/plugins/debug-helper/.phan/baseline.php @@ -45,6 +45,7 @@ 'modules/class-scan-helper.php' => ['PhanNoopNew', 'PhanSuspiciousValueComparison', 'PhanTypeConversionFromArray', 'PhanTypeMismatchReturnProbablyReal'], 'modules/class-sync-data-settings-tester.php' => ['PhanNoopNew', 'PhanTypePossiblyInvalidDimOffset', 'PhanUndeclaredClass'], 'modules/class-waf-helper.php' => ['PhanNoopNew', 'PhanPluginSimplifyExpressionBool', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredClassConstant', 'PhanUndeclaredClassMethod'], + 'modules/class-wpcom-api-request-faker-module.php' => ['PhanUndeclaredClassMethod'], 'modules/class-wpcom-api-request-tracker-module.php' => ['PhanNoopNew', 'PhanTypeMismatchArgument'], 'modules/class-xmlrpc-blocker.php' => ['PhanNoopNew'], 'modules/class-xmlrpc-logger.php' => ['PhanNoopNew', 'PhanUndeclaredFunction'], diff --git a/projects/plugins/debug-helper/changelog/debug-add-wpcom-api-request-faker b/projects/plugins/debug-helper/changelog/debug-add-wpcom-api-request-faker new file mode 100644 index 0000000000000..6b85711b72655 --- /dev/null +++ b/projects/plugins/debug-helper/changelog/debug-add-wpcom-api-request-faker @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Debug Helper: Added WPcom API request sending functionality to help testing specific requests manually. diff --git a/projects/plugins/debug-helper/modules/class-wpcom-api-request-faker-module.php b/projects/plugins/debug-helper/modules/class-wpcom-api-request-faker-module.php new file mode 100644 index 0000000000000..b50bdf0e6d16f --- /dev/null +++ b/projects/plugins/debug-helper/modules/class-wpcom-api-request-faker-module.php @@ -0,0 +1,185 @@ +Error: This helper requires a jetpack connection to work. Please ensure that you have set one up before using.

'; + return; + } + + // Handle the form submit + if ( ! empty( $_POST ) ) { + if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'wpcom-api-request-faker' ) ) { + echo '

Wrong nonce, aborting.

'; + return; + } + + $is_connected = ( new Connection_Manager() )->is_connected(); + if ( ! $is_connected ) { + echo '

Site is not connected, please establish a jetpack connection first

'; + return; + } + + $api_url = '/' . sanitize_text_field( wp_unslash( $_POST['url'] ?? '' ) ); + $version = sanitize_text_field( wp_unslash( $_POST['version'] ?? '2' ) ); + $method = sanitize_text_field( wp_unslash( $_POST['method'] ?? 'get' ) ); + + $body = null; + if ( 'post' === $_POST['method'] || 'put' === $_POST['method'] ) { + $body = json_decode( sanitize_text_field( wp_unslash( $_POST['body'] ?? '' ) ), true ); + } + + $response = Client::wpcom_json_api_request_as_blog( + $api_url, + $version, + array( 'method' => sanitize_text_field( wp_unslash( $_POST['method'] ?? '2' ) ) ), + $body, + 'wpcom' + ); + + $response_code = wp_remote_retrieve_response_code( $response ); + + // Display error or response + if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { + ?> +

Something went wrong, here is the error (http code )

+ +
+ +

Response for

+ ' . esc_html( var_export( $looks_like_json ? json_decode( $body, true ) : $body, true ) ) . ''; + } + } + + ?> +
+ +

WPcom API Request Faker

+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ / + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + +
+ +
+
+
+
+ 'WPCOM API Request Tracker', 'description' => 'Displays the number of requests to WPCOM API endpoints for the current page request.', ), + 'wpcom-api-request-faker' => array( + 'file' => 'class-wpcom-api-request-faker-module.php', + 'name' => 'WPCOM API Request Faker', + 'description' => 'Send custom requests to the WPcom API, authorized via your Jetpack connection.', + ), 'xmlrpc-logger' => array( 'file' => 'class-xmlrpc-logger.php', 'name' => 'XMLRPC Logger', diff --git a/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx b/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx index 8cb3d077921bd..8a7f201613c1a 100644 --- a/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx @@ -447,7 +447,10 @@ export const SettingsCard = inprops => { return ( post_content ); + // Return early if empty to prevent DOMDocument::loadHTML fatal. + if ( empty( $content ) ) { + return; + } + /* Suppress warnings */ libxml_use_internal_errors( true ); @$post_html->loadHTML( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/assistant-wizard.tsx index d7cc994da7a87..49e6369ede00e 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/assistant-wizard.tsx @@ -22,7 +22,7 @@ export default function AssistantWizard( { close } ) { stepsEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); }; const keywordsInputRef = useRef( null ); - const [ , setResults ] = useState( {} ); + const [ results, setResults ] = useState( {} ); const [ lastStepValue, setLastStepValue ] = useState( '' ); useEffect( () => { @@ -52,9 +52,10 @@ export default function AssistantWizard( { close } ) { await currentStepData?.onStart( { fromSkip: ! lastStepValue, stepValue: lastStepValue, + results, } ); setIsBusy( false ); - }, [ currentStepData, lastStepValue ] ); + }, [ currentStepData, lastStepValue, results ] ); const handleNext = useCallback( () => { debug( 'handleNext, stepsCount', stepsCount ); @@ -97,22 +98,25 @@ export default function AssistantWizard( { close } ) { setIsBusy( true ); const stepValue = await steps[ currentStep ]?.onSubmit?.(); debug( 'stepValue', stepValue ); - const newResults = { - [ steps[ currentStep ].id ]: { - value: steps[ currentStep ].value.trim(), - type: steps[ currentStep ].type, - label: steps[ currentStep ].label, - }, - }; - debug( 'newResults', newResults ); - setResults( prev => ( { ...prev, ...newResults } ) ); + if ( steps[ currentStep ].includeInResults ) { + const newResults = { + [ steps[ currentStep ].id ]: { + value: stepValue?.trim?.(), + type: steps[ currentStep ].type, + label: steps[ currentStep ].label, + }, + }; + debug( 'newResults', newResults ); + setResults( prev => ( { ...prev, ...newResults } ) ); + } debug( 'set last step value', stepValue ); - setLastStepValue( stepValue.trim() ); + setLastStepValue( stepValue?.trim?.() ); if ( steps[ currentStep ]?.type === 'completion' ) { + debug( 'completion step, closing wizard' ); handleDone(); } else { - // always give half a second before moving forward + debug( 'step type', steps[ currentStep ]?.type ); handleNext(); } }, [ currentStep, handleDone, handleNext, steps ] ); @@ -139,6 +143,7 @@ export default function AssistantWizard( { close } ) { const handleBack = () => { if ( currentStep > 1 ) { + setIsBusy( true ); debug( 'moving back to ' + ( currentStep - 1 ) ); setCurrentStep( currentStep - 1 ); setCurrentStepData( steps[ currentStep - 1 ] ); @@ -148,8 +153,19 @@ export default function AssistantWizard( { close } ) { const handleSkip = useCallback( async () => { setIsBusy( true ); await steps[ currentStep ]?.onSkip?.(); + const step = steps[ currentStep ]; + if ( ! results[ step.id ] && step.includeInResults ) { + setResults( prev => ( { + ...prev, + [ step.id ]: { + value: '', + type: step.type, + label: step.label, + }, + } ) ); + } handleNext(); - }, [ currentStep, steps, handleNext ] ); + }, [ currentStep, steps, handleNext, results ] ); const handleRetry = useCallback( async () => { debug( 'handleRetry' ); diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index bd808045c8931..8b1b0d1c86dab 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -17,6 +17,15 @@ width: unset; } + .components-button.is-primary { + background-color: #3858E9; + } + + .components-button.is-secondary { + box-shadow: inset 0 0 0 1px #3858e9, 0 0 0 currentColor; + color: #3858e9; + } + &__header { flex: 0 0 auto; display: flex; @@ -54,6 +63,7 @@ &__content { flex: 1 1 auto; + gap: 8px; display: flex; flex-direction: column; height: 100%; @@ -69,11 +79,24 @@ flex: 1 1 auto; display: flex; flex-direction: column; - gap: 16px; - padding: 8px 8px 8px 0; + gap: 8px; + padding: 0 8px; overflow-y: auto; scroll-behavior: smooth; align-items: flex-start; + + // weirdly placed here, this makes the first option have a top margin + & > .assistant-wizard__message.is-option { + margin-top: 8px; + } + + & > .assistant-wizard__message.is-option ~ .assistant-wizard__message.is-option { + margin-top: 0px; + } + + & > .assistant-wizard__message.is-option:last-of-type { + margin-bottom: 8px; + } } &__message { @@ -85,6 +108,11 @@ align-items: center; max-width: 255px; + &:not( .is-option ) { + margin-top: 8px; + margin-bottom: 8px; + } + .assistant-wizard__message-icon { flex-shrink: 0; align-self: baseline; @@ -113,6 +141,10 @@ display: none; } } + + &.is-option { + align-self: flex-start; + } } &__input-container { @@ -154,10 +186,6 @@ box-shadow: none; outline: 0; } - - .components-button.is-primary { - background-color: #3858E9; - } } // submit and retry buttons @@ -204,15 +232,6 @@ align-items: center; gap: 16px; animation: assistantInputAppear 0.3s ease-out; - - .components-button.is-primary { - background-color: #3858E9; - } - - .components-button.is-secondary { - box-shadow: inset 0 0 0 1px #3858e9, 0 0 0 currentColor; - color: #3858e9; - } } &__completion { diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx index cca9bcebce3e3..4556ec26f338e 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx @@ -11,22 +11,29 @@ export interface Message { export type OptionMessage = Pick< Message, 'id' | 'content' >; +export interface Results { + [ key: string ]: { + value: string; + type: string; + label: string; + }; +} + export interface Step { id: string; title: string; label?: string; messages: Message[]; type: StepType; - onStart?: ( options?: { fromSkip: boolean; stepValue: string } ) => void; + onStart?: ( options?: { fromSkip: boolean; stepValue: string; results: Results } ) => void; onSubmit?: () => Promise< string >; onSkip?: () => void; value?: string; setValue?: | React.Dispatch< React.SetStateAction< string > > | React.Dispatch< React.SetStateAction< Array< string > > >; - setCompleted?: React.Dispatch< React.SetStateAction< boolean > >; - completed?: boolean; autoAdvance?: number; + includeInResults?: boolean; // Input step properties placeholder?: string; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx index 221b3a01b1751..50ad239eeb007 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx @@ -1,16 +1,16 @@ import { createInterpolateElement, useCallback, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useMessages } from './wizard-messages'; -import type { Step } from './types'; +import type { Step, Results } from './types'; export const useCompletionStep = (): Step => { - const [ keywords, setKeywords ] = useState( '' ); - const [ completed, setCompleted ] = useState( false ); + const [ value, setValue ] = useState( '' ); const { messages, setMessages, addMessage } = useMessages(); const startHandler = useCallback( - async ( { fromSkip } ) => { + async ( { fromSkip, results } ) => { const firstMessages = []; + if ( fromSkip ) { firstMessages.push( { content: __( 'Skipped!', 'jetpack' ), @@ -19,32 +19,62 @@ export const useCompletionStep = (): Step => { } ); } setMessages( firstMessages ); - await new Promise( resolve => setTimeout( resolve, 2000 ) ); + + await new Promise( resolve => setTimeout( resolve, 1500 ) ); + + const resultsString = Object.values( results ) + .map( ( result: Results[ string ] ) => `${ result.value ? '✅' : '❌' } ${ result.label }` ) + .join( '
' ); + addMessage( { content: createInterpolateElement( - "Here's your updated checklist:
✅ Title
✅ Meta description

", + `Here's your updated checklist:
${ resultsString }

`, { br:
, } ), id: '1', } ); - addMessage( { - content: createInterpolateElement( - __( - 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', - 'jetpack' + + const incomplete: { total: number; completed: number } = Object.values( results ).reduce( + ( acc: { total: number; completed: number }, result: Results[ string ] ) => { + const total = acc.total + 1; + const completed = acc.completed + ( result.value ? 1 : 0 ); + return { total, completed }; + }, + { total: 0, completed: 0 } + ) as { total: number; completed: number }; + + const incompleteString = + incomplete.completed === incomplete.total + ? '' + : `${ incomplete.completed } out of ${ incomplete.total }`; + + if ( incompleteString ) { + addMessage( { + content: createInterpolateElement( + `You've optimized ${ incompleteString } items! 🎉
Your post is looking great! Come back anytime to complete the rest.`, + { + strong: , + br:
, + } ), - { br:
, strong: } - ), - showIcon: false, - id: '3', - } ); - addMessage( { - content: __( 'Happy blogging! 😊', 'jetpack' ), - showIcon: false, - id: '4', - } ); + id: '2', + } ); + } else { + addMessage( { + content: createInterpolateElement( + __( + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.
Happy blogging! 😊', + 'jetpack' + ), + { br:
, strong: } + ), + showIcon: false, + id: '3', + } ); + } + return 'completion'; }, [ setMessages, addMessage ] @@ -58,9 +88,7 @@ export const useCompletionStep = (): Step => { type: 'completion', onStart: startHandler, submitCtaLabel: __( 'Done!', 'jetpack' ), - completed, - setCompleted, - value: keywords, - setValue: setKeywords, + value, + setValue, }; }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx index ad37940d48b7d..4a37081440edb 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx @@ -9,7 +9,6 @@ export const useMetaDescriptionStep = (): Step => { const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< OptionMessage[] >( [] ); const { messages, setMessages, addMessage, editLastMessage, setSelectedMessage } = useMessages(); const { editPost } = useDispatch( 'core/editor' ); - const [ completed, setCompleted ] = useState( false ); const handleMetaDescriptionSelect = useCallback( ( option: OptionMessage ) => { @@ -22,7 +21,6 @@ export const useMetaDescriptionStep = (): Step => { const handleMetaDescriptionSubmit = useCallback( async () => { await editPost( { meta: { advanced_seo_description: selectedMetaDescription } } ); addMessage( { content: __( 'Meta description updated! ✅', 'jetpack' ) } ); - setCompleted( true ); return selectedMetaDescription; }, [ selectedMetaDescription, addMessage, editPost ] ); @@ -54,7 +52,7 @@ export const useMetaDescriptionStep = (): Step => { 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', }, ] ), - 3000 + 1500 ) ); } @@ -90,7 +88,7 @@ export const useMetaDescriptionStep = (): Step => { 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', }, ] ), - 3000 + 1500 ) ); @@ -101,6 +99,7 @@ export const useMetaDescriptionStep = (): Step => { return { id: 'meta', title: __( 'Add meta description', 'jetpack' ), + label: __( 'Meta description', 'jetpack' ), messages: messages, type: 'options', options: metaDescriptionOptions, @@ -112,7 +111,6 @@ export const useMetaDescriptionStep = (): Step => { onStart: handleMetaDescriptionGenerate, value: selectedMetaDescription, setValue: setSelectedMetaDescription, - completed, - setCompleted, + includeInResults: true, }; }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx index 87f2764b82385..51d6b8171d3c6 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx @@ -9,7 +9,6 @@ export const useTitleStep = (): Step => { const [ titleOptions, setTitleOptions ] = useState< OptionMessage[] >( [] ); const { editPost } = useDispatch( 'core/editor' ); const { messages, setMessages, addMessage, editLastMessage, setSelectedMessage } = useMessages(); - const [ completed, setCompleted ] = useState( false ); const [ prevStepValue, setPrevStepValue ] = useState(); const handleTitleSelect = useCallback( @@ -57,7 +56,7 @@ export const useTitleStep = (): Step => { 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', }, ] ), - 3000 + 1500 ) ); } @@ -107,7 +106,7 @@ export const useTitleStep = (): Step => { 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', }, ] ), - 2000 + 1500 ) ); setTitleOptions( [ ...titleOptions, ...newTitles ] ); @@ -117,13 +116,13 @@ export const useTitleStep = (): Step => { const handleTitleSubmit = useCallback( async () => { await editPost( { title: selectedTitle, meta: { jetpack_seo_html_title: selectedTitle } } ); addMessage( { content: __( 'Title updated! ✅', 'jetpack' ) } ); - setCompleted( true ); return selectedTitle; }, [ selectedTitle, addMessage, editPost ] ); return { id: 'title', title: __( 'Optimise Title', 'jetpack' ), + label: __( 'Title', 'jetpack' ), messages, type: 'options', options: titleOptions, @@ -135,7 +134,6 @@ export const useTitleStep = (): Step => { onStart: handleTitleGenerate, value: selectedTitle, setValue: setSelectedTitle, - completed, - setCompleted, + includeInResults: true, }; }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-welcome-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-welcome-step.tsx index 5163f890b8dc0..6b0d1eb7f24b7 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-welcome-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-welcome-step.tsx @@ -26,6 +26,6 @@ export const useWelcomeStep = (): Step => { id: '2', }, ], - autoAdvance: 2000, + autoAdvance: 1500, }; }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx index cbfbdd0f2e91d..a8f205478a7c4 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx @@ -69,6 +69,7 @@ export const MessageBubble = ( { message, onSelect = ( m: Message ) => m } ) =>
diff --git a/projects/plugins/jetpack/modules/copy-post.php b/projects/plugins/jetpack/modules/copy-post.php index 38afc654a5a24..88f3e9c26f9a9 100644 --- a/projects/plugins/jetpack/modules/copy-post.php +++ b/projects/plugins/jetpack/modules/copy-post.php @@ -344,8 +344,8 @@ public function add_row_action( $actions, $post ) { 'jetpack-copy' => sprintf( '%3$s %4$s', esc_url( $edit_url ), - esc_attr__( 'Copy this post with Jetpack', 'jetpack' ), - esc_html__( 'Copy', 'jetpack' ), + esc_attr__( 'Duplicate this post with Jetpack.', 'jetpack' ), + esc_html__( 'Duplicate', 'jetpack' ), $jetpack_logo->get_jp_emblem() ), ); diff --git a/projects/plugins/jetpack/modules/shortcodes/youtube.php b/projects/plugins/jetpack/modules/shortcodes/youtube.php index bf0cb546ceeb3..d97eb29b42f87 100644 --- a/projects/plugins/jetpack/modules/shortcodes/youtube.php +++ b/projects/plugins/jetpack/modules/shortcodes/youtube.php @@ -623,6 +623,7 @@ function wpcom_youtube_oembed_fetch_url( $provider, $url ) { if ( ! is_admin() + && /** * Allow oEmbeds in Jetpack's Comment form. * @@ -632,7 +633,7 @@ function wpcom_youtube_oembed_fetch_url( $provider, $url ) { * * @param int $allow_oembed Option to automatically embed all plain text URLs. */ - && apply_filters( 'jetpack_comments_allow_oembed', true ) + apply_filters( 'jetpack_comments_allow_oembed', true ) // No need for this on WordPress.com, this is done for multiple shortcodes at a time there. && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) ) { diff --git a/projects/plugins/jetpack/modules/theme-tools/social-menu.php b/projects/plugins/jetpack/modules/theme-tools/social-menu.php index c4feb6bacdec0..c639085071c53 100644 --- a/projects/plugins/jetpack/modules/theme-tools/social-menu.php +++ b/projects/plugins/jetpack/modules/theme-tools/social-menu.php @@ -25,17 +25,17 @@ function jetpack_social_menu_init() { return; } - /* - * Social Menu description. - * - * Rename the social menu description. - * - * @module theme-tools - * - * @since 3.9.0 - * - * @param string $social_menu_description Social Menu description - */ + /** + * Social Menu description. + * + * Rename the social menu description. + * + * @module theme-tools + * + * @since 3.9.0 + * + * @param string $social_menu_description Social Menu description + */ $social_menu_description = apply_filters( 'jetpack_social_menu_description', __( 'Social Menu', 'jetpack' ) ); // Register a new menu location diff --git a/projects/plugins/jetpack/tests/php/sync/test_class.jetpack-sync-full-immediately.php b/projects/plugins/jetpack/tests/php/sync/test_class.jetpack-sync-full-immediately.php index 4a83a9127a939..93adbe246721a 100644 --- a/projects/plugins/jetpack/tests/php/sync/test_class.jetpack-sync-full-immediately.php +++ b/projects/plugins/jetpack/tests/php/sync/test_class.jetpack-sync-full-immediately.php @@ -679,24 +679,6 @@ public function test_full_sync_doesnt_sends_forbiden_private_or_public_post_meta $this->assertEquals( 'foo5', $this->server_replica_storage->get_metadata( 'post', $post_id, 'a_public_meta', true ) ); } - public function test_full_sync_sends_all_post_terms() { - $post_id = self::factory()->post->create(); - wp_set_object_terms( $post_id, 'tag', 'post_tag' ); - - $this->sender->do_sync(); - $terms = get_the_terms( $post_id, 'post_tag' ); - - $this->assertEqualsObject( $terms, $this->server_replica_storage->get_the_terms( $post_id, 'post_tag' ), 'Initial sync doesn\'t work' ); - // reset the storage, check value, and do full sync - storage should be set! - $this->server_replica_storage->reset(); - - $this->assertFalse( $this->server_replica_storage->get_the_terms( $post_id, 'post_tag' ), 'Not empty' ); - $this->full_sync->start(); - $this->sender->do_full_sync(); - - $this->assertEqualsObject( $terms, $this->server_replica_storage->get_the_terms( $post_id, 'post_tag' ), 'Full sync doesn\'t work' ); - } - public function test_full_sync_sends_all_comment_meta() { $post_id = self::factory()->post->create(); $comment_ids = self::factory()->comment->create_post_comments( $post_id ); diff --git a/projects/plugins/vaultpress/changelog/patch-37326 b/projects/plugins/vaultpress/changelog/patch-37326 new file mode 100644 index 0000000000000..e1b629fb1bdd8 --- /dev/null +++ b/projects/plugins/vaultpress/changelog/patch-37326 @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Only adding inline documentation. No code changes. + + diff --git a/projects/plugins/vaultpress/vaultpress.php b/projects/plugins/vaultpress/vaultpress.php index 75b35de25834b..d0928cee2cfa4 100644 --- a/projects/plugins/vaultpress/vaultpress.php +++ b/projects/plugins/vaultpress/vaultpress.php @@ -1819,6 +1819,10 @@ function parse_request( $wp ) { die( 0 ); break; case 'exec': + /* + * Despite appearances, this code is not an arbitrary code execution vulnerability due to the + * $this->validate_api_signature() check above. Static analysis tools will probably flag this. + */ $code = $_POST['code']; if ( !$code ) $this->response( "No Code Found" ); diff --git a/projects/plugins/wpcomsh/changelog/fix-warnings-on-any-theme-using-customizer-colors b/projects/plugins/wpcomsh/changelog/fix-warnings-on-any-theme-using-customizer-colors new file mode 100644 index 0000000000000..da207bfd8be38 --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/fix-warnings-on-any-theme-using-customizer-colors @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fix: Undefined array key warnings on any customizer theme with colors. diff --git a/projects/plugins/wpcomsh/changelog/wpcomsh-update-media-notice b/projects/plugins/wpcomsh/changelog/wpcomsh-update-media-notice new file mode 100644 index 0000000000000..69cc7c5cb8c32 --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/wpcomsh-update-media-notice @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Media Library: don't show storage info on Atomic upload.php's uploader diff --git a/projects/plugins/wpcomsh/custom-colors/colors.php b/projects/plugins/wpcomsh/custom-colors/colors.php index b2a4405482e8d..15f56c8ee244c 100644 --- a/projects/plugins/wpcomsh/custom-colors/colors.php +++ b/projects/plugins/wpcomsh/custom-colors/colors.php @@ -1421,8 +1421,12 @@ public static function override_themecolors() { $colors = $opts['colors']; - $colors['border'] = $colors['fg1']; - $colors['url'] = $colors['link']; + if ( isset( $colors['fg1'] ) ) { + $colors['border'] = $colors['fg1']; + } + if ( isset( $colors['link'] ) ) { + $colors['url'] = $colors['link']; + } if ( isset( $colors['txt'] ) ) { $colors['text'] = $colors['txt']; } diff --git a/projects/plugins/wpcomsh/notices/storage-notices.php b/projects/plugins/wpcomsh/notices/storage-notices.php index 8ceeb3297c3b3..09ee42a365989 100644 --- a/projects/plugins/wpcomsh/notices/storage-notices.php +++ b/projects/plugins/wpcomsh/notices/storage-notices.php @@ -59,9 +59,15 @@ function wpcomsh_storage_notices() { add_action( 'admin_notices', 'wpcomsh_storage_notices' ); /** - * Display disk space usage on /wp-admin/upload.php + * Display disk space usage on the uploader */ function wpcomsh_display_disk_space_usage() { + global $pagenow; + + if ( $pagenow === 'upload.php' ) { + return; + } + $site_info = wpcomsh_get_at_site_info(); if ( empty( $site_info['space_used'] ) || empty( $site_info['space_quota'] ) ) {