diff --git a/.changeset/four-books-invent.md b/.changeset/four-books-invent.md new file mode 100644 index 000000000..21f3cec7f --- /dev/null +++ b/.changeset/four-books-invent.md @@ -0,0 +1,5 @@ +--- +'@faustwp/wordpress-plugin': patch +--- + +Fixed a bug that caused links to files in wp-content to be rewritten to the Faust Front-end site URL when they should not have been. diff --git a/.changeset/yellow-foxes-complain.md b/.changeset/yellow-foxes-complain.md new file mode 100644 index 000000000..98ec986b2 --- /dev/null +++ b/.changeset/yellow-foxes-complain.md @@ -0,0 +1,5 @@ +--- +'@faustwp/wordpress-plugin': patch +--- + +Fixed a bug where links were rewritten to the Faust Front-end Site URL when using the post editor, resulting in those rewritten links being saved to the post content and guid fields when they shouldn't be. These links are now saved with the URL pointing to the WP site, as they should be. They are still rewritten at runtime to link to the Front-end Site URL when appropriate. diff --git a/examples/next/app-router/package.json b/examples/next/app-router/package.json index a318149cb..b5dc72966 100644 --- a/examples/next/app-router/package.json +++ b/examples/next/app-router/package.json @@ -14,7 +14,7 @@ "@apollo/experimental-nextjs-app-support": "^0.5.1", "@faustwp/cli": "^1.2.0", "@faustwp/core": "^1.2.0", - "@faustwp/experimental-app-router": "^0.2.0", + "@faustwp/experimental-app-router": "^0.2.1", "graphql": "^16.7.1", "next": "^14.0.1", "react": "^18.3.0-canary-ce2bc58a9-20231102", diff --git a/package.json b/package.json index 4ef363c28..90f1d2a3e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "build:faust-cli": "npm run build --workspace=@faustwp/cli", "build:faust-core": "npm run build --workspace=@faustwp/core", "build:faust-blocks": "npm run build --workspace=@faustwp/blocks", - "build:experimentala-app-router": "npm run build --workspace=@faustwp/experimental-app-router", + "build:experimental-app-router": "npm run build --workspace=@faustwp/experimental-app-router", "clean": "npm run clean --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next --workspace=@faustwp/cli --workspace=@faustwp/core --workspace=@faustwp/experimental-app-router --workspace=@faustwp/block-editor-utils", "clean:examples": "rimraf examples/**/node_modules", "format": "npm run format --workspace=@faustjs/core --workspace=@faustjs/react --workspace=@faustjs/next --workspace=@faustwp/cli --workspace=@faustwp/core --workspace=@faustwp/experimental-app-router --workspace=@faustwp/block-editor-utils", @@ -54,7 +54,7 @@ "version": "changeset version && node scripts/versionPlugin.js", "version:nightly": "changeset version --snapshot && node scripts/versionPlugin.js", "version:status": "changeset status", - "release": "npm run build && changeset publish", + "release": "npm run build && npm run build:experimental-app-router && changeset publish", "release:nightly": "npm run build && changeset publish --tag canary", "lint": "eslint ./packages --ext js,jsx,ts,tsx --max-warnings=0", "lint:fix": "eslint ./packages --ext js,jsx,ts,tsx --max-warnings=0 --fix" diff --git a/packages/experimental-app-router/CHANGELOG.md b/packages/experimental-app-router/CHANGELOG.md index 5c52841f2..13eaf40fb 100644 --- a/packages/experimental-app-router/CHANGELOG.md +++ b/packages/experimental-app-router/CHANGELOG.md @@ -1,5 +1,11 @@ # @faustwp/experimental-app-router +## 0.2.1 + +### Patch Changes + +- 6276c80: Fix broken build from 0.2.0 + ## 0.2.0 ### Minor Changes diff --git a/packages/experimental-app-router/package.json b/packages/experimental-app-router/package.json index a0aa15799..292e8ec1d 100644 --- a/packages/experimental-app-router/package.json +++ b/packages/experimental-app-router/package.json @@ -1,6 +1,6 @@ { "name": "@faustwp/experimental-app-router", - "version": "0.2.0", + "version": "0.2.1", "description": "Experimental: A Faust package to support Next.js' App Router", "exports": { ".": "./dist/index.js", diff --git a/plugins/faustwp/includes/replacement/callbacks.php b/plugins/faustwp/includes/replacement/callbacks.php index b192afcc8..fc0bc7046 100644 --- a/plugins/faustwp/includes/replacement/callbacks.php +++ b/plugins/faustwp/includes/replacement/callbacks.php @@ -10,7 +10,9 @@ use function WPE\FaustWP\Settings\{ faustwp_get_setting, is_image_source_replacement_enabled, - is_rewrites_enabled + is_rewrites_enabled, + use_wp_domain_for_media, + use_wp_domain_for_post_and_category_urls, }; use function WPE\FaustWP\Utilities\{ plugin_version, @@ -21,32 +23,49 @@ } add_filter( 'the_content', __NAMESPACE__ . '\\content_replacement' ); +add_filter( 'wpgraphql_content_blocks_resolver_content', __NAMESPACE__ . '\\content_replacement' ); /** * Callback for WordPress 'the_content' filter. * * @param string $content The post content. * * @return string The post content. - * @todo Needs work... */ function content_replacement( $content ) { - if ( ! domain_replacement_enabled() ) { + $use_wp_domain_for_permalinks = ! domain_replacement_enabled(); + $use_wp_domain_for_media = use_wp_domain_for_media(); + + if ( $use_wp_domain_for_permalinks && $use_wp_domain_for_media ) { return $content; } $replacement = faustwp_get_setting( 'frontend_uri' ); - $site_url = site_url(); - if ( ! $replacement ) { $replacement = '/'; } - $content = str_replace( "href=\"{$site_url}", "href=\"{$replacement}", $content ); + $site_url = site_url(); + $media_dir = str_replace( $site_url, '', wp_upload_dir()['baseurl'] ); + $media_url = $site_url . $media_dir; + + if ( $use_wp_domain_for_permalinks && ! $use_wp_domain_for_media ) { + $content = str_replace( $media_url, $replacement . $media_dir, $content ); + return $content; + } + + if ( ! $use_wp_domain_for_permalinks && ! $use_wp_domain_for_media ) { + $content = str_replace( $site_url, $replacement, $content ); + return $content; + } + + if ( ! $use_wp_domain_for_permalinks && $use_wp_domain_for_media ) { + $content = preg_replace( "#{$site_url}(?!{$media_dir})#", "{$replacement}", $content ); + return $content; + } - return str_replace( 'href="//', 'href="/', $content ); + return $content; } -add_filter( 'the_content', __NAMESPACE__ . '\\image_source_replacement' ); /** * Callback for WordPress 'the_content' filter to replace paths to media. * @@ -79,21 +98,36 @@ function image_source_replacement( $content ) { * @return string One or more arrays of source data. */ function image_source_srcset_replacement( $sources ) { - if ( ! is_image_source_replacement_enabled() ) { - return $sources; - } + $use_wp_domain_for_media = use_wp_domain_for_media(); + $frontend_uri = faustwp_get_setting( 'frontend_uri' ); + $site_url = site_url(); + + /** + * For urls with no domain or the frontend domain, replace with the WP site_url. + * This was the default replacement pattern until Faust 1.2, at which point this + * was adjusted to correct replacement bugs. + */ + $patterns = array( + "#^{$site_url}/#", + '#^/#', + ); - $frontend_uri = faustwp_get_setting( 'frontend_uri' ); - $site_url = site_url(); + $replacement = $frontend_uri; - if ( is_array( $sources ) ) { - // For urls with no domain or the frontend domain, replace with the wp site_url. - $patterns = array( + /** + * If using WP domain for media and a frontend URL is encountered, rewrite it to WP URL. + */ + if ( $use_wp_domain_for_media ) { + $patterns = array( "#^{$frontend_uri}/#", '#^/#', ); + $replacement = $site_url; + } + + if ( is_array( $sources ) ) { foreach ( $sources as $width => $source ) { - $sources[ $width ]['url'] = preg_replace( $patterns, "$site_url/", $sources[ $width ]['url'] ); + $sources[ $width ]['url'] = preg_replace( $patterns, "$replacement/", $source['url'] ); } } @@ -200,13 +234,35 @@ function post_preview_link( $link, $post ) { * @return string URL used for the post. */ function post_link( $link ) { + global $pagenow; + $target_pages = array( 'admin-ajax.php', 'index.php', 'edit.php', 'post.php', 'post-new.php', 'upload.php', 'media-new.php' ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in `is_ajax_generate_permalink_request()` and `is_wp_link_ajax_request()`. + if ( empty( $_POST ) && 'post-new.php' === $pagenow ) { + return $link; + } + + // Ajax requests to generate permalink. + if ( in_array( $pagenow, $target_pages, true ) + && is_ajax_generate_permalink_request() + ) { + return $link; + } + if ( ! is_rewrites_enabled() || ( function_exists( 'is_graphql_request' ) && is_graphql_request() ) + // Block editor makes REST requests on these pages to query content. + || ( in_array( $pagenow, $target_pages, true ) && current_user_can( 'edit_posts' ) && defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { return $link; } + // Check for wp-link-ajax requests. Used by Classic Editor when linking content. + if ( is_wp_link_ajax_request() ) { + return $link; + } + return equivalent_frontend_url( $link ); } diff --git a/plugins/faustwp/includes/replacement/functions.php b/plugins/faustwp/includes/replacement/functions.php index 7b035f3e8..056a932b7 100644 --- a/plugins/faustwp/includes/replacement/functions.php +++ b/plugins/faustwp/includes/replacement/functions.php @@ -9,7 +9,8 @@ use function WPE\FaustWP\Settings\{ faustwp_get_setting, - is_rewrites_enabled + is_rewrites_enabled, + use_wp_domain_for_post_and_category_urls, }; if ( ! defined( 'ABSPATH' ) ) { @@ -19,8 +20,6 @@ /** * Determine if domain replacement can be done. * - * Enabled if query string parameter 'replace-domain' is present. - * * @return bool True if can proceed with replacement, false if else. */ function domain_replacement_enabled() { @@ -31,7 +30,7 @@ function domain_replacement_enabled() { * * @param bool $enabled True if domain replacement is enabled, false if else. */ - return apply_filters( 'faustwp_domain_replacement_enabled', is_rewrites_enabled() ); + return apply_filters( 'faustwp_domain_replacement_enabled', ! use_wp_domain_for_post_and_category_urls() ); } /** @@ -110,3 +109,25 @@ function has_file_extension( $string ) { return false; // String does not have a file extension. } } + +/** + * Determines if an AJAX request to generate permalinks is in progress. + * + * @return boolean + */ +function is_ajax_generate_permalink_request(): bool { + return ( ! empty( $_POST['samplepermalinknonce'] ) && check_ajax_referer( 'samplepermalink', 'samplepermalinknonce' ) ); +} + +/** + * Determines if a wp-link-ajax request is in progress. + * + * @return boolean + */ +function is_wp_link_ajax_request(): bool { + return ( wp_doing_ajax() + && ! empty( $_POST['_ajax_linking_nonce'] ) + && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_linking_nonce'] ) ), 'internal-linking' ) + && ! empty( $_POST['action'] ) + && 'wp-link-ajax' === $_POST['action'] ); +} diff --git a/plugins/faustwp/includes/replacement/graphql-callbacks.php b/plugins/faustwp/includes/replacement/graphql-callbacks.php index 8de262e10..e8397d88e 100644 --- a/plugins/faustwp/includes/replacement/graphql-callbacks.php +++ b/plugins/faustwp/includes/replacement/graphql-callbacks.php @@ -14,7 +14,6 @@ exit; } -add_filter( 'graphql_request_results', __NAMESPACE__ . '\\url_replacement' ); /** * Callback for WP GraphQL 'graphql_request_results' filter. * diff --git a/plugins/faustwp/includes/settings/functions.php b/plugins/faustwp/includes/settings/functions.php index 56aa42e89..ba2d643c1 100644 --- a/plugins/faustwp/includes/settings/functions.php +++ b/plugins/faustwp/includes/settings/functions.php @@ -29,6 +29,15 @@ function is_rewrites_enabled() { return '1' === faustwp_get_setting( 'enable_rewrites' ); } +/** + * Determines if posts and category URLs should link to the WP site. + * + * @return bool + */ +function use_wp_domain_for_post_and_category_urls() { + return ! is_rewrites_enabled(); +} + /** * Determine if themes are disabled. * @@ -47,6 +56,14 @@ function is_image_source_replacement_enabled() { return '1' === faustwp_get_setting( 'enable_image_source' ); } +/** + * Determine if sourcing images from WP domain is enabled. + * + * @return bool True if image sources from WP are enabled, false if else. + */ +function use_wp_domain_for_media() { + return is_image_source_replacement_enabled(); +} /** * Get the secret key setting. diff --git a/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php b/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php index 2364fca4a..8e4e57fcc 100644 --- a/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php +++ b/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php @@ -115,10 +115,6 @@ public function test_graphql_section_field_value() { $this->assertSame( 10, has_action( 'graphql_get_setting_section_field_value', 'WPE\FaustWP\GraphQL\filter_introspection' ) ); } - public function test_graphql_request_results_filter() { - $this->assertSame( 10, has_action( 'graphql_request_results', 'WPE\FaustWP\Replacement\url_replacement' ) ); - } - /** * Tests url_replacement() returns original data when rewrites are not enabled. */ diff --git a/plugins/faustwp/tests/integration/ReplacementCallbacksTests.php b/plugins/faustwp/tests/integration/ReplacementCallbacksTests.php index 922a020db..082cb58bf 100644 --- a/plugins/faustwp/tests/integration/ReplacementCallbacksTests.php +++ b/plugins/faustwp/tests/integration/ReplacementCallbacksTests.php @@ -64,10 +64,6 @@ public function test_term_link_filter() { $this->assertSame( 1000, has_action( 'term_link', 'WPE\FaustWP\Replacement\term_link' ) ); } - public function test_graphql_request_results_filter() { - $this->assertSame( 10, has_action( 'graphql_request_results', 'WPE\FaustWP\Replacement\url_replacement' ) ); - } - public function test_enqueue_preview_scripts_action() { $this->assertSame( 10, has_action( 'enqueue_block_editor_assets', 'WPE\FaustWP\Replacement\enqueue_preview_scripts' ) ); } @@ -84,6 +80,10 @@ public function test_wpseo_xml_sitemap_post_url_filter() { $this->assertSame( 10, has_action( 'wpseo_xml_sitemap_post_url', 'WPE\FaustWP\Replacement\yoast_sitemap_post_url' ) ); } + public function test_wp_calculate_image_srcset_filter(): void { + self::assertSame( 10, has_action( 'wp_calculate_image_srcset', 'WPE\FaustWP\Replacement\image_source_srcset_replacement' ) ); + } + /** * Tests content_replacement() returns original value when content replacement is not enabled. */ @@ -147,6 +147,9 @@ public function test_image_source_srcset_replacement_filters_content_when_image_ public function test_image_source_replacement_filters_content_when_image_replacement_not_enabled() { faustwp_update_setting( 'enable_image_source', '0' ); $this->assertSame( '', image_source_replacement( '' ) ); + + // Ensure unrelated domains are left alone. + $this->assertSame( '', image_source_replacement( '' ) ); } /** @@ -172,11 +175,51 @@ public function test_post_link_returns_unfiltered_link_when_content_replacement_ */ public function test_post_link_returns_filtered_link_when_content_replacement_is_enabled() { faustwp_update_setting( 'frontend_uri', 'http://moo' ); - faustwp_update_setting( 'enable_rewrites', true ); + faustwp_update_setting( 'enable_rewrites', '1' ); $this->assertSame( 'http://moo/?p=' . $this->post_id, get_permalink( $this->post_id ) ); } + public function test_post_link_returns_unfiltered_link_when_on_post_new_page(): void { + global $pagenow; + $pagenow = 'post-new.php'; + self::assertSame( 'http://example.org/hello-world', post_link( 'http://example.org/hello-world' ) ); + } + + public function test_post_link_returns_unfiltered_link_on_ajax_requests_to_generate_permalinks_using_samplepermalinknonce(): void { + global $pagenow, $_REQUEST, $_POST; + $pagenow = 'admin-ajax.php'; + wp_set_current_user( 1 ); + faustwp_update_setting( 'frontend_uri', 'http://moo' ); + faustwp_update_setting( 'enable_rewrites', '1' ); + $_REQUEST['samplepermalinknonce'] = wp_create_nonce( 'samplepermalink' ); + $_POST['samplepermalinknonce'] = $_REQUEST['samplepermalinknonce']; + + self::assertSame( 'http://example.org/hello-world', post_link( 'http://example.org/hello-world' ) ); + + unset( $_REQUEST['samplepermalinknonce'], $_POST['samplepermalinknonce'] ); + unset( $pagenow ); + wp_set_current_user( null ); + } + + public function test_post_link_returns_unfiltered_link_on_ajax_requests_to_generate_permalinks_using_ajax_linking_nonce(): void { + global $pagenow, $_POST; + $pagenow = 'admin-ajax.php'; + wp_set_current_user( 1 ); + faustwp_update_setting( 'frontend_uri', 'http://moo' ); + faustwp_update_setting( 'enable_rewrites', '1' ); + add_filter( 'wp_doing_ajax', '__return_true' ); + $_POST['_ajax_linking_nonce'] = wp_create_nonce( 'internal-linking' ); + $_POST['action'] = 'wp-link-ajax'; + + self::assertSame( 'http://example.org/hello-world', post_link( 'http://example.org/hello-world' ) ); + + unset( $_POST['_ajax_linking_nonce'], $_POST['action'] ); + unset( $pagenow ); + remove_filter( 'wp_doing_ajax', '__return_true' ); + wp_set_current_user( null ); + } + /** * Tests preview_link_in_rest_response() returns preview link in draft mode. */ @@ -227,7 +270,7 @@ public function test_post_preview_link_uses_frontend_uri_scheme() { public function test_custom_post_type_post_preview_link_returns_filtered_link_when_content_replacement_is_enabled() { faustwp_update_setting( 'frontend_uri', 'http://moo' ); - faustwp_update_setting( 'enable_rewrites', true ); + faustwp_update_setting( 'enable_rewrites', '1' ); $post_id = $this->getCustomPostType(); $this->assertSame( 'http://moo/?document=' . $post_id . '&preview=true&previewPathname=' . rawurlencode( wp_make_link_relative( get_permalink( $post_id ) ) ) . '&p=' . $post_id . '&typeName=Document', get_preview_post_link( $post_id ) ); faustwp_update_setting( 'frontend_uri', null ); @@ -240,7 +283,7 @@ public function test_custom_post_type_post_preview_link_returns_filtered_link_wh public function test_custom_post_type_post_link_returns_unfiltered_link_when_content_replacement_is_enabled() { faustwp_update_setting( 'frontend_uri', 'http://moo' ); - faustwp_update_setting( 'enable_rewrites', true ); + faustwp_update_setting( 'enable_rewrites', '1' ); $post_id = $this->getCustomPostType(); $this->assertSame( 'http://example.org/?document=' . $post_id, get_permalink($post_id) ); faustwp_update_setting( 'frontend_uri', null ); @@ -282,6 +325,44 @@ public function test_term_link_returns_filtered_link_when_rewrite_term_links_ena $this->assertSame( 'http://moo/?cat=' . $term_id, get_term_link( $term_id ) ); } + /** + * Tests content_replacement() handles mixed content blobs properly. + */ + public function test_content_replacement_properly_handles_a_mixed_content_blob() { + faustwp_update_setting( 'frontend_uri', 'http://moo' ); + faustwp_update_setting( 'enable_image_source', '1' ); + faustwp_update_setting( 'enable_rewrites', '1' ); + + $input = <<This is a post link.

This is a media link to a PDF file.

This is a media link to an image.

+ INPUT; + + $output = <<This is a post link.

This is a media link to a PDF file.

This is a media link to an image.

+ OUTPUT; + + self::assertSame( + $output, + content_replacement( $input ) + ); + + // Check that media URLs are rewritten when enable_image_source setting is configured to NOT use the WP domain. + faustwp_update_setting( 'enable_image_source', '0' ); + + $input = <<This is a post link.

This is a media link to a PDF file.

This is a media link to an image.

+ INPUT; + + $output = <<This is a post link.

This is a media link to a PDF file.

This is a media link to an image.

+ OUTPUT; + + self::assertSame( + $output, + content_replacement( $input ) + ); + } + private function getCustomPostType() { register_post_type('document', [ 'public' => true,