Multisite: Introduce metadata for sites.

A new global multisite table `wp_blogmeta` is added to the database schema, and a set of `*_site_meta()` API functions are introduced.

The implementation fails gracefully when the new table is not yet available, which may happen especially shortly after the core update, before the network has been upgraded to the new database schema. The presence of the table is detected once and stored as a global setting on the main network.

Core does not yet use site metadata, but there are several use-cases to be implemented or explored in the near future, and it allows plugins to extend sites with arbitrary data, which will come in particularly handy with the upcoming REST API endpoint for sites.

Props spacedmonkey, johnjamesjacoby, jeremyfelt, flixos90.
Fixes #37923.

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


git-svn-id: http://core.svn.wordpress.org/trunk@42666 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Felix Arntz 2018-03-16 02:15:31 +00:00
parent eda5ab56af
commit 176a289050
9 changed files with 328 additions and 84 deletions

View File

@ -135,6 +135,13 @@ function wpmu_delete_blog( $blog_id, $drop = false ) {
$wpdb->query( "DROP TABLE IF EXISTS `$table`" ); $wpdb->query( "DROP TABLE IF EXISTS `$table`" );
} }
if ( is_site_meta_supported() ) {
$blog_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->blogmeta WHERE blog_id = %d ", $blog_id ) );
foreach ( $blog_meta_ids as $mid ) {
delete_metadata_by_mid( 'blog', $mid );
}
}
$wpdb->delete( $wpdb->blogs, array( 'blog_id' => $blog_id ) ); $wpdb->delete( $wpdb->blogs, array( 'blog_id' => $blog_id ) );
/** /**

View File

@ -267,6 +267,15 @@ CREATE TABLE $wpdb->blog_versions (
PRIMARY KEY (blog_id), PRIMARY KEY (blog_id),
KEY db_version (db_version) KEY db_version (db_version)
) $charset_collate; ) $charset_collate;
CREATE TABLE $wpdb->blogmeta (
meta_id bigint(20) unsigned NOT NULL auto_increment,
blog_id bigint(20) NOT NULL default '0',
meta_key varchar(255) default NULL,
meta_value longtext,
PRIMARY KEY (meta_id),
KEY meta_key (meta_key($max_index_length)),
KEY blog_id (blog_id)
) $charset_collate;
CREATE TABLE $wpdb->registration_log ( CREATE TABLE $wpdb->registration_log (
ID bigint(20) NOT NULL auto_increment, ID bigint(20) NOT NULL auto_increment,
email varchar(255) NOT NULL default '', email varchar(255) NOT NULL default '',

View File

@ -2133,6 +2133,13 @@ function upgrade_network() {
} }
} }
} }
// 5.0
if ( $wp_current_db_version < 42836 ) {
$network_id = get_main_network_id();
delete_network_option( $network_id, 'site_meta_supported' );
is_site_meta_supported();
}
} }
// //

View File

@ -92,6 +92,7 @@ class WP_Site_Query {
* *
* @since 4.6.0 * @since 4.6.0
* @since 4.8.0 Introduced the 'lang_id', 'lang__in', and 'lang__not_in' parameters. * @since 4.8.0 Introduced the 'lang_id', 'lang__in', and 'lang__not_in' parameters.
* @since 5.0.0 Introduced the 'update_site_meta_cache' parameter.
* *
* @param string|array $query { * @param string|array $query {
* Optional. Array or query string of site query parameters. Default empty. * Optional. Array or query string of site query parameters. Default empty.
@ -137,6 +138,7 @@ class WP_Site_Query {
* @type array $search_columns Array of column names to be searched. Accepts 'domain' and 'path'. * @type array $search_columns Array of column names to be searched. Accepts 'domain' and 'path'.
* Default empty array. * Default empty array.
* @type bool $update_site_cache Whether to prime the cache for found sites. Default true. * @type bool $update_site_cache Whether to prime the cache for found sites. Default true.
* @type bool $update_site_meta_cache Whether to prime the metadata cache for found sites. Default true.
* } * }
*/ */
public function __construct( $query = '' ) { public function __construct( $query = '' ) {
@ -172,6 +174,7 @@ class WP_Site_Query {
'count' => false, 'count' => false,
'date_query' => null, // See WP_Date_Query 'date_query' => null, // See WP_Date_Query
'update_site_cache' => true, 'update_site_cache' => true,
'update_site_meta_cache' => true,
); );
if ( ! empty( $query ) ) { if ( ! empty( $query ) ) {
@ -288,7 +291,7 @@ class WP_Site_Query {
// Prime site network caches. // Prime site network caches.
if ( $this->query_vars['update_site_cache'] ) { if ( $this->query_vars['update_site_cache'] ) {
_prime_site_caches( $site_ids ); _prime_site_caches( $site_ids, $this->query_vars['update_site_meta_cache'] );
} }
// Fetch full site objects from the primed cache. // Fetch full site objects from the primed cache.

View File

@ -4726,6 +4726,38 @@ function global_terms_enabled() {
return $global_terms; return $global_terms;
} }
/**
* Determines whether site meta is enabled.
*
* This function checks whether the 'blogmeta' database table exists. The result is saved as
* a setting for the main network, making it essentially a global setting. Subsequent requests
* will refer to this setting instead of running the query.
*
* @since 5.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @return bool True if site meta is supported, false otherwise.
*/
function is_site_meta_supported() {
global $wpdb;
if ( ! is_multisite() ) {
return false;
}
$network_id = get_main_network_id();
$supported = get_network_option( $network_id, 'site_meta_supported', false );
if ( false === $supported ) {
$supported = $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->blogmeta}'" ) ? 1 : 0;
update_network_option( $network_id, 'site_meta_supported', $supported );
}
return (bool) $supported;
}
/** /**
* gmt_offset modification for smart timezone handling. * gmt_offset modification for smart timezone handling.
* *

View File

@ -581,7 +581,7 @@ function wp_start_object_cache() {
} }
if ( function_exists( 'wp_cache_add_global_groups' ) ) { if ( function_exists( 'wp_cache_add_global_groups' ) ) {
wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'blog-lookup', 'blog-details', 'site-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites' ) ); wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'blog-lookup', 'blog-details', 'site-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'blog_meta' ) );
wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) ); wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) );
} }

View File

@ -474,6 +474,7 @@ function clean_blog_cache( $blog ) {
wp_cache_delete( $domain_path_key, 'blog-id-cache' ); wp_cache_delete( $domain_path_key, 'blog-id-cache' );
wp_cache_delete( 'current_blog_' . $blog->domain, 'site-options' ); wp_cache_delete( 'current_blog_' . $blog->domain, 'site-options' );
wp_cache_delete( 'current_blog_' . $blog->domain . $blog->path, 'site-options' ); wp_cache_delete( 'current_blog_' . $blog->domain . $blog->path, 'site-options' );
wp_cache_delete( $blog_id, 'blog_meta' );
/** /**
* Fires immediately after a site has been removed from the object cache. * Fires immediately after a site has been removed from the object cache.
@ -560,21 +561,23 @@ function get_site( $site = null ) {
* Adds any sites from the given ids to the cache that do not already exist in cache. * Adds any sites from the given ids to the cache that do not already exist in cache.
* *
* @since 4.6.0 * @since 4.6.0
* @since 5.0.0 Introduced the `$update_meta_cache` parameter.
* @access private * @access private
* *
* @see update_site_cache() * @see update_site_cache()
* @global wpdb $wpdb WordPress database abstraction object. * @global wpdb $wpdb WordPress database abstraction object.
* *
* @param array $ids ID list. * @param array $ids ID list.
* @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true.
*/ */
function _prime_site_caches( $ids ) { function _prime_site_caches( $ids, $update_meta_cache = true ) {
global $wpdb; global $wpdb;
$non_cached_ids = _get_non_cached_ids( $ids, 'sites' ); $non_cached_ids = _get_non_cached_ids( $ids, 'sites' );
if ( ! empty( $non_cached_ids ) ) { if ( ! empty( $non_cached_ids ) ) {
$fresh_sites = $wpdb->get_results( sprintf( "SELECT * FROM $wpdb->blogs WHERE blog_id IN (%s)", join( ',', array_map( 'intval', $non_cached_ids ) ) ) ); $fresh_sites = $wpdb->get_results( sprintf( "SELECT * FROM $wpdb->blogs WHERE blog_id IN (%s)", join( ',', array_map( 'intval', $non_cached_ids ) ) ) );
update_site_cache( $fresh_sites ); update_site_cache( $fresh_sites, $update_meta_cache );
} }
} }
@ -582,18 +585,44 @@ function _prime_site_caches( $ids ) {
* Updates sites in cache. * Updates sites in cache.
* *
* @since 4.6.0 * @since 4.6.0
* @since 5.0.0 Introduced the `$update_meta_cache` parameter.
* *
* @param array $sites Array of site objects. * @param array $sites Array of site objects.
* @param bool $update_meta_cache Whether to update site meta cache. Default true.
*/ */
function update_site_cache( $sites ) { function update_site_cache( $sites, $update_meta_cache = true ) {
if ( ! $sites ) { if ( ! $sites ) {
return; return;
} }
$site_ids = array();
foreach ( $sites as $site ) { foreach ( $sites as $site ) {
$site_ids[] = $site->blog_id;
wp_cache_add( $site->blog_id, $site, 'sites' ); wp_cache_add( $site->blog_id, $site, 'sites' );
wp_cache_add( $site->blog_id . 'short', $site, 'blog-details' ); wp_cache_add( $site->blog_id . 'short', $site, 'blog-details' );
} }
if ( $update_meta_cache ) {
update_sitemeta_cache( $site_ids );
}
}
/**
* Updates metadata cache for list of site IDs.
*
* Performs SQL query to retrieve all metadata for the sites matching `$site_ids` and stores them in the cache.
* Subsequent calls to `get_site_meta()` will not need to query the database.
*
* @since 5.0.0
*
* @param array $site_ids List of site IDs.
* @return array|false Returns false if there is nothing to update. Returns an array of metadata on success.
*/
function update_sitemeta_cache( $site_ids ) {
if ( ! is_site_meta_supported() ) {
return false;
}
return update_meta_cache( 'blog', $site_ids );
} }
/** /**
@ -796,6 +825,154 @@ function update_blog_option( $id, $option, $value, $deprecated = null ) {
return $return; return $return;
} }
/**
* Adds metadata to a site.
*
* @since 5.0.0
*
* @param int $site_id Site ID.
* @param string $meta_key Metadata name.
* @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
* @param bool $unique Optional. Whether the same key should not be added.
* Default false.
* @return int|false Meta ID on success, false on failure.
*/
function add_site_meta( $site_id, $meta_key, $meta_value, $unique = false ) {
// Bail if site meta table is not installed.
if ( ! is_site_meta_supported() ) {
/* translators: %s: database table name */
_doing_it_wrong( __FUNCTION__, sprintf( __( 'The %s table is not installed. Please run the network database upgrade.' ), $GLOBALS['wpdb']->blogmeta ), '5.0.0' );
return false;
}
$added = add_metadata( 'blog', $site_id, $meta_key, $meta_value, $unique );
// Bust site query cache.
if ( $added ) {
wp_cache_set( 'last_changed', microtime(), 'sites' );
}
return $added;
}
/**
* Removes metadata matching criteria from a site.
*
* You can match based on the key, or key and value. Removing based on key and
* value, will keep from removing duplicate metadata with the same key. It also
* allows removing all metadata matching key, if needed.
*
* @since 5.0.0
*
* @param int $site_id Site ID.
* @param string $meta_key Metadata name.
* @param mixed $meta_value Optional. Metadata value. Must be serializable if
* non-scalar. Default empty.
* @return bool True on success, false on failure.
*/
function delete_site_meta( $site_id, $meta_key, $meta_value = '' ) {
// Bail if site meta table is not installed.
if ( ! is_site_meta_supported() ) {
/* translators: %s: database table name */
_doing_it_wrong( __FUNCTION__, sprintf( __( 'The %s table is not installed. Please run the network database upgrade.' ), $GLOBALS['wpdb']->blogmeta ), '5.0.0' );
return false;
}
$deleted = delete_metadata( 'blog', $site_id, $meta_key, $meta_value );
// Bust site query cache.
if ( $deleted ) {
wp_cache_set( 'last_changed', microtime(), 'sites' );
}
return $deleted;
}
/**
* Retrieves metadata for a site.
*
* @since 5.0.0
*
* @param int $site_id Site ID.
* @param string $key Optional. The meta key to retrieve. By default, returns
* data for all keys. Default empty.
* @param bool $single Optional. Whether to return a single value. Default false.
* @return mixed Will be an array if $single is false. Will be value of meta data
* field if $single is true.
*/
function get_site_meta( $site_id, $key = '', $single = false ) {
// Bail if site meta table is not installed.
if ( ! is_site_meta_supported() ) {
/* translators: %s: database table name */
_doing_it_wrong( __FUNCTION__, sprintf( __( 'The %s table is not installed. Please run the network database upgrade.' ), $GLOBALS['wpdb']->blogmeta ), '5.0.0' );
return false;
}
return get_metadata( 'blog', $site_id, $key, $single );
}
/**
* Updates metadata for a site.
*
* Use the $prev_value parameter to differentiate between meta fields with the
* same key and site ID.
*
* If the meta field for the site does not exist, it will be added.
*
* @since 5.0.0
*
* @param int $site_id Site ID.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
* @param mixed $prev_value Optional. Previous value to check before removing.
* Default empty.
* @return int|bool Meta ID if the key didn't exist, true on successful update,
* false on failure.
*/
function update_site_meta( $site_id, $meta_key, $meta_value, $prev_value = '' ) {
// Bail if site meta table is not installed.
if ( ! is_site_meta_supported() ) {
/* translators: %s: database table name */
_doing_it_wrong( __FUNCTION__, sprintf( __( 'The %s table is not installed. Please run the network database upgrade.' ), $GLOBALS['wpdb']->blogmeta ), '5.0.0' );
return false;
}
$updated = update_metadata( 'blog', $site_id, $meta_key, $meta_value, $prev_value );
// Bust site query cache.
if ( $updated ) {
wp_cache_set( 'last_changed', microtime(), 'sites' );
}
return $updated;
}
/**
* Deletes everything from site meta matching meta key.
*
* @since 5.0.0
*
* @param string $meta_key Metadata key to search for when deleting.
* @return bool Whether the site meta key was deleted from the database.
*/
function delete_site_meta_by_key( $meta_key ) {
// Bail if site meta table is not installed.
if ( ! is_site_meta_supported() ) {
/* translators: %s: database table name */
_doing_it_wrong( __FUNCTION__, sprintf( __( 'The %s table is not installed. Please run the network database upgrade.' ), $GLOBALS['wpdb']->blogmeta ), '5.0.0' );
return false;
}
$deleted = delete_metadata( 'blog', null, $meta_key, '', true );
// Bust site query cache.
if ( $deleted ) {
wp_cache_set( 'last_changed', microtime(), 'sites' );
}
return $deleted;
}
/** /**
* Switch the current blog. * Switch the current blog.
* *
@ -869,7 +1046,7 @@ function switch_to_blog( $new_blog, $deprecated = null ) {
if ( is_array( $global_groups ) ) { if ( is_array( $global_groups ) ) {
wp_cache_add_global_groups( $global_groups ); wp_cache_add_global_groups( $global_groups );
} else { } else {
wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details' ) ); wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details', 'blog_meta' ) );
} }
wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) ); wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) );
} }
@ -937,7 +1114,7 @@ function restore_current_blog() {
if ( is_array( $global_groups ) ) { if ( is_array( $global_groups ) ) {
wp_cache_add_global_groups( $global_groups ); wp_cache_add_global_groups( $global_groups );
} else { } else {
wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details' ) ); wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'useremail', 'userslugs', 'site-transient', 'site-options', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache', 'networks', 'sites', 'site-details', 'blog_meta' ) );
} }
wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) ); wp_cache_add_non_persistent_groups( array( 'counts', 'plugins' ) );
} }

View File

@ -4,14 +4,14 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.0-alpha-42835'; $wp_version = '5.0-alpha-42836';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
* *
* @global int $wp_db_version * @global int $wp_db_version
*/ */
$wp_db_version = 38590; $wp_db_version = 42836;
/** /**
* Holds the TinyMCE version * Holds the TinyMCE version

View File

@ -297,6 +297,7 @@ class wpdb {
*/ */
var $ms_global_tables = array( var $ms_global_tables = array(
'blogs', 'blogs',
'blogmeta',
'signups', 'signups',
'site', 'site',
'sitemeta', 'sitemeta',
@ -413,6 +414,14 @@ class wpdb {
*/ */
public $blogs; public $blogs;
/**
* Multisite Blog Metadata table
*
* @since 5.0.0
* @var string
*/
public $blogmeta;
/** /**
* Multisite Blog Versions table * Multisite Blog Versions table
* *