Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/js/_enqueues/wp/customize/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,33 @@ window.wp = window.wp || {};
return queryParams;
};

/**
* Whether a URL pathname begins with one of the given excluded path prefixes.
*
* @since 7.2.0
* @access public
*
* @alias wp.customize.utils.isExcludedPath
*
* @param {string} pathname URL pathname to test.
* @param {Array} [excludedPaths] URL path prefixes, each with a trailing slash.
* @return {boolean} Whether the pathname begins with an excluded path.
*/
api.utils.isExcludedPath = function isExcludedPath( pathname, excludedPaths ) {
var normalizedPathname;

if ( ! excludedPaths || ! excludedPaths.length ) {
return /\/wp-(admin|includes|content)(\/|$)/.test( pathname );
}

// Normalize so '/wp-admin' and '//wp-admin/' both match the '/wp-admin/' prefix.
normalizedPathname = ( pathname + '/' ).replace( /\/+/g, '/' );

return _.some( excludedPaths, function( excludedPath ) {
return 0 === normalizedPathname.indexOf( excludedPath );
} );
};

/**
* Expose the API publicly on window.wp.customize
*
Expand Down
21 changes: 13 additions & 8 deletions src/js/_enqueues/wp/customize/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -6534,6 +6534,9 @@
* @param {Array} params.allowedUrls
* @param {string} params.container A selector or jQuery element for the preview
* frame to be placed.
* @param {Array} [params.excludedPaths] URL path prefixes which are not previewable,
* such as the paths for the admin and content
* directories. Added in 7.2.0.
* @param {string} params.form
* @param {string} params.previewUrl The URL to preview.
* @param {Object} options
Expand Down Expand Up @@ -6571,8 +6574,9 @@
previewer.refreshBuffer
);

previewer.container = api.ensure( params.container );
previewer.allowedUrls = params.allowedUrls;
previewer.container = api.ensure( params.container );
previewer.allowedUrls = params.allowedUrls;
previewer.excludedPaths = params.excludedPaths;

params.url = window.location.href;

Expand All @@ -6596,8 +6600,8 @@
urlParser = document.createElement( 'a' );
urlParser.href = to;

// Abort if URL is for admin or (static) files in wp-includes or wp-content.
if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
// Abort if URL is for the admin or (static) files in wp-includes or wp-content.
if ( api.utils.isExcludedPath( urlParser.pathname, previewer.excludedPaths ) ) {
return null;
}

Expand Down Expand Up @@ -7461,10 +7465,11 @@
* @alias wp.customize.previewer
*/
api.previewer = new api.Previewer({
container: '#customize-preview',
form: '#customize-controls',
previewUrl: api.settings.url.preview,
allowedUrls: api.settings.url.allowed
container: '#customize-preview',
form: '#customize-controls',
previewUrl: api.settings.url.preview,
allowedUrls: api.settings.url.allowed,
excludedPaths: api.settings.url.excludedPaths
},/** @lends wp.customize.previewer */{

nonce: api.settings.nonce,
Expand Down
5 changes: 3 additions & 2 deletions src/js/_enqueues/wp/customize/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@
* Should the supplied link is previewable.
*
* @since 4.7.0
* @since 7.2.0 Non-previewable paths are read from settings instead of being hardcoded.
* @access public
*
* @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
Expand Down Expand Up @@ -314,8 +315,8 @@
return args.allowAdminAjax;
}

// Disallow links to admin, includes, and content.
if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) {
// Disallow links to admin, includes, content, plugins, and uploads paths, which are not previewable.
if ( api.utils.isExcludedPath( element.pathname, api.settings.url.excludedPaths ) ) {
return false;
}

Expand Down
48 changes: 48 additions & 0 deletions src/wp-includes/class-wp-customize-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,7 @@ public function customize_preview_settings() {
'self' => $self_url,
'allowed' => array_map( 'sanitize_url', $this->get_allowed_urls() ),
'allowedHosts' => array_unique( $allowed_hosts ),
'excludedPaths' => $this->get_excluded_url_paths(),
'isCrossDomain' => $this->is_cross_domain(),
),
'channel' => $this->messenger_channel,
Expand Down Expand Up @@ -4679,6 +4680,52 @@ public function get_allowed_urls() {
return $allowed_urls;
}

/**
* Gets URL path prefixes which are excluded from being previewed.
*
* @since 7.2.0
*
* @return string[] URL path prefixes, each with a trailing slash.
*/
public function get_excluded_url_paths() {
$excluded_urls = array(
admin_url( '/' ),
includes_url( '/' ),
content_url( '/' ),
plugins_url( '/' ),
wp_get_upload_dir()['baseurl'],
);

$parsed_home = wp_parse_url( home_url( '/' ) );
$home_host = strtolower( $parsed_home['host'] ?? '' ) . ':' . ( $parsed_home['port'] ?? '' );
$home_path = trailingslashit( $parsed_home['path'] ?? '/' );

$excluded_paths = array();
foreach ( $excluded_urls as $excluded_url ) {
$parsed = wp_parse_url( $excluded_url );
if ( ! is_array( $parsed ) ) {
continue;
}

// Skip URLs served from another host, such as an uploads URL pointing to a CDN.
$host = strtolower( $parsed['host'] ?? '' ) . ':' . ( $parsed['port'] ?? '' );
if ( $host !== $home_host ) {
continue;
}

$path = trailingslashit( $parsed['path'] ?? '' );

// A path at or above the home path would exclude the whole site from being previewed.
if ( str_starts_with( $home_path, $path ) ) {
continue;
}

$excluded_paths[] = $path;
}

return array_values( array_unique( $excluded_paths ) );
}

/**
* Gets messenger channel.
*
Expand Down Expand Up @@ -4938,6 +4985,7 @@ public function customize_pane_settings() {
'activated' => sanitize_url( home_url( '/' ) ),
'ajax' => sanitize_url( admin_url( 'admin-ajax.php', 'relative' ) ),
'allowed' => array_map( 'sanitize_url', $this->get_allowed_urls() ),
'excludedPaths' => $this->get_excluded_url_paths(),
'isCrossDomain' => $this->is_cross_domain(),
'home' => sanitize_url( home_url( '/' ) ),
'login' => sanitize_url( $login_url ),
Expand Down
130 changes: 130 additions & 0 deletions tests/phpunit/tests/customize/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,134 @@ public function filter_customize_allowed_urls( $urls ) {
return $urls;
}

/**
* Test WP_Customize_Manager::get_excluded_url_paths().
*
* @ticket 65030
* @covers WP_Customize_Manager::get_excluded_url_paths
*/
public function test_get_excluded_url_paths() {
$wp_customize = new WP_Customize_Manager();

$this->assertSame(
array(
'/wp-admin/',
'/wp-includes/',
'/wp-content/',
'/wp-content/plugins/',
'/wp-content/uploads/',
),
$wp_customize->get_excluded_url_paths()
);
}

/**
* Test WP_Customize_Manager::get_excluded_url_paths() for a subdirectory installation.
*
* @ticket 65030
* @covers WP_Customize_Manager::get_excluded_url_paths
*/
public function test_get_excluded_url_paths_with_subdirectory_install() {
$home_url = trailingslashit( home_url() );
$to_subdirectory = static function ( $url ) use ( $home_url ) {
return str_replace( $home_url, $home_url . 'wp-content/subsite/', $url );
};
add_filter( 'admin_url', $to_subdirectory );
add_filter( 'includes_url', $to_subdirectory );
add_filter( 'content_url', $to_subdirectory );
add_filter( 'plugins_url', $to_subdirectory );
add_filter(
'upload_dir',
static function ( $uploads ) use ( $to_subdirectory ) {
$uploads['baseurl'] = $to_subdirectory( $uploads['baseurl'] );
return $uploads;
}
);

$wp_customize = new WP_Customize_Manager();

// The site's own base path must not be excluded, only the WP directories under it.
$this->assertSame(
array(
'/wp-content/subsite/wp-admin/',
'/wp-content/subsite/wp-includes/',
'/wp-content/subsite/wp-content/',
'/wp-content/subsite/wp-content/plugins/',
'/wp-content/subsite/wp-content/uploads/',
),
$wp_customize->get_excluded_url_paths()
);
}

/**
* Test that WP_Customize_Manager::get_excluded_url_paths() never excludes the site root.
*
* @ticket 65030
* @covers WP_Customize_Manager::get_excluded_url_paths
*/
public function test_get_excluded_url_paths_skips_site_root() {
add_filter(
'content_url',
static function () {
return home_url( '/' );
}
);

$wp_customize = new WP_Customize_Manager();
$excluded = $wp_customize->get_excluded_url_paths();

$this->assertNotContains( '/', $excluded );
$this->assertNotContains( '', $excluded );
}

/**
* Test that WP_Customize_Manager::get_excluded_url_paths() never excludes an ancestor of the home path.
*
* @ticket 65030
* @covers WP_Customize_Manager::get_excluded_url_paths
*/
public function test_get_excluded_url_paths_skips_ancestors_of_home_path() {
// Simulate a site installed under wp-content whose content directory is the shared parent.
$home_url = trailingslashit( home_url() );
add_filter(
'home_url',
static function ( $url ) use ( $home_url ) {
return str_replace( $home_url, $home_url . 'wp-content/subsite/', $url );
}
);

$wp_customize = new WP_Customize_Manager();
$excluded = $wp_customize->get_excluded_url_paths();

// Excluding '/wp-content/' would exclude the whole site at '/wp-content/subsite/'.
$this->assertNotContains( '/wp-content/', $excluded );
$this->assertContains( '/wp-admin/', $excluded );
}

/**
* Test that WP_Customize_Manager::get_excluded_url_paths() skips URLs served from another host.
*
* @ticket 65030
* @covers WP_Customize_Manager::get_excluded_url_paths
*/
public function test_get_excluded_url_paths_skips_other_hosts() {
add_filter(
'upload_dir',
static function ( $uploads ) {
$uploads['baseurl'] = 'https://cdn.example.net/media';
return $uploads;
}
);

$wp_customize = new WP_Customize_Manager();
$excluded = $wp_customize->get_excluded_url_paths();

// The CDN path must not block the site's own '/media/' pages, and the CDN itself is not previewable.
$this->assertNotContains( '/media/', $excluded );
$this->assertNotContains( '/wp-content/uploads/', $excluded );
$this->assertContains( '/wp-content/', $excluded );
}

/**
* Test WP_Customize_Manager::doing_ajax().
*
Expand Down Expand Up @@ -3147,6 +3275,7 @@ public function test_customize_pane_settings() {
$this->assertSame( $autofocus, $data['autofocus'] );
$this->assertArrayHasKey( 'save', $data['nonce'] );
$this->assertArrayHasKey( 'preview', $data['nonce'] );
$this->assertSame( $this->manager->get_excluded_url_paths(), $data['url']['excludedPaths'] );

$this->assertSameSets(
array(
Expand Down Expand Up @@ -3218,6 +3347,7 @@ public function test_customize_preview_settings() {
$this->assertArrayHasKey( 'changeset', $settings );

$this->assertArrayHasKey( 'preview', $settings['nonce'] );
$this->assertSame( $this->manager->get_excluded_url_paths(), $settings['url']['excludedPaths'] );
}

/**
Expand Down
29 changes: 29 additions & 0 deletions tests/qunit/wp-admin/js/customize-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,33 @@ jQuery( function( $ ) {
queryParams = wp.customize.utils.parseQueryString( 'a=1&b=' );
assert.ok( _.isEqual( queryParams, { 'a': '1', b: '' } ) );
} );

QUnit.module( 'Customize Base: utils.isExcludedPath' );
QUnit.test( 'wp.customize.utils.isExcludedPath works', function( assert ) {
var excludedPaths = [ '/wp-admin/', '/wp-includes/', '/wp-content/' ];

// Excluded paths and everything beneath them match.
assert.ok( wp.customize.utils.isExcludedPath( '/wp-admin/', excludedPaths ) );
assert.ok( wp.customize.utils.isExcludedPath( '/wp-admin', excludedPaths ), 'bare directory without trailing slash matches' );
assert.ok( wp.customize.utils.isExcludedPath( '/wp-content/uploads/file.pdf', excludedPaths ) );
assert.ok( wp.customize.utils.isExcludedPath( '//wp-admin/', excludedPaths ), 'repeated slashes are collapsed' );

// Front-end paths do not match.
assert.ok( ! wp.customize.utils.isExcludedPath( '/', excludedPaths ) );
assert.ok( ! wp.customize.utils.isExcludedPath( '/sample-page/', excludedPaths ) );
assert.ok( ! wp.customize.utils.isExcludedPath( '/wp-admin2/', excludedPaths ), 'similarly named directory does not match' );
assert.ok( ! wp.customize.utils.isExcludedPath( '/blog/wp-content/', excludedPaths ), 'prefixes are anchored to the start of the path' );

// Subdirectory install: the site's own base path is not excluded (see #65030).
excludedPaths = [ '/wp-content/subsite/wp-admin/', '/wp-content/subsite/wp-content/' ];
assert.ok( ! wp.customize.utils.isExcludedPath( '/wp-content/subsite/', excludedPaths ) );
assert.ok( ! wp.customize.utils.isExcludedPath( '/wp-content/subsite/sample-page/', excludedPaths ) );
assert.ok( wp.customize.utils.isExcludedPath( '/wp-content/subsite/wp-admin/', excludedPaths ) );

// Without excluded paths, the legacy hardcoded paths are matched.
assert.ok( wp.customize.utils.isExcludedPath( '/wp-admin/', undefined ) );
assert.ok( wp.customize.utils.isExcludedPath( '/blog/wp-content/', undefined ), 'legacy fallback matches anywhere in the path' );
assert.ok( ! wp.customize.utils.isExcludedPath( '/sample-page/', undefined ) );
assert.ok( wp.customize.utils.isExcludedPath( '/wp-admin/', [] ), 'empty list falls back to legacy paths' );
} );
});
Loading