diff --git a/includes/admin/feedzy-rss-feeds-admin.php b/includes/admin/feedzy-rss-feeds-admin.php index 337d7dd6..fd308c79 100644 --- a/includes/admin/feedzy-rss-feeds-admin.php +++ b/includes/admin/feedzy-rss-feeds-admin.php @@ -2513,4 +2513,133 @@ public function add_black_friday_data( $configs ) { return $configs; } + + /** + * Validate the feed URL and check if it's a valid RSS/Atom feed. + * + * @return void + */ + public function validate_feed() { + try { + if ( + ! isset( $_POST['nonce'] ) || + ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), FEEDZY_BASEFILE ) + ) { + wp_send_json_error( array( 'message' => __( 'Security check failed.', 'feedzy-rss-feeds' ) ) ); + } + + $feed_urls = isset( $_POST['feed_url'] ) ? sanitize_text_field( wp_unslash( $_POST['feed_url'] ) ) : ''; + + if ( empty( $feed_urls ) ) { + wp_send_json_error( array( 'message' => __( 'Feed URL cannot be empty.', 'feedzy-rss-feeds' ) ) ); + } + + $urls = array_map( 'trim', explode( ',', $feed_urls ) ); + $urls = array_filter( $urls ); + + if ( empty( $urls ) ) { + wp_send_json_error( array( 'message' => __( 'No valid URLs provided.', 'feedzy-rss-feeds' ) ) ); + } + + $results = array(); + + foreach ( $urls as $feed_url ) { + $feed_url = esc_url_raw( $feed_url ); + + if ( ! filter_var( $feed_url, FILTER_VALIDATE_URL ) ) { + $results[] = array( + 'url' => $feed_url, + 'status' => 'error', + 'message' => __( 'Invalid URL format', 'feedzy-rss-feeds' ), + ); + continue; + } + + $feed = $this->fetch_feed( array( $feed_url ), '1_mins', array() ); + + if ( is_wp_error( $feed ) ) { + $results[] = array( + 'url' => $feed_url, + 'status' => 'error', + 'message' => __( 'Error fetching feed: ', 'feedzy-rss-feeds' ) . $feed->get_error_message(), + ); + continue; + } + + if ( + ! is_object( $feed ) || + ! method_exists( $feed, 'get_item_quantity' ) + ) { + $results[] = array( + 'url' => $feed_url, + 'status' => 'error', + 'message' => __( 'Invalid feed object returned', 'feedzy-rss-feeds' ), + ); + continue; + } + + try { + $items = $feed->get_item_quantity(); + $title = $feed->get_title(); + $error = $feed->error(); + + if ( is_array( $error ) && ! empty( $error ) ) { + $results[] = array( + 'url' => $feed_url, + 'status' => 'error', + 'message' => __( 'Error fetching feed: ', 'feedzy-rss-feeds' ) . implode( ', ', $error ), + ); + continue; + } + + if ( 0 === $items ) { + $results[] = array( + 'url' => $feed_url, + 'status' => 'warning', + 'message' => __( 'Feed is empty', 'feedzy-rss-feeds' ), + ); + continue; + } + + $results[] = array( + 'url' => $feed_url, + 'status' => 'success', + 'message' => $title . sprintf( + /* translators: %d is the number of items found in the feed */ + _n( + '%d item found', + '%d items found', + $items, + 'feedzy-rss-feeds' + ), + $items + ), + 'items' => $items, + 'title' => $title, + ); + + } catch ( Throwable $e ) { + $results[] = array( + 'url' => $feed_url, + 'status' => 'error', + /* translators: %s is the error message */ + 'message' => sprintf( __( 'Error reading feed: %s', 'feedzy-rss-feeds' ), $e->getMessage() ), + ); + } + } + + wp_send_json_success( + array( + 'results' => $results, + ) + ); + } catch ( Throwable $e ) { + wp_send_json_error( + array( + /* translators: %s is the error message */ + 'message' => sprintf( __( 'An error occurred: %s', 'feedzy-rss-feeds' ), $e->getMessage() ), + ) + ); + } + } } diff --git a/includes/admin/feedzy-rss-feeds-import.php b/includes/admin/feedzy-rss-feeds-import.php index 902415a1..d536d42e 100644 --- a/includes/admin/feedzy-rss-feeds-import.php +++ b/includes/admin/feedzy-rss-feeds-import.php @@ -163,6 +163,7 @@ public function enqueue_styles() { array( 'ajax' => array( 'security' => wp_create_nonce( FEEDZY_BASEFILE ), + 'url' => admin_url( 'admin-ajax.php' ), ), 'i10n' => array( 'importing' => __( 'Importing', 'feedzy-rss-feeds' ) . '...', @@ -189,6 +190,10 @@ public function enqueue_styles() { esc_html__( 'Upload Import', 'feedzy-rss-feeds' ) ), 'is_pro' => feedzy_is_pro(), + 'validation_messages' => array( + 'invalid_feed_url' => __( 'Invalid feed URL.', 'feedzy-rss-feeds' ), + 'error_validating_feed_url' => __( 'Error validating feed URL.', 'feedzy-rss-feeds' ), + ), ), ) ); diff --git a/includes/feedzy-rss-feeds.php b/includes/feedzy-rss-feeds.php index 47fa150e..5c8df92e 100644 --- a/includes/feedzy-rss-feeds.php +++ b/includes/feedzy-rss-feeds.php @@ -202,6 +202,7 @@ private function define_admin_hooks() { self::$instance->loader->add_filter( 'admin_footer', self::$instance->admin, 'handle_upgrade_submenu' ); self::$instance->loader->add_action( 'current_screen', self::$instance->admin, 'handle_legacy' ); self::$instance->loader->add_action( 'init', self::$instance->admin, 'register_settings' ); + self::$instance->loader->add_action( 'wp_ajax_feedzy_validate_feed', self::$instance->admin, 'validate_feed' ); // do not load this with the loader as this will need a corresponding remove_filter also. add_filter( 'update_post_metadata', array( self::$instance->admin, 'validate_category_feeds' ), 10, 5 ); diff --git a/includes/gutenberg/feedzy-rss-feeds-gutenberg-block.php b/includes/gutenberg/feedzy-rss-feeds-gutenberg-block.php index 8edb37c5..9f475aa2 100644 --- a/includes/gutenberg/feedzy-rss-feeds-gutenberg-block.php +++ b/includes/gutenberg/feedzy-rss-feeds-gutenberg-block.php @@ -75,6 +75,8 @@ public function feedzy_gutenberg_scripts() { 'imagepath' => esc_url( FEEDZY_ABSURL . 'img/' ), 'isPro' => feedzy_is_pro(), 'upsellLinkBlockEditor' => esc_url( tsdk_translate_link( tsdk_utmify( FEEDZY_UPSELL_LINK, 'keywordsfilter', 'blockeditor' ) ) ), + 'nonce' => wp_create_nonce( FEEDZY_BASEFILE ), + 'url' => admin_url( 'admin-ajax.php' ), ) ); diff --git a/includes/gutenberg/feedzy-rss-feeds-loop-block.php b/includes/gutenberg/feedzy-rss-feeds-loop-block.php index 4e63baf5..93a582c7 100644 --- a/includes/gutenberg/feedzy-rss-feeds-loop-block.php +++ b/includes/gutenberg/feedzy-rss-feeds-loop-block.php @@ -73,6 +73,8 @@ public function register_block() { array( 'imagepath' => esc_url( FEEDZY_ABSURL . 'img/' ), 'defaultImage' => esc_url( FEEDZY_ABSURL . 'img/feedzy.svg' ), + 'url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( FEEDZY_BASEFILE ), 'isPro' => feedzy_is_pro(), ) ); diff --git a/includes/views/css/import-metabox-edit.css b/includes/views/css/import-metabox-edit.css index 159d480a..81eb04f3 100644 --- a/includes/views/css/import-metabox-edit.css +++ b/includes/views/css/import-metabox-edit.css @@ -202,3 +202,33 @@ span.feedzy-spinner { .fz-import-field.hidden { display: none; } + +.feedzy-wrap .fz-validation-message { + margin: 10px 0; + padding: 12px 15px; + border-radius: 4px; + font-size: 14px; + position: relative; + z-index: 10; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + border: 1px solid #5555559c; +} + +.feedzy-wrap .fz-validation-message button { + margin-left: auto; +} + +.fz-validation-message .fz-success { + color: #155724; +} + +.fz-validation-message .fz-error { + color: #721c24; +} + +.fz-validation-message .fz-warning { + color: #856404; +} \ No newline at end of file diff --git a/includes/views/import-metabox-edit.php b/includes/views/import-metabox-edit.php index 312a556d..8328a63b 100644 --- a/includes/views/import-metabox-edit.php +++ b/includes/views/import-metabox-edit.php @@ -42,25 +42,29 @@
- -
- +
+ +
+ +
-
- -
+
+
+ ) : ( +
+
- {__('Validate', 'feedzy-rss-feeds')} - - - {this.state.error && ( -
+
+ + {this.props.attributes.categories && + this.props.attributes.categories + .length > 0 && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + + )} +
+
- )} + +
+ + {this.renderValidationResults()} +

