Introduce get_site_by_path() and further rewrite the site detection process for multisite.

This is the first big step to supporting arbitrary domains and paths. In this new approach, sites are detected first where possible, then the network is inferred. Allows filtering for arbitrary path segments, smooths out some weirdness, and removes various restrictions. A sunrise plugin could do much of its work by adding filters, if those are even needed.

see #27003.

Built from https://develop.svn.wordpress.org/trunk@27359


git-svn-id: http://core.svn.wordpress.org/trunk@27209 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Andrew Nacin 2014-03-02 22:25:14 +00:00
parent c0e436d3d3
commit ffc4ca7b01
3 changed files with 305 additions and 140 deletions

View File

@ -889,6 +889,8 @@ function populate_network( $network_id = 1, $domain = '', $email = '', $site_nam
$wpdb->insert( $wpdb->site, array( 'domain' => $domain, 'path' => $path, 'id' => $network_id ) );
}
wp_cache_delete( 'networks_have_paths', 'site-options' );
if ( !is_multisite() ) {
$site_admins = array( $site_user->user_login );
$users = get_users( array( 'fields' => array( 'ID', 'user_login' ) ) );

View File

@ -115,6 +115,8 @@ function ms_site_check() {
/**
* Sets current site name.
*
* @todo deprecate
*
* @access private
* @since 3.0.0
* @return object $current_site object with site_name
@ -138,15 +140,14 @@ function get_current_site_name( $current_site ) {
*
* @since 3.9.0
*
* @param string $domain Domain to check.
* @param string $path Path to check.
* @param string $domain Domain to check.
* @param string $path Path to check.
* @param int $segments Path segments to use. Defaults to null, or the full path.
* @return object|bool Network object if successful. False when no network is found.
*/
function get_network_by_path( $domain, $path ) {
function get_network_by_path( $domain, $path, $segments = null ) {
global $wpdb;
$network_id = false;
$domains = $exact_domains = array( $domain );
$pieces = explode( '.', $domain );
@ -158,18 +159,95 @@ function get_network_by_path( $domain, $path ) {
}
}
if ( '/' !== $path ) {
$paths = array( '/', $path );
} else {
$paths = array( '/' );
/*
* If we've gotten to this function during normal execution, there is
* more than one network installed. At this point, who knows how many
* we have. Attempt to optimize for the situation where networks are
* only domains, thus meaning paths never need to be considered.
*
* This is a very basic optimization; anything further could have drawbacks
* depending on the setup, so this is best done per-install.
*/
$using_paths = true;
if ( wp_using_ext_object_cache() ) {
$using_paths = wp_cache_get( 'networks_have_paths', 'site-options' );
if ( false === $using_paths ) {
$using_paths = (bool) $wpdb->get_var( "SELECT id FROM $wpdb->site WHERE path <> '/' LIMIT 1" );
wp_cache_add( 'networks_have_paths', (int) $using_paths, 'site-options' );
}
}
$search_domains = "'" . implode( "', '", $wpdb->_escape( $domains ) ) . "'";
$paths = "'" . implode( "', '", $wpdb->_escape( $paths ) ) . "'";
$paths = array();
if ( $using_paths ) {
$path_segments = array_filter( explode( '/', trim( $path, "/" ) ) );
$networks = $wpdb->get_results( "SELECT id, domain, path FROM $wpdb->site
WHERE domain IN ($search_domains) AND path IN ($paths)
ORDER BY CHAR_LENGTH(domain) DESC, CHAR_LENGTH(path) DESC" );
/**
* Filter the number of path segments to consider when searching for a site.
*
* @since 3.9.0
*
* @param mixed $segments The number of path segments to consider. WordPress by default looks at
* one path segment. The function default of null only makes sense when you
* know the requested path should match a network.
* @param string $domain The requested domain.
* @param string $path The requested path, in full.
*/
$segments = apply_filters( 'network_by_path_segments_count', $segments, $domain, $path );
if ( null !== $segments && count($path_segments ) > $segments ) {
$path_segments = array_slice( $path_segments, 0, $segments );
}
while ( count( $path_segments ) ) {
$paths[] = '/' . implode( '/', $path_segments ) . '/';
array_pop( $path_segments );
}
$paths[] = '/';
}
/**
* Determine a network by its domain and path.
*
* This allows one to short-circuit the default logic, perhaps by
* replacing it with a routine that is more optimal for your setup.
*
* Return null to avoid the short-circuit. Return false if no network
* can be found at the requested domain and path. Otherwise, return
* an object from wp_get_network().
*
* @since 3.9.0
*
* @param string $domain The requested domain.
* @param string $path The requested path, in full.
* @param mixed $segments The suggested number of paths to consult.
* Default null, meaning the entire path was to be consulted.
* @param array $paths The paths to search for, based on $path and $segments.
*/
$pre = apply_filters( 'pre_get_network_by_path', null, $domain, $path, $segments, $paths );
if ( null !== $pre ) {
return $pre;
}
// @todo Consider additional optimization routes, perhaps as an opt-in for plugins.
// We already have paths covered. What about how far domains should be drilled down (including www)?
$search_domains = "'" . implode( "', '", $wpdb->_escape( $domains ) ) . "'";
if ( ! $using_paths ) {
$network = $wpdb->get_row( "SELECT id, domain, path FROM $wpdb->site
WHERE domain IN ($search_domains) ORDER BY CHAR_LENGTH(domain) DESC LIMIT 1" );
if ( $network ) {
return wp_get_network( $network );
}
return false;
} else {
$search_paths = "'" . implode( "', '", $wpdb->_escape( $paths ) ) . "'";
$networks = $wpdb->get_results( "SELECT id, domain, path FROM $wpdb->site
WHERE domain IN ($search_domains) AND path IN ($search_paths)
ORDER BY CHAR_LENGTH(domain) DESC, CHAR_LENGTH(path) DESC" );
}
/*
* Domains are sorted by length of domain, then by length of path.
@ -179,7 +257,7 @@ function get_network_by_path( $domain, $path ) {
$found = false;
foreach ( $networks as $network ) {
if ( $network->domain === $domain || "www.$network->domain" === $domain ) {
if ( $network->path === $path ) {
if ( in_array( $network->path, $paths, true ) ) {
$found = true;
break;
}
@ -191,9 +269,7 @@ function get_network_by_path( $domain, $path ) {
}
if ( $found ) {
$network = wp_get_network( $network );
return $network;
return wp_get_network( $network );
}
return false;
@ -221,61 +297,95 @@ function wp_get_network( $network ) {
}
/**
* Sets current_site object.
*
* @access private
* @since 3.0.0
* @return object $current_site object
* @todo deprecate
*/
function wpmu_current_site() {
global $wpdb, $current_site, $domain, $path;
}
if ( empty( $current_site ) )
$current_site = new stdClass;
/**
* Retrieve a site object by its domain and path.
*
* @since 3.9.0
*
* @param string $domain Domain to check.
* @param string $path Path to check.
* @param int $segments Path segments to use. Defaults to null, or the full path.
* @return object|bool Site object if successful. False when no site is found.
*/
function get_site_by_path( $domain, $path, $segments = null ) {
global $wpdb;
// 1. If constants are defined, that's our network.
if ( defined( 'DOMAIN_CURRENT_SITE' ) && defined( 'PATH_CURRENT_SITE' ) ) {
$current_site->id = defined( 'SITE_ID_CURRENT_SITE' ) ? SITE_ID_CURRENT_SITE : 1;
$current_site->domain = DOMAIN_CURRENT_SITE;
$current_site->path = $path = PATH_CURRENT_SITE;
if ( defined( 'BLOG_ID_CURRENT_SITE' ) )
$current_site->blog_id = BLOG_ID_CURRENT_SITE;
elseif ( defined( 'BLOGID_CURRENT_SITE' ) ) // deprecated.
$current_site->blog_id = BLOGID_CURRENT_SITE;
$path_segments = array_filter( explode( '/', trim( $path, "/" ) ) );
// 2. Pull the network from cache, if possible.
} elseif ( ! $current_site = wp_cache_get( 'current_site', 'site-options' ) ) {
/**
* Filter the number of path segments to consider when searching for a site.
*
* @since 3.9.0
*
// 3. See if they have only one network.
$networks = $wpdb->get_col( "SELECT id FROM $wpdb->site LIMIT 2" );
* @param mixed $segments The number of path segments to consider. WordPress by default looks at
* one path segment following the network path. The function default of
* null only makes sense when you know the requested path should match a site.
* @param string $domain The requested domain.
* @param string $path The requested path, in full.
*/
$segments = apply_filters( 'site_by_path_segments_count', $segments, $domain, $path );
if ( count( $networks ) <= 1 ) {
$current_site = wp_get_network( $networks[0] );
$current_site->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id
FROM $wpdb->blogs WHERE domain = %s AND path = %s",
$current_site->domain, $current_site->path ) );
wp_cache_set( 'current_site', 'site-options' );
// 4. Multiple networks are in play. Determine which via domain and path.
} else {
// Find the first path segment.
$path = substr( $_SERVER['REQUEST_URI'], 0, 1 + strpos( $_SERVER['REQUEST_URI'], '/', 1 ) );
$current_site = get_network_by_path( $domain, $path );
// Option 1. We did not find anything.
if ( ! $current_site ) {
wp_load_translations_early();
wp_die( __( 'No site defined on this host. If you are the owner of this site, please check <a href="http://codex.wordpress.org/Debugging_a_WordPress_Network">Debugging a WordPress Network</a> for help.' ) );
}
}
if ( null !== $segments && count($path_segments ) > $segments ) {
$path_segments = array_slice( $path_segments, 0, $segments );
}
// Option 2. We found something. Load up site meta and return.
wp_load_core_site_options();
$current_site = get_current_site_name( $current_site );
return $current_site;
while ( count( $path_segments ) ) {
$paths[] = '/' . implode( '/', $path_segments ) . '/';
array_pop( $path_segments );
}
$paths[] = '/';
/**
* Determine a site by its domain and path.
*
* This allows one to short-circuit the default logic, perhaps by
* replacing it with a routine that is more optimal for your setup.
*
* Return null to avoid the short-circuit. Return false if no site
* can be found at the requested domain and path. Otherwise, return
* a site object.
*
* @since 3.9.0
*
* @param string $domain The requested domain.
* @param string $path The requested path, in full.
* @param mixed $segments The suggested number of paths to consult.
* Default null, meaning the entire path was to be consulted.
* @param array $paths The paths to search for, based on $path and $segments.
*/
$pre = apply_filters( 'pre_get_site_by_path', null, $domain, $path, $segments, $paths );
if ( null !== $pre ) {
return $pre;
}
// @todo
// get_blog_details(), caching, etc. Consider alternative optimization routes,
// perhaps as an opt-in for plugins, rather than using the pre_* filter.
// For example: The segments filter can expand or ignore paths.
// If persistent caching is enabled, we could query the DB for a path <> '/'
// then cache whether we can just always ignore paths.
if ( count( $paths ) > 1 ) {
$paths = "'" . implode( "', '", $wpdb->_escape( $paths ) ) . "'";
$site = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->blogs
WHERE domain = %s AND path IN ($paths) ORDER BY CHAR_LENGTH(path) DESC LIMIT 1", $domain ) );
} else {
$site = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->blogs WHERE domain = %s and path = %s", $domain, $paths[0] ) );
}
if ( $site ) {
// @todo get_blog_details()
return $site;
}
return false;
}
/**

View File

@ -22,109 +22,155 @@ ms_subdomain_constants();
if ( !isset( $current_site ) || !isset( $current_blog ) ) {
$domain = addslashes( $_SERVER['HTTP_HOST'] );
if ( false !== strpos( $domain, ':' ) ) {
if ( substr( $domain, -3 ) == ':80' ) {
$domain = substr( $domain, 0, -3 );
$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 );
} elseif ( substr( $domain, -4 ) == ':443' ) {
$domain = substr( $domain, 0, -4 );
$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 );
// Given the domain and path, let's try to identify the network and site.
// Usually, it's easier to query the site first, which declares its network.
// In limited situations, though, we either can or must find the network first.
$domain = strtolower( stripslashes( $_SERVER['HTTP_HOST'] ) );
if ( substr( $domain, -3 ) == ':80' ) {
$domain = substr( $domain, 0, -3 );
$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 );
} elseif ( substr( $domain, -4 ) == ':443' ) {
$domain = substr( $domain, 0, -4 );
$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 );
}
$path = stripslashes( $_SERVER['REQUEST_URI'] );
if ( is_admin() ) {
$path = preg_replace( '#(.*)/wp-admin/.*#', '$1/', $path );
}
list( $path ) = explode( '?', $path );
// If the network is defined in wp-config.php, we can simply use that.
if ( defined( 'DOMAIN_CURRENT_SITE' ) && defined( 'PATH_CURRENT_SITE' ) ) {
$current_site = new stdClass;
$current_site->id = defined( 'SITE_ID_CURRENT_SITE' ) ? SITE_ID_CURRENT_SITE : 1;
$current_site->domain = DOMAIN_CURRENT_SITE;
$current_site->path = PATH_CURRENT_SITE;
if ( defined( 'BLOG_ID_CURRENT_SITE' ) ) {
$current_site->blog_id = BLOG_ID_CURRENT_SITE;
} elseif ( defined( 'BLOGID_CURRENT_SITE' ) ) { // deprecated.
$current_site->blog_id = BLOGID_CURRENT_SITE;
}
if ( $current_site->domain === $domain && $current_site->path === $path ) {
$current_blog = get_site_by_path( $domain, $path );
} elseif ( '/' !== $current_site->path && $current_site->domain === $domain && 0 === strpos( $path, $current_site->path ) ) {
// If the current network has a path and also matches the domain and path of the request,
// we need to look for a site using the first path segment following the network's path.
$current_blog = get_site_by_path( $domain, $path, 1 + count( explode( '/', trim( $current_site->path, '/' ) ) ) );
} else {
wp_load_translations_early();
wp_die( __( 'Multisite only works without the port number in the URL.' ) );
// Otherwise, use the first path segment (as usual).
$current_blog = get_site_by_path( $domain, $path, 1 );
}
} elseif ( ! is_subdomain_install() ) {
/*
* A "subdomain" install can be re-interpreted to mean "can support any domain".
* If we're not dealing with one of these installs, then the important part is determing
* the network first, because we need the network's path to identify any sites.
*/
if ( ! $current_site = wp_cache_get( 'current_network', 'site-options' ) ) {
// Are there even two networks installed?
$one_network = $wpdb->get_row( "SELECT * FROM $wpdb->site LIMIT 2" ); // [sic]
if ( 1 === $wpdb->num_rows ) {
$current_site = wp_get_network( $one_network );
wp_cache_set( 'current_network', 'site-options' );
} elseif ( 0 === $wpdb->num_rows ) {
ms_not_installed();
}
}
if ( empty( $current_site ) ) {
$current_site = get_network_by_path( $domain, $path, 1 );
}
if ( empty( $current_site ) ) {
ms_not_installed();
} elseif ( $path === $current_site->path ) {
$current_blog = get_site_by_path( $domain, $path );
} else {
// Search the network path + one more path segment (on top of the network path).
$current_blog = get_site_by_path( $domain, $path, substr_count( $current_site->path, '/' ) );
}
} else {
// Find the site by the domain and at most the first path segment.
$current_blog = get_site_by_path( $domain, $path, 1 );
if ( $current_blog ) {
$current_site = wp_get_network( $current_blog->site_id ? $current_blog->site_id : 1 );
} else {
// If you don't have a site with the same domain/path as a network, you're pretty screwed, but:
$current_site = get_network_by_path( $domain, $path, 1 );
}
}
$domain = rtrim( $domain, '.' );
// The network declared by the site trumps any constants.
if ( $current_blog && $current_blog->site_id != $current_site->id ) {
$current_site = wp_get_network( $current_blog->site_id );
}
$path = preg_replace( '|([a-z0-9-]+.php.*)|', '', $_SERVER['REQUEST_URI'] );
$path = str_replace ( '/wp-admin/', '/', $path );
$path = preg_replace( '|(/[a-z0-9-]+?/).*|', '$1', $path );
// If we don't have a network by now, we have a problem.
if ( empty( $current_site ) ) {
ms_not_installed();
}
$current_site = wpmu_current_site();
// @todo What if the domain of the network doesn't match the current site?
$current_site->cookie_domain = $current_site->domain;
if ( 'www.' === substr( $current_site->cookie_domain, 0, 4 ) ) {
$current_site->cookie_domain = substr( $current_site->cookie_domain, 4 );
}
if ( ! isset( $current_site->blog_id ) )
$current_site->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s", $current_site->domain, $current_site->path ) );
if ( is_subdomain_install() ) {
$current_blog = wp_cache_get( 'current_blog_' . $domain, 'site-options' );
if ( !$current_blog ) {
$current_blog = get_blog_details( array( 'domain' => $domain ), false );
if ( $current_blog )
wp_cache_set( 'current_blog_' . $domain, $current_blog, 'site-options' );
}
if ( $current_blog && $current_blog->site_id != $current_site->id ) {
$current_site = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->site WHERE id = %d", $current_blog->site_id ) );
if ( ! isset( $current_site->blog_id ) )
$current_site->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s", $current_site->domain, $current_site->path ) );
} else
$blogname = substr( $domain, 0, strpos( $domain, '.' ) );
} else {
$blogname = htmlspecialchars( substr( $_SERVER[ 'REQUEST_URI' ], strlen( $path ) ) );
if ( false !== strpos( $blogname, '/' ) )
$blogname = substr( $blogname, 0, strpos( $blogname, '/' ) );
if ( false !== strpos( $blogname, '?' ) )
$blogname = substr( $blogname, 0, strpos( $blogname, '?' ) );
$reserved_blognames = array( 'page', 'comments', 'blog', 'wp-admin', 'wp-includes', 'wp-content', 'files', 'feed' );
if ( $blogname != '' && ! in_array( $blogname, $reserved_blognames ) && ! is_file( $blogname ) )
$path .= $blogname . '/';
$current_blog = wp_cache_get( 'current_blog_' . $domain . $path, 'site-options' );
if ( ! $current_blog ) {
$current_blog = get_blog_details( array( 'domain' => $domain, 'path' => $path ), false );
if ( $current_blog )
wp_cache_set( 'current_blog_' . $domain . $path, $current_blog, 'site-options' );
}
unset($reserved_blognames);
}
if ( ! defined( 'WP_INSTALLING' ) && is_subdomain_install() && ! is_object( $current_blog ) ) {
if ( defined( 'NOBLOGREDIRECT' ) ) {
$destination = NOBLOGREDIRECT;
if ( '%siteurl%' == $destination )
$destination = "http://" . $current_site->domain . $current_site->path;
// Figure out the current network's main site.
if ( ! isset( $current_site->blog_id ) ) {
if ( $current_blog && $current_blog->domain === $current_site->domain && $current_blog->path === $current_site->path ) {
$current_site->blog_id = $current_blog->blog_id;
} else {
$destination = 'http://' . $current_site->domain . $current_site->path . 'wp-signup.php?new=' . str_replace( '.' . $current_site->domain, '', $domain );
// @todo we should be able to cache the blog ID of a network's main site easily.
$current_site->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s",
$current_site->domain, $current_site->path ) );
}
header( 'Location: ' . $destination );
die();
}
if ( ! defined( 'WP_INSTALLING' ) ) {
if ( $current_site && ! $current_blog ) {
if ( $current_site->domain != $_SERVER[ 'HTTP_HOST' ] ) {
// If we haven't figured out our site, give up.
if ( empty( $current_blog ) ) {
if ( defined( 'WP_INSTALLING' ) ) {
$current_blog->blog_id = $blog_id = 1;
} elseif ( is_subdomain_install() ) {
// @todo This is only for an open registration subdomain network.
if ( defined( 'NOBLOGREDIRECT' ) ) {
if ( '%siteurl%' === NOBLOGREDIRECT ) {
$destination = "http://" . $current_site->domain . $current_site->path;
} else {
$destination = NOBLOGREDIRECT;
}
} else {
$destination = 'http://' . $current_site->domain . $current_site->path . 'wp-signup.php?new=' . str_replace( '.' . $current_site->domain, '', $domain );
}
header( 'Location: ' . $destination );
exit;
} else {
if ( 0 !== strcasecmp( $current_site->domain, $domain ) ) {
header( 'Location: http://' . $current_site->domain . $current_site->path );
exit;
}
$current_blog = get_blog_details( array( 'domain' => $current_site->domain, 'path' => $current_site->path ), false );
}
if ( ! $current_blog || ! $current_site )
ms_not_installed();
}
}
$blog_id = $current_blog->blog_id;
$public = $current_blog->public;
if ( empty( $current_blog->site_id ) )
if ( empty( $current_blog->site_id ) ) {
// This dates to [MU134] and shouldn't be relevant anymore,
// but it could be possible for arguments passed to insert_blog() etc.
$current_blog->site_id = 1;
$site_id = $current_blog->site_id;
$current_site = get_current_site_name( $current_site );
if ( ! $blog_id ) {
if ( defined( 'WP_INSTALLING' ) ) {
$current_blog->blog_id = $blog_id = 1;
} else {
wp_load_translations_early();
$msg = ! $wpdb->get_var( "SHOW TABLES LIKE '$wpdb->site'" ) ? ' ' . __( 'Database tables are missing.' ) : '';
wp_die( __( 'No site by that name on this system.' ) . $msg );
}
}
$site_id = $current_blog->site_id;
wp_load_core_site_options( $site_id );
}
$wpdb->set_prefix( $table_prefix, false ); // $table_prefix can be set in sunrise.php
$wpdb->set_blog_id( $current_blog->blog_id, $current_blog->site_id );
$table_prefix = $wpdb->get_blog_prefix();
@ -134,5 +180,12 @@ $switched = false;
// need to init cache again after blog_id is set
wp_start_object_cache();
if ( ! isset( $current_site->site_name ) ) {
$current_site->site_name = get_site_option( 'site_name' );
if ( ! $current_site->site_name ) {
$current_site->site_name = ucfirst( $current_site->domain );
}
}
// Define upload directory constants
ms_upload_constants();