Skip to content

Commit

Permalink
Feature: Error when NEXT_PUBLIC_WORDPRESS_URL same as headless site U…
Browse files Browse the repository at this point in the history
…RL (#1809)

* Feature: Error when NEXT_PUBLIC_WORDPRESS_URL pointing to headless site

* Update packages/faustwp-cli/src/healthCheck/validateNextWordPressUrl.ts

Co-authored-by: John Parris <john.parris@wpengine.com>

* Chore: Fix PHP Lint issues

* Chore: PHP Lint issues

* Feature: handle older versions of FaustWP Plugin

* Tests: Add unit tests for domains_match

* PHPCS: Fix

* PHPCS: Lint issues

* Chore: Remove WP_Rest_Response messages.

* Feat: Only perform the check on valid secret key

---------

Co-authored-by: John Parris <john.parris@wpengine.com>
  • Loading branch information
theodesp and mindctrl committed Mar 7, 2024
1 parent 259d926 commit 4724719
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/modern-tools-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@faustwp/cli': patch
'@faustwp/wordpress-plugin': patch
---

Faust now errors if the NEXT_PUBLIC_WORDPRESS_URL matches the Headless URL in Faust Plugin settings.
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/faustwp-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"jest-environment-jsdom": "29.6.4",
"rimraf": "5.0.5",
"ts-jest": "^29.1.1",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"fetch-mock-jest": "^1.5.1"
},
"dependencies": {
"archiver": "^6.0.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/faustwp-cli/src/healthCheck/validateFaustEnvVars.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getWpSecret, getWpUrl } from '../utils/index.js';
import { errorLog, infoLog, warnLog } from '../stdout/index.js';
import { validateNextWordPressUrl } from './validateNextWordPressUrl.js';

export function isWPEngineComSubdomain(url: string) {
const regex = /\b\w+\.wpengine\.com\b/;
Expand Down Expand Up @@ -63,6 +64,7 @@ export const validateFaustEnvVars = async () => {
);
process.exit(1);
}
await validateNextWordPressUrl();
} catch (error) {
console.log('error', error);
}
Expand Down
41 changes: 41 additions & 0 deletions packages/faustwp-cli/src/healthCheck/validateNextWordPressUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getWpSecret, getWpUrl } from '../utils/index.js';
import { errorLog, warnLog } from '../stdout/index.js';

/**
* Validates the NEXT_PUBLIC_WORDPRESS_URL environment variable by sending a POST request to the Faust Plugin API.
* If the URL matches the Faust Plugin Headless URL, the validation fails, and an error is logged.
*/
export async function validateNextWordPressUrl(): Promise<void> {
const apiUrl = `${getWpUrl()}/wp-json/faustwp/v1/validate_public_wordpress_url`;
const headers = {
'Content-Type': 'application/json',
'x-faustwp-secret': getWpSecret() || '',
};

const postData = {
public_wordpress_url: getWpUrl(),
};
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify(postData),
});

if (!response.ok) {
if (response.status === 404) {
// Handle the case when the route does not exist
warnLog(
'Route not found: Please update your FaustWP plugin to the latest version.',
);
} else {
errorLog(
'Validation Failed: Your Faust front-end site URL value is misconfigured. It should NOT match the `NEXT_PUBLIC_WORDPRESS_URL.`',
);
process.exit(1);
}
}
} catch (error) {
console.log('error', error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import {
isWPEngineComSubdomain,
validateFaustEnvVars,
} from '../../src/healthCheck/validateFaustEnvVars';
import fetchMock from 'fetch-mock';

import fetchMock from 'fetch-mock-jest';
/**
* @jest-environment jsdom
*/
Expand Down Expand Up @@ -56,19 +55,22 @@ describe('healthCheck/validateFaustEnvVars', () => {
});

it('logs an error when the secret key validation fails', async () => {

process.env.NEXT_PUBLIC_WORDPRESS_URL = 'https://headless.local';
process.env.FAUST_SECRET_KEY = 'invalid-secret-key';

fetchMock.post('https://headless.local/wp-json/faustwp/v1/validate_secret_key', {
status: 401,
});

fetchMock.post(
'https://headless.local/wp-json/faustwp/v1/validate_secret_key',
{
status: 401,
},
);

await validateFaustEnvVars();

return expect(Promise.resolve(validateFaustEnvVars())).toMatchSnapshot(`Ensure your FAUST_SECRET_KEY environment variable matches your Secret Key in the Faust WordPress plugin settings`);
return expect(Promise.resolve(validateFaustEnvVars())).toMatchSnapshot(
`Ensure your FAUST_SECRET_KEY environment variable matches your Secret Key in the Faust WordPress plugin settings`,
);
});

});