{__( "Enter the full URL of the feed source you wish to display here, or the name of a group you've created. Also you can add multiple URLs just separate them with a comma. You can manage your groups feed from", @@ -521,7 +644,7 @@ class Editor extends Component { {__('here', 'feedzy-rss-feeds')}

- +
)}
diff --git a/js/FeedzyBlock/style.scss b/js/FeedzyBlock/style.scss index a94aacf7..bb771e7f 100644 --- a/js/FeedzyBlock/style.scss +++ b/js/FeedzyBlock/style.scss @@ -1,6 +1,7 @@ .wp-block-feedzy-rss-feeds-feedzy-block { .feedzy-source-wrap { position: relative; + width: max-content; } .feedzy-source { margin-right: 10px; @@ -221,4 +222,9 @@ font-size: 13px; line-height: 1.4em; color: #3c434a; +} +.feedzy-validation-results { + display: flex; + flex-direction: column; + gap: 10px; } \ No newline at end of file diff --git a/js/FeedzyLoop/placeholder.js b/js/FeedzyLoop/placeholder.js index d36561f4..9e0160cc 100644 --- a/js/FeedzyLoop/placeholder.js +++ b/js/FeedzyLoop/placeholder.js @@ -3,16 +3,15 @@ * WordPress dependencies. */ import { __ } from '@wordpress/i18n'; - +import { useState } from '@wordpress/element'; import { BaseControl, Button, Placeholder, Spinner, + Notice, } from '@wordpress/components'; - import { useSelect } from '@wordpress/data'; - import { store as coreStore } from '@wordpress/core-data'; /** @@ -21,6 +20,9 @@ import { store as coreStore } from '@wordpress/core-data'; import FeedControl from './components/FeedControl'; const BlockPlaceholder = ({ attributes, setAttributes, onSaveFeed }) => { + const [isValidating, setIsValidating] = useState(false); + const [validationResults, setValidationResults] = useState([]); + const { categories, isLoading } = useSelect((select) => { const { getEntityRecords, isResolving } = select(coreStore); @@ -33,20 +35,132 @@ const BlockPlaceholder = ({ attributes, setAttributes, onSaveFeed }) => { }; }, []); + const handleLoadFeed = async () => { + if (!attributes?.feed?.source) { + return; + } + + setIsValidating(true); + setValidationResults([]); + + const isCategory = categories.some( + (cat) => cat.id === attributes.feed.source + ); + + if (isCategory && 'group' === attributes.feed.type) { + onSaveFeed(); + setIsValidating(false); + return; + } + + try { + const formData = new FormData(); + formData.append('action', 'feedzy_validate_feed'); + formData.append('feed_url', attributes.feed.source); + formData.append('nonce', window.feedzyData?.nonce); + + const response = await fetch(window.feedzyData?.url, { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + + if (data.success && data.data?.results) { + const results = data.data.results; + setValidationResults(results); + + const hasErrors = results.some( + (result) => result.status === 'error' + ); + + if (!hasErrors) { + onSaveFeed(); + } + } else if (!data.success) { + setValidationResults([ + { + status: 'error', + message: + data.data?.message || + __('Validation failed', 'feedzy-rss-feeds'), + }, + ]); + } + } catch (error) { + setValidationResults([ + { + status: 'error', + message: __( + 'Failed to validate feed. Please check your connection and try again.', + 'feedzy-rss-feeds' + ), + }, + ]); + } finally { + setIsValidating(false); + } + }; + + const handleFeedChange = (value) => { + setAttributes({ feed: value }); + }; + + const renderValidationResults = () => { + if (!validationResults || validationResults.length === 0) { + return null; + } + + return ( +
+ {validationResults.map((result, index) => ( + + {result.url && ( + <> + {result.url} +
+ + )} + {result.message} +
+ ))} +
+ ); + }; + return ( - {isLoading && ( + {(isLoading || isValidating) && (
-

{__('Fetching…', 'feedzy-rss-feeds')}

+

+ {isValidating + ? __( + 'Validating and fetching feed…', + 'feedzy-rss-feeds' + ) + : __('Loading…', 'feedzy-rss-feeds')} +

)} - {!isLoading && ( + {!isLoading && !isValidating && ( <> { value: category.id, })), ]} - onChange={(value) => setAttributes({ feed: value })} + onChange={handleFeedChange} /> + {renderValidationResults()} +

{__( 'Enter the full URL of the feed source you wish to display here, or select a Feed Group. Also you can add multiple URLs separated with a comma. You can manage your feed groups from', @@ -79,24 +195,9 @@ const BlockPlaceholder = ({ attributes, setAttributes, onSaveFeed }) => {

- - -
)} diff --git a/tests/e2e/specs/classic-block.spec.js b/tests/e2e/specs/classic-block.spec.js index e5a9c38e..f70e7741 100644 --- a/tests/e2e/specs/classic-block.spec.js +++ b/tests/e2e/specs/classic-block.spec.js @@ -4,6 +4,68 @@ import { test, expect } from '@wordpress/e2e-test-utils-playwright'; test.describe('Feedzy Classic Block', () => { + test('check validation for invalid URL', async ({ + editor, + page, + admin, + }) => { + await admin.createNewPost(); + + await editor.insertBlock({ + name: 'feedzy-rss-feeds/feedzy-block', + }); + + await page + .getByPlaceholder('Enter URL or group of your') + .fill('http://invalid-url.com/feed'); + + await page.getByRole('button', { name: 'Load Feed' }).click(); + + await page.waitForSelector('.feedzy-validation-results'); + + await expect( + page + .locator('.feedzy-validation-results .is-error') + .getByText('http://invalid-url.com/feed', { exact: true }) + ).toBeVisible(); + }); + + test('check validation for invalid and valid URL', async ({ + editor, + page, + admin, + }) => { + await admin.createNewPost(); + + await editor.insertBlock({ + name: 'feedzy-rss-feeds/feedzy-block', + }); + + await page + .getByPlaceholder('Enter URL or group of your') + .fill( + 'http://invalid-url.com/feed, https://www.nasa.gov/feeds/iotd-feed/' + ); + + await page.getByRole('button', { name: 'Load Feed' }).click(); + + await page.waitForSelector('.feedzy-validation-results'); + + await expect( + page + .locator('.feedzy-validation-results .is-error') + .getByText('http://invalid-url.com/feed', { exact: true }) + ).toBeVisible(); + + await expect( + page + .locator('.feedzy-validation-results .is-success') + .getByText('https://www.nasa.gov/feeds/iotd-feed/', { + exact: true, + }) + ).toBeVisible(); + }); + test('check aspect ratio default', async ({ editor, page, admin }) => { await admin.createNewPost(); diff --git a/tests/e2e/specs/feed.spec.js b/tests/e2e/specs/feed.spec.js index 31421f97..f1eb67c1 100644 --- a/tests/e2e/specs/feed.spec.js +++ b/tests/e2e/specs/feed.spec.js @@ -29,10 +29,6 @@ test.describe( 'Feed Settings', () => { await page.getByPlaceholder('Add a name for your import').fill(importName); // Add feed URL via tag input. - await page.getByPlaceholder('Paste your feed URL and click').fill(FEED_URL); - await page.getByPlaceholder('Paste your feed URL and click').press('Enter'); - await expect( page.getByText( FEED_URL ) ).toBeVisible(); - await addFeeds( page, [FEED_URL] ); await page.getByRole('button', { name: 'Save', exact: true }).click({ force: true, clickCount: 1 }); diff --git a/tests/e2e/specs/loop.spec.js b/tests/e2e/specs/loop.spec.js index 5583a119..37ebd186 100644 --- a/tests/e2e/specs/loop.spec.js +++ b/tests/e2e/specs/loop.spec.js @@ -106,4 +106,40 @@ test.describe('Feedzy Loop', () => { const feedzyLoopChildren = await feedzyLoop.$$(':scope > *'); expect(feedzyLoopChildren.length).toBe(5); }); + + test('check validation for invalid URL', async ({ editor, page, admin }) => { + await admin.createNewPost(); + + await editor.insertBlock({ + name: 'feedzy-rss-feeds/loop', + }); + + await page.getByPlaceholder('Enter URLs or select a Feed').fill( + 'http://invalid-url.com/feed' + ); + await page.getByRole('button', { name: 'Load Feed' }).click(); + + await page.waitForSelector('.feedzy-validation-results'); + + await expect( page.locator('.feedzy-validation-results .is-error').getByText('http://invalid-url.com/feed', { exact: true }) ).toBeVisible(); + }); + + test('check validation for invalid and valid url', async ({ editor, page, admin }) => { + await admin.createNewPost(); + + await editor.insertBlock({ + name: 'feedzy-rss-feeds/loop', + }); + + await page.getByPlaceholder('Enter URLs or select a Feed').fill( + 'http://invalid-url.com/feed, https://www.nasa.gov/feeds/iotd-feed/' + ); + await page.getByRole('button', { name: 'Load Feed' }).click(); + + await page.waitForSelector('.feedzy-validation-results'); + + await expect( page.locator('.feedzy-validation-results .is-error').getByText('http://invalid-url.com/feed', { exact: true }) ).toBeVisible(); + + await expect( page.locator('.feedzy-validation-results .is-success').getByText('https://www.nasa.gov/feeds/iotd-feed/', { exact: true }) ).toBeVisible(); + }); });