diff --git a/.eslintignore b/.eslintignore index 872f242f9..9b5e68df4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,5 @@ assets/js/bulk-export.js -assets/js/cover-image.js +assets/js/cover-media.js assets/js/export-table.js assets/js/json.js assets/js/meta-boxes.js diff --git a/admin/apple-actions/class-api-action.php b/admin/apple-actions/class-api-action.php index fce96fbe3..e73787e8d 100644 --- a/admin/apple-actions/class-api-action.php +++ b/admin/apple-actions/class-api-action.php @@ -100,5 +100,6 @@ protected function delete_post_meta( $post_id ): void { delete_post_meta( $post_id, 'apple_news_api_created_at' ); delete_post_meta( $post_id, 'apple_news_api_modified_at' ); delete_post_meta( $post_id, 'apple_news_api_share_url' ); + delete_post_meta( $post_id, 'apple_news_article_checksum' ); } } diff --git a/admin/apple-actions/index/class-channel.php b/admin/apple-actions/index/class-channel.php index 48934e1c7..c3a67f3e3 100644 --- a/admin/apple-actions/index/class-channel.php +++ b/admin/apple-actions/index/class-channel.php @@ -26,17 +26,21 @@ class Channel extends API_Action { */ public function perform() { $channel = get_transient( 'apple_news_channel' ); + if ( false === $channel ) { + $channel = ''; + if ( $this->is_api_configuration_valid() ) { try { $channel = $this->get_api()->get_channel( $this->get_setting( 'api_channel' ) ); } catch ( Request_Exception $e ) { - $channel = ''; + // Do nothing. + unset( $e ); } } - } - set_transient( 'apple_news_channel', $channel, 300 ); + set_transient( 'apple_news_channel', $channel, 300 ); + } if ( '' === $channel ) { // Unable to get channel information. This likely means the user entered their API credentials incorrectly. diff --git a/admin/apple-actions/index/class-export.php b/admin/apple-actions/index/class-export.php index b12a91754..ab5b88430 100644 --- a/admin/apple-actions/index/class-export.php +++ b/admin/apple-actions/index/class-export.php @@ -13,6 +13,7 @@ require_once dirname( __DIR__, 3 ) . '/includes/apple-exporter/autoload.php'; use Apple_Actions\Action; +use Apple_Exporter\Components\Embed_Web_Video; use Apple_Exporter\Exporter; use Apple_Exporter\Exporter_Content; use Apple_Exporter\Exporter_Content_Settings; @@ -121,34 +122,101 @@ public function fetch_exporter() { $excerpt = has_excerpt( $post ) ? wp_strip_all_tags( $post->post_excerpt ) : ''; // Get the cover configuration. - $post_thumb = null; - $cover_meta_id = get_post_meta( $this->id, 'apple_news_coverimage', true ); - $cover_caption = get_post_meta( $this->id, 'apple_news_coverimage_caption', true ); - if ( ! empty( $cover_meta_id ) ) { - if ( empty( $cover_caption ) ) { - $cover_caption = wp_get_attachment_caption( $cover_meta_id ); + $post_thumb = null; + $fall_back_to_image = false; + + $cover_provider = get_post_meta( $this->id, 'apple_news_cover_media_provider', true ); + if ( ! is_string( $cover_provider ) || ! $cover_provider ) { + $cover_provider = 'image'; + } + + if ( 'embedwebvideo' === $cover_provider ) { + $cover_url = get_post_meta( $this->id, 'apple_news_cover_embedwebvideo_url', true ); + + // Test against accepted providers. + if ( preg_match( Embed_Web_Video::YOUTUBE_MATCH, $cover_url, $cover_matches ) ) { + $post_thumb = [ + 'caption' => '', + 'url' => 'https://www.youtube.com/embed/' . end( $cover_matches ), + ]; + } elseif ( preg_match( Embed_Web_Video::VIMEO_MATCH, $cover_url, $cover_matches ) ) { + $post_thumb = [ + 'caption' => '', + 'url' => 'https://player.vimeo.com/video/' . end( $cover_matches ), + ]; + } elseif ( preg_match( Embed_Web_Video::DAILYMOTION_MATCH, $cover_url, $cover_matches ) ) { + $post_thumb = [ + 'caption' => '', + 'url' => 'https://geo.dailymotion.com/player.html?video=' . end( $cover_matches ), + ]; + } else { + $fall_back_to_image = true; } - $post_thumb = [ - 'caption' => ! empty( $cover_caption ) ? $cover_caption : '', - 'url' => wp_get_attachment_url( $cover_meta_id ), - ]; - } else { - $thumb_id = get_post_thumbnail_id( $this->id ); - $post_thumb_url = wp_get_attachment_url( $thumb_id ); - if ( empty( $cover_caption ) ) { - $cover_caption = wp_get_attachment_caption( $thumb_id ); + } + + if ( 'video_id' === $cover_provider ) { + $video_id = get_post_meta( $this->id, 'apple_news_cover_video_id', true ); + $video_url = (string) wp_get_attachment_url( $video_id ); + + if ( $video_url ) { + $post_thumb = [ + 'caption' => '', + 'url' => $video_url, + ]; + } else { + $fall_back_to_image = true; } - if ( ! empty( $post_thumb_url ) ) { - // If the post thumb URL is root-relative, convert it to fully-qualified. - if ( str_starts_with( $post_thumb_url, '/' ) ) { - $post_thumb_url = home_url( $post_thumb_url ); - } + } - // Compile the post_thumb object using the URL and caption from the featured image. + if ( 'video_url' === $cover_provider ) { + $file_url = get_post_meta( $this->id, 'apple_news_cover_video_url', true ); + $check = wp_check_filetype( $file_url ); + + if ( isset( $check['ext'] ) && 'mp4' === $check['ext'] ) { + $post_thumb = [ + 'caption' => '', + 'url' => $file_url, + ]; + } else { + $fall_back_to_image = true; + } + } + + // Provide fallback behavior so there's still a cover, e.g. if the URL becomes unsupported later. + if ( $fall_back_to_image ) { + $cover_url = ''; + $cover_provider = 'image'; + } + + if ( 'image' === $cover_provider ) { + $cover_meta_id = get_post_meta( $this->id, 'apple_news_coverimage', true ); + $cover_caption = get_post_meta( $this->id, 'apple_news_coverimage_caption', true ); + if ( ! empty( $cover_meta_id ) ) { + if ( empty( $cover_caption ) ) { + $cover_caption = wp_get_attachment_caption( $cover_meta_id ); + } $post_thumb = [ 'caption' => ! empty( $cover_caption ) ? $cover_caption : '', - 'url' => $post_thumb_url, + 'url' => wp_get_attachment_url( $cover_meta_id ), ]; + } else { + $thumb_id = get_post_thumbnail_id( $this->id ); + $post_thumb_url = wp_get_attachment_url( $thumb_id ); + if ( empty( $cover_caption ) ) { + $cover_caption = wp_get_attachment_caption( $thumb_id ); + } + if ( ! empty( $post_thumb_url ) ) { + // If the post thumb URL is root-relative, convert it to fully-qualified. + if ( str_starts_with( $post_thumb_url, '/' ) ) { + $post_thumb_url = home_url( $post_thumb_url ); + } + + // Compile the post_thumb object using the URL and caption from the featured image. + $post_thumb = [ + 'caption' => ! empty( $cover_caption ) ? $cover_caption : '', + 'url' => $post_thumb_url, + ]; + } } } @@ -160,6 +228,11 @@ public function fetch_exporter() { ]; } + // Attach the final provider slug. + if ( is_array( $post_thumb ) ) { + $post_thumb['provider'] = $cover_provider; + } + // Build the byline. $byline = $this->format_byline( $post ); @@ -208,15 +281,39 @@ public function fetch_exporter() { */ $excerpt = apply_filters( 'apple_news_exporter_excerpt', $excerpt, $post->ID ); + $cover_url = ! empty( $post_thumb['url'] ) ? $post_thumb['url'] : null; + + if ( isset( $post_thumb['provider'] ) && 'image' === $post_thumb['provider'] ) { + /** + * Filters the cover image URL of an article before it is sent to Apple News. + * + * The cover image URL is used for the Cover component, if it is active. + * + * @deprecated 2.7.0 Use the `apple_news_exporter_cover_url` filter, which includes all provider types. + * + * @param string|null $url The cover image URL for the post. + * @param int $post_id The ID of the post. + */ + $cover_url = apply_filters_deprecated( + 'apple_news_exporter_post_thumb', + [ $cover_url, $post->ID ], + '2.7.0', + 'apple_news_exporter_cover_url', + ); + } + /** - * Filters the cover image URL of an article before it is sent to Apple News. + * Filters the URL to the cover media of an article before it is sent to Apple News. * - * The cover image URL is used for the Cover component, if it is active. + * The URL may be an image URL, video file URL, or a URL to an embeddable external video, depending on the provider + * of cover media for the article. * - * @param string|null $url The cover image URL for the post. - * @param int $post_id The ID of the post. + * @param string|null $url The cover media URL for the post. + * @param string|null $provider The provider of the cover media. Possible values include 'image', 'video_url', + * 'video_id', and 'embedwebvideo'. + * @param int $post_id The ID of the post. */ - $cover_url = apply_filters( 'apple_news_exporter_post_thumb', ! empty( $post_thumb['url'] ) ? $post_thumb['url'] : null, $post->ID ); + $cover_url = apply_filters( 'apple_news_exporter_cover_url', $cover_url, $post_thumb['provider'] ?? null, $post->ID ); /** * Filters the byline of an article before it is sent to Apple News. @@ -296,8 +393,9 @@ public function fetch_exporter() { $cover_caption = apply_filters( 'apple_news_exporter_cover_caption', $cover_caption, $post->ID ); $post_thumb = [ - 'caption' => $cover_caption, - 'url' => $cover_url, + 'provider' => $cover_provider, + 'caption' => $cover_caption, + 'url' => $cover_url, ]; } else { $post_thumb = null; @@ -606,6 +704,7 @@ private function get_content( $post ) { $content = apply_filters( 'the_content', $content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound // Clean up the HTML a little. + $content = $this->exclude_selectors( $content ); $content = $this->remove_tags( $content ); $content = $this->remove_entities( $content ); @@ -643,6 +742,52 @@ private function remove_entities( $content ) { return str_replace( '&', '&', $content ); } + /** + * Remove excluded selectors from the content. + * + * @param string $content The content to be filtered. + * @return string + */ + private function exclude_selectors( $content ) { + $raw_selectors = $this->get_setting( 'excluded_selectors' ); + + $selectors = explode( ',', $raw_selectors ); + $selectors = array_map( 'trim', $selectors ); + $selectors = array_filter( $selectors ); + + if ( count( $selectors ) === 0 ) { + return $content; + } + + libxml_use_internal_errors( true ); + $dom = new \DOMDocument(); + $dom->loadHTML( '' . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + $xpath = new \DOMXPath( $dom ); + libxml_clear_errors(); + + foreach ( $selectors as $selector ) { + $nodes = []; + + if ( str_starts_with( $selector, '#' ) ) { + $nodes = $xpath->query( '//*[@id="' . substr( $selector, 1 ) . '"]' ); + } + + if ( str_starts_with( $selector, '.' ) ) { + $nodes = $xpath->query( '//*[contains(concat(" ", normalize-space(@class), " "), " ' . substr( $selector, 1 ) . ' ")]' ); + } + + if ( is_iterable( $nodes ) ) { + foreach ( $nodes as $node ) { + $node->parentNode->removeChild( $node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + } + } + + $content = $dom->saveHTML(); + + return $content; + } + /** * Loads settings for the Exporter_Content from the WordPress post metadata. * diff --git a/admin/class-admin-apple-bulk-export-page.php b/admin/class-admin-apple-bulk-export-page.php index 669ebf01a..a670d221e 100644 --- a/admin/class-admin-apple-bulk-export-page.php +++ b/admin/class-admin-apple-bulk-export-page.php @@ -41,8 +41,8 @@ public function __construct( $settings ) { add_action( 'admin_menu', [ $this, 'register_page' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ] ); - add_action( 'wp_ajax_push_post', [ $this, 'ajax_push_post' ] ); - add_filter( 'admin_title', [ $this, 'set_title' ] ); + add_action( 'wp_ajax_apple_news_push_post', [ $this, 'ajax_push_post' ] ); + add_action( 'wp_ajax_apple_news_delete_post', [ $this, 'ajax_delete_post' ] ); } /** @@ -66,45 +66,52 @@ public function register_page() { ); } - /** - * Fix the title since WordPress doesn't set one. - * - * @param string $admin_title The title to be filtered. - * @access public - * @return string The title for the screen. - */ - public function set_title( $admin_title ) { - $screen = get_current_screen(); - if ( 'admin_page_apple_news_bulk_export' === $screen->base ) { - $admin_title = __( 'Bulk Export', 'apple-news' ) . $admin_title; - } - - return $admin_title; - } - /** * Builds the plugin submenu page. * * @access public */ public function build_page() { - $ids = isset( $_GET['ids'] ) ? sanitize_text_field( wp_unslash( $_GET['ids'] ) ) : null; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - if ( ! $ids ) { + $post_ids = isset( $_GET['post_ids'] ) ? sanitize_text_field( wp_unslash( $_GET['post_ids'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! $post_ids ) { wp_safe_redirect( esc_url_raw( menu_page_url( $this->plugin_slug . '_index', false ) ) ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit + if ( ! defined( 'APPLE_NEWS_UNIT_TESTS' ) || ! APPLE_NEWS_UNIT_TESTS ) { exit; } } - // Populate $articles array with a set of valid posts. - $articles = []; - foreach ( explode( '.', $ids ) as $id ) { - $post = get_post( absint( $id ) ); + // Allow only specific actions. + if ( ! in_array( $action, [ 'apple_news_push_post', 'apple_news_delete_post' ], true ) ) { + wp_die( esc_html__( 'Invalid action.', 'apple-news' ), '', [ 'response' => 400 ] ); + } + + // Populate articles array with a set of valid posts. + $apple_posts = []; + foreach ( explode( ',', $post_ids ) as $post_id ) { + $post = get_post( (int) $post_id ); + if ( ! empty( $post ) ) { - $articles[] = $post; + $apple_posts[] = $post; } } + // Override text within the partial depending on the action. + $apple_page_title = match ( $action ) { + 'apple_news_push_post' => __( 'Bulk Export Articles', 'apple-news' ), + 'apple_news_delete_post' => __( 'Bulk Delete Articles', 'apple-news' ), + }; + $apple_page_description = match ( $action ) { + 'apple_news_push_post' => __( 'The following articles will be exported. Articles that are already published will be updated.', 'apple-news' ), + 'apple_news_delete_post' => __( 'The following articles will be deleted.', 'apple-news' ), + }; + $apple_submit_text = match ( $action ) { + 'apple_news_push_post' => __( 'Publish All', 'apple-news' ), + 'apple_news_delete_post' => __( 'Delete All', 'apple-news' ), + }; + require_once __DIR__ . '/partials/page-bulk-export.php'; } @@ -123,13 +130,7 @@ public function ajax_push_post() { // Ensure the post exists and that it's published. $post = get_post( $id ); if ( empty( $post ) ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => __( 'This post no longer exists.', 'apple-news' ), - ] - ); - wp_die(); + wp_send_json_error( __( 'This post no longer exists.', 'apple-news' ) ); } // Check capabilities. @@ -137,27 +138,17 @@ public function ajax_push_post() { /** This filter is documented in admin/class-admin-apple-post-sync.php */ apply_filters( 'apple_news_publish_capability', self::get_capability_for_post_type( 'publish_posts', $post->post_type ) ) ) ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => __( 'You do not have permission to publish to Apple News', 'apple-news' ), - ] - ); - wp_die(); + wp_send_json_error( __( 'You do not have permission to publish to Apple News', 'apple-news' ) ); } if ( 'publish' !== $post->post_status ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => sprintf( - // translators: token is a post ID. - __( 'Article %s is not published and cannot be pushed to Apple News.', 'apple-news' ), - $id - ), - ] + wp_send_json_error( + sprintf( + /* translators: %s: post ID */ + __( 'Article %s is not published and cannot be pushed to Apple News.', 'apple-news' ), + $id + ) ); - wp_die(); } $action = new Apple_Actions\Index\Push( $this->settings, $id ); @@ -168,22 +159,56 @@ public function ajax_push_post() { } if ( $errors ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => $errors, - ] - ); - } else { - echo wp_json_encode( - [ - 'success' => true, - ] - ); + wp_send_json_error( $errors ); + } + + wp_send_json_success(); + } + + /** + * Handles the ajax action to delete a post from Apple News. + * + * @access public + */ + public function ajax_delete_post() { + // Check the nonce. + check_ajax_referer( self::ACTION ); + + // Sanitize input data. + $id = isset( $_GET['id'] ) ? (int) $_GET['id'] : -1; + + $post = get_post( $id ); + + if ( empty( $post ) ) { + wp_send_json_error( __( 'This post no longer exists.', 'apple-news' ) ); + } + + /** This filter is documented in admin/class-admin-apple-post-sync.php */ + $cap = apply_filters( 'apple_news_delete_capability', self::get_capability_for_post_type( 'delete_posts', $post->post_type ) ); + + // Check capabilities. + if ( ! current_user_can( $cap ) ) { + wp_send_json_error( __( 'You do not have permission to delete posts from Apple News', 'apple-news' ) ); + } + + $errors = null; + + // Try to sync only if the post has a remote ID. Ref `Admin_Apple_Post_Sync::do_delete()`. + if ( get_post_meta( $id, 'apple_news_api_id', true ) ) { + $action = new Apple_Actions\Index\Delete( $this->settings, $id ); + + try { + $errors = $action->perform(); + } catch ( Apple_Actions\Action_Exception $e ) { + $errors = $e->getMessage(); + } + } + + if ( $errors ) { + wp_send_json_error( $errors ); } - // This is required to terminate immediately and return a valid response. - wp_die(); + wp_send_json_success(); } /** diff --git a/admin/class-admin-apple-index-page.php b/admin/class-admin-apple-index-page.php index ccef3af76..80a99e9be 100644 --- a/admin/class-admin-apple-index-page.php +++ b/admin/class-admin-apple-index-page.php @@ -110,7 +110,7 @@ public function admin_page() { * * @since 0.4.0 * @access public - * @return mixed The result of the requested action. + * @return mixed|void The result of the requested action. */ public function page_router() { $id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : null; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended @@ -130,22 +130,56 @@ public function page_router() { return $this->export_action( $id ); case self::namespace_action( 'reset' ): return $this->reset_action( $id ); - case self::namespace_action( 'push' ): // phpcs:ignore PSR2.ControlStructures.SwitchDeclaration.TerminatingComment - if ( ! $id ) { + case self::namespace_action( 'push' ): + if ( $id ) { + $this->push_action( $id ); + } else { $url = menu_page_url( $this->plugin_slug . '_bulk_export', false ); - if ( isset( $_GET['article'] ) ) { // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - $ids = is_array( $_GET['article'] ) ? array_map( 'absint', $_GET['article'] ) : absint( $_GET['article'] ); // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - $url .= '&ids=' . implode( '.', $ids ); + + if ( isset( $_GET['article'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $post_ids = is_array( $_GET['article'] ) ? array_map( 'intval', $_GET['article'] ) : (int) $_GET['article']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $url = add_query_arg( + [ + 'action' => 'apple_news_push_post', + 'post_ids' => implode( ',', $post_ids ), + ], + $url, + ); } + wp_safe_redirect( esc_url_raw( $url ) ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit + if ( ! defined( 'APPLE_NEWS_UNIT_TESTS' ) || ! APPLE_NEWS_UNIT_TESTS ) { exit; } - } else { - return $this->push_action( $id ); } + + break; case self::namespace_action( 'delete' ): - return $this->delete_action( $id ); + if ( $id ) { + $this->delete_action( $id ); + } else { + $url = menu_page_url( $this->plugin_slug . '_bulk_export', false ); + + if ( isset( $_GET['article'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $post_ids = is_array( $_GET['article'] ) ? array_map( 'intval', $_GET['article'] ) : (int) $_GET['article']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $url = add_query_arg( + [ + 'action' => 'apple_news_delete_post', + 'post_ids' => implode( ',', $post_ids ), + ], + $url, + ); + } + + wp_safe_redirect( esc_url_raw( $url ) ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit + + if ( ! defined( 'APPLE_NEWS_UNIT_TESTS' ) || ! APPLE_NEWS_UNIT_TESTS ) { + exit; + } + } + + break; } } @@ -401,7 +435,7 @@ private function delete_action( $id ) { $action = new Apple_Actions\Index\Delete( $this->settings, $id ); try { $action->perform(); - $this->notice_success( __( 'Your article has been removed from apple news.', 'apple-news' ) ); + $this->notice_success( __( 'Your article has been removed from Apple News.', 'apple-news' ) ); } catch ( Apple_Actions\Action_Exception $e ) { $this->notice_error( $e->getMessage() ); } diff --git a/admin/class-admin-apple-meta-boxes.php b/admin/class-admin-apple-meta-boxes.php index 69c90c785..05ddc358f 100644 --- a/admin/class-admin-apple-meta-boxes.php +++ b/admin/class-admin-apple-meta-boxes.php @@ -173,17 +173,21 @@ public static function save_post_meta( $post_id ) { // Save straightforward fields. $fields = [ - 'apple_news_coverimage' => 'integer', - 'apple_news_coverimage_caption' => 'textarea', - 'apple_news_is_hidden' => 'string', - 'apple_news_is_paid' => 'string', - 'apple_news_is_preview' => 'string', - 'apple_news_is_sponsored' => 'string', - 'apple_news_pullquote' => 'string', - 'apple_news_pullquote_position' => 'string', - 'apple_news_slug' => 'string', - 'apple_news_suppress_video_url' => 'boolean', - 'apple_news_use_image_component' => 'boolean', + 'apple_news_coverimage' => 'integer', + 'apple_news_coverimage_caption' => 'textarea', + 'apple_news_cover_embedwebvideo_url' => 'string', + 'apple_news_cover_media_provider' => 'string', + 'apple_news_cover_video_id' => 'integer', + 'apple_news_cover_video_url' => 'string', + 'apple_news_is_hidden' => 'string', + 'apple_news_is_paid' => 'string', + 'apple_news_is_preview' => 'string', + 'apple_news_is_sponsored' => 'string', + 'apple_news_pullquote' => 'string', + 'apple_news_pullquote_position' => 'string', + 'apple_news_slug' => 'string', + 'apple_news_suppress_video_url' => 'boolean', + 'apple_news_use_image_component' => 'boolean', ]; foreach ( $fields as $meta_key => $type ) { switch ( $type ) { diff --git a/admin/class-admin-apple-news-list-table.php b/admin/class-admin-apple-news-list-table.php index 50345a283..bcaeec206 100644 --- a/admin/class-admin-apple-news-list-table.php +++ b/admin/class-admin-apple-news-list-table.php @@ -330,8 +330,9 @@ public function get_bulk_actions() { return apply_filters( 'apple_news_bulk_actions', [ - Admin_Apple_Index_Page::namespace_action( 'push' ) => __( 'Publish/Update', 'apple-news' ), - ] + Admin_Apple_Index_Page::namespace_action( 'push' ) => __( 'Publish/Update', 'apple-news' ), + Admin_Apple_Index_Page::namespace_action( 'delete' ) => __( 'Delete', 'apple-news' ), + ], ); } diff --git a/admin/class-admin-apple-news.php b/admin/class-admin-apple-news.php index 24cd3d419..ceb9d5846 100644 --- a/admin/class-admin-apple-news.php +++ b/admin/class-admin-apple-news.php @@ -25,6 +25,7 @@ require_once dirname( __DIR__ ) . '/includes/REST/apple-news-delete.php'; require_once dirname( __DIR__ ) . '/includes/REST/apple-news-get-published-state.php'; require_once dirname( __DIR__ ) . '/includes/REST/apple-news-get-settings.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-is-valid-cover-media.php'; require_once dirname( __DIR__ ) . '/includes/REST/apple-news-modify-post.php'; require_once dirname( __DIR__ ) . '/includes/REST/apple-news-publish.php'; require_once dirname( __DIR__ ) . '/includes/REST/apple-news-sections.php'; @@ -99,44 +100,57 @@ public function __construct() { // Define custom postmeta fields to register. $postmeta = [ - 'apple_news_api_created_at' => [ + 'apple_news_api_created_at' => [ 'default' => '', ], - 'apple_news_api_id' => [ + 'apple_news_api_id' => [ 'default' => '', ], - 'apple_news_api_modified_at' => [ + 'apple_news_api_modified_at' => [ 'default' => '', ], - 'apple_news_api_revision' => [ + 'apple_news_api_revision' => [ 'default' => '', ], - 'apple_news_api_share_url' => [ + 'apple_news_api_share_url' => [ 'default' => '', ], - 'apple_news_coverimage' => [ + 'apple_news_cover_media_provider' => [ + 'default' => 'image', + ], + 'apple_news_coverimage' => [ + 'default' => 0, + 'type' => 'integer', + ], + 'apple_news_coverimage_caption' => [ + 'default' => '', + ], + 'apple_news_cover_video_id' => [ 'default' => 0, 'type' => 'integer', ], - 'apple_news_coverimage_caption' => [ + 'apple_news_cover_video_url' => [ + 'default' => '', + ], + 'apple_news_cover_embedwebvideo_url' => [ 'default' => '', ], - 'apple_news_is_hidden' => [ + 'apple_news_is_hidden' => [ 'default' => '', ], - 'apple_news_is_paid' => [ + 'apple_news_is_paid' => [ 'default' => '', ], - 'apple_news_is_preview' => [ + 'apple_news_is_preview' => [ 'default' => '', ], - 'apple_news_is_sponsored' => [ + 'apple_news_is_sponsored' => [ 'default' => '', ], - 'apple_news_maturity_rating' => [ + 'apple_news_maturity_rating' => [ 'default' => '', ], - 'apple_news_metadata' => [ + 'apple_news_metadata' => [ 'default' => '', 'sanitize_callback' => function ( $value ) { return ! empty( $value ) && is_string( $value ) ? json_decode( $value, true ) : $value; @@ -146,16 +160,16 @@ public function __construct() { ], 'type' => 'string', ], - 'apple_news_pullquote' => [ + 'apple_news_pullquote' => [ 'default' => '', ], - 'apple_news_pullquote_position' => [ + 'apple_news_pullquote_position' => [ 'default' => '', ], - 'apple_news_slug' => [ + 'apple_news_slug' => [ 'default' => '', ], - 'apple_news_sections' => [ + 'apple_news_sections' => [ 'default' => [], 'show_in_rest' => [ 'schema' => [ @@ -167,11 +181,11 @@ public function __construct() { ], 'type' => 'array', ], - 'apple_news_suppress_video_url' => [ + 'apple_news_suppress_video_url' => [ 'default' => false, 'type' => 'boolean', ], - 'apple_news_use_image_component' => [ + 'apple_news_use_image_component' => [ 'default' => false, 'type' => 'boolean', ], diff --git a/admin/class-admin-apple-post-sync.php b/admin/class-admin-apple-post-sync.php index 1ae6b8f30..02db97931 100644 --- a/admin/class-admin-apple-post-sync.php +++ b/admin/class-admin-apple-post-sync.php @@ -149,8 +149,10 @@ public function do_publish( $id, $post ) { // Proceed based on the current settings for auto publish and update. $updated = get_post_meta( $id, 'apple_news_api_id', true ); - if ( ( $updated && 'yes' !== $this->settings->api_autosync_update ) - || ( ! $updated && 'yes' !== $this->settings->api_autosync ) ) { + if ( + ( $updated && 'yes' !== $this->settings->api_autosync_update ) + || ( ! $updated && 'yes' !== $this->settings->api_autosync ) + ) { return; } diff --git a/admin/partials/cover-image.php b/admin/partials/cover-image.php deleted file mode 100644 index 0dd222447..000000000 --- a/admin/partials/cover-image.php +++ /dev/null @@ -1,46 +0,0 @@ -ID, 'apple_news_coverimage', true ); -$apple_cover_image_caption = get_post_meta( $post->ID, 'apple_news_coverimage_caption', true ); - -?> -
-
- -
- - - -
-
- - -
diff --git a/admin/partials/cover-media.php b/admin/partials/cover-media.php new file mode 100644 index 000000000..f69004695 --- /dev/null +++ b/admin/partials/cover-media.php @@ -0,0 +1,142 @@ +ID, 'apple_news_cover_media_provider', true ); +$apple_cover_image_id = get_post_meta( $post->ID, 'apple_news_coverimage', true ); +$apple_cover_image_caption = get_post_meta( $post->ID, 'apple_news_coverimage_caption', true ); +$apple_cover_video_id = get_post_meta( $post->ID, 'apple_news_cover_video_id', true ); + +?> + +

+ + + + + +
+
+ +
+ + + + +
+ + +
+
+ +
+
+ ', esc_url( wp_get_attachment_url( $apple_cover_video_id ) ) ); + $apple_add_video_hidden = 'hidden'; + $apple_remove_video_hidden = ''; + } else { + $apple_add_video_hidden = ''; + $apple_remove_video_hidden = 'hidden'; + } + ?> +
+ + + +
+ +
+ + +

+ +
+ +
+ + +

+ +
diff --git a/admin/partials/metabox-publish.php b/admin/partials/metabox-publish.php index 6cd9a7809..0f5df950e 100644 --- a/admin/partials/metabox-publish.php +++ b/admin/partials/metabox-publish.php @@ -134,8 +134,8 @@

-

- +

+
settings->get( 'api_autosync' ) diff --git a/admin/partials/page-bulk-export.php b/admin/partials/page-bulk-export.php index 790ca252a..dd5747a20 100644 --- a/admin/partials/page-bulk-export.php +++ b/admin/partials/page-bulk-export.php @@ -2,17 +2,20 @@ /** * Publish to Apple News partials: Bulk Export page template * - * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable - * - * @global array $articles - * * @package Apple_News */ +// Expect these variables to be defined in Admin_Apple_Bulk_Export_Page::build_page() but make sure they're set. +$apple_page_title ??= __( 'Bulk Export', 'apple-news' ); +$apple_page_description ??= __( 'The following articles will be affected.', 'apple-news' ); +$apple_posts ??= []; +$apple_submit_text ??= __( 'Go', 'apple-news' ); + ?>
-

-

+

+

+

diff --git a/admin/settings/class-admin-apple-settings-section-advanced.php b/admin/settings/class-admin-apple-settings-section-advanced.php index fda8cd0b9..ae0359ee0 100644 --- a/admin/settings/class-admin-apple-settings-section-advanced.php +++ b/admin/settings/class-admin-apple-settings-section-advanced.php @@ -32,22 +32,27 @@ public function __construct( $page ) { // Add the settings. $this->settings = [ - 'component_alerts' => [ + 'component_alerts' => [ 'label' => __( 'Component Alerts', 'apple-news' ), 'type' => [ 'none', 'warn', 'fail' ], 'description' => __( 'If a post has a component that is unsupported by Apple News, choose "none" to generate no alert, "warn" to provide an admin warning notice, or "fail" to generate a notice and stop publishing.', 'apple-news' ), ], - 'use_remote_images' => [ + 'use_remote_images' => [ 'label' => __( 'Use Remote Images?', 'apple-news' ), 'type' => [ 'yes', 'no' ], 'description' => __( 'Allow the Apple News API to retrieve images remotely rather than bundle them. This setting is recommended if you are having any issues with publishing images. If your images are not publicly accessible, such as on a development site, you cannot use this feature.', 'apple-news' ), ], - 'full_bleed_images' => [ + 'full_bleed_images' => [ 'label' => __( 'Use Full-Bleed Images?', 'apple-news' ), 'type' => [ 'yes', 'no' ], 'description' => __( 'If set to yes, images that are centered or have no alignment will span edge-to-edge rather than being constrained within the body margins.', 'apple-news' ), ], - 'html_support' => [ + 'deduplicate_cover_media' => [ + 'label' => __( 'Deduplicate Cover Media?', 'apple-news' ), + 'type' => [ 'yes', 'no' ], + 'description' => __( 'If set to yes, any image, video, or other content selected as an article\'s Cover Media will not appear again in the article body in Apple News.', 'apple-news' ), + ], + 'html_support' => [ 'label' => __( 'Enable HTML support?', 'apple-news' ), 'type' => [ 'yes', 'no' ], 'description' => sprintf( @@ -57,7 +62,7 @@ public function __construct( $page ) { '' ), ], - 'in_article_position' => [ + 'in_article_position' => [ 'label' => __( 'Position of In Article Module', 'apple-news' ), 'type' => 'number', 'min' => 3, @@ -65,32 +70,68 @@ public function __construct( $page ) { 'step' => 1, 'description' => __( 'If you have configured an In Article module via Customize JSON, the position that the module should be inserted into. Defaults to 3, which is after the third content block in the article body (e.g., the third paragraph).', 'apple-news' ), ], - 'aside_component_class' => [ + 'aside_component_class' => [ 'label' => __( 'Aside Content CSS Class', 'apple-news' ), 'type' => 'text', + 'size' => 100, 'description' => __( 'Enter a CSS class name that will be used to generate the Aside component. Do not prefix with a period.', 'apple-news' ), 'required' => false, ], + 'recipe_component_class' => [ + 'label' => __( 'Recipe Content CSS Class', 'apple-news' ), + 'type' => 'text', + 'size' => 100, + 'description' => __( 'Enter a CSS class name that will be used to generate the Recipe component. Do not prefix with a period.', 'apple-news' ), + 'required' => false, + ], + 'recipe_component_use_schema' => [ + 'label' => __( 'Build Recipe Component From Structured Data', 'apple-news' ), + 'type' => [ 'yes', 'no' ], + 'description' => __( 'Set to yes if you want the recipe JSON-LD to replace the HTML markup inside the specified CSS class. Set to no if you want to render the HTML markup inside the specified CSS class as-is.', 'apple-news' ), + ], + 'excluded_selectors' => [ + 'label' => __( 'Selectors', 'apple-news' ), + 'type' => 'text', + 'size' => 100, + 'description' => sprintf( + /* translators: %s: tag */ + __( 'Enter a comma-separated list of CSS class or ID selectors, like %s. Elements in post content matching these selectors will be removed from the content published to Apple News.', 'apple-news' ), + '.my-class, #my-id', + ), + 'required' => false, + ], ]; // Add the groups. $this->groups = [ - 'alerts' => [ + 'alerts' => [ 'label' => __( 'Alerts', 'apple-news' ), 'settings' => [ 'component_alerts' ], ], - 'images' => [ + 'images' => [ 'label' => __( 'Image Settings', 'apple-news' ), 'settings' => [ 'use_remote_images', 'full_bleed_images' ], ], - 'format' => [ + 'cover' => [ + 'label' => __( 'Cover Media Settings', 'apple-news' ), + 'settings' => [ 'deduplicate_cover_media' ], + ], + 'format' => [ 'label' => __( 'Format Settings', 'apple-news' ), 'settings' => [ 'html_support', 'in_article_position' ], ], - 'aside' => [ + 'aside' => [ 'label' => __( 'Aside Component', 'apple-news' ), 'settings' => [ 'aside_component_class' ], ], + 'recipe' => [ + 'label' => __( 'Recipe Component', 'apple-news' ), + 'settings' => [ 'recipe_component_class', 'recipe_component_use_schema' ], + ], + 'selectors' => [ + 'label' => __( 'Excluded Elements', 'apple-news' ), + 'settings' => [ 'excluded_selectors' ], + ], ]; parent::__construct( $page ); diff --git a/admin/settings/class-admin-apple-settings-section.php b/admin/settings/class-admin-apple-settings-section.php index a96aab801..2954abbd8 100644 --- a/admin/settings/class-admin-apple-settings-section.php +++ b/admin/settings/class-admin-apple-settings-section.php @@ -135,6 +135,7 @@ class Admin_Apple_Settings_Section extends Apple_News { 'b' => [], 'strong' => [], 'i' => [], + 'code' => [], 'em' => [], 'a' => [ 'href' => [], diff --git a/apple-news.php b/apple-news.php index 4d0f01a8c..d0f1f05de 100644 --- a/apple-news.php +++ b/apple-news.php @@ -3,7 +3,7 @@ * Plugin Name: Publish To Apple News * Plugin URI: http://github.com/alleyinteractive/apple-news * Description: Export and sync posts to Apple format. - * Version: 2.6.3 + * Version: 2.7.0 * Author: Alley * Author URI: https://alley.com * Text Domain: apple-news diff --git a/assets/js/admin-settings/index.jsx b/assets/js/admin-settings/index.jsx index ada1ca8f0..dcb463a47 100644 --- a/assets/js/admin-settings/index.jsx +++ b/assets/js/admin-settings/index.jsx @@ -23,6 +23,14 @@ function AdminSettings() { const { apple_news_automation: ruleList } = settings; const { fields } = AppleNewsAutomationConfig; + if (!ruleList) { + return null; + } + + const LINK_SECTIONS = 'links.sections'; + const allFieldsButSections = Object.keys(fields).filter((field) => field !== LINK_SECTIONS); + const sectionsOnly = [LINK_SECTIONS]; + /** * Helper function for pushing to in-memory settings inside useSiteOptions. * @param {array} updatedRules - The new array of rules. @@ -33,12 +41,13 @@ function AdminSettings() { /** * Adds a new empty rule to the end of the list. + * @param {string} field - The field to add a rule for. */ - const addRule = () => { + const addRule = (field = '') => { updateSettings([ ...(ruleList ?? []), { - field: '', + field, taxonomy: '', term_id: 0, value: '', @@ -75,11 +84,103 @@ function AdminSettings() { updateSettings(updatedRules); }; + /** + * Generates a rule component. + * @param {object} item - The rule object. + * @param {number} index - The index of the rule. + * @return {React.JSX.Element} + */ + const generateRule = (item, index) => { + const hideFieldTypes = item.field === LINK_SECTIONS ? allFieldsButSections : sectionsOnly; + const hideColumns = item.field === LINK_SECTIONS ? ['field'] : []; + + return ( + updateSettings(deleteAtIndex(ruleList, index))} + onDragEnd={(e) => { + const targetRow = document + .elementFromPoint(e.clientX, e.clientY) + .closest('.apple-news-automation-row'); + // Checking for the parent element ensures that the row is in the same table. + if (targetRow && targetRow.parentElement === e.currentTarget.parentElement) { + reorderRule( + Number(e.currentTarget.dataset.index), + Number(targetRow.dataset.index), + ); + } + }} + onUpdate={(key, value) => updateRule(index, key, value)} + taxonomy={item.taxonomy} + termId={item.term_id} + value={item.value} + index={index} + hideFieldTypes={hideFieldTypes} + hideColumns={hideColumns} + /> + ); + }; + + // Split the rows into sections and non-sections. + const { sectionAutomationRows, additionalAutomationRows } = ruleList.reduce( + (acc, item, index) => { + const ruleComponent = generateRule(item, index); + + if (item.field === LINK_SECTIONS) { + acc.sectionAutomationRows.push(ruleComponent); + } else { + acc.additionalAutomationRows.push(ruleComponent); + } + + return acc; + }, + { sectionAutomationRows: [], additionalAutomationRows: [] }, + ); + return (

{__('Apple News Automation', 'apple-news')}

{__('Configure automation rules below to automatically apply certain settings based on the taxonomy terms applied to each post.', 'apple-news')}

-

{__('For more information on how automation works, visit our wiki.', 'apple-news')}

+

+ {__('For more information on how automation works, visit our wiki.', 'apple-news')} +

+

{__('Section Mapping Automation', 'apple-news')}

+ + + + + + + + + + + {sectionAutomationRows} + +
{__('Taxonomy', 'apple-news')}{__('Term', 'apple-news')}{__('Section', 'apple-news')}{__('Delete?', 'apple-news')}
+
+
+ + {' '} + +
+
+
+

{__('Additional Automation', 'apple-news')}

@@ -91,32 +192,7 @@ function AdminSettings() { - {!loading && ruleList ? ( - ruleList.map((item, index) => ( - updateSettings(deleteAtIndex(ruleList, index))} - onDragEnd={(e) => { - const targetRow = document - .elementFromPoint(e.clientX, e.clientY) - .closest('.apple-news-automation-row'); - if (targetRow) { - reorderRule( - index, - Array.from(targetRow.parentElement.querySelectorAll('tr')) - .indexOf(targetRow), - ); - } - }} - onUpdate={(key, value) => updateRule(index, key, value)} - taxonomy={item.taxonomy} - termId={item.term_id} - value={item.value} - /> - )) - ) : null} + {additionalAutomationRows}
diff --git a/assets/js/admin-settings/rule.jsx b/assets/js/admin-settings/rule.jsx index 7ff51878f..e06b0a54d 100644 --- a/assets/js/admin-settings/rule.jsx +++ b/assets/js/admin-settings/rule.jsx @@ -21,6 +21,9 @@ function Rule({ taxonomy, termId, value, + index, + hideFieldTypes, + hideColumns, }) { const { fields, @@ -43,132 +46,178 @@ function Rule({ fieldType = 'string'; } + /** + * Get field type options. + */ + const getFieldTypes = () => { + // Filter out field types that should be hidden. + const filteredFieldsObject = Object.keys(fields).reduce((acc, fieldSlug) => { + if (!hideFieldTypes.includes(fieldSlug)) { + acc[fieldSlug] = fields[fieldSlug]; + } + return acc; + }, {}); + + return [ + { value: '', label: __('Select Field', 'apple-news') }, + ...Object.keys(filteredFieldsObject).map((fieldSlug) => ({ + label: fields[fieldSlug].label, + value: fieldSlug, + })), + ]; + }; + return ( - - onUpdate('taxonomy', next)} - options={[ - { value: '', label: __('Select Taxonomy', 'apple-news') }, - ...Object.keys(taxonomies).map((tax) => ({ value: tax, label: tax })), - ]} - value={taxonomy} - /> - - - onUpdate('term_id', next)} - taxonomy={taxonomy} - termId={termId} - /> - - - onUpdate('field', next)} - options={[ - { value: '', label: __('Select Field', 'apple-news') }, - ...Object.keys(fields).map((fieldSlug) => ({ - label: fields[fieldSlug].label, - value: fieldSlug, - })), - ]} - value={field} - /> - - - {fieldType === 'contentGenerationType' ? ( - onUpdate('value', next)} - options={[ - { value: '', label: __('None', 'apple-news') }, - { value: 'AI', label: __('AI', 'apple-news') }, - ]} - value={value} - /> - ) : null} - {fieldType === 'sections' ? ( - onUpdate('value', next)} - options={[ - { value: '', label: __('Select Section', 'apple-news') }, - ...sections.map((sect) => ({ value: sect.id, label: sect.name })), - ]} - value={value} - /> - ) : null} - {fieldType === 'boolean-select' ? ( - onUpdate('value', next)} - options={[ - { value: '', label: __('Channel Default', 'apple-news') }, - { value: 'true', label: __('True', 'apple-news') }, - { value: 'false', label: __('False', 'apple-news') }, - ]} - value={value} - /> - ) : null} - {fieldType === 'boolean' ? ( - onUpdate('value', next.toString())} - /> - ) : null} - {fieldType === 'string' ? ( - onUpdate('value', next)} - value={value} - /> - ) : null} - {fieldType === 'themes' ? ( - onUpdate('value', next)} - options={[ - { value: '', label: __('Select Theme', 'apple-news') }, - ...themes.map((name) => ({ value: name, label: name })), - ]} - value={value} - /> - ) : null} - - - - + { + hideColumns.includes('taxonomy') + ? null + : ( + + onUpdate('taxonomy', next)} + options={[ + { value: '', label: __('Select Taxonomy', 'apple-news') }, + ...Object.keys(taxonomies).map((tax) => ({ value: tax, label: tax })), + ]} + value={taxonomy} + /> + + ) + } + { + hideColumns.includes('term') ? null : ( + + onUpdate('term_id', next)} + taxonomy={taxonomy} + termId={termId} + /> + + ) + } + { + hideColumns.includes('field') ? null : ( + + onUpdate('field', next)} + options={getFieldTypes()} + value={field} + /> + + ) + } + { + hideColumns.includes('value') ? null : ( + + {fieldType === 'contentGenerationType' ? ( + onUpdate('value', next)} + options={[ + { value: '', label: __('None', 'apple-news') }, + { value: 'AI', label: __('AI', 'apple-news') }, + ]} + value={value} + /> + ) : null} + {fieldType === 'sections' ? ( + onUpdate('value', next)} + options={[ + { value: '', label: __('Select Section', 'apple-news') }, + ...sections.map((sect) => ({ value: sect.id, label: sect.name })), + ]} + value={value} + /> + ) : null} + {fieldType === 'boolean-select' ? ( + onUpdate('value', next)} + options={[ + { value: '', label: __('Channel Default', 'apple-news') }, + { value: 'true', label: __('True', 'apple-news') }, + { value: 'false', label: __('False', 'apple-news') }, + ]} + value={value} + /> + ) : null} + {fieldType === 'boolean' ? ( + onUpdate('value', next.toString())} + /> + ) : null} + {fieldType === 'string' ? ( + onUpdate('value', next)} + value={value} + /> + ) : null} + {fieldType === 'themes' ? ( + onUpdate('value', next)} + options={[ + { value: '', label: __('Select Theme', 'apple-news') }, + ...themes.map((name) => ({ value: name, label: name })), + ]} + value={value} + /> + ) : null} + + ) + } + { + hideColumns.includes('delete') ? null : ( + + + + ) + } ); } +Rule.defaultProps = { + hideColumns: [], + hideFieldTypes: [], +}; + Rule.propTypes = { busy: PropTypes.bool.isRequired, field: PropTypes.string.isRequired, + hideColumns: PropTypes.arrayOf(PropTypes.string), + hideFieldTypes: PropTypes.arrayOf(PropTypes.string), + index: PropTypes.number.isRequired, onDelete: PropTypes.func.isRequired, onDragEnd: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired, diff --git a/assets/js/bulk-export.js b/assets/js/bulk-export.js index 85752ffc3..c9d71e169 100644 --- a/assets/js/bulk-export.js +++ b/assets/js/bulk-export.js @@ -1,10 +1,13 @@ (function ( $, window, undefined ) { 'use strict'; - var started = false; + var started = false, + searchParams = new URLSearchParams( window.location.search ), + $submitButton = $( '.bulk-export-submit' ); function done() { - $( '.bulk-export-submit' ).text( 'Done' ); + $submitButton.text( 'Done' ); + $submitButton.attr( 'disabled', 'disabled' ); } function pushItem( item, next, nonce ) { @@ -12,14 +15,14 @@ var $status = $item.find( '.bulk-export-list-item-status' ); var id = +$item.data( 'post-id' ); // fetch the post-id and cast to integer - $status.removeClass( 'pending' ).addClass( 'in-progress' ).text( 'Publishing...' ); + $status.removeClass( 'pending' ).addClass( 'in-progress' ).text( 'In Progress…' ); // Send a GET request to ajaxurl, which is WordPress endpoint for AJAX // requests. Expects JSON as response. $.getJSON( ajaxurl, { - action: 'push_post', + action: searchParams.get( 'action' ), id: id, _ajax_nonce: nonce }, @@ -27,7 +30,7 @@ if ( res.success ) { $status.removeClass( 'in-progress' ).addClass( 'success' ).text( 'Success' ); } else { - $status.removeClass( 'in-progress' ).addClass( 'failed' ).text( res.error ); + $status.removeClass( 'in-progress' ).addClass( 'failed' ).text( res.data ); } next(); }, @@ -56,7 +59,7 @@ next(); } - $('.bulk-export-submit').click(function (e) { + $submitButton.click( function ( e ) { e.preventDefault(); if ( started ) { @@ -65,6 +68,6 @@ started = true; bulkPush(); - }); + } ); })( jQuery, window ); diff --git a/assets/js/cover-image.js b/assets/js/cover-image.js deleted file mode 100644 index 5eb14d23f..000000000 --- a/assets/js/cover-image.js +++ /dev/null @@ -1,60 +0,0 @@ -(function ( $, window, undefined ) { - 'use strict'; - - // Set up add and remove image functionality. - $( '.apple-news-coverimage-image-container' ).each( function () { - var $this = $( this ), - $addImgButton = $this.find( '.apple-news-coverimage-add' ), - $delImgButton = $this.find( '.apple-news-coverimage-remove' ), - $imgContainer = $this.find( '.apple-news-coverimage-image' ), - $imgIdInput = $this.find( '.apple-news-coverimage-id' ), - frame; - - // Set up handler for remove image functionality. - $delImgButton.on( 'click', function() { - $imgContainer.empty(); - $addImgButton.removeClass( 'hidden' ); - $delImgButton.addClass( 'hidden' ); - $imgIdInput.val( '' ); - } ); - - // Set up handler for add image functionality. - $addImgButton.on( 'click', function () { - - // Open frame, if it already exists. - if ( frame ) { - frame.open(); - return; - } - - // Set configuration for media frame. - frame = wp.media( { multiple: false } ); - - // Set up handler for image selection. - frame.on( 'select', function () { - - // Get information about the attachment. - var attachment = frame.state().get( 'selection' ).first().toJSON(), - imgUrl = attachment.url; - - // Set image URL to medium size, if available. - if ( attachment.sizes.medium && attachment.sizes.medium.url ) { - imgUrl = attachment.sizes.medium.url; - } - - // Clear current values. - $imgContainer.empty(); - $imgIdInput.val( '' ); - - // Add the image and ID, swap visibility of add and remove buttons. - $imgContainer.append( '' ); // phpcs:ignore WordPressVIPMinimum.JS.StringConcat.Found, WordPressVIPMinimum.JS.HTMLExecutingFunctions.append - $imgIdInput.val( attachment.id ); - $addImgButton.addClass( 'hidden' ); - $delImgButton.removeClass( 'hidden' ); - } ); - - // Open the media frame. - frame.open(); - } ); - } ); -})( jQuery, window ); diff --git a/assets/js/cover-media.js b/assets/js/cover-media.js new file mode 100644 index 000000000..50ad4be65 --- /dev/null +++ b/assets/js/cover-media.js @@ -0,0 +1,154 @@ +(function ( $, window, undefined ) { + 'use strict'; + + // Set up show/hide functionality for cover media provider containers. + (function () { + var $provider = $( '[name="apple_news_cover_media_provider"]' ), + $containers = $( '.apple-news-cover-media-provider-container' ), + showSelectedContainer = function () { + $containers.hide(); + $containers.filter( '[data-provider="' + $provider.filter( ':checked' ).val() + '"]' ).show(); + }; + + showSelectedContainer(); + $provider.on( 'change', showSelectedContainer ); + })(); + + // Set up add and remove image functionality. + $( '.apple-news-coverimage-image-container' ).each( function () { + var $this = $( this ), + $addImgButton = $this.find( '.apple-news-coverimage-add' ), + $delImgButton = $this.find( '.apple-news-coverimage-remove' ), + $imgContainer = $this.find( '.apple-news-coverimage-image' ), + $imgIdInput = $this.find( '.apple-news-coverimage-id' ), + frame; + + // Set up handler for remove image functionality. + $delImgButton.on( 'click', function() { + $imgContainer.empty(); + $addImgButton.removeClass( 'hidden' ); + $delImgButton.addClass( 'hidden' ); + $imgIdInput.val( '' ); + } ); + + // Set up handler for add image functionality. + $addImgButton.on( 'click', function () { + + // Open frame, if it already exists. + if ( frame ) { + frame.open(); + return; + } + + // Set configuration for media frame. + frame = wp.media( { multiple: false } ); + + // Set up handler for image selection. + frame.on( 'select', function () { + + // Get information about the attachment. + var attachment = frame.state().get( 'selection' ).first().toJSON(), + imgUrl = attachment.url; + + // Set image URL to medium size, if available. + if ( attachment.sizes.medium && attachment.sizes.medium.url ) { + imgUrl = attachment.sizes.medium.url; + } + + // Clear current values. + $imgContainer.empty(); + $imgIdInput.val( '' ); + + // Add the image and ID, swap visibility of add and remove buttons. + $imgContainer.append( '' ); // phpcs:ignore WordPressVIPMinimum.JS.StringConcat.Found, WordPressVIPMinimum.JS.HTMLExecutingFunctions.append + $imgIdInput.val( attachment.id ); + $addImgButton.addClass( 'hidden' ); + $delImgButton.removeClass( 'hidden' ); + } ); + + // Open the media frame. + frame.open(); + } ); + } ); + + // Set up add and remove video functionality. + $( '[data-provider="video_id"]' ).each( function () { + var $this = $( this ), + $addVideoButton = $this.find( '.apple-news-cover-video-id-add' ), + $delVideoButton = $this.find( '.apple-news-cover-video-id-remove' ), + $previewContainer = $this.find( '.apple-news-cover-media-video' ), + $videoIdInput = $this.find( '[name="apple_news_cover_video_id"]' ), + frame; + + // Set up handler for remove functionality. + $delVideoButton.on( 'click', function() { + $previewContainer.empty(); + $addVideoButton.removeClass( 'hidden' ); + $delVideoButton.addClass( 'hidden' ); + $videoIdInput.val( '' ); + } ); + + // Set up handler for add functionality. + $addVideoButton.on( 'click', function () { + // Open frame, if it already exists. + if ( frame ) { + frame.open(); + return; + } + + // Set configuration for media frame. + frame = wp.media( { + multiple: false, + library: { + type: 'video/mp4' + }, + } ); + + // Set up handler for image selection. + frame.on( 'select', function () { + // Get information about the attachment. + var attachment = frame.state().get( 'selection' ).first().toJSON(), + videoUrl = attachment.url; + + // Clear current values. + $previewContainer.empty(); + $videoIdInput.val( '' ); + + // Add the preview and ID, swap visibility of add and remove buttons. + $previewContainer.append( $( '
+HTML; + + $post_id = self::factory()->post->create( [ 'post_content' => $post_content ] ); + $json = $this->get_json_for_post( $post_id ); + + // There should be a photo component, but there shouldn't be a caption. + $this->assertTrue( isset( $json['components'][3]['components'][0]['role'] ) ); + $this->assertSame( 'photo', $json['components'][3]['components'][0]['role'] ); + $this->assertFalse( isset( $json['components'][3]['components'][0]['caption'] ) ); + } + + /** + * Test that recipe instructions are expanded from a list of strings. + */ + public function test_recipe_instructions_list_of_strings(): void { + // These instructions via https://www.delicious.com.au/recipes/huevos-rancheros-beans/7WDKzQe3. + $post_content = << +

Chocolate Cake

+ + +
+HTML; + + $post_id = self::factory()->post->create( [ 'post_content' => $post_content ] ); + $json = $this->get_json_for_post( $post_id ); + + $this->assertStringStartsWith( + 'Preheat oven to 180', + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'][0]['text'] + ); + } + + /** + * Test that recipe instructions are expanded from a list of HowToStep objects. + */ + public function test_recipe_instructions_steps(): void { + // These instructions via https://chatelaine.com/recipe/desserts/easy-chocolate-cake/. + $post_content = << +

Chocolate Cake

+ + + +HTML; + + $post_id = self::factory()->post->create( [ 'post_content' => $post_content ] ); + $json = $this->get_json_for_post( $post_id ); + + $this->assertStringStartsWith( + 'Step 1', + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'][0]['text'] + ); + $this->assertStringStartsWith( + 'PREHEAT oven to 350F', + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'][1]['text'] + ); + } + + /** + * Test that recipe instructions are expanded from a tree of HowToSection and HowToStep objects. + */ + public function test_recipe_instructions_sections_and_steps(): void { + // These instructions via https://www.ricardocuisine.com/recettes/3808-chop-suey-au-poulet. + $post_content = << +

Chocolate Cake

+ + + +HTML; + + $post_id = self::factory()->post->create( [ 'post_content' => $post_content ] ); + $json = $this->get_json_for_post( $post_id ); + + $this->assertSame( + 'Chop suey au poulet', + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'][0]['text'] + ); + $this->assertStringStartsWith( + 'Dans un bol', + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'][1]['components'][0]['components'][0]['text'] + ); + } + + /** + * Test that recipe instructions don't include a heading and body text with the same content. + */ + public function test_recipe_step_with_duplicate_text(): void { + $post_content = << +

Chocolate Cake

+ + + +HTML; + + $post_id = self::factory()->post->create( [ 'post_content' => $post_content ] ); + $json = $this->get_json_for_post( $post_id ); + + $this->assertCount( + 1, + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'] + ); + $this->assertSame( + 'body', // Not heading. + $json['components'][3]['components'][0]['components'][4]['components'][0]['components'][1]['components'][1]['components'][0]['components'][0]['role'] + ); + } + + /** + * Test that recipe HTML is processed normally when the 'use schema' setting is 'no'. + */ + public function test_recipe_component_with_component_use_schema_no(): void { + $this->settings->__set( 'recipe_component_use_schema', 'no' ); + + // These instructions via https://www.ricardocuisine.com/recettes/3808-chop-suey-au-poulet. + $post_content = << + Chocolate Cake
+ + + +HTML; + + $post_id = self::factory()->post->create( [ 'post_content' => $post_content ] ); + $json = $this->get_json_for_post( $post_id ); + + // This should be a table because the raw HTML found in the matching recipe element should be used. + $this->assertSame( + 'htmltable', + $json['components'][3]['components'][0]['role'] + ); + } +} diff --git a/tests/data/test-video.mp4 b/tests/data/test-video.mp4 new file mode 100644 index 000000000..2872b527b Binary files /dev/null and b/tests/data/test-video.mp4 differ diff --git a/tests/test-class-apple-news.php b/tests/test-class-apple-news.php index d76127b34..4294dfb9a 100644 --- a/tests/test-class-apple-news.php +++ b/tests/test-class-apple-news.php @@ -352,13 +352,193 @@ public function test_upgrade_to_2_5_0(): void { $this->assertEquals( 5, $theme->get_value( 'aside_border_width' ), 'Expected the custom blockquote border width to be applied to the aside border width as part of the upgrade.' ); } + /** + * Tests an upgrade from a version prior to 2.7.0 to version 2.7.0. + */ + public function test_upgrade_to_2_7_0(): void { + // Load the default theme and get its data. + $this->load_example_theme( 'default' ); + $theme_data = get_option( Theme::theme_key( 'Default' ) ); + + // Remove all keys for the new recipe settings. + unset( $theme_data['recipe_background_color'] ); + unset( $theme_data['recipe_background_color_dark'] ); + unset( $theme_data['recipe_body_background_color'] ); + unset( $theme_data['recipe_body_background_color_dark'] ); + unset( $theme_data['recipe_body_color'] ); + unset( $theme_data['recipe_body_color_dark'] ); + unset( $theme_data['recipe_body_font'] ); + unset( $theme_data['recipe_body_line_height'] ); + unset( $theme_data['recipe_body_link_color'] ); + unset( $theme_data['recipe_body_link_color_dark'] ); + unset( $theme_data['recipe_body_size'] ); + unset( $theme_data['recipe_body_tracking'] ); + unset( $theme_data['recipe_caption_background_color'] ); + unset( $theme_data['recipe_caption_background_color_dark'] ); + unset( $theme_data['recipe_caption_color'] ); + unset( $theme_data['recipe_caption_color_dark'] ); + unset( $theme_data['recipe_caption_font'] ); + unset( $theme_data['recipe_caption_line_height'] ); + unset( $theme_data['recipe_caption_link_color'] ); + unset( $theme_data['recipe_caption_link_color_dark'] ); + unset( $theme_data['recipe_caption_size'] ); + unset( $theme_data['recipe_caption_tracking'] ); + unset( $theme_data['recipe_details_background_color'] ); + unset( $theme_data['recipe_details_background_color_dark'] ); + unset( $theme_data['recipe_details_color'] ); + unset( $theme_data['recipe_details_color_dark'] ); + unset( $theme_data['recipe_details_font'] ); + unset( $theme_data['recipe_details_line_height'] ); + unset( $theme_data['recipe_details_link_color'] ); + unset( $theme_data['recipe_details_link_color_dark'] ); + unset( $theme_data['recipe_details_size'] ); + unset( $theme_data['recipe_details_tracking'] ); + unset( $theme_data['recipe_header2_color'] ); + unset( $theme_data['recipe_header2_color_dark'] ); + unset( $theme_data['recipe_header2_font'] ); + unset( $theme_data['recipe_header2_line_height'] ); + unset( $theme_data['recipe_header2_size'] ); + unset( $theme_data['recipe_header2_tracking'] ); + unset( $theme_data['recipe_header3_color'] ); + unset( $theme_data['recipe_header3_color_dark'] ); + unset( $theme_data['recipe_header3_font'] ); + unset( $theme_data['recipe_header3_line_height'] ); + unset( $theme_data['recipe_header3_size'] ); + unset( $theme_data['recipe_header3_tracking'] ); + unset( $theme_data['recipe_header4_color'] ); + unset( $theme_data['recipe_header4_color_dark'] ); + unset( $theme_data['recipe_header4_font'] ); + unset( $theme_data['recipe_header4_line_height'] ); + unset( $theme_data['recipe_header4_size'] ); + unset( $theme_data['recipe_header4_tracking'] ); + unset( $theme_data['recipe_title_color'] ); + unset( $theme_data['recipe_title_color_dark'] ); + unset( $theme_data['recipe_title_font'] ); + unset( $theme_data['recipe_title_line_height'] ); + unset( $theme_data['recipe_title_size'] ); + unset( $theme_data['recipe_title_tracking'] ); + + // Set different values for the source keys for the recipe settings so we can ensure they are migrated properly. + $theme_data['blockquote_background_color'] = '#abc123'; + $theme_data['blockquote_background_color_dark'] = '#bcd234'; + $theme_data['blockquote_color'] = '#cde345'; + $theme_data['blockquote_color_dark'] = '#def456'; + $theme_data['body_font'] = 'Papyrus'; + $theme_data['body_line_height'] = 123.0; + $theme_data['body_link_color'] = '#aaa111'; + $theme_data['body_link_color_dark'] = '#bbb222'; + $theme_data['body_size'] = 24; + $theme_data['body_tracking'] = 5; + $theme_data['header2_color'] = '#222222'; + $theme_data['header2_color_dark'] = '#222ddd'; + $theme_data['header2_font'] = 'Zapfino'; + $theme_data['header2_line_height'] = 48.0; + $theme_data['header2_size'] = 48; + $theme_data['header2_tracking'] = 15; + $theme_data['header3_color'] = '#333333'; + $theme_data['header3_color_dark'] = '#333ddd'; + $theme_data['header3_font'] = 'Copperplate'; + $theme_data['header3_line_height'] = 36.0; + $theme_data['header3_size'] = 36; + $theme_data['header3_tracking'] = 10; + $theme_data['header4_color'] = '#444444'; + $theme_data['header4_color_dark'] = '#444ddd'; + $theme_data['header4_font'] = 'Baskerville'; + $theme_data['header4_line_height'] = 34.0; + $theme_data['header4_size'] = 34; + $theme_data['header4_tracking'] = 9; + $theme_data['header5_color'] = '#555555'; + $theme_data['header5_color_dark'] = '#555ddd'; + $theme_data['header5_font'] = 'Verdana'; + $theme_data['header5_line_height'] = 35.0; + $theme_data['header5_size'] = 35; + $theme_data['header5_tracking'] = 8; + + // Save the updated theme data to prep for the upgrade test. + update_option( Theme::theme_key( 'Default' ), $theme_data ); + + // Run the upgrade. + $apple_news = new Apple_News(); + $apple_news->upgrade_to_2_7_0(); + $theme = Theme::get_used(); + $theme->load(); + + // Check settings that are migrated from blockquote settings. + $this->assertEquals( '#abc123', $theme->get_value( 'recipe_background_color' ), 'Expected the blockquote background color to be applied to the recipe background color as part of the upgrade.' ); + $this->assertEquals( '#abc123', $theme->get_value( 'recipe_body_background_color' ), 'Expected the blockquote background color to be applied to the recipe body background color as part of the upgrade.' ); + $this->assertEquals( '#abc123', $theme->get_value( 'recipe_caption_background_color' ), 'Expected the blockquote background color to be applied to the recipe caption background color as part of the upgrade.' ); + $this->assertEquals( '#abc123', $theme->get_value( 'recipe_details_background_color' ), 'Expected the blockquote background color to be applied to the recipe details background color as part of the upgrade.' ); + $this->assertEquals( '#bcd234', $theme->get_value( 'recipe_background_color_dark' ), 'Expected the blockquote dark background color to be applied to the recipe dark background color as part of the upgrade.' ); + $this->assertEquals( '#bcd234', $theme->get_value( 'recipe_body_background_color_dark' ), 'Expected the blockquote dark background color to be applied to the recipe body dark background color as part of the upgrade.' ); + $this->assertEquals( '#bcd234', $theme->get_value( 'recipe_caption_background_color_dark' ), 'Expected the blockquote dark background color to be applied to the recipe caption dark background color as part of the upgrade.' ); + $this->assertEquals( '#bcd234', $theme->get_value( 'recipe_details_background_color_dark' ), 'Expected the blockquote dark background color to be applied to the recipe details dark background color as part of the upgrade.' ); + $this->assertEquals( '#cde345', $theme->get_value( 'recipe_body_color' ), 'Expected the blockquote color to be applied to the recipe body color as part of the upgrade.' ); + $this->assertEquals( '#cde345', $theme->get_value( 'recipe_caption_color' ), 'Expected the blockquote color to be applied to the recipe caption color as part of the upgrade.' ); + $this->assertEquals( '#cde345', $theme->get_value( 'recipe_details_color' ), 'Expected the blockquote color to be applied to the recipe details color as part of the upgrade.' ); + $this->assertEquals( '#def456', $theme->get_value( 'recipe_body_color_dark' ), 'Expected the blockquote dark color to be applied to the recipe body dark color as part of the upgrade.' ); + $this->assertEquals( '#def456', $theme->get_value( 'recipe_caption_color_dark' ), 'Expected the blockquote dark color to be applied to the recipe caption dark color as part of the upgrade.' ); + $this->assertEquals( '#def456', $theme->get_value( 'recipe_details_color_dark' ), 'Expected the blockquote dark color to be applied to the recipe details dark color as part of the upgrade.' ); + + // Check settings that are migrated from body settings. + $this->assertEquals( 'Papyrus', $theme->get_value( 'recipe_body_font' ), 'Expected the body font to be applied to the recipe body font as part of the upgrade.' ); + $this->assertEquals( 'Papyrus', $theme->get_value( 'recipe_caption_font' ), 'Expected the body font to be applied to the recipe caption font as part of the upgrade.' ); + $this->assertEquals( 'Papyrus', $theme->get_value( 'recipe_details_font' ), 'Expected the body font to be applied to the recipe details font as part of the upgrade.' ); + $this->assertEquals( 123.0, $theme->get_value( 'recipe_body_line_height' ), 'Expected the body line height to be applied to the recipe body line height as part of the upgrade.' ); + $this->assertEquals( 123.0, $theme->get_value( 'recipe_caption_line_height' ), 'Expected the body line height to be applied to the recipe caption line height as part of the upgrade.' ); + $this->assertEquals( 123.0, $theme->get_value( 'recipe_details_line_height' ), 'Expected the body line height to be applied to the recipe details line height as part of the upgrade.' ); + $this->assertEquals( '#aaa111', $theme->get_value( 'recipe_body_link_color' ), 'Expected the body link color to be applied to the recipe body link color as part of the upgrade.' ); + $this->assertEquals( '#aaa111', $theme->get_value( 'recipe_caption_link_color' ), 'Expected the body link color to be applied to the recipe caption link color as part of the upgrade.' ); + $this->assertEquals( '#aaa111', $theme->get_value( 'recipe_details_link_color' ), 'Expected the body link color to be applied to the recipe details link color as part of the upgrade.' ); + $this->assertEquals( '#bbb222', $theme->get_value( 'recipe_body_link_color_dark' ), 'Expected the body dark link color to be applied to the recipe body dark link color as part of the upgrade.' ); + $this->assertEquals( '#bbb222', $theme->get_value( 'recipe_caption_link_color_dark' ), 'Expected the body dark link color to be applied to the recipe caption dark link color as part of the upgrade.' ); + $this->assertEquals( '#bbb222', $theme->get_value( 'recipe_details_link_color_dark' ), 'Expected the body dark link color to be applied to the recipe details dark link color as part of the upgrade.' ); + $this->assertEquals( 24, $theme->get_value( 'recipe_body_size' ), 'Expected the body size to be applied to the recipe body size as part of the upgrade.' ); + $this->assertEquals( 24, $theme->get_value( 'recipe_caption_size' ), 'Expected the body size to be applied to the recipe caption size as part of the upgrade.' ); + $this->assertEquals( 24, $theme->get_value( 'recipe_details_size' ), 'Expected the body size to be applied to the recipe details size as part of the upgrade.' ); + $this->assertEquals( 5, $theme->get_value( 'recipe_body_tracking' ), 'Expected the body tracking to be applied to the recipe body tracking as part of the upgrade.' ); + $this->assertEquals( 5, $theme->get_value( 'recipe_caption_tracking' ), 'Expected the body tracking to be applied to the recipe caption tracking as part of the upgrade.' ); + $this->assertEquals( 5, $theme->get_value( 'recipe_details_tracking' ), 'Expected the body tracking to be applied to the recipe details tracking as part of the upgrade.' ); + + // Check settings that are migrated from h2 settings. + $this->assertEquals( '#222222', $theme->get_value( 'recipe_title_color' ), 'Expected the h2 color to be applied to the recipe title color as part of the upgrade.' ); + $this->assertEquals( '#222ddd', $theme->get_value( 'recipe_title_color_dark' ), 'Expected the h2 dark color to be applied to the recipe title dark color as part of the upgrade.' ); + $this->assertEquals( 'Zapfino', $theme->get_value( 'recipe_title_font' ), 'Expected the h2 font to be applied to the recipe title font as part of the upgrade.' ); + $this->assertEquals( 48.0, $theme->get_value( 'recipe_title_line_height' ), 'Expected the h2 line height to be applied to the recipe title line height as part of the upgrade.' ); + $this->assertEquals( 48, $theme->get_value( 'recipe_title_size' ), 'Expected the h2 size to be applied to the recipe title size as part of the upgrade.' ); + $this->assertEquals( 15, $theme->get_value( 'recipe_title_tracking' ), 'Expected the h2 tracking to be applied to the recipe title tracking as part of the upgrade.' ); + + // Check settings that are migrated from h3 settings. + $this->assertEquals( '#333333', $theme->get_value( 'recipe_header2_color' ), 'Expected the h3 color to be applied to the recipe header2 color as part of the upgrade.' ); + $this->assertEquals( '#333ddd', $theme->get_value( 'recipe_header2_color_dark' ), 'Expected the h3 dark color to be applied to the recipe header2 dark color as part of the upgrade.' ); + $this->assertEquals( 'Copperplate', $theme->get_value( 'recipe_header2_font' ), 'Expected the h3 font to be applied to the recipe header2 font as part of the upgrade.' ); + $this->assertEquals( 36.0, $theme->get_value( 'recipe_header2_line_height' ), 'Expected the h3 line height to be applied to the recipe header2 line height as part of the upgrade.' ); + $this->assertEquals( 36, $theme->get_value( 'recipe_header2_size' ), 'Expected the h3 size to be applied to the recipe header2 size as part of the upgrade.' ); + $this->assertEquals( 10, $theme->get_value( 'recipe_header2_tracking' ), 'Expected the h3 tracking to be applied to the recipe header2 tracking as part of the upgrade.' ); + + // Check settings that are migrated from h4 settings. + $this->assertEquals( '#444444', $theme->get_value( 'recipe_header3_color' ), 'Expected the h4 color to be applied to the recipe header3 color as part of the upgrade.' ); + $this->assertEquals( '#444ddd', $theme->get_value( 'recipe_header3_color_dark' ), 'Expected the h4 dark color to be applied to the recipe header3 dark color as part of the upgrade.' ); + $this->assertEquals( 'Baskerville', $theme->get_value( 'recipe_header3_font' ), 'Expected the h4 font to be applied to the recipe header3 font as part of the upgrade.' ); + $this->assertEquals( 34.0, $theme->get_value( 'recipe_header3_line_height' ), 'Expected the h4 line height to be applied to the recipe header3 line height as part of the upgrade.' ); + $this->assertEquals( 34, $theme->get_value( 'recipe_header3_size' ), 'Expected the h4 size to be applied to the recipe header3 size as part of the upgrade.' ); + $this->assertEquals( 9, $theme->get_value( 'recipe_header3_tracking' ), 'Expected the h4 tracking to be applied to the recipe header3 tracking as part of the upgrade.' ); + + // Check settings that are migrated from h5 settings. + $this->assertEquals( '#555555', $theme->get_value( 'recipe_header4_color' ), 'Expected the h5 color to be applied to the recipe header4 color as part of the upgrade.' ); + $this->assertEquals( '#555ddd', $theme->get_value( 'recipe_header4_color_dark' ), 'Expected the h5 dark color to be applied to the recipe header4 dark color as part of the upgrade.' ); + $this->assertEquals( 'Verdana', $theme->get_value( 'recipe_header4_font' ), 'Expected the h5 font to be applied to the recipe header4 font as part of the upgrade.' ); + $this->assertEquals( 35.0, $theme->get_value( 'recipe_header4_line_height' ), 'Expected the h5 line height to be applied to the recipe header4 line height as part of the upgrade.' ); + $this->assertEquals( 35, $theme->get_value( 'recipe_header4_size' ), 'Expected the h5 size to be applied to the recipe header4 size as part of the upgrade.' ); + $this->assertEquals( 8, $theme->get_value( 'recipe_header4_tracking' ), 'Expected the h5 tracking to be applied to the recipe header4 tracking as part of the upgrade.' ); + } + /** * Ensures that the version in Apple_News matches the reported plugin version. * * @see Apple_News::$version */ public function test_version() { - $plugin_data = apple_news_get_plugin_data( translate: false ); + $plugin_data = apple_news_get_plugin_data(); $this->assertEquals( Apple_News::$version, $plugin_data['Version'] ); } }