describe('isWPEngineComTLD', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import fetchMock from 'fetch-mock-jest';
import { validateNextWordPressUrl } from '../../src/healthCheck/validateNextWordPressUrl';
/**
* @jest-environment jsdom
*/
describe('healthCheck/validateNextWordPressUrl', () => {
const envBackup = process.env;

beforeEach(() => {
process.env = { ...envBackup };
});

afterEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
process.env = envBackup;
});

it('exits with a 1 exit code when the WordPress URL matches the Headless URL', async () => {
// @ts-ignore
const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
if (code && code !== 0) {
throw new Error(`Exit code: ${code}`);
}
});
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();

process.env.NEXT_PUBLIC_WORDPRESS_URL = 'https://headless.local';
process.env.FAUST_SECRET_KEY = 'invalid-secret-key';

const headers = {
'Content-Type': 'application/json',
'x-faustwp-secret': process.env.FAUST_SECRET_KEY,
};

fetchMock.post(
'https://headless.local/wp-json/faustwp/v1/validate_public_wordpress_url',
{
headers,
body: JSON.stringify({
public_wordpress_url: process.env.NEXT_PUBLIC_WORDPRESS_URL,
}),
status: 400,
},
);

await validateNextWordPressUrl();
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Validation Failed: Your Faust front-end site URL value is misconfigured. It should NOT match the `NEXT_PUBLIC_WORDPRESS_URL.',
),
);
expect(mockExit).toHaveBeenCalledWith(1);
expect(fetchMock).toHaveFetched(
'https://headless.local/wp-json/faustwp/v1/validate_public_wordpress_url',
);

consoleLogSpy.mockClear();
});

it('continues silently when the route does not exist', async () => {
// @ts-ignore
const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
if (code && code !== 0) {
throw new Error(`Exit code: ${code}`);
}
});
// Mock environment variables
process.env.NEXT_PUBLIC_WORDPRESS_URL = 'http://mysite.local';
process.env.FAUST_SECRET_KEY = 'e9d5963e-bb41-4c94-a3f3-292e8903d5ea';

const headers = {
'Content-Type': 'application/json',
'x-faustwp-secret': process.env.FAUST_SECRET_KEY,
};

fetchMock.postOnce(
'http://mysite.local/wp-json/faustwp/v1/validate_public_wordpress_url',
{
status: 404,
},
);

const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();

await validateNextWordPressUrl();

expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Route not found: Please update your FaustWP plugin to the latest version.',
),
);

expect(mockExit).not.toHaveBeenCalled()
expect(fetchMock).toHaveFetched(
'http://mysite.local/wp-json/faustwp/v1/validate_public_wordpress_url',
);

consoleLogSpy.mockRestore();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import 'isomorphic-fetch';
import fetchMock from 'fetch-mock';
import fetchMock from 'fetch-mock-jest';
import { verifyGraphQLEndpoint } from '../../src/healthCheck/verifyGraphQLEndpoint.js';
import { getGraphqlEndpoint } from '../../src/utils/index.js';

Expand Down
51 changes: 51 additions & 0 deletions plugins/faustwp/includes/rest/callbacks.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use function WPE\FaustWP\Settings\faustwp_get_setting;
use function WPE\FaustWP\Settings\faustwp_update_setting;
use function WPE\FaustWP\Settings\is_telemetry_enabled;
use function WPE\FaustWP\Utilities\domains_match;

if ( ! defined( 'ABSPATH' ) ) {
exit;
Expand Down Expand Up @@ -139,6 +140,16 @@ function register_rest_routes() {
)
);

register_rest_route(
'faustwp/v1',
'/validate_public_wordpress_url',
array(
'methods' => 'POST',
'callback' => __NAMESPACE__ . '\\handle_rest_validate_public_wordpress_url_callback',
'permission_callback' => __NAMESPACE__ . '\\rest_authorize_permission_callback',
)
);

/**
* Faust.js packages now use `faustwp/v1/authorize`.
*
Expand Down Expand Up @@ -523,3 +534,43 @@ function handle_rest_validate_secret_key_callback( \WP_REST_Request $request ) {
function rest_validate_secret_key_permission_callback( \WP_REST_Request $request ) {
return rest_authorize_permission_callback( $request );
}

/**
* Callback for WordPress register_rest_route() 'callback' parameter.
*
* Handle POST /faustwp/v1/validate_public_wordpress_url response.
*
* @link https://developer.wordpress.org/reference/functions/register_rest_route/
* @link https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/#endpoint-callback
*
* @param \WP_REST_Request $request Current \WP_REST_Request object.
*
* @return mixed A \WP_REST_Response, or \WP_Error.
*/
function handle_rest_validate_public_wordpress_url_callback( \WP_REST_Request $request ) {
// Get the frontend URI setting from WordPress.
$frontend_uri = faustwp_get_setting( 'frontend_uri' );

// Retrieve the parameters from the request.
$parameters = $request->get_params();

// Check if the public_wordpress_url parameter is present in the request.
if ( isset( $parameters['public_wordpress_url'] ) ) {
// Retrieve the value of the public_wordpress_url parameter.
$public_wordpress_url = $parameters['public_wordpress_url'];

// Check if the provided WordPress URL does not match the frontend URI.
if ( ! domains_match( $public_wordpress_url, $frontend_uri ) ) {
// Return 200 OK if the URLs do not match.
$response = new \WP_REST_Response( 'OK', 200 );
} else {
// Return 400 Bad Request if the URLs match.
$response = new \WP_REST_Response( 'Bad Request', 400 );
}
} else {
// Return 400 Bad Request if the public_wordpress_url parameter is missing.
$response = new \WP_REST_Response( 'Bad Request', 400 );
}

return $response;
}
24 changes: 24 additions & 0 deletions plugins/faustwp/includes/utilities/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,27 @@ function plugin_version() {

return $plugin['Version'];
}

/**
* Checks if two domain strings represent the same domain.
*
* @param string $domain1 The first domain string.
* @param string $domain2 The second domain string.
* @return bool True if the domains match, false otherwise.
*/
function domains_match( $domain1, $domain2 ) {
// Extract the domain part.
$extract_domain = function ( $url ) {
$parsed_url = wp_parse_url( $url, PHP_URL_HOST );
return $parsed_url ? $parsed_url : null;
};

$domain1 = $extract_domain( $domain1 );
$domain2 = $extract_domain( $domain2 );

// Remove "www" prefix from domain if present.
$domain1 = preg_replace( '/^www\./i', '', $domain1 );
$domain2 = preg_replace( '/^www\./i', '', $domain2 );

return null !== $domain1 && null !== $domain2 && $domain1 === $domain2;
}
24 changes: 24 additions & 0 deletions plugins/faustwp/tests/unit/FunctionsTests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace WPE\FaustWP\Tests\Unit;

use function WPE\FaustWP\Utilities\{domains_match};

class FunctionsTests extends FaustUnitTest {
public function test_domains_match() {
// Test case 1: Same domains with different protocols
$this->assertTrue(domains_match("http://example.com", "https://example.com"));

// Test case 2: Same domains with trailing slashes
$this->assertTrue(domains_match("http://example.com/", "http://example.com"));

// Test case 3: Same domains with www prefix
$this->assertTrue(domains_match("http://www.example.com", "http://example.com"));

// Test case 4: Different domains
$this->assertFalse(domains_match("http://example1.com", "http://example2.com"));

// Test case 5: Same domains with different subdomains
$this->assertFalse(domains_match("http://1.example.com", "http://2.example.com"));
}
}

0 comments on commit 4724719

Please sign in to comment.